@event4u/agent-config 12.0.0 → 12.1.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 +122 -0
- package/agents/templates/.ai-council.yml.example +46 -3
- package/dist/agent-src/contexts/model-recommendations.md +6 -4
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +32 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +7 -7
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +41 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-K34QTSD5.js +3895 -0
- package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-LKZ5DOYW.js} +557 -297
- package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-O52B4F5S.js} +123 -2338
- package/dist/cli-delegate/cmd_conformance.js +4 -4
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- 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 +4 -4
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.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 +2574 -1080
- package/dist/install/install.mjs +744 -482
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +113 -10
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/model_tier.js +9 -0
- package/dist/scripts/_lib/model_tier.js.map +1 -1
- package/dist/scripts/condense.js +50 -0
- package/dist/scripts/condense.js.map +1 -1
- package/docs/MIGRATION.md +20 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +30 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +32 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +39 -9
- package/src/config/gate-violation-baselines.json +6 -0
- package/src/config/hook-token-budget.json +7 -1
- package/src/config/preamble-payload-budget.json +28 -0
- package/src/scripts/_dispatch.bash +10 -0
- package/src/scripts/_lib/git_common_dir.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +9 -0
- package/src/scripts/_lib/release_highlights.ts +108 -7
- package/src/scripts/_lib/skill_catalogue.ts +862 -0
- package/src/scripts/ai_council/clients.ts +182 -3
- package/src/scripts/ai_council/config.ts +16 -1
- package/src/scripts/ai_council/low_impact.ts +14 -1
- package/src/scripts/ai_council/orchestrator.ts +14 -1
- package/src/scripts/ai_council/transport_resolver.ts +32 -2
- package/src/scripts/capture_skill_catalogue.ts +185 -328
- package/src/scripts/check_branch_freshness.ts +7 -1
- package/src/scripts/check_release_published.ts +6 -2
- package/src/scripts/check_release_trunk_sync.ts +18 -2
- package/src/scripts/condense.ts +50 -1
- package/src/scripts/dispatch_r2_reviewer.ts +15 -5
- package/src/scripts/evidence_report.ts +15 -4
- package/src/scripts/hook_manifest.yaml +47 -13
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
- package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
- package/src/scripts/hot_context_hook.ts +13 -1
- package/src/scripts/install.ts +129 -0
- package/src/scripts/janitor.ts +77 -2
- package/src/scripts/lint_plan_risk_register.ts +22 -9
- package/src/scripts/lint_roadmap_blockers.ts +109 -6
- package/src/scripts/lint_scheduled_deprecations.ts +738 -0
- package/src/scripts/migration_status.ts +17 -1
- package/src/scripts/probe_review_binding_drift.ts +731 -0
- package/src/scripts/release.ts +95 -1
- package/src/scripts/schemas/command.schema.json +2 -2
- package/src/scripts/schemas/skill.schema.json +2 -1
- package/src/scripts/schemas/subagent.schema.json +2 -2
- package/src/scripts/session_eol_report.ts +81 -0
- package/src/scripts/workspace_doctor.ts +464 -0
- package/src/scripts/worktree_cleanup_check.ts +2 -1
- package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
package/src/scripts/condense.ts
CHANGED
|
@@ -1450,7 +1450,31 @@ export interface ClaudePathsPlan {
|
|
|
1450
1450
|
/** Patterns to emit under `paths:`, in declaration order. */
|
|
1451
1451
|
globs: string[];
|
|
1452
1452
|
/** Patterns deliberately not emitted, with the reason a reader can act on. */
|
|
1453
|
-
dropped: { pattern: string; reason: 'unresolved-placeholder' | 'over-budget' }[];
|
|
1453
|
+
dropped: { pattern: string; reason: 'unresolved-placeholder' | 'over-budget' | 'mixed-triggers' }[];
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* Does this rule declare any trigger that is NOT path-shaped?
|
|
1458
|
+
*
|
|
1459
|
+
* Load-bearing for `_claude_paths_plan` below, because one `triggers:` list
|
|
1460
|
+
* means two different things on two hosts. Cursor and Windsurf treat globs as
|
|
1461
|
+
* ADDITIVE — a rule with empty globs still reaches the agent through its
|
|
1462
|
+
* description (see `_emit_cursor_mdc`'s own comment). Claude Code treats
|
|
1463
|
+
* `paths:` as EXCLUSIVE: with it the rule loads on a path match; without it the
|
|
1464
|
+
* rule loads unconditionally. The emitter fed both semantics from one list, so
|
|
1465
|
+
* a rule that declared keywords *and* one path glob silently became
|
|
1466
|
+
* path-gated-only on Claude.
|
|
1467
|
+
*/
|
|
1468
|
+
export function _has_non_path_trigger(meta: Record<string, unknown>): boolean {
|
|
1469
|
+
const triggers = meta['triggers'];
|
|
1470
|
+
if (!Array.isArray(triggers)) return false;
|
|
1471
|
+
for (const t of triggers) {
|
|
1472
|
+
if (t === null || typeof t !== 'object' || Array.isArray(t)) continue;
|
|
1473
|
+
const obj = t as Record<string, unknown>;
|
|
1474
|
+
if (typeof obj['keyword'] === 'string' && obj['keyword']) return true;
|
|
1475
|
+
if (typeof obj['phrase'] === 'string' && obj['phrase']) return true;
|
|
1476
|
+
}
|
|
1477
|
+
return false;
|
|
1454
1478
|
}
|
|
1455
1479
|
|
|
1456
1480
|
/**
|
|
@@ -1470,6 +1494,31 @@ export function _claude_paths_plan(meta: Record<string, unknown>): ClaudePathsPl
|
|
|
1470
1494
|
}
|
|
1471
1495
|
const globs: string[] = [];
|
|
1472
1496
|
const dropped: ClaudePathsPlan['dropped'] = [];
|
|
1497
|
+
|
|
1498
|
+
// A rule that ALSO declares keyword / phrase triggers gets no `paths:` at
|
|
1499
|
+
// all, and therefore keeps loading unconditionally on Claude Code.
|
|
1500
|
+
//
|
|
1501
|
+
// Emitting `paths:` here would narrow the rule to a path match and discard
|
|
1502
|
+
// every keyword the author wrote, because this host reads the list as the
|
|
1503
|
+
// whole gate rather than as one way in. Measured before this guard: 25 of
|
|
1504
|
+
// 110 projected rules carried `paths:` and 19 of them also carried keyword
|
|
1505
|
+
// triggers — `design-fidelity` lost 21 of them, in exactly the pasted-
|
|
1506
|
+
// screenshot and capability-URL handovers its own routing section names as
|
|
1507
|
+
// the classes it exists to catch. Nothing in the tree recorded a decision
|
|
1508
|
+
// to make that trade, which is why it is treated as a defect.
|
|
1509
|
+
//
|
|
1510
|
+
// The drop is REPORTED rather than silent, so a rule whose obligation
|
|
1511
|
+
// really is path-bound is visible as a candidate for removing its keyword
|
|
1512
|
+
// triggers — the narrowing then becomes an authoring decision instead of an
|
|
1513
|
+
// emitter side effect. Cursor and Windsurf are untouched: globs are
|
|
1514
|
+
// additive there, so the same rule keeps both routes.
|
|
1515
|
+
if (_has_non_path_trigger(meta)) {
|
|
1516
|
+
for (const raw of derive_trigger_globs(meta)) {
|
|
1517
|
+
dropped.push({ pattern: raw, reason: 'mixed-triggers' });
|
|
1518
|
+
}
|
|
1519
|
+
return { globs, dropped };
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1473
1522
|
let spent = 0;
|
|
1474
1523
|
for (const raw of derive_trigger_globs(meta)) {
|
|
1475
1524
|
if (_is_unresolved_placeholder(raw)) {
|
|
@@ -222,13 +222,23 @@ export const REVIEW_SCOPE_NAME_ONLY_FLAGS: readonly string[] = [
|
|
|
222
222
|
'-O/dev/null',
|
|
223
223
|
];
|
|
224
224
|
|
|
225
|
-
/**
|
|
226
|
-
|
|
225
|
+
/**
|
|
226
|
+
* `git diff` argv for the review scope (patch body), config-pinned.
|
|
227
|
+
*
|
|
228
|
+
* `head` defaults to `HEAD` — the only value the dispatcher and the validator
|
|
229
|
+
* ever pass, since both bind the scope being dispatched *now*. It is a
|
|
230
|
+
* parameter so that an out-of-band measurement can re-derive the scope at a
|
|
231
|
+
* HISTORICAL revision through this same definition instead of assembling a
|
|
232
|
+
* second `git diff` argv beside it: a private copy would drift from the
|
|
233
|
+
* normative flag list above the first time a flag changes, and then report a
|
|
234
|
+
* hash no gate would ever produce.
|
|
235
|
+
*/
|
|
236
|
+
export function reviewScopeDiffArgs(base: string, head = 'HEAD'): string[] {
|
|
227
237
|
return [
|
|
228
238
|
...REVIEW_SCOPE_GIT_CONFIG,
|
|
229
239
|
'diff',
|
|
230
240
|
...REVIEW_SCOPE_DIFF_FLAGS,
|
|
231
|
-
`${base}
|
|
241
|
+
`${base}...${head}`,
|
|
232
242
|
'--',
|
|
233
243
|
':/',
|
|
234
244
|
...REVIEW_SCOPE_EXCLUDES,
|
|
@@ -236,12 +246,12 @@ export function reviewScopeDiffArgs(base: string): string[] {
|
|
|
236
246
|
}
|
|
237
247
|
|
|
238
248
|
/** `git diff --name-only` argv for the review scope (changed-file list), config-pinned. */
|
|
239
|
-
export function reviewScopeNameOnlyArgs(base: string): string[] {
|
|
249
|
+
export function reviewScopeNameOnlyArgs(base: string, head = 'HEAD'): string[] {
|
|
240
250
|
return [
|
|
241
251
|
...REVIEW_SCOPE_GIT_CONFIG,
|
|
242
252
|
'diff',
|
|
243
253
|
...REVIEW_SCOPE_NAME_ONLY_FLAGS,
|
|
244
|
-
`${base}
|
|
254
|
+
`${base}...${head}`,
|
|
245
255
|
'--',
|
|
246
256
|
':/',
|
|
247
257
|
...REVIEW_SCOPE_EXCLUDES,
|
|
@@ -29,6 +29,8 @@ import * as path from 'node:path';
|
|
|
29
29
|
import process from 'node:process';
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
31
|
|
|
32
|
+
import { workspaceIdentity } from './_lib/git_common_dir.js';
|
|
33
|
+
|
|
32
34
|
// argparse `prog` is hardcoded to "evidence_report.py" in the retired Python implementation,
|
|
33
35
|
// so usage strings stay byte-identical regardless of the `.py`/`.ts` filename.
|
|
34
36
|
const PROG = 'evidence_report.py';
|
|
@@ -39,15 +41,24 @@ const DESCRIPTION = 'Evidence Report template automation.';
|
|
|
39
41
|
// Repo-root resolution
|
|
40
42
|
// ---------------------------------------------------------------------------
|
|
41
43
|
|
|
42
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Return repo root, falling back to the relative-path heuristic.
|
|
46
|
+
*
|
|
47
|
+
* Census row R4. The previous implementation spawned
|
|
48
|
+
* `git rev-parse --show-toplevel` with **no `cwd` and no env scrub**, so an
|
|
49
|
+
* inherited `GIT_DIR` (every git hook exports one) silently answered about the
|
|
50
|
+
* hook's repository instead of this one. `workspaceIdentity()` reads files, so
|
|
51
|
+
* there is no child process to redirect. The fallback is unchanged, and in a
|
|
52
|
+
* plain invocation both forms return the same path.
|
|
53
|
+
*/
|
|
43
54
|
function _repo_root(): string {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
55
|
+
const root = workspaceIdentity().repoRoot;
|
|
56
|
+
if (!root.resolved) {
|
|
46
57
|
// Script lives at src/scripts/; repo root is two levels up.
|
|
47
58
|
const here = fileURLToPath(import.meta.url);
|
|
48
59
|
return path.resolve(path.dirname(here), '..', '..');
|
|
49
60
|
}
|
|
50
|
-
return
|
|
61
|
+
return root.value;
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
function _git_head_short(): string {
|
|
@@ -556,6 +556,30 @@ concerns:
|
|
|
556
556
|
args: []
|
|
557
557
|
fail_closed: false
|
|
558
558
|
severity: advisory
|
|
559
|
+
# road-to-inbox-harvest-2026-08-d-context-ledger Phase 1 Step 1.1 — the
|
|
560
|
+
# tool-result byte counter. `hook-token-budget.json` registers the gap it
|
|
561
|
+
# closes in its own words: bytes-into-context from tool RESULTS are
|
|
562
|
+
# instrumented nowhere, so every context-cost claim in this tree is made
|
|
563
|
+
# against a denominator missing its dominant term.
|
|
564
|
+
#
|
|
565
|
+
# CAPTURE ONLY. It appends `{ts, tool, bytes, measurable}` to a gitignored
|
|
566
|
+
# census file and emits NOTHING to the model — zero injection bytes, which
|
|
567
|
+
# is why it carries a metric registration rather than a per-concern byte
|
|
568
|
+
# cap. The threshold question (when is a result too large) is deliberately
|
|
569
|
+
# unanswered: the distribution has never been observed, and the house rule
|
|
570
|
+
# is baseline before mechanism.
|
|
571
|
+
#
|
|
572
|
+
# The record type has four fields and none can hold free text — a tool
|
|
573
|
+
# NAME, a byte COUNT, a timestamp and a measurable flag. Tool results
|
|
574
|
+
# routinely carry file contents and command output; none of it can reach
|
|
575
|
+
# this output because the output has no field able to carry it.
|
|
576
|
+
#
|
|
577
|
+
# Never blocks, never warns: exit 0 on every path. fail_closed: false.
|
|
578
|
+
tool-result-bytes:
|
|
579
|
+
script: src/scripts/hooks/tool_result_bytes_hook.ts
|
|
580
|
+
args: []
|
|
581
|
+
fail_closed: false
|
|
582
|
+
severity: advisory
|
|
559
583
|
# road-to-subagent-lifecycle-integrity Phase 3 Step 1 — spawn guard, SHADOW
|
|
560
584
|
# ONLY, per docs/contracts/concern-activation-policy.md. On every Agent/Task
|
|
561
585
|
# call it appends one `spawn_guard_shadow` line recording what a guard WOULD
|
|
@@ -623,7 +647,7 @@ platforms:
|
|
|
623
647
|
session_end: [chat-history, memory-learn, session-register]
|
|
624
648
|
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
625
649
|
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, reread-guard]
|
|
626
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
|
|
650
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, tool-result-bytes, edit-shape]
|
|
627
651
|
|
|
628
652
|
claude:
|
|
629
653
|
session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
@@ -631,14 +655,24 @@ platforms:
|
|
|
631
655
|
stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, turn-end-gate, self-repair, session-register, session-eol]
|
|
632
656
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, delegation-nudge, git-authorization, session-canary, self-repair, session-register]
|
|
633
657
|
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, reread-guard, spawn-guard-shadow]
|
|
634
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape, language-mirror]
|
|
635
|
-
# round-5 § 6.1
|
|
636
|
-
#
|
|
637
|
-
#
|
|
638
|
-
#
|
|
639
|
-
#
|
|
640
|
-
#
|
|
641
|
-
|
|
658
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, tool-result-bytes, edit-shape, language-mirror]
|
|
659
|
+
# `language-mirror` — round-5 § 6.1. `pre_compact` sets a pin-lost marker;
|
|
660
|
+
# the `post_tool_use` entry above re-emits the pin exactly once while that
|
|
661
|
+
# marker is set, then clears it. One extra injection per compaction EVENT —
|
|
662
|
+
# never one per tool call, which is the shape § 6.2 refuses. claude only: it
|
|
663
|
+
# is the sole verified platform, and the measured compaction defect is a
|
|
664
|
+
# Claude Code transcript fact. (It was the ONLY binding on this slot until
|
|
665
|
+
# the entry below; that sentence is retired rather than left to rot.)
|
|
666
|
+
#
|
|
667
|
+
# `hot-context` — road-to-inbox-harvest-2026-08-d-context-ledger Step 2.1.
|
|
668
|
+
# This is the one slot that fires WHILE state is being destroyed, and it
|
|
669
|
+
# carried no WRITER at all: the cache a post-compaction `session_start`
|
|
670
|
+
# restores was written at the last `stop`, so everything the session did
|
|
671
|
+
# between that stop and the compaction was lost with it. The concern is
|
|
672
|
+
# already bound on `stop` and `session_start`; this adds the write at the
|
|
673
|
+
# moment the loss actually happens. Deterministic rebuild, emits nothing
|
|
674
|
+
# here — the 4,096-byte row it carries is for its session_start restore.
|
|
675
|
+
pre_compact: [language-mirror, hot-context]
|
|
642
676
|
# road-to-subagent-lifecycle-integrity Phase 1 Step 1. `claude` is the only
|
|
643
677
|
# platform where both native events were measured present on the installed
|
|
644
678
|
# binary (2.1.229 — see the evidence page cited on the alias rows below).
|
|
@@ -675,7 +709,7 @@ platforms:
|
|
|
675
709
|
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
676
710
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
677
711
|
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, ui-route-nudge, code-graph-nudge, reread-guard, spawn-guard-shadow]
|
|
678
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
|
|
712
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, tool-result-bytes, edit-shape]
|
|
679
713
|
# subagent-ledger IS bound here, unlike the two nudges above, and the
|
|
680
714
|
# difference is the delivery channel rather than the platform: the nudges
|
|
681
715
|
# depend on stdout reaching the model, which this trampoline discards by
|
|
@@ -698,7 +732,7 @@ platforms:
|
|
|
698
732
|
session_end: [chat-history, memory-learn, session-register]
|
|
699
733
|
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
700
734
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
701
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
|
|
735
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, tool-result-bytes, edit-shape]
|
|
702
736
|
|
|
703
737
|
# Phase 7.6 — Cline. Hooks live under `.clinerules/hooks/<HookName>`
|
|
704
738
|
# (project, no file extension, must be executable per Cline docs) or
|
|
@@ -713,7 +747,7 @@ platforms:
|
|
|
713
747
|
session_end: [chat-history, memory-learn, session-register]
|
|
714
748
|
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
715
749
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
716
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
|
|
750
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, tool-result-bytes, edit-shape]
|
|
717
751
|
|
|
718
752
|
# Phase 7.7 — Windsurf (Cascade). Hooks live at `.windsurf/hooks.json`
|
|
719
753
|
# (project) or `~/.codeium/windsurf/hooks.json` (user). Cascade has
|
|
@@ -749,7 +783,7 @@ platforms:
|
|
|
749
783
|
session_end: [chat-history, memory-learn, session-register]
|
|
750
784
|
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
751
785
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
752
|
-
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
|
|
786
|
+
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, tool-result-bytes, edit-shape]
|
|
753
787
|
|
|
754
788
|
# Phase 7.9 — Copilot has no hook surface. Concerns route through
|
|
755
789
|
# rule-only fallback; the dispatcher silently no-ops on --platform copilot.
|
|
@@ -63,6 +63,7 @@ import { main as editShapeMain } from './edit_shape_hook.js';
|
|
|
63
63
|
import { main as rereadGuardMain } from './reread_guard_hook.js';
|
|
64
64
|
import { main as sessionEolMain } from './session_eol_hook.js';
|
|
65
65
|
import { main as subagentLedgerMain } from './subagent_ledger_hook.js';
|
|
66
|
+
import { main as toolResultBytesMain } from './tool_result_bytes_hook.js';
|
|
66
67
|
import { main as spawnGuardShadowMain } from './spawn_guard_shadow_hook.js';
|
|
67
68
|
|
|
68
69
|
/** A concern `main` — argv-taking or not; both shapes exist. */
|
|
@@ -109,5 +110,6 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
|
|
|
109
110
|
'src/scripts/hooks/reread_guard_hook.ts': rereadGuardMain as ConcernMain,
|
|
110
111
|
'src/scripts/hooks/session_eol_hook.ts': sessionEolMain as ConcernMain,
|
|
111
112
|
'src/scripts/hooks/subagent_ledger_hook.ts': subagentLedgerMain as ConcernMain,
|
|
113
|
+
'src/scripts/hooks/tool_result_bytes_hook.ts': toolResultBytesMain as ConcernMain,
|
|
112
114
|
'src/scripts/hooks/spawn_guard_shadow_hook.ts': spawnGuardShadowMain as ConcernMain,
|
|
113
115
|
};
|
|
@@ -243,6 +243,27 @@ export function buildRecordInput(facts: DispatchFacts, ts: string, id: string):
|
|
|
243
243
|
|
|
244
244
|
if (!facts.isAsync) {
|
|
245
245
|
if (facts.resolvedModel) {
|
|
246
|
+
// road-to-inbox-harvest-2026-08-d-top-band-model-economy Step 1.1.
|
|
247
|
+
//
|
|
248
|
+
// The raw id goes into the field the schema already reserves for it
|
|
249
|
+
// ("the model id the provider reported SERVING"). Without it, a
|
|
250
|
+
// dispatch that ran on a band the three-tier vocabulary cannot
|
|
251
|
+
// express is byte-identical in the record to one where the host
|
|
252
|
+
// reported no model at all: `extractModelFamily` returns null for
|
|
253
|
+
// both, so `tiers` is omitted for both.
|
|
254
|
+
//
|
|
255
|
+
// That collapse is precisely what blocks ADR-035's reopen
|
|
256
|
+
// condition. "Did a vendor ship a band the three tiers cannot
|
|
257
|
+
// express" is answerable only if an unmapped id is distinguishable
|
|
258
|
+
// from an absent one, and recording the served id separates them:
|
|
259
|
+
// unmapped ⇒ `model_served` set with `tiers` absent; unreported ⇒
|
|
260
|
+
// both absent.
|
|
261
|
+
//
|
|
262
|
+
// Vendor-neutral by construction: the id is stored verbatim as an
|
|
263
|
+
// opaque host string. No band above `high` is named and no mapping
|
|
264
|
+
// is invented — naming a fourth band is the decision this
|
|
265
|
+
// measurement exists to inform, never one it may pre-empt.
|
|
266
|
+
input.model_served = facts.resolvedModel;
|
|
246
267
|
const family = extractModelFamily(facts.resolvedModel);
|
|
247
268
|
if (family) input.tiers = [family];
|
|
248
269
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Tool-result byte counter — capture only, no behaviour change
|
|
4
|
+
* (road-to-inbox-harvest-2026-08-d-context-ledger Phase 1, Step 1.1).
|
|
5
|
+
*
|
|
6
|
+
* Why this exists. `hook-token-budget.json` registers the gap in its own
|
|
7
|
+
* words: *"bytes-into-context from tool RESULTS is not instrumented
|
|
8
|
+
* anywhere (the census covers hook payloads only)"*. The injection census
|
|
9
|
+
* measures what HOOKS put into context — a few kilobytes per turn under a
|
|
10
|
+
* per-slot cap. It does not measure what TOOLS put into context, which is
|
|
11
|
+
* the larger number by any plausible margin and the one nobody can cite.
|
|
12
|
+
* Every claim about context cost in this tree is therefore made against a
|
|
13
|
+
* denominator that excludes its own dominant term.
|
|
14
|
+
*
|
|
15
|
+
* This concern is the instrument and ONLY the instrument. It appends; it
|
|
16
|
+
* never decides, never advises, and emits nothing to the model. The
|
|
17
|
+
* threshold question — when is a tool result too large — is deliberately
|
|
18
|
+
* NOT answered here: the distribution has never been observed, and a
|
|
19
|
+
* threshold set before the distribution is a number invented rather than
|
|
20
|
+
* measured. That ordering is the house rule this repository has paid to
|
|
21
|
+
* learn more than once.
|
|
22
|
+
*
|
|
23
|
+
* ── What is recorded, and what is refused ──────────────────────────────
|
|
24
|
+
*
|
|
25
|
+
* PRIVACY BY CONSTRUCTION — never widen this file to record content.
|
|
26
|
+
*
|
|
27
|
+
* The record type has exactly three fields and NONE of them can hold free
|
|
28
|
+
* text: a timestamp, a tool NAME (an id-shaped enum — `Read`, `Bash`,
|
|
29
|
+
* `Grep` — the same single host string `orchestration_record_hook` already
|
|
30
|
+
* records), and a BYTE COUNT. A tool result routinely carries file
|
|
31
|
+
* contents, command output, secrets in an env dump, and customer data from
|
|
32
|
+
* an API response. None of it reaches this file's output, because the
|
|
33
|
+
* output has no field able to carry it. A record shape that cannot hold a
|
|
34
|
+
* secret has no scrubber that can fail — the same construction
|
|
35
|
+
* `domain-safety-pii` § Surface 2 prescribes for logs and the reason the
|
|
36
|
+
* sibling `subagent-ledger` refuses `last_assistant_message` in any form.
|
|
37
|
+
*
|
|
38
|
+
* The result is measured, never read: `_resultBytes` computes a length and
|
|
39
|
+
* discards the value in the same expression.
|
|
40
|
+
*
|
|
41
|
+
* ── Always exit 0 ─────────────────────────────────────────────────────
|
|
42
|
+
*
|
|
43
|
+
* A `warn` (exit 2) is read as a hard BLOCK on this host, and this concern
|
|
44
|
+
* has nothing to say to the model. Every path returns EXIT_ALLOW —
|
|
45
|
+
* malformed envelope, unparseable payload, unwritable disk. An instrument
|
|
46
|
+
* that can fail a turn is a mechanism, and this is not one yet.
|
|
47
|
+
*
|
|
48
|
+
* ── Absence is recorded, not inferred ─────────────────────────────────
|
|
49
|
+
*
|
|
50
|
+
* A payload carrying no readable result writes a line with `bytes: null`
|
|
51
|
+
* and `measurable: false` rather than writing nothing. The two cases are
|
|
52
|
+
* otherwise byte-identical in the output — "this host does not carry tool
|
|
53
|
+
* results in its PostToolUse payload" and "no tools ran" would produce the
|
|
54
|
+
* same empty file, and the first is a finding about the instrument's own
|
|
55
|
+
* coverage that the baseline has to be able to see. This is the
|
|
56
|
+
* instrument-goes-quiet failure the sibling ledger's round-1 review fixed
|
|
57
|
+
* one layer down, applied here before it can be shipped.
|
|
58
|
+
*/
|
|
59
|
+
import * as fs from 'node:fs';
|
|
60
|
+
import * as path from 'node:path';
|
|
61
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
62
|
+
|
|
63
|
+
import { is_replay_mode } from './state_io.js';
|
|
64
|
+
import { readHookStdin } from './hook_stdin.js';
|
|
65
|
+
|
|
66
|
+
const EXIT_ALLOW = 0;
|
|
67
|
+
|
|
68
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
69
|
+
type JsonObject = { [k: string]: JsonValue };
|
|
70
|
+
|
|
71
|
+
function isObject(v: unknown): v is JsonObject {
|
|
72
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Census file, relative to the consumer repo root. Sibling of
|
|
77
|
+
* `injection-census.jsonl` in the same directory and the same
|
|
78
|
+
* `{ts, …, bytes}` line shape — one family, two units: that file counts
|
|
79
|
+
* bytes this suite injects, this one counts bytes the host's tools do.
|
|
80
|
+
* Gitignored via `/agents/runtime/`.
|
|
81
|
+
*/
|
|
82
|
+
export const CENSUS_FILE = path.join('agents', 'runtime', 'state', 'tool-result-census.jsonl');
|
|
83
|
+
|
|
84
|
+
/** The payload keys a host may use for a tool's result, widest first. */
|
|
85
|
+
const RESULT_KEYS = ['tool_response', 'toolResponse', 'tool_result', 'toolUseResult'] as const;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* UTF-8 byte length of a tool result, or `null` when the payload carries
|
|
89
|
+
* none under any known key.
|
|
90
|
+
*
|
|
91
|
+
* A result is an object on most tools and a bare string on some (`Bash`
|
|
92
|
+
* returns its stdout directly — see the sibling hook's own test corpus), so
|
|
93
|
+
* both shapes are measured. The sibling's `extractToolResult` returns
|
|
94
|
+
* `JsonObject | null` and would report a bare-string result as absent,
|
|
95
|
+
* which is correct for its purpose (it reads named usage fields) and wrong
|
|
96
|
+
* for this one: a 400 KB command output is exactly the case this counter
|
|
97
|
+
* exists to see, and it is precisely the shape that extractor drops.
|
|
98
|
+
*
|
|
99
|
+
* Bytes, not characters: a multibyte result costs what it costs on the
|
|
100
|
+
* wire, and `String.length` would under-report it.
|
|
101
|
+
*/
|
|
102
|
+
export function _resultBytes(payload: JsonObject): number | null {
|
|
103
|
+
for (const key of RESULT_KEYS) {
|
|
104
|
+
const v = payload[key];
|
|
105
|
+
if (v === undefined || v === null) continue;
|
|
106
|
+
if (typeof v === 'string') return Buffer.byteLength(v, 'utf8');
|
|
107
|
+
try {
|
|
108
|
+
return Buffer.byteLength(JSON.stringify(v), 'utf8');
|
|
109
|
+
} catch {
|
|
110
|
+
// Circular or otherwise unserialisable — unmeasurable, not zero.
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The tool's name, read from the same positions the sibling hook reads. */
|
|
118
|
+
export function _toolName(payload: JsonObject, envelope: JsonObject): string | null {
|
|
119
|
+
const candidates = [
|
|
120
|
+
payload['tool_name'],
|
|
121
|
+
payload['toolName'],
|
|
122
|
+
payload['tool'],
|
|
123
|
+
envelope['tool_name'],
|
|
124
|
+
envelope['tool'],
|
|
125
|
+
];
|
|
126
|
+
for (const v of candidates) {
|
|
127
|
+
if (typeof v === 'string' && v) return v;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function unwrapPayload(envelope: JsonObject): JsonObject {
|
|
133
|
+
const inner = envelope['payload'];
|
|
134
|
+
return isObject(inner) ? inner : envelope;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** `${ts.slice(0,7)}` is NOT used here — the census is one flat file, as its sibling is. */
|
|
138
|
+
function censusFile(consumerRoot: string): string {
|
|
139
|
+
return path.join(consumerRoot, CENSUS_FILE);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface CensusLine {
|
|
143
|
+
ts: string;
|
|
144
|
+
tool: string | null;
|
|
145
|
+
bytes: number | null;
|
|
146
|
+
/** `false` ⇒ the payload carried no readable result. See the header. */
|
|
147
|
+
measurable: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function _appendCensusLine(consumerRoot: string, line: CensusLine): void {
|
|
151
|
+
if (is_replay_mode()) return;
|
|
152
|
+
const file = censusFile(consumerRoot);
|
|
153
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
154
|
+
fs.appendFileSync(file, `${JSON.stringify(line)}\n`, 'utf8');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Process an ALREADY-PARSED dispatcher envelope. Always returns EXIT_ALLOW. */
|
|
158
|
+
export function processEnvelope(envelope: JsonValue, consumerRoot: string): number {
|
|
159
|
+
try {
|
|
160
|
+
if (!isObject(envelope)) return EXIT_ALLOW;
|
|
161
|
+
if (envelope['event'] !== 'post_tool_use') return EXIT_ALLOW;
|
|
162
|
+
|
|
163
|
+
const payload = unwrapPayload(envelope);
|
|
164
|
+
const bytes = _resultBytes(payload);
|
|
165
|
+
|
|
166
|
+
_appendCensusLine(consumerRoot, {
|
|
167
|
+
ts: new Date().toISOString(),
|
|
168
|
+
tool: _toolName(payload, envelope),
|
|
169
|
+
bytes,
|
|
170
|
+
measurable: bytes !== null,
|
|
171
|
+
});
|
|
172
|
+
} catch {
|
|
173
|
+
// Malformed payload, unwritable disk, anything — never disturb the run.
|
|
174
|
+
return EXIT_ALLOW;
|
|
175
|
+
}
|
|
176
|
+
return EXIT_ALLOW;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Resolve the consumer repo root, same precedence as the sibling ledger. */
|
|
180
|
+
export function resolveConsumerRoot(envelope: JsonValue): string {
|
|
181
|
+
if (isObject(envelope)) {
|
|
182
|
+
for (const key of ['workspace_root', 'project_root']) {
|
|
183
|
+
const v = envelope[key];
|
|
184
|
+
if (typeof v === 'string' && v) return v;
|
|
185
|
+
}
|
|
186
|
+
const cwd = unwrapPayload(envelope)['cwd'];
|
|
187
|
+
if (typeof cwd === 'string' && cwd) return cwd;
|
|
188
|
+
}
|
|
189
|
+
return process.cwd();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function main(): number {
|
|
193
|
+
const raw = readHookStdin();
|
|
194
|
+
let envelope: JsonValue = {};
|
|
195
|
+
try {
|
|
196
|
+
envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
|
|
197
|
+
} catch {
|
|
198
|
+
return EXIT_ALLOW;
|
|
199
|
+
}
|
|
200
|
+
return processEnvelope(envelope, resolveConsumerRoot(envelope));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
204
|
+
// every module shares the bundle's `import.meta.url`.
|
|
205
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
206
|
+
function _isCliEntry(): boolean {
|
|
207
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (process.argv[1] === undefined) return false;
|
|
211
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
212
|
+
if (import.meta.url === argvUrl) return true;
|
|
213
|
+
try {
|
|
214
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (_isCliEntry()) process.exit(main());
|
|
@@ -379,7 +379,19 @@ export function main(): number {
|
|
|
379
379
|
if (_is_replay_mode()) {
|
|
380
380
|
return 0; // replay fixtures: read-only, no state mutation
|
|
381
381
|
}
|
|
382
|
-
|
|
382
|
+
// `pre_compact` joins the write set (road-to-inbox-harvest-2026-08-d-
|
|
383
|
+
// context-ledger Step 2.1). It is the only slot that fires WHILE state
|
|
384
|
+
// is being destroyed, and until now it carried no writer at all: the
|
|
385
|
+
// cache a post-compaction `session_start` restores was written at the
|
|
386
|
+
// last `stop`, so everything the session did since then was lost with
|
|
387
|
+
// the compaction. Writing here makes the restore reflect the moment
|
|
388
|
+
// before the loss instead of the moment before the last turn.
|
|
389
|
+
//
|
|
390
|
+
// Cheap and idempotent by construction: `_write_hot_context` is a
|
|
391
|
+
// deterministic rebuild from the history file, so an extra fire costs
|
|
392
|
+
// one rebuild and can never produce a worse cache than the older one it
|
|
393
|
+
// replaces. It emits nothing — the restore side is unchanged.
|
|
394
|
+
if (event === 'stop' || event === 'session_end' || event === 'pre_compact') {
|
|
383
395
|
_write_hot_context(root);
|
|
384
396
|
} else if (event === 'session_start') {
|
|
385
397
|
const source = String(payload.source ?? '');
|