@event4u/agent-config 9.32.0 → 9.34.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 +85 -72
- package/dist/agent-src/commands/agent-handoff.md +15 -3
- 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/archive_completed_roadmaps.ts +35 -1
- package/dist/agent-src/scripts/roadmap_gates.ts +107 -3
- package/dist/agent-src/skills/corpus-grounding/SKILL.md +21 -12
- package/dist/agent-src/skills/design-tokens/SKILL.md +15 -0
- 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/react-shadcn-ui/SKILL.md +24 -0
- package/dist/agent-src/skills/review-routing/SKILL.md +23 -6
- package/dist/agent-src/skills/rule-writing/SKILL.md +23 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +80 -2
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +5 -3
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +24 -0
- 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-2WELHO3P.js → chunk-I5EAFRII.js} +35 -0
- package/dist/cli-delegate/{chunk-ITIQSGOQ.js → chunk-MA4GFWDY.js} +7 -4
- package/dist/cli-delegate/{chunk-PFBV6XRT.js → chunk-Q26HT4QT.js} +1 -63
- package/dist/cli-delegate/{chunk-542DZR4P.js → chunk-REMEZSVP.js} +1660 -146
- package/dist/cli-delegate/{chunk-3EWYC3YK.js → chunk-UEZ7D35A.js} +4 -3
- 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 +21 -28
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +20 -5
- 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 +17 -17
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +2 -2
- package/dist/discovery/dormancy-report.md +17 -0
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +3 -3
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +3 -3
- package/dist/hooks/dispatch.js +114 -7
- package/dist/install/install.mjs +7 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3755 -1168
- package/docs/guidelines/agent-infra/failure-signatures.md +47 -12
- package/package.json +2 -2
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +35 -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 +25 -4
- package/src/scripts/_dispatch.bash +5 -3
- package/src/scripts/_lib/env_kill_switch.ts +37 -0
- package/src/scripts/_lib/orchestration_record.ts +45 -0
- package/src/scripts/_lib/security_lint.ts +18 -4
- package/src/scripts/_lib/self_repair.ts +60 -0
- package/src/scripts/_lib/self_repair_store.ts +65 -5
- package/src/scripts/_lib/subagent_capsule.ts +78 -1
- package/src/scripts/ai_council/clients.ts +31 -0
- package/src/scripts/ai_council/events_log.ts +130 -8
- package/src/scripts/ai_council/orchestrator.ts +50 -3
- package/src/scripts/ai_council/pricing.ts +48 -1
- package/src/scripts/ai_council/prompts.ts +165 -3
- package/src/scripts/ai_council/quorum.ts +30 -0
- package/src/scripts/ai_council/session.ts +11 -0
- package/src/scripts/ai_team/review_gate.ts +2 -2
- package/src/scripts/ask_transport.ts +20 -1
- package/src/scripts/build_discovery_manifest.ts +165 -0
- package/src/scripts/cache_realization_report.ts +38 -0
- package/src/scripts/check_claims.ts +51 -2
- package/src/scripts/check_release_highlights.ts +7 -1
- package/src/scripts/ci_time_ratio.ts +3 -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/discovery_graph.ts +201 -27
- package/src/scripts/dispatch_economy_report.ts +5 -2
- package/src/scripts/dispatch_r2_reviewer.ts +102 -1
- package/src/scripts/install.ts +20 -6
- package/src/scripts/lint_budget_ownership.ts +81 -0
- package/src/scripts/lint_handoffs.ts +92 -2
- package/src/scripts/lint_hedge_words.ts +317 -0
- package/src/scripts/lint_mcp_config_security.ts +1 -1
- package/src/scripts/lint_skill_frontmatter_safety.ts +56 -2
- 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/src/scripts/self_repair_cli.ts +26 -1
- package/src/scripts/skill_linter.ts +58 -2
|
@@ -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 {
|
|
@@ -57,8 +57,13 @@ export function templateEnvelope(): Record<string, unknown> {
|
|
|
57
57
|
acceptance_criteria: ['<what done means, one line each>'],
|
|
58
58
|
remaining: ['<open work, one line each>'],
|
|
59
59
|
not_carried_forward: ['<what the successor must re-derive from source>'],
|
|
60
|
-
decisions: ['<decision — one-line rationale>'],
|
|
60
|
+
decisions: ['<decision — one-line rationale> [reversible]'],
|
|
61
61
|
constraints: ['<binding constraint>'],
|
|
62
|
+
// Ships EMPTY, unlike the prose lists above: the entries are path refs,
|
|
63
|
+
// and a `<placeholder>` left in by a hurried writer would be a fake path
|
|
64
|
+
// rather than obvious filler. An empty list is also the honest default —
|
|
65
|
+
// nothing off limits is the common case.
|
|
66
|
+
do_not_touch: [],
|
|
62
67
|
open_worker_envelopes: [],
|
|
63
68
|
artifact_paths: ['<path to a deliverable / note / evidence file>'],
|
|
64
69
|
assumptions: [],
|
|
@@ -73,6 +78,7 @@ interface ParsedArgv {
|
|
|
73
78
|
message?: string;
|
|
74
79
|
file?: string;
|
|
75
80
|
template?: boolean;
|
|
81
|
+
verify?: boolean;
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
export function parseArgv(argv: readonly string[]): ParsedArgv {
|
|
@@ -81,6 +87,8 @@ export function parseArgv(argv: readonly string[]): ParsedArgv {
|
|
|
81
87
|
const a = argv[i] as string;
|
|
82
88
|
if (a === '--template') {
|
|
83
89
|
parsed.template = true;
|
|
90
|
+
} else if (a === '--verify') {
|
|
91
|
+
parsed.verify = true;
|
|
84
92
|
} else if (a === '--file') {
|
|
85
93
|
const value = argv[i + 1];
|
|
86
94
|
if (!value) return { ok: false, message: '--file requires a path' };
|
|
@@ -102,7 +110,7 @@ export function parseArgv(argv: readonly string[]): ParsedArgv {
|
|
|
102
110
|
*/
|
|
103
111
|
export function runSessionRecycle(
|
|
104
112
|
input: string,
|
|
105
|
-
opts: { cwd: string; now?: Date },
|
|
113
|
+
opts: { cwd: string; now?: Date; verify?: boolean },
|
|
106
114
|
): RecycleResult {
|
|
107
115
|
const out: string[] = [];
|
|
108
116
|
const err: string[] = [];
|
|
@@ -171,6 +179,15 @@ export function runSessionRecycle(
|
|
|
171
179
|
};
|
|
172
180
|
}
|
|
173
181
|
|
|
182
|
+
// `--verify` stops HERE: every rejection above has already run, and the
|
|
183
|
+
// only thing skipped is the write. Validating through a different path
|
|
184
|
+
// than the one that writes would make the check a second implementation
|
|
185
|
+
// to keep in sync — the failure this suite refuses elsewhere.
|
|
186
|
+
if (opts.verify === true) {
|
|
187
|
+
out.push(`recycle envelope VALID — ${bytes} bytes, not written (--verify).`);
|
|
188
|
+
return { code: 0, out, err };
|
|
189
|
+
}
|
|
190
|
+
|
|
174
191
|
const target = path.join(projectRoot, RECYCLE_ENVELOPE_REL);
|
|
175
192
|
try {
|
|
176
193
|
atomic_write_json(target, envelope);
|
|
@@ -193,9 +210,10 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
193
210
|
const parsed = parseArgv(argv);
|
|
194
211
|
if (!parsed.ok) {
|
|
195
212
|
const usage = [
|
|
196
|
-
'usage: agent-config session:recycle [--file <envelope.json>] [--template]',
|
|
213
|
+
'usage: agent-config session:recycle [--file <envelope.json>] [--template] [--verify]',
|
|
197
214
|
' --file <path> read the envelope JSON from a file (default: stdin)',
|
|
198
215
|
' --template print a skeleton envelope and exit',
|
|
216
|
+
' --verify validate only — same rejections, no write',
|
|
199
217
|
].join('\n');
|
|
200
218
|
if (parsed.message === 'usage') {
|
|
201
219
|
process.stdout.write(`${usage}\n`);
|
|
@@ -225,7 +243,10 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
225
243
|
return 2;
|
|
226
244
|
}
|
|
227
245
|
|
|
228
|
-
const result = runSessionRecycle(input, {
|
|
246
|
+
const result = runSessionRecycle(input, {
|
|
247
|
+
cwd: process.cwd(),
|
|
248
|
+
...(parsed.verify === true ? { verify: true } : {}),
|
|
249
|
+
});
|
|
229
250
|
for (const line of result.out) process.stdout.write(`${line}\n`);
|
|
230
251
|
for (const line of result.err) process.stderr.write(`${line}\n`);
|
|
231
252
|
return result.code;
|
|
@@ -130,7 +130,7 @@ Tier 1 — power-user (release shape, audit, migration):
|
|
|
130
130
|
user-authored files; persists standing consent
|
|
131
131
|
(install.auto_converge) on first use.
|
|
132
132
|
Flags: --dry-run | --yes
|
|
133
|
-
conformance:behavior Replay local transcripts through the mechanised conformance checks
|
|
133
|
+
conformance:behavior Replay local transcripts through the mechanised conformance checks (--why <id> traces one)
|
|
134
134
|
conformance Consumer conformance contract: doctor --ci +
|
|
135
135
|
installed-and-firing checks. Exits non-zero on
|
|
136
136
|
a failed check.
|
|
@@ -197,7 +197,7 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
|
|
|
197
197
|
session register). Flags: --json
|
|
198
198
|
sessions:claim Claim a roadmap for this session so other sessions skip
|
|
199
199
|
it; --release clears the claim
|
|
200
|
-
session:recycle Validate + write the main-session recycle envelope
|
|
200
|
+
session:recycle Validate + write the main-session recycle envelope (--verify validates only)
|
|
201
201
|
(deliberate recycle instead of auto-compact). Flags:
|
|
202
202
|
--file <json> | --template; default reads stdin
|
|
203
203
|
roadmap:progress Regenerate agents/roadmaps-progress.md from open roadmaps
|
|
@@ -207,7 +207,9 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
|
|
|
207
207
|
gates Open decisions that need you, rendered as actions —
|
|
208
208
|
roadmap blockers filtered by owner, most-unblocking
|
|
209
209
|
first. Flags: --all (include maintainer/external),
|
|
210
|
-
--json
|
|
210
|
+
--json, --reply (reply-close form: the one blocking
|
|
211
|
+
decision in full, the rest as a count; prints
|
|
212
|
+
nothing when none are yours)
|
|
211
213
|
capabilities:index Regenerate CAPABILITIES.yaml — the package coverage index
|
|
212
214
|
(capability area → coverage → backing skills/commands → gaps).
|
|
213
215
|
Pass --check to fail if stale (for CI). Reads src/ (package repo).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One definition of "is this env kill-switch on?".
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS: the predicate was written out twice, byte-for-byte —
|
|
5
|
+
* `ai_council/events_log.ts` and `ai_team/review_gate.ts` each carried a
|
|
6
|
+
* private `_kill_switch_active()` with the identical body AND the identical
|
|
7
|
+
* env var (`AGENT_CONFIG_NO_EVENTS_LOG`). `diff` over the two blocks was empty
|
|
8
|
+
* and no shared helper existed. Two copies of a boolean coercion is two places
|
|
9
|
+
* for the next accepted spelling to land in only one.
|
|
10
|
+
*
|
|
11
|
+
* The subtle half is why `'false'` is listed at all: `process.env` values are
|
|
12
|
+
* strings, so the obvious `Boolean(process.env.X)` reads the *string* `'false'`
|
|
13
|
+
* as true and silently arms a switch the operator explicitly disarmed. The
|
|
14
|
+
* off-set is therefore explicit — unset, `''`, `'0'`, `'false'`, `'False'` —
|
|
15
|
+
* and everything else is on, so a typo fails toward the safer direction (the
|
|
16
|
+
* switch suppresses a log; a spurious suppression is visible, a spurious write
|
|
17
|
+
* is not).
|
|
18
|
+
*
|
|
19
|
+
* SCOPE, stated because the source over-claimed it. `grep -rn "=== 'false'"
|
|
20
|
+
* src/scripts/` returns 10 hits; only the two migrated here share one
|
|
21
|
+
* predicate. The others are deliberately different and stay put:
|
|
22
|
+
* `orchestration_record.ts` is a CLI-flag coercion whose invalid-string
|
|
23
|
+
* fall-through is documented, `turn_end_gate_hook.ts` additionally accepts
|
|
24
|
+
* `yes`/`on`, and `validate_frontmatter.ts` is a YAML scalar parser. Folding
|
|
25
|
+
* those in would change three behaviours to remove three lines.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** The env values that mean "off". Everything else — including a typo — is on. */
|
|
29
|
+
const _OFF_VALUES: ReadonlySet<string> = new Set(['', '0', 'false', 'False']);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* True when the named environment variable is set to anything outside the
|
|
33
|
+
* off-set. Unset reads as off.
|
|
34
|
+
*/
|
|
35
|
+
export function isEnvKillSwitchActive(name: string): boolean {
|
|
36
|
+
return !_OFF_VALUES.has(process.env[name] ?? '');
|
|
37
|
+
}
|
|
@@ -64,6 +64,20 @@ export interface RecordInput {
|
|
|
64
64
|
dispatch_tokens?: number | null | undefined;
|
|
65
65
|
/** The orchestrator's own tier — the baseline the downshift cost-% measures against. */
|
|
66
66
|
session_tier?: string | null | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* The model id that was ASKED for. `tier_chosen`, `tier_source`,
|
|
69
|
+
* `session_tier` and the downshift cost-percentage are all read off the
|
|
70
|
+
* requested tier, so on an alias or a provider substitution every one of
|
|
71
|
+
* them attributes the saving to a model that never ran. Recording the
|
|
72
|
+
* requested id beside the served one is what makes that detectable.
|
|
73
|
+
*/
|
|
74
|
+
model_requested?: string | null | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* The model id the provider reported SERVING. `''` / null when the
|
|
77
|
+
* transport reports none (every CLI client) — absent is honest, not a
|
|
78
|
+
* defect, and is NOT a divergence.
|
|
79
|
+
*/
|
|
80
|
+
model_served?: string | null | undefined;
|
|
67
81
|
dispatch_outcome?: DispatchOutcome | undefined;
|
|
68
82
|
verify_mode?: VerifyMode | undefined;
|
|
69
83
|
/** QUALITY: subagent return adopted without parent rework. Optional boolean (null = not measured). */
|
|
@@ -176,6 +190,23 @@ function isInt(n: unknown): n is number {
|
|
|
176
190
|
return typeof n === 'number' && Number.isFinite(n) && Number.isInteger(n);
|
|
177
191
|
}
|
|
178
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Did the provider answer with a different model than the one requested?
|
|
195
|
+
*
|
|
196
|
+
* `null` — not decidable — whenever either id is missing or empty. That is the
|
|
197
|
+
* common case (every CLI transport reports no served id) and it is not the
|
|
198
|
+
* same as "no divergence": a `false` here would claim a comparison that never
|
|
199
|
+
* happened, which is the exact mis-attribution this field exists to expose.
|
|
200
|
+
*/
|
|
201
|
+
function modelDivergent(
|
|
202
|
+
requested: string | null | undefined,
|
|
203
|
+
served: string | null | undefined,
|
|
204
|
+
): boolean | null {
|
|
205
|
+
if (typeof requested !== 'string' || typeof served !== 'string') return null;
|
|
206
|
+
if (!requested || !served) return null;
|
|
207
|
+
return requested !== served;
|
|
208
|
+
}
|
|
209
|
+
|
|
179
210
|
/**
|
|
180
211
|
* Build + validate the audit-log-v1 line. Returns `{line, errors}`; when
|
|
181
212
|
* `errors` is non-empty, `line` is null and the caller must NOT write.
|
|
@@ -290,6 +321,13 @@ export function buildOrchestrationLine(input: RecordInput): BuiltLine {
|
|
|
290
321
|
errors.push(`trigger_arm_earlier must be one of ${TRIGGER_ARMS.join(' | ')} (or omitted / null when neither arm fired)`);
|
|
291
322
|
}
|
|
292
323
|
|
|
324
|
+
for (const [key, v] of [
|
|
325
|
+
['model_requested', input.model_requested],
|
|
326
|
+
['model_served', input.model_served],
|
|
327
|
+
] as const) {
|
|
328
|
+
if (v != null && typeof v !== 'string') errors.push(`${key} must be a string or omitted`);
|
|
329
|
+
}
|
|
330
|
+
|
|
293
331
|
if (!input.ts) errors.push('ts (ISO-8601 UTC) is required');
|
|
294
332
|
if (!input.id) errors.push('id (ULID or content hash) is required');
|
|
295
333
|
|
|
@@ -310,6 +348,13 @@ export function buildOrchestrationLine(input: RecordInput): BuiltLine {
|
|
|
310
348
|
tier_source: input.tier_source ?? null,
|
|
311
349
|
dispatch_tokens: input.dispatch_tokens ?? null,
|
|
312
350
|
session_tier: input.session_tier ?? null,
|
|
351
|
+
// Served-model attribution. `model_divergent` is DERIVED, never
|
|
352
|
+
// supplied: `null` when either id is absent — a transport that reports
|
|
353
|
+
// no served id cannot disagree with anything, and calling that `false`
|
|
354
|
+
// would read as "checked, and they matched".
|
|
355
|
+
model_requested: input.model_requested ?? null,
|
|
356
|
+
model_served: input.model_served ?? null,
|
|
357
|
+
model_divergent: modelDivergent(input.model_requested, input.model_served),
|
|
313
358
|
first_pass_success: input.first_pass_success ?? null,
|
|
314
359
|
escalated: input.escalated ?? null,
|
|
315
360
|
agent_combo: input.agent_combo ?? [],
|
|
@@ -408,10 +408,13 @@ export function* iter_corpus(
|
|
|
408
408
|
* Exit 1 iff at least one finding `is_fail` (HIGH + full weight). WARN-level
|
|
409
409
|
* (weighted-down or < HIGH) findings print but never fail the build.
|
|
410
410
|
*/
|
|
411
|
-
export function report(
|
|
411
|
+
export function report(
|
|
412
|
+
findings: readonly Finding[],
|
|
413
|
+
opts: { check_label: string; scanned_roots?: readonly string[] },
|
|
414
|
+
): number {
|
|
412
415
|
const check_label = opts.check_label;
|
|
413
416
|
if (findings.length === 0) {
|
|
414
|
-
process.stdout.write(`✅ ${check_label}: clean (${_corpus_note()}).\n`);
|
|
417
|
+
process.stdout.write(`✅ ${check_label}: clean (${_corpus_note(opts.scanned_roots)}).\n`);
|
|
415
418
|
return 0;
|
|
416
419
|
}
|
|
417
420
|
|
|
@@ -459,8 +462,19 @@ function _pyG(n: number): string {
|
|
|
459
462
|
return s;
|
|
460
463
|
}
|
|
461
464
|
|
|
462
|
-
|
|
463
|
-
|
|
465
|
+
// The clean-path line names the corpus the caller ACTUALLY walked, not the
|
|
466
|
+
// shared default. Two of five callers diverge from `DEFAULT_SCAN_ROOTS`
|
|
467
|
+
// (`lint_skill_frontmatter_safety` replaces the set outright,
|
|
468
|
+
// `lint_mcp_config_security` appends `src/templates`), so a hardcoded note made
|
|
469
|
+
// this gate report a scope in both directions at once: it claimed `src/rules`
|
|
470
|
+
// and `dist/agent-src`, which it never reads, and omitted `src/subagents`,
|
|
471
|
+
// which is the one root holding the artefact its over-broad-grant check exists
|
|
472
|
+
// for. A reader verifying that root by running the gate read the opposite of
|
|
473
|
+
// the truth. Same contract as `_lib/scan_scope.reportScanned` — the emitted
|
|
474
|
+
// scope is the scope that was walked.
|
|
475
|
+
function _corpus_note(scanned_roots?: readonly string[]): string {
|
|
476
|
+
const roots = scanned_roots && scanned_roots.length > 0 ? scanned_roots : DEFAULT_SCAN_ROOTS;
|
|
477
|
+
return 'scanned ' + roots.join(', ');
|
|
464
478
|
}
|
|
465
479
|
|
|
466
480
|
// ---------------------------------------------------------------------
|
|
@@ -538,6 +538,66 @@ export function fingerprint(defect_class: DefectClass, evidence: string): string
|
|
|
538
538
|
}
|
|
539
539
|
|
|
540
540
|
/** Fold a finding into an existing record, or open a new one. */
|
|
541
|
+
// ── creation rate cap ──────────────────────────────────────────────
|
|
542
|
+
//
|
|
543
|
+
// `upsertFinding` folds on fingerprint, so a *recurring* defect increments one
|
|
544
|
+
// record and adds no file. What nothing bounded is **creation**: the fingerprint
|
|
545
|
+
// is `defect_class + evidence`, and evidence is a quoted span from the turn, so a
|
|
546
|
+
// defect whose span varies per turn mints a fresh record every time. The only
|
|
547
|
+
// cap in the module pair was `MAX_EVIDENCE`, which bounds a span's *length* and
|
|
548
|
+
// says nothing about how many records may exist.
|
|
549
|
+
//
|
|
550
|
+
// The cap is per `source` because the two intake paths have different plausible
|
|
551
|
+
// rates and a runaway on one must not consume the other's headroom: a
|
|
552
|
+
// `user-reported` record needs the user to complain, and `self-detected` fires at
|
|
553
|
+
// most once per turn per detector. `NEW_RECORDS_PER_SOURCE_PER_WINDOW` sits above
|
|
554
|
+
// any plausible day (three detector classes, a few evidence variants each) and
|
|
555
|
+
// far below a runaway (one per turn over a long session).
|
|
556
|
+
export const NEW_RECORDS_PER_SOURCE_PER_WINDOW = 20;
|
|
557
|
+
export const CREATION_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* How many records of `source` were CREATED inside the window ending at `now`.
|
|
561
|
+
* Counts `first_seen`, never `last_seen` — an old record still being folded into
|
|
562
|
+
* is not creation and must not consume the budget.
|
|
563
|
+
*/
|
|
564
|
+
export function recentCreations(
|
|
565
|
+
existing: readonly DefectRecord[],
|
|
566
|
+
source: DefectSource,
|
|
567
|
+
now: string,
|
|
568
|
+
): number {
|
|
569
|
+
const end = Date.parse(now);
|
|
570
|
+
if (Number.isNaN(end)) {
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
return existing.filter((r) => {
|
|
574
|
+
if (r.source !== source) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
const t = Date.parse(r.first_seen);
|
|
578
|
+
// An unparseable stamp is counted: a malformed record is exactly what a
|
|
579
|
+
// runaway writer produces, and failing open there would uncap the store.
|
|
580
|
+
return Number.isNaN(t) || (end - t < CREATION_WINDOW_MS && end - t >= 0);
|
|
581
|
+
}).length;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Whether opening a NEW record for `finding` is over budget. A fingerprint that
|
|
586
|
+
* already exists is a fold, never a creation, so it is never capped — the cap
|
|
587
|
+
* must not silence a defect the store is already tracking.
|
|
588
|
+
*/
|
|
589
|
+
export function creationCapReached(
|
|
590
|
+
existing: readonly DefectRecord[],
|
|
591
|
+
finding: DefectFinding,
|
|
592
|
+
now: string,
|
|
593
|
+
): boolean {
|
|
594
|
+
const fp = fingerprint(finding.defect_class, finding.evidence);
|
|
595
|
+
if (existing.some((r) => r.fingerprint === fp)) {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
return recentCreations(existing, finding.source, now) >= NEW_RECORDS_PER_SOURCE_PER_WINDOW;
|
|
599
|
+
}
|
|
600
|
+
|
|
541
601
|
export function mergeRecord(
|
|
542
602
|
existing: DefectRecord | null,
|
|
543
603
|
finding: DefectFinding,
|
|
@@ -11,7 +11,14 @@
|
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
12
|
import * as path from 'node:path';
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
creationCapReached,
|
|
16
|
+
type DefectFinding,
|
|
17
|
+
type DefectRecord,
|
|
18
|
+
type DefectSource,
|
|
19
|
+
fingerprint,
|
|
20
|
+
mergeRecord,
|
|
21
|
+
} from './self_repair.js';
|
|
15
22
|
|
|
16
23
|
export const STORE_REL = path.join('agents', 'runtime', 'self-repair');
|
|
17
24
|
|
|
@@ -61,7 +68,10 @@ export function listRecords(root: string): DefectRecord[] {
|
|
|
61
68
|
}
|
|
62
69
|
const out: DefectRecord[] = [];
|
|
63
70
|
for (const n of names.sort()) {
|
|
64
|
-
|
|
71
|
+
// The overflow counter shares the directory and the extension but is not
|
|
72
|
+
// a record. `isRecord` would reject it anyway; naming it here keeps the
|
|
73
|
+
// exclusion intentional rather than incidental.
|
|
74
|
+
if (!n.endsWith('.json') || n === OVERFLOW_FILE) {
|
|
65
75
|
continue;
|
|
66
76
|
}
|
|
67
77
|
const rec = readRecord(root, n.slice(0, -'.json'.length));
|
|
@@ -77,13 +87,63 @@ export function openRecords(root: string): DefectRecord[] {
|
|
|
77
87
|
return listRecords(root).filter((r) => r.status === 'open');
|
|
78
88
|
}
|
|
79
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Per-source counters for findings the creation cap refused. ONE file for the
|
|
92
|
+
* whole store, so the overflow record is bounded by construction — it is the
|
|
93
|
+
* counter a runaway writer increments instead of the file it would otherwise
|
|
94
|
+
* mint. Deliberately NOT a `DefectRecord`: an overflow is not a defect and must
|
|
95
|
+
* not need a `DefectClass`, an issue-form dropdown entry, or an egress route.
|
|
96
|
+
*/
|
|
97
|
+
export const OVERFLOW_FILE = '_overflow.json';
|
|
98
|
+
|
|
99
|
+
export type OverflowCounts = Partial<Record<DefectSource, { dropped: number; last_seen: string }>>;
|
|
100
|
+
|
|
101
|
+
function overflowPath(root: string): string {
|
|
102
|
+
return path.join(storeDir(root), OVERFLOW_FILE);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function readOverflow(root: string): OverflowCounts {
|
|
106
|
+
try {
|
|
107
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(overflowPath(root), 'utf-8'));
|
|
108
|
+
return typeof parsed === 'object' && parsed !== null ? (parsed as OverflowCounts) : {};
|
|
109
|
+
} catch {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function bumpOverflow(root: string, source: DefectSource, now: string): void {
|
|
115
|
+
const counts = readOverflow(root);
|
|
116
|
+
const prev = counts[source]?.dropped ?? 0;
|
|
117
|
+
counts[source] = { dropped: prev + 1, last_seen: now };
|
|
118
|
+
const dir = storeDir(root);
|
|
119
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
120
|
+
fs.writeFileSync(overflowPath(root), `${JSON.stringify(counts, null, 2)}\n`, 'utf-8');
|
|
121
|
+
}
|
|
122
|
+
|
|
80
123
|
/**
|
|
81
124
|
* Fold a finding into the store: increments the existing record when the
|
|
82
|
-
* fingerprint matches, otherwise opens a new one. Returns the stored record
|
|
125
|
+
* fingerprint matches, otherwise opens a new one. Returns the stored record, or
|
|
126
|
+
* `null` when the per-source creation cap refused to open a new one.
|
|
127
|
+
*
|
|
128
|
+
* A refusal is COUNTED, never silent — `readOverflow` carries the per-source
|
|
129
|
+
* drop tally, so "the cap fired" is a readable fact rather than a defect that
|
|
130
|
+
* disappeared. A cap that quietly discarded reports would break this loop's own
|
|
131
|
+
* Iron Law (a defect is queued, never shrugged off) in the name of bounding it.
|
|
83
132
|
*/
|
|
84
|
-
export function upsertFinding(
|
|
133
|
+
export function upsertFinding(
|
|
134
|
+
root: string,
|
|
135
|
+
finding: DefectFinding,
|
|
136
|
+
now: string,
|
|
137
|
+
): DefectRecord | null {
|
|
85
138
|
const fp = fingerprint(finding.defect_class, finding.evidence);
|
|
86
|
-
const
|
|
139
|
+
const existing = readRecord(root, fp);
|
|
140
|
+
// The fold path never consults the cap: a fingerprint already in the store
|
|
141
|
+
// adds no file, so capping it would silence a tracked defect for no bound.
|
|
142
|
+
if (existing === null && creationCapReached(listRecords(root), finding, now)) {
|
|
143
|
+
bumpOverflow(root, finding.source, now);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const merged = mergeRecord(existing, finding, now);
|
|
87
147
|
writeRecord(root, merged);
|
|
88
148
|
return merged;
|
|
89
149
|
}
|
|
@@ -232,10 +232,35 @@ export interface MainSessionRecycleEnvelope {
|
|
|
232
232
|
* of silent. Required; an empty list is a claim, not a default.
|
|
233
233
|
*/
|
|
234
234
|
not_carried_forward: string[];
|
|
235
|
-
/**
|
|
235
|
+
/**
|
|
236
|
+
* Choices already made, each with its one-line rationale in the same line.
|
|
237
|
+
*
|
|
238
|
+
* A line MAY close with a reversibility tag — `[reversible]` or
|
|
239
|
+
* `[irreversible]` — so the successor can tell "we picked A over B, easy to
|
|
240
|
+
* revisit" from "we already migrated the data". The tag is OPTIONAL, so no
|
|
241
|
+
* committed envelope is retroactively invalid; when present its spelling is
|
|
242
|
+
* validated, which is what keeps it a checkable field rather than a doctrine
|
|
243
|
+
* nobody can verify. No claim is made that it improves resumption — that
|
|
244
|
+
* stays the registered, unmeasured `envelope_resume_success` metric.
|
|
245
|
+
*/
|
|
236
246
|
decisions?: string[];
|
|
237
247
|
/** Binding constraints the successor must not silently re-open. */
|
|
238
248
|
constraints?: string[];
|
|
249
|
+
/**
|
|
250
|
+
* Paths the successor must NOT modify — a parallel worktree, a file another
|
|
251
|
+
* session holds, a generated projection whose source lives elsewhere.
|
|
252
|
+
*
|
|
253
|
+
* Distinct from `constraints`, which carries decisions in prose: this is a
|
|
254
|
+
* list of path refs, so "was this path off limits?" is answerable by
|
|
255
|
+
* comparison instead of by reading. That checkability is the whole reason it
|
|
256
|
+
* is a field rather than a sentence — the same bar the handoff-envelope
|
|
257
|
+
* adjudication set (a field whose presence is checkable survives where a
|
|
258
|
+
* doctrine whose effect is unmeasured did not).
|
|
259
|
+
*
|
|
260
|
+
* Optional: an envelope with nothing off limits states nothing here, and an
|
|
261
|
+
* empty list is not a claim that everything is writable.
|
|
262
|
+
*/
|
|
263
|
+
do_not_touch?: string[];
|
|
239
264
|
/** Open worker CHECKPOINT envelopes by path — never inlined bodies. */
|
|
240
265
|
open_worker_envelopes?: string[];
|
|
241
266
|
/** Artifact paths (deliverables, notes, evidence) — refs, never bodies. */
|
|
@@ -444,6 +469,54 @@ export function hasBoundaryMarker(block: string): boolean {
|
|
|
444
469
|
);
|
|
445
470
|
}
|
|
446
471
|
|
|
472
|
+
/** The two accepted reversibility tags, exact spellings. */
|
|
473
|
+
export const DECISION_REVERSIBILITY_TAGS: readonly string[] = ['reversible', 'irreversible'];
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Validate the OPTIONAL trailing reversibility tag on a decision line.
|
|
477
|
+
*
|
|
478
|
+
* Fires only on a line that closes with a bracket whose content is trying to be
|
|
479
|
+
* one of the two tags — `[reversble]`, `[Irreversible!]`, `[reversible?]`. A
|
|
480
|
+
* bracket used for anything else (`[ADR-109]`, `[see #1273]`) is left alone, and
|
|
481
|
+
* a line with no bracket at all is the untagged default. So the check catches a
|
|
482
|
+
* typo in a tag someone MEANT to write, which is the only failure a
|
|
483
|
+
* presence-checkable optional field can actually have: a silently misspelled tag
|
|
484
|
+
* reads as "untagged" and loses the distinction the tag exists to carry.
|
|
485
|
+
*/
|
|
486
|
+
export function decisionTagErrors(decisions: unknown): string[] {
|
|
487
|
+
if (!Array.isArray(decisions)) {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
const errors: string[] = [];
|
|
491
|
+
for (const line of decisions) {
|
|
492
|
+
if (typeof line !== 'string') {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const m = /\[([^\]]*)\]\s*$/.exec(line.trimEnd());
|
|
496
|
+
if (m === null) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const inner = (m[1] ?? '').trim();
|
|
500
|
+
const lowered = inner.toLowerCase();
|
|
501
|
+
if (DECISION_REVERSIBILITY_TAGS.includes(lowered)) {
|
|
502
|
+
if (inner !== lowered) {
|
|
503
|
+
errors.push(
|
|
504
|
+
`decisions: reversibility tag must be lower-case — got \`[${inner}]\`, expected \`[${lowered}]\``,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (/^i?rr?ever[a-z]*[^a-z]*$/i.test(lowered)) {
|
|
510
|
+
errors.push(
|
|
511
|
+
`decisions: \`[${inner}]\` is not a reversibility tag — use exactly ` +
|
|
512
|
+
DECISION_REVERSIBILITY_TAGS.map((t) => `\`[${t}]\``).join(' or ') +
|
|
513
|
+
', or drop the bracket',
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return errors;
|
|
518
|
+
}
|
|
519
|
+
|
|
447
520
|
/** Every key `validateRecycleEnvelope` accepts — anything else is schema-invalid. */
|
|
448
521
|
const RECYCLE_ENVELOPE_KEYS: ReadonlySet<string> = new Set([
|
|
449
522
|
'capsule_version',
|
|
@@ -457,6 +530,7 @@ const RECYCLE_ENVELOPE_KEYS: ReadonlySet<string> = new Set([
|
|
|
457
530
|
'not_carried_forward',
|
|
458
531
|
'decisions',
|
|
459
532
|
'constraints',
|
|
533
|
+
'do_not_touch',
|
|
460
534
|
'open_worker_envelopes',
|
|
461
535
|
'artifact_paths',
|
|
462
536
|
'assumptions',
|
|
@@ -519,7 +593,10 @@ export function validateRecycleEnvelope(input: unknown): string[] {
|
|
|
519
593
|
checkList(errors, 'remaining', e['remaining'], MAX_LINE_CHARS, true);
|
|
520
594
|
checkList(errors, 'not_carried_forward', e['not_carried_forward'], MAX_LINE_CHARS, true);
|
|
521
595
|
checkList(errors, 'decisions', e['decisions'], MAX_LINE_CHARS, false);
|
|
596
|
+
errors.push(...decisionTagErrors(e['decisions']));
|
|
522
597
|
checkList(errors, 'constraints', e['constraints'], MAX_LINE_CHARS, false);
|
|
598
|
+
// Path refs, so the REF budget applies — not the prose one `constraints` uses.
|
|
599
|
+
checkList(errors, 'do_not_touch', e['do_not_touch'], MAX_REF_CHARS, false);
|
|
523
600
|
checkList(errors, 'open_worker_envelopes', e['open_worker_envelopes'], MAX_REF_CHARS, false);
|
|
524
601
|
checkList(errors, 'artifact_paths', e['artifact_paths'], MAX_REF_CHARS, false);
|
|
525
602
|
checkList(errors, 'suggested_skills', e['suggested_skills'], MAX_REF_CHARS, false);
|