@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,448 @@
|
|
|
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
|
+
# Walk REAL descendants via ppid — never signal a process GROUP. A pgid can be
|
|
201
|
+
# shared with an ancestor shell/session when a child is spawned without
|
|
202
|
+
# setsid(2) (tsserver commonly is), and `kill -TERM -$pgid` then kills
|
|
203
|
+
# everything sharing that group, session included. Measured 2026-09-15: this
|
|
204
|
+
# exact line took down 3 of 6 live `claude` sessions whose tsserver children
|
|
205
|
+
# happened to share the launching shell's process group. ppid is the only
|
|
206
|
+
# honest boundary — it can never reach outside the tree rooted at $pid.
|
|
207
|
+
_kill_tree() { # pid -> TERM pid and every process/pid rooted at pid, leaves ancestors untouched
|
|
208
|
+
local pid="$1" child
|
|
209
|
+
for child in $(pgrep -P "$pid" 2>/dev/null); do
|
|
210
|
+
_kill_tree "$child"
|
|
211
|
+
done
|
|
212
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
_reap() { # pid why
|
|
216
|
+
local pid="$1" why="$2" rss
|
|
217
|
+
rss=$(( $(ps -o rss= -p "$pid" 2>/dev/null | tr -d ' ' || echo 0) / 1024 ))
|
|
218
|
+
if [ "$DRY" = "1" ]; then printf ' %sWOULD%s reclaim pid=%-7s %5sMB %s\n' "$C_WARN" "$C_OFF" "$pid" "$rss" "$why"; return 0; fi
|
|
219
|
+
_kill_tree "$pid"
|
|
220
|
+
printf ' %sreclaimed%s pid=%-7s %5sMB %s\n' "$C_OK" "$C_OFF" "$pid" "$rss" "$why"
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# A governor slot owner is live work by definition — it is holding the lock the
|
|
224
|
+
# whole box queues behind.
|
|
225
|
+
_holds_slot() {
|
|
226
|
+
local pid="$1" d
|
|
227
|
+
for d in "${TMPDIR:-/tmp}/one-govern"/lock-slot-*; do
|
|
228
|
+
[ -d "$d" ] || continue
|
|
229
|
+
[ "$(cat "$d/pid" 2>/dev/null)" = "$pid" ] && return 0
|
|
230
|
+
done
|
|
231
|
+
return 1
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Is any live claude session cwd'd inside this worktree? lsof on the session pid
|
|
235
|
+
# is the only honest test — a worktree path in a command line proves nothing.
|
|
236
|
+
_worktree_has_session() {
|
|
237
|
+
local wt="$1" pid
|
|
238
|
+
for pid in $(ps -Ao pid=,command= 2>/dev/null | awk '$2 ~ /claude$/ || / claude --/ {print $1}'); do
|
|
239
|
+
lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | grep -q "^n${wt}" && return 0
|
|
240
|
+
done
|
|
241
|
+
return 1
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fix_orphans() { # delegated whole — this file must not own a second reaper
|
|
245
|
+
[ -x "$HERE/gate-reaper.sh" ] || return 0
|
|
246
|
+
local out
|
|
247
|
+
if [ "$DRY" = "1" ]; then out=$(bash "$HERE/gate-reaper.sh" --once --dry-run 2>/dev/null)
|
|
248
|
+
else out=$(bash "$HERE/gate-reaper.sh" --once 2>/dev/null); fi
|
|
249
|
+
[ -n "$out" ] && printf '%s\n' "$out" | sed 's/^/ /'
|
|
250
|
+
return 0
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fix_worktree_lsp() {
|
|
254
|
+
local pid root rss
|
|
255
|
+
for pid in $(pgrep -f 'tsserver.js|typescript/lib/tsserver' 2>/dev/null); do
|
|
256
|
+
_holds_slot "$pid" && continue
|
|
257
|
+
root=$(lsof -nP -p "$pid" 2>/dev/null | grep -oE "${REPO_ROOT}/\.claude/worktrees/[^/]+" | head -1)
|
|
258
|
+
[ -n "$root" ] || continue
|
|
259
|
+
_reap "$pid" "language server indexing $(basename "$root") — the editor config excludes worktrees"
|
|
260
|
+
done
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fix_dead_dev_servers() {
|
|
264
|
+
# Resolve each dev server by its CWD, never by string-matching its argv. The
|
|
265
|
+
# first draft grepped pids and worktree paths out of one ps stream and paired
|
|
266
|
+
# them with `paste - -`; when a server's argv carried no worktree path the
|
|
267
|
+
# columns silently shifted and it paired one pid with ANOTHER pid. A kill list
|
|
268
|
+
# built by that is a coin flip, so cwd it is.
|
|
269
|
+
local pid cwd wt
|
|
270
|
+
for pid in $(pgrep -f 'bin/astro dev' 2>/dev/null); do
|
|
271
|
+
_holds_slot "$pid" && continue
|
|
272
|
+
cwd=$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -1)
|
|
273
|
+
case "$cwd" in
|
|
274
|
+
"$REPO_ROOT"/.claude/worktrees/*) ;;
|
|
275
|
+
*) continue ;;
|
|
276
|
+
esac
|
|
277
|
+
wt="${cwd#"$REPO_ROOT"/.claude/worktrees/}"; wt="$REPO_ROOT/.claude/worktrees/${wt%%/*}"
|
|
278
|
+
_worktree_has_session "$wt" && continue
|
|
279
|
+
_reap "$pid" "dev server in $(basename "$wt") — no live session is cwd'd there"
|
|
280
|
+
done
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
run_fix() {
|
|
284
|
+
printf '%s\n' "── reclaiming ──────────────────────────────────"
|
|
285
|
+
[ "$DRY" = "1" ] && printf ' %sdry run — nothing will be killed%s\n' "$C_DIM" "$C_OFF"
|
|
286
|
+
local before after
|
|
287
|
+
before=$(swap_free_mb)
|
|
288
|
+
fix_orphans
|
|
289
|
+
fix_worktree_lsp
|
|
290
|
+
fix_dead_dev_servers
|
|
291
|
+
sleep 2
|
|
292
|
+
after=$(swap_free_mb)
|
|
293
|
+
printf ' %sswap free %sM -> %sM%s\n\n' "$C_DIM" "$before" "$after" "$C_OFF"
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# ── self-test ─────────────────────────────────────────────────────────────────
|
|
297
|
+
# Proves the two things that can silently rot: the verdict thresholds, and the
|
|
298
|
+
# probe's willingness to call a 200 that lacks its assert DEGRADED rather than UP.
|
|
299
|
+
self_test() {
|
|
300
|
+
local fails=0
|
|
301
|
+
ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; }
|
|
302
|
+
bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fails=$((fails + 1)); }
|
|
303
|
+
echo "── health.sh: the verdict bites, and a bad 200 is not UP ──"
|
|
304
|
+
|
|
305
|
+
cores() { echo 10; }; gate_headroom() { echo 4; }; orphan_count() { echo 0; }
|
|
306
|
+
load1() { echo 2.0; }; swap_free_mb() { echo 4000; }
|
|
307
|
+
[ "$(box_verdict)" != "${_x:-}" ] && case "$(box_verdict)" in HEALTHY*) ok "C1 quiet box reads HEALTHY";; *) bad "C1 quiet box: $(box_verdict)";; esac
|
|
308
|
+
|
|
309
|
+
load1() { echo 25.0; }
|
|
310
|
+
case "$(box_verdict)" in UNHEALTHY*) ok "C2 load over 2x cores reads UNHEALTHY";; *) bad "C2 got: $(box_verdict)";; esac
|
|
311
|
+
|
|
312
|
+
load1() { echo 2.0; }; swap_free_mb() { echo 100; }
|
|
313
|
+
case "$(box_verdict)" in UNHEALTHY*) ok "C3 swap under 512MB free reads UNHEALTHY";; *) bad "C3 got: $(box_verdict)";; esac
|
|
314
|
+
|
|
315
|
+
swap_free_mb() { echo 4000; }; orphan_count() { echo 3; }
|
|
316
|
+
case "$(box_verdict)" in DEGRADED*orphan*) ok "C4 orphans read DEGRADED and name the fix";; *) bad "C4 got: $(box_verdict)";; esac
|
|
317
|
+
|
|
318
|
+
orphan_count() { echo 0; }; gate_headroom() { echo 1; }
|
|
319
|
+
case "$(box_verdict)" in DEGRADED*funds*) ok "C5 headroom of 1 reads DEGRADED";; *) bad "C5 got: $(box_verdict)";; esac
|
|
320
|
+
|
|
321
|
+
# A 200 whose body lost its assert is the failure deploy.sh's probe exists to
|
|
322
|
+
# catch — a worker that boots and serves the wrong app.
|
|
323
|
+
curl() { printf 'not the droid\n200'; }
|
|
324
|
+
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
|
|
325
|
+
curl() { printf '{"status":"ok"}\n200'; }
|
|
326
|
+
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
|
|
327
|
+
|
|
328
|
+
# ── the --fix rules: each candidate must be dead by a test live work fails ──
|
|
329
|
+
# _reap is replaced by a recorder, so nothing is killed and the LIST is the
|
|
330
|
+
# assertion. That list is the whole safety argument of --fix.
|
|
331
|
+
REAPED=""
|
|
332
|
+
_reap() { REAPED="$REAPED $1"; }
|
|
333
|
+
REPO_ROOT="/repo"
|
|
334
|
+
|
|
335
|
+
pgrep() { echo 4242; }
|
|
336
|
+
lsof() { echo "n/repo/.claude/worktrees/wt-a/app/web"; } # generic on purpose: a portable script must name no monorepo path
|
|
337
|
+
_holds_slot() { return 1; }
|
|
338
|
+
_worktree_has_session() { return 1; }
|
|
339
|
+
|
|
340
|
+
REAPED=""; fix_worktree_lsp
|
|
341
|
+
case "$REAPED" in *4242*) ok "F1 a language server rooted in a worktree is a candidate";; *) bad "F1 missed it: '$REAPED'";; esac
|
|
342
|
+
|
|
343
|
+
_holds_slot() { return 0; }
|
|
344
|
+
REAPED=""; fix_worktree_lsp
|
|
345
|
+
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
|
|
346
|
+
_holds_slot() { return 1; }
|
|
347
|
+
|
|
348
|
+
REAPED=""; fix_dead_dev_servers
|
|
349
|
+
case "$REAPED" in *4242*) ok "F3 a dev server in a session-less worktree is a candidate";; *) bad "F3 missed it: '$REAPED'";; esac
|
|
350
|
+
|
|
351
|
+
_worktree_has_session() { return 0; }
|
|
352
|
+
REAPED=""; fix_dead_dev_servers
|
|
353
|
+
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
|
|
354
|
+
_worktree_has_session() { return 1; }
|
|
355
|
+
|
|
356
|
+
# A dev server in the MAIN tree is never a candidate — the cwd case only
|
|
357
|
+
# admits paths under .claude/worktrees/.
|
|
358
|
+
lsof() { echo "n/repo/app/web"; }
|
|
359
|
+
REAPED=""; fix_dead_dev_servers
|
|
360
|
+
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
|
|
361
|
+
|
|
362
|
+
# --dry-run must not kill. Restore the real _reap and prove its dry path never
|
|
363
|
+
# reaches kill by stubbing kill itself as a recorder.
|
|
364
|
+
unset -f _reap 2>/dev/null || true
|
|
365
|
+
eval "$(sed -n '/^_reap() {/,/^}/p' "${BASH_SOURCE[0]}")"
|
|
366
|
+
KILLED=""; kill() { KILLED="$KILLED $*"; }
|
|
367
|
+
DRY=1; out=$(_reap 4242 "stub");
|
|
368
|
+
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
|
|
369
|
+
unset -f kill 2>/dev/null || true
|
|
370
|
+
|
|
371
|
+
# The red proofs below run the file AGAINST ITSELF. Without this guard the
|
|
372
|
+
# gutted child runs them too, and each child spawns two more — the first
|
|
373
|
+
# version of R2 died on `mktemp: File exists`, which was the recursion, not
|
|
374
|
+
# the template.
|
|
375
|
+
if [ "${HEALTH_SELFTEST_CHILD:-0}" = "1" ]; then
|
|
376
|
+
if [ $fails -eq 0 ]; then echo "ALL GREEN (child)"; return 0; fi
|
|
377
|
+
echo "RED — $fails check(s) failed"; return 1
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
# RED PROOF — take the REAL file, delete the swap arm, and run its OWN
|
|
381
|
+
# self-test. C3 must fail there. Stubbing box_verdict to a constant would
|
|
382
|
+
# prove nothing about the arm; this proves the assertion is load-bearing.
|
|
383
|
+
local gutted anchor_lines out
|
|
384
|
+
gutted="$(mktemp "${TMPDIR:-/tmp}/health-gutted.XXXXXX")"
|
|
385
|
+
grep -v "swapfree:-""9999" "${BASH_SOURCE[0]}" > "$gutted"
|
|
386
|
+
anchor_lines=$(( $(wc -l < "${BASH_SOURCE[0]}") - $(wc -l < "$gutted") ))
|
|
387
|
+
if [ "$anchor_lines" -ne 1 ]; then
|
|
388
|
+
bad "R1 could not remove the swap arm — anchor moved, this checker is blind"
|
|
389
|
+
else
|
|
390
|
+
out=$(HEALTH_SELFTEST_CHILD=1 bash "$gutted" --self-test 2>&1)
|
|
391
|
+
case "$out" in
|
|
392
|
+
*"FAIL"*"C3"*) ok "R1 red-proof: with the swap arm deleted, C3 goes red" ;;
|
|
393
|
+
*) bad "R1 red-proof did not bite — gutted script still passed C3" ;;
|
|
394
|
+
esac
|
|
395
|
+
fi
|
|
396
|
+
rm -f "$gutted"
|
|
397
|
+
|
|
398
|
+
# RED PROOF for the fix rules — delete the governor-slot guard from the real
|
|
399
|
+
# file and require F2 to go red there. F1..F6 passing against a build that
|
|
400
|
+
# cannot spare live work would be worthless.
|
|
401
|
+
local gutted2 removed out2 guard
|
|
402
|
+
gutted2="$(mktemp "${TMPDIR:-/tmp}/health-gutted2.XXXXXX")"
|
|
403
|
+
# Assembled with printf so this line is not itself the anchor it deletes.
|
|
404
|
+
guard=$(printf '_holds_slot "$%s" && continue' pid)
|
|
405
|
+
grep -vF "$guard" "${BASH_SOURCE[0]}" > "$gutted2"
|
|
406
|
+
removed=$(( $(wc -l < "${BASH_SOURCE[0]}") - $(wc -l < "$gutted2") ))
|
|
407
|
+
if [ "$removed" -ne 2 ]; then
|
|
408
|
+
bad "R2 could not remove the slot guard (removed ${removed} lines, expected 2) — anchor moved, this checker is blind"
|
|
409
|
+
else
|
|
410
|
+
out2=$(HEALTH_SELFTEST_CHILD=1 bash "$gutted2" --self-test 2>&1)
|
|
411
|
+
case "$out2" in
|
|
412
|
+
*"FAIL"*"F2"*) ok "R2 red-proof: with the slot guard deleted, F2 goes red" ;;
|
|
413
|
+
*) bad "R2 red-proof did not bite — gutted --fix still spared a slot holder" ;;
|
|
414
|
+
esac
|
|
415
|
+
fi
|
|
416
|
+
rm -f "$gutted2"
|
|
417
|
+
|
|
418
|
+
# F8 / R3 — _reap must never signal a process GROUP. Measured 2026-09-15: a
|
|
419
|
+
# negative-pid kill of a shared pgid killed 3 live claude sessions whose
|
|
420
|
+
# tsserver children shared the launching shell's pgid. Static proof this
|
|
421
|
+
# pattern can never silently return, since the runtime proof (spawn a real
|
|
422
|
+
# shared-pgid tree, reap the child, assert the parent survives) needs a real
|
|
423
|
+
# fork this harness cannot safely stage inside a self-test. Excludes THIS
|
|
424
|
+
# comment block and itself from the scan.
|
|
425
|
+
if grep -vE '^\s*#' "${BASH_SOURCE[0]}" | grep -qE 'kill[[:space:]]+-TERM[[:space:]]+-"'; then
|
|
426
|
+
bad "F8 _reap (or something) still signals a process GROUP with -\"\$pgid\""
|
|
427
|
+
else
|
|
428
|
+
ok "F8 nothing in this file signals a process group — only _kill_tree's ppid walk"
|
|
429
|
+
fi
|
|
430
|
+
|
|
431
|
+
# _kill_tree must walk descendants (ppid) and never widen beyond them.
|
|
432
|
+
REAPED=""
|
|
433
|
+
pgrep() { case "$1" in -P) case "$2" in 4242) echo 4243 ;; 4243) : ;; esac ;; esac; }
|
|
434
|
+
kill() { REAPED="$REAPED $2"; }
|
|
435
|
+
_kill_tree 4242
|
|
436
|
+
case "$REAPED" in *" 4242"*" 4243"*|*" 4243"*" 4242"*) ok "F9 _kill_tree reaches the target and its real child" ;; *) bad "F9 got: '$REAPED'";; esac
|
|
437
|
+
unset -f pgrep kill 2>/dev/null || true
|
|
438
|
+
|
|
439
|
+
if [ $fails -eq 0 ]; then echo "ALL GREEN — the verdict bites and a bad 200 is not UP"; return 0; fi
|
|
440
|
+
echo "RED — $fails check(s) failed"; return 1
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
[ "$SELFTEST" = "1" ] && { self_test; exit $?; }
|
|
444
|
+
|
|
445
|
+
if [ "${WATCH:-0}" != "0" ]; then
|
|
446
|
+
while :; do clear 2>/dev/null || true; date '+%H:%M:%S'; run_once; sleep "$WATCH"; done
|
|
447
|
+
fi
|
|
448
|
+
run_once
|