@event4u/agent-config 14.7.0 → 14.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +245 -388
- package/README.md +1 -1
- package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
- package/dist/agent-src/commands/evals/roadmap.json +17 -1
- package/dist/agent-src/commands/pr/create.md +14 -2
- package/dist/agent-src/commands/pr/merge.md +379 -0
- package/dist/agent-src/commands/roadmap/process-full.md +157 -3
- package/dist/agent-src/commands/roadmap.md +1 -1
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +64 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
- package/dist/agent-src/rules/recurring-criticism.md +1 -1
- package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -1
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
- package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
- package/dist/agent-src/skills/security/SKILL.md +8 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +20 -2
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
- package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-EEJUMCDZ.js} +14 -14
- package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
- package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +14 -14
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +7 -7
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_handoff.js +7 -6
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_route_audit.js +6 -5
- package/dist/cli-delegate/cmd_route_explain.js +3 -3
- package/dist/cli-delegate/cmd_session_recycle.js +3 -3
- package/dist/cli-delegate/cmd_settings_get.js +3 -3
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +53 -22
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -6
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -5
- package/dist/hooks/dispatch.js +806 -681
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.js.map +1 -0
- package/dist/install/partitionEligibility.js +156 -0
- package/dist/install/partitionEligibility.js.map +1 -1
- package/dist/install/ruleLayerPartition.js +157 -0
- package/dist/install/ruleLayerPartition.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +654 -578
- package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
- package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
- package/dist/scripts/condense.js +52 -89
- package/dist/scripts/condense.js.map +1 -1
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/false-green.md +47 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/src/config/ci-local-parity.yml +21 -0
- package/src/config/estate-count-budget.json +275 -2
- package/src/config/evaluator-budgets.json +3 -3
- package/src/config/gate-coverage.yml +220 -12
- package/src/config/gate-violation-baselines.json +73 -63
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/rule-stub-ceilings.json +17 -3
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/partitionEligibility.ts +177 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_dispatch.bash +11 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -0
- package/src/scripts/_lib/envelope_grounding.ts +11 -1
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- package/src/scripts/adr/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +12 -32
- package/src/scripts/adr_cite_check.ts +540 -39
- package/src/scripts/audit_adr_coverage.ts +103 -17
- package/src/scripts/bench_hook_latency.ts +204 -0
- package/src/scripts/build_archive_index.ts +34 -0
- package/src/scripts/check_adr_frontmatter.ts +574 -39
- package/src/scripts/check_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -0
- package/src/scripts/check_references.ts +18 -0
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_single_delivery.ts +137 -4
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/condense.ts +61 -91
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/hooks/block_unauthorized_git.ts +22 -2
- package/src/scripts/hooks/dispatch_hook.ts +154 -187
- package/src/scripts/hooks/dispatch_issues.ts +38 -19
- package/src/scripts/hooks/fallback_yaml.ts +66 -0
- package/src/scripts/hooks/host_semantics.ts +21 -0
- package/src/scripts/hooks/injection_budget.ts +13 -3
- package/src/scripts/hooks/py_json_dumps.ts +103 -0
- package/src/scripts/hooks/state_io.ts +88 -1
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- package/src/scripts/injection_scan_hook.ts +137 -5
- package/src/scripts/lint_hook_manifest.ts +49 -0
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- package/src/scripts/routing_doctor.ts +5 -1
- package/src/scripts/sync_pr_branch.ts +163 -20
- package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
- package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
|
@@ -16,12 +16,19 @@
|
|
|
16
16
|
* --regen-area-readme <area> rewrite docs/adrs/<area>/README.md. Idempotent.
|
|
17
17
|
*
|
|
18
18
|
* Historical quirks are preserved deliberately — tests and downstream consumers pin the exact behaviour.
|
|
19
|
+
*
|
|
20
|
+
* Record metadata is read by `read_adr_meta`: the shared frontmatter reader
|
|
21
|
+
* (`_lib/adr_frontmatter`) when a `---` block is present, the transitional
|
|
22
|
+
* blockquote header otherwise. The seven per-area records carried only the
|
|
23
|
+
* blockquote until 2026-08-21, so both paths have to work while the two
|
|
24
|
+
* surfaces overlap.
|
|
19
25
|
*/
|
|
20
26
|
import * as fs from 'node:fs';
|
|
21
27
|
import * as path from 'node:path';
|
|
22
28
|
import process from 'node:process';
|
|
23
29
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
30
|
|
|
31
|
+
import { readAdrFrontmatterScalars } from './_lib/adr_frontmatter.js';
|
|
25
32
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
26
33
|
|
|
27
34
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -89,10 +96,26 @@ export function unlistedAreaDirs(adrsRoot: string): string[] {
|
|
|
89
96
|
|
|
90
97
|
// ^(\d{4})-([a-z0-9-]+)\.md$
|
|
91
98
|
const NAMED = /^(\d{4})-([a-z0-9-]+)\.md$/;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Metadata keys this script recognises in the transitional blockquote header.
|
|
102
|
+
*
|
|
103
|
+
* An allowlist rather than "every `Key: value` pair in a blockquote", because
|
|
104
|
+
* the blockquote block also carries prose that happens to contain colons — the
|
|
105
|
+
* `Roadmap:` pointer, a `Contract:` link, and (on `memory/0001`) a whole
|
|
106
|
+
* supersession banner. Harvesting those would put link markup into the README
|
|
107
|
+
* table. Keys are matched case-insensitively with spaces folded to `_`.
|
|
108
|
+
*/
|
|
109
|
+
const BLOCKQUOTE_KEYS = new Set([
|
|
110
|
+
'adr',
|
|
111
|
+
'area',
|
|
112
|
+
'status',
|
|
113
|
+
'date',
|
|
114
|
+
'decision',
|
|
115
|
+
'supersedes',
|
|
116
|
+
'superseded_by',
|
|
117
|
+
'type',
|
|
118
|
+
]);
|
|
96
119
|
|
|
97
120
|
interface AdrEntry {
|
|
98
121
|
num: string;
|
|
@@ -129,24 +152,87 @@ function _globMdSorted(dir: string): string[] {
|
|
|
129
152
|
return out;
|
|
130
153
|
}
|
|
131
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Read the leading `---` frontmatter block, `{}` when there is none.
|
|
157
|
+
*
|
|
158
|
+
* Delegates to the one shared reader (`_lib/adr_frontmatter`) rather than
|
|
159
|
+
* keeping this script's own regex: the tree carried three divergent ADR
|
|
160
|
+
* frontmatter parsers plus this one, and the two nested axes (`provenance`,
|
|
161
|
+
* `evidence`) are exactly the shape a `^([a-z_]+):\s*(.+?)$` regex reads as
|
|
162
|
+
* absent.
|
|
163
|
+
*
|
|
164
|
+
* What is preserved: the `{}`-on-absent return, and the scalar keys this
|
|
165
|
+
* script's callers read.
|
|
166
|
+
*
|
|
167
|
+
* What changed, stated because "preserved" was claimed for it and is false:
|
|
168
|
+
* the removed `_stripChars(v, ' "\'')` stripped repeated and unbalanced quote
|
|
169
|
+
* and space characters from both ends (Python `str.strip` semantics), while the
|
|
170
|
+
* shared reader's `stripQuotes` is `/^["'](.*)["']$/` — exactly one balanced
|
|
171
|
+
* pair. Two inputs now differ: `key: "value` keeps its opening quote (was
|
|
172
|
+
* `value`), and `key: ''v''` yields `'v'` (was `v`). No record in the corpus
|
|
173
|
+
* exercises either shape today, and the old behaviour is deliberately NOT
|
|
174
|
+
* restored — one shared reader beats a bug-compatible local copy.
|
|
175
|
+
*/
|
|
132
176
|
export function parse_fm(text: string): Record<string, string> {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
177
|
+
return readAdrFrontmatterScalars(text) ?? {};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Read the transitional blockquote metadata header.
|
|
182
|
+
*
|
|
183
|
+
* The seven per-area records predate the frontmatter contract and carry their
|
|
184
|
+
* metadata as a blockquote line instead:
|
|
185
|
+
*
|
|
186
|
+
* ```
|
|
187
|
+
* > Area: `router` · Status: accepted · Date: 2026-05-16 · Type: retrospective
|
|
188
|
+
* ```
|
|
189
|
+
*
|
|
190
|
+
* Two shapes have to be handled and both occur in the tree. Several fields
|
|
191
|
+
* share one line separated by `·`, so the split is on the separator first and
|
|
192
|
+
* the first `:` second. And the value is often wrapped in backticks
|
|
193
|
+
* (`` `router` ``), which are stripped — a table cell showing `` `accepted` ``
|
|
194
|
+
* would not match what the frontmatter path produces for the same record.
|
|
195
|
+
*
|
|
196
|
+
* Scanning stops at the first `## ` heading: everything above it is the
|
|
197
|
+
* metadata header, everything below is body prose whose blockquotes are
|
|
198
|
+
* quotations, not metadata.
|
|
199
|
+
*/
|
|
200
|
+
export function parse_blockquote_meta(text: string): Record<string, string> {
|
|
137
201
|
const out: Record<string, string> = {};
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
202
|
+
for (const raw of text.split('\n')) {
|
|
203
|
+
const line = raw.trim();
|
|
204
|
+
if (line.startsWith('## ')) break;
|
|
205
|
+
if (!line.startsWith('>')) continue;
|
|
206
|
+
for (const part of line.replace(/^>\s*/, '').split('·')) {
|
|
207
|
+
const idx = part.indexOf(':');
|
|
208
|
+
if (idx <= 0) continue;
|
|
209
|
+
const key = part.slice(0, idx).trim().toLowerCase().replace(/\s+/g, '_');
|
|
210
|
+
if (!BLOCKQUOTE_KEYS.has(key)) continue;
|
|
211
|
+
const value = _stripChars(part.slice(idx + 1), ' "\'`');
|
|
212
|
+
if (value === '') continue;
|
|
213
|
+
// First occurrence wins — a later body line never overwrites the header.
|
|
214
|
+
if (!(key in out)) out[key] = value;
|
|
215
|
+
}
|
|
146
216
|
}
|
|
147
217
|
return out;
|
|
148
218
|
}
|
|
149
219
|
|
|
220
|
+
/**
|
|
221
|
+
* One record's metadata: frontmatter when present, the blockquote header
|
|
222
|
+
* otherwise.
|
|
223
|
+
*
|
|
224
|
+
* Frontmatter wins outright rather than merging. During the transition a record
|
|
225
|
+
* carries both, and the blockquote is explicitly the human-readable duplicate —
|
|
226
|
+
* merging would make the authoritative surface depend on which of the two
|
|
227
|
+
* happened to mention a field, and a stale duplicate would then be able to
|
|
228
|
+
* contribute a value the frontmatter had already corrected.
|
|
229
|
+
*/
|
|
230
|
+
export function read_adr_meta(text: string): Record<string, string> {
|
|
231
|
+
const fm = readAdrFrontmatterScalars(text);
|
|
232
|
+
if (fm !== null && Object.keys(fm).length > 0) return fm;
|
|
233
|
+
return parse_blockquote_meta(text);
|
|
234
|
+
}
|
|
235
|
+
|
|
150
236
|
/** Mirror Python `str.strip(chars)`. */
|
|
151
237
|
function _stripChars(s: string, chars: string): string {
|
|
152
238
|
let start = 0;
|
|
@@ -193,7 +279,7 @@ export function scan_area(area: string): [AdrEntry[], string[]] {
|
|
|
193
279
|
errs.push(`${area}/${base}: filename does not match NNNN-<slug>.md`);
|
|
194
280
|
continue;
|
|
195
281
|
}
|
|
196
|
-
const fm =
|
|
282
|
+
const fm = read_adr_meta(fs.readFileSync(p, 'utf-8'));
|
|
197
283
|
adrs.push({ num: m[1] as string, slug: m[2] as string, path: base, ...fm });
|
|
198
284
|
}
|
|
199
285
|
// Gap check.
|
|
@@ -230,6 +230,159 @@ export function benchEvent(
|
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/**
|
|
234
|
+
* One dispatcher invocation that reads stdin, parses it, and exits — the
|
|
235
|
+
* transport-isolation cell of the § 2 matrix.
|
|
236
|
+
*
|
|
237
|
+
* `b-payload-read-parse-dominates`, option (a), council 2026-08-20 (2/2
|
|
238
|
+
* quorum). Phase 1 removed ten of eleven envelope stringifies and moved
|
|
239
|
+
* nothing; Phase 2 removed the body from six of eleven concerns and moved
|
|
240
|
+
* nothing. What is left between the small and large cells happens ONCE per
|
|
241
|
+
* event, before any concern runs: `readFd0ToEnd` drains the pipe and
|
|
242
|
+
* `_build_envelope` parses it. This measures exactly that, through the SAME
|
|
243
|
+
* bundle, interpreter and spawn as the slot reading it is a share of — so the
|
|
244
|
+
* constant terms cancel in the delta.
|
|
245
|
+
*
|
|
246
|
+
* Deliberately the real bundle with `--read-exit` rather than a standalone
|
|
247
|
+
* probe: a probe would re-implement the audited retrying reader, and that copy
|
|
248
|
+
* is the drift `hook_stdin` exists to prevent.
|
|
249
|
+
*/
|
|
250
|
+
export function benchReadExit(
|
|
251
|
+
event: string,
|
|
252
|
+
runs: number,
|
|
253
|
+
workspace: string,
|
|
254
|
+
): EventResult {
|
|
255
|
+
const durations: number[] = [];
|
|
256
|
+
const payload = syntheticPayload(event, workspace);
|
|
257
|
+
for (let i = 0; i < runs; i += 1) {
|
|
258
|
+
const started = performance.now();
|
|
259
|
+
const proc = spawnSync(
|
|
260
|
+
process.execPath,
|
|
261
|
+
[
|
|
262
|
+
BUNDLE_OVERRIDE ?? BUNDLE,
|
|
263
|
+
'--platform',
|
|
264
|
+
'claude',
|
|
265
|
+
'--event',
|
|
266
|
+
event,
|
|
267
|
+
'--project-dir',
|
|
268
|
+
workspace,
|
|
269
|
+
'--read-exit',
|
|
270
|
+
],
|
|
271
|
+
{
|
|
272
|
+
input: payload,
|
|
273
|
+
encoding: 'utf-8',
|
|
274
|
+
env: { ...process.env, AGENT_CONFIG_REPLAY: '1', CLAUDE_PROJECT_DIR: workspace },
|
|
275
|
+
timeout: 60000,
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
const elapsed = performance.now() - started;
|
|
279
|
+
if (proc.error) {
|
|
280
|
+
throw new Error(`read-exit failed on ${event}: ${proc.error.message}`);
|
|
281
|
+
}
|
|
282
|
+
if (!(proc.stderr ?? '').includes('--read-exit ok')) {
|
|
283
|
+
// A silent no-op here would report the fastest possible number and
|
|
284
|
+
// measure nothing — the failure mode this whole cell exists to avoid
|
|
285
|
+
// in the OTHER direction. Refuse instead of publishing it.
|
|
286
|
+
throw new Error(
|
|
287
|
+
`read-exit produced no confirmation on ${event}; the flag may not be ` +
|
|
288
|
+
`wired in the measured bundle (${BUNDLE_OVERRIDE ?? BUNDLE}). ` +
|
|
289
|
+
`stderr: ${(proc.stderr ?? '').trim().slice(0, 200)}`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
durations.push(elapsed);
|
|
293
|
+
}
|
|
294
|
+
durations.sort((a, b) => a - b);
|
|
295
|
+
return {
|
|
296
|
+
event: `read_exit:${event}`,
|
|
297
|
+
runs,
|
|
298
|
+
p50_ms: Math.round(percentile(durations, 50)),
|
|
299
|
+
p95_ms: Math.round(percentile(durations, 95)),
|
|
300
|
+
max_ms: Math.round(durations[durations.length - 1] as number),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** One row of the transport-share decomposition. */
|
|
305
|
+
export interface ReadExitCell {
|
|
306
|
+
event: string;
|
|
307
|
+
payload_bytes: number;
|
|
308
|
+
slot_small_p50_ms: number;
|
|
309
|
+
slot_large_p50_ms: number;
|
|
310
|
+
read_exit_small_p50_ms: number;
|
|
311
|
+
read_exit_large_p50_ms: number;
|
|
312
|
+
/** Large-minus-small on the full dispatch. */
|
|
313
|
+
slot_delta_ms: number;
|
|
314
|
+
/** Large-minus-small on read + parse alone. */
|
|
315
|
+
transport_delta_ms: number;
|
|
316
|
+
/**
|
|
317
|
+
* `transport_delta / slot_delta`, as a percentage, or `null` when the slot
|
|
318
|
+
* delta is not positive.
|
|
319
|
+
*
|
|
320
|
+
* `null` is a real answer and must not be rendered as 0: a run whose large
|
|
321
|
+
* cell is no slower than its small one says the fixture did not reproduce
|
|
322
|
+
* the gap on this machine, which is different from saying transport is free.
|
|
323
|
+
*/
|
|
324
|
+
transport_share_pct: number | null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The four measurements the option asks for, in one invocation.
|
|
329
|
+
*
|
|
330
|
+
* Both payload sizes are needed for the SHARE, and taking them in one run is
|
|
331
|
+
* the point: two separate runs would compare across whatever else the machine
|
|
332
|
+
* was doing, which is the cross-runner shape § 2 of the roadmap refuses as a
|
|
333
|
+
* repo fact. Arms alternate per repetition for the same reason.
|
|
334
|
+
*/
|
|
335
|
+
export function benchReadExitCell(
|
|
336
|
+
event: string,
|
|
337
|
+
runs: number,
|
|
338
|
+
workspace: string,
|
|
339
|
+
payloadBytes: number,
|
|
340
|
+
): ReadExitCell {
|
|
341
|
+
const saved = PAYLOAD_BYTES;
|
|
342
|
+
try {
|
|
343
|
+
PAYLOAD_BYTES = 0;
|
|
344
|
+
const slotSmall = benchEvent(event, runs, workspace, 'bundle');
|
|
345
|
+
const probeSmall = benchReadExit(event, runs, workspace);
|
|
346
|
+
PAYLOAD_BYTES = payloadBytes;
|
|
347
|
+
const slotLarge = benchEvent(event, runs, workspace, 'bundle');
|
|
348
|
+
const probeLarge = benchReadExit(event, runs, workspace);
|
|
349
|
+
const slot_delta_ms = slotLarge.p50_ms - slotSmall.p50_ms;
|
|
350
|
+
const transport_delta_ms = probeLarge.p50_ms - probeSmall.p50_ms;
|
|
351
|
+
return {
|
|
352
|
+
event,
|
|
353
|
+
payload_bytes: payloadBytes,
|
|
354
|
+
slot_small_p50_ms: slotSmall.p50_ms,
|
|
355
|
+
slot_large_p50_ms: slotLarge.p50_ms,
|
|
356
|
+
read_exit_small_p50_ms: probeSmall.p50_ms,
|
|
357
|
+
read_exit_large_p50_ms: probeLarge.p50_ms,
|
|
358
|
+
slot_delta_ms,
|
|
359
|
+
transport_delta_ms,
|
|
360
|
+
transport_share_pct:
|
|
361
|
+
slot_delta_ms > 0
|
|
362
|
+
? Math.round((transport_delta_ms / slot_delta_ms) * 100)
|
|
363
|
+
: null,
|
|
364
|
+
};
|
|
365
|
+
} finally {
|
|
366
|
+
PAYLOAD_BYTES = saved;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Human-readable decomposition, one line per fact. */
|
|
371
|
+
export function renderReadExitCell(cell: ReadExitCell): string {
|
|
372
|
+
const share =
|
|
373
|
+
cell.transport_share_pct === null
|
|
374
|
+
? 'n/a — the large cell was not slower than the small one in this run'
|
|
375
|
+
: `${String(cell.transport_share_pct)}%`;
|
|
376
|
+
return (
|
|
377
|
+
`read-exit cell · ${cell.event} · payload ${String(cell.payload_bytes)}B\n` +
|
|
378
|
+
` full dispatch small ${String(cell.slot_small_p50_ms).padStart(5)} ms · ` +
|
|
379
|
+
`large ${String(cell.slot_large_p50_ms).padStart(5)} ms · delta ${String(cell.slot_delta_ms)} ms\n` +
|
|
380
|
+
` read + parse small ${String(cell.read_exit_small_p50_ms).padStart(5)} ms · ` +
|
|
381
|
+
`large ${String(cell.read_exit_large_p50_ms).padStart(5)} ms · delta ${String(cell.transport_delta_ms)} ms\n` +
|
|
382
|
+
` transport share of the large-payload delta: ${share}\n`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
233
386
|
/**
|
|
234
387
|
* Same-run control: a bare `node -e 0`, measured through the identical
|
|
235
388
|
* spawnSync harness the slots use.
|
|
@@ -486,6 +639,37 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
486
639
|
PAYLOAD_BYTES = parsed;
|
|
487
640
|
}
|
|
488
641
|
|
|
642
|
+
// `--read-exit-cell <bytes>`: the transport-isolation cell. Measurement-only
|
|
643
|
+
// for the same reason `--payload-bytes` is — it PADS the payload, so it must
|
|
644
|
+
// never write a budget row or a regression baseline.
|
|
645
|
+
const readExitIdx = argv.indexOf('--read-exit-cell');
|
|
646
|
+
let readExitBytes: number | null = null;
|
|
647
|
+
if (readExitIdx >= 0) {
|
|
648
|
+
const raw = argv[readExitIdx + 1];
|
|
649
|
+
const parsed = raw === undefined ? Number.NaN : Number.parseInt(raw, 10);
|
|
650
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
651
|
+
process.stderr.write(
|
|
652
|
+
'bench_hook_latency: --read-exit-cell requires a positive integer byte count\n',
|
|
653
|
+
);
|
|
654
|
+
return 2;
|
|
655
|
+
}
|
|
656
|
+
if (gate || update || baselineIdx >= 0) {
|
|
657
|
+
process.stderr.write(
|
|
658
|
+
'bench_hook_latency: --read-exit-cell is measurement-only — a padded payload must ' +
|
|
659
|
+
"never write this tree's budget row or regression baseline\n",
|
|
660
|
+
);
|
|
661
|
+
return 2;
|
|
662
|
+
}
|
|
663
|
+
if (viaCli) {
|
|
664
|
+
process.stderr.write(
|
|
665
|
+
'bench_hook_latency: --read-exit-cell drives the bundle directly and does not use ' +
|
|
666
|
+
'the --via-cli shim path — pick one\n',
|
|
667
|
+
);
|
|
668
|
+
return 2;
|
|
669
|
+
}
|
|
670
|
+
readExitBytes = parsed;
|
|
671
|
+
}
|
|
672
|
+
|
|
489
673
|
const bundleIdx = argv.indexOf('--bundle');
|
|
490
674
|
if (bundleIdx >= 0) {
|
|
491
675
|
const raw = argv[bundleIdx + 1];
|
|
@@ -539,6 +723,26 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
539
723
|
return 2;
|
|
540
724
|
}
|
|
541
725
|
|
|
726
|
+
if (readExitBytes !== null) {
|
|
727
|
+
// Its own mode: the cell alternates two payload sizes internally, so
|
|
728
|
+
// running it alongside the ordinary sweep would report a padded number
|
|
729
|
+
// under an unpadded label.
|
|
730
|
+
const cellWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), 'ac-hook-readexit-'));
|
|
731
|
+
try {
|
|
732
|
+
// `post_tool_use` only. That is where the large cell lives: a
|
|
733
|
+
// `tool_response` does not exist on a `PreToolUse` payload, which is
|
|
734
|
+
// the same objection `syntheticPayload` raises against padding `stop`.
|
|
735
|
+
const cell = benchReadExitCell('post_tool_use', runs, cellWorkspace, readExitBytes);
|
|
736
|
+
process.stdout.write(renderReadExitCell(cell));
|
|
737
|
+
process.stdout.write(
|
|
738
|
+
'ℹ️ measurement only (no gate) — this cell never writes a budget row\n',
|
|
739
|
+
);
|
|
740
|
+
} finally {
|
|
741
|
+
fs.rmSync(cellWorkspace, { recursive: true, force: true });
|
|
742
|
+
}
|
|
743
|
+
return 0;
|
|
744
|
+
}
|
|
745
|
+
|
|
542
746
|
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'ac-hook-bench-'));
|
|
543
747
|
const results: EventResult[] = [];
|
|
544
748
|
let control: EventResult | null = null;
|
|
@@ -45,6 +45,7 @@ import * as path from 'node:path';
|
|
|
45
45
|
import process from 'node:process';
|
|
46
46
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
47
47
|
|
|
48
|
+
import { evaluateDashboardOnDisk, parseModeToken, type DashboardMode } from '../agent-src/scripts/dashboard_mode.js';
|
|
48
49
|
import { GateLedger } from './_lib/gate_ledger.js';
|
|
49
50
|
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
50
51
|
import { parse_frontmatter } from './validate_frontmatter.js';
|
|
@@ -390,6 +391,39 @@ export function main(argv: readonly string[]): number {
|
|
|
390
391
|
const json = renderJson(entries);
|
|
391
392
|
|
|
392
393
|
if (argv.includes('--check')) {
|
|
394
|
+
// `--untracked-mode` is the same three-state table the dashboard's
|
|
395
|
+
// `--check` already runs, on a second generated artefact: in-index is a
|
|
396
|
+
// failure, absent is the declared state, present is still compared
|
|
397
|
+
// against a fresh render. Untracked never means unchecked. The table
|
|
398
|
+
// itself lives in `dashboard_mode.ts` and is imported rather than
|
|
399
|
+
// restated, so the two artefacts cannot drift into different verdicts
|
|
400
|
+
// for the same observable state.
|
|
401
|
+
const mode: DashboardMode =
|
|
402
|
+
argv.map(parseModeToken).find((m) => m !== null) ?? 'tracked';
|
|
403
|
+
if (mode === 'untracked') {
|
|
404
|
+
let failed = false;
|
|
405
|
+
for (const [abs, rel, rendered] of [
|
|
406
|
+
[mdPath, `${ARCHIVE_REL}/${INDEX_MD}`, md],
|
|
407
|
+
[jsonPath, `${ARCHIVE_REL}/${INDEX_JSON}`, json],
|
|
408
|
+
] as const) {
|
|
409
|
+
const v = evaluateDashboardOnDisk({
|
|
410
|
+
mode,
|
|
411
|
+
target: abs,
|
|
412
|
+
repo_root: ROOT,
|
|
413
|
+
rendered,
|
|
414
|
+
rel,
|
|
415
|
+
regen: 'Run `task build-archive-index` to regenerate.',
|
|
416
|
+
noun: 'archive index',
|
|
417
|
+
});
|
|
418
|
+
if (v.stale) {
|
|
419
|
+
failed = true;
|
|
420
|
+
process.stderr.write(v.error ?? '');
|
|
421
|
+
} else if (!quiet && v.ok !== null) {
|
|
422
|
+
process.stdout.write(v.ok);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return failed ? 1 : 0;
|
|
426
|
+
}
|
|
393
427
|
const stale = [
|
|
394
428
|
_read(mdPath) === md ? null : `${ARCHIVE_REL}/${INDEX_MD}`,
|
|
395
429
|
_read(jsonPath) === json ? null : `${ARCHIVE_REL}/${INDEX_JSON}`,
|