@oneie/claude 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/agents/abm-strategist.md +89 -0
  2. package/agents/ads-meta.md +97 -0
  3. package/agents/analyst.md +173 -0
  4. package/agents/architect.md +280 -0
  5. package/agents/brand-guardian.md +88 -0
  6. package/agents/brand-strategist.md +92 -0
  7. package/agents/campaign-content.md +90 -0
  8. package/agents/campaign-email.md +88 -0
  9. package/agents/campaign-sms.md +87 -0
  10. package/agents/campaign-social.md +87 -0
  11. package/agents/cco.md +99 -0
  12. package/agents/ceo.md +106 -0
  13. package/agents/chairman.md +120 -0
  14. package/agents/cmo.md +101 -0
  15. package/agents/community-greeter.md +89 -0
  16. package/agents/community-moderator.md +92 -0
  17. package/agents/compliance.md +92 -0
  18. package/agents/copywriter.md +100 -0
  19. package/agents/creative-strategist.md +95 -0
  20. package/agents/cro.md +98 -0
  21. package/agents/cto.md +267 -0
  22. package/agents/customer-interviewer.md +93 -0
  23. package/agents/customer-researcher.md +89 -0
  24. package/agents/customer-success-manager.md +90 -0
  25. package/agents/customer-trainer.md +90 -0
  26. package/agents/cxo.md +100 -0
  27. package/agents/demand-creator.md +89 -0
  28. package/agents/demo-mover.md +83 -0
  29. package/agents/demo-specialist.md +89 -0
  30. package/agents/demo-thai-family-law.md +87 -0
  31. package/agents/designer.md +112 -0
  32. package/agents/discovery-caller.md +98 -0
  33. package/agents/doctor.md +139 -0
  34. package/agents/educate-coach.md +100 -0
  35. package/agents/elevate-tutor.md +83 -0
  36. package/agents/email-lifecycle-marketer.md +91 -0
  37. package/agents/engage-specialist.md +99 -0
  38. package/agents/events-coordinator.md +90 -0
  39. package/agents/foundation-builder.md +94 -0
  40. package/agents/funnel-architect.md +89 -0
  41. package/agents/gift-creator.md +100 -0
  42. package/agents/google-ads.md +103 -0
  43. package/agents/guide.md +292 -0
  44. package/agents/helpdesk-dispatcher.md +88 -0
  45. package/agents/hook-specialist.md +91 -0
  46. package/agents/identify-optimizer.md +101 -0
  47. package/agents/implementer.md +305 -0
  48. package/agents/incident-commander.md +120 -0
  49. package/agents/insights-lead.md +93 -0
  50. package/agents/journey-runner.md +113 -0
  51. package/agents/linkedin-ads.md +123 -0
  52. package/agents/live-sales-chat.md +90 -0
  53. package/agents/market-researcher.md +88 -0
  54. package/agents/media-buyer.md +96 -0
  55. package/agents/movers-customer-researcher.md +95 -0
  56. package/agents/movers-foundation-builder.md +96 -0
  57. package/agents/movers-market-researcher.md +97 -0
  58. package/agents/movers-pricing-strategist.md +171 -0
  59. package/agents/nurture-architect.md +99 -0
  60. package/agents/offer-architect.md +92 -0
  61. package/agents/onboarder.md +222 -0
  62. package/agents/onboarding-specialist.md +92 -0
  63. package/agents/operations-dashboard.md +98 -0
  64. package/agents/perf-engineer.md +348 -0
  65. package/agents/playbook-writer.md +71 -0
  66. package/agents/plg-strategist.md +91 -0
  67. package/agents/positioning-architect.md +88 -0
  68. package/agents/press-officer.md +89 -0
  69. package/agents/pricing-strategist.md +94 -0
  70. package/agents/privacy-officer.md +92 -0
  71. package/agents/referral-manager.md +91 -0
  72. package/agents/refine-analyst.md +102 -0
  73. package/agents/release-manager.md +261 -0
  74. package/agents/renewals-upsell-rep.md +90 -0
  75. package/agents/review-engineer.md +327 -0
  76. package/agents/rewards-steward.md +82 -0
  77. package/agents/sales-call-coach.md +94 -0
  78. package/agents/sales-closer.md +98 -0
  79. package/agents/security-auditor.md +313 -0
  80. package/agents/sell-closer.md +101 -0
  81. package/agents/share-amplifier.md +99 -0
  82. package/agents/social-media-manager.md +92 -0
  83. package/agents/storyteller.md +250 -0
  84. package/agents/strategist.md +83 -0
  85. package/agents/strategy-aligner.md +94 -0
  86. package/agents/support-agent.md +90 -0
  87. package/agents/tagger.md +245 -0
  88. package/agents/tech-writer.md +216 -0
  89. package/agents/test-engineer.md +243 -0
  90. package/agents/tiktok-ads.md +105 -0
  91. package/agents/tracking-engineer.md +92 -0
  92. package/agents/upsell-strategist.md +101 -0
  93. package/agents/voice.md +85 -0
  94. package/agents/w4-tools.md +153 -0
  95. package/agents/w4-verify.md +20 -138
  96. package/agents/workflow-optimiser.md +84 -0
  97. package/commands/close.md +814 -160
  98. package/commands/create.md +2 -2
  99. package/commands/deploy.md +554 -175
  100. package/commands/do.md +119 -109
  101. package/commands/fast.md +67 -0
  102. package/commands/improve.md +2 -2
  103. package/commands/one.md +418 -0
  104. package/commands/see.md +1 -1
  105. package/commands/sync.md +1 -1
  106. package/commands/tasks.md +222 -0
  107. package/commands/vespio.md +110 -0
  108. package/commands/vespio.remote.md +227 -0
  109. package/hooks/hooks.json +25 -79
  110. package/hooks/lib/governor-escape-match.sh +88 -0
  111. package/hooks/lib/hook.sh +4 -13
  112. package/hooks/lib/signal.sh +9 -2
  113. package/hooks/scripts/branch-pin.sh +6 -0
  114. package/hooks/scripts/config-protect.sh +6 -0
  115. package/hooks/scripts/do-outcome-gate.sh +19 -2
  116. package/hooks/scripts/git-add-guard.sh +43 -2
  117. package/hooks/scripts/governor-escape.sh +96 -0
  118. package/hooks/scripts/load-guard.sh +127 -0
  119. package/hooks/scripts/post-edit-check.sh +36 -29
  120. package/hooks/scripts/session-start.sh +34 -80
  121. package/hooks/scripts/task-complete-verify.sh +46 -40
  122. package/package.json +1 -1
  123. package/rules/documentation.md +9 -0
  124. package/scripts/ad-copy-lint.sh +656 -0
  125. package/scripts/agentverse-audit.sh +256 -0
  126. package/scripts/asi-walk.sh +435 -0
  127. package/scripts/astro-build-cached.sh +143 -0
  128. package/scripts/blocks-manifest-cached.sh +100 -0
  129. package/scripts/blocks-manifest.mjs +497 -0
  130. package/scripts/blocks-ratchet.sh +116 -0
  131. package/scripts/blocks-render-probe.mjs +529 -0
  132. package/scripts/blocks-usage.mjs +334 -0
  133. package/scripts/chat-context-check.sh +89 -0
  134. package/scripts/close-metrics.sh +558 -0
  135. package/scripts/close-owner.sh +268 -0
  136. package/scripts/db-sync-lock-check.sh +116 -0
  137. package/scripts/deploy-dev.sh +35 -0
  138. package/scripts/deploy-emit.sh +298 -0
  139. package/scripts/deploy-gate-check.sh +128 -0
  140. package/scripts/deploy-ready.sh +78 -0
  141. package/scripts/deploy-record.sh +605 -0
  142. package/scripts/deploy.sh +1273 -0
  143. package/scripts/do-auto.sh +598 -48
  144. package/scripts/do-brief.sh +113 -0
  145. package/scripts/do-close.sh +1137 -0
  146. package/scripts/do-commit.sh +75 -0
  147. package/scripts/do-consumer-sweep.sh +18 -1
  148. package/scripts/do-cycle-shape-check.sh +160 -0
  149. package/scripts/do-decide.sh +476 -0
  150. package/scripts/do-derive-check.sh +436 -0
  151. package/scripts/do-fleet.sh +106 -28
  152. package/scripts/do-folder.sh +10 -1
  153. package/scripts/do-next.sh +106 -0
  154. package/scripts/do-orchestrate.sh +17 -5
  155. package/scripts/do-plan-json.mjs +201 -0
  156. package/scripts/do-plan-json.sh +8 -0
  157. package/scripts/do-preflight.sh +117 -0
  158. package/scripts/do-project.sh +157 -0
  159. package/scripts/do-prove-selftest.sh +108 -0
  160. package/scripts/do-prove.sh +295 -23
  161. package/scripts/do-rank.py +31 -1
  162. package/scripts/do-recon-cache.sh +7 -1
  163. package/scripts/do-recon-pack.sh +196 -0
  164. package/scripts/do-reconcile.sh +121 -4
  165. package/scripts/do-signal.sh +280 -23
  166. package/scripts/do-smoke.sh +18 -1
  167. package/scripts/do-test-gate.sh +80 -0
  168. package/scripts/do-tick.sh +102 -0
  169. package/scripts/do-tier.sh +6 -0
  170. package/scripts/do-triage.sh +182 -0
  171. package/scripts/do-ui-gate.sh +1 -1
  172. package/scripts/do-w4-gates.sh +451 -0
  173. package/scripts/do-walk.sh +12 -1
  174. package/scripts/env-sync.sh +173 -0
  175. package/scripts/factory-brief-check.sh +330 -0
  176. package/scripts/factory-check.sh +68 -14
  177. package/scripts/factory-close-check.sh +257 -0
  178. package/scripts/factory-emit.sh +211 -0
  179. package/scripts/factory-executor-check.mjs +353 -0
  180. package/scripts/factory-peak.sh +301 -0
  181. package/scripts/factory-repo.sh +118 -3
  182. package/scripts/factory-review-check.mjs +61 -0
  183. package/scripts/factory-ship.sh +61 -0
  184. package/scripts/factory-tasks-check.sh +18 -1
  185. package/scripts/factory-turn.sh +326 -0
  186. package/scripts/factory-walk.sh +396 -0
  187. package/scripts/factory-width.sh +57 -0
  188. package/scripts/fade-toxic.sh +4 -3
  189. package/scripts/fixtures/factory-brief-real.md +44 -0
  190. package/scripts/fixtures/triage-dupe.md +5 -0
  191. package/scripts/fleet-manifest.mjs +108 -0
  192. package/scripts/fleet-status.sh +110 -0
  193. package/scripts/full-suite-paths-check.sh +144 -0
  194. package/scripts/gate-reaper-check.sh +98 -0
  195. package/scripts/gate-reaper.sh +125 -0
  196. package/scripts/gate-run.sh +73 -0
  197. package/scripts/gc-content-check.sh +140 -0
  198. package/scripts/gen-dev-config.py +20 -0
  199. package/scripts/govern-bound-check.sh +60 -0
  200. package/scripts/govern-claims-check.sh +233 -0
  201. package/scripts/govern-mem-check.sh +290 -0
  202. package/scripts/governor-doors-check.sh +362 -0
  203. package/scripts/governor-escape-check.sh +171 -0
  204. package/scripts/health.sh +413 -0
  205. package/scripts/id-inventory.mjs +418 -0
  206. package/scripts/land.sh +551 -0
  207. package/scripts/lib/gc-finished.sh +77 -0
  208. package/scripts/lib/govern.sh +361 -0
  209. package/scripts/lib/govern.ts +756 -0
  210. package/scripts/lighthouse-run.sh +187 -0
  211. package/scripts/livekit-live-check.sh +61 -0
  212. package/scripts/livekit-ratchet.sh +46 -0
  213. package/scripts/load-guard-check.sh +49 -0
  214. package/scripts/machine-check.sh +102 -0
  215. package/scripts/machine-watch.sh +177 -0
  216. package/scripts/one-agents.mjs +415 -0
  217. package/scripts/one-resume.sh +78 -0
  218. package/scripts/orphan-baseline.json +182 -0
  219. package/scripts/orphan-modules.mjs +179 -0
  220. package/scripts/pr-body.sh +335 -0
  221. package/scripts/preview-fd-check.sh +289 -0
  222. package/scripts/promise-manifest.mjs +24 -1
  223. package/scripts/release.sh +322 -0
  224. package/scripts/roles-check.sh +946 -0
  225. package/scripts/sdk-build-cached.sh +64 -0
  226. package/scripts/signal-watch.sh +241 -0
  227. package/scripts/skills-publish.sh +94 -0
  228. package/scripts/speed-cache-check.sh +173 -0
  229. package/scripts/speed-check.mjs +907 -0
  230. package/scripts/speed-parity-check.sh +648 -0
  231. package/scripts/speed-waterfall-check.sh +355 -0
  232. package/scripts/substrate-env-parity.mjs +156 -0
  233. package/scripts/tasks-claim-race.mjs +108 -0
  234. package/scripts/tasks-loop.sh +185 -0
  235. package/scripts/test-cached.sh +255 -0
  236. package/scripts/test-full.sh +87 -0
  237. package/scripts/test-honesty.mjs +137 -0
  238. package/scripts/test-lanes.sh +166 -0
  239. package/scripts/test-speed.sh +94 -0
  240. package/scripts/triage-shape-check.sh +149 -0
  241. package/scripts/tsc-cached.sh +179 -0
  242. package/scripts/typedb-flake-check.sh +197 -0
  243. package/scripts/urls-lint.sh +15 -0
  244. package/scripts/verify-fast.sh +445 -0
  245. package/scripts/vespio-sync.sh +149 -0
  246. package/scripts/wf-check.mjs +104 -1
  247. package/scripts/worktree-preview.sh +879 -0
  248. package/scripts/worktree-up.sh +208 -0
  249. package/skills/livekit-agents/SKILL.md +285 -0
  250. package/skills/livekit-agents/references/freshness-rules.md +168 -0
  251. package/skills/shadcn/SKILL.md +1 -1
  252. package/skills/signal/SKILL.md +0 -1
  253. package/skills/voice/SKILL.md +94 -6
  254. package/skills/voice/corpus-check.sh +87 -0
  255. package/hooks/scripts/compact-hint.sh +0 -35
  256. package/hooks/scripts/gate-guard.sh +0 -83
  257. package/hooks/scripts/read-tracker.sh +0 -26
  258. package/hooks/scripts/session-end-verify.sh +0 -51
  259. package/hooks/scripts/stop-reflect.sh +0 -140
  260. package/hooks/scripts/sync-priority-todo.sh +0 -57
  261. package/hooks/scripts/sync-todo-docs.sh +0 -46
  262. package/hooks/scripts/tool-signal.sh +0 -48
  263. package/scripts/do-tasks-bridge.py +0 -366
@@ -0,0 +1,551 @@
1
+ #!/usr/bin/env bash
2
+ # land.sh — a finished branch into `dev` in one command. `--to main` overrides.
3
+ #
4
+ # bash .claude/scripts/land.sh feat/campaign-one
5
+ # bash .claude/scripts/land.sh feat/campaign-one --deploy # also ship dev
6
+ # bash .claude/scripts/land.sh feat/x --quick # tsc only, for a saturated box
7
+ # bash .claude/scripts/land.sh feat/x feat/y --deploy # several, in order
8
+ #
9
+ # WHY THIS EXISTS: a fleet finishing is not the same as work landing. The branch
10
+ # still has to survive the trunk — a branch that was 12/12 green on its own can
11
+ # go red the moment it meets main, because main moved. That merge-then-gate step
12
+ # was being done by hand, once per fleet, and it is the slow part.
13
+ #
14
+ # THE ORDER IS THE POINT:
15
+ # 1 merge $TRUNK (dev) INTO the branch, in its own worktree (cheap, isolated)
16
+ # 2 run the FAST gate there (the dev-tier gate)
17
+ # 3 only then fast-forward $TRUNK (dev never breaks)
18
+ # 4 optionally ./deploy dev (fast lane again)
19
+ # Merging main into the branch first means a conflict or a red gate is discovered
20
+ # on the BRANCH, where it costs nothing, instead of on main, where every other
21
+ # fleet inherits it.
22
+ #
23
+ # What it will NOT do: touch production. Promotion to one.ie is `./deploy`, a
24
+ # full gate and a human decision. A fast pass is never reported as a full pass.
25
+ #
26
+ # --- THE OTHER DOOR: --pr ----------------------------------------------------
27
+ # bash .claude/scripts/land.sh feat/x --pr # open/update a PR, do not touch main
28
+ #
29
+ # `--pr` swaps step 3 for a pull request, and it also SKIPS STEP 1 — deliberately,
30
+ # because the two orders answer different questions. Landing merges main into the
31
+ # branch first so a conflict is discovered where it costs nothing; a PR wants the
32
+ # branch as the author wrote it, so GitHub computes the merge and the diff a
33
+ # reviewer reads is what this branch added and nothing else. Merging main in
34
+ # first would put trunk's own commits in the PR and make the branch
35
+ # non-fast-forwardable at the same time.
36
+ #
37
+ # So the gate under `--pr` proves LESS than the gate under a land: it says the
38
+ # branch is green on its own, not that it survives the trunk. The body says so,
39
+ # and reports how far trunk has moved plus whether the merge is clean
40
+ # (`git merge-tree`, which computes without mutating anything).
41
+ #
42
+ # --- --pr --deploy: run it before you propose it ------------------------------
43
+ # bash .claude/scripts/land.sh feat/x --pr --deploy --probe /pricing
44
+ #
45
+ # The full workflow, in one command and in this order:
46
+ #
47
+ # gate (fast lane, in the branch's worktree)
48
+ # → ship THAT WORKTREE to dev.one.ie
49
+ # → prove the routes against dev, with the landing rule
50
+ # → open/update the PR, carrying the dev URL and the probe verdict
51
+ #
52
+ # The pair used to be refused, and the refusal was right while `--deploy` meant
53
+ # "ship main after the fast-forward" — a PR lands nothing, so there was nothing
54
+ # to ship. What makes it work now is a property of `deploy-dev.sh`, not a flag:
55
+ # it derives its own ROOT from `${BASH_SOURCE[0]}/../..`, so invoking THE
56
+ # WORKTREE'S COPY ships the worktree's tree. `$wt/.claude/scripts/deploy-dev.sh`
57
+ # puts the BRANCH on dev.one.ie; `$ROOT`'s copy would have put main there and
58
+ # called it the branch.
59
+ #
60
+ # The probe is `do-prove.sh` with both its bases pinned to dev, never curl.
61
+ # do-prove carries the LANDING RULE — a route counts only if the run ENDED on
62
+ # the path asked for — and deploy-dev.sh's own check is `curl / -> 200`, which
63
+ # a redirect to /signin satisfies. Both bases are pinned because do-prove falls
64
+ # back to PROVE_PROD_URL (https://one.ie by default) when its dev base does not
65
+ # answer: an unreachable dev would otherwise quietly prove PRODUCTION and report
66
+ # it as the branch passing. And `PROVE: skipped` exits 0, so the route COUNT is
67
+ # read, not the exit code — an unrun probe is not a pass.
68
+ #
69
+ # dev.one.ie IS ONE SLOT, and it reads and writes production's rows. So this door
70
+ # takes exactly one branch: two branches would mean the second overwrites the
71
+ # first while the first is still being "tested". It is also why nothing here
72
+ # touches production — promotion to one.ie is `./deploy`, a full gate, a human.
73
+ #
74
+ # The body is `pr-body.sh` — every number derived from git at PR time. Re-running
75
+ # is the normal case, not the edge case: an existing open PR is UPDATED, never
76
+ # duplicated.
77
+ set -uo pipefail
78
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
79
+ cd "$ROOT" || exit 1
80
+
81
+ # TRUNK is what a branch LANDS INTO; PR_BASE is what `dev` is RELEASED into.
82
+ # They are deliberately different: branches integrate on dev and main receives a
83
+ # pull request (root CLAUDE.md § the loop). Before 2026-09-06 both were `main`,
84
+ # so nothing ever advanced dev and it fell behind by construction — 5 commits one
85
+ # way, 8 the other, undetected until someone asked.
86
+ TRUNK="${LAND_TRUNK:-dev}"
87
+ DEPLOY=0 DRY=0 QUICK=0 PR=0 PR_BASE="main" BRANCHES=() PROBES=()
88
+ for a in "$@"; do
89
+ case "$a" in
90
+ --deploy) DEPLOY=1 ;;
91
+ --quick) QUICK=1 ;;
92
+ --pr) PR=1 ;;
93
+ --to) LAND_TRUNK_ARG=1 ;;
94
+ --probe) WANT_PROBE=1 ;;
95
+ -n|--dry-run) DRY=1 ;;
96
+ -h|--help) sed -n '2,80p' "${BASH_SOURCE[0]}"; exit 0 ;;
97
+ -*) echo "land.sh: unknown flag '$a'" >&2; exit 2 ;;
98
+ *) if [[ "${WANT_PROBE:-0}" == 1 ]]; then PROBES+=("$a"); WANT_PROBE=0;
99
+ elif [[ "${LAND_TRUNK_ARG:-0}" == 1 ]]; then TRUNK="$a"; LAND_TRUNK_ARG=0;
100
+ else BRANCHES+=("$a"); fi ;;
101
+ esac
102
+ done
103
+ [[ "${WANT_PROBE:-0}" == 1 ]] && { echo "land.sh: --probe needs a route" >&2; exit 2; }
104
+ (( ${#BRANCHES[@]} )) || { echo "land.sh: name at least one branch" >&2; exit 2; }
105
+ # --pr --deploy is the full workflow (gate -> dev -> probe -> PR) and it ships
106
+ # the BRANCH's worktree, not main. dev.one.ie is one slot, so it takes one branch:
107
+ # with two, the second overwrites the first while the first is being probed.
108
+ if (( PR && DEPLOY )) && (( ${#BRANCHES[@]} > 1 )); then
109
+ echo "land.sh: --pr --deploy ships one branch to dev.one.ie — name one, not ${#BRANCHES[@]}" >&2
110
+ exit 2
111
+ fi
112
+ # --probe only means anything when something was shipped to probe.
113
+ if (( ${#PROBES[@]} )) && ! (( PR && DEPLOY )); then
114
+ echo "land.sh: --probe needs --pr --deploy (there is nothing on dev to probe otherwise)" >&2
115
+ exit 2
116
+ fi
117
+ (( PR )) && ! command -v gh >/dev/null 2>&1 && { echo "land.sh: --pr needs the gh CLI" >&2; exit 2; }
118
+
119
+ GRN=$'\033[32m'; RED=$'\033[31m'; YEL=$'\033[33m'; DIM=$'\033[2m'; OFF=$'\033[0m'
120
+ [[ -t 1 ]] || { GRN=""; RED=""; YEL=""; DIM=""; OFF=""; }
121
+ say() { printf '%s\n' "$*"; }
122
+ step() { printf '\n%s══ %s%s\n' "$DIM" "$*" "$OFF"; }
123
+ ok() { printf '%s ✓%s %s\n' "$GRN" "$OFF" "$*"; }
124
+ bad() { printf '%s ✗%s %s\n' "$RED" "$OFF" "$*"; }
125
+ warn() { printf '%s !%s %s\n' "$YEL" "$OFF" "$*"; }
126
+
127
+ # The worktree that holds a branch — a fleet branch lives in one, and gating
128
+ # there keeps the shared main tree free for the next fleet.
129
+ worktree_of() {
130
+ git worktree list --porcelain \
131
+ | awk -v b="refs/heads/$1" '/^worktree /{w=$2} /^branch /{if ($2==b) print w}' \
132
+ | head -1
133
+ }
134
+
135
+ LANDED=() FAILED=() LANDED_QUICK=0 PRS=() DEV_NOTE="" DEV_URL="https://dev.one.ie"
136
+ BR_T0=0 GATE_T=0 DEV_T=0 PROBE_T=0 PR_T=0; PH=()
137
+
138
+ # The record the /deploy page renders. A RED run is the one most worth a timing —
139
+ # a record holding only successes cannot be read for a trend, and the phase that
140
+ # refused is exactly what a reader wants to see. It can never change an exit
141
+ # status: the whole call is `|| true`.
142
+ _land_record() { # <branch> <verdict>
143
+ [[ -x "$ROOT/.claude/scripts/deploy-record.sh" ]] || return 0
144
+ (( DRY )) && return 0
145
+ local door="land"
146
+ (( PR )) && door="$door --pr"
147
+ (( DEPLOY )) && door="$door --deploy"
148
+ local target="$TRUNK"
149
+ (( PR && DEPLOY )) && target="$DEV_URL"
150
+ local -a args=(--door "$door" --branch "$1" --verdict "$2" --target "$target"
151
+ --sha "$(git -C "$ROOT" rev-parse --short "$1" 2>/dev/null || echo '')"
152
+ --wall "$(( SECONDS - BR_T0 ))")
153
+ local p; for p in ${PH[@]+"${PH[@]}"}; do args+=(--phase "$p"); done
154
+ bash "$ROOT/.claude/scripts/deploy-record.sh" "${args[@]}" >/dev/null 2>&1 || true
155
+ # The same nudge deploy.sh prints. This door is where it is cheapest to act
156
+ # on: land runs many times a day, the operator already has the worktrees open,
157
+ # and once the rows are committed on one the message degrades to the very
158
+ # command being typed here — `land.sh <branch>`. Note ROOT is MAIN's tree (run
159
+ # MAIN's copy), so --land itself is refused here; the message says where to go.
160
+ # Never fatal — `|| true`, like the record call above it.
161
+ local pend; pend="$(bash "$ROOT/.claude/scripts/deploy-record.sh" --pending 2>/dev/null)" || true
162
+ [[ -n "$pend" ]] && say "$pend"
163
+ return 0
164
+ }
165
+
166
+ for br in "${BRANCHES[@]}"; do
167
+ step "land $br"
168
+ BR_T0=$SECONDS; PH=(); GATE_T=0; DEV_T=0; PROBE_T=0; PR_T=0
169
+
170
+ git show-ref --verify --quiet "refs/heads/$br" || { bad "no such branch"; FAILED+=("$br: missing"); continue; }
171
+
172
+ wt="$(worktree_of "$br")"
173
+ if [[ -z "$wt" ]]; then
174
+ wt="$ROOT/.land-worktrees/${br//\//-}"
175
+ if (( DRY )); then say " [dry] git worktree add $wt $br"
176
+ else
177
+ git worktree add "$wt" "$br" >/dev/null 2>&1 || { bad "could not check out $br"; FAILED+=("$br: checkout"); continue; }
178
+ fi
179
+ fi
180
+ say " worktree: $wt"
181
+
182
+ # 1 — main INTO the branch. A conflict surfaces here, not on main.
183
+ # Skipped under --pr: a reviewer must see what this branch added, not what
184
+ # trunk did while it built, and a branch with main merged in is no longer
185
+ # fast-forwardable. `pr-body.sh` reports mergeability without mutating.
186
+ if (( PR )); then say " ${DIM}step 1 skipped — a PR shows the branch as written${OFF}"
187
+ elif (( DRY )); then say " [dry] (cd $wt && git merge $TRUNK)"
188
+ else
189
+ if ! ( cd "$wt" && git merge --no-edit "$TRUNK" >/dev/null 2>&1 ); then
190
+ bad "merge conflict with $TRUNK — resolve in $wt"
191
+ ( cd "$wt" && git merge --abort >/dev/null 2>&1 )
192
+ FAILED+=("$br: conflict")
193
+ _land_record "$br" red; continue
194
+ fi
195
+ ok "merged $TRUNK into $br"
196
+ fi
197
+
198
+ # 1.5 — the worktree node_modules trap. A fresh worktree has none, so every
199
+ # gate that resolves through a service package dies on an import and reports a
200
+ # RED gate when nothing is wrong with the code. `bun install` here is NOT the
201
+ # fix: it re-points main's one.ie/web/node_modules/@oneie/sdk at this
202
+ # worktree's dist-less SDK and breaks the shared tree. A symlink to main's
203
+ # already-installed deps is read-only reuse and costs nothing.
204
+ #
205
+ # ENUMERATED FROM THE TREE, not from a list. This started as two hardcoded
206
+ # directories — packages/sdk, then one.ie/web — each added after somebody's
207
+ # branch went falsely red, and on 2026-09-05 it went red a third time for
208
+ # `channels/` and `pay/backend/`: four suites under one.ie/web import
209
+ # `channels/src/tools/pages.ts`, which imports `zod`, which a worktree without
210
+ # channels/node_modules cannot resolve. The gate said `fast gate RED` and the
211
+ # branch it was blaming contained nothing but shell scripts. Asking git for
212
+ # every tracked package.json learns the next service without being told.
213
+ while IFS= read -r pj; do
214
+ d="$(dirname "$pj")"
215
+ [[ "$d" == "." ]] && continue
216
+ [[ -d "$ROOT/$d/node_modules" ]] || continue
217
+ [[ -e "$wt/$d/node_modules" ]] && continue
218
+ if (( DRY )); then say " [dry] ln -s $ROOT/$d/node_modules $wt/$d/node_modules"
219
+ else
220
+ mkdir -p "$wt/$d"
221
+ ln -s "$ROOT/$d/node_modules" "$wt/$d/node_modules" && ok "linked $d/node_modules (no bun install — that clobbers main)"
222
+ fi
223
+ done < <(git -C "$ROOT" ls-files -- '*package.json' ':!:**/node_modules/**' 2>/dev/null || true)
224
+
225
+ # 1.55 — one.ie/web is the one that can be present but INCOMPLETE (a partial
226
+ # install leaves the directory there, so the `-e` test above passes and tsc
227
+ # still fails `File 'astro/tsconfigs/strict' not found`). Repair it by hand.
228
+ if [[ -d "$ROOT/one.ie/web/node_modules/astro/tsconfigs" ]]; then
229
+ webnm="$wt/one.ie/web/node_modules"
230
+ if [[ -e "$webnm" && ! -e "$webnm/astro/tsconfigs/strict.json" ]]; then
231
+ if (( DRY )); then say " [dry] replace incomplete $webnm with main's"
232
+ else
233
+ rm -rf "$webnm"
234
+ ln -s "$ROOT/one.ie/web/node_modules" "$webnm" && ok "replaced incomplete web node_modules with main's"
235
+ fi
236
+ fi
237
+ fi
238
+
239
+ # 1.56 — fleets.json is gitignored generated output. tsc imports it; a worktree
240
+ # that never ran `gen:fleets` fails TS2307 and the land reads as a red gate.
241
+ if [[ -f "$ROOT/one.ie/web/src/data/fleets.json" && ! -e "$wt/one.ie/web/src/data/fleets.json" ]]; then
242
+ if (( DRY )); then say " [dry] ln fleets.json"
243
+ else ln -s "$ROOT/one.ie/web/src/data/fleets.json" "$wt/one.ie/web/src/data/fleets.json" && ok "linked fleets.json"
244
+ fi
245
+ fi
246
+
247
+ # 1.57 — src/lib/generated/ is the same trap one directory over, and it bit a
248
+ # fresh land worktree on 2026-09-05: `page-modified.json` is written by
249
+ # `gen:page-modified` (a step of `verify`, never of a bare checkout), so tsc
250
+ # stopped at TS2307 and the branch read RED with nothing wrong in it.
251
+ #
252
+ # Linking the DIRECTORY does not work and the first attempt at this proved it:
253
+ # `src/lib/generated/` is tracked (it holds `substrate-client.ts`), so the
254
+ # worktree already has the directory and only the gitignored files inside it
255
+ # are missing. `! -e` on the directory is therefore always false, the link
256
+ # never fires, and the gate stays red for a reason nothing in the output names.
257
+ #
258
+ # So ask git which files it IGNORES under that path and link those. That still
259
+ # learns a new generator without this script being told about it — the failure
260
+ # mode of the enumerated approach (1.55, 1.56, each added one file at a time
261
+ # after somebody's branch went falsely red) is what this avoids.
262
+ gendir="one.ie/web/src/lib/generated"
263
+ if [[ -d "$ROOT/$gendir" ]]; then
264
+ while IFS= read -r rel; do
265
+ [[ -n "$rel" ]] || continue
266
+ [[ -e "$wt/$rel" ]] && continue
267
+ if (( DRY )); then say " [dry] ln -s $rel"
268
+ else
269
+ mkdir -p "$(dirname "$wt/$rel")"
270
+ ln -s "$ROOT/$rel" "$wt/$rel" && ok "linked ${rel#one.ie/web/src/lib/} (gen:* output a checkout never has)"
271
+ fi
272
+ done < <(git -C "$ROOT" ls-files --others --ignored --exclude-standard -- "$gendir" 2>/dev/null || true)
273
+ fi
274
+
275
+ # 1.6 — the same gap, one layer up: a worktree has no one.ie/web/.env, so
276
+ # GATEWAY_API_KEY is unset and every real-substrate suite SKIPS. A skipped suite
277
+ # is NOT a passing suite — the gate would go green having never run the substrate
278
+ # paths, which is worse than red. Link main's .env so the gate means what it says.
279
+ for envf in one.ie/web/.env channels/.env; do
280
+ if [[ -f "$ROOT/$envf" && ! -e "$wt/$envf" ]]; then
281
+ if (( DRY )); then say " [dry] ln -s $ROOT/$envf $wt/$envf"
282
+ else ln -s "$ROOT/$envf" "$wt/$envf" && ok "linked $envf (else substrate suites silently skip)"
283
+ fi
284
+ fi
285
+ done
286
+
287
+ # 2 — the fast gate, in the branch's worktree, through the governor.
288
+ if (( DRY )); then
289
+ (( QUICK )) && say " [dry] (cd $wt/one.ie/web && npx tsc --noEmit) # --quick" \
290
+ || say " [dry] (cd $wt/one.ie/web && bun run verify:fast)"
291
+ else
292
+ if (( QUICK )); then
293
+ # --quick: tsc only. It catches type breaks, renames and dead imports —
294
+ # which is most of what a fleet gets wrong — and nothing else. It does NOT
295
+ # catch a logic regression, and it is NOT the fast lane: verify:fast also
296
+ # runs the tests related to the diff plus the pinned config/parity gates
297
+ # that import nothing from what they guard. Use it to move a queue when the
298
+ # box is saturated; never as the last gate before production.
299
+ warn "gate: TYPECHECK ONLY (--quick) — weaker than the fast lane, never a full pass"
300
+ if ! ( cd "$wt/one.ie/web" && bash "$ROOT/.claude/scripts/gate-run.sh" quicktsc -- npx tsc --noEmit ); then
301
+ bad "typecheck RED on $br — not landing it"
302
+ FAILED+=("$br: tsc red")
303
+ _land_record "$br" red; continue
304
+ fi
305
+ ok "typecheck green (tests NOT run)"
306
+ LANDED_QUICK=1
307
+ else
308
+ say " gate: FAST LANE (not a full pass)"
309
+ if ! ( cd "$wt/one.ie/web" && bun run verify:fast ); then
310
+ bad "fast gate RED on $br — not landing it"
311
+ FAILED+=("$br: gate red")
312
+ _land_record "$br" red; continue
313
+ fi
314
+ ok "fast gate green"
315
+ fi
316
+ GATE_T=$(( SECONDS - BR_T0 ))
317
+ PH+=("$( (( QUICK )) && printf 'tsc only' || printf 'fast gate' ):0:$GATE_T:gate:pass")
318
+ fi
319
+
320
+ # 2.5 — run it before proposing it. Only under `--pr --deploy`: ship THIS
321
+ # WORKTREE to dev.one.ie, then prove the routes there. A PR whose branch has
322
+ # never been executed is a diff, not a proposal.
323
+ if (( PR && DEPLOY )); then
324
+ step "dev.one.ie ← $br"
325
+ if (( DRY )); then
326
+ say " [dry] (cd $wt && DEV_SKIP_GATE=$(( QUICK ? 0 : 1 )) bash $wt/.claude/scripts/deploy-dev.sh)"
327
+ dry_probe=""
328
+ for r in ${PROBES[@]+"${PROBES[@]}"}; do dry_probe="$dry_probe --route $r"; done
329
+ [[ -n "$dry_probe" ]] || dry_probe=" --route /"
330
+ say " [dry] PROVE_BASE_URL=$DEV_URL PROVE_PROD_URL=$DEV_URL do-prove.sh$dry_probe"
331
+ DEV_NOTE="$DEV_URL — not run (dry run)"
332
+ else
333
+ # The worktree's OWN copy. $ROOT's would ship main and call it the branch.
334
+ # The gate is skipped here because step 2 just ran it in this same tree —
335
+ # except under --quick, where step 2 ran tsc only and is NOT the fast lane,
336
+ # so deploy-dev.sh must run its own before anything reaches dev.
337
+ if ! ( cd "$wt" && DEV_SKIP_GATE=$(( QUICK ? 0 : 1 )) bash "$wt/.claude/scripts/deploy-dev.sh" ); then
338
+ bad "dev deploy failed for $br — not opening a PR for code that will not ship"
339
+ FAILED+=("$br: dev deploy")
340
+ _land_record "$br" red; continue
341
+ fi
342
+ ok "shipped $br to $DEV_URL"
343
+ DEV_T=$(( SECONDS - BR_T0 - GATE_T ))
344
+ PH+=("ship dev:$GATE_T:$DEV_T:ship:pass")
345
+
346
+ # The probe. Both bases pinned to dev: do-prove falls back to
347
+ # PROVE_PROD_URL when its dev base is silent, and an unreachable dev would
348
+ # otherwise prove https://one.ie and report it as this branch passing.
349
+ probe_args=()
350
+ for r in ${PROBES[@]+"${PROBES[@]}"}; do probe_args+=(--route "$r"); done
351
+ (( ${#probe_args[@]} )) || probe_args=(--route /)
352
+ say " probe: ${probe_args[*]} (landing rule, signed-out)"
353
+ probe_out="$( cd "$wt" && PROVE_BASE_URL="$DEV_URL" PROVE_PROD_URL="$DEV_URL" \
354
+ bash "$wt/.claude/scripts/do-prove.sh" "${probe_args[@]}" 2>&1 )"
355
+ printf '%s\n' "$probe_out" | sed 's/^/ /'
356
+ # Read the ROUTE COUNT, not the exit code: `PROVE: skipped (no reachable
357
+ # environment)` exits 0, and an unrun probe is not a pass.
358
+ proven="$(printf '%s' "$probe_out" | sed -n 's/^PROVE: pass (\([0-9][0-9]*\) route.*/\1/p' | tail -1)"
359
+ if [[ -z "$proven" || "$proven" -lt 1 ]]; then
360
+ bad "probe did not prove a single route on dev — not opening a PR"
361
+ FAILED+=("$br: probe")
362
+ _land_record "$br" red; continue
363
+ fi
364
+ ok "proved $proven route(s) on $DEV_URL"
365
+ PROBE_T=$(( SECONDS - BR_T0 - GATE_T - DEV_T ))
366
+ PH+=("probe dev:$(( GATE_T + DEV_T )):$PROBE_T:probe:pass")
367
+ DEV_NOTE="$DEV_URL — $proven route(s) proven signed-out (landing rule); dev shares production's data"
368
+ fi
369
+ fi
370
+
371
+ # 3 — the door. Either main fast-forwards to the branch, or the branch becomes
372
+ # a pull request. Never both: a PR that has already been merged into main is a
373
+ # PR with nothing to review.
374
+ if (( PR )); then
375
+ # The gate label is PASSED to pr-body.sh rather than inferred by it. That
376
+ # script runs no gate and must never imply one; what actually ran is known
377
+ # only here, and --quick has to say out loud that it is weaker.
378
+ gate_label="fast lane (green) — branch only, trunk not merged in"
379
+ (( QUICK )) && gate_label="typecheck only (--quick) — NOT the fast lane, never a full pass"
380
+ (( DRY )) && gate_label="not run (dry run)"
381
+
382
+ if (( DRY )); then
383
+ say " [dry] git push -u origin $br"
384
+ say " [dry] gh pr create --base $PR_BASE --head $br --title \"$(bash "$ROOT/.claude/scripts/pr-body.sh" "$br" --base "$PR_BASE" --title 2>/dev/null)\""
385
+ say " [dry] (body from pr-body.sh — $(bash "$ROOT/.claude/scripts/pr-body.sh" "$br" --base "$PR_BASE" 2>/dev/null | wc -l | tr -d ' ') lines${DEV_NOTE:+, + dev line})"
386
+ PRS+=("$br: [dry]")
387
+ else
388
+ if ! git push -u origin "$br" >/dev/null 2>&1; then
389
+ bad "could not push $br to origin"
390
+ FAILED+=("$br: push"); continue
391
+ fi
392
+ ok "pushed $br"
393
+
394
+ body_file="$(mktemp)"
395
+ # --dev is passed only when something was actually shipped and probed.
396
+ # pr-body.sh renders no line without it, which is the honest default: a PR
397
+ # silent about dev is a PR whose branch was never run.
398
+ dev_args=()
399
+ [[ -n "$DEV_NOTE" ]] && dev_args=(--dev "$DEV_NOTE")
400
+ bash "$ROOT/.claude/scripts/pr-body.sh" "$br" --base "$PR_BASE" --gate "$gate_label" \
401
+ ${dev_args[@]+"${dev_args[@]}"} > "$body_file" 2>/dev/null
402
+ title="$(bash "$ROOT/.claude/scripts/pr-body.sh" "$br" --base "$PR_BASE" --title 2>/dev/null)"
403
+ [ -s "$body_file" ] || { bad "pr-body.sh produced nothing for $br"; rm -f "$body_file"; FAILED+=("$br: body"); continue; }
404
+
405
+ # Idempotent by design. A re-run after "main moved", a second cycle on the
406
+ # same branch, or simply landing twice must UPDATE the open PR — creating
407
+ # is what fails when one already exists, so ask first.
408
+ existing="$(gh pr list --head "$br" --base "$PR_BASE" --state open --json number \
409
+ --jq '.[0].number' 2>/dev/null || true)"
410
+ if [ -n "$existing" ]; then
411
+ if gh pr edit "$existing" --title "$title" --body-file "$body_file" >/dev/null 2>&1; then
412
+ url="$(gh pr view "$existing" --json url --jq .url 2>/dev/null || echo "#$existing")"
413
+ ok "updated PR $url"
414
+ PRS+=("$br: $url (updated)")
415
+ else
416
+ bad "could not update PR #$existing for $br"
417
+ FAILED+=("$br: pr edit")
418
+ fi
419
+ else
420
+ if url="$(gh pr create --base "$PR_BASE" --head "$br" --title "$title" --body-file "$body_file" 2>&1 | tail -1)"; then
421
+ ok "opened PR $url"
422
+ PRS+=("$br: $url")
423
+ else
424
+ bad "could not open a PR for $br: $url"
425
+ FAILED+=("$br: pr create")
426
+ fi
427
+ fi
428
+ rm -f "$body_file"
429
+ fi
430
+ # The PR path returns here, BEFORE the `LANDED+=` record below — so without
431
+ # this line a green `--pr` run recorded nothing and the page showed only the
432
+ # runs that failed. Measured on the first green run of this door: PR #35
433
+ # opened, 768s of real work, and the record stayed empty.
434
+ PR_T=$(( SECONDS - BR_T0 - GATE_T - DEV_T - PROBE_T ))
435
+ PH+=("open PR:$(( GATE_T + DEV_T + PROBE_T )):$PR_T:pr:pass")
436
+ _land_record "$br" green
437
+ continue
438
+ fi
439
+
440
+ if (( DRY )); then say " [dry] advance $TRUNK to $br (in its worktree if it has one, else git fetch .)"
441
+ else
442
+ # Advance $TRUNK WITHOUT MOVING HEAD in the shared tree. Switching HEAD here
443
+ # is what `hook:branch-pin` refuses and the canon forbids: concurrent
444
+ # sessions get pulled onto each other's branches. Two ways to avoid it, and
445
+ # which applies is decided by git, not by taste:
446
+ # * $TRUNK is checked out in a worktree -> `fetch .` REFUSES it outright
447
+ # ("refusing to fetch into branch ... checked out at ..."), so
448
+ # fast-forward inside that worktree instead.
449
+ # * $TRUNK is checked out nowhere -> there is no worktree to run in, so
450
+ # move the ref with `fetch .`, which refuses a non-fast-forward exactly
451
+ # as `merge --ff-only` does.
452
+ # Measured 2026-09-06: dev IS checked out at .claude/worktrees/dev on this
453
+ # box, so the fetch-only version of this failed on its first real use.
454
+ TRUNK_WT="$(git worktree list --porcelain \
455
+ | awk -v b="refs/heads/$TRUNK" '/^worktree /{w=$2} $0=="branch "b{print w; exit}')"
456
+ # CAPTURE the error, never swallow it. Until 2026-09-09 both forms ended in
457
+ # `>/dev/null 2>&1` and EVERY failure printed "$TRUNK moved", which is the
458
+ # wrong diagnosis most of the time on this box: $TRUNK is checked out in a
459
+ # worktree OTHER SESSIONS EDIT, so a fast-forward is refused whenever one of
460
+ # them holds an uncommitted file this branch also touches. Measured that day
461
+ # — three consecutive lands reported "dev moved" while `dev` was 0 commits
462
+ # behind the branch; the real error was only visible by running the merge by
463
+ # hand:
464
+ # error: Your local changes to the following files would be overwritten
465
+ # by merge: one.ie/web/src/data/deploy-runs.json
466
+ # "moved" sends you to re-run (which fails identically, forever); the truth
467
+ # sends you to one named file. A wrong diagnosis costs more than no
468
+ # diagnosis, because it is actionable in the wrong direction.
469
+ if [[ -n "$TRUNK_WT" ]]; then
470
+ _adv() { git -C "$TRUNK_WT" merge --ff-only "$br" 2>&1; }
471
+ else
472
+ _adv() { git fetch . "$br:$TRUNK" 2>&1; }
473
+ fi
474
+ if ! adv_err="$(_adv)"; then
475
+ # Three causes, and they need three different next moves.
476
+ if [[ "$adv_err" == *"would be overwritten by merge"* || "$adv_err" == *"local changes"* ]]; then
477
+ # Name the files. `git` already listed them; pass them straight through
478
+ # rather than re-deriving, so this cannot drift from what git checked.
479
+ dirty="$(printf '%s\n' "$adv_err" | sed -n 's/^\t//p' | tr '\n' ' ')"
480
+ warn "$TRUNK's worktree ($TRUNK_WT) has UNCOMMITTED changes to: ${dirty:-<see below>}"
481
+ warn " $TRUNK did NOT move — the fast-forward was refused by a dirty tree."
482
+ warn " Another session is likely mid-edit there. Do not blanket-restore:"
483
+ warn " check whether those files' contents are already in $br, and clear"
484
+ warn " only the paths that are (git checkout -- <path>), never -A."
485
+ FAILED+=("$br: $TRUNK worktree dirty"); continue
486
+ fi
487
+ if [[ "$adv_err" == *"non-fast-forward"* || "$adv_err" == *"Not possible to fast-forward"* || "$adv_err" == *"diverge"* ]]; then
488
+ warn "$TRUNK genuinely moved — merge it in and re-run land.sh for $br"
489
+ FAILED+=("$br: $TRUNK moved"); continue
490
+ fi
491
+ warn "could not advance $TRUNK — git said:"
492
+ printf '%s\n' "$adv_err" | sed 's/^/ /' >&2
493
+ FAILED+=("$br: advance failed"); continue
494
+ fi
495
+ # READ THE REF BACK. A successful --ff-only is not proof the branch is IN
496
+ # main a moment later: several sessions land into one shared tree, and on
497
+ # 2026-09-05 two of these printed "main fast-forwarded to feat/deploy-page"
498
+ # while main's reflog recorded a NEIGHBOUR's branch name at the very sha
499
+ # this script had just reported. Twelve commits sat outside main for an hour
500
+ # and were reported as landed, twice, because the script trusted its own
501
+ # exit code over the ref. `git merge` succeeding is a claim; the branch
502
+ # being an ancestor of main is the fact.
503
+ if ! git merge-base --is-ancestor "$br" "$TRUNK"; then
504
+ bad "$TRUNK does NOT contain $br after a clean fast-forward — a concurrent land overwrote it"
505
+ say " $TRUNK is now $(git rev-parse --short "$TRUNK"); $br is $(git rev-parse --short "$br")"
506
+ FAILED+=("$br: ff did not hold"); _land_record "$br" red; continue
507
+ fi
508
+ ok "$TRUNK fast-forwarded to $br (verified: $TRUNK contains $(git rev-parse --short "$br"))"
509
+ fi
510
+
511
+ LANDED+=("$br")
512
+ _land_record "$br" green
513
+ done
514
+
515
+ # 4 — one dev deploy for the whole batch, not one per branch. Not under --pr:
516
+ # that path already shipped the branch's own worktree in step 2.5, and this would
517
+ # overwrite it with main — the tree the PR is NOT proposing.
518
+ if (( DEPLOY )) && ! (( PR )) && (( ${#LANDED[@]} )); then
519
+ step "deploy dev"
520
+ if (( DRY )); then say " [dry] bash .claude/scripts/deploy.sh dev"
521
+ else
522
+ bash "$ROOT/.claude/scripts/deploy.sh" dev || { bad "dev deploy failed"; FAILED+=("deploy"); }
523
+ fi
524
+ fi
525
+
526
+ step "report"
527
+ (( ${#LANDED[@]} )) && ok "landed: ${LANDED[*]}"
528
+ if (( ${#PRS[@]} )); then
529
+ for p in "${PRS[@]}"; do ok "pr: $p"; done
530
+ fi
531
+ (( LANDED_QUICK )) && warn "typecheck-only gate was used — run the full suite before ./deploy to production"
532
+ (( ${#FAILED[@]} )) && bad "failed: ${FAILED[*]}"
533
+ # --only dims,feedback is deliberate. A BRANCH NAME is not a plan slug: without
534
+ # --tid the task leg resolves by the `slug:` tag and would close whatever plan
535
+ # happens to share the name. And land runs the FAST lane, so the receipt gate is
536
+ # unrun on every land — filing that would fire a row on every land forever, with
537
+ # nothing anyone can do, which is the EXPIRED class deploy-record.sh names. The
538
+ # pheromone is the part that is true here.
539
+ # Close the loop on BOTH exits — a red land is an outcome, not an absence of one.
540
+ # `|| true` on purpose: a logging debt must never change what the merge door did.
541
+ (( ${#FAILED[@]} )) && { bash "$(dirname "${BASH_SOURCE[0]}")/do-close.sh" "${BRANCHES[0]##*/}" --status failed --only dims,feedback || true; exit 1; }
542
+ say ""
543
+ if (( PR )); then
544
+ say " Nothing landed — these are pull requests. main is untouched."
545
+ say " The gate that ran proves the branch is green ON ITS OWN, not that it survives the trunk."
546
+ [[ -n "$DEV_NOTE" ]] && say " Running on dev: $DEV_URL — dev shares production's DATA. Merge the PR, then ./deploy."
547
+ else
548
+ say " dev only. Promote to production with: ./deploy (full gate, human decision)"
549
+ fi
550
+ bash "$(dirname "${BASH_SOURCE[0]}")/do-close.sh" "${BRANCHES[0]##*/}" --only dims,feedback || true
551
+ exit 0
@@ -0,0 +1,77 @@
1
+ #!/bin/bash
2
+ # gc-finished.sh — "is this branch's work already on the platform?"
3
+ #
4
+ # ONE definition, sourced by both the sweep (do-auto.sh --gc) and its red proof
5
+ # (gc-content-check.sh). The matcher for hook:governor-escape lives in a shared
6
+ # lib for the same reason: a checker that re-implements what it checks proves
7
+ # only that two copies agree.
8
+ #
9
+ # NOT `rev-list --count BASE..branch`. That counts COMMITS, and a branch whose
10
+ # patch reached BASE under a different sha — cherry-picked, rebased, landed by a
11
+ # squash — keeps commits of its own for ever. Measured 2026-09-07 on this repo:
12
+ # fix/webhook-500's single patch was already in dev under another sha, so the
13
+ # count read "1 unmerged" and the sweep had been keeping a ~120MB checkout of
14
+ # work the platform already had. A sweep that cannot recognise its own work is
15
+ # not a sweep, it is a hoard.
16
+ #
17
+ # `git cherry` is the obvious fix and is NOT sufficient alone. A MERGE commit
18
+ # carries no patch of its own, so cherry reports 0-new for a branch that holds
19
+ # content nothing else has — merge/trunk-reconcile, the same day, 24 files and
20
+ # 3416 lines dev had never seen. Sweeping on cherry would have deleted the only
21
+ # checkout of a hand-made merge resolution.
22
+ #
23
+ # The predicate true in BOTH cases is content identity: every path the branch
24
+ # touched since the merge base is now byte-identical in BASE. A rebased duplicate
25
+ # passes; a merge carrying unique files fails; an ordinary unlanded branch fails.
26
+
27
+ # gc_finished <base> <branch> -> 0 = finished (its content is in base), 1 = not
28
+ gc_finished() {
29
+ local base="$1" b="$2"
30
+ local -a paths=()
31
+ # Paths the branch changed since the merge base. `...` (three dots) is the
32
+ # load-bearing part: `..` would also list what BASE changed underneath it, and
33
+ # a busy trunk would then make every branch look like it carries everything.
34
+ while IFS= read -r f; do [ -n "$f" ] && paths+=("$f"); done < <(git diff --name-only "${base}...${b}" 2>/dev/null)
35
+ # A branch that changed nothing since the merge base is finished by definition.
36
+ [ ${#paths[@]} -eq 0 ] && return 0
37
+ # Compare those paths as they stand NOW in each tip. --quiet exits 1 on any
38
+ # difference, which is the answer, not an error — never let `set -e` see it.
39
+ git diff --quiet "$base" "$b" -- "${paths[@]}" 2>/dev/null
40
+ }
41
+
42
+ # gc_carries <base> <branch> — how many paths the BRANCH still carries that base
43
+ # does not have. This is the number the sweep prints, so it has to mean what it
44
+ # says: `git diff --name-only base branch | wc -l` (two dots) counts everything
45
+ # that differs in EITHER direction, so a branch adding one file to a trunk that
46
+ # has moved on by 500 commits reports "carries 508 paths". Measured on this repo
47
+ # 2026-09-07: fix/webhook-500-again touches ONE file and read 508. The paths that
48
+ # belong to the branch are the ones it changed since the merge base AND that
49
+ # still differ from base now — the same set gc_finished tests.
50
+ gc_carries() {
51
+ local base="$1" b="$2" n=0 f
52
+ while IFS= read -r f; do
53
+ [ -n "$f" ] || continue
54
+ git diff --quiet "$base" "$b" -- "$f" 2>/dev/null || n=$((n+1))
55
+ done < <(git diff --name-only "${base}...${b}" 2>/dev/null)
56
+ echo "$n"
57
+ }
58
+
59
+ # Dirt that is not work: build output, worktree bookkeeping, a regenerated data
60
+ # file. None of it is recoverable from a checkout and all of it regenerates, so
61
+ # it must not stand between a finished branch and its removal.
62
+ GC_EPHEMERAL="${GC_EPHEMERAL:-one\.ie/web/\.astro|one\.ie/web/\.wrangler|one\.ie/web/\.preview|\.dev\.pid|\.worktree-port|deploy-runs\.json|node_modules}"
63
+
64
+ # gc_real_dirt <worktree> — the uncommitted lines that are actually WORK, empty
65
+ # when the tree is clean or carries only ephemera.
66
+ #
67
+ # The `|| true` is load-bearing and is why this is a function rather than an
68
+ # inline pipeline. `grep -v` exits 1 when it filters EVERYTHING out — which is
69
+ # exactly the clean-worktree case — and the caller runs under `set -euo
70
+ # pipefail`, so `real="$(... | grep -v ...)"` killed the sweep the first time it
71
+ # met a worktree it was supposed to remove. Measured 2026-09-07: the sweep
72
+ # printed four keep lines, exited 1, and never reached its own summary; piped
73
+ # through `cat` or `tail` the failure was invisible, which is how it nearly got
74
+ # reported as a clean run.
75
+ gc_real_dirt() {
76
+ git -C "$1" status --porcelain 2>/dev/null | grep -vE "$GC_EPHEMERAL" || true
77
+ }