@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +384 -3
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/decision.md +7 -0
- package/dist/agent-src/commands/analyze/inbox.md +264 -10
- package/dist/agent-src/commands/analyze.md +1 -1
- package/dist/agent-src/commands/optimize/project.md +10 -0
- package/dist/agent-src/commands/roadmap/process-full.md +132 -4
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +31 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +161 -1
- package/dist/agent-src/rules/context-hygiene.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +111 -7
- package/dist/agent-src/rules/fix-what-you-see.md +108 -0
- package/dist/agent-src/rules/no-roadmap-references.md +0 -5
- package/dist/agent-src/rules/recurring-criticism.md +110 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
- package/dist/agent-src/rules/rule-type-governance.md +0 -2
- package/dist/agent-src/rules/self-repair-loop.md +17 -0
- package/dist/agent-src/rules/skill-quality.md +0 -2
- package/dist/agent-src/rules/source-confidentiality.md +37 -7
- package/dist/agent-src/scripts/archival_sweep.ts +117 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
- package/dist/agent-src/scripts/gate_budget.ts +282 -0
- package/dist/agent-src/scripts/gate_execute.ts +72 -31
- package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
- package/dist/agent-src/scripts/update_roadmap_progress.ts +251 -7
- package/dist/agent-src/skills/adr-create/SKILL.md +25 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
- package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
- package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
- package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
- package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
- package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
- package/dist/agent-src/skills/decision-review/SKILL.md +18 -0
- package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
- package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
- package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
- package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
- package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
- package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
- package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
- package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
- package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
- package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +45 -2
- package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/dist/cli/registry.js +1 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
- package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-4VR4ORQ6.js} +10 -7
- package/dist/cli-delegate/chunk-6EOCNQAM.js +402 -0
- package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-74E3TKWV.js} +381 -280
- package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-7F3VYSM2.js} +4 -4
- package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
- package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
- package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-GJ5BVHXM.js} +72 -165
- package/dist/cli-delegate/{chunk-ZPSXJTLI.js → chunk-JZ4NR2TW.js} +353 -1
- package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
- package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
- package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
- package/dist/cli-delegate/cmd_conformance.js +459 -709
- package/dist/cli-delegate/cmd_doctor.js +6 -6
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +7 -6
- package/dist/cli-delegate/cmd_handoff.js +6 -6
- package/dist/cli-delegate/cmd_packs_active.js +1 -1
- package/dist/cli-delegate/cmd_preflight.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +7 -6
- package/dist/cli-delegate/cmd_session_recycle.js +840 -30
- package/dist/cli-delegate/cmd_settings_migrate.js +7 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +7 -6
- package/dist/cli-delegate/cmd_uninstall.js +9 -7
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +7 -6
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +72 -29
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +3127 -2022
- package/dist/install/claudePathsPlan.js +187 -0
- package/dist/install/claudePathsPlan.js.map +1 -0
- package/dist/install/hostLayerFingerprint.js +136 -0
- package/dist/install/hostLayerFingerprint.js.map +1 -0
- package/dist/install/install.mjs +553 -425
- package/dist/install/partitionEligibility.js +276 -0
- package/dist/install/partitionEligibility.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1744 -1191
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_src.js +41 -0
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/scripts/_lib/install_layout.js +83 -0
- package/dist/scripts/_lib/install_layout.js.map +1 -0
- package/dist/scripts/_lib/installed_lock.js +444 -0
- package/dist/scripts/_lib/installed_lock.js.map +1 -0
- package/dist/scripts/condense.js +13 -164
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +4 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +12 -6
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
- package/docs/guidelines/agent-infra/engineering-memory-data-format.md +49 -0
- package/docs/guidelines/agent-infra/memory-access.md +17 -0
- package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archival_sweep.ts +117 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
- package/src/agent-src/scripts/gate_budget.ts +282 -0
- package/src/agent-src/scripts/gate_execute.ts +72 -31
- package/src/agent-src/scripts/roadmap_gates.ts +74 -3
- package/src/agent-src/scripts/update_roadmap_progress.ts +251 -7
- package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/src/config/agent-settings.template.yml +27 -2
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/estate-count-budget.json +367 -3
- package/src/config/gate-violation-baselines.json +41 -41
- package/src/config/pack-size-budget.json +3 -2
- package/src/config/rule-activation-census.json +14 -6
- package/src/config/rule-stub-ceilings.json +19 -6
- package/src/install/claudePathsPlan.ts +196 -0
- package/src/install/hostLayerFingerprint.ts +145 -0
- package/src/install/partitionEligibility.ts +325 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
- package/src/scripts/_dispatch.bash +6 -1
- package/src/scripts/_lib/ac_heading.ts +30 -0
- package/src/scripts/_lib/agent_src.ts +42 -0
- package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
- package/src/scripts/_lib/envelope_grounding.ts +38 -8
- package/src/scripts/_lib/installed_lock.ts +50 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +85 -0
- package/src/scripts/_lib/md_section.ts +129 -0
- package/src/scripts/_lib/scope_guard.sh +86 -4
- package/src/scripts/_lib/self_repair.ts +19 -0
- package/src/scripts/_lib/self_repair_class_b.ts +171 -0
- package/src/scripts/_lib/subagent_capsule.ts +81 -1
- package/src/scripts/adr/regenerate_index.ts +13 -2
- package/src/scripts/adr_cite_check.ts +424 -0
- package/src/scripts/ai_council/cli_help.ts +55 -0
- package/src/scripts/before_complete_hook.ts +153 -28
- package/src/scripts/bench_ab_v2_complexity.ts +27 -7
- package/src/scripts/check_adr_frontmatter.ts +187 -1
- package/src/scripts/check_references.ts +60 -0
- package/src/scripts/check_release_published.ts +32 -11
- package/src/scripts/check_single_delivery.ts +517 -0
- package/src/scripts/ci_settle.ts +170 -0
- package/src/scripts/condense.ts +20 -170
- package/src/scripts/council_cli.ts +3 -3
- package/src/scripts/dispatch_r2_reviewer.ts +6 -14
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +77 -14
- package/src/scripts/hooks/block_no_verify.ts +61 -10
- package/src/scripts/hooks/block_unauthorized_git.ts +1 -1
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
- package/src/scripts/hooks/run_continuation_hook.ts +887 -68
- package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
- package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
- package/src/scripts/hooks/state_io.ts +782 -20
- package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
- package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
- package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
- package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
- package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
- package/src/scripts/install.sh +12 -2
- package/src/scripts/install.ts +16 -11
- package/src/scripts/language_mirror_hook.ts +561 -25
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_plan_risk_register.ts +4 -3
- package/src/scripts/lint_skill_router_head.ts +4 -4
- package/src/scripts/mcp_server/tools.ts +2 -2
- package/src/scripts/memory_consolidation.ts +189 -0
- package/src/scripts/memory_eviction.ts +384 -0
- package/src/scripts/new_roadmap.ts +136 -0
- package/src/scripts/pack_mcp_content.ts +20 -5
- package/src/scripts/release.ts +37 -38
- package/src/scripts/report_layer_overlap.ts +78 -0
- package/src/scripts/report_memory_pointers.ts +622 -0
- package/src/scripts/self_repair_hook.ts +37 -5
- package/src/scripts/session_register_hook.ts +115 -15
- package/src/scripts/skill_usage_collect.ts +11 -1
- package/src/scripts/skill_usage_report.ts +278 -8
- package/src/scripts/sync_pr_branch.ts +7 -0
- package/src/server/schemas/settings.ts +8 -0
- package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
- package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
- /package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
|
@@ -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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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:
|
|
39
|
-
* so such an entry is misclassified rather than
|
|
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
|
|
65
|
+
* Where the class-1 receipt ledger lives.
|
|
50
66
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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 =
|
|
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
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
313
|
+
`class 1 and ${verdict.detail}, so it renders instead of running`,
|
|
287
314
|
),
|
|
288
315
|
code: 0,
|
|
289
316
|
};
|
|
290
317
|
}
|
|
291
|
-
|
|
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
|
|
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
|
|
322
|
-
'Hard-Floor action under non-destructive-by-default.
|
|
323
|
-
'
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|