@event4u/agent-config 14.22.0 → 14.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +159 -380
  5. package/README.md +1 -1
  6. package/dist/agent-src/commands/pr/merge.md +47 -38
  7. package/dist/agent-src/commands/roadmap/next.md +8 -0
  8. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
  9. package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
  10. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +106 -4
  11. package/dist/agent-src/skills/code-intelligence/SKILL.md +57 -1
  12. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
  13. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
  14. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  15. package/dist/cli/registry.js +1 -1
  16. package/dist/cli-delegate/archive_completed_roadmaps.js +52 -4
  17. package/dist/cli-delegate/{chunk-TGIPZBCW.js → chunk-2KKXJW25.js} +22 -8
  18. package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
  19. package/dist/cli-delegate/{chunk-BIXEDQPZ.js → chunk-KCNTUSPI.js} +1 -1
  20. package/dist/cli-delegate/cmd_export.js +2 -2
  21. package/dist/cli-delegate/cmd_handoff.js +20 -12
  22. package/dist/cli-delegate/cmd_prune.js +2 -2
  23. package/dist/cli-delegate/cmd_session_recycle.js +3 -13
  24. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  25. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  26. package/dist/cli-delegate/cmd_sync.js +2 -2
  27. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  28. package/dist/cli-delegate/cmd_validate.js +2 -2
  29. package/dist/discovery/deprecation-report.md +1 -1
  30. package/dist/discovery/discovery-manifest.json +8 -8
  31. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  32. package/dist/discovery/discovery-manifest.summary.md +1 -1
  33. package/dist/discovery/dormancy-report.md +1 -1
  34. package/dist/discovery/orphan-report.md +1 -1
  35. package/dist/discovery/packs.json +2 -2
  36. package/dist/discovery/trust-report.md +1 -1
  37. package/dist/discovery/workspaces.json +2 -2
  38. package/dist/hooks/dispatch.js +880 -289
  39. package/dist/install/install.mjs +15 -3
  40. package/dist/mcp/registry-manifest.json +2 -2
  41. package/dist/mcp/server.json +2 -2
  42. package/dist/mcp/server.mjs +5458 -2453
  43. package/dist/scripts/_lib/lean_projection_mode.js +108 -0
  44. package/dist/scripts/_lib/router_match.js +243 -0
  45. package/dist/scripts/_lib/rule_injection.js +216 -0
  46. package/dist/scripts/condense.js +20 -20
  47. package/dist/scripts/project_thin_rules.js +139 -19
  48. package/dist/server/schemas/settings.js +16 -3
  49. package/docs/MIGRATION.md +56 -0
  50. package/docs/contracts/settings-classes.md +39 -7
  51. package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
  52. package/llms.txt +1 -1
  53. package/package.json +1 -1
  54. package/src/agent-src/scripts/archive_completed_roadmaps.ts +106 -4
  55. package/src/config/agent-settings.template.yml +47 -1
  56. package/src/config/continuity-surface.json +7 -0
  57. package/src/config/evaluator-budgets.json +4 -4
  58. package/src/config/gate-violation-baselines.json +2 -2
  59. package/src/config/hook-token-budget.json +7 -6
  60. package/src/config/packed-binary-manifest.json +1 -1
  61. package/src/config/routing-coverage-seed.json +9 -2
  62. package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
  63. package/src/scripts/_cli/handoff_sessions.ts +14 -44
  64. package/src/scripts/_lib/continuity_slot.ts +295 -0
  65. package/src/scripts/_lib/continuity_writer.ts +253 -0
  66. package/src/scripts/_lib/hook_settings.ts +85 -0
  67. package/src/scripts/_lib/lean_projection_mode.ts +127 -0
  68. package/src/scripts/_lib/packed_binary_predicate.ts +37 -0
  69. package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
  70. package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
  71. package/src/scripts/_lib/session_eol.ts +44 -0
  72. package/src/scripts/_lib/session_index_trust.ts +346 -0
  73. package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
  74. package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
  75. package/src/scripts/audit_mcp_tools.ts +69 -25
  76. package/src/scripts/build_mcp_catalog.ts +24 -1
  77. package/src/scripts/check_host_tree_parity.ts +291 -0
  78. package/src/scripts/check_preamble_payload_budget.ts +136 -7
  79. package/src/scripts/check_rule_projection_integrity.ts +102 -2
  80. package/src/scripts/ci_settle.ts +205 -5
  81. package/src/scripts/code_graph/build.ts +79 -0
  82. package/src/scripts/code_graph/cli.ts +258 -7
  83. package/src/scripts/code_graph/detect.ts +38 -0
  84. package/src/scripts/code_graph/query.ts +25 -2
  85. package/src/scripts/code_graph/sqlite_store.ts +16 -1
  86. package/src/scripts/code_graph/types.ts +52 -3
  87. package/src/scripts/code_graph/validate.ts +3 -1
  88. package/src/scripts/code_graph/verbs.ts +590 -0
  89. package/src/scripts/condense.ts +20 -20
  90. package/src/scripts/handoff_context_hook.ts +39 -1
  91. package/src/scripts/hook_manifest.json +1 -1
  92. package/src/scripts/hook_manifest.yaml +24 -1
  93. package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
  94. package/src/scripts/hooks/code_graph_context_hook.ts +7 -18
  95. package/src/scripts/hooks/push_settle_hook.ts +3 -2
  96. package/src/scripts/hooks/rule_inject_hook.ts +107 -20
  97. package/src/scripts/hooks/session_eol_hook.ts +120 -2
  98. package/src/scripts/hot_context_hook.ts +62 -7
  99. package/src/scripts/lint_roadmap_blockers.ts +164 -1
  100. package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
  101. package/src/scripts/mcp_server/graph_tools.ts +294 -0
  102. package/src/scripts/mcp_server/path_util.ts +31 -0
  103. package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
  104. package/src/scripts/mcp_server/tools.ts +3 -13
  105. package/src/scripts/model_rule_injection.ts +61 -4
  106. package/src/scripts/project_thin_rules.ts +149 -16
  107. package/src/scripts/report_standing_payload_by_host.ts +650 -0
  108. package/src/scripts/schemas/agent-settings.schema.json +7 -1
  109. package/src/scripts/session_memory_index.ts +81 -8
  110. package/src/server/schemas/settings.ts +23 -4
  111. package/src/vendor/grammars/README.md +27 -2
@@ -19,7 +19,7 @@
19
19
  * `.augment/` projections.
20
20
  *
21
21
  * NOTE (faithful literal): the thin-entry pointer keeps the verbatim
22
- * `Body: [`{rule_id}`](../../.agent-src.uncondensed/rules/{rule_id}.md)`
22
+ * `Body: [`{rule_id}`](../../dist/agent-src/rules/{rule_id}.md)`
23
23
  * Markdown link from the retired Python implementation. The same-basename `.py` twin
24
24
  * carries that literal, so the ADR-051 legacy-path guard exempts this `.ts`
25
25
  * file by twin-parity.
@@ -34,6 +34,7 @@ import * as fs from 'node:fs';
34
34
  import * as path from 'node:path';
35
35
  import process from 'node:process';
36
36
  import { fileURLToPath, pathToFileURL } from 'node:url';
37
+ import { pathOnlyRuleIds, triggerlessRuleIds, } from './_lib/rule_injection.js';
37
38
  import * as token_count from './_lib/token_count.js';
38
39
  const _HERE = fileURLToPath(import.meta.url);
39
40
  // REPO_ROOT = Path(__file__).resolve().parent.parent.parent — three dirs up
@@ -72,21 +73,85 @@ export function kernel_ids() {
72
73
  * visible instead of being absorbed into the pointer count
73
74
  * (road-to-trigger-delivered-rule-bodies 1.3).
74
75
  */
76
+ /**
77
+ * Rules whose ONLY triggers are path-shaped — reachable from `pre_tool_use`
78
+ * and from no other slot (road-to-delivery-for-every-host 3.2).
79
+ *
80
+ * They must never be thinned while the delivery concern is NOT bound on
81
+ * `pre_tool_use`, because a stub plus a slot nobody listens on is a rule that
82
+ * reaches the model at no scope at all. This is the same failure the
83
+ * `no_trigger_ids` exemption prevents, one step out: there the rule has nothing
84
+ * to match on, here it has nothing to match ON.
85
+ *
86
+ * Stated as a PROPERTY rather than a list of ids. If the concern is ever bound
87
+ * on `pre_tool_use` again, this exemption is what should be reconsidered — not
88
+ * three names someone has to remember.
89
+ *
90
+ * WHAT THIS EXEMPTION DOES **NOT** COVER — recorded 2026-09-08, R2 finding 1,
91
+ * because the silence read as coverage. The predicate is `every` trigger
92
+ * path-shaped. A rule with BOTH path and non-path triggers is therefore THINNED,
93
+ * and its path-shaped half then has no carrier at all: the stub carries no
94
+ * frontmatter (`condense.ts` writes it with `_writeText`, bypassing
95
+ * `_emit_claude_rule`'s host-native `paths:` key), `pre_tool_use` lost the
96
+ * binding under owner ruling E2, and `user_prompt_submit` never populates
97
+ * `openFiles`. Measured over the current `dist/router.json`: **18 such rules**,
98
+ * all non-kernel and all thinned — augment-edit-discipline, design-fidelity,
99
+ * doc-screenshot-hygiene, domain-adoption-policy,
100
+ * framework-neutrality-in-generic-skills, image-likeness-and-rights,
101
+ * laravel-translations, lethal-trifecta-guard, linked-projects-onboarding-gate,
102
+ * low-impact-corpus-privacy-floor, markdown-safe-codeblocks, onboarding-gate,
103
+ * persona-governance, php-coding, provider-lifecycle-discipline,
104
+ * roadmap-ci-steps-policy, roadmap-progress-sync, settings-ask-protocol.
105
+ *
106
+ * They do NOT reach the model at no scope: every non-path route
107
+ * (keyword / phrase / command) still delivers the body through the concern, and
108
+ * on `claude-code` they never had a separate host-native path route to lose —
109
+ * `_claude_paths_plan` emits no `paths:` for a mixed-trigger rule on purpose, so
110
+ * under `eager-all` they loaded UNCONDITIONALLY. What is lost is the difference
111
+ * between unconditional and prompt-triggered: a session that touches a matching
112
+ * file and says nothing that matches gets nothing.
113
+ *
114
+ * The labelled corpus cannot see this and the endpoint says so rather than
115
+ * implying otherwise — `model_rule_injection --endpoints` (b) now publishes the
116
+ * shipped `user_prompt_submit` reach beside the pre-registered reading, and
117
+ * every one of these 18 has at least one prompt-matching positive, so none
118
+ * appears in its "reachable only via a path trigger" column.
119
+ *
120
+ * WHY THIS IS NOT WIDENED TO `some` HERE. Measured: exempting all 18 moves the
121
+ * thin rule layer 23,592 -> 39,921 GPT tok (+16,329, +69 %), which is a budget
122
+ * move of a magnitude no agent may take, and it would erase most of the saving
123
+ * the flip is licensed on. Restoring a path route needs either that exemption
124
+ * under a re-anchored baseline or a `pre_tool_use` binding under a raised slot
125
+ * cap — both owner-reserved, so the closure is tracked rather than taken here.
126
+ * ADR-267's Consequences section names the receiver.
127
+ */
128
+ /**
129
+ * ONE spelling of the router-derived sets, shared with the injector (R2
130
+ * finding 11).
131
+ *
132
+ * `path_only_ids` and `no_trigger_ids` were each a second, hand-rolled
133
+ * implementation of `_lib/rule_injection.ts::pathOnlyRuleIds` /
134
+ * `triggerlessRuleIds` — verified identical over the current router at the time
135
+ * (symmetric difference empty), so drift risk rather than a live defect. It is
136
+ * exactly the hazard `THIN_ENTRY_MARKER`'s own comment argues against one
137
+ * screen below ("a gate that re-spelled this string would drift from the writer
138
+ * silently"), and the two consumers here are the PROJECTOR's exemption set and
139
+ * the INJECTOR's reachability set: they must not be able to disagree about which
140
+ * rules the delivery path can reach.
141
+ *
142
+ * The finding named `path_only_ids`. The defect-pattern sweep found the SECOND
143
+ * instance one function down — `no_trigger_ids` against `triggerlessRuleIds` —
144
+ * and it is delegated in the same change. Count: 2 of 2 duplicated
145
+ * router-derived sets in this file, 0 remaining.
146
+ */
147
+ function _router() {
148
+ return JSON.parse(fs.readFileSync(ROUTER, 'utf-8'));
149
+ }
150
+ export function path_only_ids() {
151
+ return pathOnlyRuleIds(_router());
152
+ }
75
153
  export function no_trigger_ids() {
76
- const data = JSON.parse(fs.readFileSync(ROUTER, 'utf-8'));
77
- const out = new Set();
78
- for (const tier of ['tier_1', 'tier_2']) {
79
- const entries = data[tier];
80
- if (!Array.isArray(entries))
81
- continue;
82
- for (const e of entries) {
83
- const obj = e;
84
- const t = obj.triggers;
85
- if (!Array.isArray(t) || t.length === 0)
86
- out.add(String(obj.id));
87
- }
88
- }
89
- return out;
154
+ return new Set(triggerlessRuleIds(_router()));
90
155
  }
91
156
  /**
92
157
  * id → workspaces for every non-kernel router entry (router.json schema v2,
@@ -179,6 +244,42 @@ function _trigger_hint(fm) {
179
244
  function _title(s) {
180
245
  return s.replace(/[A-Za-z]+/g, (w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase());
181
246
  }
247
+ /**
248
+ * The marker that makes a projected entry recognisable as a stub.
249
+ *
250
+ * Exported so the WRITER and every DETECTOR share one definition. A gate that
251
+ * re-spelled this string would drift from the writer silently, and the failure
252
+ * would be invisible in exactly the direction that matters: a stub the gate
253
+ * fails to recognise reads as a complete rule body.
254
+ */
255
+ export const THIN_ENTRY_MARKER = '> Routed rule — load the body on trigger-match.';
256
+ /** Is this projected entry a pointer stub rather than a rule body? */
257
+ export function is_thin_entry(text) {
258
+ return text.includes(THIN_ENTRY_MARKER);
259
+ }
260
+ /**
261
+ * Where a stub tells a reader the body actually is.
262
+ *
263
+ * FIXED 2026-09-07: this pointed into the uncondensed source tree ADR-051
264
+ * retired — the literal path is deliberately NOT reproduced here, because
265
+ * `check_no_uncondensed_refs` matches the string and cannot tell a repair
266
+ * record from a live reference; `git log -S` on this file finds the old value.
267
+ * It exists in no checkout, so EVERY stub's fallback pointer
268
+ * resolved to nothing — invisible under `delivery`, where the hook loads the
269
+ * body from `dist/agent-src/rules` and never follows this link, and total under
270
+ * `thin`, where the pointer is the only path to the body there is.
271
+ *
272
+ * KNOWN LIMIT, stated rather than discovered later: one relative prefix cannot
273
+ * be right for every tree, because they sit at different depths — `.claude/rules`
274
+ * and `.cursor/rules` are two levels below the repo root and `.clinerules` is
275
+ * one. `../../` is correct for the first two, which are the only trees the
276
+ * shipped `lean_projection.hosts` default can thin. A stub in `.clinerules`
277
+ * needs an operator to have added `cline` to that list, and its pointer is off
278
+ * by one level. That is strictly better than the previous state, where the
279
+ * pointer was dead in all three, and it is recorded here so the remaining case
280
+ * is a known limit rather than a surprise.
281
+ */
282
+ const BODY_LINK_PREFIX = '../../dist/agent-src/rules/';
182
283
  /** Build the minimal progressive-disclosure pointer for a non-kernel rule. */
183
284
  export function thin_entry(rule_id, text) {
184
285
  const [fm] = split_frontmatter(text);
@@ -187,13 +288,25 @@ export function thin_entry(rule_id, text) {
187
288
  const title = _title(rule_id.replace(/-/g, ' '));
188
289
  const fires = hint ? ` Fires on: ${hint}.` : '';
189
290
  return (`## ${title}\n` +
190
- `> Routed rule — load the body on trigger-match.${fires} ${desc} ` +
191
- `Body: [\`${rule_id}\`](../../.agent-src.uncondensed/rules/${rule_id}.md)\n`);
291
+ `${THIN_ENTRY_MARKER}${fires} ${desc} ` +
292
+ `Body: [\`${rule_id}\`](${BODY_LINK_PREFIX}${rule_id}.md)\n`);
192
293
  }
193
294
  /** Map {filename: thin_or_full_text} for every rule. Kernel stays full. */
194
- export function build_thin(rules_dir = RULES_SOURCE, scope = null) {
295
+ export function build_thin(rules_dir = RULES_SOURCE, scope = null,
296
+ /**
297
+ * Optional sink for the D3 diagnostic
298
+ * (road-to-delivery-for-every-host 2.2).
299
+ *
300
+ * An `auto` rule the router gives NO trigger is already kept full-bodied by
301
+ * `noTrigger` below, which is the substantive protection and predates this
302
+ * roadmap. What did not exist is any way to SEE it happen: the rule is
303
+ * silently exempted, so an author who removes a rule's last trigger gets a
304
+ * silently eager rule and no signal. The line makes the exemption audible.
305
+ */
306
+ announce = null) {
195
307
  const kernel = kernel_ids();
196
308
  const noTrigger = no_trigger_ids();
309
+ const pathOnly = path_only_ids();
197
310
  const wsMap = scope !== null ? rule_workspaces_map() : new Map();
198
311
  const out = new Map();
199
312
  for (const p of _globSortedMd(rules_dir)) {
@@ -202,7 +315,14 @@ export function build_thin(rules_dir = RULES_SOURCE, scope = null) {
202
315
  if (!id_in_scope(stem, scope, kernel, wsMap, fm_workspaces(text))) {
203
316
  continue; // out of workspace scope — no body, no pointer line
204
317
  }
205
- const full = kernel.has(stem) || noTrigger.has(stem);
318
+ const full = kernel.has(stem) || noTrigger.has(stem) || pathOnly.has(stem);
319
+ if (announce !== null && noTrigger.has(stem) && !kernel.has(stem)) {
320
+ announce(`D3: trigger-less auto rule ${path.basename(p)} — kept full-bodied, never thinned`);
321
+ }
322
+ if (announce !== null && pathOnly.has(stem) && !kernel.has(stem) && !noTrigger.has(stem)) {
323
+ announce(`E2: path-only auto rule ${path.basename(p)} — kept full-bodied; its only triggers ` +
324
+ `are path-shaped and the delivery concern is not bound on pre_tool_use`);
325
+ }
206
326
  out.set(path.basename(p), full ? text : thin_entry(stem, text));
207
327
  }
208
328
  return out;
@@ -29,7 +29,8 @@ const confidenceBand = z.enum(['off', 'low', 'medium', 'high']);
29
29
  const onBlock = z.enum(['stop', 'ask', 'warn']);
30
30
  const onBlockFallback = z.enum(['stop', 'warn']);
31
31
  const modelAutoSwitch = z.enum(['auto', 'suggest', 'off']);
32
- const leanProjectionMode = z.enum(['eager-all', 'thin']);
32
+ const leanProjectionMode = z.enum(['eager-all', 'thin', 'delivery']);
33
+ const leanProjectionHost = z.enum(['claude-code', 'cursor', 'cline']);
33
34
  const projectionMode = z.enum(['legacy-all', 'scoped']);
34
35
  const memoryCadence = z.enum(['auto', 'always', 'never']);
35
36
  const projectAudience = z.enum(['self', 'internal', 'client', 'public']);
@@ -46,8 +47,16 @@ export const settingsSchema = z.object({
46
47
  rule_loading_tier: ruleLoadingTier.default('balanced').describe('Master switch for which rule tiers load and how cautiously the agent spends tokens. minimal = only the 9 kernel rules (cheapest, fewest guardrails). balanced = kernel + tier-1 (recommended default). full = kernel + tier-1 + tier-2 (most guardrails, highest token cost). custom = roll your own in agents/overrides/. LEGACY: superseded by discipline_profile — when that key is set it wins (mapping: minimal→off, balanced→essential, full→full).'),
47
48
  discipline_profile: disciplineProfile.optional().describe('The ONE runtime knob for the discipline-rule tier (successor of rule_loading_tier; council 2026-07-07). off = kernel only (~1x tokens). essential = kernel + the measured lift-carrying rules (~3.3x, keeps the weak-host discipline lift). full = everything (~11.7x, EXPERIMENTAL — residual lift over essential not established). auto = resolve per session against the evidence-gated NULL-lift disable-list in src/config/host-capabilities.yml (measured-null host → off, otherwise → essential). Optional and opt-in until the P1/P2 evidence gates pass (agents/roadmaps/road-to-discipline-profile-tiering.md); absent = legacy rule_loading_tier applies.'),
48
49
  lean_projection: z.object({
49
- mode: leanProjectionMode.default('eager-all').describe('How the per-tool projector emits the rule layer. eager-all = every rule body inlined into every projection (default, safe). thin = kernel rules full-bodied + non-kernel rules as router-resolved pointers (~45k GPT-tok lighter per session). EXPERIMENTAL: validate with the live A/B before flipping; one-flip revert to eager-all.'),
50
- }).default({ mode: 'eager-all' }),
50
+ mode: leanProjectionMode.default('eager-all').describe('How the per-tool projector emits the rule layer. eager-all = every rule body inlined into every projection (safe; what an ABSENT key still resolves to, per ADR-267 decision point 4). thin = kernel rules full-bodied + non-kernel rules as router-resolved pointers (~45k GPT-tok lighter per session), with no delivery mechanism. delivery = thin stubs PLUS the rule-inject hook concern, which delivers a body back on a trigger match; this is the value the SHIPPED TEMPLATE carries for Claude Code since ADR-267, and the activation charge for it is paid in src/config/hook-token-budget.json (user_prompt_submit slot sum 4,096 -> 16,384).'),
51
+ hosts: z.array(leanProjectionHost).default(['claude-code']).describe('Which hosts a thinning mode may thin (ADR-267). Absent resolves to [claude-code]. '
52
+ + 'Only the three hosts with a per-rule rule tree are accepted, and this enum is the '
53
+ + 'AUTHORING layer: an id outside it is a hard validation FAILURE here and under '
54
+ + 'validate_agent_settings, never a warning. resolveLeanProjectionHosts is the '
55
+ + 'READ-TIME backstop for a hand-edited .agent-settings.yml that passed neither, and '
56
+ + 'THERE the id is dropped with a warning (R2 finding 6 — the two layers answer '
57
+ + 'differently and neither said so). Every host outside this list receives exactly '
58
+ + 'what eager-all writes, asserted byte-for-byte by check_host_tree_parity.'),
59
+ }).default({ mode: 'eager-all', hosts: ['claude-code'] }),
51
60
  cost: z.object({
52
61
  budgets: z.object({
53
62
  daily: z.number().min(0).default(0).describe('Daily USD ceiling across all model calls. The agent warns at 50% / 75% / 90% and either stops or warns at 100% depending on cost.enforcement. Set 0 to disable the daily budget entirely.'),
@@ -229,6 +238,10 @@ export const settingsSchema = z.object({
229
238
  api_paths: z.array(z.string()).default([]).describe('Optional glob list that makes API-endpoint detection explicit instead of heuristic (e.g. ["app/Http/Controllers/Api/**", "src/pages/api/**"]). Empty (default) = a light path/extension heuristic that fails open.'),
230
239
  }),
231
240
  }),
241
+ continuity: z.object({
242
+ auto_record: z.enum(['on', 'off']).default('off').describe('Deterministic continuity-record writer at session end (road-to-continuity-writer-activation Phase 1). on = the session-eol concern writes the continuity_record capsule variant on Stop for a substantive session that has claimed a roadmap; every field is computed from on-disk state, with no model spend and no subprocess. off (default) = no automatic record — while session:recycle is still the normal path, a second producer on it before the parity evidence is in would be unverified.'),
243
+ run_checkpoints: z.enum(['on', 'off']).default('on').describe('Deterministic run-checkpoint production at session end (road-to-continuity-writer-activation Phase 1.4). on (default) = a session above the recycle threshold and inside a running roadmap contract leaves agents/runtime/state/checkpoints/<run>.json, so a killed run resumes from a derived checkpoint rather than from bookkeeping. off = no checkpoint; continuity writing, the recycle advisory and the context-fill surface are unaffected. Default ON because it is the behaviour the tree already had — the switch exists to make the three session-end handlers independently disableable, not to change what ships.'),
244
+ }),
232
245
  memory: z.object({
233
246
  cadence: memoryCadence.default('always').describe('Cadence of the 🧠 memory-visibility line after a memory-consulting step. always (default) = show whenever a memory type was asked; auto = show only when 3+ types were consulted (less noise); never = suppress. Distinct from rule_loading_tier — owns its own key since the 2026-06-01 untangle.'),
234
247
  review_threshold: z.number().int().min(0).default(10).describe('Maximum number of memory entries /memory:load surfaces inline before falling back to a summary view. Default 10 keeps the chat readable. Raise to see more candidates, lower to keep the context tight.'),
package/docs/MIGRATION.md CHANGED
@@ -42,6 +42,62 @@ reason, which no arithmetic can.
42
42
  or that promises a future breaking removal, gets a row here in the same commit
43
43
  that makes the promise. A promise with no row is not tracked and will be missed.
44
44
 
45
+ ## 14.22.x — the code-graph engine gets readers, and the MCP install hint changes
46
+
47
+ 💡 advisory · 🔄 automatic — **one exception, below.**
48
+
49
+ The 14.21.x entry above shipped the engine to consumers. This one ships the
50
+ things that read it (`road-to-a-graph-that-is-shipped`, Phases 3 and 4).
51
+
52
+ ### Additive — nothing to do
53
+
54
+ - **Four new `code-graph` subcommands**, all answering questions about a DIFF
55
+ rather than a symbol: `impact --diff <rev>`, `tests-for <symbol>`,
56
+ `untested --diff <rev>`, `dead`. Each prints the resolution mechanisms it
57
+ accepted, the mechanisms it refused, and the graph's three-state staleness.
58
+ No existing subcommand changed, and no new top-level verb was added.
59
+ - **Five new MCP tools** on the existing stdio server — `graph_impact`,
60
+ `graph_tests_for`, `graph_dead`, `graph_query`, `graph_path` — taking the
61
+ catalogue from 31 tools to 36. Four are read-only; `graph_impact` runs
62
+ `git diff --name-only` to resolve its rev and declares `side_effect: shell`
63
+ for that reason.
64
+ - **A new `tests` edge relation**, derived in the build pass from a test file's
65
+ imports. `SCHEMA_VERSION` 3 → 4 and `GRAPH_STORE_VERSION` 3 → 4, so an
66
+ existing cache and its SQLite twin are refused and rebuilt on the next
67
+ `code-graph build`. The cache is derived and disposable
68
+ (ADR-129) — there is nothing to migrate, and a rebuild takes seconds.
69
+
70
+ ### The one thing to check — `dead` refuses by default
71
+
72
+ `agent-config code-graph dead` exits **1** with an empty list unless you either
73
+ supply an entry-point list (`--entry-points <file>`) or accept the gap
74
+ (`--accept-missing-exports`). That is deliberate: this engine records no
75
+ exportedness, so without a supplied list an exported-but-not-yet-imported public
76
+ symbol is indistinguishable from a dead one, and a confident false "dead" invites
77
+ a deletion the graph cannot justify. If you wire `dead` into a script, handle the
78
+ refusal — an exit 1 here is not a crash, and treating it as an empty dead list
79
+ is the false negative the refusal exists to prevent.
80
+
81
+ ### One consumer-visible string changed
82
+
83
+ `install_hint_stdio` in the published MCP tool catalogue was
84
+ `npx -y @event4u/agent-config mcp-server` and is now **`agent-config mcp-server`**
85
+ — the installed-binary form the setup docs already lead with. It resolves no
86
+ dist-tag, so the server you run is the version you installed rather than
87
+ whatever the registry served most recently. **It assumes `agent-config` is on
88
+ PATH.** If you read that field programmatically and rely on `npx`, keep using
89
+ the pinned form from [`mcp-server.md`](mcp-server.md) —
90
+ `npx -y @event4u/agent-config@<version> mcp-server` — which is unchanged and
91
+ still the documented client configuration.
92
+
93
+ ### Standing context cost
94
+
95
+ Registering the stdio MCP server now costs **4,876 tokens** of standing context
96
+ per session, up from 3,886 — the five graph tools measure 993 between them. It
97
+ remains roughly 4x under the ~20,000-token Tool Search deferral threshold at a
98
+ 200k window, so the surface still loads upfront. Measured, with the prior reading
99
+ kept: `agents/evidence/metrics/mcp-tool-standing-cost.jsonl`.
100
+
45
101
  ## 14.21.x — the code-graph engine ships to consumers
46
102
 
47
103
  💡 advisory · 🔄 automatic — no action required.
@@ -247,8 +247,33 @@ dispatch runs, not WHETHER the layer exists, so they keep their own C rows.
247
247
  |---|---|
248
248
  | A — preference | 26 |
249
249
  | B — consent | 3 |
250
- | C — guarded | 110 |
251
- | **Total** | **139** |
250
+ | C — guarded | 113 |
251
+ | **Total** | **142** |
252
+
253
+ It rose to 142 on 2026-09-08 from two independent additions that landed in the same
254
+ day and are counted together here: `lean_projection.hosts` (one C, `policy`, from
255
+ `road-to-delivery-for-every-host`) and the two continuity keys below. The merge that
256
+ combined them re-derived the total from the class table rather than adding either
257
+ side's figure to the other, which is the arithmetic `lint_settings_classes` checks.
258
+
259
+ It rose on 2026-09-08 when `road-to-continuity-writer-activation`
260
+ step 1.2 added `continuity.auto_record` — one C, `consent`: it arms an automatic
261
+ producer on the normal session-end path, and the roadmap step requires the
262
+ default to stay `off` until parity is measured, which is a decision no agent may
263
+ take by writing the key. Step 1.4 added
264
+ `continuity.run_checkpoints` the same day — a second C, shipping `on` because
265
+ that is the behavior the tree already had. It exists so the three session-end
266
+ handlers are independently disableable, which is what the 2026-09-07 council's
267
+ D2 asked for; it changes nothing that ships.
268
+
269
+ Its disposition is `consent`, and `derivable` was considered first and rejected:
270
+ `derivable` is the **deletion queue**, a claim that the key disappears once the
271
+ mechanism it names exists. This one is an operator kill switch over a recovery
272
+ artifact, so it is meant to survive — and nothing in the tree can derive whether
273
+ an operator wants resumability traded for quiet. It sits in the same shape as the
274
+ kill switches already classified `consent` (`knowledge.global_sharing.enabled`
275
+ and its allowlist): the mechanism's own preconditions are computable, the
276
+ operator's decision to suppress it is not.
252
277
 
253
278
  The total was 140 until 2026-08-12, when five of the six keys no code path read were
254
279
  deleted, minus the one held open (§ The six unread keys, below): one A
@@ -338,9 +363,9 @@ the template, which is the drift this contract exists to prevent.
338
363
  |---|---|
339
364
  | derivable | 83 |
340
365
  | un-inferrable | 9 |
341
- | consent | 41 |
342
- | policy | 6 |
343
- | **Total** | **139** |
366
+ | consent | 43 |
367
+ | policy | 7 |
368
+ | **Total** | **142** |
344
369
 
345
370
  First measured 2026-08-12 at 140 leaves (derivable 88 · consent 38 ·
346
371
  un-inferrable 9 · policy 5), from the table below rather than predicted — the
@@ -355,8 +380,12 @@ mechanism it names actually exists, so 83 measures work outstanding, not keys
355
380
  about to disappear. The count is expected to fall while `un-inferrable` does not
356
381
  — those 9 plus whatever survives re-examination in `consent` are the floor this
357
382
  surface has, and stating it is the point. `policy` is the smallest class and the
358
- only one whose action is a *move* rather than a keep or a delete: five keys carry
359
- a project fact the tree could hold instead.
383
+ only one whose action is a *move* rather than a keep or a delete: seven keys carry
384
+ a project fact the tree could hold instead. (This sentence read "five" while the
385
+ counts table beside it read 6, and 2026-09-08 added `lean_projection.hosts` as the
386
+ seventh — corrected in both directions rather than only incremented, because a
387
+ prose number that disagrees with a computed one beside it is the exact defect
388
+ `lint_settings_classes` fails the two Total rows for.)
360
389
 
361
390
  ## The floor — the nine keys no mechanism can derive
362
391
 
@@ -440,6 +469,7 @@ Rows follow template order, so a diff against the template reads straight down.
440
469
  | `discipline_profile` | C | `__DISCIPLINE_PROFILE__` | master switch for the discipline rule tier | derivable — its own `auto` resolution against `src/config/host-capabilities.yml` |
441
470
  | `rule_loading_tier` | C | `__RULE_LOADING_TIER__` | legacy master switch for rule loading | derivable — `discipline_profile` supersedes it with a documented mapping (minimal→off, balanced→essential, full→full) |
442
471
  | `lean_projection.mode` | C | `eager-all` | `thin` removes rule bodies from the agent's context | derivable — `probe_host_compliance.ts` already computes the per-host thin/eager recommendation |
472
+ | `lean_projection.hosts` | C | `[claude-code]` | decides WHICH hosts a thinning mode may thin, so an added id removes rule bodies from that host's context | policy — the value is a package-level decision the tree already holds: ADR-267 scopes the shipped `delivery` flip to `claude-code` alone. NOT `derivable`, and the distinction is the whole reason for this row: the mechanism that would compute an admissible host set, `admissibleUnderE3` in `src/scripts/_lib/injection_effect.ts`, admits **nothing** today — `src/config/host-injection-effect.json` carries 1 `observed-false` and 8 `unobserved` rows and no `observed-true` — so a derivation would yield `[]` rather than the shipped `[claude-code]`. Naming a mechanism that returns empty as the replacement would put a key in the deletion queue whose deletion would silently change the default. Move target: the ADR plus that census, once a host earns an `observed-true` row |
443
473
  | `telegraph.speak` | C | `false` | ships a rule body; a token-cost lever in both directions | derivable — the telegraph kill-criterion bench verdict is a package-level decision, not a per-install one |
444
474
  | `tokens.rich_skills` | C | `"on"` | token-spend lever | derivable — the skill's own `token_budget_class: rich` declaration plus the CI ceiling in `lint_token_budget_discipline.ts` |
445
475
  | `cost.budgets.daily` | C | `0` | rolling spend ceiling | consent |
@@ -537,6 +567,8 @@ Rows follow template order, so a diff against the template reads straight down.
537
567
  | `commands.create_pr.screenshots` | C | `false` | puts captured screenshots into a published PR body | consent |
538
568
  | `commands.create_pr.ui_paths` | C | `[]` | glob allowlist | derivable — the frontend-surface heuristic the PR-description flow already applies when the glob list is empty |
539
569
  | `commands.create_pr.api_paths` | C | `[]` | glob allowlist | derivable — the API-endpoint heuristic the same flow already applies as its documented empty-list fallback |
570
+ | `continuity.auto_record` | C | `"off"` | arms an automatic producer on the normal session-end path | consent |
571
+ | `continuity.run_checkpoints` | C | `"on"` | disabling it removes a recovery artifact a killed run resumes from | consent |
540
572
  | `memory.cadence` | C | `always` | suppressing the visibility line hides what the agent learned from the user | derivable — the hits/asks count the memory-visibility summary already computes; the line only exists when memory was consulted |
541
573
  | `memory.review_threshold` | A | `10` | when a review preview surfaces; governs no gate | derivable — the unreviewed-intake count `/memory load` already computes before rendering its preview |
542
574
  | `memory.redact_patterns` | C | `[]` | deny-list of secret and PII regexes | policy |
@@ -0,0 +1,90 @@
1
+ # Notes-first reasoning — horizon and reopening mechanics
2
+
3
+ Migrated out of [`notes-first-reasoning`](../../../src/rules/notes-first-reasoning.md)
4
+ on 2026-09-08. That rule is delivered on **every subagent spawn**, so its body is
5
+ paid per spawn; this material is reference read on demand. Nothing was dropped —
6
+ the four passages below are the rule's own text, verbatim, and the rule keeps
7
+ both Iron Laws and points here.
8
+
9
+ Same reason, and the same remedy, as the 2026-08-29 drain run recorded for
10
+ `source-confidentiality`: the addition had no room under the per-spawn ceiling,
11
+ and raising that ceiling is the config-weakening move this repository refuses.
12
+
13
+ ### Why reasoning stays out of the reply, and what grounds the notes file
14
+
15
+ Reasoning dumped into the user-facing answer is both noise and a
16
+ `reasoning_extraction` refusal risk (see `rdp-gate`). Keep it in the notes file.
17
+
18
+ The notes file is grounded in the documented cross-run lessons memory
19
+ (consolidated via [`memory-consolidation`](../../../src/skills/memory-consolidation/SKILL.md));
20
+ the rule's in-task sections are a local derivation for within-task scope. The
21
+ gate itself reads settings + task-signal + host self-assessment.
22
+
23
+ ### The two section fields, in full
24
+
25
+ The rule lists the sections and no count. That is deliberate: the structure
26
+ carries the enumeration, so there is no "write N hypotheses" instruction to
27
+ follow — a run records what the work actually surfaced, and an empty section is
28
+ a fact about the work rather than a gap in the notes.
29
+
30
+ `## Predictions` carries **chosen form** · prediction · confidence · result ·
31
+ lesson — the calibration loop: hypothesis → prediction → reality →
32
+ calibration. The form is named first because a prediction with no subject
33
+ cannot be contradicted: when the observation comes back, "the prediction
34
+ missed" has to point at *what* it was a prediction about, or nothing reopens.
35
+
36
+ `## Decisions` escalates to
37
+ [`decision-record`](../../../src/skills/decision-record/SKILL.md)/ADR when the
38
+ decision is cross-task or architectural — litmus: would a dev on a different
39
+ component next month need this context? And `next-commitment` is the boundary
40
+ this choice authorises work up to, not the plan for it: choosing a form is not
41
+ permission to execute a twelve-step plan on it without re-reading the position.
42
+
43
+ ### The horizon — which boundaries count
44
+
45
+ The boundary is the nearest of these that the plan actually reaches: a **test
46
+ result** · a **type or schema inspection** · a **compile** · a **runtime probe**
47
+ · a **call-site inventory** · a **dry run** · a **browser observation** · a
48
+ **dependency-contract read**. The set is enumerable, which is the whole point —
49
+ "the work up to the nearest of those" is decidable from the plan, and `next
50
+ justified commitment` as prose degrades to *"implement solution"*, which is the
51
+ starting point wearing a label.
52
+
53
+ ### Width scales with reversibility, never with task size
54
+
55
+ | The action is… | The horizon runs to… |
56
+ |---|---|
57
+ | a reversible local edit | a whole implementation slice — stopping at the first boundary costs more than it saves |
58
+ | stateful or cross-layer | the next boundary from the list above |
59
+ | irreversible or a public contract | **before** the irreversible step, unless direct evidence already covers it |
60
+
61
+ ### The look-ahead ladder is a CEILING, never a quota
62
+
63
+ Three to five moves is the *maximum*, and only for a forcing, irreversible or
64
+ externally observable line. Stateful or cross-layer gets two to three. A quiet
65
+ reversible line gets **one, deliberately** — and no artifact anywhere requires a
66
+ minimum depth. Always-three-to-five across all variants is 1024 leaves at four
67
+ branches and depth five, which buys nothing and costs the run its budget.
68
+
69
+ ### Reopening — the two consequences
70
+
71
+ A reopen driven by ritual rather than by contradicting evidence is ceremony, and
72
+ an uncapped reopen is a loop with better manners. Two consequences:
73
+
74
+ - **A rejection whose premise is falsified is not a rejection.** A candidate
75
+ killed by an assumption that later turns out false is *un-evaluated*, not
76
+ dead — it returns to the set. That is what makes a stated `killed-if`
77
+ condition worth carrying: without it, "we ruled that out" is unfalsifiable.
78
+ - **The first failure reads the candidate list before the retry**, where one
79
+ exists — see [`autonomy-mechanics`](../../../src/agent-src/contexts/execution/autonomy-mechanics.md)
80
+ § Retry-budget escalation ladder. Where none exists the ladder is unchanged.
81
+ This adds a branch; it does not move the N=3 budget.
82
+
83
+ ## See also
84
+
85
+ - [`notes-first-reasoning`](../../../src/rules/notes-first-reasoning.md) — the
86
+ rule this material was paid for in, and which keeps both Iron Laws.
87
+ - [`rdp-gate`](../../../src/agent-src/contexts/execution/rdp-gate.md) — the
88
+ engagement gate that decides whether the notes discipline runs at all.
89
+ - [`decision-record`](../../../src/skills/decision-record/SKILL.md) — where a
90
+ cross-task or architectural decision escalates to.
package/llms.txt CHANGED
@@ -41,7 +41,7 @@ canvas-design: Creating static visual art — posters, marketing visuals, brand
41
41
  character-consistency: Use when a character must stay visually identical across AI video scenes — locks identity tokens (silhouette, palette, wardrobe, prop) in JSON. Triggers 'character lock', 'same character'.
42
42
  check-refs: Use when verifying cross-references between skills, rules, commands, guidelines, and context documents are not broken after edits, renames, or deletions.
43
43
  churn-prevention: Use when designing churn defence — health-score signals, churn-cause split (involuntary / value / relationship / fit), early-warning loop. Triggers on 'why are accounts leaving'.
44
- code-intelligence: Route codebase-structure questions (who calls X, where is this used, what imports, change-impact) to an existing code-graph first: cheaper, never more precise; grep stays routine. Also 'call graph'.
44
+ code-intelligence: Route codebase-structure questions (who calls X, where used, change-impact) to an existing code-graph first: impact, tests-for, dead cheaper, never more precise; grep routine. Also 'call graph'.
45
45
  code-refactoring: When the user says 'refactor this', 'rename class', or 'move method'. Safely refactors code in any language — finds all callers, updates downstream dependencies, verifies via quality tools.
46
46
  code-review: Use when the user says \"review this\", \"check my code\", or wants feedback on changes. Reviews for correctness, quality, security, and coding standards.
47
47
  command-routing: Use when the user invokes a slash command like /create-pr, /commit, /fix-ci, or pastes command file content — routes to the right command with context inference and GitHub API patterns.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
3
  "mcpName": "io.github.event4u-app/agent-config",
4
- "version": "14.22.0",
4
+ "version": "14.23.0",
5
5
  "description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including the counts in these badges.",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -58,7 +58,11 @@ import * as path from 'node:path';
58
58
  import process from 'node:process';
59
59
  import { fileURLToPath } from 'node:url';
60
60
 
61
- import { collect } from './update_roadmap_progress.js';
61
+ import {
62
+ blocker_is_resolved,
63
+ collect,
64
+ parse_blockers,
65
+ } from './update_roadmap_progress.js';
62
66
  import { guardedBaselineProblems, parseGuardedBaselines } from './guarded_baseline.js';
63
67
 
64
68
  import { isCliEntry } from './_cli_entry.js';
@@ -265,6 +269,80 @@ function _git_mv(root: string, src_rel: string, dst_rel: string, dry_run: boolea
265
269
  }
266
270
  }
267
271
 
272
+ // ---------------------------------------------------------------------
273
+ // The index/working-tree blocker divergence check (1b) + staging (1a)
274
+ // ---------------------------------------------------------------------
275
+ //
276
+ // The open-blocker refusal below reads the file with `fs.readFileSync`, i.e.
277
+ // the WORKING TREE. `git mv` then moves the INDEX ENTRY, which still carries
278
+ // whatever was staged. When the two disagree the commit ships the index
279
+ // content — content this gate never read.
280
+ //
281
+ // Measured, once, at full cost (2026-09-08): an edit removing a roadmap's
282
+ // blocker was made and never staged; `git mv` moved the stale index entry; the
283
+ // archived file shipped a blocker declared `Status: open` that had already been
284
+ // relocated to an active roadmap. The same blocker was open in two files at
285
+ // once, one of them archived, and it was caught by a human reading
286
+ // `git status` rather than by any gate.
287
+ //
288
+ // The check is scoped to blocker id → status and deliberately NOT to whole-file
289
+ // equality. An unstaged typo fix in a roadmap title has nothing to do with the
290
+ // defect, and refusing archival for it trains `git add -A`, which destroys the
291
+ // staging discipline this check depends on. AI council 2026-09-08, 2/2 present
292
+ // (anthropic/claude-sonnet-4-5 + openai/gpt-4o), on that scoping specifically:
293
+ // broad scoping is "user-hostile, trains developers to `git add -A`
294
+ // reflexively, defeating staging discipline".
295
+
296
+ /** Blocker id → status, as the archive-transition check compares them. */
297
+ function _blockerStatuses(text: string): Map<string, string> {
298
+ const out = new Map<string, string>();
299
+ for (const b of parse_blockers(text)) {
300
+ out.set(b.id, blocker_is_resolved(b) ? 'resolved' : 'open');
301
+ }
302
+ return out;
303
+ }
304
+
305
+ /**
306
+ * `git show :<rel>` — the STAGED content of a path, or null when the path has
307
+ * no index entry.
308
+ *
309
+ * Null is the untracked / pre-first-commit case, which `_git_mv` already
310
+ * handles by falling back to a plain rename. There is no index entry to
311
+ * disagree with, so the divergence check has nothing to assert and says so by
312
+ * returning null rather than by guessing at an empty string — an empty staged
313
+ * file and an absent one differ, and only the second is legitimate here.
314
+ */
315
+ function _indexContent(root: string, rel: string): string | null {
316
+ const cp = _run(['git', 'show', `:${rel}`], root);
317
+ return cp.returncode === 0 ? cp.stdout : null;
318
+ }
319
+
320
+ /**
321
+ * Human-readable disagreements between the staged and working-tree blocker
322
+ * maps of one path. Empty array = the two agree, or there is no index entry.
323
+ */
324
+ function _blockerDivergence(root: string, rel: string, workTreeText: string): string[] {
325
+ const staged = _indexContent(root, rel);
326
+ if (staged === null) {
327
+ return [];
328
+ }
329
+ const wt = _blockerStatuses(workTreeText);
330
+ const idx = _blockerStatuses(staged);
331
+ const ids = [...new Set([...wt.keys(), ...idx.keys()])].sort();
332
+ const out: string[] = [];
333
+ for (const id of ids) {
334
+ const a = idx.get(id);
335
+ const b = wt.get(id);
336
+ if (a === b) {
337
+ continue;
338
+ }
339
+ out.push(
340
+ `blocker '${id}': staged=${a ?? 'absent'} vs working tree=${b ?? 'absent'}`,
341
+ );
342
+ }
343
+ return out;
344
+ }
345
+
268
346
  /**
269
347
  * Repo-relative paths of files containing `needle`, by walking the filesystem.
270
348
  *
@@ -547,9 +625,8 @@ function archive_completed(
547
625
  // and invisible in a sweep that only reports what it moved; a MALFORMED
548
626
  // one on `[x]` would otherwise archive with an annotation claiming the
549
627
  // opposite. Both are named here instead.
550
- const guarded = parseGuardedBaselines(
551
- fs.readFileSync(path.join(roadmap_root, stats.rel), 'utf-8'),
552
- );
628
+ const roadmapText = fs.readFileSync(path.join(roadmap_root, stats.rel), 'utf-8');
629
+ const guarded = parseGuardedBaselines(roadmapText);
553
630
  if (guarded.length > 0) {
554
631
  process.stderr.write(
555
632
  ` ⚠️ ${stats.rel}: ${guarded.length} guarded-baseline step(s) — ` +
@@ -603,12 +680,37 @@ function archive_completed(
603
680
  if (changed_only && !(touched as Set<string>).has(old_rel)) {
604
681
  continue; // complete, but not this branch's work
605
682
  }
683
+ // The gate above validated the WORKING TREE; `git mv` ships the INDEX.
684
+ // When they disagree about a blocker, the validated content is not the
685
+ // content that gets committed — see § the index/working-tree blocker
686
+ // divergence check above for the measured failure.
687
+ const divergence = _blockerDivergence(root, old_rel, roadmapText);
688
+ if (divergence.length > 0) {
689
+ process.stderr.write(
690
+ ` ⚠️ ${stats.rel}: staged and working-tree blockers disagree ` +
691
+ `(${divergence.length}) — not archived. \`git mv\` would commit ` +
692
+ 'the staged content, which this sweep never validated. Stage or ' +
693
+ 'discard the change, then re-run.\n',
694
+ );
695
+ for (const d of divergence) {
696
+ process.stderr.write(` · ${d}\n`);
697
+ }
698
+ continue;
699
+ }
606
700
  const new_rel = `agents/roadmaps/archive/${stats.rel}`;
607
701
  const mv = _git_mv(root, old_rel, new_rel, dry_run);
608
702
  if (!mv.ok) {
609
703
  process.stderr.write(` ⚠️ could not archive ${old_rel} (mv failed)\n`);
610
704
  continue;
611
705
  }
706
+ // `git mv` moved the index ENTRY, not the working-tree content. Staging
707
+ // the destination makes what gets committed identical to what the checks
708
+ // above read — the divergence check refuses a disagreement that exists
709
+ // BEFORE the move, and this closes one created by an edit between the
710
+ // move and the commit.
711
+ if (!dry_run && mv.tracked) {
712
+ _run(['git', 'add', '--', new_rel], root);
713
+ }
612
714
  const refs = _inbound_ref_rewrite(root, old_rel, new_rel, dry_run, mv.tracked);
613
715
  // Stage rewritten refs only in a tracked repo; an untracked consumer's
614
716
  // files are not in the index, so the filesystem rewrite is the whole job.