Context
ADR 0004 (learn/agentos/decisions/0004-github-content-architecture.md) gained a three-section substrate-evolution-guard layer tonight (2026-05-15):
- §1.3 — The Regeneratable-Cache Strategic Principle (via PR #11401)
- §2.6 — The Clean-Cut Pattern (via PR #11398)
- §5.6 — Deprecation-theater anti-pattern (via PR #11398)
The layer was authored explicitly to prevent the @deprecated theater regression operator caught on PR #11381 — preservation-tendency on retired primitives like chunkPath.mjs / archivePath.mjs after their call sites get rewired. The substrate-evolution-guard is discipline-only (pattern-recognition aid for agents reading merged ADR substrate before authoring).
Empirical recurrence proves it's necessary but not sufficient: at the very next PR after the §1.3/§2.6/§5.6 layer merged (PR #11403, Cycle-1 review at PRR_kwDODSospM8AAAABAB3NsQ), the same clean-cut regression recurred — chunkPath.mjs + archivePath.mjs + ArchivePath.spec.mjs were preserved as dead code despite zero callers after PR #11403's import-removals. The regression was caught at peer-review time (Cycle-1 CHANGES_REQUESTED), not at CI time. The empirical anchor — two same-pattern occurrences across two different families within ~8 hours, with the second occurring after the discipline-only guard layer landed — justifies a mechanical-enforcement layer.
The Problem
The §1.3/§2.6/§5.6 guards work as recognition aids when an agent CITES them at AC-authoring or review time. They do NOT block via automated CI check. This means:
- Agents authoring substrate-rewire PRs must pull-and-read merged ADR substrate before authoring — a discipline-layer responsibility that empirically broke twice in one session.
- Reviewers carry the load of catching the regression. The lookback distance between author-time and reviewer-time is the regression window.
- Future ADR §2.3 RETIRED additions will face the same risk profile: retired-primitive imports can linger in source even after call-site rewires, with no automated tripwire.
Pattern-recognition + mechanical-enforcement closes the gap. Pattern-recognition is necessary (without it, mechanical enforcement is brittle/ad-hoc); mechanical enforcement is necessary (without it, pattern-recognition relies on humans/agents staying maximally attentive across all PRs).
The Architectural Reality
learn/agentos/decisions/0004-github-content-architecture.md defines the canonical authority for retired primitives. §2.3 RETIRED naming + §2.6 Clean-Cut Pattern + §1.3 Regeneratable-Cache Strategic Principle together establish the discipline. Future retirements add to this surface.
ai/services/github-workflow/shared/ is the current locus of retired primitives (chunkPath.mjs, archivePath.mjs — both deleted in PR #11403 commit 79ac1f8c9).
.github/workflows/ carries existing CI surface — relevant precedent: skill-manifest-lint.yml (substrate-discipline check on .agents/skills/** shape).
buildScripts/util/ is the canonical home for small CI-invoked utility scripts. Convention: .mjs ES modules, runnable via node buildScripts/util/<name>.mjs, wired into CI via a new step in an existing workflow.
The mechanical-enforcement layer fits as: a new buildScripts/util/checkRetiredPrimitives.mjs script that scans for retired-primitive imports under ai/ (excluding spec files), exits non-zero if any are found; wired into the existing Tests workflow (.github/workflows/tests.yml) as a new pre-test job step OR into skill-manifest-lint.yml as a sibling step.
The Fix
Add a small CI script + workflow wiring:
1. New script: buildScripts/util/checkRetiredPrimitives.mjs
import {execSync} from 'node:child_process';
import process from 'node:process';
const RETIRED_PRIMITIVES = [
'shared/chunkPath.mjs',
'shared/archivePath.mjs'
];
const SEARCH_ROOT = 'ai/';
const EXCLUDE_GLOB = ['*.spec.mjs', '*.test.mjs'];
function buildPattern() {
return RETIRED_PRIMITIVES.map(p => `from.*['"\`].*${p.replace(/\./g, '\\.')}`).join('\\|');
}
function main() {
const pattern = buildPattern();
const excludes = EXCLUDE_GLOB.map(g => `--exclude="${g}"`).join(' ');
let stdout;
try {
stdout = execSync(`grep -rn ${excludes} "${pattern}" ${SEARCH_ROOT}`, {encoding: 'utf8'});
} catch (err) {
if (err.status === 1) {
console.log(`[checkRetiredPrimitives] PASS: no retired-primitive imports under ${SEARCH_ROOT}`);
return;
}
throw err;
}
console.error(`[checkRetiredPrimitives] FAIL: found retired-primitive imports under ${SEARCH_ROOT}:`);
console.error(stdout);
console.error('\nRefer to ADR 0004 §2.6 Clean-Cut Pattern — retired primitives must be DELETED, not preserved as dead code after call-site migration.');
process.exit(1);
}
main();
2. CI wiring
Add a new step to .github/workflows/tests.yml (or skill-manifest-lint.yml if grouping with substrate-discipline checks). Example:
- name: Check retired primitives
run: node buildScripts/util/checkRetiredPrimitives.mjs
Wire as a fast pre-test gate (sub-second runtime — pure grep scan).
3. Optional: npm script alias
Add to package.json:
"check-retired-primitives": "node buildScripts/util/checkRetiredPrimitives.mjs"
So local dev workflow can run npm run check-retired-primitives before pushing.
Acceptance Criteria
Out of Scope
- Generalizing to all ADR-§2.3-RETIRED tables across all ADRs: keep this scoped to ADR 0004's retired primitives initially. Future ADRs gaining §2.3 RETIRED tables can extend
RETIRED_PRIMITIVES array additively. A fully-table-driven check (parse ADR markdown → extract RETIRED entries) is a future substrate ticket if this primitive proves successful.
- Auto-deletion of retired primitive files: discipline-only — the check FAILS on import-presence, doesn't auto-remove files. Author still owns the deletion in the call-site-migration PR.
- Linting other substrate-discipline rules (@deprecated annotations, etc.): scope of separate substrate tickets per pattern. This ticket is narrowly retired-primitive-imports.
- CI for archived/historical retired primitives that have been fully purged: once all current retired primitives are deleted from
dev, the script is still useful as a forward-defense against future regressions.
Avoided Traps
- Hardcoding a single-occurrence check (only
chunkPath.mjs): rejected — the array shape (RETIRED_PRIMITIVES) lets future ADR §2.3 additions extend without rewriting the script. Empirically: even at this filing time, two retired primitives are in scope.
- Building a complex AST-based detector: rejected — grep on
from.*['"].*` is sufficient. Imports in modern JS/MJS are statically declarable; AST parsing is over-engineering for the actual pattern.
- Wiring as a hard pre-commit hook: rejected — pre-commit hooks bypass CI authority + complicate local dev. CI-step placement matches existing substrate-discipline-check pattern (
skill-manifest-lint.yml precedent).
- Coupling to ADR markdown parsing: rejected for this ticket — adds substrate-coupling between the check script and ADR substrate format. Future ticket may pursue this if the array maintenance becomes painful at >5-10 entries. For now, explicit array is simpler and equally correct.
Related
- Authority artifact: ADR 0004
learn/agentos/decisions/0004-github-content-architecture.md — §1.3 + §2.3 + §2.6 + §5.6 substrate-evolution-guard layer
- Empirical anchor for filing:
- PR #11381 (Lane A foundation): operator caught @deprecated theater regression on merged code — friction → gold seed
- PR #11397 → PR #11398 (Gemini, §2.6 + §5.6): pattern + anti-pattern layer
- PR #11400 → PR #11401 (me, §1.3): strategic principle layer
- PR #11403 (Lane B): Cycle-1 same-pattern recurrence (PRR_kwDODSospM8AAAABAB3NsQ) — empirical V-B-A that discipline-only guards are necessary-but-not-sufficient
- PR #11403 Cycle-2 (PRR_kwDODSospM8AAAABAB-g-A): clean-cut closed; this ticket queued as follow-up
- Precedent CI shape:
.github/workflows/skill-manifest-lint.yml — substrate-discipline check for .agents/skills/** shape; lift its workflow-step pattern
- Epic ancestor: Epic #11372 (ADR 0004 Phase 1) — this is substrate-meta improvement; not a direct sub-task
Origin Session
- Origin Session ID:
656c0935-0b3e-4b06-9b14-548524275859
- Cross-session anchor: ADR 0004 §1.3 authoring session
b8a152e1-a41c-49aa-8196-8e5d2eba84ca (this session's earlier-arc continuation)
Retrieval Hint
Search for CI grep-fail retired primitives clean-cut pattern ADR 0004 mechanical enforcement substrate-evolution-guard.
Or commit-range anchor: PR #11403 Cycle-1 review at PRR_kwDODSospM8AAAABAB3NsQ is the empirical recurrence; PR #11403 Cycle-2 at PRR_kwDODSospM8AAAABAB-g-A is the queued-follow-up commitment.
Context
ADR 0004 (
learn/agentos/decisions/0004-github-content-architecture.md) gained a three-section substrate-evolution-guard layer tonight (2026-05-15):The layer was authored explicitly to prevent the @deprecated theater regression operator caught on PR #11381 — preservation-tendency on retired primitives like
chunkPath.mjs/archivePath.mjsafter their call sites get rewired. The substrate-evolution-guard is discipline-only (pattern-recognition aid for agents reading merged ADR substrate before authoring).Empirical recurrence proves it's necessary but not sufficient: at the very next PR after the §1.3/§2.6/§5.6 layer merged (PR #11403, Cycle-1 review at
PRR_kwDODSospM8AAAABAB3NsQ), the same clean-cut regression recurred —chunkPath.mjs+archivePath.mjs+ArchivePath.spec.mjswere preserved as dead code despite zero callers after PR #11403's import-removals. The regression was caught at peer-review time (Cycle-1 CHANGES_REQUESTED), not at CI time. The empirical anchor — two same-pattern occurrences across two different families within ~8 hours, with the second occurring after the discipline-only guard layer landed — justifies a mechanical-enforcement layer.The Problem
The §1.3/§2.6/§5.6 guards work as recognition aids when an agent CITES them at AC-authoring or review time. They do NOT block via automated CI check. This means:
Pattern-recognition + mechanical-enforcement closes the gap. Pattern-recognition is necessary (without it, mechanical enforcement is brittle/ad-hoc); mechanical enforcement is necessary (without it, pattern-recognition relies on humans/agents staying maximally attentive across all PRs).
The Architectural Reality
learn/agentos/decisions/0004-github-content-architecture.mddefines the canonical authority for retired primitives. §2.3 RETIRED naming + §2.6 Clean-Cut Pattern + §1.3 Regeneratable-Cache Strategic Principle together establish the discipline. Future retirements add to this surface.ai/services/github-workflow/shared/is the current locus of retired primitives (chunkPath.mjs,archivePath.mjs— both deleted in PR #11403 commit79ac1f8c9)..github/workflows/carries existing CI surface — relevant precedent:skill-manifest-lint.yml(substrate-discipline check on.agents/skills/**shape).buildScripts/util/is the canonical home for small CI-invoked utility scripts. Convention:.mjsES modules, runnable vianode buildScripts/util/<name>.mjs, wired into CI via a new step in an existing workflow.The mechanical-enforcement layer fits as: a new
buildScripts/util/checkRetiredPrimitives.mjsscript that scans for retired-primitive imports underai/(excluding spec files), exits non-zero if any are found; wired into the existingTestsworkflow (.github/workflows/tests.yml) as a new pre-test job step OR intoskill-manifest-lint.ymlas a sibling step.The Fix
Add a small CI script + workflow wiring:
1. New script:
buildScripts/util/checkRetiredPrimitives.mjs// buildScripts/util/checkRetiredPrimitives.mjs import {execSync} from 'node:child_process'; import process from 'node:process'; /** * Retired primitives that MUST NOT be imported by non-spec source. * Add new entries as ADR 0004 §2.3 RETIRED table grows. * * Format: array of regex-safe import-path fragments that appear inside `from '...'` strings. */ const RETIRED_PRIMITIVES = [ 'shared/chunkPath.mjs', 'shared/archivePath.mjs' ]; const SEARCH_ROOT = 'ai/'; const EXCLUDE_GLOB = ['*.spec.mjs', '*.test.mjs']; function buildPattern() { return RETIRED_PRIMITIVES.map(p => `from.*['"\`].*${p.replace(/\./g, '\\.')}`).join('\\|'); } function main() { const pattern = buildPattern(); const excludes = EXCLUDE_GLOB.map(g => `--exclude="${g}"`).join(' '); let stdout; try { stdout = execSync(`grep -rn ${excludes} "${pattern}" ${SEARCH_ROOT}`, {encoding: 'utf8'}); } catch (err) { // grep returns exit 1 on zero matches — the CLEAN case. if (err.status === 1) { console.log(`[checkRetiredPrimitives] PASS: no retired-primitive imports under ${SEARCH_ROOT}`); return; } throw err; } console.error(`[checkRetiredPrimitives] FAIL: found retired-primitive imports under ${SEARCH_ROOT}:`); console.error(stdout); console.error('\nRefer to ADR 0004 §2.6 Clean-Cut Pattern — retired primitives must be DELETED, not preserved as dead code after call-site migration.'); process.exit(1); } main();2. CI wiring
Add a new step to
.github/workflows/tests.yml(orskill-manifest-lint.ymlif grouping with substrate-discipline checks). Example:- name: Check retired primitives run: node buildScripts/util/checkRetiredPrimitives.mjsWire as a fast pre-test gate (sub-second runtime — pure grep scan).
3. Optional: npm script alias
Add to
package.json:"check-retired-primitives": "node buildScripts/util/checkRetiredPrimitives.mjs"So local dev workflow can run
npm run check-retired-primitivesbefore pushing.Acceptance Criteria
buildScripts/util/checkRetiredPrimitives.mjsexists with the prescribed shape —RETIRED_PRIMITIVESconstant array, grep-scan logic, exit-1-on-match behavior.tests.ymlorskill-manifest-lint.yml) as a new step. Workflow runs on every PR todev.devhead (post-PR-#11403-merge) exits 0 — substrate is in compliance after Lane B clean-cut.chunkPath.mjs(e.g., temp branch withimport './shared/chunkPath.mjs'in a non-spec file) makes the script exit non-zero, with output naming the offending file:line + the ADR 0004 §2.6 reference.npm run check-retired-primitivesalias added topackage.jsonfor local dev use.Out of Scope
RETIRED_PRIMITIVESarray additively. A fully-table-driven check (parse ADR markdown → extract RETIRED entries) is a future substrate ticket if this primitive proves successful.dev, the script is still useful as a forward-defense against future regressions.Avoided Traps
chunkPath.mjs): rejected — the array shape (RETIRED_PRIMITIVES) lets future ADR §2.3 additions extend without rewriting the script. Empirically: even at this filing time, two retired primitives are in scope.from.*['"].*skill-manifest-lint.ymlprecedent).Related
learn/agentos/decisions/0004-github-content-architecture.md— §1.3 + §2.3 + §2.6 + §5.6 substrate-evolution-guard layer.github/workflows/skill-manifest-lint.yml— substrate-discipline check for.agents/skills/**shape; lift its workflow-step patternOrigin Session
656c0935-0b3e-4b06-9b14-548524275859b8a152e1-a41c-49aa-8196-8e5d2eba84ca(this session's earlier-arc continuation)Retrieval Hint
Search for
CI grep-fail retired primitives clean-cut pattern ADR 0004 mechanical enforcement substrate-evolution-guard.Or commit-range anchor: PR #11403 Cycle-1 review at
PRR_kwDODSospM8AAAABAB3NsQis the empirical recurrence; PR #11403 Cycle-2 atPRR_kwDODSospM8AAAABAB-g-Ais the queued-follow-up commitment.