@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
@@ -17,8 +17,31 @@
17
17
  # hooks and do-signal.sh post to, plus the stream the listeners sit on:
18
18
  #
19
19
  # health GET /api/health the worker answers
20
- # world:announce POST a canary tagged [cc,factory,canary] intake's door.
21
- # Matches nobody by design; what is measured is the wire.
20
+ # world:announce POST a canary tagged [signal-watch-wire] on the
21
+ # MARKETPLACE board intake's door.
22
+ # The wire, and only the wire: zero subscribers matched, so
23
+ # the number is the door's own cost with no fan-out in it.
24
+ # It used to be sent on the caller's own board and the header
25
+ # claimed it "matches nobody by design" — it matched `ceo`,
26
+ # which subscribes to EVERY tag by canon (root CLAUDE.md: the
27
+ # CEO is the default receiver, always listening). Measured
28
+ # 2026-09-08 on prod server-timing, same door, one variable:
29
+ # matched=0 -> data 259/332/401 ms; matched=1 -> data
30
+ # 1535..2066 ms. So ~1.3s per matched subscriber was being
31
+ # read as "the wire". The probe now ASSERTS matched=0 and
32
+ # goes UNRUN if it ever matches — a wire probe that fanned
33
+ # out did not measure the wire, and an unrun door is not a
34
+ # pass. The fan-out's own cost is a separate finding
35
+ # (text/announce-fanout-plan.md), not a budget to widen.
36
+ # BOTH halves are load-bearing and both were measured
37
+ # wrong before: the BOARD (own board -> `ceo`, who
38
+ # follows every tag) and the TAG (`cc` on the marketplace
39
+ # board -> the live `cc-proof-listener`, found 2026-09-08
40
+ # by this very assertion on its first run). Hence a
41
+ # RESERVED tag nobody stakes, on the shared board. If
42
+ # someone ever stakes `signal-watch-wire`, the row goes
43
+ # unrun and says so, rather than quietly reporting a
44
+ # fan-out as the wire.
22
45
  # tasks:everywhere POST {workspace:factory-do} the board read
23
46
  # factory:size POST {paths:[this file]} the size hop
24
47
  # stream GET channels.one.ie/stream/space:vespio time to first byte
@@ -80,9 +103,14 @@ _row() { # lane door verdict wall budget placement phases note
80
103
  case "$3" in pass) PASS=$((PASS+1));; fail) FAIL=$((FAIL+1));; n/a) NA=$((NA+1));; unrun) UNRUN=$((UNRUN+1));; esac
81
104
  }
82
105
 
83
- # door <lane> <name> <method> <url> <payload|-> <budget_ms> <key|->
106
+ # door <lane> <name> <method> <url> <payload|-> <budget_ms> <key|-> [expect|-]
107
+ # `expect` is a fixed string the RESPONSE BODY must contain for the number to
108
+ # mean what the door claims to measure. Absent it, a door can keep answering 200
109
+ # fast while measuring something else entirely — which is exactly how the
110
+ # announce canary reported "the wire" while paying a full fan-out. A miss is
111
+ # UNRUN, never fail and never pass: the door ran, but not the door we named.
84
112
  door() {
85
- local lane="$1" name="$2" method="$3" url="$4" payload="$5" budget key="$7" hdr body out code wall placement phases verdict note=""
113
+ local lane="$1" name="$2" method="$3" url="$4" payload="$5" budget key="$7" expect="${8:--}" hdr body out code wall placement phases verdict note=""
86
114
  budget=$(_budget "$6")
87
115
  hdr=$(mktemp); body=$(mktemp)
88
116
  if [ "$method" = POST ] && [ "$key" = "-" ]; then
@@ -104,6 +132,7 @@ door() {
104
132
  elif [ "$code" != "200" ]; then verdict=fail; note="http $code $(head -c 80 "$body" | tr -d '\n')"
105
133
  elif [ "$method" = POST ] && ! grep -q '"outcome":"result"' "$body"; then verdict=fail; note="refused: $(head -c 100 "$body" | tr -d '\n')"
106
134
  elif [ "$method" = POST ] && grep -q '"ok":false' "$body"; then verdict=unrun; note="refused before the work: $(grep -oE '"error":"[^"]{0,80}' "$body" | head -1 | cut -d'"' -f4)"
135
+ elif [ "$expect" != "-" ] && ! grep -qF "$expect" "$body"; then verdict=unrun; note="not the door we named: body lacks $expect — $(head -c 60 "$body" | tr -d '\n')"
107
136
  elif [ "$wall" -gt "$budget" ]; then verdict=fail; note="over budget by $((wall-budget)) ms"
108
137
  else verdict=pass; fi
109
138
  _row "$lane" "$name" "$verdict" "$wall" "$budget" "${placement:-}" "${phases:-}" "$note"
@@ -131,7 +160,12 @@ listeners() {
131
160
 
132
161
  lane() { # prod|local
133
162
  local l="$1" base key
134
- if [ "$l" = prod ]; then base="$PROD_URL"; key=$(_key "$ROOT/one.ie/web/.env")
163
+ # The prod credential file honours ONE_ENV_FILE / DO_ENV_FILE, like every other
164
+ # credential read in the harness (factory-repo.sh --check-env-indirection).
165
+ # A relative path is taken from $ROOT; the shell's GATEWAY_API_KEY still wins.
166
+ local env_file="${ONE_ENV_FILE:-${DO_ENV_FILE:-one.ie/web/.env}}"
167
+ case "$env_file" in /*) ;; *) env_file="$ROOT/$env_file" ;; esac
168
+ if [ "$l" = prod ]; then base="$PROD_URL"; key=$(_key "$env_file")
135
169
  else
136
170
  base="$LOCAL_URL"; key=$(_key "$ROOT/one.ie/web/.dev.vars")
137
171
  if ! curl -s -o /dev/null --max-time 3 "$base/api/health"; then
@@ -143,10 +177,27 @@ lane() { # prod|local
143
177
  # watched (2026-09-05 — announce 1.3-1.8s, board/size 0.8-1.5s), lowered only
144
178
  # after a measured improvement ships. Never raised to make a red row green.
145
179
  # announce: 2000 → 800 after the parallel-read ship (2026-09-05, measured 149-379 ms).
180
+ # UNCHANGED 2026-09-08 on purpose. The door it describes is now the WIRE door
181
+ # (board=marketplace, matched=0), whose data phase measures 259-401 ms — the
182
+ # same range 800 was set against. The wall it is compared to also carries the
183
+ # CLIENT's egress, which is not the worker's: `health` does det=0 ms of server
184
+ # work and has walked 134 → 72 → 437 → 463 ms across the ledger purely on where
185
+ # the laptop was sitting. So a marginal announce/health miss can be this box's
186
+ # network, not the door. Whether these budgets should be worker-phase budgets
187
+ # rather than wall budgets is a RECALIBRATION — a human decision, deliberately
188
+ # not taken here. A budget moved to fit an observation stops meaning anything.
146
189
  local b1 b2 b3; if [ "$l" = prod ]; then b1=400; b2=1500; b3=800; else b1=150; b2=300; b3=300; fi
147
190
  door "$l" health GET "$base/api/health" - "$b1" -
191
+ # SIGNAL_WATCH_WIRE_OWN_BOARD=1 exists for --self-test case 4 ONLY: DROPPING
192
+ # the board key sends the canary to the caller's own board, where `ceo` matches
193
+ # every tag by canon — the durable red half of the assertion below. (Setting
194
+ # `board` to an arbitrary slug does NOT work: the receiver's Zod schema rejects
195
+ # it with a 400, which is a different failure and proves nothing.) Never set
196
+ # this in a real run.
197
+ local wire_board='"board":"marketplace",'
198
+ if [ "${SIGNAL_WATCH_WIRE_OWN_BOARD:-0}" = 1 ]; then wire_board=''; fi
148
199
  door "$l" world:announce POST "$base/api/ask/world:announce" \
149
- "{\"data\":{\"tags\":[\"cc\",\"factory\",\"canary\"],\"type\":\"signal:canary\",\"text\":\"signal-watch $(date -u +%H:%M:%SZ)\",\"priority\":\"fyi\",\"slug\":\"one\"}}" "$b3" "$key"
200
+ "{\"data\":{\"tags\":[\"signal-watch-wire\"],${wire_board}\"type\":\"signal:canary\",\"text\":\"signal-watch $(date -u +%H:%M:%SZ)\",\"priority\":\"fyi\",\"slug\":\"one\"}}" "$b3" "$key" '"matched":0'
150
201
  door "$l" tasks:everywhere POST "$base/api/ask/tasks:everywhere" '{"data":{"workspace":"factory-do","limit":200}}' "$b2" "$key"
151
202
  door "$l" factory:size POST "$base/api/ask/factory:size" '{"data":{"paths":[".claude/scripts/signal-watch.sh"]}}' "$b2" "$key"
152
203
  }
@@ -174,6 +225,12 @@ if [ "${SELF_TEST:-0}" = 1 ]; then
174
225
  out=$(GATEWAY_API_KEY= SIGNAL_WATCH_BUDGET_SCALE=100 bash "$tmp/.claude/scripts/signal-watch.sh" --prod --no-ledger 2>&1); rc=$?
175
226
  [ "$rc" -eq 3 ] && grep -q "unrun" <<<"$out" && echo " ok exit 3, unrun rows" || { echo " FAIL rc=$rc"; echo "$out" | tail -4; fails=$((fails+1)); }
176
227
  rm -rf "$tmp"
228
+ echo "== 4. a wire probe that MATCHED somebody is unrun (exit 3), not a fast pass"
229
+ # Red half: point the canary back at the caller's own board, where `ceo`
230
+ # subscribes to every tag. The door still answers 200 with outcome=result —
231
+ # only the assertion catches it.
232
+ out=$(SIGNAL_WATCH_WIRE_OWN_BOARD=1 SIGNAL_WATCH_BUDGET_SCALE=100 bash "$0" --prod --no-ledger 2>&1); rc=$?
233
+ [ "$rc" -eq 3 ] && grep -q 'not the door we named' <<<"$out" && echo " ok exit 3, fan-out named as not-the-wire" || { echo " FAIL rc=$rc"; echo "$out" | tail -4; fails=$((fails+1)); }
177
234
  echo "signal-watch --self-test: $fails failed"
178
235
  exit "$fails"
179
236
  fi
@@ -28,6 +28,16 @@
28
28
 
29
29
  set -euo pipefail
30
30
 
31
+ # _GATE — see governor-doors-check.sh "compute sites". A gate_lock dedupes
32
+ # identical work; only a SLOT bounds N worktrees running N computes at once.
33
+ # Empty inside an existing gate so the outer slot is inherited, never doubled.
34
+ _GATE=()
35
+ if [ "${GOVERN_IN_GATE:-0}" != "1" ]; then
36
+ _GR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/gate-run.sh"
37
+ [ -f "$_GR" ] && _GATE=( bash "$_GR" "compute:$(basename "${BASH_SOURCE[0]}")" -- )
38
+ fi
39
+
40
+
31
41
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
32
42
  WEB="$ROOT/one.ie/web"
33
43
  REL_MEMO="one.ie/web/src/lib/domain-memo.ts"
@@ -120,7 +130,7 @@ fi
120
130
  echo "[speed-cache-check] RED run — $RED_SOURCE_DESC"
121
131
  cd "$WEB"
122
132
  set +e
123
- RED_OUT="$(bunx vitest run "$TEST_FILE" -t "$COLD_TEST_NAME" 2>&1)"
133
+ RED_OUT="$("${_GATE[@]}" bunx vitest run "$TEST_FILE" -t "$COLD_TEST_NAME" 2>&1)"
124
134
  RED_STATUS=$?
125
135
  set -e
126
136
  cd "$ROOT"
@@ -147,7 +157,7 @@ TMP_BROKEN=""
147
157
  echo "[speed-cache-check] GREEN run — working tree $REL_MEMO"
148
158
  cd "$WEB"
149
159
  set +e
150
- GREEN_OUT="$(bunx vitest run "$TEST_FILE" 2>&1)"
160
+ GREEN_OUT="$("${_GATE[@]}" bunx vitest run "$TEST_FILE" 2>&1)"
151
161
  GREEN_STATUS=$?
152
162
  set -e
153
163
  cd "$ROOT"
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env bash
2
+ # sweep.sh — every finished branch into `dev`, one gate, one sha to promote.
3
+ #
4
+ # manifest: monorepo-only
5
+ #
6
+ # THE RULES. Each is true at the line it guards; the story that earned it is one
7
+ # command away and is never summarised here.
8
+ #
9
+ # ONE gate for N branches, never one gate per branch. incident:sweep-n-trees
10
+ # A batched red RESETS dev; re-land with land.sh, which incident:sweep-batched-red-owner
11
+ # still names the owner. land.sh's default is UNCHANGED.
12
+ # gc measures against origin/main, after a fetch. Removal incident:gc-stale-base
13
+ # is do-auto.sh --gc's; this never re-implements it.
14
+ # A dry run starts no gates, so it is never refused for incident:sweep-dry-run-never-refused
15
+ # the box. Reap only when orphans > 0.
16
+ #
17
+ # Accounts: bash .claude/scripts/incident.sh --for .claude/scripts/sweep.sh
18
+ #
19
+ # Usage:
20
+ # sweep.sh # doctor · fetch · classify · land · ONE gate · gc · close
21
+ # sweep.sh --dry-run # the whole plan, nothing merged, removed or pushed
22
+ # sweep.sh --inventory # the classification table only (what --self-test drives)
23
+ # sweep.sh --full # force the FULL lane on the integrated tree
24
+ # sweep.sh --pr # also push dev and open/refresh the dev -> main PR
25
+ # sweep.sh --no-doctor # skip the box read (never skip it because it said no)
26
+ # sweep.sh --self-test # mint a sandbox estate and prove the classifier bites
27
+ #
28
+ # It does NOT ship. The last two doors are release-manager's
29
+ # (../CLAUDE.md § The dev -> prod loop); this one ends by naming the sha to promote.
30
+ #
31
+ # Exit codes, distinct and named — "it refused" is not a finding:
32
+ # 0 ok · 2 usage · 3 box refused (doctor) · 4 integrated gate RED (dev reset)
33
+ # 5 a merge conflicted (dev reset) · 6 nothing to sweep and nothing to promote
34
+ set -uo pipefail
35
+
36
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
37
+ # SWEEP_ROOT lets --self-test point the whole classifier at a sandbox estate.
38
+ # HERE always stays the real script dir, so the lib it sources is never the
39
+ # sandbox's (there isn't one) — the sandbox supplies REFS, not code.
40
+ ROOT="${SWEEP_ROOT:-$(cd "$HERE/../.." && pwd)}"
41
+ cd "$ROOT" || exit 2
42
+
43
+ # SWEEP_GATE_CMD is honoured ONLY under SWEEP_ROOT — there is no way to spell
44
+ # "skip the gate" in a real run. incident:sweep-sandbox-seam
45
+ SANDBOX=0; [ -n "${SWEEP_ROOT:-}" ] && SANDBOX=1
46
+ TRUNK="${SWEEP_TRUNK:-dev}"
47
+ GC_BASE="${SWEEP_GC_BASE:-origin/main}"
48
+ KEEP="${SWEEP_KEEP:-main dev release}"
49
+ DRY=0; INVENTORY=0; FULL=0; PR=0; DOCTOR=1; SELF_TEST=0
50
+
51
+ while [ $# -gt 0 ]; do
52
+ case "$1" in
53
+ --dry-run) DRY=1 ;;
54
+ --inventory) INVENTORY=1 ;;
55
+ --full) FULL=1 ;;
56
+ --pr) PR=1 ;;
57
+ --no-doctor) DOCTOR=0 ;;
58
+ --self-test) SELF_TEST=1 ;;
59
+ -h|--help) sed -n '1,60p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
60
+ *) echo "sweep: unknown flag $1" >&2; exit 2 ;;
61
+ esac
62
+ shift
63
+ done
64
+
65
+ say() { printf '%s\n' "$*"; }
66
+ ok() { printf ' ok %s\n' "$*"; }
67
+ bad() { printf ' RED %s\n' "$*" >&2; }
68
+
69
+ # ---------------------------------------------------------------- classify ---
70
+ # ONE definition of "finished", shared with the sweep that does the removing.
71
+ . "$HERE/lib/gc-finished.sh"
72
+
73
+ # _live <worktree-dir> — is anything running in this tree?
74
+ # The trailing slash is load-bearing; pgrep -f reads argv, not cwd. incident:live-anchor
75
+ _live() {
76
+ local wt="$1"
77
+ [ -n "$(pgrep -f "${wt%/}/" 2>/dev/null | grep -v "^$$\$" || true)" ]
78
+ }
79
+
80
+ # _worktree_of <branch> — the checkout holding it, empty when there is none.
81
+ _worktree_of() {
82
+ git -C "$ROOT" worktree list --porcelain 2>/dev/null \
83
+ | awk -v b="refs/heads/$1" '
84
+ /^worktree /{wt=substr($0,10)}
85
+ /^branch /{if (substr($0,8)==b) {print wt; exit}}'
86
+ }
87
+
88
+ # classify <branch> -> "<class> <detail>"
89
+ # infra one of the branches the loop itself stands on — never touched
90
+ # landed its content is already in the trunk (content identity, not counting)
91
+ # busy ahead, but its worktree is dirty or something is running in it
92
+ # ready ahead, clean, idle — this is the only class that gets merged
93
+ classify() {
94
+ local b="$1" wt ahead dirt
95
+ case " $KEEP " in *" $b "*) echo "infra -"; return ;; esac
96
+ wt="$(_worktree_of "$b")"
97
+ if gc_finished "$TRUNK" "$b"; then echo "landed ${wt:--}"; return; fi
98
+ ahead="$(gc_carries "$TRUNK" "$b")"
99
+ if [ -n "$wt" ]; then
100
+ dirt="$(gc_real_dirt "$wt")"
101
+ [ -n "$dirt" ] && { echo "busy uncommitted-work-in-$wt"; return; }
102
+ _live "$wt" && { echo "busy something-running-in-$wt"; return; }
103
+ fi
104
+ echo "ready carries-${ahead}-path(s)"
105
+ }
106
+
107
+ inventory() {
108
+ local b cls
109
+ while IFS= read -r b; do
110
+ [ -n "$b" ] || continue
111
+ cls="$(classify "$b")"
112
+ printf '%s\t%s\n' "$b" "$cls"
113
+ done < <(git -C "$ROOT" for-each-ref --format='%(refname:short)' refs/heads/)
114
+ }
115
+
116
+ # --------------------------------------------------------------- self-test ---
117
+ # A green run on the live estate proves only that a no-op is a no-op.
118
+ self_test() {
119
+ local rc=0
120
+
121
+ # _mint <dir> [extra-shell] — a throwaway estate with one branch of every
122
+ # class. `extra` runs last, inside the repo, so a case can add its own shape.
123
+ _mint() {
124
+ local d="$1" extra="${2:-}"
125
+ ( set -e
126
+ mkdir -p "$d"; cd "$d"
127
+ git init -q -b dev .
128
+ git config user.email s@s; git config user.name s
129
+ echo base > a.txt
130
+ # one.ie/web must be TRACKED or .astro never surfaces as its own path.
131
+ # incident:porcelain-collapses-untracked-dir
132
+ mkdir -p one.ie/web; echo keep > one.ie/web/keep.txt
133
+ git add a.txt one.ie/web/keep.txt; git commit -qm base
134
+
135
+ # landed: its patch is in dev under a DIFFERENT sha — the rebase/squash
136
+ # case `rev-list --count` gets wrong, which is why the predicate is content.
137
+ git checkout -qb feat/landed; echo one > b.txt; git add b.txt; git commit -qm one
138
+ git checkout -q dev; echo one > b.txt; git add b.txt; git commit -qm "one, landed differently"
139
+
140
+ # ready: ahead, clean, no worktree.
141
+ git checkout -qb feat/ready dev; echo two > c.txt; git add c.txt; git commit -qm two
142
+
143
+ # busy-dirty: ahead, with uncommitted WORK in its checkout.
144
+ git checkout -qb feat/dirty dev; echo three > d.txt; git add d.txt; git commit -qm three
145
+ git checkout -q dev
146
+ git worktree add -q "$d/wt-dirty" feat/dirty
147
+ echo scribble >> "$d/wt-dirty/d.txt"
148
+
149
+ # ephemeral-only dirt must NOT read as busy — build output is not work.
150
+ git checkout -qb feat/ephemeral dev; echo four > e.txt; git add e.txt; git commit -qm four
151
+ git checkout -q dev
152
+ git worktree add -q "$d/wt-eph" feat/ephemeral
153
+ mkdir -p "$d/wt-eph/one.ie/web/.astro"; echo x > "$d/wt-eph/one.ie/web/.astro/x"
154
+
155
+ git checkout -q dev
156
+ [ -n "$extra" ] && eval "$extra"
157
+ true
158
+ ) >/dev/null 2>&1
159
+ }
160
+
161
+ _run() { # <dir> <gate-cmd> [flags...] -> prints output, returns sweep's exit
162
+ local d="$1" gate="$2"; shift 2
163
+ ( cd "$d" && SWEEP_ROOT="$d" SWEEP_GATE_CMD="$gate" SWEEP_TRUNK=dev \
164
+ SWEEP_KEEP="main dev release" bash "$HERE/sweep.sh" --no-doctor "$@" 2>&1 )
165
+ }
166
+ _assert() { # <label> <condition-result 0/1>
167
+ if [ "$2" = 0 ]; then ok "$1"; else bad "$1"; rc=1; fi
168
+ }
169
+
170
+ say "── sweep --self-test ──"
171
+
172
+ # ---- 1. the classifier, one branch of every class ------------------------
173
+ local t1 out
174
+ t1="$(mktemp -d)"; _mint "$t1" || { bad "sandbox 1 would not build"; return 1; }
175
+ out="$( cd "$t1" && SWEEP_ROOT="$t1" SWEEP_TRUNK=dev SWEEP_KEEP="main dev release" \
176
+ bash "$HERE/sweep.sh" --inventory 2>/dev/null )"
177
+ printf '%s\n' "$out" | sed 's/^/ /'
178
+ _expect() { # <branch> <class>
179
+ local got; got="$(printf '%s\n' "$out" | awk -v b="$1" '$1==b{print $2}')"
180
+ if [ "$got" = "$2" ]; then ok "classify $1 -> $2"
181
+ else bad "classify $1 -> ${got:-<absent>} (expected $2)"; rc=1; fi
182
+ }
183
+ _expect dev infra
184
+ _expect feat/landed landed
185
+ _expect feat/ready ready
186
+ _expect feat/dirty busy
187
+ _expect feat/ephemeral ready
188
+
189
+ # ---- 2. liveness, both directions ---------------------------------------
190
+ # `exec -a` puts the path in argv, which is what pgrep -f reads. incident:live-anchor
191
+ if _live "$t1/definitely-not-a-tree"; then bad "_live said yes for an empty path"; rc=1
192
+ else ok "_live is false for an empty path"; fi
193
+ local holder
194
+ ( exec -a "node $t1/one.ie/web/dev-server" sleep 20 ) >/dev/null 2>&1 &
195
+ holder=$!; sleep 0.3
196
+ _live "$t1" && ok "_live sees a live process under the tree" || { bad "_live missed it"; rc=1; }
197
+ # The anchor: `pgrep -f worktrees/media` also matches `worktrees/mediawire`.
198
+ if _live "${t1}-neighbour"; then bad "_live matched a PREFIX neighbour"; rc=1
199
+ else ok "_live does not match a prefix neighbour"; fi
200
+ kill "$holder" 2>/dev/null; wait "$holder" 2>/dev/null
201
+
202
+ # ---- 3. the MERGE path: ready branches land, busy ones do not ------------
203
+ # Everything below MOVES REFS, which is why it is tested. incident:sweep-sandbox-seam
204
+ local t2 pre post
205
+ t2="$(mktemp -d)"; _mint "$t2"
206
+ pre="$(git -C "$t2" rev-parse HEAD)"
207
+ out="$(_run "$t2" true)"; local st2=$?
208
+ post="$(git -C "$t2" rev-parse HEAD)"
209
+ _assert "merge: exits 0 with a green gate" $([ $st2 -eq 0 ] && echo 0 || echo 1)
210
+ _assert "merge: dev moved" $([ "$pre" != "$post" ] && echo 0 || echo 1)
211
+ _assert "merge: feat/ready landed (c.txt)" $(git -C "$t2" cat-file -e dev:c.txt 2>/dev/null && echo 0 || echo 1)
212
+ _assert "merge: feat/ephemeral landed (e.txt)" $(git -C "$t2" cat-file -e dev:e.txt 2>/dev/null && echo 0 || echo 1)
213
+ _assert "merge: feat/dirty did NOT land (d.txt absent)" $(git -C "$t2" cat-file -e dev:d.txt 2>/dev/null && echo 1 || echo 0)
214
+ _assert "merge: reports gates=1" $(printf '%s' "$out" | grep -q "gates=1" && echo 0 || echo 1)
215
+ _assert "merge: reports lane=fast" $(printf '%s' "$out" | grep -q "lane=fast" && echo 0 || echo 1)
216
+
217
+ # ---- 4. the lane escalates on schema/, by itself -------------------------
218
+ local t3
219
+ t3="$(mktemp -d)"
220
+ _mint "$t3" 'git checkout -qb feat/schema dev; mkdir -p schema; echo "define x" > schema/one.tql; git add schema; git commit -qm schema; git checkout -q dev'
221
+ out="$(_run "$t3" true)"
222
+ _assert "lane: a schema/ touch selects FULL without being asked" \
223
+ $(printf '%s' "$out" | grep -q "lane=full" && echo 0 || echo 1)
224
+
225
+ # ---- 5. a RED gate resets dev and names the re-land -----------------------
226
+ local t4 pre4 post4
227
+ t4="$(mktemp -d)"; _mint "$t4"
228
+ pre4="$(git -C "$t4" rev-parse HEAD)"
229
+ out="$(_run "$t4" false)"; local st4=$?
230
+ post4="$(git -C "$t4" rev-parse HEAD)"
231
+ _assert "red gate: exit 4" $([ $st4 -eq 4 ] && echo 0 || echo 1)
232
+ _assert "red gate: dev reset to the pre-sweep sha" $([ "$pre4" = "$post4" ] && echo 0 || echo 1)
233
+ _assert "red gate: names land.sh per branch" \
234
+ $(printf '%s' "$out" | grep -q "land.sh feat/ready" && echo 0 || echo 1)
235
+
236
+ # ---- 6. a CONFLICT resets dev too — its own exit code (5, not 4) ---------
237
+ local t5 pre5 post5
238
+ t5="$(mktemp -d)"
239
+ _mint "$t5" 'git checkout -qb feat/clash dev; echo mine > a.txt; git commit -qam mine; git checkout -q dev; echo theirs > a.txt; git commit -qam theirs'
240
+ pre5="$(git -C "$t5" rev-parse HEAD)"
241
+ out="$(_run "$t5" true)"; local st5=$?
242
+ post5="$(git -C "$t5" rev-parse HEAD)"
243
+ _assert "conflict: exit 5" $([ $st5 -eq 5 ] && echo 0 || echo 1)
244
+ _assert "conflict: dev reset to the pre-sweep sha" $([ "$pre5" = "$post5" ] && echo 0 || echo 1)
245
+
246
+ # ---- 7. the stub is unreachable without a sandbox ------------------------
247
+ _assert "SWEEP_GATE_CMD is honoured only under SWEEP_ROOT" \
248
+ $(grep -q 'if (( SANDBOX )) && \[ -n "\${SWEEP_GATE_CMD:-}" \]' "$HERE/sweep.sh" && echo 0 || echo 1)
249
+
250
+ rm -rf "$t1" "$t2" "$t3" "$t4" "$t5"
251
+ say ""
252
+ [ $rc -eq 0 ] && say " self-test PASS" || say " self-test RED"
253
+ return $rc
254
+ }
255
+
256
+ [ $SELF_TEST -eq 1 ] && { self_test; exit $?; }
257
+ [ $INVENTORY -eq 1 ] && { inventory; exit 0; }
258
+
259
+ # ------------------------------------------------------------ phase 0 doctor --
260
+ # The box read comes FIRST because a sweep starts gates, and a saturated box
261
+ # turns a fine tree red for queueing. health.sh --fix never kills a claude
262
+ # session, a governor-slot holder, or anything in the main tree.
263
+ VERDICT="skipped"; ORPHANS="?"; FUNDED="?"; WHY=""
264
+
265
+ # _box — re-read the doctor's instrument. health.sh owns every number; this only
266
+ # reads them. HEALTHY | DEGRADED | UNHEALTHY (health.sh:307-319).
267
+ _box() {
268
+ local j
269
+ j="$( bash "$HERE/health.sh" --box --json 2>/dev/null )"
270
+ VERDICT="$(printf '%s' "$j" | sed -n 's/.*"verdict":"\([A-Z]*\)".*/\1/p')"
271
+ WHY="$(printf '%s' "$j" | sed -n 's/.*"why":"\([^"]*\)".*/\1/p')"
272
+ ORPHANS="$(printf '%s' "$j" | sed -n 's/.*"orphans":\([0-9]*\).*/\1/p')"
273
+ FUNDED="$(printf '%s' "$j" | sed -n 's/.*"gates_funded":\([0-9]*\).*/\1/p')"
274
+ VERDICT="${VERDICT:-unknown}"
275
+ }
276
+
277
+ if [ $DOCTOR -eq 1 ]; then
278
+ say "── doctor ──"
279
+ _box
280
+ say " box: $VERDICT — ${WHY:-no reason given}"
281
+ say " orphans=$ORPHANS gates_funded=$FUNDED"
282
+
283
+ # Reap only when there is something to reap. incident:sweep-dry-run-never-refused
284
+ if [ "${ORPHANS:-0}" != "0" ]; then
285
+ bash "$HERE/gate-reaper.sh" --once 2>/dev/null | sed 's/^/ /'
286
+ _box
287
+ say " box after reap: $VERDICT (orphans=$ORPHANS)"
288
+ fi
289
+
290
+ case "$VERDICT" in
291
+ UNHEALTHY)
292
+ # A dry run starts no gates, so it is never refused.
293
+ if (( DRY )); then say " (dry run — reporting the box, refusing nothing)"; else
294
+ bad "box is UNHEALTHY — $WHY"
295
+ say " A sweep STARTS gates. On a paging box the gate runs long and a slow"
296
+ say " gate is not a red one, but it is indistinguishable from one at the"
297
+ say " wall clock. Hand the box to the doctor first:"
298
+ say " Agent({ subagent_type: \"doctor\", model: \"opus\","
299
+ say " prompt: \"health.sh says: $WHY. Reclaim what nothing is coming back for, then report.\" })"
300
+ say " Or override, knowing the gate will be slow: sweep.sh --no-doctor"
301
+ exit 3
302
+ fi ;;
303
+ DEGRADED)
304
+ # Headroom of 1: a reason to run ONE gate, not a reason to refuse.
305
+ say " proceeding: DEGRADED funds $FUNDED concurrent gate(s), and a sweep needs exactly one" ;;
306
+ *)
307
+ ok "box is $VERDICT" ;;
308
+ esac
309
+ fi
310
+
311
+ # --------------------------------------------------------------- phase 1 fetch --
312
+ say "── fetch ──"
313
+ git -C "$ROOT" fetch -q --prune origin 2>/dev/null || say " (offline — measuring against local refs)"
314
+ TRUNK_WT="$(_worktree_of "$TRUNK")"
315
+ [ -n "$TRUNK_WT" ] || { bad "no worktree holds $TRUNK — cut one with worktree-up.sh $TRUNK"; exit 2; }
316
+ ok "$TRUNK is checked out at $TRUNK_WT"
317
+
318
+ # ------------------------------------------------------------ phase 2 classify --
319
+ say "── classify ──"
320
+ READY=(); LANDED=0; BUSY=0; INFRA=0
321
+ while IFS=$'\t' read -r b rest; do
322
+ cls="${rest%% *}"; detail="${rest#* }"
323
+ case "$cls" in
324
+ infra) INFRA=$((INFRA+1)) ;;
325
+ landed) LANDED=$((LANDED+1)); say " landed $b" ;;
326
+ busy) BUSY=$((BUSY+1)); say " busy $b — $detail" ;;
327
+ ready) READY+=("$b"); say " ready $b — $detail" ;;
328
+ esac
329
+ done < <(inventory)
330
+ say " ready=${#READY[@]} landed=$LANDED busy=$BUSY infra=$INFRA"
331
+
332
+ PRE_SWEEP_SHA="$(git -C "$TRUNK_WT" rev-parse HEAD)"
333
+
334
+ # --------------------------------------------------------------- phase 3 land --
335
+ MERGED=0; TOUCHED=""
336
+ if [ ${#READY[@]} -gt 0 ]; then
337
+ say "── land (batched into $TRUNK) ──"
338
+ for b in "${READY[@]}"; do
339
+ probe="$( TSC_CACHE_ROOT="$(_worktree_of "$b")" \
340
+ bash "$ROOT/.claude/scripts/tsc-cached.sh" --probe one.ie/web 2>/dev/null || echo unknown )"
341
+ if (( DRY )); then say " [dry] merge $b into $TRUNK (own typecheck: $probe)"; MERGED=$((MERGED+1)); continue; fi
342
+ say " merging $b (own typecheck: $probe)"
343
+ if ! git -C "$TRUNK_WT" merge --no-edit "$b" >/dev/null 2>&1; then
344
+ git -C "$TRUNK_WT" merge --abort 2>/dev/null
345
+ git -C "$TRUNK_WT" reset --hard "$PRE_SWEEP_SHA" >/dev/null 2>&1
346
+ bad "$b conflicts with $TRUNK — $TRUNK reset to $PRE_SWEEP_SHA, land it alone: land.sh $b"
347
+ exit 5
348
+ fi
349
+ TOUCHED="$TOUCHED$(git -C "$TRUNK_WT" diff --name-only "$PRE_SWEEP_SHA" HEAD)"$'\n'
350
+ MERGED=$((MERGED+1))
351
+ done
352
+ fi
353
+
354
+ # --------------------------------------------------------- phase 4 ONE gate ---
355
+ # schema, the SDK and auth take the FULL lane, whatever the default is.
356
+ GATES=0; LANE="none"
357
+ if [ $MERGED -gt 0 ] || [ $FULL -eq 1 ]; then
358
+ if [ $FULL -eq 1 ] || printf '%s' "$TOUCHED" | grep -qE '^(schema/|packages/sdk/|one\.ie/web/src/lib/(authority|role-check))'; then
359
+ LANE="full"
360
+ else
361
+ LANE="fast"
362
+ fi
363
+ say "── gate (ONE, on the integrated tree) ── lane=$LANE"
364
+ if (( DRY )); then
365
+ say " [dry] (cd $TRUNK_WT/one.ie/web && gate-run.sh sweep -- $([ $LANE = full ] && echo 'FULL_VERIFY=1 bun run verify' || echo 'bun run verify:fast'))"
366
+ else
367
+ GATES=1
368
+ # Status captured per branch of the if, never from `$?` after `fi`, and the
369
+ # gate is never piped. incident:gate-status-never-piped
370
+ st=0
371
+ if (( SANDBOX )) && [ -n "${SWEEP_GATE_CMD:-}" ]; then
372
+ eval "$SWEEP_GATE_CMD" || st=$?
373
+ elif [ "$LANE" = "full" ]; then
374
+ ( cd "$TRUNK_WT/one.ie/web" && FULL_VERIFY=1 bash "$ROOT/.claude/scripts/gate-run.sh" sweep -- bun run verify ) || st=$?
375
+ else
376
+ ( cd "$TRUNK_WT/one.ie/web" && bash "$ROOT/.claude/scripts/gate-run.sh" sweep -- bun run verify:fast ) || st=$?
377
+ fi
378
+ if [ $st -ne 0 ]; then
379
+ git -C "$TRUNK_WT" reset --hard "$PRE_SWEEP_SHA" >/dev/null 2>&1
380
+ bad "integrated gate RED (exit $st) — $TRUNK reset to $PRE_SWEEP_SHA, nothing landed"
381
+ say " the batch cannot name which of ${#READY[@]} owns it. Re-land one at a time,"
382
+ say " where land.sh diagnoses branch / dev / seam / environment:"
383
+ for b in "${READY[@]}"; do say " bash .claude/scripts/land.sh $b"; done
384
+ exit 4
385
+ fi
386
+ ok "integrated $LANE gate green on $(git -C "$TRUNK_WT" rev-parse --short HEAD)"
387
+ fi
388
+ fi
389
+
390
+ # ----------------------------------------------------------------- phase 5 gc --
391
+ say "── gc (base $GC_BASE — a branch is finished when RELEASED, not integrated) ──"
392
+ if (( SANDBOX )); then
393
+ say " (sandbox — the collector is do-auto.sh's and is not re-run here)"
394
+ elif (( DRY )); then
395
+ DO_GC_BASE="$GC_BASE" bash "$ROOT/.claude/scripts/do-auto.sh" --gc --dry-run 2>&1 | sed 's/^/ /'
396
+ else
397
+ DO_GC_BASE="$GC_BASE" bash "$ROOT/.claude/scripts/do-auto.sh" --gc 2>&1 | sed 's/^/ /'
398
+ fi
399
+
400
+ # ------------------------------------------------------------- phase 6 ready ---
401
+ say "── ready to deploy ──"
402
+ DEV_SHA="$(git -C "$TRUNK_WT" rev-parse HEAD)"
403
+ AHEAD_OF_MAIN="$(git -C "$ROOT" rev-list --count "origin/main..$DEV_SHA" 2>/dev/null || echo 0)"
404
+ if [ "$AHEAD_OF_MAIN" = "0" ] && [ $MERGED -eq 0 ]; then
405
+ say " $TRUNK carries nothing origin/main does not have — nothing to promote"
406
+ say "ready=0 merged=0 landed=$LANDED busy=$BUSY infra=$INFRA gates=$GATES lane=$LANE box=$VERDICT dev=$(git -C "$TRUNK_WT" rev-parse --short HEAD) ahead_of_main=0"
407
+ exit 6
408
+ fi
409
+ if (( DRY )); then
410
+ say " [dry] git push origin $TRUNK"
411
+ (( PR )) && say " [dry] gh pr create --base main --head $TRUNK"
412
+ else
413
+ if (( SANDBOX )); then say " (sandbox — not pushing)"
414
+ else git -C "$TRUNK_WT" push -q origin "$TRUNK" 2>/dev/null && ok "pushed $TRUNK" || say " (push skipped — offline or nothing new)"; fi
415
+ if (( PR )); then
416
+ gh pr create --base main --head "$TRUNK" --fill 2>/dev/null \
417
+ || gh pr list --base main --head "$TRUNK" --json url --jq '.[0].url' 2>/dev/null | sed 's/^/ PR: /'
418
+ fi
419
+ fi
420
+ say " $TRUNK is $AHEAD_OF_MAIN commit(s) ahead of origin/main at $(git -C "$TRUNK_WT" rev-parse --short HEAD)"
421
+ say " the ship door is release-manager's, never this script's:"
422
+ say " Agent({ subagent_type: \"release-manager\", model: \"opus\", prompt: \"…promote $DEV_SHA, then ship\" })"
423
+
424
+ say ""
425
+ # THE NUMERIC CLOSE. Every number is counted, not narrated; `gates` is the point.
426
+ say "ready=${#READY[@]} merged=$MERGED landed=$LANDED busy=$BUSY infra=$INFRA gates=$GATES lane=$LANE box=$VERDICT dev=$(git -C "$TRUNK_WT" rev-parse --short HEAD) ahead_of_main=$AHEAD_OF_MAIN"