@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.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/abm-strategist.md +67 -1
- package/agents/ads-meta.md +67 -1
- package/agents/analyst.md +67 -1
- package/agents/animator.md +108 -0
- package/agents/architect.md +269 -20
- package/agents/brand-guardian.md +67 -1
- package/agents/brand-strategist.md +67 -1
- package/agents/campaign-content.md +67 -1
- package/agents/campaign-email.md +67 -1
- package/agents/campaign-sms.md +67 -1
- package/agents/campaign-social.md +67 -1
- package/agents/cco.md +83 -2
- package/agents/ceo.md +108 -11
- package/agents/chairman.md +197 -0
- package/agents/cmo.md +82 -2
- package/agents/community-greeter.md +67 -1
- package/agents/community-moderator.md +67 -1
- package/agents/compliance.md +67 -1
- package/agents/copywriter.md +67 -1
- package/agents/creative-strategist.md +67 -1
- package/agents/cro.md +81 -1
- package/agents/cto.md +266 -28
- package/agents/customer-interviewer.md +67 -1
- package/agents/customer-researcher.md +67 -1
- package/agents/customer-success-manager.md +67 -1
- package/agents/customer-trainer.md +67 -1
- package/agents/cxo.md +82 -1
- package/agents/demand-creator.md +67 -1
- package/agents/demo-mover.md +67 -1
- package/agents/demo-specialist.md +67 -1
- package/agents/demo-thai-family-law.md +67 -1
- package/agents/designer.md +67 -1
- package/agents/discovery-caller.md +67 -1
- package/agents/doctor.md +269 -0
- package/agents/educate-coach.md +67 -1
- package/agents/elevate-tutor.md +67 -1
- package/agents/email-lifecycle-marketer.md +67 -1
- package/agents/engage-specialist.md +67 -1
- package/agents/events-coordinator.md +67 -1
- package/agents/foundation-builder.md +67 -1
- package/agents/funnel-architect.md +67 -1
- package/agents/gift-creator.md +67 -1
- package/agents/google-ads.md +67 -1
- package/agents/guide.md +67 -1
- package/agents/helpdesk-dispatcher.md +67 -1
- package/agents/hook-specialist.md +67 -1
- package/agents/identify-optimizer.md +67 -1
- package/agents/implementer.md +313 -45
- package/agents/incident-commander.md +67 -1
- package/agents/insights-lead.md +87 -1
- package/agents/journey-runner.md +67 -1
- package/agents/linkedin-ads.md +67 -1
- package/agents/live-sales-chat.md +67 -1
- package/agents/market-researcher.md +67 -1
- package/agents/media-buyer.md +67 -1
- package/agents/memory-keeper.md +195 -0
- package/agents/movers-customer-researcher.md +67 -1
- package/agents/movers-foundation-builder.md +67 -1
- package/agents/movers-market-researcher.md +67 -1
- package/agents/movers-pricing-strategist.md +67 -1
- package/agents/nurture-architect.md +67 -1
- package/agents/offer-architect.md +67 -1
- package/agents/onboarder.md +67 -1
- package/agents/onboarding-specialist.md +67 -1
- package/agents/operations-dashboard.md +87 -1
- package/agents/perf-engineer.md +333 -37
- package/agents/playbook-writer.md +67 -1
- package/agents/plg-strategist.md +67 -1
- package/agents/positioning-architect.md +67 -1
- package/agents/press-officer.md +67 -1
- package/agents/pricing-strategist.md +67 -1
- package/agents/privacy-officer.md +67 -1
- package/agents/referral-manager.md +67 -1
- package/agents/refine-analyst.md +67 -1
- package/agents/release-manager.md +446 -39
- package/agents/renewals-upsell-rep.md +67 -1
- package/agents/review-engineer.md +319 -45
- package/agents/rewards-steward.md +67 -1
- package/agents/sales-call-coach.md +67 -1
- package/agents/sales-closer.md +67 -1
- package/agents/security-auditor.md +343 -48
- package/agents/sell-closer.md +67 -1
- package/agents/share-amplifier.md +67 -1
- package/agents/social-media-manager.md +67 -1
- package/agents/storyteller.md +301 -0
- package/agents/strategist.md +67 -1
- package/agents/strategy-aligner.md +67 -1
- package/agents/support-agent.md +67 -1
- package/agents/tagger.md +327 -0
- package/agents/tech-writer.md +195 -22
- package/agents/test-engineer.md +398 -29
- package/agents/tiktok-ads.md +67 -1
- package/agents/tracking-engineer.md +67 -1
- package/agents/trailkeeper.md +181 -0
- package/agents/upsell-strategist.md +67 -1
- package/agents/voice.md +67 -1
- package/agents/w1-recon.md +1 -1
- package/agents/w2-decide.md +1 -1
- package/agents/w3-edit.md +8 -2
- package/agents/w4-verify.md +13 -0
- package/agents/workflow-optimiser.md +81 -1
- package/commands/close.md +916 -160
- package/commands/deploy.md +102 -724
- package/commands/do.md +58 -2
- package/commands/sweep.md +159 -0
- package/commands/tasks.md +222 -0
- package/hooks/scripts/dev-only.sh +135 -0
- package/hooks/scripts/git-add-guard.sh +37 -2
- package/hooks/scripts/session-start.sh +32 -4
- package/package.json +1 -1
- package/rules/scripts.md +85 -0
- package/scripts/CLAUDE.md +315 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agent-actor-parity.sh +129 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/chrome.mjs +18 -0
- package/scripts/close-metrics.sh +587 -0
- package/scripts/close-owner.sh +326 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-emit.sh +311 -0
- package/scripts/deploy-gate-check.sh +155 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy-schema-check.sh +58 -0
- package/scripts/deploy.sh +393 -243
- package/scripts/do-auto.sh +127 -26
- package/scripts/do-board.sh +429 -0
- package/scripts/do-close.sh +1184 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-fleet.sh +8 -2
- package/scripts/do-plan-json.mjs +110 -12
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +86 -10
- package/scripts/do-rank.py +200 -3
- package/scripts/do-reconcile.sh +73 -12
- package/scripts/do-signal.sh +101 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-w4-gates.sh +11 -1
- package/scripts/do-world-check.sh +153 -0
- package/scripts/download-stats.sh +172 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +18 -1
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +71 -0
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/flywheel-outcome.sh +63 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +9 -0
- package/scripts/gate-watchdog.sh +619 -0
- package/scripts/gc-content-check.sh +142 -0
- package/scripts/gh-traffic-capture.sh +153 -0
- package/scripts/govern-order-check.sh +202 -0
- package/scripts/governor-doors-check.sh +86 -5
- package/scripts/health.sh +448 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/incident.sh +212 -0
- package/scripts/land.sh +755 -45
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/livekit-ratchet.sh +18 -1
- package/scripts/machine-check.sh +1 -1
- package/scripts/memory-index-budget.sh +79 -0
- package/scripts/npm-downloads.sh +109 -0
- package/scripts/one-agents.mjs +204 -8
- package/scripts/one-resume.sh +31 -3
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/redirect-lint.sh +169 -0
- package/scripts/release.sh +40 -6
- package/scripts/resume-lost-sessions.sh +68 -0
- package/scripts/shoot-pages.mjs +140 -0
- package/scripts/signal-meta-backfill.ts +451 -0
- package/scripts/signal-watch.sh +63 -6
- package/scripts/speed-cache-check.sh +12 -2
- package/scripts/sweep.sh +426 -0
- package/scripts/task-titles-dump.ts +101 -0
- package/scripts/test-cached.sh +47 -10
- package/scripts/test-lanes.sh +14 -0
- package/scripts/thread-name-backfill.ts +215 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +155 -8
- package/scripts/typedb-flake-check.sh +3 -1
- package/scripts/urls-lint.sh +8 -0
- package/scripts/verify-board-doors.sh +80 -0
- package/scripts/verify-fast.sh +159 -6
- package/scripts/worktree-up.sh +21 -3
- package/skills/astro/SKILL.md +9 -3
- package/skills/astro/optimize-performance.md +3 -2
- package/skills/cloudflare/SKILL.md +3 -2
- package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
- package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
- package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
- package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
- package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
- package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
- package/skills/cloudflare-security-audit/HUNTING.md +251 -0
- package/skills/cloudflare-security-audit/LICENSE +21 -0
- package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
- package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
- package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
- package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
- package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
- package/skills/cloudflare-security-audit/SKILL.md +192 -0
- package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
- package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
- package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
- package/skills/cloudflare-security-audit/report-schema.json +461 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
- package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
- package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
- package/skills/deploy/REFERENCE.md +713 -0
- package/skills/deploy/SKILL.md +140 -0
- package/skills/fleet-audit/SKILL.md +58 -0
- package/skills/meeting/SKILL.md +220 -0
- package/skills/planning/SKILL.md +256 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/typedb/SKILL.md +7 -0
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/templates/template-agent.md +7 -1
- package/templates/template-feature.md +9 -0
- 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
|