@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.
Files changed (107) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +147 -0
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze.md +2 -1
  5. package/dist/agent-src/commands/council/debate.md +13 -4
  6. package/dist/agent-src/commands/council/default.md +26 -11
  7. package/dist/agent-src/commands/council.md +1 -1
  8. package/dist/agent-src/commands/design-system/capture.md +44 -0
  9. package/dist/agent-src/commands/design-system/generate.md +75 -0
  10. package/dist/agent-src/commands/design-system/import.md +63 -0
  11. package/dist/agent-src/commands/design-system.md +66 -0
  12. package/dist/agent-src/commands/pr/create.md +51 -26
  13. package/dist/agent-src/commands/worktree/create.md +5 -3
  14. package/dist/agent-src/rules/settings-ask-protocol.md +14 -9
  15. package/dist/agent-src/skills/ai-council/SKILL.md +24 -6
  16. package/dist/agent-src/skills/corpus-grounding/SKILL.md +12 -1
  17. package/dist/agent-src/skills/corpus-grounding/scripts/decision_engine.ts +215 -7
  18. package/dist/agent-src/skills/corpus-grounding/scripts/ground.ts +39 -1
  19. package/dist/agent-src/skills/design-intelligence/ATTRIBUTION.md +19 -2
  20. package/dist/agent-src/skills/design-intelligence/data/manifest.json +36 -1
  21. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -0
  22. package/dist/agent-src/skills/design-system-capture/SKILL.md +9 -0
  23. package/dist/agent-src/skills/design-system-capture/references/design-system-json.md +70 -0
  24. package/dist/agent-src/skills/git-workflow/SKILL.md +9 -3
  25. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +9 -6
  26. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -21
  27. package/dist/agent-src/templates/agent-settings.md +0 -1
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
  30. package/dist/cli-delegate/{chunk-BV3WR67D.js → chunk-55WZN5VF.js} +3 -6
  31. package/dist/cli-delegate/{chunk-X3VOARZD.js → chunk-L565YFXH.js} +3 -3
  32. package/dist/cli-delegate/{chunk-JMQZ7MM5.js → chunk-MNWRDMR5.js} +1 -1
  33. package/dist/cli-delegate/{chunk-IR3IVRVC.js → chunk-NLP54U3J.js} +2 -1
  34. package/dist/cli-delegate/{chunk-FV34EGEX.js → chunk-NN4KW4HZ.js} +1 -1
  35. package/dist/cli-delegate/{chunk-6ENSXSBW.js → chunk-PPQO4NGR.js} +1 -1
  36. package/dist/cli-delegate/chunk-RLN73PR5.js +157 -0
  37. package/dist/cli-delegate/{chunk-6LOPHJLD.js → chunk-RYNADUFG.js} +1 -1
  38. package/dist/cli-delegate/{chunk-I5EAFRII.js → chunk-ZPSXJTLI.js} +2 -154
  39. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  40. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  41. package/dist/cli-delegate/cmd_conformance.js +673 -215
  42. package/dist/cli-delegate/cmd_doctor.js +2 -2
  43. package/dist/cli-delegate/cmd_explain.js +2 -2
  44. package/dist/cli-delegate/cmd_export.js +4 -4
  45. package/dist/cli-delegate/cmd_handoff.js +10 -8
  46. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  47. package/dist/cli-delegate/cmd_migrate.js +2 -2
  48. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  49. package/dist/cli-delegate/cmd_prune.js +4 -4
  50. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  51. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  52. package/dist/cli-delegate/cmd_session_recycle.js +6 -4
  53. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  54. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  55. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  56. package/dist/cli-delegate/cmd_sync.js +4 -4
  57. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  58. package/dist/cli-delegate/cmd_update.js +1 -1
  59. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  60. package/dist/cli-delegate/cmd_validate.js +4 -4
  61. package/dist/cli-delegate/cmd_versions.js +1 -1
  62. package/dist/discovery/deprecation-report.md +1 -1
  63. package/dist/discovery/discovery-manifest.json +127 -22
  64. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  65. package/dist/discovery/discovery-manifest.summary.md +3 -3
  66. package/dist/discovery/dormancy-report.md +1 -1
  67. package/dist/discovery/orphan-report.md +1 -1
  68. package/dist/discovery/packs.json +9 -5
  69. package/dist/discovery/trust-report.md +2 -2
  70. package/dist/discovery/workspaces.json +8 -4
  71. package/dist/hooks/dispatch.js +994 -399
  72. package/dist/install/install.mjs +5 -7
  73. package/dist/mcp/registry-manifest.json +2 -2
  74. package/dist/mcp/server.json +2 -2
  75. package/dist/mcp/server.mjs +1104 -593
  76. package/dist/scripts/_lib/agent_settings.js +8 -0
  77. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  78. package/dist/server/schemas/settings.js +3 -4
  79. package/dist/server/schemas/settings.js.map +1 -1
  80. package/dist/ui/assets/{index-5fXOHoVh.js → index-B0aMJnuz.js} +2 -2
  81. package/dist/ui/assets/{index-5fXOHoVh.js.map → index-B0aMJnuz.js.map} +1 -1
  82. package/dist/ui/index.html +1 -1
  83. package/docs/contracts/settings-classes.md +12 -9
  84. package/package.json +1 -1
  85. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
  86. package/src/config/agent-settings.template.yml +15 -18
  87. package/src/config/gate-coverage.yml +33 -2
  88. package/src/config/gate-violation-baselines.json +2 -2
  89. package/src/config/pack-size-budget.json +3 -2
  90. package/src/scripts/_lib/agent_settings.ts +8 -0
  91. package/src/scripts/_lib/design_system_import.ts +987 -0
  92. package/src/scripts/check_branch_freshness.ts +238 -6
  93. package/src/scripts/design_system_import.ts +264 -0
  94. package/src/scripts/external_sources_denylist.json +7 -2
  95. package/src/scripts/hook_manifest.yaml +71 -2
  96. package/src/scripts/hooks/concern_registry.ts +4 -0
  97. package/src/scripts/hooks/dispatch_hook.ts +10 -0
  98. package/src/scripts/hooks/host_semantics.ts +2 -0
  99. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +212 -0
  100. package/src/scripts/hooks/subagent_ledger_hook.ts +723 -0
  101. package/src/scripts/hooks/turn_end_gate_hook.ts +39 -2
  102. package/src/scripts/lint_hook_manifest.ts +2 -0
  103. package/src/scripts/lint_rule_skill_pack_reach.ts +194 -13
  104. package/src/scripts/session_register_hook.ts +120 -10
  105. package/src/server/schemas/settings.ts +3 -6
  106. package/tests/fixtures/hooks/subagent_start.json +7 -0
  107. 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> = {
@@ -62,6 +62,8 @@ const CLAUDE_HOOK_EVENT_NAME: Record<string, string> = {
62
62
  session_end: "SessionEnd",
63
63
  stop: "Stop",
64
64
  pre_compact: "PreCompact",
65
+ subagent_start: "SubagentStart",
66
+ subagent_stop: "SubagentStop",
65
67
  };
66
68
 
67
69
  /**
@@ -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());