@event4u/agent-config 14.6.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 (188) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +260 -7
  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/roadmap/process-full.md +116 -30
  7. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +31 -0
  8. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +135 -104
  9. package/dist/agent-src/rules/context-hygiene.md +2 -0
  10. package/dist/agent-src/rules/decision-revisit-gate.md +3 -0
  11. package/dist/agent-src/rules/fix-what-you-see.md +108 -0
  12. package/dist/agent-src/rules/no-roadmap-references.md +0 -5
  13. package/dist/agent-src/rules/recurring-criticism.md +110 -0
  14. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  15. package/dist/agent-src/rules/rule-type-governance.md +0 -2
  16. package/dist/agent-src/rules/self-repair-loop.md +17 -0
  17. package/dist/agent-src/rules/skill-quality.md +0 -2
  18. package/dist/agent-src/rules/source-confidentiality.md +37 -7
  19. package/dist/agent-src/scripts/archival_sweep.ts +117 -0
  20. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  21. package/dist/agent-src/scripts/gate_budget.ts +282 -0
  22. package/dist/agent-src/scripts/gate_execute.ts +72 -31
  23. package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
  24. package/dist/agent-src/scripts/update_roadmap_progress.ts +251 -7
  25. package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
  26. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
  27. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
  28. package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
  29. package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
  30. package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
  31. package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
  32. package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
  33. package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
  34. package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
  35. package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
  36. package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
  37. package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
  38. package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
  39. package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
  40. package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
  41. package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
  42. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
  43. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  44. package/dist/agent-src/templates/roadmaps.md +45 -2
  45. package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  46. package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  47. package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  48. package/dist/cli/registry.js +1 -1
  49. package/dist/cli/registry.js.map +1 -1
  50. package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
  51. package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-4VR4ORQ6.js} +10 -7
  52. package/dist/cli-delegate/chunk-6EOCNQAM.js +402 -0
  53. package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-74E3TKWV.js} +381 -280
  54. package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-7F3VYSM2.js} +4 -4
  55. package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
  56. package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
  57. package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-GJ5BVHXM.js} +72 -165
  58. package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-JZ4NR2TW.js} +338 -0
  59. package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
  60. package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
  61. package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
  62. package/dist/cli-delegate/cmd_conformance.js +459 -709
  63. package/dist/cli-delegate/cmd_doctor.js +6 -6
  64. package/dist/cli-delegate/cmd_explain.js +1 -1
  65. package/dist/cli-delegate/cmd_export.js +7 -6
  66. package/dist/cli-delegate/cmd_handoff.js +6 -6
  67. package/dist/cli-delegate/cmd_packs_active.js +1 -1
  68. package/dist/cli-delegate/cmd_preflight.js +2 -2
  69. package/dist/cli-delegate/cmd_prune.js +7 -6
  70. package/dist/cli-delegate/cmd_session_recycle.js +840 -30
  71. package/dist/cli-delegate/cmd_settings_migrate.js +7 -6
  72. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  73. package/dist/cli-delegate/cmd_sync.js +7 -6
  74. package/dist/cli-delegate/cmd_uninstall.js +9 -7
  75. package/dist/cli-delegate/cmd_update.js +1 -1
  76. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  77. package/dist/cli-delegate/cmd_validate.js +7 -6
  78. package/dist/discovery/deprecation-report.md +1 -1
  79. package/dist/discovery/discovery-manifest.json +68 -25
  80. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  81. package/dist/discovery/discovery-manifest.summary.md +4 -4
  82. package/dist/discovery/dormancy-report.md +1 -1
  83. package/dist/discovery/orphan-report.md +1 -1
  84. package/dist/discovery/packs.json +10 -8
  85. package/dist/discovery/trust-report.md +3 -3
  86. package/dist/discovery/workspaces.json +8 -6
  87. package/dist/hooks/dispatch.js +2901 -2053
  88. package/dist/install/claudePathsPlan.js +187 -0
  89. package/dist/install/claudePathsPlan.js.map +1 -0
  90. package/dist/install/hostLayerFingerprint.js +136 -0
  91. package/dist/install/hostLayerFingerprint.js.map +1 -0
  92. package/dist/install/install.mjs +553 -425
  93. package/dist/install/partitionEligibility.js +276 -0
  94. package/dist/install/partitionEligibility.js.map +1 -0
  95. package/dist/mcp/registry-manifest.json +3 -3
  96. package/dist/mcp/server.json +2 -2
  97. package/dist/mcp/server.mjs +1744 -1191
  98. package/dist/router.json +1 -1
  99. package/dist/scripts/_lib/agent_src.js +41 -0
  100. package/dist/scripts/_lib/agent_src.js.map +1 -1
  101. package/dist/scripts/_lib/install_layout.js +83 -0
  102. package/dist/scripts/_lib/install_layout.js.map +1 -0
  103. package/dist/scripts/_lib/installed_lock.js +444 -0
  104. package/dist/scripts/_lib/installed_lock.js.map +1 -0
  105. package/dist/scripts/condense.js +13 -164
  106. package/dist/scripts/condense.js.map +1 -1
  107. package/dist/server/schemas/settings.js +4 -0
  108. package/dist/server/schemas/settings.js.map +1 -1
  109. package/docs/contracts/settings-classes.md +12 -6
  110. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
  111. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
  112. package/docs/guidelines/agent-infra/memory-access.md +17 -0
  113. package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  114. package/package.json +1 -1
  115. package/src/agent-src/scripts/archival_sweep.ts +117 -0
  116. package/src/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  117. package/src/agent-src/scripts/gate_budget.ts +282 -0
  118. package/src/agent-src/scripts/gate_execute.ts +72 -31
  119. package/src/agent-src/scripts/roadmap_gates.ts +74 -3
  120. package/src/agent-src/scripts/update_roadmap_progress.ts +251 -7
  121. package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  122. package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  123. package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  124. package/src/config/agent-settings.template.yml +27 -2
  125. package/src/config/ci-local-parity.yml +18 -0
  126. package/src/config/estate-count-budget.json +339 -3
  127. package/src/config/gate-violation-baselines.json +41 -41
  128. package/src/config/rule-activation-census.json +14 -6
  129. package/src/config/rule-stub-ceilings.json +19 -6
  130. package/src/install/claudePathsPlan.ts +196 -0
  131. package/src/install/hostLayerFingerprint.ts +145 -0
  132. package/src/install/partitionEligibility.ts +325 -0
  133. package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
  134. package/src/scripts/_dispatch.bash +6 -1
  135. package/src/scripts/_lib/ac_heading.ts +30 -0
  136. package/src/scripts/_lib/agent_src.ts +42 -0
  137. package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
  138. package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
  139. package/src/scripts/_lib/envelope_grounding.ts +38 -8
  140. package/src/scripts/_lib/installed_lock.ts +50 -5
  141. package/src/scripts/_lib/md_section.ts +129 -0
  142. package/src/scripts/_lib/scope_guard.sh +26 -3
  143. package/src/scripts/_lib/self_repair.ts +19 -0
  144. package/src/scripts/_lib/self_repair_class_b.ts +171 -0
  145. package/src/scripts/ai_council/cli_help.ts +55 -0
  146. package/src/scripts/before_complete_hook.ts +153 -28
  147. package/src/scripts/bench_ab_v2_complexity.ts +27 -7
  148. package/src/scripts/check_references.ts +60 -0
  149. package/src/scripts/check_release_published.ts +32 -11
  150. package/src/scripts/check_single_delivery.ts +59 -15
  151. package/src/scripts/ci_settle.ts +170 -0
  152. package/src/scripts/condense.ts +20 -170
  153. package/src/scripts/council_cli.ts +3 -3
  154. package/src/scripts/dispatch_r2_reviewer.ts +6 -14
  155. package/src/scripts/hook_manifest.json +1 -1
  156. package/src/scripts/hook_manifest.yaml +77 -14
  157. package/src/scripts/hooks/block_no_verify.ts +61 -10
  158. package/src/scripts/hooks/block_unauthorized_git.ts +1 -1
  159. package/src/scripts/hooks/concern_registry.ts +4 -0
  160. package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
  161. package/src/scripts/hooks/run_continuation_hook.ts +8 -5
  162. package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
  163. package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
  164. package/src/scripts/hooks/state_io.ts +782 -20
  165. package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
  166. package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
  167. package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
  168. package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
  169. package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
  170. package/src/scripts/install.ts +16 -11
  171. package/src/scripts/language_mirror_hook.ts +561 -25
  172. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  173. package/src/scripts/lint_plan_risk_register.ts +4 -3
  174. package/src/scripts/lint_skill_router_head.ts +4 -4
  175. package/src/scripts/mcp_server/tools.ts +2 -2
  176. package/src/scripts/new_roadmap.ts +136 -0
  177. package/src/scripts/pack_mcp_content.ts +20 -5
  178. package/src/scripts/release.ts +37 -38
  179. package/src/scripts/report_layer_overlap.ts +6 -0
  180. package/src/scripts/self_repair_hook.ts +37 -5
  181. package/src/scripts/session_register_hook.ts +11 -0
  182. package/src/scripts/skill_usage_collect.ts +11 -1
  183. package/src/scripts/skill_usage_report.ts +278 -8
  184. package/src/scripts/sync_pr_branch.ts +7 -0
  185. package/src/server/schemas/settings.ts +8 -0
  186. package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
  187. package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
  188. /package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
@@ -3,13 +3,6 @@ type: "auto"
3
3
  tier: "mechanical-already"
4
4
  description: "Naming an external repo this package copied/harvested/compared against — keep the tracked tree source-anonymous"
5
5
  alwaysApply: false
6
- triggers:
7
- - path_prefix: "src/skills/"
8
- - path_prefix: "src/rules/"
9
- - path_prefix: "src/domains/"
10
- - path_prefix: "docs/"
11
- - path_prefix: "agents/evidence/"
12
- - path_prefix: "agents/roadmaps/"
13
6
  validator_ignore:
14
7
  - type: "substring"
15
8
  pattern: "external_sources_denylist"
@@ -96,6 +89,43 @@ runs in the package CI pipeline and fails the build on any denied source token
96
89
  in a non-carve-out tracked file. The linter is a deterministic net, not a
97
90
  substitute for not writing the attribution in the first place.
98
91
 
92
+ ## Why this rule is not path-scoped
93
+
94
+ Under [ADR-236](../docs/decisions/ADR-236-one-artefact-one-layer.md) this rule is
95
+ delivered by the PROJECT layer only — it exists to maintain this package, so the
96
+ global layer no longer carries an unscoped twin of it. A `paths:`-scoped rule is
97
+ **not re-injected after `/compact`** (ADR-227:79-80), so scoping it would mean the
98
+ obligation silently disappears mid-session with nothing left to reload it.
99
+
100
+ It is therefore delivered unconditionally, and the path triggers were removed
101
+ rather than worked around. **This section is the shared record for all four such
102
+ rules** — `no-roadmap-references`, `rule-type-governance` and `skill-quality` carry
103
+ the same change and no note of their own. Two reasons: four copies of the rationale
104
+ cost more standing context than the decision they explain, and the other three are
105
+ migrated POINTER stubs held at their pointer's size by
106
+ `check_rule_stub_ceiling` — prose added there is prose in the wrong place by that
107
+ gate's own contract, which is what caught the first attempt to duplicate it.
108
+
109
+ AI council 2026-08-20, 2/2 convergent on this option over three alternatives, with
110
+ two independent decisive arguments. First: the obligation governs an **authoring
111
+ decision**, which happens before any file exists for a path trigger to match — so
112
+ path-scoping fails at greenfield creation whether or not a compaction ever occurs.
113
+ Second: the measured cost is small against what the partition returns. Keeping an
114
+ unscoped global copy was rejected because it re-creates the duplication the
115
+ partition exists to remove; splitting the four by "has a CI gate" was rejected by
116
+ both seats as the wrong axis — a validator observes an outcome, it does not hold an
117
+ obligation during the session, and `rule-type-governance` has no gate at all.
118
+
119
+ **Cost, measured and corrected.** An earlier revision of this note claimed 1,754
120
+ tokens (1.8 % of the partition's 96,584-token saving). That figure was a
121
+ `chars / 4` proxy over the projected files and **understated the real cost by about
122
+ half**: `check_rule_activation_census`, which counts with the exact BPE tokenizer,
123
+ measures the unconditional corpus growing **108,130 → 111,642, i.e. +3,512 tokens
124
+ (3.6 %)** — and that number includes this rationale, which is why it lives in one
125
+ place instead of four. The verdict does not depend on the figure: one seat argued
126
+ the token axis was the wrong one entirely, the other that cost is a legitimate axis
127
+ which simply loses this particular comparison. Both readings survive 3.6 %.
128
+
99
129
  ## See also
100
130
 
101
131
  - [`source-of-truth`](source-of-truth.md) — edit `src/`, never the projections.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Archival-sweep spawner for the roadmap dashboard.
3
+ *
4
+ * Split out of `update_roadmap_progress.ts` rather than authored there: that
5
+ * file sits at the 1500-line source ceiling `check_source_size_budget`
6
+ * ratchets, and the sweep plus its contract is the newest and most separable
7
+ * thing in it. The contract below is that text, moved intact.
8
+ *
9
+ * `--archive` (opt-in; wired into `task roadmap-progress` and
10
+ * `agent-config roadmap:progress`) runs the archival sweep before rendering
11
+ * instead of printing "Completed roadmaps not yet archived" and leaving the
12
+ * work to a human who has been ignoring the line for weeks. It is a flag rather
13
+ * than the default because the PostToolUse hook re-runs the WRITE path once per
14
+ * turn on every roadmap edit, and a hook that silently `git mv`s files mid-work
15
+ * is a bigger problem than the warning it would remove. `--check` never
16
+ * archives — see `run_archival_sweep`.
17
+ *
18
+ * WHY the warning existed at all, since this is the one place a reader of the
19
+ * flag will look: the sweep's own default is `--changed-only`, so a PR archives
20
+ * exactly the roadmaps it completed. A roadmap completed by a PR whose sweep did
21
+ * not run is then complete, on the trunk, and OUTSIDE every later branch's
22
+ * history — no `--changed-only` sweep finds it again. Measured 2026-08-20: six
23
+ * such roadmaps, with every regen reprinting the warning at whoever ran it.
24
+ * `/create-pr` § 1c keeps `--changed-only`; repo-wide reconciliation is this
25
+ * flag's job and nothing else's.
26
+ *
27
+ * This layout is deliberate rather than convenient: the prose homes for it are
28
+ * both at their ceilings (`roadmap-progress-mechanics.md` sits 122 chars under
29
+ * the 16,000-char depth ceiling, and `roadmap-management/SKILL.md` tips
30
+ * `skill_too_large` on any prose block), so the contract lives with the code it
31
+ * governs. Moving it out means making room first.
32
+ *
33
+ * AI council 2026-08-20 (anthropic + openai, blind peer review): both seats
34
+ * converged on explicit opt-in + `--all` scope + hook archival-free, and both
35
+ * independently required the `--check` exclusion, the archive-before-render
36
+ * order, and that a failed sweep must not render. RECORDED DISSENT: one seat
37
+ * argued for flipping the sweep's own default to `--all` instead. Rejected —
38
+ * that makes every PR a potential estate-wide cleanup, which is exactly what
39
+ * `--changed-only` exists to prevent.
40
+ */
41
+
42
+ import { spawnSync } from 'node:child_process';
43
+ import * as fs from 'node:fs';
44
+ import * as path from 'node:path';
45
+ import { fileURLToPath } from 'node:url';
46
+
47
+ // Same directory as `update_roadmap_progress.ts`, so the `path.dirname` below
48
+ // resolves the sweep script exactly as it did before the split.
49
+ const _HERE = fileURLToPath(import.meta.url);
50
+
51
+ function _isFile(p: string): boolean {
52
+ try {
53
+ return fs.statSync(p).isFile();
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Run the archival sweep for every complete roadmap, in-place.
61
+ *
62
+ * Spawned rather than imported: `archive_completed_roadmaps.ts` imports
63
+ * `collect` from `update_roadmap_progress.ts`, so a static import back would close a cycle, and
64
+ * `main()` is synchronous so a dynamic `await import()` is not available either.
65
+ * The sweep is the same twin `_regen_dashboard` already spawns in the other
66
+ * direction, so the spawn shape is the established one in this pair.
67
+ *
68
+ * No recursion: the sweep re-runs this script with NO flags, so its inner
69
+ * dashboard pass has archival off and terminates at depth 2.
70
+ *
71
+ * `--all`, not the sweep's `--changed-only` default. The default exists so a PR
72
+ * archives exactly the roadmaps it completed; here the caller asked the
73
+ * repo-wide dashboard to reconcile the estate, and the roadmaps that leak are
74
+ * by construction the ones NOT in this branch's history — six had accumulated
75
+ * on the trunk while the warning printed every regen. A roadmap the sweep
76
+ * refuses (open blockers) stays put and is still reported below.
77
+ */
78
+ export function run_archival_sweep(root: string): SweepResult {
79
+ const script = path.join(path.dirname(_HERE), 'archive_completed_roadmaps.ts');
80
+ if (!_isFile(script)) {
81
+ // A consumer install without the sweep script: nothing to run, and that
82
+ // is a normal state — the warning below still reports the roadmaps.
83
+ return { ran: false, ok: true, stdout: '', stderr: '' };
84
+ }
85
+ const argv = [script, '--all', '--repo-root', root];
86
+ const binName = process.platform === 'win32' ? 'tsx.cmd' : 'tsx';
87
+ let dir = path.dirname(_HERE);
88
+ for (;;) {
89
+ const candidate = path.join(dir, 'node_modules', '.bin', binName);
90
+ if (fs.existsSync(candidate)) {
91
+ return _sweepResult(spawnSync(candidate, argv, { cwd: root, encoding: 'utf-8' }));
92
+ }
93
+ const parent = path.dirname(dir);
94
+ if (parent === dir) {
95
+ break;
96
+ }
97
+ dir = parent;
98
+ }
99
+ return _sweepResult(spawnSync('npx', ['tsx', ...argv], { cwd: root, encoding: 'utf-8' }));
100
+ }
101
+
102
+ export interface SweepResult {
103
+ ran: boolean;
104
+ ok: boolean;
105
+ stdout: string;
106
+ stderr: string;
107
+ }
108
+
109
+ function _sweepResult(r: ReturnType<typeof spawnSync>): SweepResult {
110
+ return {
111
+ ran: true,
112
+ ok: r.status === 0,
113
+ stdout: String(r.stdout ?? ''),
114
+ stderr: String(r.stderr ?? ''),
115
+ };
116
+ }
117
+
@@ -31,6 +31,7 @@
31
31
  * ./scripts-run src/scripts/archive_completed_roadmaps # --changed-only (default)
32
32
  * ./scripts-run src/scripts/archive_completed_roadmaps --all
33
33
  * ./scripts-run src/scripts/archive_completed_roadmaps --base origin/main --dry-run
34
+ * ./scripts-run src/scripts/archive_completed_roadmaps --repo-root /path/to/proj
34
35
  *
35
36
  * --- Parity notes (ADR-200) ---
36
37
  *
@@ -462,19 +463,21 @@ function _runTwin(root: string, script: string): void {
462
463
  const _PROG = 'archive_completed_roadmaps.py';
463
464
 
464
465
  function _usage(): string {
465
- return `usage: ${_PROG} [-h] [--all] [--base BASE] [--dry-run]\n`;
466
+ return `usage: ${_PROG} [-h] [--all] [--base BASE] [--dry-run] [--repo-root REPO_ROOT]\n`;
466
467
  }
467
468
 
468
469
  interface Args {
469
470
  all: boolean;
470
471
  base: string;
471
472
  dry_run: boolean;
473
+ repo_root: string | null;
472
474
  }
473
475
 
474
476
  function _parseArgs(argv: readonly string[]): Args {
475
477
  let all = false;
476
478
  let base = 'origin/main';
477
479
  let dry_run = false;
480
+ let repo_root: string | null = null;
478
481
  const emitError = (msg: string): never => {
479
482
  process.stderr.write(_usage());
480
483
  process.stderr.write(`${_PROG}: error: ${msg}\n`);
@@ -502,17 +505,30 @@ function _parseArgs(argv: readonly string[]): Args {
502
505
  } else if (tok.startsWith('--base=')) {
503
506
  base = tok.slice('--base='.length);
504
507
  i += 1;
508
+ } else if (tok === '--repo-root') {
509
+ const val = argv[i + 1];
510
+ if (val === undefined) {
511
+ emitError('argument --repo-root: expected one argument');
512
+ }
513
+ repo_root = val as string;
514
+ i += 2;
515
+ } else if (tok.startsWith('--repo-root=')) {
516
+ repo_root = tok.slice('--repo-root='.length);
517
+ i += 1;
505
518
  } else {
506
519
  emitError(`unrecognized arguments: ${tok}`);
507
520
  }
508
521
  }
509
- return { all, base, dry_run };
522
+ return { all, base, dry_run, repo_root };
510
523
  }
511
524
 
512
525
  function main(argv?: readonly string[]): number {
513
526
  const ns = _parseArgs(argv ?? process.argv.slice(2));
514
527
 
515
- const root = _repo_root();
528
+ // An explicit `--repo-root` wins over the `git rev-parse` resolution. The
529
+ // dashboard generator passes it so a monorepo sub-project sweep cannot walk
530
+ // up to the enclosing repo and archive the PARENT's roadmaps.
531
+ const root = ns.repo_root ?? _repo_root();
516
532
  const archived = archive_completed(root, {
517
533
  changed_only: !ns.all,
518
534
  base: ns.base,
@@ -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
+ }