@@ -15,6 +15,7 @@ import { copilotBridge } from '../bridges/copilot.js';
1515import { mergeMarkedContent , removeMarkedBlock } from '../core/markers.js' ;
1616import { cleanStaleFiles } from '../core/scope-filename.js' ;
1717import { detectLegacyFiles , migrateLegacyFiles } from '../core/cleanup.js' ;
18+ import { buildCanonicalOutputs , writeCanonical } from '../core/canonical.js' ;
1819import { fileExists } from '../utils/fs.js' ;
1920import * as ui from '../utils/ui.js' ;
2021import { ICONS } from '../utils/ui.js' ;
@@ -45,6 +46,8 @@ export interface MigrationResult {
4546export 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
251292export 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