@event4u/agent-config 14.5.0 → 14.7.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 (203) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +384 -3
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/decision.md +7 -0
  5. package/dist/agent-src/commands/analyze/inbox.md +264 -10
  6. package/dist/agent-src/commands/analyze.md +1 -1
  7. package/dist/agent-src/commands/optimize/project.md +10 -0
  8. package/dist/agent-src/commands/roadmap/process-full.md +132 -4
  9. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +31 -0
  10. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +161 -1
  11. package/dist/agent-src/rules/context-hygiene.md +2 -0
  12. package/dist/agent-src/rules/decision-revisit-gate.md +111 -7
  13. package/dist/agent-src/rules/fix-what-you-see.md +108 -0
  14. package/dist/agent-src/rules/no-roadmap-references.md +0 -5
  15. package/dist/agent-src/rules/recurring-criticism.md +110 -0
  16. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  17. package/dist/agent-src/rules/rule-type-governance.md +0 -2
  18. package/dist/agent-src/rules/self-repair-loop.md +17 -0
  19. package/dist/agent-src/rules/skill-quality.md +0 -2
  20. package/dist/agent-src/rules/source-confidentiality.md +37 -7
  21. package/dist/agent-src/scripts/archival_sweep.ts +117 -0
  22. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  23. package/dist/agent-src/scripts/gate_budget.ts +282 -0
  24. package/dist/agent-src/scripts/gate_execute.ts +72 -31
  25. package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
  26. package/dist/agent-src/scripts/update_roadmap_progress.ts +251 -7
  27. package/dist/agent-src/skills/adr-create/SKILL.md +25 -3
  28. package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
  29. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
  30. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
  31. package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
  32. package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
  33. package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
  34. package/dist/agent-src/skills/decision-review/SKILL.md +18 -0
  35. package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
  36. package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
  37. package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
  38. package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
  39. package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
  40. package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
  41. package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
  42. package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
  43. package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
  44. package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
  45. package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
  46. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
  47. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  48. package/dist/agent-src/templates/roadmaps.md +45 -2
  49. package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  50. package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  51. package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  52. package/dist/cli/registry.js +1 -1
  53. package/dist/cli/registry.js.map +1 -1
  54. package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
  55. package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-4VR4ORQ6.js} +10 -7
  56. package/dist/cli-delegate/chunk-6EOCNQAM.js +402 -0
  57. package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-74E3TKWV.js} +381 -280
  58. package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-7F3VYSM2.js} +4 -4
  59. package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
  60. package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
  61. package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-GJ5BVHXM.js} +72 -165
  62. package/dist/cli-delegate/{chunk-ZPSXJTLI.js → chunk-JZ4NR2TW.js} +353 -1
  63. package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
  64. package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
  65. package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
  66. package/dist/cli-delegate/cmd_conformance.js +459 -709
  67. package/dist/cli-delegate/cmd_doctor.js +6 -6
  68. package/dist/cli-delegate/cmd_explain.js +1 -1
  69. package/dist/cli-delegate/cmd_export.js +7 -6
  70. package/dist/cli-delegate/cmd_handoff.js +6 -6
  71. package/dist/cli-delegate/cmd_packs_active.js +1 -1
  72. package/dist/cli-delegate/cmd_preflight.js +2 -2
  73. package/dist/cli-delegate/cmd_prune.js +7 -6
  74. package/dist/cli-delegate/cmd_session_recycle.js +840 -30
  75. package/dist/cli-delegate/cmd_settings_migrate.js +7 -6
  76. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  77. package/dist/cli-delegate/cmd_sync.js +7 -6
  78. package/dist/cli-delegate/cmd_uninstall.js +9 -7
  79. package/dist/cli-delegate/cmd_update.js +1 -1
  80. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  81. package/dist/cli-delegate/cmd_validate.js +7 -6
  82. package/dist/discovery/deprecation-report.md +1 -1
  83. package/dist/discovery/discovery-manifest.json +72 -29
  84. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  85. package/dist/discovery/discovery-manifest.summary.md +4 -4
  86. package/dist/discovery/dormancy-report.md +1 -1
  87. package/dist/discovery/orphan-report.md +1 -1
  88. package/dist/discovery/packs.json +10 -8
  89. package/dist/discovery/trust-report.md +3 -3
  90. package/dist/discovery/workspaces.json +8 -6
  91. package/dist/hooks/dispatch.js +3127 -2022
  92. package/dist/install/claudePathsPlan.js +187 -0
  93. package/dist/install/claudePathsPlan.js.map +1 -0
  94. package/dist/install/hostLayerFingerprint.js +136 -0
  95. package/dist/install/hostLayerFingerprint.js.map +1 -0
  96. package/dist/install/install.mjs +553 -425
  97. package/dist/install/partitionEligibility.js +276 -0
  98. package/dist/install/partitionEligibility.js.map +1 -0
  99. package/dist/mcp/registry-manifest.json +3 -3
  100. package/dist/mcp/server.json +2 -2
  101. package/dist/mcp/server.mjs +1744 -1191
  102. package/dist/router.json +1 -1
  103. package/dist/scripts/_lib/agent_src.js +41 -0
  104. package/dist/scripts/_lib/agent_src.js.map +1 -1
  105. package/dist/scripts/_lib/install_layout.js +83 -0
  106. package/dist/scripts/_lib/install_layout.js.map +1 -0
  107. package/dist/scripts/_lib/installed_lock.js +444 -0
  108. package/dist/scripts/_lib/installed_lock.js.map +1 -0
  109. package/dist/scripts/condense.js +13 -164
  110. package/dist/scripts/condense.js.map +1 -1
  111. package/dist/server/schemas/settings.js +4 -0
  112. package/dist/server/schemas/settings.js.map +1 -1
  113. package/docs/contracts/settings-classes.md +12 -6
  114. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
  115. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
  116. package/docs/guidelines/agent-infra/engineering-memory-data-format.md +49 -0
  117. package/docs/guidelines/agent-infra/memory-access.md +17 -0
  118. package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  119. package/package.json +1 -1
  120. package/src/agent-src/scripts/archival_sweep.ts +117 -0
  121. package/src/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  122. package/src/agent-src/scripts/gate_budget.ts +282 -0
  123. package/src/agent-src/scripts/gate_execute.ts +72 -31
  124. package/src/agent-src/scripts/roadmap_gates.ts +74 -3
  125. package/src/agent-src/scripts/update_roadmap_progress.ts +251 -7
  126. package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  127. package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  128. package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  129. package/src/config/agent-settings.template.yml +27 -2
  130. package/src/config/ci-local-parity.yml +18 -0
  131. package/src/config/estate-count-budget.json +367 -3
  132. package/src/config/gate-violation-baselines.json +41 -41
  133. package/src/config/pack-size-budget.json +3 -2
  134. package/src/config/rule-activation-census.json +14 -6
  135. package/src/config/rule-stub-ceilings.json +19 -6
  136. package/src/install/claudePathsPlan.ts +196 -0
  137. package/src/install/hostLayerFingerprint.ts +145 -0
  138. package/src/install/partitionEligibility.ts +325 -0
  139. package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
  140. package/src/scripts/_dispatch.bash +6 -1
  141. package/src/scripts/_lib/ac_heading.ts +30 -0
  142. package/src/scripts/_lib/agent_src.ts +42 -0
  143. package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
  144. package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
  145. package/src/scripts/_lib/envelope_grounding.ts +38 -8
  146. package/src/scripts/_lib/installed_lock.ts +50 -5
  147. package/src/scripts/_lib/layer_overlap_notice.ts +85 -0
  148. package/src/scripts/_lib/md_section.ts +129 -0
  149. package/src/scripts/_lib/scope_guard.sh +86 -4
  150. package/src/scripts/_lib/self_repair.ts +19 -0
  151. package/src/scripts/_lib/self_repair_class_b.ts +171 -0
  152. package/src/scripts/_lib/subagent_capsule.ts +81 -1
  153. package/src/scripts/adr/regenerate_index.ts +13 -2
  154. package/src/scripts/adr_cite_check.ts +424 -0
  155. package/src/scripts/ai_council/cli_help.ts +55 -0
  156. package/src/scripts/before_complete_hook.ts +153 -28
  157. package/src/scripts/bench_ab_v2_complexity.ts +27 -7
  158. package/src/scripts/check_adr_frontmatter.ts +187 -1
  159. package/src/scripts/check_references.ts +60 -0
  160. package/src/scripts/check_release_published.ts +32 -11
  161. package/src/scripts/check_single_delivery.ts +517 -0
  162. package/src/scripts/ci_settle.ts +170 -0
  163. package/src/scripts/condense.ts +20 -170
  164. package/src/scripts/council_cli.ts +3 -3
  165. package/src/scripts/dispatch_r2_reviewer.ts +6 -14
  166. package/src/scripts/hook_manifest.json +1 -1
  167. package/src/scripts/hook_manifest.yaml +77 -14
  168. package/src/scripts/hooks/block_no_verify.ts +61 -10
  169. package/src/scripts/hooks/block_unauthorized_git.ts +1 -1
  170. package/src/scripts/hooks/concern_registry.ts +4 -0
  171. package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
  172. package/src/scripts/hooks/run_continuation_hook.ts +887 -68
  173. package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
  174. package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
  175. package/src/scripts/hooks/state_io.ts +782 -20
  176. package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
  177. package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
  178. package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
  179. package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
  180. package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
  181. package/src/scripts/install.sh +12 -2
  182. package/src/scripts/install.ts +16 -11
  183. package/src/scripts/language_mirror_hook.ts +561 -25
  184. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  185. package/src/scripts/lint_plan_risk_register.ts +4 -3
  186. package/src/scripts/lint_skill_router_head.ts +4 -4
  187. package/src/scripts/mcp_server/tools.ts +2 -2
  188. package/src/scripts/memory_consolidation.ts +189 -0
  189. package/src/scripts/memory_eviction.ts +384 -0
  190. package/src/scripts/new_roadmap.ts +136 -0
  191. package/src/scripts/pack_mcp_content.ts +20 -5
  192. package/src/scripts/release.ts +37 -38
  193. package/src/scripts/report_layer_overlap.ts +78 -0
  194. package/src/scripts/report_memory_pointers.ts +622 -0
  195. package/src/scripts/self_repair_hook.ts +37 -5
  196. package/src/scripts/session_register_hook.ts +115 -15
  197. package/src/scripts/skill_usage_collect.ts +11 -1
  198. package/src/scripts/skill_usage_report.ts +278 -8
  199. package/src/scripts/sync_pr_branch.ts +7 -0
  200. package/src/server/schemas/settings.ts +8 -0
  201. package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
  202. package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
  203. /package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
@@ -22,11 +22,17 @@
22
22
  * instead of one entry.
23
23
  * - **No resolve on failure.** A non-zero exit reports and changes nothing.
24
24
  * A gate marked resolved by a command that failed is worse than an open one.
25
- * - **No invented ledger.** Class 1 needs a standing budget, and the shape of
26
- * that budget is an OPEN maintainer decision (`b-gate-budget-preauth`).
27
- * Until it is taken there is no ledger, so class 1 takes the
28
- * render-instead-of-run path — which is the behaviour the blocker itself
29
- * prescribes for a missing ledger, not a stub of the decision.
25
+ * - **No spend outside the standing budget.** Class 1 runs only under the
26
+ * caps `b-gate-budget-preauth` decided (option (a), 2026-08-20): a per-run
27
+ * cap AND a rolling-7-day cap, with an append-only receipt as the audit
28
+ * surface. No caps configured, no USD estimate in the entry, over either
29
+ * cap — every one of those RENDERS instead of running, which is the
30
+ * behaviour the blocker itself prescribes for a missing ledger.
31
+ * - **No consent from the ledger.** The receipt records CONSUMPTION only.
32
+ * `agents/runtime/state/` is agent-writable, so reading an "authorisation"
33
+ * out of it would be the agent consenting on the user's behalf — the exact
34
+ * threat model the live-trigger-eval terminal abort exists for. `--confirm`
35
+ * remains the consent on every class-1 run.
30
36
  * - **No class-3 change.** The class-3 branch executes nothing and writes
31
37
  * nothing, so `agent-config gates` renders a class-3 entry exactly as it did
32
38
  * before this module existed. `--execute` on one returns a one-line notice
@@ -35,8 +41,9 @@
35
41
  * markdown field. Without the flag the command is echoed and refused, so
36
42
  * the operator sees the exact string first — the this-turn, names-the-object
37
43
  * confirmation `non-destructive-by-default` requires. A command carrying a
38
- * Hard-Floor action is refused even WITH the flag: class 0 means reversible,
39
- * so such an entry is misclassified rather than merely dangerous.
44
+ * Hard-Floor action is refused even WITH the flag: classes 0 and 1 are both
45
+ * defined as reversible, so such an entry is misclassified rather than
46
+ * merely dangerous.
40
47
  */
41
48
 
42
49
  import * as fs from 'node:fs';
@@ -44,15 +51,24 @@ import * as path from 'node:path';
44
51
  import { spawnSync } from 'node:child_process';
45
52
 
46
53
  import { collect, blocker_class, type Blocker } from './update_roadmap_progress.js';
54
+ import {
55
+ appendGateBudgetReceipt,
56
+ evaluateGateBudget,
57
+ GATE_BUDGET_LEDGER_REL,
58
+ parseBudgetUsd,
59
+ readGateBudgetCaps,
60
+ readGateBudgetLedger,
61
+ } from './gate_budget.js';
62
+ import { load_agent_settings } from '../templates/scripts/work_engine/_lib/agent_settings.js';
47
63
 
48
64
  /**
49
- * Where a class-1 receipt ledger would live once the budget decision is taken.
65
+ * Where the class-1 receipt ledger lives.
50
66
  *
51
- * Named here so the absent-ledger branch has something concrete to report, and
52
- * deliberately NOT created: the path is a consequence of the decision, and
53
- * writing it now would answer a question that is still the maintainer's.
67
+ * Kept as a name here because it was already exported, but the definition
68
+ * moved to `gate_budget.ts` when the budget decision landed — one path, one
69
+ * builder, so a relocation cannot leave a second joiner behind.
54
70
  */
55
- const LEDGER_REL = path.join('agents', 'runtime', 'state', 'gate-budget-ledger.jsonl');
71
+ const LEDGER_REL = GATE_BUDGET_LEDGER_REL;
56
72
 
57
73
  type Outcome = 'resolved' | 'failed' | 'rendered' | 'not-found';
58
74
 
@@ -260,6 +276,7 @@ function execute(
260
276
  }
261
277
  const { blocker, file, rel } = found;
262
278
  const cls = blocker_class(blocker);
279
+ const repoRoot = path.dirname(path.dirname(roadmapRoot));
263
280
 
264
281
  if (cls === '3') {
265
282
  // Byte-identical to today by construction: nothing is executed and
@@ -273,28 +290,32 @@ function execute(
273
290
  if (cls === '2') {
274
291
  return { outcome: 'rendered', report: consentLine(blocker), code: 0 };
275
292
  }
293
+ // Class 1 runs under the standing budget decided by `b-gate-budget-preauth`
294
+ // option (a): a per-run cap AND a rolling-7-day cap, with an append-only
295
+ // receipt as the audit surface. Every refusal path RENDERS rather than
296
+ // running — the blocker's own prescription for a missing ledger,
297
+ // generalised to every way the budget can say no. The `--confirm`
298
+ // requirement below is untouched: the caps bound the size of an authorised
299
+ // spend, they never supply the authorisation.
300
+ let budgeted: { estimateUsd: number } | null = null;
276
301
  if (cls === '1') {
277
- const ledger = path.join(path.dirname(path.dirname(roadmapRoot)), LEDGER_REL);
278
- if (!fs.existsSync(ledger)) {
279
- // The blocker's own prescription for a missing ledger: render the
280
- // consent line INSTEAD of running. Not a degraded mode — the
281
- // budget shape is an open decision and this is what open means.
302
+ const verdict = evaluateGateBudget({
303
+ caps: readGateBudgetCaps(load_agent_settings({ cwd: repoRoot })),
304
+ records: readGateBudgetLedger(repoRoot),
305
+ estimateUsd: parseBudgetUsd(blocker.budget),
306
+ now,
307
+ });
308
+ if (!verdict.ok) {
282
309
  return {
283
310
  outcome: 'rendered',
284
311
  report: consentLine(
285
312
  blocker,
286
- 'class 1 with no budget ledger, so it renders instead of running',
313
+ `class 1 and ${verdict.detail}, so it renders instead of running`,
287
314
  ),
288
315
  code: 0,
289
316
  };
290
317
  }
291
- return {
292
- outcome: 'rendered',
293
- report:
294
- `${id} is class 1 and a ledger exists at ${LEDGER_REL}, but spend execution ` +
295
- 'is gated on `b-gate-budget-preauth`, which is open. Rendering instead of running.\n',
296
- code: 0,
297
- };
318
+ budgeted = { estimateUsd: verdict.estimateUsd };
298
319
  }
299
320
 
300
321
  const command = commandOf(blocker);
@@ -304,7 +325,7 @@ function execute(
304
325
  // and guessing a command here is the one thing that must not happen.
305
326
  return {
306
327
  outcome: 'failed',
307
- report: `${id} is class 0 but carries no **Run:** command. Nothing was executed.\n`,
328
+ report: `${id} is class ${cls} but carries no **Run:** command. Nothing was executed.\n`,
308
329
  code: 1,
309
330
  };
310
331
  }
@@ -318,9 +339,9 @@ function execute(
318
339
  return {
319
340
  outcome: 'failed',
320
341
  report:
321
- `${id} is class 0 but its **Run:** command contains \`${floor}\`, which is a ` +
322
- 'Hard-Floor action under non-destructive-by-default. Class 0 means ' +
323
- 'deterministic, free and REVERSIBLE, so this entry is misclassified. ' +
342
+ `${id} is class ${cls} but its **Run:** command contains \`${floor}\`, which is a ` +
343
+ 'Hard-Floor action under non-destructive-by-default. Classes 0 and 1 are ' +
344
+ 'both defined as REVERSIBLE, so this entry is misclassified. ' +
324
345
  'Nothing was executed; re-author the class.\n',
325
346
  code: 1,
326
347
  };
@@ -333,7 +354,7 @@ function execute(
333
354
  return {
334
355
  outcome: 'rendered',
335
356
  report:
336
- `${id} is class 0 and would run, from the repo root:\n\n` +
357
+ `${id} is class ${cls} and would run, from the repo root:\n\n` +
337
358
  ` ${command}\n\n` +
338
359
  'Nothing has been executed. Re-run with `--confirm` to execute it and ' +
339
360
  'record the evidence at the blocker.\n',
@@ -344,7 +365,7 @@ function execute(
344
365
  const r = spawnSync(command, {
345
366
  shell: true,
346
367
  encoding: 'utf-8',
347
- cwd: path.dirname(path.dirname(roadmapRoot)),
368
+ cwd: repoRoot,
348
369
  timeout: 120_000,
349
370
  });
350
371
  const output = `${r.stdout ?? ''}${r.stderr ?? ''}`;
@@ -367,6 +388,22 @@ function execute(
367
388
  code: 1,
368
389
  };
369
390
  }
391
+ if (budgeted !== null) {
392
+ // Written BEFORE the file rewrite: an unreceipted spend is invisible to
393
+ // the rolling cap, and the cap failing open is the one way this
394
+ // mechanism could become a blank cheque. `actual_usd` is null because
395
+ // nothing on this path observes the provider's billed amount — the
396
+ // estimate is what the cap compared against and what it records.
397
+ appendGateBudgetReceipt(repoRoot, {
398
+ kind: 'consumption',
399
+ blocker: id,
400
+ authorization: 'confirm-flag',
401
+ estimated_usd: budgeted.estimateUsd,
402
+ actual_usd: null,
403
+ at: now.toISOString(),
404
+ single_use: true,
405
+ });
406
+ }
370
407
  const when = today(now);
371
408
  if (!appendEvidence(file, id, command, output, when)) {
372
409
  return {
@@ -382,6 +419,10 @@ function execute(
382
419
  report:
383
420
  `${id} resolved. Ran \`${command}\` (exit 0); evidence appended in ${rel} ` +
384
421
  `and the status flipped to resolved ${when}.\n` +
422
+ (budgeted === null
423
+ ? ''
424
+ : `Receipt appended to ${LEDGER_REL} ` +
425
+ `($${budgeted.estimateUsd.toFixed(2)} estimated).\n`) +
385
426
  // The dashboard is derived from the file this just rewrote, so it
386
427
  // is now stale. Saying so is the same follow-up `renderResumed`
387
428
  // already prints for its own file-move suggestion.
@@ -427,13 +427,83 @@ export function sheetQuestion(b: Blocker): { text: string; source: 'question' |
427
427
  * none is recorded it says so and names what the reader must supply, so
428
428
  * regenerating the sheet cannot silently overwrite an answer with a guess.
429
429
  */
430
- function renderSheet(entries: readonly Entry[], now: Date): string {
430
+ /**
431
+ * The recorded answer to the consolidated decision sheet.
432
+ *
433
+ * The sheet is DERIVED, so an answer written into it is lost on the next run —
434
+ * the file says so in its own header. But `road-to-estate-drawdown` blocker
435
+ * `b-consolidated-decision-sheet` resolves only when "the sheet records which
436
+ * option was used", and a derived file cannot carry that by being edited. So the
437
+ * option lives in a NON-derived sibling and the sheet reads it, which keeps the
438
+ * sheet deterministic and still lets it state the answer.
439
+ *
440
+ * Read, never written, and absent is a first-class answer: no marker means the
441
+ * sheet renders exactly as before, so this is additive to every existing caller.
442
+ */
443
+ interface SheetAnswer {
444
+ option: string;
445
+ answered: string;
446
+ authority: string;
447
+ }
448
+
449
+ const SHEET_ANSWER_REL = path.join('agents', 'decisions', 'consolidated-decision-sheet-answer.md');
450
+
451
+ /**
452
+ * One line, three fields, all required — a partial marker is treated as absent
453
+ * rather than rendered half-filled, because a header that named an option with
454
+ * no date or authority would be less honest than one that says nothing.
455
+ */
456
+ const SHEET_ANSWER_RE =
457
+ /<!--\s*sheet-answer:\s*(.+?)\s*\|\s*answered:\s*(\d{4}-\d{2}-\d{2})\s*\|\s*authority:\s*(\S+)\s*-->/;
458
+
459
+ export function readSheetAnswer(repoRoot: string): SheetAnswer | null {
460
+ const file = path.join(repoRoot, SHEET_ANSWER_REL);
461
+ let text: string;
462
+ try {
463
+ text = fs.readFileSync(file, 'utf8');
464
+ } catch {
465
+ return null;
466
+ }
467
+ const m = SHEET_ANSWER_RE.exec(text);
468
+ if (m === null) {
469
+ return null;
470
+ }
471
+ return {
472
+ option: (m[1] as string).trim(),
473
+ answered: (m[2] as string).trim(),
474
+ authority: (m[3] as string).trim(),
475
+ };
476
+ }
477
+
478
+ /** The header block naming the recorded answer, or nothing when none is recorded. */
479
+ function answerBanner(answer: SheetAnswer | null): string[] {
480
+ if (answer === null) {
481
+ return [];
482
+ }
483
+ return [
484
+ '>',
485
+ `> **ANSWERED ${answer.answered} — ${answer.option}.**`,
486
+ `> Recorded in \`${SHEET_ANSWER_REL}\` (not derived);`,
487
+ `> authority \`${answer.authority}\`.`,
488
+ '> The rows below are the ones still OPEN: an answered',
489
+ '> row stays here until the work its own entry names is done, so this is a work',
490
+ '> queue and not a count of unanswered questions.',
491
+ ];
492
+ }
493
+
494
+ function renderSheet(
495
+ entries: readonly Entry[],
496
+ now: Date,
497
+ answer: SheetAnswer | null = null,
498
+ ): string {
431
499
  const mine = entries.filter((e) => needsUser(e.blocker.owner));
432
500
  const stamp = now.toISOString().slice(0, 10);
433
501
  if (mine.length === 0) {
502
+ const banner = answerBanner(answer);
434
503
  return (
435
504
  `# Consolidated decision sheet — nothing owned by you (${stamp})\n\n` +
436
- 'No open blocker in the active roadmap tree carries `Owner: user`. Nothing to answer.\n'
505
+ 'No open blocker in the active roadmap tree carries `Owner: user`. Nothing to answer.\n' +
506
+ (banner.length === 0 ? '' : '\n' + banner.slice(1).join('\n') + '\n')
437
507
  );
438
508
  }
439
509
 
@@ -461,6 +531,7 @@ function renderSheet(entries: readonly Entry[], now: Date): string {
461
531
  '> This file is DERIVED — every line above and below comes from the roadmaps themselves,',
462
532
  '> so regenerating is deterministic and an answer written into this file would be lost.',
463
533
  '> Answers go back into each roadmap at its own blocker; the agent does that.',
534
+ ...answerBanner(answer),
464
535
  '',
465
536
  '| # | Decision | Roadmap | Unblocks | Default source |',
466
537
  '|---:|---|---|---:|---|',
@@ -847,7 +918,7 @@ function main(argv?: readonly string[]): number {
847
918
  const resumed = reply || sheet ? [] : probeLater(roadmapRoot, repoRoot);
848
919
  process.stdout.write(
849
920
  sheet
850
- ? renderSheet(entries, new Date())
921
+ ? renderSheet(entries, new Date(), readSheetAnswer(repoRoot))
851
922
  : reply
852
923
  ? renderReply(entries)
853
924
  : json
@@ -32,8 +32,12 @@
32
32
  *
33
33
  * Invocation (from project root):
34
34
  * node node_modules/.bin/tsx .augment/scripts/update_roadmap_progress.ts # rewrite
35
+ * node node_modules/.bin/tsx .augment/scripts/update_roadmap_progress.ts --archive # rewrite + archive completed
35
36
  * node node_modules/.bin/tsx .augment/scripts/update_roadmap_progress.ts --check # CI: exit 1 if stale
36
37
  *
38
+ * `--archive` is opt-in and `--check` refuses it; the full contract, the
39
+ * council record and the sweep spawner live in `archival_sweep.ts`.
40
+ *
37
41
  * --- Parity notes (ADR-200) ---
38
42
  *
39
43
  * - Python `round()` is banker's rounding (round-half-to-even). Percentages and
@@ -59,6 +63,7 @@ import { spawnSync } from 'node:child_process';
59
63
  import * as fs from 'node:fs';
60
64
  import * as path from 'node:path';
61
65
  import { fileURLToPath, pathToFileURL } from 'node:url';
66
+ import { run_archival_sweep } from './archival_sweep.js';
62
67
  import type * as YamlModule from 'yaml';
63
68
 
64
69
  const _HERE = fileURLToPath(import.meta.url);
@@ -280,12 +285,21 @@ function is_draft(fm: Record<string, string>): boolean {
280
285
  return DRAFT_VALUES.has((fm['status'] ?? '').toLowerCase());
281
286
  }
282
287
 
283
- function is_roadmap_candidate(p: string): boolean {
284
- const name = path.basename(p);
288
+ /**
289
+ * The NAME half of the candidate test. Split out because the parked inventory
290
+ * reads `later/` on purpose and `is_roadmap_candidate` rejects any path carrying
291
+ * an excluded component — `later` included. Sharing this half keeps
292
+ * `later/README.md` excluded from both sides by one rule rather than two.
293
+ */
294
+ function is_roadmap_name(name: string): boolean {
285
295
  if (EXCLUDE_NAMES.has(name)) {
286
296
  return false;
287
297
  }
288
- if (EXCLUDE_PREFIXES.some((pre) => name.startsWith(pre))) {
298
+ return !EXCLUDE_PREFIXES.some((pre) => name.startsWith(pre));
299
+ }
300
+
301
+ function is_roadmap_candidate(p: string): boolean {
302
+ if (!is_roadmap_name(path.basename(p))) {
289
303
  return false;
290
304
  }
291
305
  // path.parts — every component of the path; exclude if any is an excluded dir.
@@ -741,6 +755,140 @@ function collect(roadmap_root: string): RoadmapStats[] {
741
755
  return results;
742
756
  }
743
757
 
758
+ /** One parked roadmap, as the inventory needs it. */
759
+ interface ParkedRoadmap {
760
+ rel: string;
761
+ resume: string | null;
762
+ /** Only the linter's looser marker matched — CI passes, the cell cannot quote. */
763
+ unlabelled: boolean;
764
+ /** Open blockers still carried by the parked file. */
765
+ open_blockers: number;
766
+ /** Of those, the ones whose owner is the user. */
767
+ needs_user: number;
768
+ }
769
+
770
+ /**
771
+ * A line that STATES when the roadmap comes back. Two tiers exist because
772
+ * `lint_roadmap_later_disposition` accepts a bare `trigger`, which is too loose to
773
+ * quote from — measured over the live tree it matched a `Source:` path containing
774
+ * `mixed-trigger-cleanup`, producing cells that were wrong rather than short.
775
+ */
776
+ const RESUME_STATEMENT = /\b(blocked until|resume when|resume-when|blocked-until)\b/i;
777
+
778
+ /**
779
+ * The linter's looser vocabulary. A file matching only this DOES carry a resume
780
+ * condition as far as CI is concerned, so reporting it as missing would make the
781
+ * dashboard contradict a gate that already passed.
782
+ */
783
+ const RESUME_LOOSE = /\btrigger\b/i;
784
+
785
+ /**
786
+ * Parked roadmaps, with the resume condition each one records.
787
+ *
788
+ * A view over existing data, NOT a new governance mechanism: nothing here gates,
789
+ * counts toward a ratchet, or changes what `collect()` scans. It exists because
790
+ * `later/` is excluded from `collect()` and from `/roadmap:process-*` — correct,
791
+ * parked work is not backlog — and that exclusion had no counterweight, so a
792
+ * roadmap moved there left the dashboard entirely.
793
+ */
794
+ function collect_parked(roadmap_root: string): ParkedRoadmap[] {
795
+ const dir = path.join(roadmap_root, 'later');
796
+ let names: string[];
797
+ try {
798
+ names = fs.readdirSync(dir).sort();
799
+ } catch {
800
+ return [];
801
+ }
802
+ const out: ParkedRoadmap[] = [];
803
+ for (const name of names) {
804
+ const full = path.join(dir, name);
805
+ // The NAME half only: the full predicate rejects every path under `later`.
806
+ if (!_isFile(full) || !is_roadmap_name(name)) {
807
+ continue;
808
+ }
809
+ let text: string;
810
+ try {
811
+ text = fs.readFileSync(full, { encoding: 'utf-8' });
812
+ } catch {
813
+ continue;
814
+ }
815
+ if (is_draft(parse_frontmatter(text))) {
816
+ continue;
817
+ }
818
+ const cell = resume_cell(text);
819
+ // Parked blockers are still open blockers — counting them here is what stops
820
+ // this section becoming the surface where a park reads as a resolution.
821
+ const open = parse_blockers(text).filter((b) => !blocker_is_resolved(b));
822
+ out.push({
823
+ rel: name,
824
+ resume: cell.stated,
825
+ unlabelled: cell.unlabelled,
826
+ open_blockers: open.length,
827
+ needs_user: open.filter((b) => blocker_needs_user(b.owner)).length,
828
+ });
829
+ }
830
+ return out;
831
+ }
832
+
833
+ /**
834
+ * What brings a parked roadmap back, as one table cell. Both fields false means the
835
+ * file records nothing — said outright, because an empty cell reads as "nothing to
836
+ * wait for".
837
+ *
838
+ * The PARAGRAPH is joined, not the matched line: roadmap prose wraps at ~80
839
+ * columns, and quoting one line ended 39 of 52 cells mid-clause. A cell stopping at
840
+ * "the pair for" is not a shorter sentence, it is a different claim.
841
+ *
842
+ * Both tiers are tested over the SAME lines. Testing the loose one whole-file while
843
+ * the strict one ran per line made `unlabelled` fire on any file using the word
844
+ * "trigger" anywhere.
845
+ */
846
+ function resume_cell(text: string): { stated: string | null; unlabelled: boolean } {
847
+ const lines = text.split('\n');
848
+ for (let i = 0; i < lines.length; i++) {
849
+ const raw = lines[i] as string;
850
+ if (!RESUME_STATEMENT.test(raw)) {
851
+ continue;
852
+ }
853
+ const parts: string[] = [raw];
854
+ // Continuation lines of the same paragraph: non-blank, and not the start of
855
+ // a new block (list item, heading, fence, table row).
856
+ for (let j = i + 1; j < lines.length; j++) {
857
+ const next = (lines[j] as string).replace(/^\s*>\s?/, '').trim();
858
+ if (next === '' || /^([-*+]\s|#{1,6}\s|```|\||>\s*$)/.test(next)) {
859
+ break;
860
+ }
861
+ parts.push(next);
862
+ }
863
+ const joined = cell_text(parts.join(' '));
864
+ if (joined === '') {
865
+ continue;
866
+ }
867
+ return { stated: joined, unlabelled: false };
868
+ }
869
+ return { stated: null, unlabelled: lines.some((l) => RESUME_LOOSE.test(l)) };
870
+ }
871
+
872
+ /**
873
+ * One roadmap sentence, safe between two pipes. ORDER IS LOAD-BEARING: truncate
874
+ * before escaping, because cutting an escaped string severs a `\|` and leaves a
875
+ * dangling backslash. HTML comments are removed, not escaped — a `<!--` reaching a
876
+ * cell comments out the rest of the document, and one already did.
877
+ */
878
+ function cell_text(raw: string): string {
879
+ let out = raw
880
+ .replace(/^\s*>\s?/, '')
881
+ .replace(/<!--[\s\S]*?-->/g, '')
882
+ .replace(/<!--|-->/g, '')
883
+ .replace(/\*\*/g, '')
884
+ .replace(/\s+/g, ' ')
885
+ .trim();
886
+ if (out.length > 200) {
887
+ out = `${out.slice(0, 197).trimEnd()}...`;
888
+ }
889
+ return out.replace(/\|/g, '\\|');
890
+ }
891
+
744
892
  function unarchived_complete(roadmaps: RoadmapStats[]): RoadmapStats[] {
745
893
  return roadmaps.filter((r) => r.total_active > 0 && r.open_ === 0 && r.deferred === 0);
746
894
  }
@@ -812,7 +960,7 @@ function collect_bundles(repo_root: string): Bundle[] {
812
960
  return out;
813
961
  }
814
962
 
815
- function render(roadmaps: RoadmapStats[], bundles: Bundle[] | null = null): string {
963
+ function render(roadmaps: RoadmapStats[], bundles: Bundle[] | null, roadmap_root: string): string {
816
964
  const total_done = roadmaps.reduce((s, r) => s + r.done, 0);
817
965
  const total_active = roadmaps.reduce((s, r) => s + r.total_active, 0);
818
966
  const overall_pct = total_active ? _pyRound((total_done * 100) / total_active) : 0;
@@ -836,6 +984,7 @@ function render(roadmaps: RoadmapStats[], bundles: Bundle[] | null = null): stri
836
984
  '[skipped/](roadmaps/skipped/) · [later/](roadmaps/later/)' +
837
985
  (total_open_blockers > 0
838
986
  ? ` · **${total_open_blockers}** open blocker${total_open_blockers !== 1 ? 's' : ''}` +
987
+ ' in the active tree' +
839
988
  (user_open_blockers > 0
840
989
  ? `, **${user_open_blockers}** need you → \`agent-config gates\``
841
990
  : '')
@@ -939,6 +1088,58 @@ function render(roadmaps: RoadmapStats[], bundles: Bundle[] | null = null): stri
939
1088
  });
940
1089
  lines.push('');
941
1090
  lines.push('---\n');
1091
+
1092
+ // Listed under the active table so the two are read together. Parked work
1093
+ // carries no counts here; what a reader needs is what brings it back.
1094
+ const parked = collect_parked(roadmap_root);
1095
+ if (parked.length > 0) {
1096
+ lines.push(
1097
+ `## Parked — \`later/\` (${parked.length} roadmap` +
1098
+ `${parked.length !== 1 ? 's' : ''}, not active backlog)\n`,
1099
+ );
1100
+ lines.push(
1101
+ '> Excluded from the table above and from `/roadmap:process-*` by ' +
1102
+ 'design. Listed here so a resume condition is visible without ' +
1103
+ 'opening the file.\n',
1104
+ );
1105
+ const parked_open = parked.reduce((n, r) => n + r.open_blockers, 0);
1106
+ const parked_user = parked.reduce((n, r) => n + r.needs_user, 0);
1107
+ if (parked_open > 0) {
1108
+ lines.push(
1109
+ `> Carrying **${parked_open}** open blocker` +
1110
+ `${parked_open !== 1 ? 's' : ''}` +
1111
+ (parked_user > 0 ? `, **${parked_user}** owned by you` : '') +
1112
+ ' — parking resolves nothing, so these are NOT in the ' +
1113
+ 'active-tree count above.\n',
1114
+ );
1115
+ }
1116
+ lines.push('| Roadmap | Open blockers | Resume when |');
1117
+ lines.push('|---|---:|---|');
1118
+ for (const r of parked) {
1119
+ const cell =
1120
+ r.resume ??
1121
+ (r.unlabelled
1122
+ ? '_condition present but unlabelled — see file_'
1123
+ : '_no resume line recorded_');
1124
+ const blockers =
1125
+ r.open_blockers === 0
1126
+ ? '0'
1127
+ : `${r.open_blockers}${r.needs_user > 0 ? ` (${r.needs_user} you)` : ''}`;
1128
+ // `<!-- ref-ignore -->` per row — a correctness fix, not a suppression. A
1129
+ // resume condition NAMES WHAT DOES NOT EXIST YET ("when `x.md` exists" is
1130
+ // the commonest shape in `later/`), so checking these quoted paths as live
1131
+ // references fires the gate on correct content. It fired immediately. The
1132
+ // authoritative copy of each path is the roadmap file; this table quotes
1133
+ // it, and that gate does not walk `later/` either way.
1134
+ lines.push(
1135
+ `| [${r.rel}](roadmaps/later/${r.rel}) | ${blockers} | ${cell} |` +
1136
+ ' <!-- ref-ignore -->',
1137
+ );
1138
+ }
1139
+ lines.push('');
1140
+ lines.push('---\n');
1141
+ }
1142
+
942
1143
  lines.push('## Per-roadmap phase breakdown\n');
943
1144
  for (const r of roadmaps) {
944
1145
  lines.push(`### [${r.rel}](roadmaps/${r.rel})\n`);
@@ -1013,16 +1214,18 @@ class ArgparseExit extends Error {
1013
1214
  const _PROG = 'update_roadmap_progress.py';
1014
1215
 
1015
1216
  function _usage(): string {
1016
- return `usage: ${_PROG} [-h] [--check] [--repo-root REPO_ROOT]\n`;
1217
+ return `usage: ${_PROG} [-h] [--check] [--archive | --no-archive] [--repo-root REPO_ROOT]\n`;
1017
1218
  }
1018
1219
 
1019
1220
  interface Args {
1020
1221
  check: boolean;
1222
+ archive: boolean;
1021
1223
  repo_root: string;
1022
1224
  }
1023
1225
 
1024
1226
  function _parseArgs(argv: readonly string[]): Args {
1025
1227
  let check = false;
1228
+ let archive = false;
1026
1229
  let repo_root = process.cwd();
1027
1230
  const emitError = (msg: string): never => {
1028
1231
  process.stderr.write(_usage());
@@ -1038,6 +1241,12 @@ function _parseArgs(argv: readonly string[]): Args {
1038
1241
  } else if (tok === '--check') {
1039
1242
  check = true;
1040
1243
  i += 1;
1244
+ } else if (tok === '--archive') {
1245
+ archive = true;
1246
+ i += 1;
1247
+ } else if (tok === '--no-archive') {
1248
+ archive = false;
1249
+ i += 1;
1041
1250
  } else if (tok === '--repo-root') {
1042
1251
  const val = argv[i + 1];
1043
1252
  if (val === undefined) {
@@ -1052,7 +1261,13 @@ function _parseArgs(argv: readonly string[]): Args {
1052
1261
  emitError(`unrecognized arguments: ${tok}`);
1053
1262
  }
1054
1263
  }
1055
- return { check, repo_root };
1264
+ if (check && archive) {
1265
+ // A gate that mutates the tree it is checking cannot be trusted by CI
1266
+ // (council 2026-08-20, both seats). Refuse the combination outright
1267
+ // rather than silently letting one win.
1268
+ emitError('argument --archive: not allowed with --check');
1269
+ }
1270
+ return { check, archive, repo_root };
1056
1271
  }
1057
1272
 
1058
1273
  /**
@@ -1094,8 +1309,31 @@ function main(argv?: readonly string[]): number {
1094
1309
  process.stdout.write(`ℹ️ No roadmaps directory at ${roadmap_root} — nothing to do.\n`);
1095
1310
  return 0;
1096
1311
  }
1312
+ // Archive before rendering, so the dashboard describes the tree the sweep
1313
+ // leaves behind rather than the one it found. `--check` never archives: a
1314
+ // gate that mutates the tree it is checking cannot be trusted by CI.
1315
+ let sweep_out = '';
1316
+ if (!args.check && args.archive && unarchived_complete(collect(roadmap_root)).length > 0) {
1317
+ const swept = run_archival_sweep(repo_root);
1318
+ sweep_out = swept.stdout;
1319
+ if (swept.stderr) {
1320
+ process.stderr.write(swept.stderr);
1321
+ }
1322
+ if (!swept.ok) {
1323
+ // A half-finished sweep leaves the tree in a shape the dashboard
1324
+ // would describe as if it were intentional. Report and stop before
1325
+ // writing (council 2026-08-20: "archival failures must prevent
1326
+ // dashboard rendering so partially updated state is not presented
1327
+ // as current").
1328
+ process.stderr.write(
1329
+ '❌ The archival sweep failed — dashboard NOT regenerated. ' +
1330
+ 'Fix the tree, then re-run.\n',
1331
+ );
1332
+ return 1;
1333
+ }
1334
+ }
1097
1335
  const roadmaps = collect(roadmap_root);
1098
- const new_text = render(roadmaps, collect_bundles(repo_root));
1336
+ const new_text = render(roadmaps, collect_bundles(repo_root), roadmap_root);
1099
1337
  const current = fs.existsSync(target) ? fs.readFileSync(target, { encoding: 'utf-8' }) : '';
1100
1338
  const complete = unarchived_complete(roadmaps);
1101
1339
  const pending = pending_iron_law_3(roadmaps);
@@ -1166,7 +1404,13 @@ function main(argv?: readonly string[]): number {
1166
1404
  `${roadmaps.length} roadmap(s) · ` +
1167
1405
  `${roadmaps.reduce((s, r) => s + r.done, 0)}/${roadmaps.reduce((s, r) => s + r.total_active, 0)} steps done.\n`,
1168
1406
  );
1407
+ if (sweep_out) {
1408
+ process.stdout.write(sweep_out);
1409
+ }
1169
1410
  if (complete.length) {
1411
+ // Whatever survived the sweep is complete but NOT archivable — the
1412
+ // sweep already said why on stderr (an open blocker outlives its
1413
+ // steps). Without `--archive` this is every complete roadmap.
1170
1414
  process.stderr.write(
1171
1415
  '⚠️ Completed roadmaps not yet archived — move to ' +
1172
1416
  '`agents/roadmaps/archive/`:\n',