@event4u/agent-config 14.6.0 → 14.8.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 (258) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +255 -145
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/inbox.md +264 -10
  5. package/dist/agent-src/commands/analyze.md +1 -1
  6. package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
  7. package/dist/agent-src/commands/evals/roadmap.json +17 -1
  8. package/dist/agent-src/commands/pr/create.md +14 -2
  9. package/dist/agent-src/commands/pr/merge.md +379 -0
  10. package/dist/agent-src/commands/roadmap/process-full.md +272 -32
  11. package/dist/agent-src/commands/roadmap.md +1 -1
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
  14. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +95 -2
  15. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +289 -106
  16. package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
  17. package/dist/agent-src/rules/context-hygiene.md +2 -0
  18. package/dist/agent-src/rules/decision-revisit-gate.md +67 -32
  19. package/dist/agent-src/rules/fix-what-you-see.md +108 -0
  20. package/dist/agent-src/rules/no-roadmap-references.md +0 -5
  21. package/dist/agent-src/rules/recurring-criticism.md +110 -0
  22. package/dist/agent-src/rules/roadmap-progress-sync.md +20 -1
  23. package/dist/agent-src/rules/rule-type-governance.md +0 -2
  24. package/dist/agent-src/rules/self-repair-loop.md +17 -0
  25. package/dist/agent-src/rules/skill-quality.md +0 -2
  26. package/dist/agent-src/rules/source-confidentiality.md +37 -7
  27. package/dist/agent-src/scripts/archival_sweep.ts +117 -0
  28. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
  29. package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
  30. package/dist/agent-src/scripts/gate_budget.ts +282 -0
  31. package/dist/agent-src/scripts/gate_execute.ts +72 -31
  32. package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
  33. package/dist/agent-src/scripts/update_roadmap_progress.ts +296 -53
  34. package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
  35. package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
  36. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
  37. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
  38. package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
  39. package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
  40. package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
  41. package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
  42. package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
  43. package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
  44. package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
  45. package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
  46. package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
  47. package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
  48. package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
  49. package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
  50. package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
  51. package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
  52. package/dist/agent-src/skills/security/SKILL.md +8 -0
  53. package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
  54. package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
  55. package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
  56. package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
  57. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
  58. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  59. package/dist/agent-src/templates/roadmaps.md +65 -4
  60. package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  61. package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  62. package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  63. package/dist/cli/registry.js +2 -1
  64. package/dist/cli/registry.js.map +1 -1
  65. package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
  66. package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-7PRE6ZV2.js} +338 -0
  67. package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
  68. package/dist/cli-delegate/chunk-E4XIZDIB.js +402 -0
  69. package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-EEJUMCDZ.js} +16 -16
  70. package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
  71. package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-H4VIVD4C.js} +385 -284
  72. package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
  73. package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-LPPWO5CE.js} +12 -8
  74. package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
  75. package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
  76. package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-PMEKGDED.js} +74 -167
  77. package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
  78. package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
  79. package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
  80. package/dist/cli-delegate/cmd_conformance.js +466 -716
  81. package/dist/cli-delegate/cmd_converge.js +3 -3
  82. package/dist/cli-delegate/cmd_doctor.js +10 -10
  83. package/dist/cli-delegate/cmd_explain.js +1 -1
  84. package/dist/cli-delegate/cmd_export.js +10 -9
  85. package/dist/cli-delegate/cmd_handoff.js +10 -9
  86. package/dist/cli-delegate/cmd_memory_get.js +2 -1
  87. package/dist/cli-delegate/cmd_migrate.js +2 -2
  88. package/dist/cli-delegate/cmd_packs_active.js +1 -1
  89. package/dist/cli-delegate/cmd_preflight.js +5 -5
  90. package/dist/cli-delegate/cmd_prune.js +11 -10
  91. package/dist/cli-delegate/cmd_route_audit.js +6 -5
  92. package/dist/cli-delegate/cmd_route_explain.js +3 -3
  93. package/dist/cli-delegate/cmd_session_recycle.js +840 -30
  94. package/dist/cli-delegate/cmd_settings_get.js +3 -3
  95. package/dist/cli-delegate/cmd_settings_migrate.js +10 -9
  96. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  97. package/dist/cli-delegate/cmd_sync.js +11 -10
  98. package/dist/cli-delegate/cmd_uninstall.js +13 -11
  99. package/dist/cli-delegate/cmd_update.js +4 -4
  100. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  101. package/dist/cli-delegate/cmd_validate.js +11 -10
  102. package/dist/cli-delegate/cmd_versions.js +3 -3
  103. package/dist/discovery/deprecation-report.md +1 -1
  104. package/dist/discovery/discovery-manifest.json +111 -37
  105. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  106. package/dist/discovery/discovery-manifest.summary.md +5 -5
  107. package/dist/discovery/dormancy-report.md +1 -1
  108. package/dist/discovery/orphan-report.md +1 -1
  109. package/dist/discovery/packs.json +15 -12
  110. package/dist/discovery/trust-report.md +3 -3
  111. package/dist/discovery/workspaces.json +11 -8
  112. package/dist/hooks/dispatch.js +3165 -2192
  113. package/dist/install/claudePathsPlan.js +187 -0
  114. package/dist/install/claudePathsPlan.js.map +1 -0
  115. package/dist/install/globalRuleLayers.js +169 -0
  116. package/dist/install/globalRuleLayers.js.map +1 -0
  117. package/dist/install/hostLayerFingerprint.js +136 -0
  118. package/dist/install/hostLayerFingerprint.js.map +1 -0
  119. package/dist/install/install.mjs +553 -425
  120. package/dist/install/partitionEligibility.js +432 -0
  121. package/dist/install/partitionEligibility.js.map +1 -0
  122. package/dist/install/ruleLayerPartition.js +157 -0
  123. package/dist/install/ruleLayerPartition.js.map +1 -0
  124. package/dist/mcp/registry-manifest.json +3 -3
  125. package/dist/mcp/server.json +2 -2
  126. package/dist/mcp/server.mjs +1863 -1234
  127. package/dist/router.json +1 -1
  128. package/dist/scripts/_lib/agent_src.js +41 -0
  129. package/dist/scripts/_lib/agent_src.js.map +1 -1
  130. package/dist/scripts/_lib/install_layout.js +83 -0
  131. package/dist/scripts/_lib/install_layout.js.map +1 -0
  132. package/dist/scripts/_lib/installed_lock.js +444 -0
  133. package/dist/scripts/_lib/installed_lock.js.map +1 -0
  134. package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
  135. package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
  136. package/dist/scripts/condense.js +63 -251
  137. package/dist/scripts/condense.js.map +1 -1
  138. package/dist/server/schemas/settings.js +4 -0
  139. package/dist/server/schemas/settings.js.map +1 -1
  140. package/docs/contracts/settings-classes.md +12 -6
  141. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
  142. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
  143. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
  144. package/docs/guidelines/agent-infra/false-green.md +47 -1
  145. package/docs/guidelines/agent-infra/memory-access.md +17 -0
  146. package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  147. package/package.json +1 -1
  148. package/src/agent-src/scripts/archival_sweep.ts +117 -0
  149. package/src/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
  150. package/src/agent-src/scripts/dashboard_mode.ts +212 -0
  151. package/src/agent-src/scripts/gate_budget.ts +282 -0
  152. package/src/agent-src/scripts/gate_execute.ts +72 -31
  153. package/src/agent-src/scripts/roadmap_gates.ts +74 -3
  154. package/src/agent-src/scripts/update_roadmap_progress.ts +296 -53
  155. package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  156. package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  157. package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  158. package/src/config/agent-settings.template.yml +27 -2
  159. package/src/config/ci-local-parity.yml +39 -0
  160. package/src/config/estate-count-budget.json +612 -3
  161. package/src/config/evaluator-budgets.json +3 -3
  162. package/src/config/gate-coverage.yml +220 -12
  163. package/src/config/gate-violation-baselines.json +73 -63
  164. package/src/config/gitignore-block.txt +15 -2
  165. package/src/config/hook-latency-budget.json +5 -0
  166. package/src/config/rule-activation-census.json +14 -6
  167. package/src/config/rule-stub-ceilings.json +34 -7
  168. package/src/install/claudePathsPlan.ts +196 -0
  169. package/src/install/globalRuleLayers.ts +197 -0
  170. package/src/install/hostLayerFingerprint.ts +145 -0
  171. package/src/install/partitionEligibility.ts +502 -0
  172. package/src/install/ruleLayerPartition.ts +212 -0
  173. package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
  174. package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
  175. package/src/scripts/_dispatch.bash +17 -1
  176. package/src/scripts/_lib/ac_heading.ts +30 -0
  177. package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
  178. package/src/scripts/_lib/adr_frontmatter.ts +459 -0
  179. package/src/scripts/_lib/agent_src.ts +42 -0
  180. package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
  181. package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
  182. package/src/scripts/_lib/envelope_grounding.ts +49 -9
  183. package/src/scripts/_lib/installed_lock.ts +50 -5
  184. package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
  185. package/src/scripts/_lib/md_section.ts +129 -0
  186. package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
  187. package/src/scripts/_lib/scope_guard.sh +26 -3
  188. package/src/scripts/_lib/self_repair.ts +19 -0
  189. package/src/scripts/_lib/self_repair_class_b.ts +171 -0
  190. package/src/scripts/adr/evidence_census.ts +899 -0
  191. package/src/scripts/adr/regenerate_index.ts +12 -32
  192. package/src/scripts/adr_cite_check.ts +540 -39
  193. package/src/scripts/ai_council/cli_help.ts +55 -0
  194. package/src/scripts/audit_adr_coverage.ts +103 -17
  195. package/src/scripts/before_complete_hook.ts +153 -28
  196. package/src/scripts/bench_ab_v2_complexity.ts +27 -7
  197. package/src/scripts/bench_hook_latency.ts +204 -0
  198. package/src/scripts/build_archive_index.ts +34 -0
  199. package/src/scripts/check_adr_frontmatter.ts +574 -39
  200. package/src/scripts/check_hook_bundle_content.ts +219 -0
  201. package/src/scripts/check_new_adr_evidence.ts +529 -0
  202. package/src/scripts/check_references.ts +78 -0
  203. package/src/scripts/check_release_published.ts +32 -11
  204. package/src/scripts/check_rule_layer_partition.ts +531 -0
  205. package/src/scripts/check_single_delivery.ts +194 -17
  206. package/src/scripts/check_standing_rule_delivery.ts +8 -2
  207. package/src/scripts/ci_settle.ts +170 -0
  208. package/src/scripts/condense.ts +79 -259
  209. package/src/scripts/council_cli.ts +3 -3
  210. package/src/scripts/dispatch_r2_reviewer.ts +6 -14
  211. package/src/scripts/external_sources_denylist.json +4 -1
  212. package/src/scripts/hook_manifest.json +1 -1
  213. package/src/scripts/hook_manifest.yaml +77 -14
  214. package/src/scripts/hooks/block_no_verify.ts +61 -10
  215. package/src/scripts/hooks/block_unauthorized_git.ts +21 -1
  216. package/src/scripts/hooks/concern_registry.ts +4 -0
  217. package/src/scripts/hooks/dispatch_hook.ts +154 -187
  218. package/src/scripts/hooks/dispatch_issues.ts +38 -19
  219. package/src/scripts/hooks/fallback_yaml.ts +66 -0
  220. package/src/scripts/hooks/host_semantics.ts +21 -0
  221. package/src/scripts/hooks/injection_budget.ts +13 -3
  222. package/src/scripts/hooks/py_json_dumps.ts +103 -0
  223. package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
  224. package/src/scripts/hooks/run_continuation_hook.ts +8 -5
  225. package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
  226. package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
  227. package/src/scripts/hooks/state_io.ts +870 -21
  228. package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
  229. package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
  230. package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
  231. package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
  232. package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
  233. package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
  234. package/src/scripts/injection_scan_hook.ts +137 -5
  235. package/src/scripts/install.ts +16 -11
  236. package/src/scripts/language_mirror_hook.ts +561 -25
  237. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  238. package/src/scripts/lint_hook_manifest.ts +49 -0
  239. package/src/scripts/lint_plan_risk_register.ts +4 -3
  240. package/src/scripts/lint_provenance_vocabulary.ts +472 -4
  241. package/src/scripts/lint_skill_router_head.ts +4 -4
  242. package/src/scripts/mcp_server/tools.ts +2 -2
  243. package/src/scripts/new_roadmap.ts +136 -0
  244. package/src/scripts/pack_mcp_content.ts +20 -5
  245. package/src/scripts/release.ts +37 -38
  246. package/src/scripts/report_layer_overlap.ts +6 -0
  247. package/src/scripts/routing_doctor.ts +5 -1
  248. package/src/scripts/self_repair_hook.ts +37 -5
  249. package/src/scripts/session_register_hook.ts +11 -0
  250. package/src/scripts/skill_usage_collect.ts +11 -1
  251. package/src/scripts/skill_usage_report.ts +278 -8
  252. package/src/scripts/sync_pr_branch.ts +170 -20
  253. package/src/server/schemas/settings.ts +8 -0
  254. package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
  255. package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
  256. package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
  257. package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
  258. package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
@@ -0,0 +1,212 @@
1
+ // The `--check` tracked/untracked mode table for the roadmap dashboard.
2
+ //
3
+ // Extracted from `update_roadmap_progress.ts` so the verdict is a pure function
4
+ // over observable state — the generator does the I/O, this decides. It also
5
+ // keeps that script under its per-file size cap.
6
+
7
+ import { spawnSync } from 'node:child_process';
8
+ import * as fs from 'node:fs';
9
+
10
+ /**
11
+ * Is the dashboard expected to be a committed artefact? Explicit, never
12
+ * inferred from git state — an unconditional "absent means pass" cannot tell a
13
+ * correctly-untracked repo from a generator that silently stopped producing
14
+ * output. `tracked` is the default and the historical behaviour.
15
+ */
16
+ export type DashboardMode = 'tracked' | 'untracked';
17
+
18
+ /**
19
+ * Is `abs` in the git index? Used only by `--check --untracked-mode`, to tell
20
+ * "correctly absent" from "still committed". No git at all answers `false`,
21
+ * which is correct: nothing is tracked there.
22
+ */
23
+ export function isTrackedInGit(abs: string, repo_root: string): boolean {
24
+ const r = spawnSync('git', ['ls-files', '--error-unmatch', '--', abs], {
25
+ cwd: repo_root,
26
+ encoding: 'utf-8',
27
+ timeout: 10_000,
28
+ });
29
+ return r.status === 0;
30
+ }
31
+
32
+ export interface DashboardStateInput {
33
+ mode: DashboardMode;
34
+ /** Is the file on disk? */
35
+ present: boolean;
36
+ /** Is the file in the git index? Only consulted in `untracked` mode. */
37
+ trackedInGit: boolean;
38
+ /** Does the on-disk content equal a fresh render? Only meaningful if present. */
39
+ current: boolean;
40
+ /** Repo-relative path, for the messages. */
41
+ rel: string;
42
+ /**
43
+ * How to regenerate, for the stale/missing messages. Defaults to the
44
+ * dashboard's own command. A second caller — `build_archive_index --check`
45
+ * — reuses this table for a different generated artefact, and a message
46
+ * telling that reader to run the dashboard generator would be wrong.
47
+ */
48
+ regen?: string;
49
+ /**
50
+ * What the artefact is called in the "deliberately does not commit the X"
51
+ * hint. Defaults to `dashboard` so every existing message is byte-identical.
52
+ */
53
+ noun?: string;
54
+ }
55
+
56
+ export interface DashboardStateVerdict {
57
+ /** True when the dashboard half of `--check` fails. */
58
+ stale: boolean;
59
+ /** Diagnostic for stderr, or null. */
60
+ error: string | null;
61
+ /** Success line for stdout, or null when `stale`. */
62
+ ok: string | null;
63
+ }
64
+
65
+ // The SOURCE path, not `.augment/scripts/…`: that tree is a gitignored
66
+ // projection built by `task sync`, so the advice failed outright in a fresh
67
+ // worktree and in any CI step ordered before the sync.
68
+ const REGEN =
69
+ 'Run `./scripts-run src/agent-src/scripts/update_roadmap_progress` ' +
70
+ 'to regenerate (or `task roadmap-progress` in Taskfile projects).';
71
+
72
+ /**
73
+ * The mode table:
74
+ *
75
+ * tracked: absent -> fail | present+stale -> fail | present+current -> pass
76
+ * untracked: in-index -> fail | absent -> pass | present+stale -> fail
77
+ * | present+current -> pass
78
+ *
79
+ * Absence is distinguished ONLY in untracked mode; a present file is
80
+ * freshness-checked in both, so untracked never means unchecked.
81
+ */
82
+ export function evaluateDashboardState(inp: DashboardStateInput): DashboardStateVerdict {
83
+ const { mode, present, trackedInGit, current, rel } = inp;
84
+ const regen = inp.regen ?? REGEN;
85
+ const noun = inp.noun ?? 'dashboard';
86
+
87
+ if (mode === 'untracked' && trackedInGit) {
88
+ // Migration incomplete: declared untracked, still carried by git.
89
+ // Print the fix; never run it — git-index ops are the user's.
90
+ return {
91
+ stale: true,
92
+ error:
93
+ `❌ ${rel} is still tracked by git, but this repository runs ` +
94
+ '`--untracked-mode`. Untrack it, keeping the working-tree file:\n' +
95
+ ` git rm --cached ${rel}\n`,
96
+ ok: null,
97
+ };
98
+ }
99
+
100
+ if (!present) {
101
+ if (mode === 'tracked') {
102
+ return {
103
+ stale: true,
104
+ error:
105
+ `❌ ${rel} is missing. ` +
106
+ regen.replace('to regenerate', 'to generate it') +
107
+ ' If this repository deliberately does not commit the ' +
108
+ `${noun}, pass \`--untracked-mode\`.\n`,
109
+ ok: null,
110
+ };
111
+ }
112
+ // untracked + absent: the declared state. "up to date" would mislead.
113
+ return {
114
+ stale: false,
115
+ error: null,
116
+ ok: `✅ ${rel} is not committed here (--untracked-mode) and no stale copy is on disk.\n`,
117
+ };
118
+ }
119
+
120
+ if (!current) {
121
+ return { stale: true, error: `❌ ${rel} is stale. ${regen}\n`, ok: null };
122
+ }
123
+ return { stale: false, error: null, ok: `✅ ${rel} is up to date.\n` };
124
+ }
125
+
126
+ /**
127
+ * `evaluateDashboardState` against the real filesystem and git index. The
128
+ * generator hands over the paths; every read the verdict needs happens here, so
129
+ * the caller's check branch stays three lines.
130
+ */
131
+ export function evaluateDashboardOnDisk(opts: {
132
+ mode: DashboardMode;
133
+ target: string;
134
+ repo_root: string;
135
+ rendered: string;
136
+ rel: string;
137
+ regen?: string;
138
+ noun?: string;
139
+ }): DashboardStateVerdict {
140
+ const present = fs.existsSync(opts.target);
141
+ return evaluateDashboardState({
142
+ mode: opts.mode,
143
+ present,
144
+ trackedInGit: opts.mode === 'untracked' && isTrackedInGit(opts.target, opts.repo_root),
145
+ current: present && fs.readFileSync(opts.target, { encoding: 'utf-8' }) === opts.rendered,
146
+ rel: opts.rel,
147
+ ...(opts.regen === undefined ? {} : { regen: opts.regen }),
148
+ ...(opts.noun === undefined ? {} : { noun: opts.noun }),
149
+ });
150
+ }
151
+
152
+ /** One roadmap row, as far as the estate report below needs it. */
153
+ export interface EstateRow {
154
+ rel: string;
155
+ done: number;
156
+ total_active: number;
157
+ deferred: number;
158
+ }
159
+
160
+ /**
161
+ * The `--check` report: the artefact's own verdict, then the estate-wide
162
+ * conditions — completed-but-unarchived, and Iron Law 3 deferred items.
163
+ *
164
+ * `dashboardOnly` drops the estate half. Two obligations with two blast radii:
165
+ * the artefact verdict is about ONE file and is safe behind a required status
166
+ * check, while the estate conditions are about roadmaps an unrelated change
167
+ * never touched, so requiring them turns any pre-existing estate defect into a
168
+ * merge block on every PR. It did, on 2026-08-22.
169
+ *
170
+ * Extracted from the generator so the report is a pure function of the verdict
171
+ * and two lists — and because the generator sits exactly on its 1500-line
172
+ * source ceiling, where the honest way to add a feature is to take lines out.
173
+ */
174
+ export function reportCheckVerdict(inp: {
175
+ verdict: DashboardStateVerdict;
176
+ complete: readonly EstateRow[];
177
+ pending: readonly EstateRow[];
178
+ dashboardOnly: boolean;
179
+ warnMergeGated: () => void;
180
+ gatedCount: number;
181
+ }): { rc: number; stdout: string; stderr: string } {
182
+ const { verdict, dashboardOnly } = inp;
183
+ const complete = dashboardOnly ? [] : inp.complete;
184
+ const pending = dashboardOnly ? [] : inp.pending;
185
+ let err = verdict.error ?? '';
186
+ if (complete.length) {
187
+ err +=
188
+ '❌ Completed roadmaps are still in `agents/roadmaps/` — ' +
189
+ 'move them to `agents/roadmaps/archive/` (per the ' +
190
+ '`roadmap-progress-sync` rule):\n';
191
+ for (const r of complete) err += ` - ${r.rel} (${r.done}/${r.total_active} done)\n`;
192
+ }
193
+ if (pending.length) {
194
+ err +=
195
+ '❌ Iron Law 3 — roadmaps with unresolved `[~]` deferred ' +
196
+ 'items must NOT auto-archive. Resolve via `roadmap-management § 4b` ' +
197
+ '(spawn follow-up, restore, or cancel):\n';
198
+ for (const r of pending) {
199
+ err += ` - ${r.rel} (${r.done}/${r.total_active} done · ${r.deferred} deferred)\n`;
200
+ }
201
+ }
202
+ if (!dashboardOnly && inp.gatedCount > 0) inp.warnMergeGated();
203
+ const rc = verdict.stale || complete.length > 0 || pending.length > 0 ? 1 : 0;
204
+ return { rc, stdout: rc === 0 ? (verdict.ok ?? '') : '', stderr: err };
205
+ }
206
+
207
+ /** `--tracked-mode` / `--untracked-mode` → the mode, or null for any other token. */
208
+ export function parseModeToken(tok: string): DashboardMode | null {
209
+ if (tok === '--tracked-mode') return 'tracked';
210
+ if (tok === '--untracked-mode') return 'untracked';
211
+ return null;
212
+ }
@@ -0,0 +1,282 @@
1
+ /**
2
+ * gate_budget — the class-1 spend bound and its append-only receipt ledger
3
+ * (`road-to-gate-autonomy` Phase 2, blocker `b-gate-budget-preauth`).
4
+ *
5
+ * WHY THIS EXISTS. Class 1 of the gate taxonomy is "billable or long-running
6
+ * but reversible; the human ingredient is *spend consent*, not judgment".
7
+ * `gate_execute.ts` shipped without it on purpose — "no invented ledger",
8
+ * because the budget SHAPE was an open maintainer decision. That decision is
9
+ * now taken (option (a): a per-run cap AND a rolling-window cap, with an
10
+ * append-only receipt ledger as the audit surface), so this module supplies
11
+ * exactly the shape that was decided and nothing more.
12
+ *
13
+ * WHAT THIS IS NOT. It is **not** a consent bypass. `gates --execute` still
14
+ * requires `--confirm` on every class-1 run, so the this-turn,
15
+ * names-the-exact-object confirmation `non-destructive-by-default` asks for is
16
+ * unchanged. The caps bound the SIZE of an authorised spend; they never
17
+ * manufacture the authorisation. A ledger under `agents/runtime/state/` is
18
+ * agent-writable, so a mechanism that read an "authorisation" out of it would
19
+ * be an agent consenting on the user's behalf — which is precisely the threat
20
+ * model the live-trigger-eval terminal abort exists for. That is why the
21
+ * receipt records CONSUMPTION only, and why step 2.3's bypass flag is not
22
+ * built on top of this file.
23
+ *
24
+ * A per-run cap alone is not a spend bound — it bounds one mistake, not a week
25
+ * of them — which is the whole reason the decision carries two numbers.
26
+ */
27
+ import * as fs from 'node:fs';
28
+ import * as path from 'node:path';
29
+
30
+ /**
31
+ * Ledger location, relative to the workspace root.
32
+ *
33
+ * The name is the one already present in `gate_execute.ts` as the
34
+ * absent-ledger placeholder, kept verbatim so the path a class-1 entry has
35
+ * been REPORTING since Phase 2 is the path it now reads. `agents/runtime/state/`
36
+ * is this tree's state convention (`run_checkpoint`, `unattended_guard`,
37
+ * `recycle_envelope_paths` all live there); a bare `agents/state/` is not.
38
+ */
39
+ export const GATE_BUDGET_LEDGER_REL = path.join(
40
+ 'agents',
41
+ 'runtime',
42
+ 'state',
43
+ 'gate-budget-ledger.jsonl',
44
+ );
45
+
46
+ /**
47
+ * Build the absolute ledger path.
48
+ *
49
+ * Callers import THIS rather than joining {@link GATE_BUDGET_LEDGER_REL}
50
+ * themselves: a bare relative constant is the shape that goes silently dead
51
+ * when the location moves, because nothing joins it any more and nothing
52
+ * complains.
53
+ */
54
+ export function gateBudgetLedgerPath(workspaceRoot: string): string {
55
+ return path.join(workspaceRoot, GATE_BUDGET_LEDGER_REL);
56
+ }
57
+
58
+ /** The two caps decided by option (a). Both are USD. */
59
+ export interface GateBudgetCaps {
60
+ maxCostPerRunUsd: number;
61
+ maxCostPerRolling7dUsd: number;
62
+ }
63
+
64
+ /**
65
+ * One append-only ledger line.
66
+ *
67
+ * The five fields the decision names — blocker, authorization, estimated and
68
+ * actual cost, timestamp — plus the single-use marker. `authorization` records
69
+ * HOW the spend was consented to; today the only value a run can honestly
70
+ * write is `confirm-flag`, because `--confirm` is the consent. A future
71
+ * human-only authorisation surface would add its own value here rather than
72
+ * reinterpreting this one.
73
+ */
74
+ export interface GateBudgetReceipt {
75
+ kind: 'consumption';
76
+ blocker: string;
77
+ authorization: 'confirm-flag';
78
+ estimated_usd: number;
79
+ actual_usd: number | null;
80
+ at: string;
81
+ single_use: true;
82
+ }
83
+
84
+ /** Rolling window, in days, that `maxCostPerRolling7dUsd` bounds. */
85
+ export const ROLLING_WINDOW_DAYS = 7;
86
+
87
+ function _num(value: unknown): number | null {
88
+ if (typeof value === 'number' && Number.isFinite(value)) {
89
+ return value;
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function _dig(settings: unknown, keys: readonly string[]): unknown {
95
+ let cursor: unknown = settings;
96
+ for (const k of keys) {
97
+ if (cursor === null || typeof cursor !== 'object' || Array.isArray(cursor)) {
98
+ return undefined;
99
+ }
100
+ cursor = (cursor as Record<string, unknown>)[k];
101
+ }
102
+ return cursor;
103
+ }
104
+
105
+ /**
106
+ * Read the caps out of an already-merged settings dict.
107
+ *
108
+ * Returns `null` when either cap is absent or non-numeric — deliberately NOT a
109
+ * fallback to the template numbers. An install whose settings do not carry the
110
+ * caps has not authorised a standing budget, and inventing one from the
111
+ * template default would authorise spend nobody configured. The caller renders
112
+ * the consent line instead.
113
+ */
114
+ export function readGateBudgetCaps(settings: unknown): GateBudgetCaps | null {
115
+ const perRun = _num(_dig(settings, ['roadmap', 'gate_budget', 'max_cost_per_run_usd']));
116
+ const rolling = _num(
117
+ _dig(settings, ['roadmap', 'gate_budget', 'max_cost_per_rolling_7d_usd']),
118
+ );
119
+ if (perRun === null || rolling === null) {
120
+ return null;
121
+ }
122
+ return { maxCostPerRunUsd: perRun, maxCostPerRolling7dUsd: rolling };
123
+ }
124
+
125
+ /**
126
+ * Parse a USD figure out of a blocker's `Budget:` field.
127
+ *
128
+ * An explicit currency marker (`$` or `USD`) is REQUIRED. Without it a field
129
+ * like "~50 queries per run" would be read as fifty dollars, and a spend bound
130
+ * derived from a misread is worse than no bound: it would let a large run
131
+ * through under a small number. Returns `null` when no USD figure is stated.
132
+ */
133
+ export function parseBudgetUsd(field: string): number | null {
134
+ const text = field.trim();
135
+ if (text === '') {
136
+ return null;
137
+ }
138
+ const dollar = /\$\s*([0-9]+(?:[.,][0-9]+)?)/.exec(text);
139
+ if (dollar) {
140
+ return Number.parseFloat(dollar[1]!.replace(',', '.'));
141
+ }
142
+ const usd = /([0-9]+(?:[.,][0-9]+)?)\s*(?:usd|dollars?)\b/i.exec(text);
143
+ if (usd) {
144
+ return Number.parseFloat(usd[1]!.replace(',', '.'));
145
+ }
146
+ return null;
147
+ }
148
+
149
+ /** Read every well-formed receipt. A malformed line is skipped, never fatal. */
150
+ export function readGateBudgetLedger(workspaceRoot: string): GateBudgetReceipt[] {
151
+ const file = gateBudgetLedgerPath(workspaceRoot);
152
+ if (!fs.existsSync(file)) {
153
+ return [];
154
+ }
155
+ const out: GateBudgetReceipt[] = [];
156
+ for (const line of fs.readFileSync(file, 'utf-8').split('\n')) {
157
+ const trimmed = line.trim();
158
+ if (trimmed === '') {
159
+ continue;
160
+ }
161
+ let parsed: unknown;
162
+ try {
163
+ parsed = JSON.parse(trimmed);
164
+ } catch {
165
+ continue;
166
+ }
167
+ if (parsed === null || typeof parsed !== 'object') {
168
+ continue;
169
+ }
170
+ const rec = parsed as Record<string, unknown>;
171
+ if (rec['kind'] !== 'consumption' || typeof rec['blocker'] !== 'string') {
172
+ continue;
173
+ }
174
+ out.push({
175
+ kind: 'consumption',
176
+ blocker: rec['blocker'],
177
+ authorization: 'confirm-flag',
178
+ estimated_usd: _num(rec['estimated_usd']) ?? 0,
179
+ actual_usd: _num(rec['actual_usd']),
180
+ at: typeof rec['at'] === 'string' ? rec['at'] : '',
181
+ single_use: true,
182
+ });
183
+ }
184
+ return out;
185
+ }
186
+
187
+ /** Append one receipt. Append-only by construction — nothing here rewrites. */
188
+ export function appendGateBudgetReceipt(
189
+ workspaceRoot: string,
190
+ receipt: GateBudgetReceipt,
191
+ ): void {
192
+ const file = gateBudgetLedgerPath(workspaceRoot);
193
+ fs.mkdirSync(path.dirname(file), { recursive: true });
194
+ fs.appendFileSync(file, `${JSON.stringify(receipt)}\n`, 'utf-8');
195
+ }
196
+
197
+ /**
198
+ * Spend inside the rolling window.
199
+ *
200
+ * `actual_usd` when the run reported one, the estimate otherwise — an
201
+ * unreported actual must not read as zero spend. A receipt with an unparseable
202
+ * timestamp counts as IN the window, because dropping it would understate the
203
+ * total and the cap is meant to fail closed.
204
+ */
205
+ export function rollingSpendUsd(records: readonly GateBudgetReceipt[], now: Date): number {
206
+ const cutoff = now.getTime() - ROLLING_WINDOW_DAYS * 24 * 60 * 60 * 1000;
207
+ let total = 0;
208
+ for (const r of records) {
209
+ const stamp = Date.parse(r.at);
210
+ if (Number.isFinite(stamp) && stamp < cutoff) {
211
+ continue;
212
+ }
213
+ total += r.actual_usd ?? r.estimated_usd;
214
+ }
215
+ return total;
216
+ }
217
+
218
+ export type GateBudgetRefusal =
219
+ | 'no_caps'
220
+ | 'no_estimate'
221
+ | 'over_per_run'
222
+ | 'over_rolling_7d';
223
+
224
+ export type GateBudgetVerdict =
225
+ | { ok: true; estimateUsd: number; rollingUsd: number }
226
+ | { ok: false; reason: GateBudgetRefusal; detail: string };
227
+
228
+ /**
229
+ * Decide whether a class-1 run fits inside the standing budget.
230
+ *
231
+ * Every refusal path renders rather than runs — the blocker's own prescription
232
+ * for a missing ledger, generalised to every way the budget can say no.
233
+ */
234
+ export function evaluateGateBudget(opts: {
235
+ caps: GateBudgetCaps | null;
236
+ records: readonly GateBudgetReceipt[];
237
+ estimateUsd: number | null;
238
+ now: Date;
239
+ }): GateBudgetVerdict {
240
+ const { caps, records, estimateUsd, now } = opts;
241
+ if (caps === null) {
242
+ return {
243
+ ok: false,
244
+ reason: 'no_caps',
245
+ detail:
246
+ 'no standing class-1 budget is configured ' +
247
+ '(`roadmap.gate_budget.max_cost_per_run_usd` and ' +
248
+ '`roadmap.gate_budget.max_cost_per_rolling_7d_usd`)',
249
+ };
250
+ }
251
+ if (estimateUsd === null) {
252
+ return {
253
+ ok: false,
254
+ reason: 'no_estimate',
255
+ detail:
256
+ 'the entry states no USD estimate in its **Budget:** field, so no cap ' +
257
+ 'can be compared against it',
258
+ };
259
+ }
260
+ if (estimateUsd > caps.maxCostPerRunUsd) {
261
+ return {
262
+ ok: false,
263
+ reason: 'over_per_run',
264
+ detail:
265
+ `the estimate ($${estimateUsd.toFixed(2)}) exceeds the per-run cap ` +
266
+ `($${caps.maxCostPerRunUsd.toFixed(2)})`,
267
+ };
268
+ }
269
+ const spent = rollingSpendUsd(records, now);
270
+ if (spent + estimateUsd > caps.maxCostPerRolling7dUsd) {
271
+ return {
272
+ ok: false,
273
+ reason: 'over_rolling_7d',
274
+ detail:
275
+ `$${spent.toFixed(2)} is already receipted in the last ` +
276
+ `${String(ROLLING_WINDOW_DAYS)} days and this run would add ` +
277
+ `$${estimateUsd.toFixed(2)}, over the rolling cap ` +
278
+ `($${caps.maxCostPerRolling7dUsd.toFixed(2)})`,
279
+ };
280
+ }
281
+ return { ok: true, estimateUsd, rollingUsd: spent };
282
+ }
@@ -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.