@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5

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 (234) hide show
  1. package/CHANGELOG.md +79 -95
  2. package/CLAUDE-TEMPLATE.md +115 -0
  3. package/README.md +126 -245
  4. package/aesop.config.example.json +34 -26
  5. package/bin/CLAUDE.md +67 -0
  6. package/bin/cli.js +593 -36
  7. package/daemons/CLAUDE.md +36 -0
  8. package/daemons/backup-fleet.sh +92 -21
  9. package/daemons/run-watchdog.sh +95 -12
  10. package/dash/CLAUDE.md +32 -0
  11. package/dash/dash-extra.mjs +194 -21
  12. package/dash/watchdog-gui.sh +23 -6
  13. package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
  14. package/docs/CHECKPOINTING.md +100 -0
  15. package/docs/DISPATCH-MODEL.md +11 -7
  16. package/docs/FORENSICS.md +204 -0
  17. package/docs/GOVERNANCE.md +4 -2
  18. package/docs/HOOK-INSTALL.md +307 -0
  19. package/docs/HOW-THE-LOOP-WORKS.md +175 -0
  20. package/docs/MEMORY-TEMPLATE.md +61 -0
  21. package/docs/README.md +44 -0
  22. package/docs/RELEASING.md +159 -0
  23. package/docs/RELIABILITY.md +61 -0
  24. package/docs/RESTORE.md +397 -0
  25. package/docs/SCRIPTS-POLICY.md +97 -0
  26. package/docs/archive/README.md +3 -0
  27. package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
  28. package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
  29. package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
  30. package/docs/archive/spikes/tiered-cognition/README.md +27 -0
  31. package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
  32. package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
  33. package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
  34. package/docs/case-study-portfolio.md +61 -0
  35. package/docs/self-stats-data.json +11 -0
  36. package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
  37. package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
  38. package/docs/templates/PROPOSALS-LOG.example.md +64 -0
  39. package/hooks/CLAUDE.md +61 -0
  40. package/hooks/claude/force-model-policy.mjs +159 -0
  41. package/hooks/pre-push-policy.sh +753 -0
  42. package/mcp/CLAUDE.md +213 -0
  43. package/mcp/package.json +26 -0
  44. package/mcp/server.mjs +543 -0
  45. package/monitor/.signal-state.json +3 -0
  46. package/monitor/ACTIONS.log +1 -0
  47. package/monitor/BRIEF.md +24 -0
  48. package/monitor/CHARTER.md +75 -88
  49. package/monitor/CLAUDE.md +40 -0
  50. package/monitor/SIGNALS.json +54 -0
  51. package/monitor/collect-signals.mjs +613 -61
  52. package/package.json +15 -3
  53. package/scan/fleet-scan.example.mjs +292 -0
  54. package/skills/CLAUDE.md +3 -0
  55. package/skills/healthcheck/SKILL.md +44 -0
  56. package/skills/power/SKILL.md +161 -0
  57. package/state_store/CLAUDE.md +39 -0
  58. package/state_store/__init__.py +24 -0
  59. package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
  60. package/state_store/__pycache__/api.cpython-314.pyc +0 -0
  61. package/state_store/__pycache__/export.cpython-314.pyc +0 -0
  62. package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
  63. package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
  64. package/state_store/__pycache__/store.cpython-314.pyc +0 -0
  65. package/state_store/api.py +35 -0
  66. package/state_store/export.py +19 -0
  67. package/state_store/ingest.py +24 -0
  68. package/state_store/projections.py +52 -0
  69. package/state_store/store.py +102 -0
  70. package/tools/CLAUDE.md +72 -0
  71. package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
  72. package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
  73. package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
  74. package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
  75. package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
  76. package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
  77. package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
  78. package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
  79. package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
  80. package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
  81. package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
  82. package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
  83. package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
  84. package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
  85. package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
  86. package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
  87. package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
  88. package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
  89. package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
  90. package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
  91. package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
  92. package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
  93. package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
  94. package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
  95. package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
  96. package/tools/agent-forensics.sh +186 -0
  97. package/tools/alert_bridge.py +449 -0
  98. package/tools/buildlog.py +182 -0
  99. package/tools/ci_merge_wait.py +317 -0
  100. package/tools/ensure_state.py +94 -0
  101. package/tools/eod_sweep.py +239 -0
  102. package/tools/fleet_ledger.py +272 -0
  103. package/tools/fleet_prompt_extractor.py +134 -0
  104. package/tools/healthcheck.py +296 -0
  105. package/tools/heartbeat.py +131 -0
  106. package/tools/inbox_drain.py +249 -0
  107. package/tools/launch_tui.py +51 -3
  108. package/tools/lock.mjs +211 -0
  109. package/tools/metrics_gate.py +205 -0
  110. package/tools/orchestrator_status.py +99 -0
  111. package/tools/power_selftest.py +386 -0
  112. package/tools/prepublish_scan.py +84 -0
  113. package/tools/proposals.mjs +248 -0
  114. package/tools/reconstitute.sh +467 -0
  115. package/tools/rotate_logs.py +228 -0
  116. package/tools/scanner_selftest.py +303 -0
  117. package/tools/secret_scan.py +138 -31
  118. package/tools/self_stats.py +509 -0
  119. package/tools/session_usage_summary.py +198 -0
  120. package/tools/stall_check.py +172 -0
  121. package/tools/svg_to_png.mjs +50 -0
  122. package/tools/transcript_replay.py +236 -0
  123. package/tools/transcript_timeline.py +184 -0
  124. package/tools/verify_dash.py +513 -0
  125. package/tools/verify_submit_encoding.py +202 -0
  126. package/ui/CLAUDE.md +125 -0
  127. package/ui/__pycache__/agents.cpython-314.pyc +0 -0
  128. package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
  129. package/ui/__pycache__/config.cpython-314.pyc +0 -0
  130. package/ui/__pycache__/cost.cpython-314.pyc +0 -0
  131. package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
  132. package/ui/__pycache__/handler.cpython-314.pyc +0 -0
  133. package/ui/__pycache__/render.cpython-314.pyc +0 -0
  134. package/ui/__pycache__/serve.cpython-314.pyc +0 -0
  135. package/ui/__pycache__/sse.cpython-314.pyc +0 -0
  136. package/ui/agents.py +183 -0
  137. package/ui/api/__init__.py +84 -0
  138. package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
  139. package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
  140. package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
  141. package/ui/api/submit.py +97 -0
  142. package/ui/api/tracker.py +116 -0
  143. package/ui/collectors.py +735 -0
  144. package/ui/config.py +123 -0
  145. package/ui/cost.py +260 -0
  146. package/ui/csrf.py +144 -0
  147. package/ui/handler.py +676 -0
  148. package/ui/render.py +49 -0
  149. package/ui/serve.py +68 -796
  150. package/ui/sse.py +183 -0
  151. package/ui/web/.gitattributes +13 -0
  152. package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
  153. package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
  154. package/ui/web/dist/index.html +14 -0
  155. package/ui/web/index.html +13 -0
  156. package/ui/web/package-lock.json +2225 -0
  157. package/ui/web/package.json +26 -0
  158. package/ui/web/src/App.test.tsx +74 -0
  159. package/ui/web/src/App.tsx +142 -0
  160. package/ui/web/src/CONTRIBUTING-UI.md +49 -0
  161. package/ui/web/src/components/AgentRow.css +187 -0
  162. package/ui/web/src/components/AgentRow.test.tsx +209 -0
  163. package/ui/web/src/components/AgentRow.tsx +207 -0
  164. package/ui/web/src/components/AgentsPanel.css +108 -0
  165. package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
  166. package/ui/web/src/components/AgentsPanel.tsx +58 -0
  167. package/ui/web/src/components/AlertsPanel.css +88 -0
  168. package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
  169. package/ui/web/src/components/AlertsPanel.tsx +67 -0
  170. package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
  171. package/ui/web/src/components/BacklogPanel.tsx +122 -0
  172. package/ui/web/src/components/CostChart.css +110 -0
  173. package/ui/web/src/components/CostChart.test.tsx +144 -0
  174. package/ui/web/src/components/CostChart.tsx +152 -0
  175. package/ui/web/src/components/CostTable.css +93 -0
  176. package/ui/web/src/components/CostTable.test.tsx +165 -0
  177. package/ui/web/src/components/CostTable.tsx +94 -0
  178. package/ui/web/src/components/EventsFeed.css +68 -0
  179. package/ui/web/src/components/EventsFeed.test.tsx +36 -0
  180. package/ui/web/src/components/EventsFeed.tsx +31 -0
  181. package/ui/web/src/components/HealthHeader.css +137 -0
  182. package/ui/web/src/components/HealthHeader.test.tsx +278 -0
  183. package/ui/web/src/components/HealthHeader.tsx +281 -0
  184. package/ui/web/src/components/InboxForm.css +135 -0
  185. package/ui/web/src/components/InboxForm.test.tsx +208 -0
  186. package/ui/web/src/components/InboxForm.tsx +116 -0
  187. package/ui/web/src/components/MessagesTail.module.css +144 -0
  188. package/ui/web/src/components/MessagesTail.test.tsx +176 -0
  189. package/ui/web/src/components/MessagesTail.tsx +94 -0
  190. package/ui/web/src/components/ReposPanel.css +90 -0
  191. package/ui/web/src/components/ReposPanel.test.tsx +45 -0
  192. package/ui/web/src/components/ReposPanel.tsx +67 -0
  193. package/ui/web/src/components/Scorecard.css +106 -0
  194. package/ui/web/src/components/Scorecard.test.tsx +117 -0
  195. package/ui/web/src/components/Scorecard.tsx +85 -0
  196. package/ui/web/src/components/Timeline.module.css +151 -0
  197. package/ui/web/src/components/Timeline.test.tsx +215 -0
  198. package/ui/web/src/components/Timeline.tsx +99 -0
  199. package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
  200. package/ui/web/src/components/TrackerBoard.tsx +107 -0
  201. package/ui/web/src/components/TrackerCard.test.tsx +180 -0
  202. package/ui/web/src/components/TrackerCard.tsx +160 -0
  203. package/ui/web/src/components/TrackerForm.test.tsx +189 -0
  204. package/ui/web/src/components/TrackerForm.tsx +144 -0
  205. package/ui/web/src/lib/api.ts +218 -0
  206. package/ui/web/src/lib/format.test.ts +89 -0
  207. package/ui/web/src/lib/format.ts +103 -0
  208. package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
  209. package/ui/web/src/lib/sanitizeUrl.ts +38 -0
  210. package/ui/web/src/lib/types.ts +230 -0
  211. package/ui/web/src/lib/useHashRoute.test.ts +60 -0
  212. package/ui/web/src/lib/useHashRoute.ts +23 -0
  213. package/ui/web/src/lib/useSSE.ts +175 -0
  214. package/ui/web/src/main.tsx +10 -0
  215. package/ui/web/src/styles/global.css +179 -0
  216. package/ui/web/src/styles/theme.css +184 -0
  217. package/ui/web/src/styles/work.css +572 -0
  218. package/ui/web/src/test/fixtures.ts +385 -0
  219. package/ui/web/src/test/setup.ts +49 -0
  220. package/ui/web/src/views/Activity.module.css +43 -0
  221. package/ui/web/src/views/Activity.test.tsx +89 -0
  222. package/ui/web/src/views/Activity.tsx +31 -0
  223. package/ui/web/src/views/Cost.css +87 -0
  224. package/ui/web/src/views/Cost.test.tsx +142 -0
  225. package/ui/web/src/views/Cost.tsx +54 -0
  226. package/ui/web/src/views/Overview.css +51 -0
  227. package/ui/web/src/views/Overview.test.tsx +76 -0
  228. package/ui/web/src/views/Overview.tsx +46 -0
  229. package/ui/web/src/views/Work.test.tsx +82 -0
  230. package/ui/web/src/views/Work.tsx +79 -0
  231. package/ui/web/src/vite-env.d.ts +10 -0
  232. package/ui/web/tsconfig.json +22 -0
  233. package/ui/web/vite.config.ts +25 -0
  234. package/ui/web/vitest.config.ts +12 -0
@@ -0,0 +1,61 @@
1
+ # hooks/ — Installable org-policy git pre-push enforcement
2
+
3
+ **Purpose**: Ship executable git hooks that gate pushes with organization security policies (branch protection, secret scanning).
4
+
5
+ ## Hook: pre-push-policy.sh
6
+
7
+ Runs on `git push` via `.git/hooks/pre-push` symlink or copy.
8
+
9
+ **Checks & Exit Contract**:
10
+ 1. `check_branch_policy()` — blocks direct pushes to main/master; exit 1 on violation
11
+ 2. `check_secret_scan()` — runs `tools/secret_scan.py --staged`; exit 1 on failure
12
+ 3. Both trigger `log_block()` to append audit record before exit
13
+
14
+ **Audit-Ledger Contract**:
15
+ - Path: `${AESOP_ROOT:-$HOME/aesop}/state/SECURITY-AUDIT.log` (append-only, git-ignored)
16
+ - Format: JSON-lines (one record per line)
17
+ - Schema: `{"ts":"2025-07-12T14:32:01Z","repo":"aesop","event":"push_blocked","reason":"secret_scan_failure","user":"alice"}`
18
+ - All string values must be json_escaped (backslash → `\\`, quote → `\"`)
19
+
20
+ **Self-Test Convention**:
21
+ - `bash hooks/pre-push-policy.sh --test` runs the self-test suite, including:
22
+ 1. Branch policy blocks main/master
23
+ 2. Branch policy allows feature/* branches
24
+ 3. Audit log JSON format is valid
25
+ 4. JSON escaping handles special chars (quotes, backslashes)
26
+ 5. stdin handling (git pre-push pipe) doesn't crash hook
27
+ - Exit 0 = all pass; exit 1 = any fail
28
+
29
+ **Installation**:
30
+ - See `docs/HOOK-INSTALL.md` for symlink (Linux/macOS/Git Bash) and copy (Windows) methods
31
+ - Test with `bash hooks/pre-push-policy.sh --test` before org distribution
32
+
33
+ ## Hook: hooks/claude/force-model-policy.mjs
34
+
35
+ Claude Code hook enforcing subagent Haiku dispatch (cost optimization).
36
+
37
+ **Trigger**: On skill invocation or task delegation from main orchestrator thread. Examines Claude API request and enforces model constraint.
38
+
39
+ **Policy**:
40
+ - **Main orchestrator** (Fable/Opus on primary): no override (uses native model)
41
+ - **Subagent dispatch** (fleet workers): **enforce Haiku** (claude-haiku-4-5-*). Exit 1 on non-Haiku model request.
42
+ - **Specialists** (typed dispatches): Pin model to Haiku in the dispatch call; hook validates + blocks if violated.
43
+
44
+ **Logging**:
45
+ - On policy violation: log to `state/SECURITY-ALERTS.log` with timestamp, model-name, worker-id, and reason.
46
+ - No alerts on compliant requests.
47
+
48
+ **Self-Test**:
49
+ - `node hooks/claude/force-model-policy.mjs --test` validates:
50
+ 1. Haiku model allowed on subagents
51
+ 2. Non-Haiku (e.g., Opus) blocked on subagents
52
+ 3. Orchestrator not subject to policy
53
+ 4. JSON logging format is valid
54
+ - Exit 0 = all pass; exit 1 = any fail
55
+
56
+ ## Invariants
57
+
58
+ - POSIX sh compatible, CRLF-safe (no line continuations)
59
+ - Tolerate git pre-push stdin (ref list) + optional args without choking
60
+ - Fail-open only for missing optional tooling (secret_scan.py absent → allow); fail-closed for policy checks
61
+ - Use `AESOP_ROOT` env var or `$HOME/aesop` fallback; no hardcoded machine paths/usernames
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ // force-model-policy.mjs — Claude Code PreToolUse hook enforcing the
3
+ // "subagents are always Haiku" cardinal rule as versioned, executable policy.
4
+ //
5
+ // Wire it in settings.json under hooks.PreToolUse with matcher "Agent|Task"
6
+ // (see docs/HOOK-INSTALL.md). For every subagent dispatch:
7
+ //
8
+ // - model absent or non-compliant -> rewritten to the policy model ("haiku",
9
+ // or aesop.config.json cardinal_rules.subagent_model when present)
10
+ // - prompt contains [[ALLOW-NON-HAIKU]] -> deliberate escape hatch: input is
11
+ // left untouched, but the bypass is announced via permissionDecisionReason
12
+ // (visible in the transcript) AND appended as a JSON-line audit record to
13
+ // ${AESOP_ROOT:-~/aesop}/state/MODEL-POLICY-ESCAPES.log so every use is
14
+ // reviewable — prompts are untrusted text and can smuggle the marker.
15
+ // - anything else (compliant, other tools, malformed input) -> no output
16
+ //
17
+ // Output contract source: Claude Code hooks reference ("PreToolUse Decision
18
+ // Control", https://docs.anthropic.com/en/docs/claude-code/hooks) — a hook may
19
+ // emit on stdout:
20
+ // {"hookSpecificOutput":{"hookEventName":"PreToolUse",
21
+ // "permissionDecision":"allow","updatedInput":{...}}}
22
+ // where updatedInput replaces tool_input for the dispatched call. Emitting
23
+ // nothing (exit 0) means "no opinion" and the call proceeds unchanged.
24
+ //
25
+ // Reliability rule: this hook NEVER crashes the harness. Any parse or IO
26
+ // failure results in no output and exit 0 (fail-open), and nothing from the
27
+ // payload is ever logged on failure paths. stdin is raced against a 2s timer
28
+ // so a never-closing pipe cannot hang Agent/Task dispatch (fail-open, no
29
+ // rewrite). The only deliberate exception to "log nothing" is the escape-hatch
30
+ // audit record above.
31
+
32
+ import fs from 'node:fs';
33
+ import os from 'node:os';
34
+ import path from 'node:path';
35
+
36
+ const ESCAPE_HATCH = '[[ALLOW-NON-HAIKU]]';
37
+ const DEFAULT_MODEL = 'haiku';
38
+ const GOVERNED_TOOLS = new Set(['Agent', 'Task']);
39
+ const STDIN_TIMEOUT_MS = 2000;
40
+ const ESCAPE_LOG_NAME = 'MODEL-POLICY-ESCAPES.log';
41
+
42
+ /** State root, consistent with the repo's resolution pattern
43
+ * (`${AESOP_ROOT:-$HOME/aesop}/state/` — see hooks/pre-push-policy.sh). */
44
+ function stateDir() {
45
+ const root = process.env.AESOP_ROOT || path.join(os.homedir(), 'aesop');
46
+ return path.join(root, 'state');
47
+ }
48
+
49
+ /** Append a JSON-line audit record for an escape-hatch use. Best-effort:
50
+ * an unwritable log never blocks the dispatch (fail-open). */
51
+ function logEscapeUse(payload) {
52
+ try {
53
+ const dir = stateDir();
54
+ fs.mkdirSync(dir, { recursive: true });
55
+ const input = payload.tool_input || {};
56
+ const rec = {
57
+ ts: new Date().toISOString(),
58
+ event: 'model_policy_escape',
59
+ tool: payload.tool_name,
60
+ session_id: typeof payload.session_id === 'string' ? payload.session_id : null,
61
+ cwd: typeof payload.cwd === 'string' ? payload.cwd : null,
62
+ description: typeof input.description === 'string' ? input.description : null,
63
+ requested_model: typeof input.model === 'string' ? input.model : null,
64
+ prompt_head: typeof input.prompt === 'string' ? input.prompt.slice(0, 200) : null
65
+ };
66
+ fs.appendFileSync(path.join(dir, ESCAPE_LOG_NAME), JSON.stringify(rec) + '\n');
67
+ } catch {
68
+ // audit logging is best-effort; never block or crash the harness
69
+ }
70
+ }
71
+
72
+ /** Resolve the policy model: aesop.config.json cardinal_rules.subagent_model,
73
+ * looked up in $AESOP_ROOT then cwd; falls back to "haiku". */
74
+ function policyModel() {
75
+ const roots = [process.env.AESOP_ROOT, process.cwd()].filter(Boolean);
76
+ for (const root of roots) {
77
+ try {
78
+ const cfg = JSON.parse(fs.readFileSync(path.join(root, 'aesop.config.json'), 'utf8'));
79
+ const m = cfg && cfg.cardinal_rules && cfg.cardinal_rules.subagent_model;
80
+ if (typeof m === 'string' && m.trim()) return m.trim();
81
+ } catch {
82
+ // missing or unreadable config in this root — try the next one
83
+ }
84
+ }
85
+ return DEFAULT_MODEL;
86
+ }
87
+
88
+ /** Read stdin raced against a timer: a pipe that never closes must not hang
89
+ * Agent/Task dispatch. On timeout, resolve '' (fail-open, no rewrite). */
90
+ function readStdin(timeoutMs = STDIN_TIMEOUT_MS) {
91
+ return new Promise((resolve) => {
92
+ let settled = false;
93
+ const finish = (value) => {
94
+ if (settled) return;
95
+ settled = true;
96
+ clearTimeout(timer);
97
+ resolve(value);
98
+ };
99
+ const timer = setTimeout(() => finish(''), timeoutMs);
100
+ let data = '';
101
+ process.stdin.setEncoding('utf8');
102
+ process.stdin.on('data', (chunk) => { data += chunk; });
103
+ process.stdin.on('end', () => finish(data));
104
+ process.stdin.on('error', () => finish(''));
105
+ });
106
+ }
107
+
108
+ async function main() {
109
+ const raw = await readStdin();
110
+
111
+ let payload;
112
+ try {
113
+ payload = JSON.parse(raw);
114
+ } catch {
115
+ return; // malformed input: fail-open, no output, log nothing
116
+ }
117
+ if (!payload || typeof payload !== 'object') return;
118
+ if (!GOVERNED_TOOLS.has(payload.tool_name)) return;
119
+
120
+ const input = payload.tool_input;
121
+ if (!input || typeof input !== 'object') return;
122
+
123
+ const model = policyModel();
124
+
125
+ // Deliberate opt-out — but never silent: prompts are untrusted text, so the
126
+ // marker could be smuggled in from repo/file content. Every use is announced
127
+ // in the transcript (permissionDecisionReason) and appended to the audit log.
128
+ if (typeof input.prompt === 'string' && input.prompt.includes(ESCAPE_HATCH)) {
129
+ logEscapeUse(payload);
130
+ process.stdout.write(JSON.stringify({
131
+ hookSpecificOutput: {
132
+ hookEventName: 'PreToolUse',
133
+ permissionDecision: 'allow',
134
+ permissionDecisionReason:
135
+ `Model policy BYPASSED via ${ESCAPE_HATCH} escape hatch: this ` +
136
+ `${payload.tool_name} dispatch keeps model ` +
137
+ `"${typeof input.model === 'string' ? input.model : '(default)'}" ` +
138
+ `instead of policy model "${model}". Use recorded in ` +
139
+ `state/${ESCAPE_LOG_NAME}.`
140
+ }
141
+ }) + '\n');
142
+ return;
143
+ }
144
+
145
+ if (input.model === model) return; // already compliant — no opinion
146
+
147
+ process.stdout.write(JSON.stringify({
148
+ hookSpecificOutput: {
149
+ hookEventName: 'PreToolUse',
150
+ permissionDecision: 'allow',
151
+ permissionDecisionReason:
152
+ `Model policy: subagent dispatches run on "${model}" (cardinal rule; ` +
153
+ `override with ${ESCAPE_HATCH} in the prompt).`,
154
+ updatedInput: { ...input, model }
155
+ }
156
+ }) + '\n');
157
+ }
158
+
159
+ main().catch(() => { /* never crash the harness */ }).finally(() => process.exit(0));