@oneie/claude 0.8.0 → 0.10.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 (230) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/abm-strategist.md +67 -1
  3. package/agents/ads-meta.md +67 -1
  4. package/agents/analyst.md +67 -1
  5. package/agents/animator.md +108 -0
  6. package/agents/architect.md +269 -20
  7. package/agents/brand-guardian.md +67 -1
  8. package/agents/brand-strategist.md +67 -1
  9. package/agents/campaign-content.md +67 -1
  10. package/agents/campaign-email.md +67 -1
  11. package/agents/campaign-sms.md +67 -1
  12. package/agents/campaign-social.md +67 -1
  13. package/agents/cco.md +83 -2
  14. package/agents/ceo.md +108 -11
  15. package/agents/chairman.md +197 -0
  16. package/agents/cmo.md +82 -2
  17. package/agents/community-greeter.md +67 -1
  18. package/agents/community-moderator.md +67 -1
  19. package/agents/compliance.md +67 -1
  20. package/agents/copywriter.md +67 -1
  21. package/agents/creative-strategist.md +67 -1
  22. package/agents/cro.md +81 -1
  23. package/agents/cto.md +266 -28
  24. package/agents/customer-interviewer.md +67 -1
  25. package/agents/customer-researcher.md +67 -1
  26. package/agents/customer-success-manager.md +67 -1
  27. package/agents/customer-trainer.md +67 -1
  28. package/agents/cxo.md +82 -1
  29. package/agents/demand-creator.md +67 -1
  30. package/agents/demo-mover.md +67 -1
  31. package/agents/demo-specialist.md +67 -1
  32. package/agents/demo-thai-family-law.md +67 -1
  33. package/agents/designer.md +67 -1
  34. package/agents/discovery-caller.md +67 -1
  35. package/agents/doctor.md +269 -0
  36. package/agents/educate-coach.md +67 -1
  37. package/agents/elevate-tutor.md +67 -1
  38. package/agents/email-lifecycle-marketer.md +67 -1
  39. package/agents/engage-specialist.md +67 -1
  40. package/agents/events-coordinator.md +67 -1
  41. package/agents/foundation-builder.md +67 -1
  42. package/agents/funnel-architect.md +67 -1
  43. package/agents/gift-creator.md +67 -1
  44. package/agents/google-ads.md +67 -1
  45. package/agents/guide.md +67 -1
  46. package/agents/helpdesk-dispatcher.md +67 -1
  47. package/agents/hook-specialist.md +67 -1
  48. package/agents/identify-optimizer.md +67 -1
  49. package/agents/implementer.md +313 -45
  50. package/agents/incident-commander.md +67 -1
  51. package/agents/insights-lead.md +87 -1
  52. package/agents/journey-runner.md +67 -1
  53. package/agents/linkedin-ads.md +67 -1
  54. package/agents/live-sales-chat.md +67 -1
  55. package/agents/market-researcher.md +67 -1
  56. package/agents/media-buyer.md +67 -1
  57. package/agents/memory-keeper.md +195 -0
  58. package/agents/movers-customer-researcher.md +67 -1
  59. package/agents/movers-foundation-builder.md +67 -1
  60. package/agents/movers-market-researcher.md +67 -1
  61. package/agents/movers-pricing-strategist.md +67 -1
  62. package/agents/nurture-architect.md +67 -1
  63. package/agents/offer-architect.md +67 -1
  64. package/agents/onboarder.md +67 -1
  65. package/agents/onboarding-specialist.md +67 -1
  66. package/agents/operations-dashboard.md +87 -1
  67. package/agents/perf-engineer.md +333 -37
  68. package/agents/playbook-writer.md +67 -1
  69. package/agents/plg-strategist.md +67 -1
  70. package/agents/positioning-architect.md +67 -1
  71. package/agents/press-officer.md +67 -1
  72. package/agents/pricing-strategist.md +67 -1
  73. package/agents/privacy-officer.md +67 -1
  74. package/agents/referral-manager.md +67 -1
  75. package/agents/refine-analyst.md +67 -1
  76. package/agents/release-manager.md +446 -39
  77. package/agents/renewals-upsell-rep.md +67 -1
  78. package/agents/review-engineer.md +319 -45
  79. package/agents/rewards-steward.md +67 -1
  80. package/agents/sales-call-coach.md +67 -1
  81. package/agents/sales-closer.md +67 -1
  82. package/agents/security-auditor.md +343 -48
  83. package/agents/sell-closer.md +67 -1
  84. package/agents/share-amplifier.md +67 -1
  85. package/agents/social-media-manager.md +67 -1
  86. package/agents/storyteller.md +301 -0
  87. package/agents/strategist.md +67 -1
  88. package/agents/strategy-aligner.md +67 -1
  89. package/agents/support-agent.md +67 -1
  90. package/agents/tagger.md +327 -0
  91. package/agents/tech-writer.md +195 -22
  92. package/agents/test-engineer.md +398 -29
  93. package/agents/tiktok-ads.md +67 -1
  94. package/agents/tracking-engineer.md +67 -1
  95. package/agents/trailkeeper.md +181 -0
  96. package/agents/upsell-strategist.md +67 -1
  97. package/agents/voice.md +67 -1
  98. package/agents/w1-recon.md +1 -1
  99. package/agents/w2-decide.md +1 -1
  100. package/agents/w3-edit.md +8 -2
  101. package/agents/w4-verify.md +13 -0
  102. package/agents/workflow-optimiser.md +81 -1
  103. package/commands/close.md +916 -160
  104. package/commands/deploy.md +102 -724
  105. package/commands/do.md +58 -2
  106. package/commands/sweep.md +159 -0
  107. package/commands/tasks.md +222 -0
  108. package/hooks/scripts/dev-only.sh +135 -0
  109. package/hooks/scripts/git-add-guard.sh +37 -2
  110. package/hooks/scripts/session-start.sh +32 -4
  111. package/package.json +1 -1
  112. package/rules/scripts.md +85 -0
  113. package/scripts/CLAUDE.md +315 -0
  114. package/scripts/ad-copy-lint.sh +656 -0
  115. package/scripts/agent-actor-parity.sh +129 -0
  116. package/scripts/blocks-manifest-cached.sh +100 -0
  117. package/scripts/chat-context-check.sh +89 -0
  118. package/scripts/chrome.mjs +18 -0
  119. package/scripts/close-metrics.sh +587 -0
  120. package/scripts/close-owner.sh +326 -0
  121. package/scripts/db-sync-lock-check.sh +116 -0
  122. package/scripts/deploy-emit.sh +311 -0
  123. package/scripts/deploy-gate-check.sh +155 -0
  124. package/scripts/deploy-ready.sh +78 -0
  125. package/scripts/deploy-record.sh +605 -0
  126. package/scripts/deploy-schema-check.sh +58 -0
  127. package/scripts/deploy.sh +393 -243
  128. package/scripts/do-auto.sh +127 -26
  129. package/scripts/do-board.sh +429 -0
  130. package/scripts/do-close.sh +1184 -0
  131. package/scripts/do-consumer-sweep.sh +18 -1
  132. package/scripts/do-decide.sh +476 -0
  133. package/scripts/do-fleet.sh +8 -2
  134. package/scripts/do-plan-json.mjs +110 -12
  135. package/scripts/do-prove-selftest.sh +108 -0
  136. package/scripts/do-prove.sh +86 -10
  137. package/scripts/do-rank.py +200 -3
  138. package/scripts/do-reconcile.sh +73 -12
  139. package/scripts/do-signal.sh +101 -23
  140. package/scripts/do-smoke.sh +18 -1
  141. package/scripts/do-w4-gates.sh +11 -1
  142. package/scripts/do-world-check.sh +153 -0
  143. package/scripts/download-stats.sh +172 -0
  144. package/scripts/factory-brief-check.sh +330 -0
  145. package/scripts/factory-check.sh +18 -1
  146. package/scripts/factory-close-check.sh +257 -0
  147. package/scripts/factory-emit.sh +211 -0
  148. package/scripts/factory-executor-check.mjs +353 -0
  149. package/scripts/factory-peak.sh +301 -0
  150. package/scripts/factory-repo.sh +71 -0
  151. package/scripts/factory-review-check.mjs +61 -0
  152. package/scripts/factory-tasks-check.sh +18 -1
  153. package/scripts/fixtures/factory-brief-real.md +44 -0
  154. package/scripts/flywheel-outcome.sh +63 -0
  155. package/scripts/gate-reaper-check.sh +98 -0
  156. package/scripts/gate-reaper.sh +9 -0
  157. package/scripts/gate-watchdog.sh +619 -0
  158. package/scripts/gc-content-check.sh +142 -0
  159. package/scripts/gh-traffic-capture.sh +153 -0
  160. package/scripts/govern-order-check.sh +202 -0
  161. package/scripts/governor-doors-check.sh +86 -5
  162. package/scripts/health.sh +448 -0
  163. package/scripts/id-inventory.mjs +418 -0
  164. package/scripts/incident.sh +212 -0
  165. package/scripts/land.sh +755 -45
  166. package/scripts/lib/gc-finished.sh +77 -0
  167. package/scripts/livekit-ratchet.sh +18 -1
  168. package/scripts/machine-check.sh +1 -1
  169. package/scripts/memory-index-budget.sh +79 -0
  170. package/scripts/npm-downloads.sh +109 -0
  171. package/scripts/one-agents.mjs +204 -8
  172. package/scripts/one-resume.sh +31 -3
  173. package/scripts/pr-body.sh +335 -0
  174. package/scripts/preview-fd-check.sh +289 -0
  175. package/scripts/redirect-lint.sh +169 -0
  176. package/scripts/release.sh +40 -6
  177. package/scripts/resume-lost-sessions.sh +68 -0
  178. package/scripts/shoot-pages.mjs +140 -0
  179. package/scripts/signal-meta-backfill.ts +451 -0
  180. package/scripts/signal-watch.sh +63 -6
  181. package/scripts/speed-cache-check.sh +12 -2
  182. package/scripts/sweep.sh +426 -0
  183. package/scripts/task-titles-dump.ts +101 -0
  184. package/scripts/test-cached.sh +47 -10
  185. package/scripts/test-lanes.sh +14 -0
  186. package/scripts/thread-name-backfill.ts +215 -0
  187. package/scripts/triage-shape-check.sh +149 -0
  188. package/scripts/tsc-cached.sh +155 -8
  189. package/scripts/typedb-flake-check.sh +3 -1
  190. package/scripts/urls-lint.sh +8 -0
  191. package/scripts/verify-board-doors.sh +80 -0
  192. package/scripts/verify-fast.sh +159 -6
  193. package/scripts/worktree-up.sh +21 -3
  194. package/skills/astro/SKILL.md +9 -3
  195. package/skills/astro/optimize-performance.md +3 -2
  196. package/skills/cloudflare/SKILL.md +3 -2
  197. package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
  198. package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
  199. package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
  200. package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  201. package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  202. package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  203. package/skills/cloudflare-security-audit/HUNTING.md +251 -0
  204. package/skills/cloudflare-security-audit/LICENSE +21 -0
  205. package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  206. package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  207. package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
  208. package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
  209. package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  210. package/skills/cloudflare-security-audit/SKILL.md +192 -0
  211. package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  212. package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
  213. package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  214. package/skills/cloudflare-security-audit/report-schema.json +461 -0
  215. package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
  216. package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
  217. package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
  218. package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
  219. package/skills/deploy/REFERENCE.md +713 -0
  220. package/skills/deploy/SKILL.md +140 -0
  221. package/skills/fleet-audit/SKILL.md +58 -0
  222. package/skills/meeting/SKILL.md +220 -0
  223. package/skills/planning/SKILL.md +256 -0
  224. package/skills/shadcn/SKILL.md +1 -1
  225. package/skills/typedb/SKILL.md +7 -0
  226. package/skills/voice/SKILL.md +94 -6
  227. package/skills/voice/corpus-check.sh +87 -0
  228. package/templates/template-agent.md +7 -1
  229. package/templates/template-feature.md +9 -0
  230. package/templates/template-todo.md +29 -0
@@ -0,0 +1,619 @@
1
+ #!/usr/bin/env bash
2
+ # gate-watchdog.sh — break the one governor jam whose owners are both ALIVE.
3
+ #
4
+ # manifest: portable
5
+ #
6
+ # WHY (measured 2026-09-13: three times in ~35 minutes, 8-28 minutes each at 0%
7
+ # CPU, one of them a production release gate). The governor has two resources and
8
+ # two callers took them in opposite orders:
9
+ #
10
+ # A: gate-run.sh(verify-fast) holds slot-1 -> its child tsc-cached.sh
11
+ # waits for lock-tsc-one.ie_web
12
+ # B: tsc-cached.sh holds lock-tsc-one.ie_web -> its inner gate-run.sh
13
+ # waits for a slot
14
+ #
15
+ # Two orders, one cycle. `gate-reaper.sh` and govern.ts's own `reapLock` reap a
16
+ # DEAD owner; both of these are alive, so nothing in the harness could see it.
17
+ # Every other session queues behind them and reads the wait as a hang.
18
+ #
19
+ # The code fix is SLOT-before-LOCK everywhere (tsc-cached.sh + do-reconcile.sh,
20
+ # proved by `govern-order-check.sh`). This script is the operational half: a
21
+ # worktree only carries that fix once it rebases, and until every tree on the box
22
+ # has it, one inverted participant re-forms the cycle.
23
+ #
24
+ # ── WHICH PROCESS IT CUTS, AND WHY THAT ONE ─────────────────────────────────
25
+ # The cycle always has exactly ONE process on the wrong side: a lock holder that
26
+ # holds NO slot. Cut it and the lock lands on a waiter that already holds a slot,
27
+ # which can then finish and release everything. Measured the same day: three
28
+ # standalone holders killed in sequence (the lock passing to the next standalone
29
+ # each time), then it landed on an in-gate owner and tsc ran at 121% CPU.
30
+ #
31
+ # It NEVER cuts a slot owner. The scratchpad draft did, as a fallback when no
32
+ # standalone holder was found; that throws away a whole verify run on the exact
33
+ # evidence that says the cycle is NOT the one described above. No proven cycle,
34
+ # no cut — the jam is reported and a human decides.
35
+ #
36
+ # ── THE PREDICATE IS POSITIVE EVIDENCE, NOT ABSENCE ─────────────────────────
37
+ # "Idle" is not "deadlocked". On a swapping box a real tsc blocked on page-ins
38
+ # sits at 0% CPU for minutes, and a watchdog that kills a slow-but-working gate
39
+ # is worse than no watchdog. So a victim must satisfy ALL of:
40
+ #
41
+ # 1. it owns a non-slot lock in GOVERN_DIR and is alive
42
+ # 2. it holds NO slot — no gate-run.sh in its ancestry, and its pid is in no
43
+ # lock-slot-*/pid
44
+ # 3. it HAS a gate-run.sh descendant that holds no slot — so it is provably
45
+ # inside the slot queue while holding the lock. This is the clause that
46
+ # separates a deadlock from a process that is merely quiet
47
+ # 4. at least one slot is held by a live owner, and NO slot owner's subtree has
48
+ # anything over CPU_FLOOR% — nobody with a slot is computing, so the queue
49
+ # cannot drain on its own
50
+ # 5. a SLOT has been held for >= STALL_MIN minutes — read from the slot
51
+ # DIRECTORY's mtime, which mkdir(2) stamps at acquisition and nothing writes
52
+ # into afterwards. No in-process timer, so --once and the loop share one
53
+ # clock and there is no state file
54
+ # 5b. and the victim's own lock is older than LOCK_MIN seconds, so a handoff is
55
+ # never mistaken for a jam
56
+ #
57
+ # THE CLOCK IS THE SLOT, NOT THE LOCK, and that was measured the hard way. In the
58
+ # live jam of 2026-09-13 16:00Z the LOCK changed hands every ~9 minutes — each
59
+ # standalone holder was killed by its session's 600s Bash ceiling, released, and
60
+ # the next queued standalone took it, resetting the directory mtime. The jam
61
+ # itself ran >20 minutes; no single lock holding ever reached 10. A watchdog
62
+ # clocking the lock would have watched that jam forever and never fired.
63
+ # The slot is the thing that does not rotate: slot-1 was held by one pid,
64
+ # subtree idle, for the whole episode.
65
+ #
66
+ # Then it re-checks the whole predicate after CONFIRM_SECS and requires the SAME
67
+ # victim pid and the SAME lock mtime before cutting. A healthy box costs one ps.
68
+ #
69
+ # The floor's ceiling is set by the thing it prevents: gate-run.sh queues for
70
+ # GOVERN_QUEUE_WAIT (default 1800s) and then prints "no slot after 1800s —
71
+ # machine saturated" and exits 1, which tsc-cached reports as a FAILED typecheck
72
+ # that never ran. Acting at 10 minutes is acting before the false RED.
73
+ #
74
+ # Usage: gate-watchdog.sh [--once] [--interval N] [--dry-run] [--self-test]
75
+ # (no flags) loop every --interval seconds (default 60) until interrupted
76
+ # --once one sweep, then exit — this is the session-start form
77
+ # --dry-run name the victim, cut nothing
78
+ # --self-test drive both halves in a sandboxed GOVERN_DIR (see --self-test)
79
+ #
80
+ # Prints one line per finding; SILENT when there is nothing to do, so it is safe
81
+ # to run from a hook. Every environment knob: GATE_WATCHDOG_STALL_MIN,
82
+ # GATE_WATCHDOG_CPU_FLOOR, GATE_WATCHDOG_CONFIRM.
83
+ set -uo pipefail
84
+
85
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
86
+ G="${GOVERN_DIR:-${TMPDIR:-/tmp}/one-govern}"
87
+ STALL_MIN="${GATE_WATCHDOG_STALL_MIN:-10}"
88
+ LOCK_MIN="${GATE_WATCHDOG_LOCK_MIN:-60}"
89
+ CPU_FLOOR="${GATE_WATCHDOG_CPU_FLOOR:-20}"
90
+ CONFIRM_SECS="${GATE_WATCHDOG_CONFIRM:-3}"
91
+ ANCESTRY_MAX="${GATE_WATCHDOG_ANCESTRY_MAX:-16}"
92
+
93
+ ONCE=0; INTERVAL=60; DRY=0; SELFTEST=0
94
+ while [ $# -gt 0 ]; do
95
+ case "$1" in
96
+ --once) ONCE=1 ;;
97
+ --dry-run) DRY=1 ;;
98
+ --interval) INTERVAL="${2:-60}"; shift ;;
99
+ --self-test) SELFTEST=1 ;;
100
+ # The whole header, not a line range: a range drifts silently every time the
101
+ # measured account above grows, and --help then prints the middle of an
102
+ # argument instead of the usage.
103
+ -h|--help) sed -n '2,/^set -uo pipefail$/p' "${BASH_SOURCE[0]}" | sed '$d'; exit 0 ;;
104
+ *) echo "gate-watchdog: unknown flag $1" >&2; exit 2 ;;
105
+ esac
106
+ shift
107
+ done
108
+
109
+ say() { printf '[gate-watchdog] %s\n' "$*"; }
110
+
111
+ # ── the process table, read ONCE per sweep ──────────────────────────────────
112
+ # One snapshot answers every ancestry, descendant, liveness and CPU question, so
113
+ # the whole script is driven by a stubbed `ps` in --self-test, and a sweep cannot
114
+ # see two different machines halfway through. Liveness IS presence in this
115
+ # snapshot — `kill -0` would answer about a different instant.
116
+ PS_SNAP=""
117
+ _snap() { PS_SNAP="$(ps -Ao pid=,ppid=,pcpu=,command= 2>/dev/null)"; }
118
+
119
+ _ppid() { awk -v p="$1" '$1==p {print $2; exit}' <<<"$PS_SNAP"; }
120
+ _pcpu() { awk -v p="$1" '$1==p {print $3; exit}' <<<"$PS_SNAP"; }
121
+ _cmd() { awk -v p="$1" '$1==p {for(i=4;i<=NF;i++) printf "%s ", $i; exit}' <<<"$PS_SNAP"; }
122
+ _alive() { [ -n "${1:-}" ] && [ -n "$(_ppid "$1")" ]; }
123
+ _kids() { awk -v p="$1" '$2==p {print $1}' <<<"$PS_SNAP"; }
124
+
125
+ # Every descendant of $1, deepest first, so children die before their parent.
126
+ # DOWNWARD ONLY, never a process group: the victim's PARENT is a live Claude
127
+ # session's shell (measured — pid 13011's parent chain was zsh -> claude), and
128
+ # gate-reaper's `kill -TERM -pgid` would take the session with it.
129
+ _descendants() {
130
+ local k
131
+ for k in $(_kids "$1"); do _descendants "$k"; done
132
+ printf '%s\n' "$1"
133
+ }
134
+
135
+ _ancestry_has_gate_run() { # 0 = this pid sits under a gate-run.sh (holds a slot)
136
+ local q="$1" i
137
+ for i in $(seq 1 "$ANCESTRY_MAX"); do
138
+ [ -n "$q" ] && [ "$q" != "1" ] && [ "$q" != "0" ] || return 1
139
+ case "$(_cmd "$q")" in *gate-run.sh*) return 0 ;; esac
140
+ q="$(_ppid "$q")"
141
+ done
142
+ return 1
143
+ }
144
+
145
+ _max_cpu_in_subtree() { # highest %CPU anywhere under (and including) $1
146
+ local p m=0 c
147
+ for p in $(_descendants "$1"); do
148
+ c="$(_pcpu "$p")"; [ -n "$c" ] || continue
149
+ awk -v a="$c" -v b="$m" 'BEGIN { exit !(a > b) }' && m="$c"
150
+ done
151
+ printf '%s' "$m"
152
+ }
153
+
154
+ # ── the governor's two resources, as read off disk ──────────────────────────
155
+ _slot_pids() { # pid of every slot dir that has one (alive or not)
156
+ local d p
157
+ for d in "$G"/lock-slot-*; do
158
+ [ -f "$d/pid" ] || continue
159
+ p="$(tr -d '[:space:]' < "$d/pid" 2>/dev/null)"
160
+ [ -n "$p" ] && printf '%s\n' "$p"
161
+ done
162
+ }
163
+
164
+ _holds_a_slot() { local p; for p in $(_slot_pids); do [ "$p" = "$1" ] && return 0; done; return 1; }
165
+
166
+ _lock_age_secs() {
167
+ local m; m="$(stat -f '%m' "$1" 2>/dev/null)" || return 1
168
+ [ -n "$m" ] || return 1
169
+ printf '%s' "$(( $(date +%s) - m ))"
170
+ }
171
+
172
+ # ── find_victim — echoes "<pid> <lockdir> <mtime>", or nothing ──────────────
173
+ # Prints its reasoning to stderr under GATE_WATCHDOG_DEBUG=1; silent otherwise.
174
+ find_victim() {
175
+ # NO ARRAYS. bash 3.2 is what macOS ships, and `${#arr[@]}` on an EMPTY array
176
+ # under `set -u` is an unbound-variable error there — on the healthy-box path,
177
+ # which is the one path that must never fail.
178
+ local d p slot_owners="" so busy_slot=0 kids_gr n oldest=0 a
179
+ _snap
180
+
181
+ # (4a) at least one LIVE slot owner. No slot held => no queue => nothing to break.
182
+ for so in $(_slot_pids); do _alive "$so" && slot_owners="$slot_owners $so"; done
183
+ [ -n "${slot_owners// /}" ] || { _dbg "no live slot owner — not a jam"; return 1; }
184
+
185
+ # (4b) nobody holding a slot is computing. A gate over the floor means the
186
+ # queue IS draining, however slowly, and a cut would throw away live work.
187
+ for so in $slot_owners; do
188
+ n="$(_max_cpu_in_subtree "$so")"
189
+ if awk -v a="$n" -v f="$CPU_FLOOR" 'BEGIN { exit !(a >= f) }'; then
190
+ _dbg "slot owner $so busy at ${n}% — box is working, not jammed"; busy_slot=1; break
191
+ fi
192
+ done
193
+ [ "$busy_slot" = "0" ] || return 1
194
+
195
+ # (5) THE JAM'S OWN CLOCK. How long has a slot been held while nothing under it
196
+ # computes? A legitimate gate is not idle for ten minutes — the full suite is
197
+ # 87s and a 2.5GB tsc burns CPU throughout. See the header for why this is not
198
+ # measured on the lock.
199
+ for d in "$G"/lock-slot-*; do
200
+ [ -f "$d/pid" ] || continue
201
+ p="$(tr -d '[:space:]' < "$d/pid" 2>/dev/null)"
202
+ _alive "$p" || continue
203
+ a="$(_lock_age_secs "$d")" || continue
204
+ [ "$a" -gt "$oldest" ] && oldest="$a"
205
+ done
206
+ if [ "$oldest" -lt $(( STALL_MIN * 60 )) ]; then
207
+ _dbg "oldest held slot is ${oldest}s, under the ${STALL_MIN}m floor"; return 1
208
+ fi
209
+
210
+ for d in "$G"/lock-*; do
211
+ case "$d" in *lock-slot-*) continue ;; esac
212
+ [ -f "$d/pid" ] || continue
213
+ p="$(tr -d '[:space:]' < "$d/pid" 2>/dev/null)"
214
+ # (1) a live owner. A DEAD owner's lock is gate_sweep_stale's job, not ours.
215
+ _alive "$p" || { _dbg "$(basename "$d"): owner ${p:-?} is dead — reaper's job"; continue; }
216
+ # (2) it must hold NO slot. An in-gate holder is the INNOCENT side of the
217
+ # cycle (SLOT then LOCK, the correct order) and cutting it costs a verify run.
218
+ if _holds_a_slot "$p" || _ancestry_has_gate_run "$p"; then
219
+ _dbg "$(basename "$d"): owner $p is inside a gate — innocent side"; continue
220
+ fi
221
+ # (3) POSITIVE EVIDENCE the cycle is closed: it has a gate-run.sh descendant
222
+ # that holds no slot, i.e. it is queueing for a slot while holding the lock.
223
+ kids_gr=""
224
+ for n in $(_descendants "$p"); do
225
+ [ "$n" = "$p" ] && continue
226
+ case "$(_cmd "$n")" in *gate-run.sh*) _holds_a_slot "$n" || kids_gr="$n" ;; esac
227
+ done
228
+ [ -n "$kids_gr" ] || { _dbg "$(basename "$d"): owner $p is not in the slot queue — quiet, not deadlocked"; continue; }
229
+ # (5b) don't race a handoff. The lock passes between standalone holders every
230
+ # few minutes during a jam; a directory created two seconds ago names a
231
+ # process that has not had time to be stuck.
232
+ local age; age="$(_lock_age_secs "$d")" || continue
233
+ if [ "$age" -lt "$LOCK_MIN" ]; then
234
+ _dbg "$(basename "$d"): taken ${age}s ago — a handoff, not yet a jam"; continue
235
+ fi
236
+ printf '%s %s %s\n' "$p" "$d" "$(stat -f '%m' "$d" 2>/dev/null)"
237
+ return 0
238
+ done
239
+ return 1
240
+ }
241
+
242
+ _dbg() { [ -n "${GATE_WATCHDOG_DEBUG:-}" ] && printf '[gate-watchdog] .. %s\n' "$*" >&2; return 0; }
243
+
244
+ cut_victim() {
245
+ local vpid="$1" vlock="$2" p
246
+ _snap
247
+ for p in $(_descendants "$vpid"); do kill -TERM "$p" 2>/dev/null; done
248
+ sleep 4
249
+ _snap
250
+ for p in $(_descendants "$vpid"); do kill -KILL "$p" 2>/dev/null; done
251
+ # tsc-cached traps EXIT/TERM and releases its own lock; belt and braces for the
252
+ # KILL path, where no trap runs. gate_sweep_stale reclaims a dead owner's lock.
253
+ if [ -f "$ROOT/.claude/scripts/lib/govern.sh" ]; then
254
+ bash -c ". '$ROOT/.claude/scripts/lib/govern.sh' >/dev/null 2>&1; gate_sweep_stale" >/dev/null 2>&1 || true
255
+ fi
256
+ [ -n "$vlock" ] && [ -d "$vlock" ] && rm -rf "$vlock" 2>/dev/null
257
+ # The orphans a broken cycle leaves behind are gate-reaper's shape, not ours.
258
+ # GATE_WATCHDOG_NO_REAP=1 holds it back, so --self-test never touches the real
259
+ # process table while proving the cut.
260
+ [ "${GATE_WATCHDOG_NO_REAP:-0}" = "1" ] \
261
+ || bash "$ROOT/.claude/scripts/gate-reaper.sh" --once 2>/dev/null || true
262
+ return 0
263
+ }
264
+
265
+ sweep() {
266
+ local v vpid vlock vmt v2
267
+ v="$(find_victim)" || return 0
268
+ # find_victim ran in a command substitution, so ITS snapshot died with the
269
+ # subshell. Take one here for the reporting and the kill walk.
270
+ _snap
271
+ vpid="$(awk '{print $1}' <<<"$v")"
272
+ vlock="$(awk '{print $2}' <<<"$v")"
273
+ vmt="$(awk '{print $3}' <<<"$v")"
274
+
275
+ if [ "$DRY" = "1" ]; then
276
+ say "WOULD cut pid=$vpid — holds $(basename "$vlock") ($(_lock_age_secs "$vlock")s) with NO slot while every slot has sat idle >= ${STALL_MIN}m: $(_cmd "$vpid" | cut -c1-70)"
277
+ return 0
278
+ fi
279
+
280
+ # CONFIRM. A jam that resolves in three seconds was a queue, not a cycle.
281
+ sleep "$CONFIRM_SECS"
282
+ v2="$(find_victim)" || { say "jam cleared while confirming — cut nothing"; return 0; }
283
+ if [ "$(awk '{print $1}' <<<"$v2")" != "$vpid" ] || [ "$(awk '{print $3}' <<<"$v2")" != "$vmt" ]; then
284
+ say "jam moved while confirming (was pid=$vpid) — cut nothing"
285
+ return 0
286
+ fi
287
+
288
+ say "BREAK — $(basename "$vlock") held by $vpid with NO slot ($(_lock_age_secs "$vlock")s) while every slot has been held and idle for >= ${STALL_MIN}m: $(_cmd "$vpid" | cut -c1-70)"
289
+ cut_victim "$vpid" "$vlock"
290
+ say "cut pid=$vpid, released $(basename "$vlock") — the lock now falls to a waiter that already holds a slot"
291
+ return 0
292
+ }
293
+
294
+ # ─────────────────────────────────────────────────────────────────────────────
295
+ # --self-test — both halves, in a sandbox GOVERN_DIR that is never the machine's.
296
+ #
297
+ # C1 a REAL planted deadlock is named, then cut, and the lock is released
298
+ # C2 the victim's own parent shell SURVIVES the cut (downward-only walk)
299
+ # C3 the sandbox's slot owner survives — a slot owner is never the victim
300
+ # C4 healthy box (no slot held) — nothing named
301
+ # C5 merely BUSY (a slot owner computing) — nothing named, even with a
302
+ # textbook deadlock shape sitting next to it
303
+ # C6 young jam (a slot held under the floor) — nothing named
304
+ # C6b a fresh HANDOFF of the lock inside an old jam — nothing named
305
+ # C7 the lock holder is itself in a gate — the innocent side, nothing named
306
+ # C8 a quiet lock holder with NO queueing gate-run — nothing named
307
+ # R1 RED PROOF: drop clause (3) and C8 must start naming the innocent
308
+ # R2 RED PROOF: drop clause (4b) and C5 must start naming a working box
309
+ # R3 RED PROOF: drop clause (5) and C6 must start naming a young queue
310
+ # R4 RED PROOF: drop clause (2) and C7 must start naming the in-gate holder
311
+ # H1 hooks/session-start.sh SAYS it broke a real planted jam — the silent
312
+ # path is not the only one that can rot
313
+ # ─────────────────────────────────────────────────────────────────────────────
314
+ self_test() {
315
+ local fails=0
316
+ # SB is deliberately GLOBAL: the EXIT trap that removes it fires after this
317
+ # function has returned, and a `local` SB is unbound by then — which under
318
+ # `set -u` made a fully green run end on "SB: unbound variable".
319
+ SB="$(mktemp -d "${TMPDIR:-/tmp}/gate-watchdog-check.XXXXXX")"
320
+ trap 'rm -rf "$SB"' EXIT
321
+ ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; }
322
+ bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fails=$((fails + 1)); }
323
+
324
+ local SELF="${BASH_SOURCE[0]}"
325
+ # Declared up here, not inside C1's else-branch: a fixture that fails to plant
326
+ # would otherwise leave it unset and `set -u` would take H1 down with it.
327
+ local back; back="$(date -v-30M +%Y%m%d%H%M.%S 2>/dev/null || date -d '-30 min' +%Y%m%d%H%M.%S)"
328
+
329
+ # ── the REAL half ──────────────────────────────────────────────────────────
330
+ # A sandbox governor with one slot, held by a live dummy, and a planted
331
+ # standalone holder of `lock-tsc-probe` whose inner gate-run.sh queues for that
332
+ # slot forever. This is govern-order-check.sh's red-proof shape, stripped of
333
+ # tsc: the deadlock, with no typecheck to pay for.
334
+ echo "── C1-C3: a real planted deadlock ──"
335
+ local gsb="$SB/govern"; mkdir -p "$gsb"
336
+ ( sleep 120 ) & local slot_pid=$!
337
+ mkdir -p "$gsb/lock-slot-1"; echo "$slot_pid" > "$gsb/lock-slot-1/pid"
338
+
339
+ # The victim's PARENT — a stand-in for the Claude session's shell. It must
340
+ # still be alive afterwards.
341
+ local plant="$SB/plant.sh"
342
+ cat > "$plant" <<PLANT
343
+ . "$ROOT/.claude/scripts/lib/govern.sh"
344
+ gate_lock tsc-probe 5 || exit 9
345
+ trap 'gate_release_all' EXIT INT TERM
346
+ bash "$ROOT/.claude/scripts/gate-run.sh" planted -- sleep 90
347
+ PLANT
348
+ # The `sleep 300` is what makes C2 meaningful: this shell stands in for the
349
+ # Claude session's zsh, and it must still be here after its child is cut. A
350
+ # bare `( plant ) &` would exit WITH the plant and C2 would pass vacuously.
351
+ GOVERN_DIR="$gsb" GOVERN_MAX_GATES=1 GOVERN_QUEUE_WAIT=120 \
352
+ bash -c "bash '$plant' & sleep 300" >"$SB/plant.log" 2>&1 &
353
+ local parent_pid=$!
354
+
355
+ # Wait for the plant to take the lock and for its gate-run to be queueing.
356
+ local i victim=""
357
+ for i in $(seq 1 40); do
358
+ [ -f "$gsb/lock-tsc-probe/pid" ] && victim="$(tr -d '[:space:]' < "$gsb/lock-tsc-probe/pid")"
359
+ [ -n "$victim" ] && pgrep -P "$victim" >/dev/null 2>&1 && break
360
+ sleep 0.5
361
+ done
362
+ if [ -z "$victim" ]; then
363
+ bad "C1 the plant never took the sandbox lock — fixture broken: $(cat "$SB/plant.log")"
364
+ else
365
+ # Backdate BOTH directories to clear the two floors — mkdir(2) stamped each
366
+ # of them a moment ago. The slot carries the jam's clock; the lock only has
367
+ # to be older than the handoff floor.
368
+ touch -t "$back" "$gsb/lock-tsc-probe" "$gsb/lock-slot-1"
369
+ local out
370
+ out="$(GOVERN_DIR="$gsb" bash "$SELF" --once --dry-run 2>&1)"
371
+ if grep -q "WOULD cut pid=$victim " <<<"$out"; then
372
+ ok "C1a the planted standalone lock holder is NAMED ($victim)"
373
+ else
374
+ bad "C1a the deadlock was NOT seen. got: ${out:-<silence>}"
375
+ fi
376
+ out="$(GOVERN_DIR="$gsb" GATE_WATCHDOG_CONFIRM=1 GATE_WATCHDOG_NO_REAP=1 \
377
+ bash "$SELF" --once 2>&1)"
378
+ sleep 1
379
+ if kill -0 "$victim" 2>/dev/null; then
380
+ bad "C1b the victim survived the cut — the jam is still there. got: $out"
381
+ else
382
+ ok "C1b the victim is cut"
383
+ fi
384
+ if [ -d "$gsb/lock-tsc-probe" ]; then
385
+ bad "C1c the lock was NOT released — cutting the holder bought nothing"
386
+ else
387
+ ok "C1c the lock is released — it can now fall to a waiter that holds a slot"
388
+ fi
389
+ if kill -0 "$parent_pid" 2>/dev/null; then
390
+ ok "C2 the victim's PARENT shell survived (no process-group kill)"
391
+ else
392
+ bad "C2 the parent shell died — this would kill a Claude session"
393
+ fi
394
+ if kill -0 "$slot_pid" 2>/dev/null; then
395
+ ok "C3 the slot owner survived — a slot owner is never the victim"
396
+ else
397
+ bad "C3 the SLOT OWNER was killed — that throws away a whole verify run"
398
+ fi
399
+ fi
400
+ # The shell announces a terminated background job on ITS stderr, at `wait`.
401
+ # Redirect the whole block, not each kill, or a green run ends in two lines of
402
+ # "Terminated: 15" that read like a failure.
403
+ { pkill -P "$parent_pid" 2>/dev/null
404
+ kill -TERM "$parent_pid" "$slot_pid" 2>/dev/null
405
+ wait "$parent_pid"; wait "$slot_pid"; } >/dev/null 2>&1
406
+
407
+ # ── H1: the hook's jam path ────────────────────────────────────────────────
408
+ # The SILENT path is easy to prove and easy to get wrong in the other
409
+ # direction: a typo in session-start's grep anchor means the watchdog breaks
410
+ # jams forever and nobody ever sees a line — the class of defect § 5 of
411
+ # text/health-docs.md is entirely about. So plant a SECOND deadlock and drive
412
+ # the real hook against it.
413
+ #
414
+ # The hook also runs gate-reaper.sh --once against the real process table.
415
+ # That is exactly what every session start already does, it is ppid==1 + gate
416
+ # shape + a 15-minute floor, and gate-reaper-check.sh proves it spares live
417
+ # work — so it is left alone rather than stubbed.
418
+ echo "── H1: hooks/session-start.sh reports a break ──"
419
+ local hook="$ROOT/.claude/hooks/session-start.sh"
420
+ if [ ! -f "$hook" ]; then
421
+ bad "H1 session-start.sh is missing — the watchdog is wired to nothing"
422
+ else
423
+ local hsb="$SB/hookgov" hv="" h_parent h_slot hout
424
+ mkdir -p "$hsb"
425
+ ( sleep 120 ) & h_slot=$!
426
+ mkdir -p "$hsb/lock-slot-1"; echo "$h_slot" > "$hsb/lock-slot-1/pid"
427
+ GOVERN_DIR="$hsb" GOVERN_MAX_GATES=1 GOVERN_QUEUE_WAIT=120 \
428
+ bash -c "bash '$plant' & sleep 300" >"$SB/plant2.log" 2>&1 &
429
+ h_parent=$!
430
+ for i in $(seq 1 40); do
431
+ [ -f "$hsb/lock-tsc-probe/pid" ] && hv="$(tr -d '[:space:]' < "$hsb/lock-tsc-probe/pid")"
432
+ [ -n "$hv" ] && pgrep -P "$hv" >/dev/null 2>&1 && break
433
+ sleep 0.5
434
+ done
435
+ if [ -z "$hv" ]; then
436
+ bad "H1 the second plant never took the lock — fixture broken: $(cat "$SB/plant2.log")"
437
+ else
438
+ touch -t "$back" "$hsb/lock-tsc-probe" "$hsb/lock-slot-1"
439
+ hout="$(CLAUDE_PROJECT_DIR="$ROOT" GOVERN_DIR="$hsb" GATE_WATCHDOG_CONFIRM=1 \
440
+ GATE_WATCHDOG_NO_REAP=1 ONE_API_URL="http://127.0.0.1:1" \
441
+ bash "$hook" 2>/dev/null)"
442
+ grep -q "broke 1 governor deadlock" <<<"$hout" \
443
+ && ok "H1 the hook SAYS it broke the jam — its grep anchor matches say()" \
444
+ || bad "H1 the hook was SILENT about a real break — anchor drift. got: ${hout:-<silence>}"
445
+ fi
446
+ { pkill -P "$h_parent" 2>/dev/null
447
+ kill -TERM "$h_parent" "$h_slot" 2>/dev/null
448
+ wait "$h_parent"; wait "$h_slot"; } >/dev/null 2>&1
449
+ fi
450
+
451
+ # ── the MUST-NOT-CUT half, on fixtures ─────────────────────────────────────
452
+ # A stubbed `ps` on PATH is the whole machine here, exactly as
453
+ # gate-reaper-check.sh does it: no load is spawned and no real pid is read.
454
+ echo "── C4-C8: shapes that must NEVER be cut ──"
455
+ mkdir -p "$SB/bin"
456
+ cat > "$SB/bin/ps" <<'PSSTUB'
457
+ #!/usr/bin/env bash
458
+ cat "$WATCHDOG_FIXTURE"
459
+ PSSTUB
460
+ chmod +x "$SB/bin/ps"
461
+
462
+ # <case> <dir> — build a sandbox governor + process table for one shape.
463
+ fixture() { # $1 name; stdin = the ps table; $2.. = "lockname:pid" entries
464
+ local name="$1"; shift
465
+ local dir="$SB/$name"; mkdir -p "$dir"
466
+ cat > "$SB/$name.ps"
467
+ local e n p
468
+ for e in "$@"; do
469
+ n="${e%%:*}"; p="${e##*:}"
470
+ mkdir -p "$dir/$n"; printf '%s' "$p" > "$dir/$n/pid"
471
+ touch -t "$(date -v-30M +%Y%m%d%H%M.%S 2>/dev/null || date -d '-30 min' +%Y%m%d%H%M.%S)" "$dir/$n"
472
+ done
473
+ printf '%s' "$dir"
474
+ }
475
+ run_fixture() { # $1 dir, $2 ps table, rest = extra env
476
+ WATCHDOG_FIXTURE="$2" PATH="$SB/bin:$PATH" GOVERN_DIR="$1" \
477
+ bash "${3:-$SELF}" --once --dry-run 2>&1
478
+ }
479
+
480
+ # The canonical jam, as a process table:
481
+ # 900 gate-run.sh verify-fast — holds slot-1, idle
482
+ # 901 its child, idle
483
+ # 800 tsc-cached.sh app/site — holds lock-tsc-x, NO slot, idle
484
+ # 801 gate-run.sh tsc-cache — its child, queueing for a slot
485
+ # 700 the session shell (parent of 800)
486
+ local d out
487
+
488
+ d="$(fixture c4 "lock-tsc-x:800" <<'T'
489
+ 700 1 0.0 /bin/zsh -c claude-session
490
+ 800 700 0.0 bash .claude/scripts/tsc-cached.sh app/site
491
+ 801 800 0.0 bash .claude/scripts/gate-run.sh tsc-cache:app/site -- bash -c tsc
492
+ T
493
+ )"
494
+ out="$(run_fixture "$d" "$SB/c4.ps")"
495
+ [ -z "$out" ] && ok "C4 healthy box (no slot held) — silent" \
496
+ || bad "C4 named a victim with NO slot held: $out"
497
+
498
+ d="$(fixture c5 "lock-slot-1:900" "lock-tsc-x:800" <<'T'
499
+ 700 1 0.0 /bin/zsh -c claude-session
500
+ 800 700 0.0 bash .claude/scripts/tsc-cached.sh app/site
501
+ 801 800 0.0 bash .claude/scripts/gate-run.sh tsc-cache:app/site -- bash -c tsc
502
+ 900 1 0.0 bash .claude/scripts/gate-run.sh verify-fast -- bash verify-fast.sh
503
+ 901 900 98.4 node vitest run
504
+ T
505
+ )"
506
+ out="$(run_fixture "$d" "$SB/c5.ps")"
507
+ [ -z "$out" ] && ok "C5 merely busy (a slot owner computing) — silent" \
508
+ || bad "C5 would cut a gate while the box is WORKING: $out"
509
+
510
+ d="$(fixture c6 "lock-slot-1:900" "lock-tsc-x:800" <<'T'
511
+ 700 1 0.0 /bin/zsh -c claude-session
512
+ 800 700 0.0 bash .claude/scripts/tsc-cached.sh app/site
513
+ 801 800 0.0 bash .claude/scripts/gate-run.sh tsc-cache:app/site -- bash -c tsc
514
+ 900 1 0.0 bash .claude/scripts/gate-run.sh verify-fast -- bash verify-fast.sh
515
+ T
516
+ )"
517
+ touch "$d/lock-slot-1" # the SLOT back to NOW — the jam is young
518
+ out="$(run_fixture "$d" "$SB/c6.ps")"
519
+ [ -z "$out" ] && ok "C6 young jam (slot held under the ${STALL_MIN}m floor) — silent" \
520
+ || bad "C6 cut while the queue had barely started: $out"
521
+
522
+ # C6b — the handoff. The jam is old, but THIS holder took the lock two seconds
523
+ # ago. During the live 2026-09-13 jam the lock changed hands every ~9 minutes;
524
+ # a holder that has not yet had time to be stuck is not the one to cut.
525
+ d="$(fixture c6b "lock-slot-1:900" "lock-tsc-x:800" <<'T'
526
+ 700 1 0.0 /bin/zsh -c claude-session
527
+ 800 700 0.0 bash .claude/scripts/tsc-cached.sh app/site
528
+ 801 800 0.0 bash .claude/scripts/gate-run.sh tsc-cache:app/site -- bash -c tsc
529
+ 900 1 0.0 bash .claude/scripts/gate-run.sh verify-fast -- bash verify-fast.sh
530
+ T
531
+ )"
532
+ touch "$d/lock-tsc-x" # the LOCK back to NOW — a fresh handoff
533
+ out="$(run_fixture "$d" "$SB/c6b.ps")"
534
+ [ -z "$out" ] && ok "C6b a lock taken seconds ago (a handoff) — silent" \
535
+ || bad "C6b cut a holder that had just taken the lock: $out"
536
+
537
+ d="$(fixture c7 "lock-slot-1:900" "lock-tsc-x:901" <<'T'
538
+ 900 1 0.0 bash .claude/scripts/gate-run.sh verify-fast -- bash verify-fast.sh
539
+ 901 900 0.0 bash .claude/scripts/tsc-cached.sh app/site
540
+ 902 901 0.0 bash .claude/scripts/gate-run.sh tsc-cache:app/site -- bash -c tsc
541
+ T
542
+ )"
543
+ out="$(run_fixture "$d" "$SB/c7.ps")"
544
+ [ -z "$out" ] && ok "C7 the in-gate lock holder (innocent side) — silent" \
545
+ || bad "C7 would cut the SLOT-then-LOCK side, costing a verify run: $out"
546
+
547
+ d="$(fixture c8 "lock-slot-1:900" "lock-tsc-x:800" <<'T'
548
+ 700 1 0.0 /bin/zsh -c claude-session
549
+ 800 700 0.0 bash .claude/scripts/tsc-cached.sh app/site
550
+ 900 1 0.0 bash .claude/scripts/gate-run.sh verify-fast -- bash verify-fast.sh
551
+ T
552
+ )"
553
+ out="$(run_fixture "$d" "$SB/c8.ps")"
554
+ [ -z "$out" ] && ok "C8 a quiet lock holder NOT in the slot queue — silent" \
555
+ || bad "C8 cut a process that was merely quiet: $out"
556
+
557
+ # ── RED PROOFS ─────────────────────────────────────────────────────────────
558
+ echo "── R1-R4: the checks have teeth ──"
559
+ local GUT1="$SB/gutted-clause3.sh"
560
+ sed 's/^ \[ -n "\$kids_gr" \] ||.*$/ kids_gr="${kids_gr:-1}"/' "$SELF" > "$GUT1"
561
+ if ! grep -q 'kids_gr="${kids_gr:-1}"' "$GUT1"; then
562
+ bad "R1 could not gut clause (3) — the anchor moved and C8 is blind"
563
+ else
564
+ out="$(run_fixture "$SB/c8" "$SB/c8.ps" "$GUT1")"
565
+ grep -q "WOULD cut pid=800 " <<<"$out" \
566
+ && ok "R1 red-proof: without clause (3) the quiet holder IS cut, so C8 bites" \
567
+ || bad "R1 the gutted watchdog still spared 800 — C8 proves nothing: ${out:-<silence>}"
568
+ fi
569
+
570
+ local GUT2="$SB/gutted-clause4b.sh"
571
+ sed 's/^ \[ "\$busy_slot" = "0" \] || return 1$/ busy_slot=0/' "$SELF" > "$GUT2"
572
+ if ! grep -q '^ busy_slot=0$' "$GUT2"; then
573
+ bad "R2 could not gut clause (4b) — the anchor moved and C5 is blind"
574
+ else
575
+ out="$(run_fixture "$SB/c5" "$SB/c5.ps" "$GUT2")"
576
+ grep -q "WOULD cut pid=800 " <<<"$out" \
577
+ && ok "R2 red-proof: without clause (4b) a WORKING box is cut, so C5 bites" \
578
+ || bad "R2 the gutted watchdog still spared a busy box — C5 proves nothing: ${out:-<silence>}"
579
+ fi
580
+
581
+ local GUT3="$SB/gutted-clause5.sh"
582
+ sed '/^ if \[ "\$oldest" -lt/ s/.*/ if false; then/' "$SELF" > "$GUT3"
583
+ if ! grep -q '^ if false; then$' "$GUT3"; then
584
+ bad "R3 could not gut clause (5) — the anchor moved and C6 is blind"
585
+ else
586
+ out="$(run_fixture "$SB/c6" "$SB/c6.ps" "$GUT3")"
587
+ grep -q "WOULD cut pid=800 " <<<"$out" \
588
+ && ok "R3 red-proof: without the slot clock a JUST-STARTED queue is cut, so C6 bites" \
589
+ || bad "R3 the gutted watchdog still spared a young jam — C6 proves nothing: ${out:-<silence>}"
590
+ fi
591
+
592
+ # R4 is the one that nearly went missing. C7's fixture has ONE non-slot lock,
593
+ # so after clause (2) skips the in-gate holder the loop simply runs out of
594
+ # candidates — C7 would stay green with clause (2) deleted, and clause (2) is
595
+ # the whole of what stands between this watchdog and cutting a process inside
596
+ # somebody's running verify. Gut it and C7 must start naming 901.
597
+ local GUT4="$SB/gutted-clause2.sh"
598
+ sed '/^ if _holds_a_slot "\$p" || _ancestry_has_gate_run "\$p"; then$/ s/.*/ if false; then/' "$SELF" > "$GUT4"
599
+ if ! grep -q '^ if false; then$' "$GUT4"; then
600
+ bad "R4 could not gut clause (2) — the anchor moved and C7 is blind"
601
+ else
602
+ out="$(run_fixture "$SB/c7" "$SB/c7.ps" "$GUT4")"
603
+ grep -q "WOULD cut pid=901 " <<<"$out" \
604
+ && ok "R4 red-proof: without clause (2) the IN-GATE holder is cut, so C7 bites" \
605
+ || bad "R4 the gutted watchdog still spared 901 — C7 is vacuous and clause (2) is untested: ${out:-<silence>}"
606
+ fi
607
+
608
+ echo
609
+ if [ "$fails" -eq 0 ]; then
610
+ echo "ALL GREEN — it breaks the planted cycle and cuts nothing on a healthy, busy, young or innocent box"
611
+ return 0
612
+ fi
613
+ echo "RED — $fails check(s) failed"
614
+ return 1
615
+ }
616
+
617
+ if [ "$SELFTEST" = "1" ]; then self_test; exit $?; fi
618
+ if [ "$ONCE" = "1" ]; then sweep; exit 0; fi
619
+ while :; do sweep; sleep "$INTERVAL"; done