@event4u/agent-config 10.3.0 → 11.0.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 +147 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze.md +2 -1
- package/dist/agent-src/commands/council/debate.md +13 -4
- package/dist/agent-src/commands/council/default.md +26 -11
- package/dist/agent-src/commands/council.md +1 -1
- package/dist/agent-src/commands/design-system/capture.md +44 -0
- package/dist/agent-src/commands/design-system/generate.md +75 -0
- package/dist/agent-src/commands/design-system/import.md +63 -0
- package/dist/agent-src/commands/design-system.md +66 -0
- package/dist/agent-src/commands/pr/create.md +51 -26
- package/dist/agent-src/commands/worktree/create.md +5 -3
- package/dist/agent-src/rules/settings-ask-protocol.md +14 -9
- package/dist/agent-src/skills/ai-council/SKILL.md +24 -6
- package/dist/agent-src/skills/corpus-grounding/SKILL.md +12 -1
- package/dist/agent-src/skills/corpus-grounding/scripts/decision_engine.ts +215 -7
- package/dist/agent-src/skills/corpus-grounding/scripts/ground.ts +39 -1
- package/dist/agent-src/skills/design-intelligence/ATTRIBUTION.md +19 -2
- package/dist/agent-src/skills/design-intelligence/data/manifest.json +36 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +9 -0
- package/dist/agent-src/skills/design-system-capture/references/design-system-json.md +70 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +9 -3
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +9 -6
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -21
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
- package/dist/cli-delegate/{chunk-BV3WR67D.js → chunk-55WZN5VF.js} +3 -6
- package/dist/cli-delegate/{chunk-X3VOARZD.js → chunk-L565YFXH.js} +3 -3
- package/dist/cli-delegate/{chunk-JMQZ7MM5.js → chunk-MNWRDMR5.js} +1 -1
- package/dist/cli-delegate/{chunk-IR3IVRVC.js → chunk-NLP54U3J.js} +2 -1
- package/dist/cli-delegate/{chunk-FV34EGEX.js → chunk-NN4KW4HZ.js} +1 -1
- package/dist/cli-delegate/{chunk-6ENSXSBW.js → chunk-PPQO4NGR.js} +1 -1
- package/dist/cli-delegate/chunk-RLN73PR5.js +157 -0
- package/dist/cli-delegate/{chunk-6LOPHJLD.js → chunk-RYNADUFG.js} +1 -1
- package/dist/cli-delegate/{chunk-I5EAFRII.js → chunk-ZPSXJTLI.js} +2 -154
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +673 -215
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +10 -8
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +6 -4
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +127 -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 +9 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +8 -4
- package/dist/hooks/dispatch.js +994 -399
- package/dist/install/install.mjs +5 -7
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1104 -593
- package/dist/scripts/_lib/agent_settings.js +8 -0
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/server/schemas/settings.js +3 -4
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/{index-5fXOHoVh.js → index-B0aMJnuz.js} +2 -2
- package/dist/ui/assets/{index-5fXOHoVh.js.map → index-B0aMJnuz.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +12 -9
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
- package/src/config/agent-settings.template.yml +15 -18
- package/src/config/gate-coverage.yml +33 -2
- package/src/config/gate-violation-baselines.json +2 -2
- package/src/config/pack-size-budget.json +3 -2
- package/src/scripts/_lib/agent_settings.ts +8 -0
- package/src/scripts/_lib/design_system_import.ts +987 -0
- package/src/scripts/check_branch_freshness.ts +238 -6
- package/src/scripts/design_system_import.ts +264 -0
- package/src/scripts/external_sources_denylist.json +7 -2
- package/src/scripts/hook_manifest.yaml +71 -2
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +10 -0
- package/src/scripts/hooks/host_semantics.ts +2 -0
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +212 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +723 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +39 -2
- package/src/scripts/lint_hook_manifest.ts +2 -0
- package/src/scripts/lint_rule_skill_pack_reach.ts +194 -13
- package/src/scripts/session_register_hook.ts +120 -10
- package/src/server/schemas/settings.ts +3 -6
- package/tests/fixtures/hooks/subagent_start.json +7 -0
- package/tests/fixtures/hooks/subagent_stop.json +8 -0
|
@@ -535,6 +535,48 @@ concerns:
|
|
|
535
535
|
args: []
|
|
536
536
|
fail_closed: false
|
|
537
537
|
severity: advisory
|
|
538
|
+
# road-to-subagent-lifecycle-integrity Phase 1 (Steps 2+3) — the subagent
|
|
539
|
+
# lifecycle ledger. Binds ONLY the two new events; on every other slot it is
|
|
540
|
+
# not listed at all. Appends `subagent_start` / `subagent_stop` lines to
|
|
541
|
+
# gitignored runtime state and keeps one open-record file per live dispatch
|
|
542
|
+
# so depth and concurrency are derived from the set rather than asserted.
|
|
543
|
+
#
|
|
544
|
+
# CAPTURE ONLY, and that is load-bearing rather than cautious: Phase 1's
|
|
545
|
+
# falsifier (envelope return rate >=95% and no dispatch over 2x its tier
|
|
546
|
+
# budget-equivalent duration) can only be read off a window no mechanism
|
|
547
|
+
# reacted to. `validateResponse` runs in observe mode — the verdict is
|
|
548
|
+
# written down and dropped, never emitted as a decision. Phase 2's
|
|
549
|
+
# return-gate and Phase 3's spawn-guard are the concerns allowed to act, and
|
|
550
|
+
# both are pre-registered as gated on the baseline this one produces.
|
|
551
|
+
#
|
|
552
|
+
# Never blocks, never warns: exit 0 on every path including a malformed
|
|
553
|
+
# payload or an unwritable disk. fail_closed: false.
|
|
554
|
+
subagent-ledger:
|
|
555
|
+
script: src/scripts/hooks/subagent_ledger_hook.ts
|
|
556
|
+
args: []
|
|
557
|
+
fail_closed: false
|
|
558
|
+
severity: advisory
|
|
559
|
+
# road-to-subagent-lifecycle-integrity Phase 3 Step 1 — spawn guard, SHADOW
|
|
560
|
+
# ONLY, per docs/contracts/concern-activation-policy.md. On every Agent/Task
|
|
561
|
+
# call it appends one `spawn_guard_shadow` line recording what a guard WOULD
|
|
562
|
+
# have done at three candidate threshold pairs. It emits nothing to the model
|
|
563
|
+
# and has no code path that can deny.
|
|
564
|
+
#
|
|
565
|
+
# It skips the warn rung deliberately: the activation policy rules it out for
|
|
566
|
+
# a concern on its way to blocking, because a verified-firing per-turn
|
|
567
|
+
# injection left its own compliance rate unmoved (session-canary, 24 of 29)
|
|
568
|
+
# while both blocking carriers in conformance round 5 reached zero. A warn
|
|
569
|
+
# would pay this concern's full per-call cost and buy nothing measurable;
|
|
570
|
+
# shadow pays the same cost and buys the distribution the shipped threshold
|
|
571
|
+
# has to be derived from.
|
|
572
|
+
#
|
|
573
|
+
# Never blocks, never warns: exit 0 on every path. fail_closed: false.
|
|
574
|
+
spawn-guard-shadow:
|
|
575
|
+
script: src/scripts/hooks/spawn_guard_shadow_hook.ts
|
|
576
|
+
args: []
|
|
577
|
+
fail_closed: false
|
|
578
|
+
severity: advisory
|
|
579
|
+
tools: [Agent, Task]
|
|
538
580
|
|
|
539
581
|
# Role axis (road-to-token-economy-dispatch Phase 2). The dispatcher reads
|
|
540
582
|
# AGENT_CONFIG_SESSION_ROLE (resolved via _lib/session_role.ts — set ONLY by
|
|
@@ -568,7 +610,7 @@ platforms:
|
|
|
568
610
|
session_end: [chat-history, memory-learn, session-register]
|
|
569
611
|
stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, turn-end-gate, self-repair, session-register, session-eol]
|
|
570
612
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, delegation-nudge, git-authorization, session-canary, self-repair, session-register]
|
|
571
|
-
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]
|
|
613
|
+
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]
|
|
572
614
|
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]
|
|
573
615
|
# round-5 § 6.1 — the ONLY binding on this slot in the tree. `pre_compact`
|
|
574
616
|
# sets a pin-lost marker; the `post_tool_use` entry above re-emits the pin
|
|
@@ -577,6 +619,11 @@ platforms:
|
|
|
577
619
|
# shape § 6.2 refuses. claude only: it is the sole verified platform, and
|
|
578
620
|
# the measured compaction defect is a Claude Code transcript fact.
|
|
579
621
|
pre_compact: [language-mirror]
|
|
622
|
+
# road-to-subagent-lifecycle-integrity Phase 1 Step 1. `claude` is the only
|
|
623
|
+
# platform where both native events were measured present on the installed
|
|
624
|
+
# binary (2.1.229 — see the evidence page cited on the alias rows below).
|
|
625
|
+
subagent_start: [subagent-ledger]
|
|
626
|
+
subagent_stop: [subagent-ledger]
|
|
580
627
|
|
|
581
628
|
# Cowork — the Claude desktop app's local-agent-mode runtime, built
|
|
582
629
|
# on top of the Claude Code CLI. Same lifecycle vocabulary, same
|
|
@@ -607,8 +654,17 @@ platforms:
|
|
|
607
654
|
# still costs the dispatch, and its entire output is thrown away.
|
|
608
655
|
stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
|
|
609
656
|
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
|
|
610
|
-
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]
|
|
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]
|
|
611
658
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record, edit-shape]
|
|
659
|
+
# subagent-ledger IS bound here, unlike the two nudges above, and the
|
|
660
|
+
# difference is the delivery channel rather than the platform: the nudges
|
|
661
|
+
# depend on stdout reaching the model, which this trampoline discards by
|
|
662
|
+
# construction; the ledger's entire output is a disk append, which survives
|
|
663
|
+
# a discarded stdout and a discarded exit code alike. It stays dead until
|
|
664
|
+
# the upstream settings-source blockers land and the events actually fire —
|
|
665
|
+
# structurally ready, costing nothing in the meantime.
|
|
666
|
+
subagent_start: [subagent-ledger]
|
|
667
|
+
subagent_stop: [subagent-ledger]
|
|
612
668
|
|
|
613
669
|
# Phase 7.5 — Cursor. `.cursor/hooks.json` (project) is read by the
|
|
614
670
|
# IDE and CLI; `~/.cursor/hooks.json` (user) is opt-in via
|
|
@@ -698,6 +754,12 @@ native_event_aliases:
|
|
|
698
754
|
PostToolUse: post_tool_use
|
|
699
755
|
PreToolUse: pre_tool_use
|
|
700
756
|
PreCompact: pre_compact
|
|
757
|
+
# road-to-subagent-lifecycle-integrity Phase 1 Step 1. Both tokens were
|
|
758
|
+
# re-extracted from the installed binary at 2.1.229 (exact-token counts 12
|
|
759
|
+
# and 24) rather than carried over from the 2.1.220 spike — see
|
|
760
|
+
# agents/evidence/investigations/subagent-lifecycle-phase0-host-pin.md.
|
|
761
|
+
SubagentStart: subagent_start
|
|
762
|
+
SubagentStop: subagent_stop
|
|
701
763
|
# Cowork shares Claude Code's PascalCase event vocabulary verbatim
|
|
702
764
|
# (Cowork is Claude Code under the hood). Native names are duplicated
|
|
703
765
|
# here rather than aliased so dispatcher logs / feedback carry the
|
|
@@ -710,6 +772,13 @@ native_event_aliases:
|
|
|
710
772
|
PostToolUse: post_tool_use
|
|
711
773
|
PreToolUse: pre_tool_use
|
|
712
774
|
PreCompact: pre_compact
|
|
775
|
+
# Cowork is Claude Code under the hood, so the subagent pair is aliased
|
|
776
|
+
# here for the same reason the rest of the table is duplicated rather than
|
|
777
|
+
# referenced. NOT independently measured on a Cowork build — the alias
|
|
778
|
+
# costs nothing if the host never sends the event, and the ledger records
|
|
779
|
+
# absence as data rather than assuming arrival.
|
|
780
|
+
SubagentStart: subagent_start
|
|
781
|
+
SubagentStop: subagent_stop
|
|
713
782
|
cursor:
|
|
714
783
|
sessionStart: session_start
|
|
715
784
|
sessionEnd: session_end
|
|
@@ -62,6 +62,8 @@ import { main as turnEndGateMain } from './turn_end_gate_hook.js';
|
|
|
62
62
|
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
|
+
import { main as subagentLedgerMain } from './subagent_ledger_hook.js';
|
|
66
|
+
import { main as spawnGuardShadowMain } from './spawn_guard_shadow_hook.js';
|
|
65
67
|
|
|
66
68
|
/** A concern `main` — argv-taking or not; both shapes exist. */
|
|
67
69
|
export type ConcernMain = (argv?: string[]) => number | undefined | void;
|
|
@@ -106,4 +108,6 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
|
|
|
106
108
|
'src/scripts/hooks/edit_shape_hook.ts': editShapeMain as ConcernMain,
|
|
107
109
|
'src/scripts/hooks/reread_guard_hook.ts': rereadGuardMain as ConcernMain,
|
|
108
110
|
'src/scripts/hooks/session_eol_hook.ts': sessionEolMain as ConcernMain,
|
|
111
|
+
'src/scripts/hooks/subagent_ledger_hook.ts': subagentLedgerMain as ConcernMain,
|
|
112
|
+
'src/scripts/hooks/spawn_guard_shadow_hook.ts': spawnGuardShadowMain as ConcernMain,
|
|
109
113
|
};
|
|
@@ -80,6 +80,14 @@ export const EXIT_WARN = 2;
|
|
|
80
80
|
// Per Council Round 2 (Q3): `agent_error` covers agent-level crashes
|
|
81
81
|
// that are not concern-triggered, so chat-history can checkpoint
|
|
82
82
|
// partial sessions on abnormal exit.
|
|
83
|
+
//
|
|
84
|
+
// `subagent_start` / `subagent_stop` (road-to-subagent-lifecycle-integrity
|
|
85
|
+
// Phase 1 Step 1) are the dispatch-level twins of the session pair: they
|
|
86
|
+
// bracket ONE subagent run rather than one session. The host tokens they map
|
|
87
|
+
// to were re-extracted at 2.1.229 rather than carried over from the 2.1.220
|
|
88
|
+
// spike — see agents/evidence/investigations/subagent-lifecycle-phase0-host-pin.md.
|
|
89
|
+
// Only `claude` and `cowork` alias them today; every other platform simply
|
|
90
|
+
// never sends them, which the alias table already expresses by omission.
|
|
83
91
|
export const EVENT_VOCABULARY: ReadonlySet<string> = new Set([
|
|
84
92
|
"session_start",
|
|
85
93
|
"session_end",
|
|
@@ -89,6 +97,8 @@ export const EVENT_VOCABULARY: ReadonlySet<string> = new Set([
|
|
|
89
97
|
"stop",
|
|
90
98
|
"pre_compact",
|
|
91
99
|
"agent_error",
|
|
100
|
+
"subagent_start",
|
|
101
|
+
"subagent_stop",
|
|
92
102
|
]);
|
|
93
103
|
|
|
94
104
|
const _SEVERITY_BY_EXIT: Record<number, string> = {
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Spawn-guard — SHADOW ONLY (road-to-subagent-lifecycle-integrity Phase 3
|
|
4
|
+
* Step 1, activated per `docs/contracts/concern-activation-policy.md`).
|
|
5
|
+
*
|
|
6
|
+
* What it does: on every `Agent` / `Task` tool call, read the open-dispatch
|
|
7
|
+
* set the Phase-1 ledger maintains, and append one `spawn_guard_shadow` line
|
|
8
|
+
* recording what a guard WOULD have done at several candidate thresholds.
|
|
9
|
+
* It emits nothing to the model and it never blocks. Exit is always 0.
|
|
10
|
+
*
|
|
11
|
+
* ── Why shadow and not warn ───────────────────────────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* The plan's own text said "ships warn-first … then flips to deny on
|
|
14
|
+
* evidence, per the concern activation policy". That policy did not exist
|
|
15
|
+
* when the step was written — three roadmaps cited it and none had written
|
|
16
|
+
* it — so the step could not be executed without inventing the posture it had
|
|
17
|
+
* deliberately deferred. The policy now exists, and it rules the warn rung
|
|
18
|
+
* out for a concern on its way to blocking:
|
|
19
|
+
*
|
|
20
|
+
* - `session-canary` is a per-turn injection, VERIFIED to fire, whose
|
|
21
|
+
* compliance miss rate did not move (24 of 29 task starts).
|
|
22
|
+
* - conformance round 5: both blocking carriers reached zero violations,
|
|
23
|
+
* neither advisory carrier did.
|
|
24
|
+
*
|
|
25
|
+
* A warn would pay this concern's full per-call cost and, on the evidence,
|
|
26
|
+
* buy nothing. Shadow pays the same cost and buys the distribution the
|
|
27
|
+
* threshold has to be derived from — which is the only thing standing
|
|
28
|
+
* between this guard and a number picked out of the air.
|
|
29
|
+
*
|
|
30
|
+
* ── Why several thresholds at once ────────────────────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* One candidate answers yes/no; a spread answers "at which value does this
|
|
33
|
+
* start catching things, and at which does it start denying legitimate
|
|
34
|
+
* work". The policy requires the shipped default to sit at the legitimate
|
|
35
|
+
* distribution's 99th percentile + 1, and that cannot be read off a single
|
|
36
|
+
* candidate. The plan's own N=2 / M=4 is included as one candidate among
|
|
37
|
+
* three, which is the honest status of a pre-registered starting value.
|
|
38
|
+
*
|
|
39
|
+
* ── What this does NOT measure, stated rather than implied ────────────────
|
|
40
|
+
*
|
|
41
|
+
* Depth. The estimate is the deepest open record + 1, maximised over ALL open
|
|
42
|
+
* records regardless of lineage, because pre-spawn there is no `agent_id` to
|
|
43
|
+
* resolve a real parent from. That makes it >= 2 whenever anything is open —
|
|
44
|
+
* flat fan-out included — so it **decides nothing**: no candidate's verdict
|
|
45
|
+
* reads it, and the line carries `depth_usable_for_derivation: false`. It
|
|
46
|
+
* rides along as context and becomes a real measurement only when Phase 0
|
|
47
|
+
* Step 4 establishes whether `agent_id` reaches a PreToolUse payload.
|
|
48
|
+
*
|
|
49
|
+
* PRIVACY BY CONSTRUCTION: counts and candidate verdicts only. No prompt, no
|
|
50
|
+
* agent id, no tool input — the record type has no field able to hold any.
|
|
51
|
+
*/
|
|
52
|
+
import * as fs from 'node:fs';
|
|
53
|
+
import * as path from 'node:path';
|
|
54
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
55
|
+
|
|
56
|
+
import { appendLedgerLine, openRecordStats, resolveConsumerRoot } from './subagent_ledger_hook.js';
|
|
57
|
+
import { readHookStdin } from './hook_stdin.js';
|
|
58
|
+
|
|
59
|
+
const EXIT_ALLOW = 0;
|
|
60
|
+
|
|
61
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
62
|
+
type JsonObject = { [k: string]: JsonValue };
|
|
63
|
+
|
|
64
|
+
function isObject(v: unknown): v is JsonObject {
|
|
65
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The two subagent-dispatch tool names this repo has observed across hosts. */
|
|
69
|
+
export const DISPATCH_TOOL_NAMES: ReadonlySet<string> = new Set(['Agent', 'Task']);
|
|
70
|
+
|
|
71
|
+
/** One candidate guard configuration under shadow evaluation. */
|
|
72
|
+
export interface Candidate {
|
|
73
|
+
readonly label: string;
|
|
74
|
+
/** Refuse when the new dispatch's depth would reach this. */
|
|
75
|
+
readonly max_depth: number;
|
|
76
|
+
/** Refuse when this many dispatches are already open. */
|
|
77
|
+
readonly max_concurrent: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The candidate spread. `n2m4` is the roadmap's pre-registered pair, carried
|
|
82
|
+
* as ONE candidate rather than as the answer; the wider pairs exist so the
|
|
83
|
+
* shadow log yields a curve instead of a single verdict.
|
|
84
|
+
*/
|
|
85
|
+
export const CANDIDATES: readonly Candidate[] = [
|
|
86
|
+
{ label: 'n2m4', max_depth: 2, max_concurrent: 4 },
|
|
87
|
+
{ label: 'n3m6', max_depth: 3, max_concurrent: 6 },
|
|
88
|
+
{ label: 'n4m8', max_depth: 4, max_concurrent: 8 },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
export interface ShadowVerdict {
|
|
92
|
+
readonly label: string;
|
|
93
|
+
readonly would_deny: boolean;
|
|
94
|
+
/** Which arm tripped. Only `concurrent` can appear — see below. */
|
|
95
|
+
readonly on: string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Pure: evaluate every candidate against the observed state.
|
|
100
|
+
*
|
|
101
|
+
* **Only the concurrency arm produces a verdict** (R2 round 2, finding 3). The
|
|
102
|
+
* depth arm is confounded by construction and was silently poisoning the very
|
|
103
|
+
* curve this shadow exists to produce:
|
|
104
|
+
*
|
|
105
|
+
* `depth_estimate` is `max(open depths) + 1`, maximised over ALL open records
|
|
106
|
+
* regardless of lineage, and every record carries depth >= 1. So the estimate
|
|
107
|
+
* is >= 2 whenever anything at all is open — including a flat fan-out with no
|
|
108
|
+
* nesting whatsoever. `n2m4`'s depth arm would then read `would_deny` on an
|
|
109
|
+
* ordinary sibling spawn, and the 99th-percentile derivation the activation
|
|
110
|
+
* policy prescribes would be computed over the estimator's artefact rather
|
|
111
|
+
* than over observed nesting.
|
|
112
|
+
*
|
|
113
|
+
* This was not caught by reasoning; it was caught by a reviewer after an
|
|
114
|
+
* end-to-end probe produced exactly that reading and it was briefly mistaken
|
|
115
|
+
* for a signal.
|
|
116
|
+
*
|
|
117
|
+
* Real per-spawn depth needs the parent lineage, which needs `agent_id` on the
|
|
118
|
+
* PreToolUse payload — Phase 0 Step 4, still open. Until it lands, the depth
|
|
119
|
+
* number rides along as context, flagged unusable, and decides nothing.
|
|
120
|
+
*/
|
|
121
|
+
export function evaluateCandidates(depthEstimate: number, concurrentOpen: number): ShadowVerdict[] {
|
|
122
|
+
void depthEstimate;
|
|
123
|
+
return CANDIDATES.map((c) => {
|
|
124
|
+
const on: string[] = [];
|
|
125
|
+
if (concurrentOpen >= c.max_concurrent) on.push('concurrent');
|
|
126
|
+
return { label: c.label, would_deny: on.length > 0, on };
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function unwrapPayload(envelope: JsonObject): JsonObject {
|
|
131
|
+
const inner = envelope['payload'];
|
|
132
|
+
return isObject(inner) ? inner : envelope;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function extractToolName(payload: JsonObject): string | null {
|
|
136
|
+
const v = payload['tool_name'] ?? payload['toolName'] ?? payload['tool'];
|
|
137
|
+
return typeof v === 'string' && v ? v : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function processEnvelope(envelope: JsonValue, consumerRoot: string): number {
|
|
141
|
+
try {
|
|
142
|
+
if (!isObject(envelope)) return EXIT_ALLOW;
|
|
143
|
+
const payload = unwrapPayload(envelope);
|
|
144
|
+
const toolName = extractToolName(payload);
|
|
145
|
+
if (toolName === null || !DISPATCH_TOOL_NAMES.has(toolName)) return EXIT_ALLOW;
|
|
146
|
+
|
|
147
|
+
const stats = openRecordStats(consumerRoot);
|
|
148
|
+
// Pre-spawn there is no agent id to resolve a parent from; the deepest
|
|
149
|
+
// open record + 1 is an upper bound, exact only for a linear chain.
|
|
150
|
+
const depthEstimate = stats.max_depth + 1;
|
|
151
|
+
const verdicts = evaluateCandidates(depthEstimate, stats.open_count);
|
|
152
|
+
const nowIso = new Date().toISOString();
|
|
153
|
+
|
|
154
|
+
appendLedgerLine(consumerRoot, nowIso, {
|
|
155
|
+
event: 'spawn_guard_shadow',
|
|
156
|
+
ts: nowIso,
|
|
157
|
+
tool: toolName,
|
|
158
|
+
concurrent_open: stats.open_count,
|
|
159
|
+
depth_estimate: depthEstimate,
|
|
160
|
+
depth_estimate_basis: 'deepest-open-record-plus-one',
|
|
161
|
+
// R2 round 2, finding 3: the estimate is >= 2 whenever anything is
|
|
162
|
+
// open, nesting or not, so it must not reach a derivation. It rides
|
|
163
|
+
// as context with the flag that says so.
|
|
164
|
+
depth_usable_for_derivation: false,
|
|
165
|
+
// R2 round 2, finding 4: without these, a quiet estate, a ledger
|
|
166
|
+
// this hook cannot see, and a root mismatch all emit byte-identical
|
|
167
|
+
// records — the instrument-goes-quiet failure one layer up from the
|
|
168
|
+
// one the ledger already fixed for itself.
|
|
169
|
+
ledger_present: stats.ledger_present,
|
|
170
|
+
stale_open_excluded: stats.stale_excluded,
|
|
171
|
+
candidates: verdicts.map((v) => ({ label: v.label, would_deny: v.would_deny, on: v.on })),
|
|
172
|
+
// Shadow posture is a property of this file, not of a setting —
|
|
173
|
+
// there is no code path here that can deny.
|
|
174
|
+
posture: 'shadow',
|
|
175
|
+
});
|
|
176
|
+
} catch {
|
|
177
|
+
// Never disturb a tool call for a measurement.
|
|
178
|
+
return EXIT_ALLOW;
|
|
179
|
+
}
|
|
180
|
+
return EXIT_ALLOW;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// R2 round 2, finding 6: this file used to carry its OWN root-resolution
|
|
184
|
+
// order, a third one alongside the ledger's and the turn-end gate's. A reader
|
|
185
|
+
// that resolves a different root than the writer finds an empty ledger, and
|
|
186
|
+
// every consumer here reads an empty ledger as "nothing is open" — a silent
|
|
187
|
+
// wrong answer rather than an error. One exported resolver, three callers.
|
|
188
|
+
|
|
189
|
+
export function main(): number {
|
|
190
|
+
const raw = readHookStdin();
|
|
191
|
+
let envelope: JsonValue = {};
|
|
192
|
+
try {
|
|
193
|
+
envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
|
|
194
|
+
} catch {
|
|
195
|
+
return EXIT_ALLOW;
|
|
196
|
+
}
|
|
197
|
+
return processEnvelope(envelope, resolveConsumerRoot(envelope));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
201
|
+
function _isCliEntry(): boolean {
|
|
202
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) return false;
|
|
203
|
+
if (process.argv[1] === undefined) return false;
|
|
204
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
205
|
+
if (import.meta.url === argvUrl) return true;
|
|
206
|
+
try {
|
|
207
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (_isCliEntry()) process.exit(main());
|