@event4u/agent-config 14.6.0 → 14.7.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 (188) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +260 -7
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/inbox.md +264 -10
  5. package/dist/agent-src/commands/analyze.md +1 -1
  6. package/dist/agent-src/commands/roadmap/process-full.md +116 -30
  7. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +31 -0
  8. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +135 -104
  9. package/dist/agent-src/rules/context-hygiene.md +2 -0
  10. package/dist/agent-src/rules/decision-revisit-gate.md +3 -0
  11. package/dist/agent-src/rules/fix-what-you-see.md +108 -0
  12. package/dist/agent-src/rules/no-roadmap-references.md +0 -5
  13. package/dist/agent-src/rules/recurring-criticism.md +110 -0
  14. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  15. package/dist/agent-src/rules/rule-type-governance.md +0 -2
  16. package/dist/agent-src/rules/self-repair-loop.md +17 -0
  17. package/dist/agent-src/rules/skill-quality.md +0 -2
  18. package/dist/agent-src/rules/source-confidentiality.md +37 -7
  19. package/dist/agent-src/scripts/archival_sweep.ts +117 -0
  20. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  21. package/dist/agent-src/scripts/gate_budget.ts +282 -0
  22. package/dist/agent-src/scripts/gate_execute.ts +72 -31
  23. package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
  24. package/dist/agent-src/scripts/update_roadmap_progress.ts +251 -7
  25. package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
  26. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
  27. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
  28. package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
  29. package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
  30. package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
  31. package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
  32. package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
  33. package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
  34. package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
  35. package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
  36. package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
  37. package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
  38. package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
  39. package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
  40. package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
  41. package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
  42. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
  43. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  44. package/dist/agent-src/templates/roadmaps.md +45 -2
  45. package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  46. package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  47. package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  48. package/dist/cli/registry.js +1 -1
  49. package/dist/cli/registry.js.map +1 -1
  50. package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
  51. package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-4VR4ORQ6.js} +10 -7
  52. package/dist/cli-delegate/chunk-6EOCNQAM.js +402 -0
  53. package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-74E3TKWV.js} +381 -280
  54. package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-7F3VYSM2.js} +4 -4
  55. package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
  56. package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
  57. package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-GJ5BVHXM.js} +72 -165
  58. package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-JZ4NR2TW.js} +338 -0
  59. package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
  60. package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
  61. package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
  62. package/dist/cli-delegate/cmd_conformance.js +459 -709
  63. package/dist/cli-delegate/cmd_doctor.js +6 -6
  64. package/dist/cli-delegate/cmd_explain.js +1 -1
  65. package/dist/cli-delegate/cmd_export.js +7 -6
  66. package/dist/cli-delegate/cmd_handoff.js +6 -6
  67. package/dist/cli-delegate/cmd_packs_active.js +1 -1
  68. package/dist/cli-delegate/cmd_preflight.js +2 -2
  69. package/dist/cli-delegate/cmd_prune.js +7 -6
  70. package/dist/cli-delegate/cmd_session_recycle.js +840 -30
  71. package/dist/cli-delegate/cmd_settings_migrate.js +7 -6
  72. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  73. package/dist/cli-delegate/cmd_sync.js +7 -6
  74. package/dist/cli-delegate/cmd_uninstall.js +9 -7
  75. package/dist/cli-delegate/cmd_update.js +1 -1
  76. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  77. package/dist/cli-delegate/cmd_validate.js +7 -6
  78. package/dist/discovery/deprecation-report.md +1 -1
  79. package/dist/discovery/discovery-manifest.json +68 -25
  80. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  81. package/dist/discovery/discovery-manifest.summary.md +4 -4
  82. package/dist/discovery/dormancy-report.md +1 -1
  83. package/dist/discovery/orphan-report.md +1 -1
  84. package/dist/discovery/packs.json +10 -8
  85. package/dist/discovery/trust-report.md +3 -3
  86. package/dist/discovery/workspaces.json +8 -6
  87. package/dist/hooks/dispatch.js +2901 -2053
  88. package/dist/install/claudePathsPlan.js +187 -0
  89. package/dist/install/claudePathsPlan.js.map +1 -0
  90. package/dist/install/hostLayerFingerprint.js +136 -0
  91. package/dist/install/hostLayerFingerprint.js.map +1 -0
  92. package/dist/install/install.mjs +553 -425
  93. package/dist/install/partitionEligibility.js +276 -0
  94. package/dist/install/partitionEligibility.js.map +1 -0
  95. package/dist/mcp/registry-manifest.json +3 -3
  96. package/dist/mcp/server.json +2 -2
  97. package/dist/mcp/server.mjs +1744 -1191
  98. package/dist/router.json +1 -1
  99. package/dist/scripts/_lib/agent_src.js +41 -0
  100. package/dist/scripts/_lib/agent_src.js.map +1 -1
  101. package/dist/scripts/_lib/install_layout.js +83 -0
  102. package/dist/scripts/_lib/install_layout.js.map +1 -0
  103. package/dist/scripts/_lib/installed_lock.js +444 -0
  104. package/dist/scripts/_lib/installed_lock.js.map +1 -0
  105. package/dist/scripts/condense.js +13 -164
  106. package/dist/scripts/condense.js.map +1 -1
  107. package/dist/server/schemas/settings.js +4 -0
  108. package/dist/server/schemas/settings.js.map +1 -1
  109. package/docs/contracts/settings-classes.md +12 -6
  110. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
  111. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
  112. package/docs/guidelines/agent-infra/memory-access.md +17 -0
  113. package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  114. package/package.json +1 -1
  115. package/src/agent-src/scripts/archival_sweep.ts +117 -0
  116. package/src/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  117. package/src/agent-src/scripts/gate_budget.ts +282 -0
  118. package/src/agent-src/scripts/gate_execute.ts +72 -31
  119. package/src/agent-src/scripts/roadmap_gates.ts +74 -3
  120. package/src/agent-src/scripts/update_roadmap_progress.ts +251 -7
  121. package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  122. package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  123. package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  124. package/src/config/agent-settings.template.yml +27 -2
  125. package/src/config/ci-local-parity.yml +18 -0
  126. package/src/config/estate-count-budget.json +339 -3
  127. package/src/config/gate-violation-baselines.json +41 -41
  128. package/src/config/rule-activation-census.json +14 -6
  129. package/src/config/rule-stub-ceilings.json +19 -6
  130. package/src/install/claudePathsPlan.ts +196 -0
  131. package/src/install/hostLayerFingerprint.ts +145 -0
  132. package/src/install/partitionEligibility.ts +325 -0
  133. package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
  134. package/src/scripts/_dispatch.bash +6 -1
  135. package/src/scripts/_lib/ac_heading.ts +30 -0
  136. package/src/scripts/_lib/agent_src.ts +42 -0
  137. package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
  138. package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
  139. package/src/scripts/_lib/envelope_grounding.ts +38 -8
  140. package/src/scripts/_lib/installed_lock.ts +50 -5
  141. package/src/scripts/_lib/md_section.ts +129 -0
  142. package/src/scripts/_lib/scope_guard.sh +26 -3
  143. package/src/scripts/_lib/self_repair.ts +19 -0
  144. package/src/scripts/_lib/self_repair_class_b.ts +171 -0
  145. package/src/scripts/ai_council/cli_help.ts +55 -0
  146. package/src/scripts/before_complete_hook.ts +153 -28
  147. package/src/scripts/bench_ab_v2_complexity.ts +27 -7
  148. package/src/scripts/check_references.ts +60 -0
  149. package/src/scripts/check_release_published.ts +32 -11
  150. package/src/scripts/check_single_delivery.ts +59 -15
  151. package/src/scripts/ci_settle.ts +170 -0
  152. package/src/scripts/condense.ts +20 -170
  153. package/src/scripts/council_cli.ts +3 -3
  154. package/src/scripts/dispatch_r2_reviewer.ts +6 -14
  155. package/src/scripts/hook_manifest.json +1 -1
  156. package/src/scripts/hook_manifest.yaml +77 -14
  157. package/src/scripts/hooks/block_no_verify.ts +61 -10
  158. package/src/scripts/hooks/block_unauthorized_git.ts +1 -1
  159. package/src/scripts/hooks/concern_registry.ts +4 -0
  160. package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
  161. package/src/scripts/hooks/run_continuation_hook.ts +8 -5
  162. package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
  163. package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
  164. package/src/scripts/hooks/state_io.ts +782 -20
  165. package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
  166. package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
  167. package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
  168. package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
  169. package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
  170. package/src/scripts/install.ts +16 -11
  171. package/src/scripts/language_mirror_hook.ts +561 -25
  172. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  173. package/src/scripts/lint_plan_risk_register.ts +4 -3
  174. package/src/scripts/lint_skill_router_head.ts +4 -4
  175. package/src/scripts/mcp_server/tools.ts +2 -2
  176. package/src/scripts/new_roadmap.ts +136 -0
  177. package/src/scripts/pack_mcp_content.ts +20 -5
  178. package/src/scripts/release.ts +37 -38
  179. package/src/scripts/report_layer_overlap.ts +6 -0
  180. package/src/scripts/self_repair_hook.ts +37 -5
  181. package/src/scripts/session_register_hook.ts +11 -0
  182. package/src/scripts/skill_usage_collect.ts +11 -1
  183. package/src/scripts/skill_usage_report.ts +278 -8
  184. package/src/scripts/sync_pr_branch.ts +7 -0
  185. package/src/server/schemas/settings.ts +8 -0
  186. package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
  187. package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
  188. /package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
@@ -0,0 +1,464 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * source-first-gate — SHADOW ONLY (road-to-source-first-frontend Phase 3
4
+ * Steps 1–2, activated per `docs/contracts/concern-activation-policy.md`).
5
+ *
6
+ * What it does: on every capture-shaped tool call, record what a source-first
7
+ * gate WOULD have warned under several candidate gating rules. It emits
8
+ * nothing to the model and it never blocks. Exit is always 0.
9
+ *
10
+ * ── Why shadow and not the step's literal "exit 2 warn" ───────────────────
11
+ *
12
+ * Phase 3 Step 1 was drafted as `exit 2 warn`; Step 2 left the posture as an
13
+ * explicit maintainer call and the roadmap's own 2026-08-17 correction records
14
+ * that the activation policy **contradicts** the drafted default-ON warn and
15
+ * that the step "needs re-arguing against it, not a citation". Re-argued, and
16
+ * the policy wins on both of the step's own dependencies:
17
+ *
18
+ * 1. **The verifier exemption stops being a shipping dependency.** Risk 3 of
19
+ * that roadmap is "the gate warns the one actor doing it right" — a
20
+ * verifier subagent screenshotting for QA. The harm in that risk is
21
+ * entirely in the *emission*: a concern that emits nothing warns nobody,
22
+ * correctly or otherwise. The clean exemption key is the payload
23
+ * `agent_id` / `agent_type` field that `road-to-subagent-lifecycle-
24
+ * integrity` Phase 0 Step 4 spikes and its Phase 4 Step 1 binds; both are
25
+ * still `[ ]`, and both are *fields on the record below* rather than
26
+ * branches, so the shadow window measures how large the exemption would
27
+ * have to be instead of guessing it.
28
+ * 2. **The matcher census stops being a shipping dependency.** Phase 1
29
+ * Step 2 is `[~]`: one host was censused and its only capture tool
30
+ * (`screencapture`) photographs the display rather than a page, so a
31
+ * matcher built from it alone "would watch the wrong surface". Under a
32
+ * warn posture that is a false-positive generator. Under shadow it is the
33
+ * measurement — `capture_kind` below separates page-reaching capture from
34
+ * display capture on every record, which is exactly the datum the census
35
+ * is missing.
36
+ *
37
+ * The policy's ladder is `shadow → blocking`, with advisory as a *destination*
38
+ * rather than a waiting room. This concern's intended terminal posture IS
39
+ * advisory (a screenshot is a judgement call, the `ui-route-nudge` argument
40
+ * verbatim, and Phase 3 Step 1 says "never a block"). That does not license
41
+ * skipping shadow: the policy skips the advisory *rung* on the way to blocking
42
+ * because a reminder buys little, and the same measurement debt applies to a
43
+ * terminal advisory — without the shadow window there is no false-positive
44
+ * rate for a warn either.
45
+ *
46
+ * ── Flip condition, pre-registered as Step 2 requires ─────────────────────
47
+ *
48
+ * Shadow → advisory (warn, exit 2) when ALL of:
49
+ * a. >= 100 capture-shaped records, or >= 2 weeks of window, whichever
50
+ * comes first, including at least one record with
51
+ * `handover_seen_in_session: true` (the policy's window floor);
52
+ * b. one candidate rule's false-positive share — records where the
53
+ * candidate would warn AND `capture_kind` is not `page` — is <= 1 %;
54
+ * c. `verifier_exemption_decidable` is true on the records, i.e. SLI
55
+ * Phase 4 Step 1 has landed and the payload axis can carry the
56
+ * exemption;
57
+ * d. the shipped matcher is narrowed to the entries that actually appear,
58
+ * which closes Phase 1 Step 2's multi-host half by observation.
59
+ * Reverse trigger: no records in 8 weeks → evaluate removal; the concern's
60
+ * population would then be as empty as the read-before-write rate's.
61
+ *
62
+ * ── What this does NOT measure, stated rather than implied ────────────────
63
+ *
64
+ * "The session has a routed design handover" is the step's first condition and
65
+ * **nothing records it**. `design-fidelity` is a rule; it has no runtime
66
+ * carrier, so "the same trigger event `design-fidelity` records" describes an
67
+ * event that does not exist. Two observable proxies are recorded side by side
68
+ * instead of one being promoted by preference — a handover path named by any
69
+ * tool this session, and a handover artifact present on disk — and the
70
+ * candidate spread is what tells them apart. Picking one now would be the
71
+ * threshold-first mistake the activation policy names.
72
+ *
73
+ * PRIVACY BY CONSTRUCTION: booleans, counts, a matcher label, and the
74
+ * id-shaped `agent_type` enum (recorded verbatim, the same stance
75
+ * `subagent_ledger_hook.ts` states for that field). No prompt, no command
76
+ * text, no file path, no `agent_id` — the record type has no field able to
77
+ * hold any.
78
+ */
79
+ import * as fs from 'node:fs';
80
+ import * as path from 'node:path';
81
+ import { fileURLToPath, pathToFileURL } from 'node:url';
82
+
83
+ import { is_replay_mode } from './state_io.js';
84
+ import { readHookStdin } from './hook_stdin.js';
85
+ import { resolveConsumerRoot } from './subagent_ledger_hook.js';
86
+
87
+ const EXIT_ALLOW = 0;
88
+
89
+ /** Would-warns per session before the valve goes silent — `MAX_NUDGES` shape. */
90
+ export const MAX_SHADOW_WARNS = 2;
91
+
92
+ /** Shadow log; `/agents/runtime/` is gitignored, so this stays local-only. */
93
+ export const SHADOW_LOG = path.join('agents', 'runtime', 'state', 'source-first-gate.jsonl');
94
+ /** Per-session latches. */
95
+ export const STATE_FILE = path.join('agents', 'runtime', 'state', 'source-first-gate.json');
96
+
97
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
98
+ type JsonObject = { [k: string]: JsonValue };
99
+
100
+ function isObject(v: unknown): v is JsonObject {
101
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
102
+ }
103
+
104
+ function unwrapPayload(envelope: JsonObject): JsonObject {
105
+ const inner = envelope['payload'];
106
+ return isObject(inner) ? inner : envelope;
107
+ }
108
+
109
+ /**
110
+ * What a capture-shaped tool call reaches.
111
+ *
112
+ * `page` is the surface this concern cares about; `display` is the Phase-1
113
+ * census finding that the one tool actually present on the censused host
114
+ * photographs the physical screen, so it cannot be the "screenshot instead of
115
+ * source" path at all. Keeping them apart on every record is what lets the
116
+ * flip condition compute a false-positive share instead of asserting one.
117
+ */
118
+ export type CaptureKind = 'page' | 'display' | 'unknown';
119
+
120
+ export interface MatcherHit {
121
+ readonly label: string;
122
+ readonly capture_kind: CaptureKind;
123
+ }
124
+
125
+ /**
126
+ * Tool-name matchers. TREE-SOURCED, not recalled — each entry is a name this
127
+ * repository already writes down, which is the closest available substitute
128
+ * for the multi-host census Phase 1 Step 2 could not finish:
129
+ *
130
+ * - `browser_take_screenshot` — `src/skills/mcp/SKILL.md` § tool table.
131
+ * - `claude-in-chrome` — `src/skills/screenshot-hygiene/SKILL.md`,
132
+ * `docs/decisions/ADR-125-doc-screenshot-anonymization.md`.
133
+ * - `screencapture` — the one entry the census measured present,
134
+ * `tests/design-artifacts/eval-fixtures.md` § the census note.
135
+ *
136
+ * The trailing generic patterns exist because host tool naming varies (the
137
+ * `Bash`/`BashTool`/`launch-process` spread the manifest already warns about
138
+ * for the blocking guards). Under shadow an over-broad matcher costs a log
139
+ * line, and its breadth is the thing being measured; a narrowed shipped
140
+ * matcher is flip condition (d).
141
+ */
142
+ export const TOOL_MATCHERS: readonly { pattern: RegExp; hit: MatcherHit }[] = [
143
+ { pattern: /^browser_take_screenshot$/i, hit: { label: 'playwright-mcp', capture_kind: 'page' } },
144
+ { pattern: /^mcp__claude-in-chrome__/i, hit: { label: 'claude-in-chrome', capture_kind: 'page' } },
145
+ { pattern: /^(browser_)?take_screenshot$/i, hit: { label: 'chrome-devtools-mcp', capture_kind: 'page' } },
146
+ { pattern: /screen_?shot/i, hit: { label: 'tool-name-generic', capture_kind: 'unknown' } },
147
+ { pattern: /screen_?capture/i, hit: { label: 'tool-name-generic', capture_kind: 'unknown' } },
148
+ ];
149
+
150
+ /** Shell-shaped tool names whose command string is worth reading. */
151
+ const SHELL_TOOLS = /^(bash|bashtool|launch[-_]process|shell|run[-_]command)$/i;
152
+
153
+ /** Command-text matchers for a capture reached through a shell. */
154
+ export const COMMAND_MATCHERS: readonly { pattern: RegExp; hit: MatcherHit }[] = [
155
+ { pattern: /\bscreencapture\b/i, hit: { label: 'macos-screencapture', capture_kind: 'display' } },
156
+ { pattern: /\bimport\s+-window\b/i, hit: { label: 'x11-import', capture_kind: 'display' } },
157
+ { pattern: /\bgnome-screenshot\b|\bspectacle\b|\bscrot\b/i, hit: { label: 'linux-screenshot', capture_kind: 'display' } },
158
+ ];
159
+
160
+ /**
161
+ * The handover path shapes `design-fidelity` routes on a FILE rather than on a
162
+ * word: `file_pattern: *design.html` and `path_prefix: .claude/design-system/`.
163
+ *
164
+ * DIRECTION-AGNOSTIC ON PURPOSE, and that is the one difference from
165
+ * `ui_route_nudge_hook.isArtifactRead`, which requires a non-write. Handover
166
+ * *presence* and handover *having been read* are the two separate facts this
167
+ * concern needs, and a single predicate cannot carry both. The read latch
168
+ * below delegates to that exported predicate rather than re-deriving it;
169
+ * `source_first_gate.test.ts` pins the two path shapes against each other so
170
+ * the copy cannot drift.
171
+ */
172
+ export function isHandoverPath(file: string): boolean {
173
+ if (!file) return false;
174
+ const normalized = file.replace(/\\/g, '/').toLowerCase();
175
+ return normalized.endsWith('design.html') || normalized.includes('.claude/design-system/');
176
+ }
177
+
178
+ export interface CaptureEvent {
179
+ /** Matcher hit, or null when this is not a capture-shaped call. */
180
+ readonly capture: MatcherHit | null;
181
+ /** Path the tool names; "" when it names none. */
182
+ readonly file: string;
183
+ /** True when the tool proposes new content (Write / Edit shape). */
184
+ readonly isWrite: boolean;
185
+ readonly agentIdPresent: boolean;
186
+ readonly agentType: string | null;
187
+ }
188
+
189
+ function str(o: JsonObject, ...keys: string[]): string | null {
190
+ for (const k of keys) {
191
+ const v = o[k];
192
+ if (typeof v === 'string' && v) return v;
193
+ }
194
+ return null;
195
+ }
196
+
197
+ /** Classify one envelope. Pure apart from the regex tables above. */
198
+ export function extractEvent(outer: JsonValue): CaptureEvent | null {
199
+ if (!isObject(outer)) return null;
200
+ const payload = unwrapPayload(outer);
201
+ const toolName = str(payload, 'tool_name', 'toolName', 'tool');
202
+
203
+ const ti = payload['tool_input'] ?? payload['toolInput'] ?? payload['input'];
204
+ const input = isObject(ti) ? ti : {};
205
+ const file = str(input, 'file_path', 'path', 'filePath') ?? '';
206
+ let isWrite = false;
207
+ for (const key of ['content', 'new_string', 'new_str', 'text', 'newText']) {
208
+ if (typeof input[key] === 'string' && (input[key] as string).length > 0) isWrite = true;
209
+ }
210
+
211
+ let capture: MatcherHit | null = null;
212
+ if (toolName) {
213
+ for (const m of TOOL_MATCHERS) {
214
+ if (m.pattern.test(toolName)) {
215
+ capture = m.hit;
216
+ break;
217
+ }
218
+ }
219
+ if (!capture && SHELL_TOOLS.test(toolName)) {
220
+ const command = str(input, 'command', 'cmd', 'script') ?? '';
221
+ for (const m of COMMAND_MATCHERS) {
222
+ if (m.pattern.test(command)) {
223
+ capture = m.hit;
224
+ break;
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ return {
231
+ capture,
232
+ file,
233
+ isWrite,
234
+ agentIdPresent: str(payload, 'agent_id', 'agentId') !== null,
235
+ agentType: str(payload, 'agent_type', 'agentType'),
236
+ };
237
+ }
238
+
239
+ export interface SessionState {
240
+ /** A handover path was named by any tool this session. */
241
+ handoverSeen: boolean;
242
+ /** A handover artifact was READ this session (rung 1 of the ladder). */
243
+ sourceRead: boolean;
244
+ /** Would-warns already recorded, for the valve arm. */
245
+ wouldWarn: number;
246
+ }
247
+
248
+ export const EMPTY_STATE: SessionState = { handoverSeen: false, sourceRead: false, wouldWarn: 0 };
249
+
250
+ /** Sessions retained; oldest drop past this — `ui_route_nudge` shape. */
251
+ export const MAX_SESSIONS = 50;
252
+
253
+ export function readState(root: string, session: string): SessionState {
254
+ try {
255
+ const all = JSON.parse(fs.readFileSync(path.join(root, STATE_FILE), 'utf-8')) as Record<
256
+ string,
257
+ SessionState
258
+ >;
259
+ const entry = all[session];
260
+ if (
261
+ entry &&
262
+ typeof entry.handoverSeen === 'boolean' &&
263
+ typeof entry.sourceRead === 'boolean' &&
264
+ typeof entry.wouldWarn === 'number'
265
+ ) {
266
+ return entry;
267
+ }
268
+ } catch {
269
+ /* fresh session */
270
+ }
271
+ return { ...EMPTY_STATE };
272
+ }
273
+
274
+ export function writeState(root: string, session: string, state: SessionState): void {
275
+ if (is_replay_mode()) return;
276
+ try {
277
+ const file = path.join(root, STATE_FILE);
278
+ let all: Record<string, SessionState> = {};
279
+ try {
280
+ all = JSON.parse(fs.readFileSync(file, 'utf-8')) as Record<string, SessionState>;
281
+ } catch {
282
+ // Fresh or unparseable. A corrupt file has already lost whatever
283
+ // it held, so starting empty is recovery rather than discard; the
284
+ // cost is bounded to a re-latch in the sessions in flight.
285
+ }
286
+ if (!isObject(all as unknown)) all = {};
287
+ all[session] = state;
288
+ const keys = Object.keys(all);
289
+ if (keys.length > MAX_SESSIONS) {
290
+ const pruned: Record<string, SessionState> = {};
291
+ for (const key of keys.slice(keys.length - MAX_SESSIONS)) pruned[key] = all[key]!;
292
+ all = pruned;
293
+ }
294
+ fs.mkdirSync(path.dirname(file), { recursive: true });
295
+ fs.writeFileSync(file, JSON.stringify(all));
296
+ } catch {
297
+ /* fail-open: a persistence failure must never disturb a tool call */
298
+ }
299
+ }
300
+
301
+ /** One candidate gating rule under shadow evaluation. */
302
+ export interface Candidate {
303
+ readonly label: string;
304
+ readonly requiresHandoverSeen: boolean;
305
+ readonly requiresHandoverOnDisk: boolean;
306
+ /** Either proxy satisfies the handover condition. */
307
+ readonly handoverEither: boolean;
308
+ }
309
+
310
+ /**
311
+ * The candidate spread. `handover-session` is Phase 3 Step 1's literal gating
312
+ * rule, carried as ONE candidate rather than as the answer; `unread-only` is
313
+ * the loosest rule and therefore the fire-volume ceiling the others are read
314
+ * against. A spread yields the curve the activation policy derives from — one
315
+ * candidate would only yield a yes/no.
316
+ */
317
+ export const CANDIDATES: readonly Candidate[] = [
318
+ { label: 'handover-session', requiresHandoverSeen: true, requiresHandoverOnDisk: false, handoverEither: false },
319
+ { label: 'handover-disk', requiresHandoverSeen: false, requiresHandoverOnDisk: true, handoverEither: false },
320
+ { label: 'handover-either', requiresHandoverSeen: false, requiresHandoverOnDisk: false, handoverEither: true },
321
+ { label: 'unread-only', requiresHandoverSeen: false, requiresHandoverOnDisk: false, handoverEither: false },
322
+ ];
323
+
324
+ export interface CandidateVerdict {
325
+ readonly label: string;
326
+ readonly would_warn: boolean;
327
+ }
328
+
329
+ export function evaluateCandidates(
330
+ handoverSeen: boolean,
331
+ handoverOnDisk: boolean,
332
+ sourceRead: boolean,
333
+ ): CandidateVerdict[] {
334
+ return CANDIDATES.map((c) => {
335
+ if (sourceRead) return { label: c.label, would_warn: false };
336
+ if (c.requiresHandoverSeen) return { label: c.label, would_warn: handoverSeen };
337
+ if (c.requiresHandoverOnDisk) return { label: c.label, would_warn: handoverOnDisk };
338
+ if (c.handoverEither) return { label: c.label, would_warn: handoverSeen || handoverOnDisk };
339
+ return { label: c.label, would_warn: true };
340
+ });
341
+ }
342
+
343
+ /** True when a handover artifact sits in the workspace, independent of reads. */
344
+ export function handoverPresentOnDisk(root: string): boolean {
345
+ try {
346
+ return fs.statSync(path.join(root, '.claude', 'design-system')).isDirectory();
347
+ } catch {
348
+ return false;
349
+ }
350
+ }
351
+
352
+ export function appendShadowLine(root: string, line: Record<string, unknown>): void {
353
+ if (is_replay_mode()) return;
354
+ try {
355
+ const file = path.join(root, SHADOW_LOG);
356
+ fs.mkdirSync(path.dirname(file), { recursive: true });
357
+ fs.appendFileSync(file, `${JSON.stringify(line)}\n`, 'utf8');
358
+ } catch {
359
+ /* never disturb a tool call for a measurement */
360
+ }
361
+ }
362
+
363
+ /**
364
+ * The state transition, pure — so the test exercises the logic rather than the
365
+ * filesystem. A handover path seen latches presence; a handover READ latches
366
+ * rung 1. The read latch must be evaluated independently of the presence latch:
367
+ * an early return on the first hit would drop the read from the numerator, the
368
+ * same not-disjoint hazard `isArtifactRead`'s own header records.
369
+ */
370
+ export function nextState(event: CaptureEvent, state: SessionState): SessionState {
371
+ let next = state;
372
+ if (isHandoverPath(event.file)) next = { ...next, handoverSeen: true };
373
+ if (!event.isWrite && isHandoverPath(event.file)) next = { ...next, sourceRead: true };
374
+ return next;
375
+ }
376
+
377
+ export function processEnvelope(envelope: JsonValue, consumerRoot: string, session: string): number {
378
+ try {
379
+ const event = extractEvent(envelope);
380
+ if (!event) return EXIT_ALLOW;
381
+
382
+ const before = readState(consumerRoot, session);
383
+ const after = nextState(event, before);
384
+
385
+ if (event.capture === null) {
386
+ if (
387
+ after.handoverSeen !== before.handoverSeen ||
388
+ after.sourceRead !== before.sourceRead
389
+ ) {
390
+ writeState(consumerRoot, session, after);
391
+ }
392
+ return EXIT_ALLOW;
393
+ }
394
+
395
+ const onDisk = handoverPresentOnDisk(consumerRoot);
396
+ const verdicts = evaluateCandidates(after.handoverSeen, onDisk, after.sourceRead);
397
+ const anyWarn = verdicts.some((v) => v.would_warn);
398
+ const valveWouldSilence = before.wouldWarn >= MAX_SHADOW_WARNS;
399
+ const nowIso = new Date().toISOString();
400
+
401
+ appendShadowLine(consumerRoot, {
402
+ event: 'source_first_gate_shadow',
403
+ ts: nowIso,
404
+ tool_matcher: event.capture.label,
405
+ // The census finding, on every record: `display` capture cannot be
406
+ // the "screenshot instead of source" path, so a candidate warning
407
+ // on one is a false positive by construction. Flip condition (b)
408
+ // computes its share from this field.
409
+ capture_kind: event.capture.capture_kind,
410
+ handover_seen_in_session: after.handoverSeen,
411
+ handover_present_on_disk: onDisk,
412
+ source_read_in_session: after.sourceRead,
413
+ prior_would_warn_this_session: before.wouldWarn,
414
+ valve_would_silence: valveWouldSilence,
415
+ candidates: verdicts.map((v) => ({ label: v.label, would_warn: v.would_warn })),
416
+ // The verifier exemption's key, measured rather than assumed. Both
417
+ // SLI Phase 0 Step 4 and Phase 4 Step 1 are open, so this is
418
+ // expected false today; a run of trues is what makes flip
419
+ // condition (c) satisfiable.
420
+ agent_id_present: event.agentIdPresent,
421
+ agent_type: event.agentType,
422
+ verifier_exemption_decidable: event.agentIdPresent || event.agentType !== null,
423
+ // Shadow is a property of this file, not of a setting — there is no
424
+ // code path here that can warn or deny.
425
+ posture: 'shadow',
426
+ });
427
+
428
+ const persisted =
429
+ anyWarn && !valveWouldSilence ? { ...after, wouldWarn: after.wouldWarn + 1 } : after;
430
+ writeState(consumerRoot, session, persisted);
431
+ } catch {
432
+ return EXIT_ALLOW;
433
+ }
434
+ return EXIT_ALLOW;
435
+ }
436
+
437
+ export function main(): number {
438
+ const raw = readHookStdin();
439
+ let envelope: JsonValue = {};
440
+ try {
441
+ envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
442
+ } catch {
443
+ return EXIT_ALLOW;
444
+ }
445
+ const sessionVal = isObject(envelope) ? envelope['session_id'] : null;
446
+ const session = typeof sessionVal === 'string' && sessionVal ? sessionVal : 'unknown';
447
+ return processEnvelope(envelope, resolveConsumerRoot(envelope), session);
448
+ }
449
+
450
+ // Bundle-safety: never auto-run when inlined into an esbuild bundle, where
451
+ // every module shares the bundle's `import.meta.url`.
452
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
453
+ function _isCliEntry(): boolean {
454
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) return false;
455
+ if (process.argv[1] === undefined) return false;
456
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
457
+ if (import.meta.url === argvUrl) return true;
458
+ try {
459
+ return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
460
+ } catch {
461
+ return false;
462
+ }
463
+ }
464
+ if (_isCliEntry()) process.exit(main());