@oneie/claude 0.7.0 → 0.9.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 (263) hide show
  1. package/agents/abm-strategist.md +89 -0
  2. package/agents/ads-meta.md +97 -0
  3. package/agents/analyst.md +173 -0
  4. package/agents/architect.md +280 -0
  5. package/agents/brand-guardian.md +88 -0
  6. package/agents/brand-strategist.md +92 -0
  7. package/agents/campaign-content.md +90 -0
  8. package/agents/campaign-email.md +88 -0
  9. package/agents/campaign-sms.md +87 -0
  10. package/agents/campaign-social.md +87 -0
  11. package/agents/cco.md +99 -0
  12. package/agents/ceo.md +106 -0
  13. package/agents/chairman.md +120 -0
  14. package/agents/cmo.md +101 -0
  15. package/agents/community-greeter.md +89 -0
  16. package/agents/community-moderator.md +92 -0
  17. package/agents/compliance.md +92 -0
  18. package/agents/copywriter.md +100 -0
  19. package/agents/creative-strategist.md +95 -0
  20. package/agents/cro.md +98 -0
  21. package/agents/cto.md +267 -0
  22. package/agents/customer-interviewer.md +93 -0
  23. package/agents/customer-researcher.md +89 -0
  24. package/agents/customer-success-manager.md +90 -0
  25. package/agents/customer-trainer.md +90 -0
  26. package/agents/cxo.md +100 -0
  27. package/agents/demand-creator.md +89 -0
  28. package/agents/demo-mover.md +83 -0
  29. package/agents/demo-specialist.md +89 -0
  30. package/agents/demo-thai-family-law.md +87 -0
  31. package/agents/designer.md +112 -0
  32. package/agents/discovery-caller.md +98 -0
  33. package/agents/doctor.md +139 -0
  34. package/agents/educate-coach.md +100 -0
  35. package/agents/elevate-tutor.md +83 -0
  36. package/agents/email-lifecycle-marketer.md +91 -0
  37. package/agents/engage-specialist.md +99 -0
  38. package/agents/events-coordinator.md +90 -0
  39. package/agents/foundation-builder.md +94 -0
  40. package/agents/funnel-architect.md +89 -0
  41. package/agents/gift-creator.md +100 -0
  42. package/agents/google-ads.md +103 -0
  43. package/agents/guide.md +292 -0
  44. package/agents/helpdesk-dispatcher.md +88 -0
  45. package/agents/hook-specialist.md +91 -0
  46. package/agents/identify-optimizer.md +101 -0
  47. package/agents/implementer.md +305 -0
  48. package/agents/incident-commander.md +120 -0
  49. package/agents/insights-lead.md +93 -0
  50. package/agents/journey-runner.md +113 -0
  51. package/agents/linkedin-ads.md +123 -0
  52. package/agents/live-sales-chat.md +90 -0
  53. package/agents/market-researcher.md +88 -0
  54. package/agents/media-buyer.md +96 -0
  55. package/agents/movers-customer-researcher.md +95 -0
  56. package/agents/movers-foundation-builder.md +96 -0
  57. package/agents/movers-market-researcher.md +97 -0
  58. package/agents/movers-pricing-strategist.md +171 -0
  59. package/agents/nurture-architect.md +99 -0
  60. package/agents/offer-architect.md +92 -0
  61. package/agents/onboarder.md +222 -0
  62. package/agents/onboarding-specialist.md +92 -0
  63. package/agents/operations-dashboard.md +98 -0
  64. package/agents/perf-engineer.md +348 -0
  65. package/agents/playbook-writer.md +71 -0
  66. package/agents/plg-strategist.md +91 -0
  67. package/agents/positioning-architect.md +88 -0
  68. package/agents/press-officer.md +89 -0
  69. package/agents/pricing-strategist.md +94 -0
  70. package/agents/privacy-officer.md +92 -0
  71. package/agents/referral-manager.md +91 -0
  72. package/agents/refine-analyst.md +102 -0
  73. package/agents/release-manager.md +261 -0
  74. package/agents/renewals-upsell-rep.md +90 -0
  75. package/agents/review-engineer.md +327 -0
  76. package/agents/rewards-steward.md +82 -0
  77. package/agents/sales-call-coach.md +94 -0
  78. package/agents/sales-closer.md +98 -0
  79. package/agents/security-auditor.md +313 -0
  80. package/agents/sell-closer.md +101 -0
  81. package/agents/share-amplifier.md +99 -0
  82. package/agents/social-media-manager.md +92 -0
  83. package/agents/storyteller.md +250 -0
  84. package/agents/strategist.md +83 -0
  85. package/agents/strategy-aligner.md +94 -0
  86. package/agents/support-agent.md +90 -0
  87. package/agents/tagger.md +245 -0
  88. package/agents/tech-writer.md +216 -0
  89. package/agents/test-engineer.md +243 -0
  90. package/agents/tiktok-ads.md +105 -0
  91. package/agents/tracking-engineer.md +92 -0
  92. package/agents/upsell-strategist.md +101 -0
  93. package/agents/voice.md +85 -0
  94. package/agents/w4-tools.md +153 -0
  95. package/agents/w4-verify.md +20 -138
  96. package/agents/workflow-optimiser.md +84 -0
  97. package/commands/close.md +814 -160
  98. package/commands/create.md +2 -2
  99. package/commands/deploy.md +554 -175
  100. package/commands/do.md +119 -109
  101. package/commands/fast.md +67 -0
  102. package/commands/improve.md +2 -2
  103. package/commands/one.md +418 -0
  104. package/commands/see.md +1 -1
  105. package/commands/sync.md +1 -1
  106. package/commands/tasks.md +222 -0
  107. package/commands/vespio.md +110 -0
  108. package/commands/vespio.remote.md +227 -0
  109. package/hooks/hooks.json +25 -79
  110. package/hooks/lib/governor-escape-match.sh +88 -0
  111. package/hooks/lib/hook.sh +4 -13
  112. package/hooks/lib/signal.sh +9 -2
  113. package/hooks/scripts/branch-pin.sh +6 -0
  114. package/hooks/scripts/config-protect.sh +6 -0
  115. package/hooks/scripts/do-outcome-gate.sh +19 -2
  116. package/hooks/scripts/git-add-guard.sh +43 -2
  117. package/hooks/scripts/governor-escape.sh +96 -0
  118. package/hooks/scripts/load-guard.sh +127 -0
  119. package/hooks/scripts/post-edit-check.sh +36 -29
  120. package/hooks/scripts/session-start.sh +34 -80
  121. package/hooks/scripts/task-complete-verify.sh +46 -40
  122. package/package.json +1 -1
  123. package/rules/documentation.md +9 -0
  124. package/scripts/ad-copy-lint.sh +656 -0
  125. package/scripts/agentverse-audit.sh +256 -0
  126. package/scripts/asi-walk.sh +435 -0
  127. package/scripts/astro-build-cached.sh +143 -0
  128. package/scripts/blocks-manifest-cached.sh +100 -0
  129. package/scripts/blocks-manifest.mjs +497 -0
  130. package/scripts/blocks-ratchet.sh +116 -0
  131. package/scripts/blocks-render-probe.mjs +529 -0
  132. package/scripts/blocks-usage.mjs +334 -0
  133. package/scripts/chat-context-check.sh +89 -0
  134. package/scripts/close-metrics.sh +558 -0
  135. package/scripts/close-owner.sh +268 -0
  136. package/scripts/db-sync-lock-check.sh +116 -0
  137. package/scripts/deploy-dev.sh +35 -0
  138. package/scripts/deploy-emit.sh +298 -0
  139. package/scripts/deploy-gate-check.sh +128 -0
  140. package/scripts/deploy-ready.sh +78 -0
  141. package/scripts/deploy-record.sh +605 -0
  142. package/scripts/deploy.sh +1273 -0
  143. package/scripts/do-auto.sh +598 -48
  144. package/scripts/do-brief.sh +113 -0
  145. package/scripts/do-close.sh +1137 -0
  146. package/scripts/do-commit.sh +75 -0
  147. package/scripts/do-consumer-sweep.sh +18 -1
  148. package/scripts/do-cycle-shape-check.sh +160 -0
  149. package/scripts/do-decide.sh +476 -0
  150. package/scripts/do-derive-check.sh +436 -0
  151. package/scripts/do-fleet.sh +106 -28
  152. package/scripts/do-folder.sh +10 -1
  153. package/scripts/do-next.sh +106 -0
  154. package/scripts/do-orchestrate.sh +17 -5
  155. package/scripts/do-plan-json.mjs +201 -0
  156. package/scripts/do-plan-json.sh +8 -0
  157. package/scripts/do-preflight.sh +117 -0
  158. package/scripts/do-project.sh +157 -0
  159. package/scripts/do-prove-selftest.sh +108 -0
  160. package/scripts/do-prove.sh +295 -23
  161. package/scripts/do-rank.py +31 -1
  162. package/scripts/do-recon-cache.sh +7 -1
  163. package/scripts/do-recon-pack.sh +196 -0
  164. package/scripts/do-reconcile.sh +121 -4
  165. package/scripts/do-signal.sh +280 -23
  166. package/scripts/do-smoke.sh +18 -1
  167. package/scripts/do-test-gate.sh +80 -0
  168. package/scripts/do-tick.sh +102 -0
  169. package/scripts/do-tier.sh +6 -0
  170. package/scripts/do-triage.sh +182 -0
  171. package/scripts/do-ui-gate.sh +1 -1
  172. package/scripts/do-w4-gates.sh +451 -0
  173. package/scripts/do-walk.sh +12 -1
  174. package/scripts/env-sync.sh +173 -0
  175. package/scripts/factory-brief-check.sh +330 -0
  176. package/scripts/factory-check.sh +68 -14
  177. package/scripts/factory-close-check.sh +257 -0
  178. package/scripts/factory-emit.sh +211 -0
  179. package/scripts/factory-executor-check.mjs +353 -0
  180. package/scripts/factory-peak.sh +301 -0
  181. package/scripts/factory-repo.sh +118 -3
  182. package/scripts/factory-review-check.mjs +61 -0
  183. package/scripts/factory-ship.sh +61 -0
  184. package/scripts/factory-tasks-check.sh +18 -1
  185. package/scripts/factory-turn.sh +326 -0
  186. package/scripts/factory-walk.sh +396 -0
  187. package/scripts/factory-width.sh +57 -0
  188. package/scripts/fade-toxic.sh +4 -3
  189. package/scripts/fixtures/factory-brief-real.md +44 -0
  190. package/scripts/fixtures/triage-dupe.md +5 -0
  191. package/scripts/fleet-manifest.mjs +108 -0
  192. package/scripts/fleet-status.sh +110 -0
  193. package/scripts/full-suite-paths-check.sh +144 -0
  194. package/scripts/gate-reaper-check.sh +98 -0
  195. package/scripts/gate-reaper.sh +125 -0
  196. package/scripts/gate-run.sh +73 -0
  197. package/scripts/gc-content-check.sh +140 -0
  198. package/scripts/gen-dev-config.py +20 -0
  199. package/scripts/govern-bound-check.sh +60 -0
  200. package/scripts/govern-claims-check.sh +233 -0
  201. package/scripts/govern-mem-check.sh +290 -0
  202. package/scripts/governor-doors-check.sh +362 -0
  203. package/scripts/governor-escape-check.sh +171 -0
  204. package/scripts/health.sh +413 -0
  205. package/scripts/id-inventory.mjs +418 -0
  206. package/scripts/land.sh +551 -0
  207. package/scripts/lib/gc-finished.sh +77 -0
  208. package/scripts/lib/govern.sh +361 -0
  209. package/scripts/lib/govern.ts +756 -0
  210. package/scripts/lighthouse-run.sh +187 -0
  211. package/scripts/livekit-live-check.sh +61 -0
  212. package/scripts/livekit-ratchet.sh +46 -0
  213. package/scripts/load-guard-check.sh +49 -0
  214. package/scripts/machine-check.sh +102 -0
  215. package/scripts/machine-watch.sh +177 -0
  216. package/scripts/one-agents.mjs +415 -0
  217. package/scripts/one-resume.sh +78 -0
  218. package/scripts/orphan-baseline.json +182 -0
  219. package/scripts/orphan-modules.mjs +179 -0
  220. package/scripts/pr-body.sh +335 -0
  221. package/scripts/preview-fd-check.sh +289 -0
  222. package/scripts/promise-manifest.mjs +24 -1
  223. package/scripts/release.sh +322 -0
  224. package/scripts/roles-check.sh +946 -0
  225. package/scripts/sdk-build-cached.sh +64 -0
  226. package/scripts/signal-watch.sh +241 -0
  227. package/scripts/skills-publish.sh +94 -0
  228. package/scripts/speed-cache-check.sh +173 -0
  229. package/scripts/speed-check.mjs +907 -0
  230. package/scripts/speed-parity-check.sh +648 -0
  231. package/scripts/speed-waterfall-check.sh +355 -0
  232. package/scripts/substrate-env-parity.mjs +156 -0
  233. package/scripts/tasks-claim-race.mjs +108 -0
  234. package/scripts/tasks-loop.sh +185 -0
  235. package/scripts/test-cached.sh +255 -0
  236. package/scripts/test-full.sh +87 -0
  237. package/scripts/test-honesty.mjs +137 -0
  238. package/scripts/test-lanes.sh +166 -0
  239. package/scripts/test-speed.sh +94 -0
  240. package/scripts/triage-shape-check.sh +149 -0
  241. package/scripts/tsc-cached.sh +179 -0
  242. package/scripts/typedb-flake-check.sh +197 -0
  243. package/scripts/urls-lint.sh +15 -0
  244. package/scripts/verify-fast.sh +445 -0
  245. package/scripts/vespio-sync.sh +149 -0
  246. package/scripts/wf-check.mjs +104 -1
  247. package/scripts/worktree-preview.sh +879 -0
  248. package/scripts/worktree-up.sh +208 -0
  249. package/skills/livekit-agents/SKILL.md +285 -0
  250. package/skills/livekit-agents/references/freshness-rules.md +168 -0
  251. package/skills/shadcn/SKILL.md +1 -1
  252. package/skills/signal/SKILL.md +0 -1
  253. package/skills/voice/SKILL.md +94 -6
  254. package/skills/voice/corpus-check.sh +87 -0
  255. package/hooks/scripts/compact-hint.sh +0 -35
  256. package/hooks/scripts/gate-guard.sh +0 -83
  257. package/hooks/scripts/read-tracker.sh +0 -26
  258. package/hooks/scripts/session-end-verify.sh +0 -51
  259. package/hooks/scripts/stop-reflect.sh +0 -140
  260. package/hooks/scripts/sync-priority-todo.sh +0 -57
  261. package/hooks/scripts/sync-todo-docs.sh +0 -46
  262. package/hooks/scripts/tool-signal.sh +0 -48
  263. package/scripts/do-tasks-bridge.py +0 -366
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bash
2
+ # governor-escape-check.sh — proof for hook:governor-escape, both directions.
3
+ #
4
+ # bash .claude/scripts/governor-escape-check.sh
5
+ #
6
+ # Three parts, and the third is the one that makes the first two mean anything:
7
+ #
8
+ # A the matcher's case table — every escape BLOCKS, and every innocent
9
+ # command that merely NAMES an escape is ALLOWED. A guard with false
10
+ # positives gets switched off, which is worse than no guard at all.
11
+ # B the REAL hook, driven end to end with a real payload on stdin. A
12
+ # matcher-only table stays green while the hook is broken (a wrong jq
13
+ # path, a missing tool_name guard, malformed deny JSON), so at least one
14
+ # BLOCK and one allow go through the actual file.
15
+ # C the RED PROOF. Gut the matcher and assert A goes red; neuter the hook
16
+ # (ECC_DISABLED_HOOKS) and assert B goes red. A checker that stays green
17
+ # against a gutted subject proves nothing — the same argument
18
+ # govern-claims-check.sh makes when it stubs _gv_alive.
19
+ #
20
+ # Exits non-zero on any failure. Touches nothing outside a temp dir.
21
+ # Test-case strings deliberately avoid absolute monorepo paths: this script is
22
+ # classified `portable` in factory-repo.sh's manifest, and --check-portability
23
+ # greps the file CONTENTS for them.
24
+
25
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
26
+ MATCH="${GOV_MATCH_FILE:-$ROOT/.claude/hooks/lib/governor-escape-match.sh}"
27
+ HOOK="$ROOT/.claude/hooks/governor-escape.sh"
28
+ # shellcheck source=/dev/null
29
+ . "$MATCH"
30
+
31
+ # A stray ECC_DISABLED_HOOKS in the operator's environment would neuter part B
32
+ # and read as a red. Clear it for the real run; part C sets it explicitly on the
33
+ # child it wants neutered.
34
+ [ -z "${GOV_CHECK_INNER:-}" ] && unset ECC_DISABLED_HOOKS
35
+
36
+ bad=0
37
+ ok() { printf ' ok %s\n' "$*"; }
38
+ nope() { printf ' FAIL %s\n' "$*"; bad=$((bad + 1)); }
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # A — the matcher
42
+ # ---------------------------------------------------------------------------
43
+ m_check() { # m_check <BLOCK|allow> <label> <command>
44
+ local want="$1" label="$2" cmd="$3" kind
45
+ kind="$(gov_escape_kind "$cmd")"
46
+ local got="allow"; [ -n "$kind" ] && got="BLOCK"
47
+ if [ "$got" = "$want" ]; then ok "$want $label${kind:+ [$kind]}"
48
+ else nope "$want expected, got $got — $label"; fi
49
+ }
50
+
51
+ part_a() {
52
+ echo "== A. matcher — escapes must BLOCK"
53
+ m_check BLOCK 'bun run test:raw' 'cd web && bun run test:raw'
54
+ m_check BLOCK 'bun run verify:raw' 'bun run verify:raw'
55
+ m_check BLOCK 'bunx vitest run <file>' 'bunx vitest run tests/unit/foo.test.ts'
56
+ m_check BLOCK 'npx vitest' 'npx vitest'
57
+ m_check BLOCK 'bare vitest run' 'vitest run'
58
+ m_check BLOCK 'path-invoked vitest' 'node_modules/.bin/vitest run'
59
+ m_check BLOCK 'vitest after an operator' 'echo starting; bunx vitest run'
60
+ m_check BLOCK 'GOVERN_DISABLE in front' 'GOVERN_DISABLE=1 bun run verify'
61
+ m_check BLOCK 'CI=1 in front' 'CI=1 bunx vitest run'
62
+ m_check BLOCK 'env bypass beats gate-run' 'GOVERN_DISABLE=1 bash .claude/scripts/gate-run.sh verify -- bun run verify'
63
+
64
+ echo "== A. matcher — merely NAMING an escape must be allowed"
65
+ m_check allow 'the governed form' 'bash .claude/scripts/gate-run.sh test -- bunx vitest run tests/unit/foo.test.ts'
66
+ m_check allow 'governed package script' 'cd web && bun run verify'
67
+ m_check allow 'the dev lane' 'cd web && bun run verify:fast'
68
+ m_check allow 'a commit message' "git commit -m 'fix: bun run test:raw is the escape hatch'"
69
+ m_check allow 'grepping for it' 'grep -rn "verify:raw" web/package.json'
70
+ m_check allow 'reading package.json' "sed -n '47,48p' web/package.json"
71
+ m_check allow 'reading the vitest config' 'cat web/vitest.config.ts'
72
+ m_check allow 'echoing the override' 'echo "GOVERN_DISABLE=1 bun run verify is the override"'
73
+ m_check allow 'env bypass, but no gate' 'GOVERN_DISABLE=1 ls -la'
74
+ m_check allow 'CI=1 on a non-gate' 'CI=1 bun run build'
75
+ m_check allow 'reading the hook itself' 'cat .claude/hooks/governor-escape.sh'
76
+ m_check allow 'tsc is out of scope' 'bunx tsc --noEmit'
77
+ # THE FALSE POSITIVE THAT ALREADY BIT SOMEONE. hook:load-guard REFUSES this
78
+ # command: the `|` INSIDE the quoted regex is a shell-operator position as
79
+ # far as a position-anchored pattern is concerned, and what follows it is
80
+ # `tsc --noEmit'` -- which load-guard.sh:57 matches as a bare alternative.
81
+ # Reading `ps` is not running a gate. This guard cannot reproduce it: the
82
+ # vitest rule needs the literal word `vitest` at that position, and tsc is
83
+ # deliberately out of scope -- so the scope decision is load-bearing, not
84
+ # incidental.
85
+ m_check allow 'ps piped into grep' "ps -Ao rss,command | grep -E 'vitest|tsc --noEmit'"
86
+ m_check allow 'ps grep, vitest first' "ps -Ao command | grep -E 'vitest|astro check'"
87
+ m_check allow 'pgrep for a runner' 'pgrep -fl vitest'
88
+ # The heredoc case. `grep -E` anchors ^ per LINE, so a doc BODY quoting an
89
+ # escape would match without _gov_strip_heredocs. Writing about the escape is
90
+ # the most innocent act there is.
91
+ m_check allow 'heredoc body quoting it' "$(printf '%s\n' \
92
+ "cat > notes.md <<'EOF'" \
93
+ 'bun run test:raw is the ungoverned lane' \
94
+ 'GOVERN_DISABLE=1 bun run verify bypasses the cap' \
95
+ 'EOF')"
96
+ }
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # B — the real hook, end to end
100
+ # ---------------------------------------------------------------------------
101
+ h_deny() { # h_deny <command> -> prints "deny" or "allow"
102
+ local payload out
103
+ payload="$(jq -nc --arg c "$1" --arg d "$ROOT" \
104
+ '{tool_name:"Bash",cwd:$d,tool_input:{command:$c}}')"
105
+ out="$(printf '%s' "$payload" | CLAUDE_PROJECT_DIR="$ROOT" bash "$HOOK" 2>&1)"
106
+ case "$out" in *'"permissionDecision":"deny"'*) printf 'deny' ;; *) printf 'allow' ;; esac
107
+ }
108
+
109
+ part_b() {
110
+ echo "== B. the hook itself, driven with a real payload"
111
+ local got
112
+ got="$(h_deny 'cd web && bun run test:raw')"
113
+ [ "$got" = "deny" ] && ok "hook DENIES bun run test:raw" \
114
+ || nope "hook did not deny bun run test:raw (got $got)"
115
+ got="$(h_deny 'GOVERN_DISABLE=1 bun run verify')"
116
+ [ "$got" = "deny" ] && ok "hook DENIES GOVERN_DISABLE=1 bun run verify" \
117
+ || nope "hook did not deny the env bypass (got $got)"
118
+ got="$(h_deny "git commit -m 'fix: bun run test:raw is the escape hatch'")"
119
+ [ "$got" = "allow" ] && ok "hook ALLOWS a commit message that quotes it" \
120
+ || nope "hook denied an innocent commit message — FALSE POSITIVE"
121
+ got="$(h_deny 'bash .claude/scripts/gate-run.sh test -- bunx vitest run tests/unit/foo.test.ts')"
122
+ [ "$got" = "allow" ] && ok "hook ALLOWS the governed form" \
123
+ || nope "hook denied the governed form (got $got)"
124
+
125
+ # The documented human override must actually work, or nobody can get past it.
126
+ local payload out
127
+ payload="$(jq -nc --arg c 'bun run test:raw' --arg d "$ROOT" \
128
+ '{tool_name:"Bash",cwd:$d,tool_input:{command:$c}}')"
129
+ out="$(printf '%s' "$payload" | CLAUDE_PROJECT_DIR="$ROOT" \
130
+ ECC_DISABLED_HOOKS=hook:governor-escape bash "$HOOK" 2>&1)"
131
+ case "$out" in
132
+ *'"permissionDecision":"deny"'*) nope "ECC_DISABLED_HOOKS=hook:governor-escape did NOT disable it" ;;
133
+ *) ok "ECC_DISABLED_HOOKS=hook:governor-escape releases it (the human override)" ;;
134
+ esac
135
+ }
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # C — the red proof
139
+ # ---------------------------------------------------------------------------
140
+ part_c() {
141
+ echo "== C. red proof — gut the subject, the checks must go RED"
142
+ local tmp; tmp="$(mktemp -d)"
143
+ trap 'rm -rf "$tmp"' EXIT
144
+
145
+ # C1 — a matcher that never matches. If part A still passes, part A is theatre.
146
+ { cat "$MATCH"; printf '\ngov_escape_kind() { return 1; }\n'; } > "$tmp/stub.sh"
147
+ if GOV_CHECK_INNER=matcher GOV_MATCH_FILE="$tmp/stub.sh" \
148
+ bash "${BASH_SOURCE[0]}" >"$tmp/a.log" 2>&1; then
149
+ nope "matcher gutted to always-allow and part A STILL PASSED"
150
+ else
151
+ ok "matcher gutted -> part A goes red ($(grep -c '^ FAIL' "$tmp/a.log") failures)"
152
+ fi
153
+
154
+ # C2 — a hook that cannot deny. If part B still passes, part B is theatre.
155
+ if GOV_CHECK_INNER=hook ECC_DISABLED_HOOKS=hook:governor-escape \
156
+ bash "${BASH_SOURCE[0]}" >"$tmp/b.log" 2>&1; then
157
+ nope "hook disabled and part B STILL PASSED"
158
+ else
159
+ ok "hook disabled -> part B goes red ($(grep -c '^ FAIL' "$tmp/b.log") failures)"
160
+ fi
161
+ }
162
+
163
+ case "${GOV_CHECK_INNER:-}" in
164
+ matcher) part_a ;;
165
+ hook) part_b ;;
166
+ *) part_a; echo; part_b; echo; part_c ;;
167
+ esac
168
+
169
+ echo
170
+ if [ "$bad" -eq 0 ]; then echo "RESULT: PASS — the guard bites, and it does not bite the innocent"
171
+ else echo "RESULT: RED — $bad failure(s)"; exit 1; fi
@@ -0,0 +1,413 @@
1
+ #!/usr/bin/env bash
2
+ # health.sh — is this box healthy enough to work on, and is the estate up?
3
+ #
4
+ # manifest: portable
5
+ #
6
+ # WHY (measured 2026-09-05). Answering "why is localhost slow" took a dozen
7
+ # ad-hoc commands: uptime, vm_stat, sysctl vm.swapusage, three greps of ps, a
8
+ # gate-reaper dry run, an ls of the governor dir, and a curl per surface. Every
9
+ # one of those numbers already had a home; nothing put them on one page. The
10
+ # session that prompted this found 19 orphaned workerd processes holding the box
11
+ # at 20.5G of 21.5G swap — visible in ps all along, in a shape nobody was
12
+ # looking at.
13
+ #
14
+ # It reuses rather than restates: machine-check.sh owns the process/memory read,
15
+ # gate-reaper.sh --dry-run owns the orphan shape, govern.sh owns the arithmetic.
16
+ # This adds the VERDICT and the service probes, and nothing else.
17
+ #
18
+ # Usage: health.sh [--box] [--services] [--json] [--watch N] [--self-test]
19
+ # (no flags) both halves, human-readable
20
+ # --box the machine only --services the estate only
21
+ # --json machine-readable, both halves, no colour
22
+ # --watch N re-run every N seconds until interrupted
23
+ # --fix reclaim what nothing is coming back for, then re-read
24
+ # --dry-run with --fix, name every kill instead of making it
25
+ # --signal emit the box verdict into the world, tagged `engineering`, so
26
+ # the doctor sees the Mac in the same graph as the estate
27
+ # --self-test prove the verdict thresholds bite (red proof included)
28
+ #
29
+ # WHAT --fix WILL AND WILL NOT TOUCH. It reclaims three shapes, each one dead by
30
+ # a test that cannot be true of live work:
31
+ # 1. orphaned gates and workerd — delegated whole to gate-reaper.sh, whose own
32
+ # checker proves it spares a live parent and a fresh reparent
33
+ # 2. language servers rooted in a WORKTREE — the editor should not have opened
34
+ # a project there at all (.vscode/settings.json excludes it); the real
35
+ # tsserver for the main tree is never a candidate
36
+ # 3. dev servers in a worktree with NO live claude session — the session that
37
+ # started it is gone, so nobody is watching the port
38
+ # It will NEVER kill a claude session, anything holding a governor slot, or
39
+ # anything running in the main tree. Those are judgement calls a script does not
40
+ # get to make: a session classifier misfired twice on 2026-09-05, which is the
41
+ # reason this list is shapes-with-a-dead-owner and not "looks idle".
42
+ set -uo pipefail
43
+
44
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
45
+ # shellcheck source=lib/govern.sh
46
+ . "$HERE/lib/govern.sh" 2>/dev/null || true
47
+
48
+ WANT_BOX=1; WANT_SVC=1; AS_JSON=0; WATCH=0; SELFTEST=0; FIX=0; DRY=0; SIGNAL=0
49
+ while [ $# -gt 0 ]; do
50
+ case "$1" in
51
+ --box) WANT_SVC=0 ;;
52
+ --services) WANT_BOX=0 ;;
53
+ --json) AS_JSON=1 ;;
54
+ --watch) WATCH="${2:-10}"; shift ;;
55
+ --fix) FIX=1 ;;
56
+ --dry-run) DRY=1 ;;
57
+ --signal) SIGNAL=1 ;;
58
+ --self-test) SELFTEST=1 ;;
59
+ -h|--help) sed -n '2,24p' "${BASH_SOURCE[0]}"; exit 0 ;;
60
+ *) echo "health: unknown flag $1" >&2; exit 2 ;;
61
+ esac
62
+ shift
63
+ done
64
+
65
+ C_OK=$'\033[32m'; C_WARN=$'\033[33m'; C_BAD=$'\033[31m'; C_DIM=$'\033[2m'; C_OFF=$'\033[0m'
66
+ [ -t 1 ] || { C_OK=""; C_WARN=""; C_BAD=""; C_DIM=""; C_OFF=""; }
67
+ [ "$AS_JSON" = "1" ] && { C_OK=""; C_WARN=""; C_BAD=""; C_DIM=""; C_OFF=""; }
68
+
69
+ # ── the box ───────────────────────────────────────────────────────────────────
70
+ # Every number here has exactly one source. load and swap come from the kernel;
71
+ # headroom comes from govern.sh so the dashboard can never disagree with the
72
+ # governor that actually admits gates.
73
+ cores() { sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 1; }
74
+ load1() { uptime | sed -E 's/.*load averages?: *([0-9.]+).*/\1/'; }
75
+ swap_used_mb() { sysctl -n vm.swapusage 2>/dev/null | sed -E 's/.*used = ([0-9.]+)M.*/\1/' | cut -d. -f1; }
76
+ swap_free_mb() { sysctl -n vm.swapusage 2>/dev/null | sed -E 's/.*free = ([0-9.]+)M.*/\1/' | cut -d. -f1; }
77
+ orphan_count() {
78
+ [ -x "$HERE/gate-reaper.sh" ] || { echo 0; return; }
79
+ bash "$HERE/gate-reaper.sh" --once --dry-run 2>/dev/null | grep -c 'WOULD reap' || true
80
+ }
81
+ slots_held() { ls -d "${TMPDIR:-/tmp}/one-govern"/lock-slot-* 2>/dev/null | wc -l | tr -d ' '; }
82
+ sessions() { ps -Ao command= 2>/dev/null | grep -cE '^claude( --|$)' || true; }
83
+
84
+ # VERDICT — the whole point of the file. Thresholds are the ones the harness
85
+ # already uses: machine-check calls load healthy under `cores`, and govern.sh
86
+ # refuses to fund a second cycle below one cycle's price. A dashboard that
87
+ # invents its own thresholds would drift from the governor within a week.
88
+ box_verdict() {
89
+ local c load_i head orph swapfree
90
+ c=$(cores); load_i=$(printf '%.0f' "$(load1)"); head=$(gate_headroom 2>/dev/null || echo 99)
91
+ orph=$(orphan_count); swapfree=$(swap_free_mb)
92
+ if [ "$orph" -gt 0 ]; then echo "DEGRADED|${orph} abandoned gate(s)/orphan(s) burning the box — gate-reaper.sh --once"; return; fi
93
+ if [ "$load_i" -gt $(( c * 2 )) ]; then echo "UNHEALTHY|load ${load_i} is over 2x the ${c} cores — gates are queueing, not running"; return; fi
94
+ if [ "${swapfree:-9999}" -lt 512 ]; then echo "UNHEALTHY|swap has ${swapfree}MB free — the box is paging, every gate runs long"; return; fi
95
+ if [ "$load_i" -gt "$c" ]; then echo "DEGRADED|load ${load_i} is over ${c} cores — work is contending"; return; fi
96
+ if [ "${head:-99}" -lt 2 ]; then echo "DEGRADED|memory funds only ${head} concurrent gate — close idle sessions to widen"; return; fi
97
+ echo "HEALTHY|load ${load_i} on ${c} cores, memory funds ${head} gates"
98
+ }
99
+
100
+ print_box() {
101
+ local v state why
102
+ v=$(box_verdict); state="${v%%|*}"; why="${v#*|}"
103
+ local col="$C_OK"; [ "$state" = "DEGRADED" ] && col="$C_WARN"; [ "$state" = "UNHEALTHY" ] && col="$C_BAD"
104
+ printf '%s\n' "── box ─────────────────────────────────────────"
105
+ printf ' %s%s%s %s\n' "$col" "$state" "$C_OFF" "$why"
106
+ printf ' %sload %s · %s cores · swap %sM used / %sM free · gates funded %s · slots held %s · sessions %s · orphans %s%s\n' \
107
+ "$C_DIM" "$(load1)" "$(cores)" "$(swap_used_mb)" "$(swap_free_mb)" \
108
+ "$(gate_headroom 2>/dev/null || echo '?')" "$(slots_held)" "$(sessions)" "$(orphan_count)" "$C_OFF"
109
+ }
110
+
111
+ # ── the estate ────────────────────────────────────────────────────────────────
112
+ # The URL/assert pairs are deploy.sh's, deliberately. If a probe here disagrees
113
+ # with the one that gates a deploy, this file is the one that is wrong.
114
+ SURFACES="one.ie|https://one.ie/api/health|\"status\":\"ok\"
115
+ api|https://api.one.ie/health|
116
+ channels|https://channels.one.ie/health|
117
+ pay|https://pay.one.ie/status|\"ok\""
118
+
119
+ probe() { # name url assert -> "name|state|code|ms|note"
120
+ local name="$1" url="$2" assert="$3" body code ms t0 t1
121
+ t0=$(date +%s%N 2>/dev/null || echo 0)
122
+ body=$(curl -sL --max-time 8 -w $'\n%{http_code}' "$url" 2>/dev/null)
123
+ t1=$(date +%s%N 2>/dev/null || echo 0)
124
+ ms=$(( (t1 - t0) / 1000000 )); [ "$ms" -lt 0 ] && ms=0
125
+ code="${body##*$'\n'}"; body="${body%$'\n'*}"
126
+ if [ "$code" != "200" ]; then echo "${name}|DOWN|${code:-000}|${ms}|no 200"; return; fi
127
+ if [ -n "$assert" ] && ! printf '%s' "$body" | grep -qF "$assert"; then
128
+ echo "${name}|DEGRADED|${code}|${ms}|200 but missing ${assert}"; return
129
+ fi
130
+ echo "${name}|UP|${code}|${ms}|"
131
+ }
132
+
133
+ print_services() {
134
+ printf '%s\n' "── estate ──────────────────────────────────────"
135
+ local line name url assert r state code ms note col
136
+ while IFS='|' read -r name url assert; do
137
+ [ -n "$name" ] || continue
138
+ r=$(probe "$name" "$url" "$assert")
139
+ IFS='|' read -r _ state code ms note <<<"$r"
140
+ col="$C_OK"; [ "$state" = "DEGRADED" ] && col="$C_WARN"; [ "$state" = "DOWN" ] && col="$C_BAD"
141
+ printf ' %s%-9s%s %-4s %5sms %s%s%s\n' "$col" "$state" "$C_OFF" "$code" "$ms" "$C_DIM" "${name} ${note}" "$C_OFF"
142
+ done <<<"$SURFACES"
143
+ printf ' %s%-9s%s — — %ssync · backup — cron-only, no health endpoint exists%s\n' \
144
+ "$C_DIM" "UNKNOWN" "$C_OFF" "$C_DIM" "$C_OFF"
145
+ }
146
+
147
+ print_json() {
148
+ local v state why
149
+ v=$(box_verdict); state="${v%%|*}"; why="${v#*|}"
150
+ printf '{"box":{"verdict":"%s","why":"%s","load":%s,"cores":%s,"swap_used_mb":%s,"swap_free_mb":%s,"gates_funded":%s,"slots_held":%s,"sessions":%s,"orphans":%s},"services":[' \
151
+ "$state" "$why" "$(load1)" "$(cores)" "$(swap_used_mb)" "$(swap_free_mb)" \
152
+ "$(gate_headroom 2>/dev/null || echo 0)" "$(slots_held)" "$(sessions)" "$(orphan_count)"
153
+ local first=1 name url assert r s code ms note
154
+ while IFS='|' read -r name url assert; do
155
+ [ -n "$name" ] || continue
156
+ r=$(probe "$name" "$url" "$assert"); IFS='|' read -r _ s code ms note <<<"$r"
157
+ [ $first = 1 ] || printf ','; first=0
158
+ printf '{"name":"%s","state":"%s","code":"%s","ms":%s}' "$name" "$s" "$code" "$ms"
159
+ done <<<"$SURFACES"
160
+ printf ']}\n'
161
+ }
162
+
163
+ # ── signal ────────────────────────────────────────────────────────────────────
164
+ # The estate's half (health:diagnose) already writes its verdict back as weighted
165
+ # paths. This is the Mac's half of the same graph: same tag vocabulary, so a
166
+ # symptom here ranks beside a symptom there instead of living in a terminal
167
+ # nobody reads. Tagged `engineering` because that is the doctor's bare stake —
168
+ # a namespaced tag would match zero subscribers (root CLAUDE.md § the stake is
169
+ # written as BARE words).
170
+ emit_verdict() {
171
+ local lib="$HERE/../hooks/lib/signal.sh"
172
+ [ -f "$lib" ] || return 0
173
+ local v state why
174
+ v=$(box_verdict); state="${v%%|*}"; why="${v#*|}"
175
+ # shellcheck source=../hooks/lib/signal.sh
176
+ . "$lib" 2>/dev/null || return 0
177
+ command -v emit_world >/dev/null 2>&1 || return 0
178
+ emit_world "health" "$([ "$state" = "HEALTHY" ] && echo fyi || echo attention)" \
179
+ "engineering,health,box" "box is ${state}: ${why}" \
180
+ "verdict=${state}" "load=$(load1)" "cores=$(cores)" \
181
+ "swap_free_mb=$(swap_free_mb)" "gates_funded=$(gate_headroom 2>/dev/null || echo 0)" \
182
+ "orphans=$(orphan_count)" "sessions=$(sessions)" 2>/dev/null || true
183
+ }
184
+
185
+ run_once() {
186
+ if [ "$AS_JSON" = "1" ]; then print_json; return; fi
187
+ [ "$FIX" = "1" ] && run_fix
188
+ [ "$SIGNAL" = "1" ] && emit_verdict
189
+ [ "$WANT_BOX" = "1" ] && print_box
190
+ [ "$WANT_BOX" = "1" ] && [ "$WANT_SVC" = "1" ] && echo
191
+ [ "$WANT_SVC" = "1" ] && print_services
192
+ return 0
193
+ }
194
+
195
+ # ── fix ───────────────────────────────────────────────────────────────────────
196
+ # Every candidate is named with the reason it is dead, so a --dry-run reads as
197
+ # an argument and not as a list of pids.
198
+ REPO_ROOT="$(cd "$HERE/../.." && pwd)"
199
+
200
+ _reap() { # pid why
201
+ local pid="$1" why="$2" rss pgid
202
+ rss=$(( $(ps -o rss= -p "$pid" 2>/dev/null | tr -d ' ' || echo 0) / 1024 ))
203
+ if [ "$DRY" = "1" ]; then printf ' %sWOULD%s reclaim pid=%-7s %5sMB %s\n' "$C_WARN" "$C_OFF" "$pid" "$rss" "$why"; return 0; fi
204
+ pgid=$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ')
205
+ kill -TERM -"${pgid:-$pid}" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
206
+ printf ' %sreclaimed%s pid=%-7s %5sMB %s\n' "$C_OK" "$C_OFF" "$pid" "$rss" "$why"
207
+ }
208
+
209
+ # A governor slot owner is live work by definition — it is holding the lock the
210
+ # whole box queues behind.
211
+ _holds_slot() {
212
+ local pid="$1" d
213
+ for d in "${TMPDIR:-/tmp}/one-govern"/lock-slot-*; do
214
+ [ -d "$d" ] || continue
215
+ [ "$(cat "$d/pid" 2>/dev/null)" = "$pid" ] && return 0
216
+ done
217
+ return 1
218
+ }
219
+
220
+ # Is any live claude session cwd'd inside this worktree? lsof on the session pid
221
+ # is the only honest test — a worktree path in a command line proves nothing.
222
+ _worktree_has_session() {
223
+ local wt="$1" pid
224
+ for pid in $(ps -Ao pid=,command= 2>/dev/null | awk '$2 ~ /claude$/ || / claude --/ {print $1}'); do
225
+ lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | grep -q "^n${wt}" && return 0
226
+ done
227
+ return 1
228
+ }
229
+
230
+ fix_orphans() { # delegated whole — this file must not own a second reaper
231
+ [ -x "$HERE/gate-reaper.sh" ] || return 0
232
+ local out
233
+ if [ "$DRY" = "1" ]; then out=$(bash "$HERE/gate-reaper.sh" --once --dry-run 2>/dev/null)
234
+ else out=$(bash "$HERE/gate-reaper.sh" --once 2>/dev/null); fi
235
+ [ -n "$out" ] && printf '%s\n' "$out" | sed 's/^/ /'
236
+ return 0
237
+ }
238
+
239
+ fix_worktree_lsp() {
240
+ local pid root rss
241
+ for pid in $(pgrep -f 'tsserver.js|typescript/lib/tsserver' 2>/dev/null); do
242
+ _holds_slot "$pid" && continue
243
+ root=$(lsof -nP -p "$pid" 2>/dev/null | grep -oE "${REPO_ROOT}/\.claude/worktrees/[^/]+" | head -1)
244
+ [ -n "$root" ] || continue
245
+ _reap "$pid" "language server indexing $(basename "$root") — the editor config excludes worktrees"
246
+ done
247
+ }
248
+
249
+ fix_dead_dev_servers() {
250
+ # Resolve each dev server by its CWD, never by string-matching its argv. The
251
+ # first draft grepped pids and worktree paths out of one ps stream and paired
252
+ # them with `paste - -`; when a server's argv carried no worktree path the
253
+ # columns silently shifted and it paired one pid with ANOTHER pid. A kill list
254
+ # built by that is a coin flip, so cwd it is.
255
+ local pid cwd wt
256
+ for pid in $(pgrep -f 'bin/astro dev' 2>/dev/null); do
257
+ _holds_slot "$pid" && continue
258
+ cwd=$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -1)
259
+ case "$cwd" in
260
+ "$REPO_ROOT"/.claude/worktrees/*) ;;
261
+ *) continue ;;
262
+ esac
263
+ wt="${cwd#"$REPO_ROOT"/.claude/worktrees/}"; wt="$REPO_ROOT/.claude/worktrees/${wt%%/*}"
264
+ _worktree_has_session "$wt" && continue
265
+ _reap "$pid" "dev server in $(basename "$wt") — no live session is cwd'd there"
266
+ done
267
+ }
268
+
269
+ run_fix() {
270
+ printf '%s\n' "── reclaiming ──────────────────────────────────"
271
+ [ "$DRY" = "1" ] && printf ' %sdry run — nothing will be killed%s\n' "$C_DIM" "$C_OFF"
272
+ local before after
273
+ before=$(swap_free_mb)
274
+ fix_orphans
275
+ fix_worktree_lsp
276
+ fix_dead_dev_servers
277
+ sleep 2
278
+ after=$(swap_free_mb)
279
+ printf ' %sswap free %sM -> %sM%s\n\n' "$C_DIM" "$before" "$after" "$C_OFF"
280
+ }
281
+
282
+ # ── self-test ─────────────────────────────────────────────────────────────────
283
+ # Proves the two things that can silently rot: the verdict thresholds, and the
284
+ # probe's willingness to call a 200 that lacks its assert DEGRADED rather than UP.
285
+ self_test() {
286
+ local fails=0
287
+ ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; }
288
+ bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fails=$((fails + 1)); }
289
+ echo "── health.sh: the verdict bites, and a bad 200 is not UP ──"
290
+
291
+ cores() { echo 10; }; gate_headroom() { echo 4; }; orphan_count() { echo 0; }
292
+ load1() { echo 2.0; }; swap_free_mb() { echo 4000; }
293
+ [ "$(box_verdict)" != "${_x:-}" ] && case "$(box_verdict)" in HEALTHY*) ok "C1 quiet box reads HEALTHY";; *) bad "C1 quiet box: $(box_verdict)";; esac
294
+
295
+ load1() { echo 25.0; }
296
+ case "$(box_verdict)" in UNHEALTHY*) ok "C2 load over 2x cores reads UNHEALTHY";; *) bad "C2 got: $(box_verdict)";; esac
297
+
298
+ load1() { echo 2.0; }; swap_free_mb() { echo 100; }
299
+ case "$(box_verdict)" in UNHEALTHY*) ok "C3 swap under 512MB free reads UNHEALTHY";; *) bad "C3 got: $(box_verdict)";; esac
300
+
301
+ swap_free_mb() { echo 4000; }; orphan_count() { echo 3; }
302
+ case "$(box_verdict)" in DEGRADED*orphan*) ok "C4 orphans read DEGRADED and name the fix";; *) bad "C4 got: $(box_verdict)";; esac
303
+
304
+ orphan_count() { echo 0; }; gate_headroom() { echo 1; }
305
+ case "$(box_verdict)" in DEGRADED*funds*) ok "C5 headroom of 1 reads DEGRADED";; *) bad "C5 got: $(box_verdict)";; esac
306
+
307
+ # A 200 whose body lost its assert is the failure deploy.sh's probe exists to
308
+ # catch — a worker that boots and serves the wrong app.
309
+ curl() { printf 'not the droid\n200'; }
310
+ case "$(probe x http://stub '"status":"ok"')" in *"|DEGRADED|"*) ok "C6 a 200 missing its assert is DEGRADED, not UP";; *) bad "C6 got: $(probe x http://stub '\"status\":\"ok\"')";; esac
311
+ curl() { printf '{"status":"ok"}\n200'; }
312
+ case "$(probe x http://stub '"status":"ok"')" in *"|UP|"*) ok "C7 a 200 carrying its assert is UP";; *) bad "C7 got: $(probe x http://stub)";; esac
313
+
314
+ # ── the --fix rules: each candidate must be dead by a test live work fails ──
315
+ # _reap is replaced by a recorder, so nothing is killed and the LIST is the
316
+ # assertion. That list is the whole safety argument of --fix.
317
+ REAPED=""
318
+ _reap() { REAPED="$REAPED $1"; }
319
+ REPO_ROOT="/repo"
320
+
321
+ pgrep() { echo 4242; }
322
+ lsof() { echo "n/repo/.claude/worktrees/wt-a/app/web"; } # generic on purpose: a portable script must name no monorepo path
323
+ _holds_slot() { return 1; }
324
+ _worktree_has_session() { return 1; }
325
+
326
+ REAPED=""; fix_worktree_lsp
327
+ case "$REAPED" in *4242*) ok "F1 a language server rooted in a worktree is a candidate";; *) bad "F1 missed it: '$REAPED'";; esac
328
+
329
+ _holds_slot() { return 0; }
330
+ REAPED=""; fix_worktree_lsp
331
+ case "$REAPED" in *4242*) bad "F2 killed a language server that holds a governor slot";; *) ok "F2 a slot holder is spared — it is the lock the box queues behind";; esac
332
+ _holds_slot() { return 1; }
333
+
334
+ REAPED=""; fix_dead_dev_servers
335
+ case "$REAPED" in *4242*) ok "F3 a dev server in a session-less worktree is a candidate";; *) bad "F3 missed it: '$REAPED'";; esac
336
+
337
+ _worktree_has_session() { return 0; }
338
+ REAPED=""; fix_dead_dev_servers
339
+ case "$REAPED" in *4242*) bad "F4 killed a dev server whose worktree HAS a live session";; *) ok "F4 a worktree with a live session is spared";; esac
340
+ _worktree_has_session() { return 1; }
341
+
342
+ # A dev server in the MAIN tree is never a candidate — the cwd case only
343
+ # admits paths under .claude/worktrees/.
344
+ lsof() { echo "n/repo/app/web"; }
345
+ REAPED=""; fix_dead_dev_servers
346
+ case "$REAPED" in *4242*) bad "F5 killed a dev server running in the MAIN tree";; *) ok "F5 the main tree's dev server is never a candidate";; esac
347
+
348
+ # --dry-run must not kill. Restore the real _reap and prove its dry path never
349
+ # reaches kill by stubbing kill itself as a recorder.
350
+ unset -f _reap 2>/dev/null || true
351
+ eval "$(sed -n '/^_reap() {/,/^}/p' "${BASH_SOURCE[0]}")"
352
+ KILLED=""; kill() { KILLED="$KILLED $*"; }
353
+ DRY=1; out=$(_reap 4242 "stub");
354
+ case "$out$KILLED" in *WOULD*) [ -z "$KILLED" ] && ok "F6 --dry-run names the kill and never calls it" || bad "F6 dry run called kill: $KILLED";; *) bad "F6 dry run printed: $out";; esac
355
+ unset -f kill 2>/dev/null || true
356
+
357
+ # The red proofs below run the file AGAINST ITSELF. Without this guard the
358
+ # gutted child runs them too, and each child spawns two more — the first
359
+ # version of R2 died on `mktemp: File exists`, which was the recursion, not
360
+ # the template.
361
+ if [ "${HEALTH_SELFTEST_CHILD:-0}" = "1" ]; then
362
+ if [ $fails -eq 0 ]; then echo "ALL GREEN (child)"; return 0; fi
363
+ echo "RED — $fails check(s) failed"; return 1
364
+ fi
365
+
366
+ # RED PROOF — take the REAL file, delete the swap arm, and run its OWN
367
+ # self-test. C3 must fail there. Stubbing box_verdict to a constant would
368
+ # prove nothing about the arm; this proves the assertion is load-bearing.
369
+ local gutted anchor_lines out
370
+ gutted="$(mktemp "${TMPDIR:-/tmp}/health-gutted.XXXXXX")"
371
+ grep -v "swapfree:-""9999" "${BASH_SOURCE[0]}" > "$gutted"
372
+ anchor_lines=$(( $(wc -l < "${BASH_SOURCE[0]}") - $(wc -l < "$gutted") ))
373
+ if [ "$anchor_lines" -ne 1 ]; then
374
+ bad "R1 could not remove the swap arm — anchor moved, this checker is blind"
375
+ else
376
+ out=$(HEALTH_SELFTEST_CHILD=1 bash "$gutted" --self-test 2>&1)
377
+ case "$out" in
378
+ *"FAIL"*"C3"*) ok "R1 red-proof: with the swap arm deleted, C3 goes red" ;;
379
+ *) bad "R1 red-proof did not bite — gutted script still passed C3" ;;
380
+ esac
381
+ fi
382
+ rm -f "$gutted"
383
+
384
+ # RED PROOF for the fix rules — delete the governor-slot guard from the real
385
+ # file and require F2 to go red there. F1..F6 passing against a build that
386
+ # cannot spare live work would be worthless.
387
+ local gutted2 removed out2 guard
388
+ gutted2="$(mktemp "${TMPDIR:-/tmp}/health-gutted2.XXXXXX")"
389
+ # Assembled with printf so this line is not itself the anchor it deletes.
390
+ guard=$(printf '_holds_slot "$%s" && continue' pid)
391
+ grep -vF "$guard" "${BASH_SOURCE[0]}" > "$gutted2"
392
+ removed=$(( $(wc -l < "${BASH_SOURCE[0]}") - $(wc -l < "$gutted2") ))
393
+ if [ "$removed" -ne 2 ]; then
394
+ bad "R2 could not remove the slot guard (removed ${removed} lines, expected 2) — anchor moved, this checker is blind"
395
+ else
396
+ out2=$(HEALTH_SELFTEST_CHILD=1 bash "$gutted2" --self-test 2>&1)
397
+ case "$out2" in
398
+ *"FAIL"*"F2"*) ok "R2 red-proof: with the slot guard deleted, F2 goes red" ;;
399
+ *) bad "R2 red-proof did not bite — gutted --fix still spared a slot holder" ;;
400
+ esac
401
+ fi
402
+ rm -f "$gutted2"
403
+
404
+ if [ $fails -eq 0 ]; then echo "ALL GREEN — the verdict bites and a bad 200 is not UP"; return 0; fi
405
+ echo "RED — $fails check(s) failed"; return 1
406
+ }
407
+
408
+ [ "$SELFTEST" = "1" ] && { self_test; exit $?; }
409
+
410
+ if [ "${WATCH:-0}" != "0" ]; then
411
+ while :; do clear 2>/dev/null || true; date '+%H:%M:%S'; run_once; sleep "$WATCH"; done
412
+ fi
413
+ run_once