@@ -3,47 +3,92 @@ import path from 'node:path';
33import process from 'node:process' ;
44
55/**
6- * Pre-Flight (structural fast-path): authoring `ai/scripts/diagnostics/check-retired-primitives.mjs`
7- * matches sibling pattern of `ai/scripts/diagnostics/check-substrate-size.mjs` and
8- * `ai/scripts/lint/lint-skill-manifest.mjs` in `ai/scripts/`; all three are mechanical-enforcement /
9- * CI scripts for agent substrate validation; sibling-file-lift applies; no novel directory
10- * choice.
6+ * Pre-Flight (structural fast-path): `ai/scripts/diagnostics/check-retired-primitives.mjs`
7+ * matches the sibling pattern of `ai/scripts/diagnostics/check-substrate-size.mjs` and
8+ * `ai/scripts/lint/lint-skill-manifest.mjs` in `ai/scripts/`; all are mechanical-enforcement /
9+ * CI scripts for agent substrate validation; sibling-file-lift applies; no novel directory choice.
1110 *
12- * @summary CI grep-fail check that retired primitives (ADR 0004 §2.3) are not
13- * imported from non-spec source files. Complements ADR 0004's discipline-only
14- * §1.3/§2.6/§5.6 substrate-evolution-guard layer with mechanical enforcement,
15- * closing the reviewer-time lookback window where dead-code preservation can
16- * otherwise survive until peer review.
11+ * @summary CI grep-fail check that retired Agent-OS primitives are not re-introduced under `ai/`.
12+ *
13+ * Three enforcement categories:
14+ * 1. **Retired module imports** — a retired primitive imported from a non-spec source file
15+ * (ADR 0004 §2.3 / §2.6 Clean-Cut Pattern).
16+ * 2. **Retired per-MCP-server config flags** — a removed boot-time auto-* flag re-declared as a
17+ * `leaf(...)` in a server `config.template.mjs`.
18+ * 3. **Retired MCP tools** — a removed tool's `operationId` re-added to a server `openapi.yaml`
19+ * (the tool-shape SSOT).
20+ *
21+ * Why categories 2 + 3 exist: the orchestrator daemon is the single source of truth for background
22+ * memory/KB maintenance. The Agent OS evolves fast, so a backlog ticket older than a week may carry
23+ * a premise a later migration has obsoleted. A fresh session acting on such a ticket can naively
24+ * "re-implement the missing config" and resurrect a per-MCP-instance self-trigger — the exact pattern
25+ * that caused duplicate background work across harness-spawned instances. A comment cannot stop that;
26+ * a red merge-gate can. This guard converts that regression class into an unmergeable failure with a
27+ * premise-correcting message that names the owning subsystem.
1728 *
1829 * @see learn/agentos/decisions/0004-github-content-architecture.md §2.6 Clean-Cut Pattern
1930 */
2031
32+ const SEARCH_ROOT = 'ai/' ;
33+
2134/**
22- * Retired primitives that MUST NOT be imported from non-spec source files.
23- * Add new entries as ADR 0004 §2.3 RETIRED table grows. Each entry is the import-path
24- * fragment as it would appear inside a `from '...'` clause.
25- *
26- * Anchored to the universal GitHub-content substrate: both files are retired
27- * once their call sites use `contentPath.mjs` / `contentIndex.mjs`. The §2.6
28- * Clean-Cut Pattern mandates deletion, not preservation as dead code.
35+ * Orchestrator-SSOT migration references, surfaced in the category 2 + 3 failure messages so a
36+ * developer who hits the guard can find why the primitive was retired and who owns the behavior now.
37+ * Load-bearing (it is the actionable pointer), not decorative archaeology.
38+ */
39+ const ORCHESTRATOR_REF = '#12139 / #12065' ;
40+
41+ /**
42+ * Retired module-import fragments that MUST NOT be imported from non-spec source files.
43+ * Each entry is the import-path fragment as it appears inside a `from '...'` clause.
44+ * Add entries as the ADR 0004 §2.3 RETIRED table grows. The §2.6 Clean-Cut Pattern mandates
45+ * deletion, not preservation as dead code.
2946 */
3047const RETIRED_PRIMITIVES = [
3148 'shared/chunkPath.mjs' ,
3249 'shared/archivePath.mjs'
3350] ;
3451
35- const SEARCH_ROOT = 'ai/' ;
36- const EXCLUDE_GLOB = [ '*.spec.mjs' , '*.test.mjs' ] ;
37-
3852/**
39- * Escapes ALL regex metacharacters in a string fragment so it can be safely embedded
40- * in an extended-regex pattern. This keeps future retired-primitive entries
41- * from becoming regex metacharacter input.
53+ * Retired per-MCP-server boot-time config flags. Removed in #12139; the orchestrator daemon (#12065)
54+ * drives every one of these behaviors on its own schedule (dream / summary / golden-path / chroma /
55+ * kbSync tasks). Re-declaring any as a `leaf(...)` in a server `config.template.mjs` would resurrect
56+ * the per-instance self-trigger that caused duplicate background work across harness-spawned MCP
57+ * instances (Claude Desktop / Codex / Antigravity each spawn N instances per server).
4258 *
43- * Prior version escaped only `.`, leaving backslashes + other metacharacters (`* + ? ^ $ { } ( ) | [ ] \\`)
44- * unprotected. While the current `RETIRED_PRIMITIVES` values contain none of these, defensive
45- * full-metachar escaping makes the function safe for any future fragment a maintainer adds —
46- * matching MDN's canonical `escapeRegExp` shape.
59+ * `autoStartInference` is the canonical trap: its config was string-matched but the auto-start LOGIC
60+ * was never implemented. A stale ticket reading "implement inference auto-start" would lead a fresh
61+ * session to re-add the flag, not knowing the orchestrator owns inference lifecycle (mlx/lms
62+ * continuous tasks). De-duplicated across servers (`autoStartDatabase` was declared in both
63+ * memory-core and knowledge-base).
64+ */
65+ const RETIRED_CONFIG_FLAGS = [
66+ 'autoSummarize' ,
67+ 'autoStartDatabase' ,
68+ 'autoStartInference' ,
69+ 'autoDream' ,
70+ 'autoGoldenPath' ,
71+ 'realTimeMemoryParsing' ,
72+ 'autoIngestFileSystem' ,
73+ 'autoSync'
74+ ] ;
75+
76+ /**
77+ * Retired MCP tools. Removed in #12139; the orchestrator daemon owns the Chroma database lifecycle
78+ * (`manage_database`) and session summarization (`summarize_sessions`) — the MCP servers are pure
79+ * clients. Re-adding an `operationId` for either to a server `openapi.yaml` re-advertises a tool the
80+ * orchestrator owns (and breaks the derived tool-consistency specs, which assert that every
81+ * `operationId` has a `serviceMapping` handler).
82+ */
83+ const RETIRED_MCP_TOOLS = [
84+ 'manage_database' ,
85+ 'summarize_sessions'
86+ ] ;
87+
88+ /**
89+ * Escapes ALL regex metacharacters in a string fragment so it can be safely embedded in an
90+ * extended-regex pattern, keeping retired-item entries from becoming regex metacharacter input.
91+ * Matches MDN's canonical `escapeRegExp` shape.
4792 *
4893 * @param {string } s Raw fragment string to escape.
4994 * @returns {string } Regex-safe escaped form.
@@ -53,32 +98,80 @@ function escapeRegex(s) {
5398}
5499
55100/**
56- * Builds a single grep extended-regex pattern matching any `from '...<retired>'` style import,
57- * tolerating single, double, or template-literal quoting. All regex metacharacters in path
58- * fragments are escaped via `escapeRegex()` so values containing `*`, `[`, `\\`, etc. are
59- * treated as literals.
60- * @returns {string }
101+ * Builds the three enforcement categories. Each declares: the retired `items`, the grep extended-regex
102+ * `pattern` that detects a re-introduction, the file `include` globs to scan (empty = all files), the
103+ * `exclude` globs, and a premise-correcting `help` footer printed on a hit. A category with zero items
104+ * is skipped by {@link main}.
105+ *
106+ * - **import** — `from '...<fragment>'` across all of `ai/` (excluding specs), tolerating single,
107+ * double, or template-literal quoting.
108+ * - **config flag** — `<flag>:` as an object key (anchored to line-start indentation, so JSDoc/comment
109+ * mentions like ` * autoSync:` or `// autoSync` do not match), scanned only in `config.template.mjs`.
110+ * - **MCP tool** — `operationId: <tool>` at line end (so `manage_database_v2` does not false-match),
111+ * scanned only in `openapi.yaml`.
112+ *
113+ * @returns {Array<{label:string, items:string[], pattern:string, include:string[], exclude:string[], help:string[]}> }
61114 */
62- function buildPattern ( ) {
63- return RETIRED_PRIMITIVES
64- . map ( fragment => `from[[:space:]]+['"\`].*${ escapeRegex ( fragment ) } ` )
65- . join ( '|' ) ;
115+ function buildCategories ( ) {
116+ return [
117+ {
118+ label : 'retired-primitive import' ,
119+ items : RETIRED_PRIMITIVES ,
120+ pattern : RETIRED_PRIMITIVES . map ( f => `from[[:space:]]+['"\`].*${ escapeRegex ( f ) } ` ) . join ( '|' ) ,
121+ include : [ ] ,
122+ exclude : [ '*.spec.mjs' , '*.test.mjs' ] ,
123+ help : [
124+ 'Refer to ADR 0004 §2.6 (Clean-Cut Pattern) and §5.6 (Deprecation-theater anti-pattern):' ,
125+ ' learn/agentos/decisions/0004-github-content-architecture.md' ,
126+ 'Retired primitives must be DELETED, not preserved as dead code after call-site migration.'
127+ ]
128+ } ,
129+ {
130+ label : 'retired config flag' ,
131+ items : RETIRED_CONFIG_FLAGS ,
132+ pattern : RETIRED_CONFIG_FLAGS . map ( f => `^[[:space:]]*${ escapeRegex ( f ) } [[:space:]]*:` ) . join ( '|' ) ,
133+ include : [ 'config.template.mjs' ] ,
134+ exclude : [ ] ,
135+ help : [
136+ `These boot-time auto-* flags are retired (${ ORCHESTRATOR_REF } ). The orchestrator daemon is the` ,
137+ 'single source of truth for dream / summary / golden-path / chroma / kbSync — per-MCP-server' ,
138+ 'self-triggers caused duplicate background work across harness-spawned instances.' ,
139+ 'Do NOT re-add the flag. If a backlog ticket told you to "implement" one of these, that ticket\'s' ,
140+ 'premise is obsolete — close/annotate it rather than resurrect the config. (e.g. autoStartInference' ,
141+ 'never had real logic; the orchestrator owns inference lifecycle via mlx/lms continuous tasks.)'
142+ ]
143+ } ,
144+ {
145+ label : 'retired MCP tool' ,
146+ items : RETIRED_MCP_TOOLS ,
147+ pattern : RETIRED_MCP_TOOLS . map ( t => `operationId:[[:space:]]*${ escapeRegex ( t ) } [[:space:]]*$` ) . join ( '|' ) ,
148+ include : [ 'openapi.yaml' ] ,
149+ exclude : [ ] ,
150+ help : [
151+ `These MCP tools are retired (${ ORCHESTRATOR_REF } ). The orchestrator owns the Chroma database` ,
152+ 'lifecycle (manage_database) and session summarization (summarize_sessions); the MCP servers are' ,
153+ 'pure clients. Do NOT re-add the operationId — the orchestrator drives these behaviors.'
154+ ]
155+ }
156+ ] ;
66157}
67158
68159/**
69- * Runs the grep scan. Returns matching lines on hit, empty string on clean.
70- * Distinguishes "no match" (exit 1) from real grep error (exit 2+).
71- *
72- * Uses execFileSync (no shell intermediate) so the pattern can contain all three quote types
73- * (`'`, `"`, `\``) safely without shell-quoting hazards.
160+ * Runs one grep scan. Returns matching lines on hit, empty string on clean. Distinguishes "no match"
161+ * (grep exit 1, the CLEAN case) from a real grep error (exit 2+, re-thrown). Uses execFileSync (no
162+ * shell intermediate) so the pattern can contain all three quote types (`'`, `"`, `` ` ``) safely.
74163 *
75164 * @param {string } pattern Extended-regex pattern.
165+ * @param {Object } [opts]
166+ * @param {string[] } [opts.include=[]] grep `--include` globs (empty = all files).
167+ * @param {string[] } [opts.exclude=[]] grep `--exclude` globs.
76168 * @returns {string }
77169 */
78- function runScan ( pattern ) {
170+ function runScan ( pattern , { include = [ ] , exclude = [ ] } = { } ) {
79171 const args = [
80172 '-rnE' ,
81- ...EXCLUDE_GLOB . map ( g => `--exclude=${ g } ` ) ,
173+ ...exclude . map ( g => `--exclude=${ g } ` ) ,
174+ ...include . map ( g => `--include=${ g } ` ) ,
82175 pattern ,
83176 SEARCH_ROOT
84177 ] ;
@@ -96,33 +189,45 @@ function runScan(pattern) {
96189}
97190
98191function main ( ) {
99- if ( RETIRED_PRIMITIVES . length === 0 ) {
100- console . log ( '[checkRetiredPrimitives] PASS: RETIRED_PRIMITIVES table is empty — nothing to enforce.' ) ;
192+ const categories = buildCategories ( ) . filter ( category => category . items . length > 0 ) ;
193+
194+ if ( categories . length === 0 ) {
195+ console . log ( '[checkRetiredPrimitives] PASS: no retired items configured — nothing to enforce.' ) ;
101196 process . exit ( 0 ) ;
102197 }
103198
104- const root = path . resolve ( process . cwd ( ) ) ;
105- const pattern = buildPattern ( ) ;
106-
107- console . log ( `\n🔍 Checking for retired-primitive imports under ${ SEARCH_ROOT } ...` ) ;
108- console . log ( ` Enforcing ${ RETIRED_PRIMITIVES . length } retired primitive(s):` ) ;
109- RETIRED_PRIMITIVES . forEach ( p => console . log ( ` • ${ p } ` ) ) ;
199+ console . log ( `\n🔍 Checking for retired Agent-OS primitives under ${ SEARCH_ROOT } ...` ) ;
200+ categories . forEach ( category => {
201+ console . log ( ` ${ category . label } (${ category . items . length } ): ${ category . items . join ( ', ' ) } ` ) ;
202+ } ) ;
110203 console . log ( '-' . repeat ( 80 ) ) ;
111204
112- const matches = runScan ( pattern ) ;
205+ const failures = [ ] ;
206+
207+ for ( const category of categories ) {
208+ const matches = runScan ( category . pattern , { include : category . include , exclude : category . exclude } ) ;
209+ if ( matches . trim ( ) !== '' ) {
210+ failures . push ( { category, matches} ) ;
211+ }
212+ }
113213
114- if ( matches . trim ( ) === '' ) {
115- console . log ( `✅ PASS: no retired-primitive imports found under ${ SEARCH_ROOT } .` ) ;
116- console . log ( ` Substrate is in compliance with ADR 0004 §2.6 Clean-Cut Pattern.\n` ) ;
214+ if ( failures . length === 0 ) {
215+ console . log ( `✅ PASS: no retired imports, config flags, or MCP tools found under ${ SEARCH_ROOT } .` ) ;
216+ console . log ( ` Substrate is in compliance ( ADR 0004 §2.6 Clean-Cut Pattern + orchestrator-SSOT ${ ORCHESTRATOR_REF } ) .\n` ) ;
117217 process . exit ( 0 ) ;
118218 }
119219
120- console . error ( `❌ FAIL: retired-primitive imports found under ${ SEARCH_ROOT } :\n` ) ;
121- console . error ( matches ) ;
122- console . error ( `\nRoot: ${ root } ` ) ;
123- console . error ( `Refer to ADR 0004 §2.6 (Clean-Cut Pattern) and §5.6 (Deprecation-theater anti-pattern):` ) ;
124- console . error ( ` learn/agentos/decisions/0004-github-content-architecture.md` ) ;
125- console . error ( `Retired primitives must be DELETED, not preserved as dead code after call-site migration.\n` ) ;
220+ console . error ( `❌ FAIL: retired Agent-OS primitives re-introduced under ${ SEARCH_ROOT } :\n` ) ;
221+
222+ for ( const { category, matches} of failures ) {
223+ console . error ( ` ▸ ${ category . label } :` ) ;
224+ console . error ( matches . replace ( / ^ / gm, ' ' ) . trimEnd ( ) ) ;
225+ console . error ( '' ) ;
226+ category . help . forEach ( line => console . error ( ` ${ line } ` ) ) ;
227+ console . error ( '' ) ;
228+ }
229+
230+ console . error ( `Root: ${ path . resolve ( process . cwd ( ) ) } \n` ) ;
126231 process . exit ( 1 ) ;
127232}
128233
0 commit comments