@event4u/agent-config 9.32.0 → 9.33.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 +96 -1
- package/dist/agent-src/commands/review/routing.md +3 -4
- package/dist/agent-src/contexts/communication/rules-auto/reply-close-mechanics.md +65 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +3 -0
- package/dist/agent-src/rules/delegation-policy.md +6 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +107 -3
- package/dist/agent-src/skills/docker/SKILL.md +11 -0
- package/dist/agent-src/skills/laravel/SKILL.md +11 -0
- package/dist/agent-src/skills/review-routing/SKILL.md +23 -6
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/ownership-map.example.yml +1 -1
- package/dist/cli/registry.js +4 -4
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-HTIU3W56.js +66 -0
- package/dist/cli-delegate/{chunk-ITIQSGOQ.js → chunk-MA4GFWDY.js} +7 -4
- package/dist/cli-delegate/{chunk-542DZR4P.js → chunk-PMQ6Z3T3.js} +1651 -144
- package/dist/cli-delegate/{chunk-PFBV6XRT.js → chunk-Q26HT4QT.js} +1 -63
- package/dist/cli-delegate/{chunk-LVRYBKTI.js → chunk-YPAWXXXR.js} +158 -0
- package/dist/cli-delegate/cmd_conformance.js +256 -226
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +19 -26
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +13 -3
- package/dist/cli-delegate/cmd_settings_get.js +4 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_settings_set.js +5 -3
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +8 -8
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +18 -0
- package/dist/install/install.mjs +7 -4
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3637 -1158
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +107 -3
- package/src/agent-src/templates/scripts/ownership-map.example.yml +1 -1
- package/src/config/cost-parity-budget.json +167 -0
- package/src/config/gate-coverage.yml +44 -5
- package/src/config/quorum-attendance-budget.json +39 -0
- package/src/scripts/_cli/cmd_doctor.ts +69 -1
- package/src/scripts/_cli/cmd_session_recycle.ts +19 -3
- package/src/scripts/_dispatch.bash +5 -3
- package/src/scripts/_lib/orchestration_record.ts +45 -0
- package/src/scripts/ai_council/clients.ts +31 -0
- package/src/scripts/ai_council/events_log.ts +128 -6
- package/src/scripts/ai_council/orchestrator.ts +12 -3
- package/src/scripts/ai_council/pricing.ts +48 -1
- package/src/scripts/ai_council/quorum.ts +20 -0
- package/src/scripts/ai_council/session.ts +11 -0
- package/src/scripts/ask_transport.ts +20 -1
- package/src/scripts/cache_realization_report.ts +38 -0
- package/src/scripts/check_release_highlights.ts +7 -1
- package/src/scripts/conformance_scan.ts +105 -0
- package/src/scripts/cost/track.mjs +39 -0
- package/src/scripts/cost_summary.ts +104 -1
- package/src/scripts/council_cli.ts +131 -13
- package/src/scripts/dispatch_economy_report.ts +5 -2
- package/src/scripts/install.ts +20 -6
- package/src/scripts/lint_budget_ownership.ts +81 -0
- package/src/scripts/lint_skill_frontmatter_safety.ts +55 -1
- package/src/scripts/lint_workflow_paths.ts +384 -0
- package/src/scripts/preamble_byte_census.ts +9 -1
- package/src/scripts/report_carrier_divergence.ts +10 -0
- package/src/scripts/report_conformance_funnel.ts +40 -0
- package/src/scripts/schemas/subagent.schema.json +3 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
3
|
"mcpName": "io.github.event4u-app/agent-config",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.33.0",
|
|
5
5
|
"description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including \"zero runtime daemon\".",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -18,10 +18,34 @@
|
|
|
18
18
|
* Blocking a pipeline on "the user has an open decision" would turn a
|
|
19
19
|
* visibility aid into a second thing to fight.
|
|
20
20
|
*
|
|
21
|
+
* `--reply` is the same projection shaped for the END OF A CHAT REPLY rather
|
|
22
|
+
* than a terminal pane, and it exists because the pull channel above is only
|
|
23
|
+
* read by someone who already knows to ask. Transcript forensics over three
|
|
24
|
+
* consumer sessions found five "what do I do now?" follow-ups, every one of
|
|
25
|
+
* them next to a blocker the agent HAD reported — as a file reference, as a
|
|
26
|
+
* bare option count, or as a choice of substitute work. What the same corpus
|
|
27
|
+
* also shows is that a tool's output gets carried into the reply verbatim
|
|
28
|
+
* (the dashboard's step count appears word-for-word inside eight option
|
|
29
|
+
* blocks). So the fix is not to make the agent format better from memory; it
|
|
30
|
+
* is to hand it the finished text. See ADR-222.
|
|
31
|
+
*
|
|
32
|
+
* Two properties carry the contract mechanically:
|
|
33
|
+
* - Nothing owned by the user open ⇒ EMPTY output. "No blocker → no block"
|
|
34
|
+
* stops being a judgement call, so the command is safe to call
|
|
35
|
+
* unconditionally at reply-close.
|
|
36
|
+
* - Exactly ONE blocker is rendered in full — the one that unblocks most.
|
|
37
|
+
* Every other one is a single trailing line. The user is never handed a
|
|
38
|
+
* flat list to weigh themselves.
|
|
39
|
+
*
|
|
40
|
+
* Labels are English like the rest of the CLI surface; the agent mirrors them
|
|
41
|
+
* into the user's language when it lifts them into a reply, exactly as it does
|
|
42
|
+
* for `Recommendation:` / `Empfehlung:`.
|
|
43
|
+
*
|
|
21
44
|
* Invocation (from project root):
|
|
22
45
|
* ./agent-config gates # decisions owned by the user
|
|
23
46
|
* ./agent-config gates --all # every open blocker, grouped by owner
|
|
24
47
|
* ./agent-config gates --json # machine-readable
|
|
48
|
+
* ./agent-config gates --reply # reply-close form; empty when none
|
|
25
49
|
*/
|
|
26
50
|
|
|
27
51
|
import * as path from 'node:path';
|
|
@@ -209,6 +233,76 @@ function render(entries: readonly Entry[], all: boolean): string {
|
|
|
209
233
|
return lines.join('\n') + '\n';
|
|
210
234
|
}
|
|
211
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Reply-close form: the one blocking decision in full, the rest as one line.
|
|
238
|
+
*
|
|
239
|
+
* Deliberately narrower than `render()`. `Status` and `Owner` are roadmap-file
|
|
240
|
+
* metadata that a reader of a reply cannot act on, so they are dropped; what
|
|
241
|
+
* survives is what the user has to DO and how they will know it is done. The
|
|
242
|
+
* five-field shape stays where it is enforced — in the roadmap file — and does
|
|
243
|
+
* not follow the blocker into prose it would only pad.
|
|
244
|
+
*
|
|
245
|
+
* Returns '' when nothing is owned by the user, so the caller can append the
|
|
246
|
+
* result unconditionally and get silence when silence is correct.
|
|
247
|
+
*/
|
|
248
|
+
function renderReply(entries: readonly Entry[]): string {
|
|
249
|
+
const mine = entries.filter((e) => needsUser(e.blocker.owner));
|
|
250
|
+
if (mine.length === 0) {
|
|
251
|
+
return '';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// `collectEntries` already sorted by unblocking weight, so the first entry
|
|
255
|
+
// IS the one that matters most. Picking it here rather than asking the
|
|
256
|
+
// caller to choose is the point: a flat list is the failure mode.
|
|
257
|
+
const lead = mine[0] as Entry;
|
|
258
|
+
const rest = mine.slice(1);
|
|
259
|
+
const lines: string[] = [];
|
|
260
|
+
|
|
261
|
+
lines.push(`Blocked: ${lead.blocker.blocks} — ${lead.roadmapRel}`);
|
|
262
|
+
|
|
263
|
+
const steps = regroupTodo(lead.blocker.todo);
|
|
264
|
+
const doLabel = isLegacy(lead.blocker) ? 'Blocked until:' : 'Do:';
|
|
265
|
+
if (steps.length === 0) {
|
|
266
|
+
lines.push(
|
|
267
|
+
`${doLabel} (no steps recorded — the blocker entry needs a **What to do:** list)`,
|
|
268
|
+
);
|
|
269
|
+
} else if (steps.length === 1) {
|
|
270
|
+
lines.push(...wrap(`${doLabel} ${steps[0] as string}`, WIDTH, ' '));
|
|
271
|
+
} else {
|
|
272
|
+
// Emitted as authored, never re-numbered: real blocker entries already
|
|
273
|
+
// carry their own `1.` / `2.` markers (the CI-enforced `What to do:`
|
|
274
|
+
// shape), so adding our own would produce "1. 1. …". An entry that
|
|
275
|
+
// arrives unnumbered stays unnumbered — that is a fact about the
|
|
276
|
+
// roadmap entry, and papering over it here would hide it from the
|
|
277
|
+
// person who could fix it.
|
|
278
|
+
lines.push(`${doLabel}`);
|
|
279
|
+
for (const s of steps) {
|
|
280
|
+
lines.push(...wrap(` ${s}`, WIDTH, ' '));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
lines.push(...wrap(`Done when: ${lead.blocker.resolvedWhen}`, WIDTH, ' '));
|
|
284
|
+
|
|
285
|
+
if (rest.length > 0) {
|
|
286
|
+
// A count, not a roster. Naming all of them reproduces the exact
|
|
287
|
+
// failure this form exists to end — "zwölf Entscheidungen bei Dir …
|
|
288
|
+
// rund 850 Zeilen", which the transcript shows produced no decision at
|
|
289
|
+
// all. The one that blocks is written out above; the rest are one
|
|
290
|
+
// command away, and saying so is shorter than listing them and more
|
|
291
|
+
// honest than hiding them.
|
|
292
|
+
lines.push('');
|
|
293
|
+
lines.push(
|
|
294
|
+
...wrap(
|
|
295
|
+
`${rest.length} other decision${rest.length !== 1 ? 's' : ''} also wait${rest.length === 1 ? 's' : ''} on you, ` +
|
|
296
|
+
'none blocking this — `agent-config gates`.',
|
|
297
|
+
WIDTH,
|
|
298
|
+
' ',
|
|
299
|
+
),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return lines.join('\n') + '\n';
|
|
304
|
+
}
|
|
305
|
+
|
|
212
306
|
function renderJson(entries: readonly Entry[], all: boolean): string {
|
|
213
307
|
const pick = all ? entries : entries.filter((e) => needsUser(e.blocker.owner));
|
|
214
308
|
return (
|
|
@@ -270,18 +364,28 @@ function main(argv?: readonly string[]): number {
|
|
|
270
364
|
const args = argv ?? process.argv.slice(2);
|
|
271
365
|
const all = args.includes('--all');
|
|
272
366
|
const json = args.includes('--json');
|
|
367
|
+
const reply = args.includes('--reply');
|
|
273
368
|
|
|
274
369
|
const repoRoot = _resolveRepoRoot(process.cwd());
|
|
275
370
|
const roadmapRoot = path.join(repoRoot, 'agents', 'roadmaps');
|
|
276
371
|
if (!_isDir(roadmapRoot)) {
|
|
372
|
+
// `--reply` stays silent here too: a project without roadmaps has no
|
|
373
|
+
// blocker to hand over, and a reply-close line saying so would be the
|
|
374
|
+
// ceremony the form exists to avoid.
|
|
277
375
|
process.stdout.write(
|
|
278
|
-
|
|
376
|
+
reply
|
|
377
|
+
? ''
|
|
378
|
+
: json
|
|
379
|
+
? '{"needsYou":0,"other":0,"blockers":[]}\n'
|
|
380
|
+
: 'No roadmaps directory — nothing to report.\n',
|
|
279
381
|
);
|
|
280
382
|
return 0;
|
|
281
383
|
}
|
|
282
384
|
|
|
283
385
|
const entries = collectEntries(roadmapRoot);
|
|
284
|
-
process.stdout.write(
|
|
386
|
+
process.stdout.write(
|
|
387
|
+
reply ? renderReply(entries) : json ? renderJson(entries, all) : render(entries, all),
|
|
388
|
+
);
|
|
285
389
|
return 0;
|
|
286
390
|
}
|
|
287
391
|
|
|
@@ -308,5 +412,5 @@ if (_isCliEntry()) {
|
|
|
308
412
|
process.exitCode = main();
|
|
309
413
|
}
|
|
310
414
|
|
|
311
|
-
export { main, needsUser, regroupTodo, wrap, collectEntries, render, renderJson };
|
|
415
|
+
export { main, needsUser, regroupTodo, wrap, collectEntries, render, renderJson, renderReply };
|
|
312
416
|
export type { Entry };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Copy this file to `.github/ownership-map.yml` (preferred) or
|
|
4
4
|
# `agents/ownership-map.yml` and adapt the entries to your stack.
|
|
5
5
|
# Omitting the file entirely is fine — routing falls back to the
|
|
6
|
-
#
|
|
6
|
+
# common role vocabulary in `skill:review-routing`.
|
|
7
7
|
#
|
|
8
8
|
# First match wins per file. List narrower globs before broader ones.
|
|
9
9
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Committed target table for the road-to-cost-parity-* family (part 0, Phase 1). Registers what a session costs today, what the family is steering toward, and what the rule payload actually is PER HOST — the correction that matters most, because every diet target in the source drafts was set against one aggregate number that describes no host. Nothing here changes behaviour: no carrier ships for these thresholds (part 0 Phase 4.5), and no gate reads the target columns. What IS gated is the file's own shape — lint_budget_ownership rejects a row without a `source`, and rejects a `revised_from` without `revision_evidence`, so a number cannot enter or move without a traceable origin.",
|
|
3
|
+
"schema_version": 1,
|
|
4
|
+
"registered_at": "2026-08-10",
|
|
5
|
+
"owner": "maintainer",
|
|
6
|
+
"review_by": "2026-11-10",
|
|
7
|
+
"row_schema": {
|
|
8
|
+
"_comment": "Reading of part 0 § 1.2b: `schema_version` / `registered_at` / `owner` / `review_by` are FILE-level (as in every sibling budget file — a per-row copy would be four constants repeated N times), and every ROW carries `source` plus the three honest-null fields from § 1.3. The validator enforces exactly that split.",
|
|
9
|
+
"required_row_fields": ["id", "unit", "baseline", "source", "revisable", "revision_evidence", "revised_from"],
|
|
10
|
+
"honest_null_contract": "A target bends to a measurement, never to a preference. `revisable: true` says the number is revisable at all; `revised_from` records what it used to be once it moves; `revision_evidence` must then point at the evidence note that forced the move. The validator fails the file when `revised_from` is set and `revision_evidence` is empty — a revision without published evidence is the one form of drift this schema exists to refuse."
|
|
11
|
+
},
|
|
12
|
+
"measurement": {
|
|
13
|
+
"_comment": "Both measurement rules from part 0's Context are fields here, not prose: a rule that lives only in a paragraph is a rule the next author does not inherit.",
|
|
14
|
+
"measurement_date": "2026-08-10",
|
|
15
|
+
"commit": "c073d5732fb890bbf8b41ec879f62f651c7ba293",
|
|
16
|
+
"projection_regenerated": true,
|
|
17
|
+
"regeneration_command": "task sync && task generate-tools — verified no-op afterwards (git status clean), so the payload rows below measure the commit and not when someone last ran a generator",
|
|
18
|
+
"emitter_generation": "real, frontmatter-less projection files (25 of 110 .claude/rules entries carry a `paths:` block; the rest carry no frontmatter). NOT the older emitter that symlinked .claude/rules/* into dist/agent-src/rules/ and so inherited dist's full frontmatter. This is the THIRD variable behind the 92-vs-110 file-count spread: a projection count is a fact about which emitter last wrote the tree, on top of how stale that tree is.",
|
|
19
|
+
"carrier_divergence_admitted": false,
|
|
20
|
+
"carrier_divergence_reason": "report_carrier_divergence compares a globally installed carrier against the project projection on disk; both can be stale independently, so it measures relative cache staleness rather than any property of the source. It may diagnose a behind-the-times install; it may never gate a phase, and no row here is derived from it."
|
|
21
|
+
},
|
|
22
|
+
"session_cost_targets": [
|
|
23
|
+
{
|
|
24
|
+
"id": "median_final_context_tokens",
|
|
25
|
+
"unit": "billable input tokens of the last main-chain assistant record (per cc_transcript.ts)",
|
|
26
|
+
"baseline": 519349,
|
|
27
|
+
"interim": 300000,
|
|
28
|
+
"target": 200000,
|
|
29
|
+
"direction": "lower_is_better",
|
|
30
|
+
"source": "agents/evidence/analysis/token-economy-recycling-phase1.md:45 — median over 201 sessions with parseable final usage (:44) out of 205 non-empty (:10)",
|
|
31
|
+
"revisable": true,
|
|
32
|
+
"revision_evidence": "",
|
|
33
|
+
"revised_from": null
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "p90_final_context_tokens",
|
|
37
|
+
"unit": "billable input tokens of the last main-chain assistant record",
|
|
38
|
+
"baseline": 807937,
|
|
39
|
+
"interim": 550000,
|
|
40
|
+
"target": 400000,
|
|
41
|
+
"direction": "lower_is_better",
|
|
42
|
+
"source": "agents/evidence/analysis/token-economy-recycling-phase1.md:45 — same 201-session corpus",
|
|
43
|
+
"revisable": true,
|
|
44
|
+
"revision_evidence": "",
|
|
45
|
+
"revised_from": null
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "auto_compact_incidence",
|
|
49
|
+
"unit": "share of sessions with >= 1 auto-compaction event",
|
|
50
|
+
"baseline": 0.112,
|
|
51
|
+
"interim": 0.05,
|
|
52
|
+
"target": 0.0,
|
|
53
|
+
"direction": "lower_is_better",
|
|
54
|
+
"source": "agents/evidence/analysis/token-economy-recycling-phase1.md:59 — 23 of 205 sessions, 31 events (:60), all trigger:\"auto\"",
|
|
55
|
+
"revisable": true,
|
|
56
|
+
"revision_evidence": "",
|
|
57
|
+
"revised_from": null
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "late_early_per_call_cost_ratio",
|
|
61
|
+
"unit": "median billable input per call over the last 20 calls / the first 20 calls of a session",
|
|
62
|
+
"baseline": 2.1,
|
|
63
|
+
"interim": 1.6,
|
|
64
|
+
"target": 1.3,
|
|
65
|
+
"direction": "lower_is_better",
|
|
66
|
+
"source": "agents/evidence/analysis/token-economy-recycling-phase1.md:69-75 — 145 sessions with >= 200 main-chain calls; median early 279,859 vs late 594,985; p90 ratio 3.3x",
|
|
67
|
+
"revisable": true,
|
|
68
|
+
"revision_evidence": "",
|
|
69
|
+
"revised_from": null
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"payload_rows": [
|
|
73
|
+
{
|
|
74
|
+
"_comment": "Per host, never one number. The aggregate the source drafts dieted against (\"419 KB always-loaded layer\") describes no host that exists: it is the whole rule tree, and only 9 of 116 rules are type: always.",
|
|
75
|
+
"id": "src_rules_maintained",
|
|
76
|
+
"unit": "bytes across src/rules/*.md",
|
|
77
|
+
"baseline": 424896,
|
|
78
|
+
"files": 116,
|
|
79
|
+
"target_bytes": null,
|
|
80
|
+
"target_owner": "none — this is the maintained source, not a delivered payload; it shrinks only as a consequence of a diet elsewhere",
|
|
81
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration (see `measurement`)",
|
|
82
|
+
"revisable": true,
|
|
83
|
+
"revision_evidence": "",
|
|
84
|
+
"revised_from": null
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "dist_agent_src_rules",
|
|
88
|
+
"unit": "bytes across dist/agent-src/rules/*.md",
|
|
89
|
+
"baseline": 420051,
|
|
90
|
+
"files": 115,
|
|
91
|
+
"target_bytes": null,
|
|
92
|
+
"target_owner": "none — the tracked projection source. It is the ONLY rule-payload figure that is stable across checkouts, because it is committed; every other row below is regenerated and therefore machine-dependent.",
|
|
93
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration",
|
|
94
|
+
"revisable": true,
|
|
95
|
+
"revision_evidence": "",
|
|
96
|
+
"revised_from": null
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "augment_rules_payload",
|
|
100
|
+
"unit": "bytes across .augment/rules/*.md",
|
|
101
|
+
"baseline": 420051,
|
|
102
|
+
"files": 115,
|
|
103
|
+
"target_bytes": null,
|
|
104
|
+
"target_owner": "src/config/budgets.yml § standing_rule_delivery (total_cap_tokens 110000) already owns the delivered-rule ceiling. This family does not open a second, byte-denominated target for the same surface — a metric with two owners is a metric with none.",
|
|
105
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration; mirrors dist/ by symlink",
|
|
106
|
+
"revisable": true,
|
|
107
|
+
"revision_evidence": "",
|
|
108
|
+
"revised_from": null
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "claude_rules_payload",
|
|
112
|
+
"unit": "bytes across .claude/rules/*.md",
|
|
113
|
+
"baseline": 339035,
|
|
114
|
+
"files": 110,
|
|
115
|
+
"target_bytes": null,
|
|
116
|
+
"target_owner": "src/config/budgets.yml § standing_rule_delivery — same ceiling, same reason as the augment row",
|
|
117
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration; 25 of 110 files carry a `paths:` block, the rest none",
|
|
118
|
+
"revisable": true,
|
|
119
|
+
"revision_evidence": "",
|
|
120
|
+
"revised_from": null
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"_comment": "CORRECTION to part 0's triage table, which recorded these two as \"~3 KB / ~0 KB\" and therefore as already-lean hosts needing no target. Re-measured post-regeneration they are 408 KB and 330 KB — a full payload each. The earlier reading is a stale-projection artefact of exactly the class measurement rule 1 warns about: both carriers are UNTRACKED and written only by `task generate-tools`, so a checkout that has not run it reads them as near-empty. Risk 3's mitigation (\"the lean hosts need no target\") does not survive the re-measure and is not encoded here.",
|
|
124
|
+
"id": "clinerules_payload",
|
|
125
|
+
"unit": "bytes across .clinerules/*.md",
|
|
126
|
+
"baseline": 417703,
|
|
127
|
+
"files": 111,
|
|
128
|
+
"target_bytes": null,
|
|
129
|
+
"target_owner": "unowned — a diet for this host belongs to the parked rule-payload part of this family, not to part 0",
|
|
130
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration; untracked, generated by task generate-tools",
|
|
131
|
+
"revisable": true,
|
|
132
|
+
"revision_evidence": "",
|
|
133
|
+
"revised_from": null
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "windsurfrules_payload",
|
|
137
|
+
"unit": "bytes of the single .windsurfrules file",
|
|
138
|
+
"baseline": 338160,
|
|
139
|
+
"files": 1,
|
|
140
|
+
"target_bytes": null,
|
|
141
|
+
"target_owner": "unowned — see the clinerules row; same correction, same parked owner",
|
|
142
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration; untracked, generated by task generate-tools",
|
|
143
|
+
"revisable": true,
|
|
144
|
+
"revision_evidence": "",
|
|
145
|
+
"revised_from": null
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"id": "gemini_md_payload",
|
|
149
|
+
"unit": "bytes of GEMINI.md",
|
|
150
|
+
"baseline": 2982,
|
|
151
|
+
"files": 1,
|
|
152
|
+
"target_bytes": null,
|
|
153
|
+
"target_owner": "none — genuinely lean, and the only host in this table for which that is true. It is a pointer file, not a rule payload.",
|
|
154
|
+
"source": "measured 2026-08-10 at commit c073d5732, post-regeneration",
|
|
155
|
+
"revisable": true,
|
|
156
|
+
"revision_evidence": "",
|
|
157
|
+
"revised_from": null
|
|
158
|
+
}
|
|
159
|
+
],
|
|
160
|
+
"anchors": {
|
|
161
|
+
"premium_band_tokens": 200000,
|
|
162
|
+
"rationale": "Above ~200k a session sits in the premium pricing band, which is why the median target is set there rather than at a round fraction of the baseline.",
|
|
163
|
+
"known_noise_problem": "187 of 205 observed sessions end at or above 200k, so a session-end hint at this threshold would fire on ~91% of sessions — a constant line, not an advisory. The 200k figure is a TARGET here and nothing else.",
|
|
164
|
+
"no_carrier_ships": "Part 0 § 4.5: no session-end advisory ships for this anchor. The existing 800k recycle threshold (src/config/recycle-threshold-budget.json) stays the only strong line, and any carrier for 200k is a separate, separately-argued change.",
|
|
165
|
+
"source": "agents/evidence/analysis/token-economy-recycling-phase1.md — session-final distribution (:45) over the 205-session corpus (:10)"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -40,13 +40,16 @@
|
|
|
40
40
|
# visible, or it repeats the sin it exists to catch.
|
|
41
41
|
# canary optional mutation recipe (Phase 7) — see CANARY below
|
|
42
42
|
#
|
|
43
|
-
# COVERAGE — THE HONEST DENOMINATOR (re-measured 2026-08-
|
|
43
|
+
# COVERAGE — THE HONEST DENOMINATOR (re-measured 2026-08-10)
|
|
44
44
|
# ----------------------------------------------------------
|
|
45
|
-
#
|
|
45
|
+
# 251 `lint_*` / `check_*` / `audit_*` scripts live under `src/scripts/`
|
|
46
46
|
# (the count is computed, not asserted — `check_gate_coverage.count_gate_scripts`).
|
|
47
|
-
# **
|
|
48
|
-
# CI-identical argv, and all
|
|
47
|
+
# **33 of them emit the machine-readable `scanned: <N>` line under their
|
|
48
|
+
# CI-identical argv, and all 33 are listed below.** The manifest is therefore
|
|
49
49
|
# complete with respect to its own contract, and covers ~13 % of the population.
|
|
50
|
+
# (The 2026-08-05 reading said "32 … and all 31 are listed": the two halves of
|
|
51
|
+
# one sentence disagreed by one, which is the drift this paragraph exists to
|
|
52
|
+
# make visible. Both are recounted above, not adjusted toward each other.)
|
|
50
53
|
#
|
|
51
54
|
# Two revisions of this header have now gone stale the same way. It claimed
|
|
52
55
|
# "8 of 211", then "22 of 223" while 26 entries were listed. Both halves drift
|
|
@@ -98,7 +101,7 @@
|
|
|
98
101
|
# which is the shape this repo tells a gate author to use — writes the line from
|
|
99
102
|
# inside the library, so its own source never matches and the test cannot see it.
|
|
100
103
|
# Live split: **1 unregistered gate emits the inline shape (`check_gate_coverage`
|
|
101
|
-
# itself), and
|
|
104
|
+
# itself), and 205 of 251 gate scripts emit through the helper.** So for the
|
|
102
105
|
# recommended shape the sentence above is false, and the list CAN fall behind.
|
|
103
106
|
#
|
|
104
107
|
# Not widened here, deliberately. Widening the regex to include the helper turns
|
|
@@ -557,6 +560,42 @@ gates:
|
|
|
557
560
|
untouched original stayed clean. Floor at 450 against a live 503 leaves
|
|
558
561
|
room for skill retirement without redding the change that retires one.
|
|
559
562
|
|
|
563
|
+
- id: lint_workflow_paths
|
|
564
|
+
argv: []
|
|
565
|
+
min_scanned: 20
|
|
566
|
+
corpus: "every workflow under .github/workflows (28 at baseline); the unit is the file, and each file's `paths:` / `paths-ignore:` entries are resolved against `git ls-files`"
|
|
567
|
+
status: enforced
|
|
568
|
+
canary:
|
|
569
|
+
class: undetected-antipattern
|
|
570
|
+
path: .github/workflows/zz-canary-workflow-paths.yml
|
|
571
|
+
content: |
|
|
572
|
+
name: Canary
|
|
573
|
+
on:
|
|
574
|
+
pull_request:
|
|
575
|
+
paths:
|
|
576
|
+
- "retired/tree/that/never/existed/**"
|
|
577
|
+
jobs:
|
|
578
|
+
noop:
|
|
579
|
+
runs-on: ubuntu-latest
|
|
580
|
+
steps:
|
|
581
|
+
- run: echo canary
|
|
582
|
+
note: >
|
|
583
|
+
Registered STRICT on day one for the same reason `lint_example_fences`
|
|
584
|
+
was, and on the same evidence standard: the classification pass ran
|
|
585
|
+
BEFORE the gate existed and returned **20 dead entries across 6 workflow
|
|
586
|
+
files**, each classified by hand into one of four causes — a tree retired
|
|
587
|
+
by ADR-051, a gitignored projection that can never appear in a diff, two
|
|
588
|
+
paths moved by the Python-to-TS migration, and bare root paths whose real
|
|
589
|
+
files sit under `src/`. All 20 were repaired in the change that added the
|
|
590
|
+
gate, so the corpus is verified empty rather than unexamined, and an
|
|
591
|
+
advisory gate over it could only ever be ignored. Floor at 20 against a
|
|
592
|
+
live 28 so retiring a workflow does not red the change that retires it.
|
|
593
|
+
Worth recording that the highest-severity hit was invisible to the hand
|
|
594
|
+
census and only the gate found it: `release-validation.yml` filtered on a
|
|
595
|
+
root `marketplace.json` that has never existed, so a version drift in the
|
|
596
|
+
real manifest did not re-trigger the `version-consistency` job whose
|
|
597
|
+
entire purpose is catching that drift.
|
|
598
|
+
|
|
560
599
|
- id: check_depth_budget
|
|
561
600
|
argv: []
|
|
562
601
|
min_scanned: 140
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Pre-registered council ATTENDANCE metrics (road-to-inbox-harvest-2026-08-b Phase 1.4). Registered here rather than in roadmap prose because a roadmap is archived when it closes and a metric outlives it — the sibling budget files (hook-token-budget.json, recycle-threshold-budget.json) already carry the owner / review_by / honest-gap discipline this reuses. The instrument is the quorum_result line appendQuorumEvent writes to agents/runtime/council/events.log (schema v2); nothing here is measured yet, and no threshold is committed before data exists.",
|
|
3
|
+
"schema_version": 1,
|
|
4
|
+
"registered_at": "2026-08-10",
|
|
5
|
+
"owner": "maintainer",
|
|
6
|
+
"review_by": "2026-11-10",
|
|
7
|
+
"unit": "quorum_result event lines in agents/runtime/council/events.log",
|
|
8
|
+
"privacy": "a quorum_result line carries {phase, command, dispatch, verdict, threshold, configured_total, total, present, solo, absent[{member, reason}]} — counts, closed vocabularies, and member ids only. The type has no field able to hold free-form content, so the CLI's own `detail` string (provider error text, which can embed paths and prompt fragments) is dropped at the boundary rather than scrubbed.",
|
|
9
|
+
"phase_split": "A pass emits `pre_run` (build_members — who could be constructed) and, on the `run` path, `post_run` (_postRunQuorum — who actually returned a usable response). They disagree whenever a member constructs and then fails mid-flight, so a rate computed over both double-counts. A `pre_run` line is NOT evidence a pass ran: `command: estimate` is a spend-free preview, and a `run` / `debate` line is written before the `--confirm` gate (which defaults to false and returns before any provider call) and before the necessity and size-fit gates. A `post_run` line is the only line that proves providers were called — which is why every rate below is post_run-scoped, and why `command` exists at all.",
|
|
10
|
+
"attendance_rate": {
|
|
11
|
+
"definition": "of the members that entered the pass, how many answered: present / total per post_run pass. Bounded by 1 by construction (evaluateQuorum clamps present into [0, total]).",
|
|
12
|
+
"instrument": "quorum_result lines, phase=post_run, command=run, fields present + total",
|
|
13
|
+
"threshold": "none committed before data — baseline first, threshold at review date"
|
|
14
|
+
},
|
|
15
|
+
"roster_shortfall_rate": {
|
|
16
|
+
"definition": "how often the council shrank BEFORE the pass: share of post_run passes with total < configured_total, plus the size of the gap. This is the half attendance_rate cannot see — a pass with 3 configured, 2 failing to construct and 1 answering reads 1/1 = 100 % attendance and a shortfall of 2. Both numbers are needed and neither substitutes for the other; a single ratio over mixed rosters is the error this pair replaces.",
|
|
17
|
+
"instrument": "quorum_result lines, phase=post_run, command=run, configured_total vs total",
|
|
18
|
+
"threshold": "none committed before data — baseline first, threshold at review date",
|
|
19
|
+
"not_an_error": "total > configured_total is legitimate and not a shortfall: `--siblings` fans ONE enabled config entry into N clients, so the client roster can exceed the entry count. configured_total counts enabled config ENTRIES; total counts CLIENTS. Compare them for direction, never divide one by the other."
|
|
20
|
+
},
|
|
21
|
+
"solo_conclusion_rate": {
|
|
22
|
+
"definition": "share of post_run passes with `solo: true` — written by the isSoloConcluded predicate in quorum.ts, never re-derived from the numbers here, so one change to the rule cannot leave this definition stale. ceil(n/2) makes 1-of-2 a legitimate conclusion; this measures how often that legitimacy is used, not whether it should be. THREE cases carry solo: true and they are not the same finding — a degraded multi-member council (configured_total > 1), a council configured with one member (configured_total = 1, dispatch = full), and a deliberate --single dispatch (dispatch = single). Split on dispatch and configured_total before reading the rate.",
|
|
23
|
+
"instrument": "quorum_result lines, phase=post_run, command=run, field solo, split by dispatch + configured_total",
|
|
24
|
+
"threshold": "none committed before data. The quorum-solo-floor blocker pre-registers the three outcomes this rate decides between — a third CLI member, a gate-scoped min_present: 2, or a published null under 5 % — and that choice is made against real data, never ahead of it. It is decided on the degraded case only; a --single pass is the operator asking for one voice, not the council failing to assemble."
|
|
25
|
+
},
|
|
26
|
+
"absent_reason_distribution": {
|
|
27
|
+
"definition": "count per reason over absent[] entries — no_binary / no_auth / timeout / quota (AbsentReason) plus the two runtime fallbacks the CLI writes directly, `unavailable` (transport resolved to nothing) and `binary_missing` (a CLI client refused to construct). Distinguishes a council degraded by configuration from one degraded by provider behaviour, which have opposite fixes.",
|
|
28
|
+
"instrument": "quorum_result lines, absent[].reason, split by phase (a pre_run absence is configuration; a post_run absence is mid-flight). Excluding `command: estimate` is NOT sufficient: `run` and `debate` also reach construction and return at the `--confirm` gate, which defaults to false. The reliable filter is a pre_run line whose pass produced a matching post_run line; a pre_run line with no post_run partner reached construction and stopped there, whatever its command says.",
|
|
29
|
+
"threshold": "none committed before data — the distribution IS the finding at review date",
|
|
30
|
+
"merge_before_reading": "`no_binary` and `binary_missing` are two spellings of one condition — the CLI binary is unusable — written by different code paths (transport_resolver's static classification vs the CLI client's construction failure). They are recorded separately because that is what the tree emits, not because they are separate findings; merge them before reading the distribution, or the largest bucket is split in two."
|
|
31
|
+
},
|
|
32
|
+
"honest_null_consequence": "If the log accumulates and the solo-conclusion rate sits near zero, that is the published answer to the quorum-solo-floor blocker and 1.5 is cancelled against it — not quietly dropped. If attendance data never accumulates at all (nobody runs a metered council pass in the window), that is reported as an unmeasured metric at the review date, never as a passing one.",
|
|
33
|
+
"honest_gaps": [
|
|
34
|
+
"`lens` and `invocation` are empty on every pre_run line — neither is known at member-construction time — so any per-lens breakdown is post_run-only.",
|
|
35
|
+
"`command: debate` carries the pre_run half ONLY. There is no _postRunQuorum on the debate path, and defining `usable response` for a multi-round debate is a separate question this registration does not answer. So debate contributes to absent_reason_distribution and to nothing else: attendance_rate and solo_conclusion_rate cover the `run` path, and a reader must not read them as covering every metered pass.",
|
|
36
|
+
"A pre_run line proves construction happened, never that a pass followed — the `--confirm` gate (default false), the necessity gate and the size-fit gate all abort afterwards and nothing on the line records it. Any count over pre_run lines is an upper bound; pair it with a post_run line before treating it as a pass.",
|
|
37
|
+
"The CLI emit wiring itself has no test. The two modules are covered directly, but nothing exercises build_members / cmd_run / cmd_debate end-to-end against a captured log — and `_emitQuorumEvent` forwards no AppendEventOptions, so there is no target to capture into. Two defects in this file's first version (a `dispatch` written from the flag rather than the outcome, and a rate mixing config entries with clients) were found by review rather than by a test, which is what that gap costs."
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -90,6 +90,8 @@ import * as installed_tools from '../_lib/installed_tools.js';
|
|
|
90
90
|
import * as user_global_paths from '../_lib/user_global_paths.js';
|
|
91
91
|
import * as global_deploy_inventory from '../_lib/global_deploy_inventory.js';
|
|
92
92
|
import { censusDuplicateScope } from '../_lib/duplicate_scope_census.js';
|
|
93
|
+
import * as preamble_byte_census from '../preamble_byte_census.js';
|
|
94
|
+
import * as dispatch_economy_report from '../dispatch_economy_report.js';
|
|
93
95
|
import { git_common_dir } from '../_lib/git_common_dir.js';
|
|
94
96
|
import * as sync_gitattributes from '../sync_gitattributes.js';
|
|
95
97
|
import {
|
|
@@ -3244,6 +3246,7 @@ interface Options {
|
|
|
3244
3246
|
check: string | null;
|
|
3245
3247
|
trace_root: boolean;
|
|
3246
3248
|
context: boolean;
|
|
3249
|
+
anatomy: boolean;
|
|
3247
3250
|
repair: string | null;
|
|
3248
3251
|
}
|
|
3249
3252
|
|
|
@@ -3254,13 +3257,15 @@ const PROG = 'agent-config doctor';
|
|
|
3254
3257
|
// terminal width; golden tests assert the `usage:` token + exit code only.
|
|
3255
3258
|
const USAGE =
|
|
3256
3259
|
`usage: ${PROG} [-h] [--project PROJECT] [--json] [--ci] [--check ID]\n` +
|
|
3257
|
-
' [--trace-root] [--context] [--
|
|
3260
|
+
' [--trace-root] [--context] [--anatomy]\n' +
|
|
3261
|
+
' [--repair ID]\n';
|
|
3258
3262
|
|
|
3259
3263
|
const _STORE_TRUE_FLAGS: Record<string, keyof Options> = {
|
|
3260
3264
|
'--json': 'json',
|
|
3261
3265
|
'--ci': 'ci',
|
|
3262
3266
|
'--trace-root': 'trace_root',
|
|
3263
3267
|
'--context': 'context',
|
|
3268
|
+
'--anatomy': 'anatomy',
|
|
3264
3269
|
};
|
|
3265
3270
|
|
|
3266
3271
|
const _VALUE_FLAGS: Record<string, keyof Options> = {
|
|
@@ -3283,6 +3288,7 @@ function _parse(argv: string[]): Options {
|
|
|
3283
3288
|
check: null,
|
|
3284
3289
|
trace_root: false,
|
|
3285
3290
|
context: false,
|
|
3291
|
+
anatomy: false,
|
|
3286
3292
|
repair: null,
|
|
3287
3293
|
};
|
|
3288
3294
|
|
|
@@ -3408,6 +3414,65 @@ function _run_context(opts: Options): number {
|
|
|
3408
3414
|
return 0;
|
|
3409
3415
|
}
|
|
3410
3416
|
|
|
3417
|
+
/**
|
|
3418
|
+
* `--anatomy` — the injection anatomy, rendered from measurements that already
|
|
3419
|
+
* exist. Composes `preamble_byte_census` (what occupies the always-loaded
|
|
3420
|
+
* preamble) with `dispatch_economy_report` (what the dispatch legs cost). It
|
|
3421
|
+
* takes NO new measurement and adds no threshold: both sources are advisory
|
|
3422
|
+
* reports and stay advisory here.
|
|
3423
|
+
*
|
|
3424
|
+
* Neither module runs anything at import time — `preamble_byte_census` gained
|
|
3425
|
+
* the `__AGENT_CONFIG_BUNDLE__` guard for exactly this import, since inside the
|
|
3426
|
+
* `build:cli-delegate` bundle its old entry check would have matched whenever
|
|
3427
|
+
* this command was invoked directly.
|
|
3428
|
+
*/
|
|
3429
|
+
function _run_anatomy(opts: Options): number {
|
|
3430
|
+
const census = preamble_byte_census;
|
|
3431
|
+
const economy = dispatch_economy_report;
|
|
3432
|
+
|
|
3433
|
+
const byteCensus = census.buildReport(census.parseArgs([]));
|
|
3434
|
+
|
|
3435
|
+
// The economy half reads transcripts and a registration file; either can be
|
|
3436
|
+
// absent on a consumer machine. An absent half is reported as absent, never
|
|
3437
|
+
// as a zero — a fabricated zero is the failure both source reports already
|
|
3438
|
+
// refuse to make.
|
|
3439
|
+
let economyReport: ReturnType<typeof economy.buildReport> | null = null;
|
|
3440
|
+
let economyError: string | null = null;
|
|
3441
|
+
try {
|
|
3442
|
+
economyReport = economy.buildReport({
|
|
3443
|
+
root: economy.DEFAULT_PROJECTS_ROOT,
|
|
3444
|
+
auditDir: path.join(process.cwd(), economy.DEFAULT_AUDIT_DIR),
|
|
3445
|
+
maxAgeDays: 14,
|
|
3446
|
+
});
|
|
3447
|
+
} catch (err) {
|
|
3448
|
+
economyError = err instanceof Error ? err.message : String(err);
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
if (opts.json) {
|
|
3452
|
+
print(
|
|
3453
|
+
_jsonDumpsIndentAscii(
|
|
3454
|
+
{
|
|
3455
|
+
preamble_byte_census: byteCensus,
|
|
3456
|
+
dispatch_economy: economyReport,
|
|
3457
|
+
dispatch_economy_unavailable: economyError,
|
|
3458
|
+
} as unknown as Dict,
|
|
3459
|
+
2,
|
|
3460
|
+
),
|
|
3461
|
+
);
|
|
3462
|
+
return 0;
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
print('── injection anatomy ─────────────────────────────────────────');
|
|
3466
|
+
print(census.renderText(byteCensus));
|
|
3467
|
+
print('');
|
|
3468
|
+
print(
|
|
3469
|
+
economyReport !== null
|
|
3470
|
+
? economy.renderText(economyReport)
|
|
3471
|
+
: `dispatch-economy report unavailable: ${economyError}`,
|
|
3472
|
+
);
|
|
3473
|
+
return 0;
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3411
3476
|
function _run_repair(opts: Options): number {
|
|
3412
3477
|
const target = opts.repair;
|
|
3413
3478
|
if (target === 'wizard-state') {
|
|
@@ -3464,6 +3529,9 @@ function main(argv: string[] | null = null): number {
|
|
|
3464
3529
|
if (opts.context) {
|
|
3465
3530
|
return _run_context(opts);
|
|
3466
3531
|
}
|
|
3532
|
+
if (opts.anatomy) {
|
|
3533
|
+
return _run_anatomy(opts);
|
|
3534
|
+
}
|
|
3467
3535
|
let project_root: string;
|
|
3468
3536
|
let origin: string;
|
|
3469
3537
|
try {
|
|
@@ -73,6 +73,7 @@ interface ParsedArgv {
|
|
|
73
73
|
message?: string;
|
|
74
74
|
file?: string;
|
|
75
75
|
template?: boolean;
|
|
76
|
+
verify?: boolean;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
export function parseArgv(argv: readonly string[]): ParsedArgv {
|
|
@@ -81,6 +82,8 @@ export function parseArgv(argv: readonly string[]): ParsedArgv {
|
|
|
81
82
|
const a = argv[i] as string;
|
|
82
83
|
if (a === '--template') {
|
|
83
84
|
parsed.template = true;
|
|
85
|
+
} else if (a === '--verify') {
|
|
86
|
+
parsed.verify = true;
|
|
84
87
|
} else if (a === '--file') {
|
|
85
88
|
const value = argv[i + 1];
|
|
86
89
|
if (!value) return { ok: false, message: '--file requires a path' };
|
|
@@ -102,7 +105,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgv {
|
|
|
102
105
|
*/
|
|
103
106
|
export function runSessionRecycle(
|
|
104
107
|
input: string,
|
|
105
|
-
opts: { cwd: string; now?: Date },
|
|
108
|
+
opts: { cwd: string; now?: Date; verify?: boolean },
|
|
106
109
|
): RecycleResult {
|
|
107
110
|
const out: string[] = [];
|
|
108
111
|
const err: string[] = [];
|
|
@@ -171,6 +174,15 @@ export function runSessionRecycle(
|
|
|
171
174
|
};
|
|
172
175
|
}
|
|
173
176
|
|
|
177
|
+
// `--verify` stops HERE: every rejection above has already run, and the
|
|
178
|
+
// only thing skipped is the write. Validating through a different path
|
|
179
|
+
// than the one that writes would make the check a second implementation
|
|
180
|
+
// to keep in sync — the failure this suite refuses elsewhere.
|
|
181
|
+
if (opts.verify === true) {
|
|
182
|
+
out.push(`recycle envelope VALID — ${bytes} bytes, not written (--verify).`);
|
|
183
|
+
return { code: 0, out, err };
|
|
184
|
+
}
|
|
185
|
+
|
|
174
186
|
const target = path.join(projectRoot, RECYCLE_ENVELOPE_REL);
|
|
175
187
|
try {
|
|
176
188
|
atomic_write_json(target, envelope);
|
|
@@ -193,9 +205,10 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
193
205
|
const parsed = parseArgv(argv);
|
|
194
206
|
if (!parsed.ok) {
|
|
195
207
|
const usage = [
|
|
196
|
-
'usage: agent-config session:recycle [--file <envelope.json>] [--template]',
|
|
208
|
+
'usage: agent-config session:recycle [--file <envelope.json>] [--template] [--verify]',
|
|
197
209
|
' --file <path> read the envelope JSON from a file (default: stdin)',
|
|
198
210
|
' --template print a skeleton envelope and exit',
|
|
211
|
+
' --verify validate only — same rejections, no write',
|
|
199
212
|
].join('\n');
|
|
200
213
|
if (parsed.message === 'usage') {
|
|
201
214
|
process.stdout.write(`${usage}\n`);
|
|
@@ -225,7 +238,10 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
225
238
|
return 2;
|
|
226
239
|
}
|
|
227
240
|
|
|
228
|
-
const result = runSessionRecycle(input, {
|
|
241
|
+
const result = runSessionRecycle(input, {
|
|
242
|
+
cwd: process.cwd(),
|
|
243
|
+
...(parsed.verify === true ? { verify: true } : {}),
|
|
244
|
+
});
|
|
229
245
|
for (const line of result.out) process.stdout.write(`${line}\n`);
|
|
230
246
|
for (const line of result.err) process.stderr.write(`${line}\n`);
|
|
231
247
|
return result.code;
|