Skip to content

Commit 5dc770d

Browse files
committed
feat(cli): complete phase 2 canonical distribution and hardening
1 parent c619f88 commit 5dc770d

9 files changed

Lines changed: 632 additions & 18 deletions

File tree

packages/cli/src/commands/compile.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { copilotBridge } from '../bridges/copilot.js';
1515
import { mergeMarkedContent, removeMarkedBlock } from '../core/markers.js';
1616
import { cleanStaleFiles } from '../core/scope-filename.js';
1717
import { detectLegacyFiles, migrateLegacyFiles } from '../core/cleanup.js';
18+
import { buildCanonicalOutputs, writeCanonical } from '../core/canonical.js';
1819
import { fileExists } from '../utils/fs.js';
1920
import * as ui from '../utils/ui.js';
2021
import { ICONS } from '../utils/ui.js';
@@ -45,6 +46,8 @@ export interface MigrationResult {
4546
export interface CompileResult {
4647
results: BridgeResult[];
4748
activeRuleCount: number;
49+
canonicalFileCount: number;
50+
canonicalError?: string;
4851
assetPaths: string[];
4952
elapsedMs: number;
5053
staleResults: StaleFileResult[];
@@ -235,6 +238,35 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
235238
}
236239
}
237240

241+
// Canonical output intentionally always runs, even when --tool filters bridges.
242+
// This keeps `.agents/rules/devw` as the source-of-truth for doctor checks and distribution.
243+
const canonicalOutputs = buildCanonicalOutputs(rules);
244+
let canonicalPaths: string[] = [];
245+
let canonicalError: string | undefined;
246+
if (write) {
247+
try {
248+
canonicalPaths = await writeCanonical(cwd, canonicalOutputs);
249+
for (const relativePath of canonicalPaths) {
250+
results.push({ bridgeId: 'canonical', outputPath: relativePath, success: true });
251+
}
252+
} catch (err) {
253+
canonicalError = err instanceof Error ? err.message : String(err);
254+
const errorPaths = [...canonicalOutputs.keys()];
255+
if (errorPaths.length > 0) {
256+
for (const relativePath of errorPaths) {
257+
results.push({ bridgeId: 'canonical', outputPath: relativePath, success: false, error: canonicalError });
258+
}
259+
} else {
260+
results.push({ bridgeId: 'canonical', outputPath: '.agents/rules/devw', success: false, error: canonicalError });
261+
}
262+
}
263+
} else {
264+
for (const [relativePath, content] of canonicalOutputs) {
265+
canonicalPaths.push(relativePath);
266+
results.push({ bridgeId: 'canonical', outputPath: relativePath, success: true, content });
267+
}
268+
}
269+
238270
let assetPaths: string[] = [];
239271
if (write) {
240272
const hash = computeRulesHash(activeRules);
@@ -245,7 +277,16 @@ export async function executePipeline(options: PipelineOptions): Promise<Compile
245277
}
246278

247279
const elapsedMs = performance.now() - startTime;
248-
return { results, activeRuleCount: activeRules.length, assetPaths, elapsedMs, staleResults, migration };
280+
return {
281+
results,
282+
activeRuleCount: activeRules.length,
283+
canonicalFileCount: canonicalPaths.length,
284+
canonicalError,
285+
assetPaths,
286+
elapsedMs,
287+
staleResults,
288+
migration,
289+
};
249290
}
250291

251292
export async function runCompile(options: CompileOptions): Promise<void> {
@@ -286,12 +327,23 @@ export async function runCompile(options: CompileOptions): Promise<void> {
286327
// Summary of what would be generated
287328
const fileCount = result.results.filter((r) => r.success).length;
288329
ui.newline();
289-
ui.info(`Would generate ${String(fileCount)} file${fileCount !== 1 ? 's' : ''} from ${String(result.activeRuleCount)} rules`);
330+
ui.info(
331+
`Would generate ${String(fileCount)} file${fileCount !== 1 ? 's' : ''} (${String(result.canonicalFileCount)} canonical) from ${String(result.activeRuleCount)} rules`,
332+
);
290333
return;
291334
}
292335

293336
const result = await executePipeline({ cwd, tool: options.tool });
294337

338+
if (options.tool) {
339+
ui.info('Note: canonical output is always refreshed in .agents/rules/devw');
340+
}
341+
342+
if (result.canonicalError) {
343+
ui.warn(`Canonical write failed: ${result.canonicalError}`);
344+
ui.warn('Tool-specific outputs were still written');
345+
}
346+
295347
// Show migration messages if any
296348
if (result.migration.actions.length > 0) {
297349
ui.newline();
@@ -306,6 +358,7 @@ export async function runCompile(options: CompileOptions): Promise<void> {
306358

307359
ui.newline();
308360
ui.success(`Compiled ${String(result.activeRuleCount)} rules ${ICONS.arrow} ${String(allPaths.length)} file${allPaths.length !== 1 ? 's' : ''} ${ui.timing(result.elapsedMs)}`);
361+
ui.info(`Canonical files: ${String(result.canonicalFileCount)}`);
309362
ui.newline();
310363

311364
if (options.verbose) {

0 commit comments

Comments
 (0)