@oneie/claude 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/abm-strategist.md +67 -1
  3. package/agents/ads-meta.md +67 -1
  4. package/agents/analyst.md +67 -1
  5. package/agents/animator.md +108 -0
  6. package/agents/architect.md +269 -20
  7. package/agents/brand-guardian.md +67 -1
  8. package/agents/brand-strategist.md +67 -1
  9. package/agents/campaign-content.md +67 -1
  10. package/agents/campaign-email.md +67 -1
  11. package/agents/campaign-sms.md +67 -1
  12. package/agents/campaign-social.md +67 -1
  13. package/agents/cco.md +83 -2
  14. package/agents/ceo.md +108 -11
  15. package/agents/chairman.md +197 -0
  16. package/agents/cmo.md +82 -2
  17. package/agents/community-greeter.md +67 -1
  18. package/agents/community-moderator.md +67 -1
  19. package/agents/compliance.md +67 -1
  20. package/agents/copywriter.md +67 -1
  21. package/agents/creative-strategist.md +67 -1
  22. package/agents/cro.md +81 -1
  23. package/agents/cto.md +266 -28
  24. package/agents/customer-interviewer.md +67 -1
  25. package/agents/customer-researcher.md +67 -1
  26. package/agents/customer-success-manager.md +67 -1
  27. package/agents/customer-trainer.md +67 -1
  28. package/agents/cxo.md +82 -1
  29. package/agents/demand-creator.md +67 -1
  30. package/agents/demo-mover.md +67 -1
  31. package/agents/demo-specialist.md +67 -1
  32. package/agents/demo-thai-family-law.md +67 -1
  33. package/agents/designer.md +67 -1
  34. package/agents/discovery-caller.md +67 -1
  35. package/agents/doctor.md +269 -0
  36. package/agents/educate-coach.md +67 -1
  37. package/agents/elevate-tutor.md +67 -1
  38. package/agents/email-lifecycle-marketer.md +67 -1
  39. package/agents/engage-specialist.md +67 -1
  40. package/agents/events-coordinator.md +67 -1
  41. package/agents/foundation-builder.md +67 -1
  42. package/agents/funnel-architect.md +67 -1
  43. package/agents/gift-creator.md +67 -1
  44. package/agents/google-ads.md +67 -1
  45. package/agents/guide.md +67 -1
  46. package/agents/helpdesk-dispatcher.md +67 -1
  47. package/agents/hook-specialist.md +67 -1
  48. package/agents/identify-optimizer.md +67 -1
  49. package/agents/implementer.md +313 -45
  50. package/agents/incident-commander.md +67 -1
  51. package/agents/insights-lead.md +87 -1
  52. package/agents/journey-runner.md +67 -1
  53. package/agents/linkedin-ads.md +67 -1
  54. package/agents/live-sales-chat.md +67 -1
  55. package/agents/market-researcher.md +67 -1
  56. package/agents/media-buyer.md +67 -1
  57. package/agents/memory-keeper.md +195 -0
  58. package/agents/movers-customer-researcher.md +67 -1
  59. package/agents/movers-foundation-builder.md +67 -1
  60. package/agents/movers-market-researcher.md +67 -1
  61. package/agents/movers-pricing-strategist.md +67 -1
  62. package/agents/nurture-architect.md +67 -1
  63. package/agents/offer-architect.md +67 -1
  64. package/agents/onboarder.md +67 -1
  65. package/agents/onboarding-specialist.md +67 -1
  66. package/agents/operations-dashboard.md +87 -1
  67. package/agents/perf-engineer.md +333 -37
  68. package/agents/playbook-writer.md +67 -1
  69. package/agents/plg-strategist.md +67 -1
  70. package/agents/positioning-architect.md +67 -1
  71. package/agents/press-officer.md +67 -1
  72. package/agents/pricing-strategist.md +67 -1
  73. package/agents/privacy-officer.md +67 -1
  74. package/agents/referral-manager.md +67 -1
  75. package/agents/refine-analyst.md +67 -1
  76. package/agents/release-manager.md +446 -39
  77. package/agents/renewals-upsell-rep.md +67 -1
  78. package/agents/review-engineer.md +319 -45
  79. package/agents/rewards-steward.md +67 -1
  80. package/agents/sales-call-coach.md +67 -1
  81. package/agents/sales-closer.md +67 -1
  82. package/agents/security-auditor.md +343 -48
  83. package/agents/sell-closer.md +67 -1
  84. package/agents/share-amplifier.md +67 -1
  85. package/agents/social-media-manager.md +67 -1
  86. package/agents/storyteller.md +301 -0
  87. package/agents/strategist.md +67 -1
  88. package/agents/strategy-aligner.md +67 -1
  89. package/agents/support-agent.md +67 -1
  90. package/agents/tagger.md +327 -0
  91. package/agents/tech-writer.md +195 -22
  92. package/agents/test-engineer.md +398 -29
  93. package/agents/tiktok-ads.md +67 -1
  94. package/agents/tracking-engineer.md +67 -1
  95. package/agents/trailkeeper.md +181 -0
  96. package/agents/upsell-strategist.md +67 -1
  97. package/agents/voice.md +67 -1
  98. package/agents/w1-recon.md +1 -1
  99. package/agents/w2-decide.md +1 -1
  100. package/agents/w3-edit.md +8 -2
  101. package/agents/w4-verify.md +13 -0
  102. package/agents/workflow-optimiser.md +81 -1
  103. package/commands/close.md +916 -160
  104. package/commands/deploy.md +102 -724
  105. package/commands/do.md +58 -2
  106. package/commands/sweep.md +159 -0
  107. package/commands/tasks.md +222 -0
  108. package/hooks/scripts/dev-only.sh +135 -0
  109. package/hooks/scripts/git-add-guard.sh +37 -2
  110. package/hooks/scripts/session-start.sh +32 -4
  111. package/package.json +1 -1
  112. package/rules/scripts.md +85 -0
  113. package/scripts/CLAUDE.md +315 -0
  114. package/scripts/ad-copy-lint.sh +656 -0
  115. package/scripts/agent-actor-parity.sh +129 -0
  116. package/scripts/blocks-manifest-cached.sh +100 -0
  117. package/scripts/chat-context-check.sh +89 -0
  118. package/scripts/chrome.mjs +18 -0
  119. package/scripts/close-metrics.sh +587 -0
  120. package/scripts/close-owner.sh +326 -0
  121. package/scripts/db-sync-lock-check.sh +116 -0
  122. package/scripts/deploy-emit.sh +311 -0
  123. package/scripts/deploy-gate-check.sh +155 -0
  124. package/scripts/deploy-ready.sh +78 -0
  125. package/scripts/deploy-record.sh +605 -0
  126. package/scripts/deploy-schema-check.sh +58 -0
  127. package/scripts/deploy.sh +393 -243
  128. package/scripts/do-auto.sh +127 -26
  129. package/scripts/do-board.sh +429 -0
  130. package/scripts/do-close.sh +1184 -0
  131. package/scripts/do-consumer-sweep.sh +18 -1
  132. package/scripts/do-decide.sh +476 -0
  133. package/scripts/do-fleet.sh +8 -2
  134. package/scripts/do-plan-json.mjs +110 -12
  135. package/scripts/do-prove-selftest.sh +108 -0
  136. package/scripts/do-prove.sh +86 -10
  137. package/scripts/do-rank.py +200 -3
  138. package/scripts/do-reconcile.sh +73 -12
  139. package/scripts/do-signal.sh +101 -23
  140. package/scripts/do-smoke.sh +18 -1
  141. package/scripts/do-w4-gates.sh +11 -1
  142. package/scripts/do-world-check.sh +153 -0
  143. package/scripts/download-stats.sh +172 -0
  144. package/scripts/factory-brief-check.sh +330 -0
  145. package/scripts/factory-check.sh +18 -1
  146. package/scripts/factory-close-check.sh +257 -0
  147. package/scripts/factory-emit.sh +211 -0
  148. package/scripts/factory-executor-check.mjs +353 -0
  149. package/scripts/factory-peak.sh +301 -0
  150. package/scripts/factory-repo.sh +71 -0
  151. package/scripts/factory-review-check.mjs +61 -0
  152. package/scripts/factory-tasks-check.sh +18 -1
  153. package/scripts/fixtures/factory-brief-real.md +44 -0
  154. package/scripts/flywheel-outcome.sh +63 -0
  155. package/scripts/gate-reaper-check.sh +98 -0
  156. package/scripts/gate-reaper.sh +9 -0
  157. package/scripts/gate-watchdog.sh +619 -0
  158. package/scripts/gc-content-check.sh +142 -0
  159. package/scripts/gh-traffic-capture.sh +153 -0
  160. package/scripts/govern-order-check.sh +202 -0
  161. package/scripts/governor-doors-check.sh +86 -5
  162. package/scripts/health.sh +448 -0
  163. package/scripts/id-inventory.mjs +418 -0
  164. package/scripts/incident.sh +212 -0
  165. package/scripts/land.sh +755 -45
  166. package/scripts/lib/gc-finished.sh +77 -0
  167. package/scripts/livekit-ratchet.sh +18 -1
  168. package/scripts/machine-check.sh +1 -1
  169. package/scripts/memory-index-budget.sh +79 -0
  170. package/scripts/npm-downloads.sh +109 -0
  171. package/scripts/one-agents.mjs +204 -8
  172. package/scripts/one-resume.sh +31 -3
  173. package/scripts/pr-body.sh +335 -0
  174. package/scripts/preview-fd-check.sh +289 -0
  175. package/scripts/redirect-lint.sh +169 -0
  176. package/scripts/release.sh +40 -6
  177. package/scripts/resume-lost-sessions.sh +68 -0
  178. package/scripts/shoot-pages.mjs +140 -0
  179. package/scripts/signal-meta-backfill.ts +451 -0
  180. package/scripts/signal-watch.sh +63 -6
  181. package/scripts/speed-cache-check.sh +12 -2
  182. package/scripts/sweep.sh +426 -0
  183. package/scripts/task-titles-dump.ts +101 -0
  184. package/scripts/test-cached.sh +47 -10
  185. package/scripts/test-lanes.sh +14 -0
  186. package/scripts/thread-name-backfill.ts +215 -0
  187. package/scripts/triage-shape-check.sh +149 -0
  188. package/scripts/tsc-cached.sh +155 -8
  189. package/scripts/typedb-flake-check.sh +3 -1
  190. package/scripts/urls-lint.sh +8 -0
  191. package/scripts/verify-board-doors.sh +80 -0
  192. package/scripts/verify-fast.sh +159 -6
  193. package/scripts/worktree-up.sh +21 -3
  194. package/skills/astro/SKILL.md +9 -3
  195. package/skills/astro/optimize-performance.md +3 -2
  196. package/skills/cloudflare/SKILL.md +3 -2
  197. package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
  198. package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
  199. package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
  200. package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  201. package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  202. package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  203. package/skills/cloudflare-security-audit/HUNTING.md +251 -0
  204. package/skills/cloudflare-security-audit/LICENSE +21 -0
  205. package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  206. package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  207. package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
  208. package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
  209. package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  210. package/skills/cloudflare-security-audit/SKILL.md +192 -0
  211. package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  212. package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
  213. package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  214. package/skills/cloudflare-security-audit/report-schema.json +461 -0
  215. package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
  216. package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
  217. package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
  218. package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
  219. package/skills/deploy/REFERENCE.md +713 -0
  220. package/skills/deploy/SKILL.md +140 -0
  221. package/skills/fleet-audit/SKILL.md +58 -0
  222. package/skills/meeting/SKILL.md +220 -0
  223. package/skills/planning/SKILL.md +256 -0
  224. package/skills/shadcn/SKILL.md +1 -1
  225. package/skills/typedb/SKILL.md +7 -0
  226. package/skills/voice/SKILL.md +94 -6
  227. package/skills/voice/corpus-check.sh +87 -0
  228. package/templates/template-agent.md +7 -1
  229. package/templates/template-feature.md +9 -0
  230. package/templates/template-todo.md +29 -0
@@ -0,0 +1,605 @@
1
+ #!/usr/bin/env bash
2
+ # deploy-record.sh — append one run to the record the /deploy page renders.
3
+ #
4
+ # deploy-record.sh --door "land --pr --deploy" --target dev.one.ie \
5
+ # --branch feat/x --sha abc1234 --verdict red --wall 232.7 \
6
+ # --phase "sdk + tsc:0:33:gate:pass" --phase "vitest pool:33:199:gate:fail" \
7
+ # --note "four suites failed to import"
8
+ #
9
+ # WHY A FILE AND NOT A SIGNAL. `world:announce` fans a signal to whoever staked on
10
+ # its tags and marks the delivery path; it is a router, not a log. Nothing it
11
+ # writes is a table a public page can read back, and `deliverToSubscriber` lands
12
+ # in a tenant's inbox — the wrong place and the wrong audience for "what shipped".
13
+ # `factory.astro` already settled this shape for the same problem: it renders
14
+ # `src/data/factory-check.json`, generated by a script, and its own comment says
15
+ # it is data, not a live query.
16
+ #
17
+ # So the record is a committed JSON file. That is what makes it visible from the
18
+ # deployed worker at all: a deploy runs on somebody's laptop, and `one-prod`
19
+ # cannot read that laptop's disk. A run enters the record the moment its writer
20
+ # commits, and never before — which is why the page labels the newest run with
21
+ # its sha and lets the reader see for themselves whether prod is running it.
22
+ #
23
+ # NEWEST FIRST, capped. An unbounded log would grow the worker bundle with every
24
+ # deploy; the page shows a window, not a history.
25
+ #
26
+ # WHY THE PRIMARY WORKTREE, NEVER $ROOT. A production ship runs deploy.sh from
27
+ # `.release/` (`release.sh ship`), a LINKED worktree, so `${BASH_SOURCE[0]}/../..`
28
+ # resolves THERE and this script used to append to `.release/`'s own TRACKED copy.
29
+ # Two failures, both measured on 2026-09-07's 10:50Z production ship. It left the
30
+ # release tree dirty, and `release.sh promote` refuses a dirty release tree (exit
31
+ # 6, "release tree is dirty — the receipt would not describe what ships") — so one
32
+ # recorded ship blocked the next promote until the file was restored by hand. And
33
+ # nothing ever commits from `.release/`, so that ship's own record was written
34
+ # where it could never land: its row is absent from origin/main to this day, and
35
+ # the page has only ever shown `land` runs to dev.
36
+ #
37
+ # So the record lands in the PRIMARY worktree whichever tree the door ran from.
38
+ # That is a seam, not a special case for `.release/`: every linked worktree has
39
+ # both problems, and the primary tree is the only place a row has ever reached
40
+ # trunk from — `git log -- one.ie/web/src/data/deploy-runs.json` is a list of
41
+ # ordinary commits made there. A resolution that does not already hold the
42
+ # record's directory falls back to $ROOT, so a bad resolution degrades to the old
43
+ # behaviour instead of inventing a phantom ledger somewhere new.
44
+ #
45
+ # THE TRUNK LEG, and why it stops one command short of automatic. The row now
46
+ # lands in the PRIMARY worktree, where a commit CAN pick it up — but nothing
47
+ # commits it, and the primary tree is the one place in this repo that
48
+ # structurally cannot push: `hook:branch-pin` pins it to `main`, it is
49
+ # permanently dirty (99 uncommitted paths, 85 of them also changed on trunk,
50
+ # measured 2026-09-08), and it sat 171 commits behind origin/main with no
51
+ # fast-forward. A commit made there lands on a stale main. The leg cannot end
52
+ # in the tree the row lands in.
53
+ #
54
+ # It is deliberately NOT pushed the rest of the way. `../CLAUDE.md § The dev →
55
+ # prod loop` says main receives merges, never edits — trunk is reached through
56
+ # `dev` and a pull request. A recorder that pushed its own row would invent a
57
+ # seventh door past the loop this repo just locked; it would need credentials
58
+ # and a network at ship time, which a deploy must never depend on; and it would
59
+ # race the other sessions rewriting the same 12-row capped array. A silent
60
+ # failed push is precisely the failure this script was fixed for on 2026-09-08:
61
+ # a record written where nobody looks. Automating it would rebuild that.
62
+ #
63
+ # What IS removed is having to remember, and having to notice. Pending is a
64
+ # computable fact with a named exit code and a one-command committer:
65
+ #
66
+ # --pending the runs landing would ADD to the trunk ref (not merely the
67
+ # ones it lacks: see below). Exit 6 when there are
68
+ # any, silent 0 when there are none. deploy.sh and land.sh print
69
+ # it in their own report, inside `|| true` — a LOGGING debt must
70
+ # never fail a ship. (verify-fast's deferred-pin ledger refuses a
71
+ # deploy; that is a TEST debt, and this is not one.)
72
+ # It reads two states and prints the instruction that fits: no
73
+ # row committed anywhere, or already committed on a branch that a
74
+ # worktree has checked out — which is exit 6 just the same, because
75
+ # a row on a branch has not reached trunk.
76
+ # --land the leg itself, run from a worktree cut from trunk: union the
77
+ # pending rows into THIS tree's copy, stage EXACTLY one path,
78
+ # commit. It refuses on the primary tree (exit 7), it never
79
+ # pushes, and the branch reaches trunk through land.sh like any
80
+ # other branch.
81
+ #
82
+ # ONE PATH, EVER. ~7 sessions edit the shared tree concurrently, so a blanket
83
+ # stage sweeps a neighbour's uncommitted work into this commit — which is what
84
+ # `hook:git-add-guard` refuses when a human types it. That hook is PreToolUse on
85
+ # a human's Bash call and does not police what a script does internally, so
86
+ # --self-test is the enforcement: it dirties a neighbouring TRACKED file, lands,
87
+ # and asserts both halves — the commit names exactly one path, AND the
88
+ # neighbour is STILL dirty afterwards. Gutted to the blanket form, both fail.
89
+ set -uo pipefail
90
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
91
+ REL="one.ie/web/src/data/deploy-runs.json"
92
+ # The primary worktree, or $ROOT when there is no answer to trust. `git worktree
93
+ # list` prints the primary tree FIRST — that is the documented order, and it is
94
+ # the only fact this needs. `substr($0,10)` rather than `$2` so a path with a
95
+ # space in it survives.
96
+ _record_root() {
97
+ local primary
98
+ command -v git >/dev/null 2>&1 || { printf '%s\n' "$ROOT"; return 0; }
99
+ primary="$(git -C "$ROOT" worktree list --porcelain 2>/dev/null \
100
+ | awk '/^worktree /{print substr($0,10); exit}')"
101
+ if [ -n "$primary" ] && [ -d "$primary/${REL%/*}" ]; then
102
+ printf '%s\n' "$primary"
103
+ else
104
+ printf '%s\n' "$ROOT"
105
+ fi
106
+ }
107
+ FILE="${DEPLOY_RECORD_FILE:-$(_record_root)/$REL}"
108
+ KEEP="${DEPLOY_RECORD_KEEP:-12}"
109
+
110
+ # The ref a row must reach to count as recorded. origin/main IS trunk here; the
111
+ # fallbacks exist so a fresh clone or a --self-test sandbox gets a real answer
112
+ # rather than a silent empty set, which would read as "nothing pending".
113
+ _trunk_ref() { # <repo-root>
114
+ local R="$1" r
115
+ [ -n "${DEPLOY_RECORD_TRUNK:-}" ] && { printf '%s\n' "$DEPLOY_RECORD_TRUNK"; return 0; }
116
+ for r in origin/main main; do
117
+ git -C "$R" rev-parse --verify -q "$r" >/dev/null 2>&1 && { printf '%s\n' "$r"; return 0; }
118
+ done
119
+ printf 'HEAD\n'
120
+ }
121
+
122
+ # The slice the record file holds, given a base and rows to add. ONE definition,
123
+ # used both to answer "what is pending" and to land it — two spellings of the
124
+ # same slice is the drift `test-full.sh` exists to prevent, and here it produced
125
+ # a real defect: pending was "what trunk lacks", which counted rows the cap can
126
+ # never admit. Those are EXPIRED, not pending, and no amount of landing removes
127
+ # them, so --pending fired on every deploy forever. Pending is now what the union
128
+ # would actually ADD.
129
+ _JQ_UNION='
130
+ def k: "\(.at)|\(.door // "")|\(.sha // "")|\(.target // "")";
131
+ def _union($base; $add): (($base + $add) | unique_by(k) | sort_by(.at) | reverse | .[0:$keep]);
132
+ '
133
+
134
+ # The runs landing would ADD to the trunk ref — not merely the ones it lacks.
135
+ # A row older than a full cap of trunk rows can never be admitted, so reporting
136
+ # it is reporting work nobody can do. Returned as a JSON array. Keyed on
137
+ # at+door+sha+target: `at` is second-resolution and two doors can finish in the
138
+ # same second, so `at` alone is not a key. Any doubt answers [] — a nudge that
139
+ # fires on a bad read is a nudge nobody reads.
140
+ _pending_rows() {
141
+ local dir R rel hf ref
142
+ command -v git >/dev/null 2>&1 || { printf '[]\n'; return 0; }
143
+ [ -f "$FILE" ] || { printf '[]\n'; return 0; }
144
+ dir="$(cd "$(dirname "$FILE")" 2>/dev/null && pwd)" || { printf '[]\n'; return 0; }
145
+ R="$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null)"
146
+ [ -n "$R" ] || { printf '[]\n'; return 0; }
147
+ rel="${FILE#"$R"/}"
148
+ ref="$(_trunk_ref "$R")"
149
+ hf="$(mktemp)"
150
+ git -C "$R" show "$ref:$rel" > "$hf" 2>/dev/null || :
151
+ [ -s "$hf" ] || printf '{"runs":[]}' > "$hf"
152
+ jq -n --slurpfile w "$FILE" --slurpfile h "$hf" --argjson keep "$KEEP" "$_JQ_UNION"'
153
+ ((($h[0] // {}).runs) // []) as $base
154
+ | ((($w[0] // {}).runs) // []) as $work
155
+ | ($base | map(k)) as $seen
156
+ | _union($base; $work)
157
+ | map(. as $r | select(($seen | index($r|k)) == null))' 2>/dev/null || printf '[]\n'
158
+ rm -f "$hf"
159
+ }
160
+
161
+ # Where the pending rows already sit, if anywhere. A row committed on a branch
162
+ # still has not reached trunk, so it is still pending — but "nobody has committed
163
+ # these" and "land the branch" are different instructions, and printing the wrong
164
+ # one on every ship is exactly how a nudge stops being read. Candidates are the
165
+ # branches actually CHECKED OUT in worktrees: they are the only places --land can
166
+ # have put a row (it refuses on the primary tree), and there are ~9 of them
167
+ # against 63 local branches. Matched on the newest pending row, which is one jq
168
+ # per candidate rather than a set comparison.
169
+ _pending_inflight() { # <repo-root> <rel> <key-of-newest> → branch names
170
+ local R="$1" rel="$2" key="$3" ref name cand out=""
171
+ for ref in $(git -C "$R" worktree list --porcelain 2>/dev/null | awk '/^branch /{print $2}'); do
172
+ name="${ref#refs/heads/}"
173
+ cand="$(git -C "$R" show "$ref:$rel" 2>/dev/null)" || continue
174
+ [ -n "$cand" ] || continue
175
+ [ "$(jq -r --arg k "$key" '[.runs[]?
176
+ | select("\(.at)|\(.door // "")|\(.sha // "")|\(.target // "")" == $k)]
177
+ | length' <<<"$cand" 2>/dev/null)" = "0" ] && continue
178
+ out="$out $name"
179
+ done
180
+ # The leading space is deliberate: callers interpolate this straight after a
181
+ # colon and straight after a command name, and both need the separator.
182
+ printf '%s\n' "$out"
183
+ }
184
+
185
+ # The nudge. Silent and 0 when nothing is pending — a line printed on every
186
+ # deploy is a line nobody reads. Exit 6 is the whole point: it is checkable.
187
+ _pending_report() {
188
+ local rows n newest key onbr dir R rel
189
+ rows="$(_pending_rows)"
190
+ n="$(jq 'length' <<<"$rows" 2>/dev/null)" || n=0
191
+ case "$n" in ''|*[!0-9]*) n=0 ;; esac
192
+ [ "$n" -gt 0 ] || return 0
193
+ newest="$(jq -r 'sort_by(.at) | reverse | .[0]
194
+ | "\(.at) \(.door) \(.sha // "—") \(.verdict // "—")"' <<<"$rows")"
195
+ key="$(jq -r 'sort_by(.at) | reverse | .[0]
196
+ | "\(.at)|\(.door // "")|\(.sha // "")|\(.target // "")"' <<<"$rows")"
197
+ # Recomputed, never inherited: _pending_rows runs inside a command
198
+ # substitution, so anything it sets lives and dies in that subshell.
199
+ dir="$(cd "$(dirname "$FILE")" 2>/dev/null && pwd)" || dir="."
200
+ R="$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null)" || R=""
201
+ rel="$REL"; [ -n "$R" ] && rel="${FILE#"$R"/}"
202
+ onbr=""; [ -n "$R" ] && onbr="$(_pending_inflight "$R" "$rel" "$key")"
203
+ if [ -n "$onbr" ]; then
204
+ printf '%s\n' \
205
+ "[deploy-record] $n run(s) recorded, already committed on:$onbr — but NOT IN TRUNK yet." \
206
+ " newest: $newest" \
207
+ " the branch is all that is left:" \
208
+ " bash .claude/scripts/land.sh${onbr}"
209
+ else
210
+ printf '%s\n' \
211
+ "[deploy-record] $n run(s) recorded but NOT IN TRUNK. /deploy renders this file at BUILD" \
212
+ " time, so a row nobody commits never reaches the page." \
213
+ " newest: $newest" \
214
+ " held in: $FILE" \
215
+ " land it, from a worktree cut from trunk — never the primary tree:" \
216
+ " bash .claude/scripts/deploy-record.sh --land"
217
+ fi
218
+ return 6
219
+ }
220
+
221
+ DOOR="" TARGET="" BRANCH="" SHA="" VERDICT="" WALL="" NOTE="" SELF_TEST=0 PENDING=0 LAND=0
222
+ PHASES=()
223
+ while [ $# -gt 0 ]; do
224
+ case "$1" in
225
+ --door) DOOR="${2:-}"; shift 2 ;;
226
+ --target) TARGET="${2:-}"; shift 2 ;;
227
+ --branch) BRANCH="${2:-}"; shift 2 ;;
228
+ --sha) SHA="${2:-}"; shift 2 ;;
229
+ --verdict) VERDICT="${2:-}"; shift 2 ;;
230
+ --wall) WALL="${2:-}"; shift 2 ;;
231
+ --note) NOTE="${2:-}"; shift 2 ;;
232
+ --phase) PHASES+=("${2:-}"); shift 2 ;;
233
+ --self-test) SELF_TEST=1; shift ;;
234
+ --pending) PENDING=1; shift ;;
235
+ --land) LAND=1; shift ;;
236
+ -h|--help) sed -n '2,12p' "${BASH_SOURCE[0]}"; exit 0 ;;
237
+ *) echo "deploy-record.sh: unknown flag '$1'" >&2; exit 2 ;;
238
+ esac
239
+ done
240
+
241
+ command -v jq >/dev/null 2>&1 || { echo "deploy-record.sh: needs jq" >&2; exit 3; }
242
+
243
+ _emit() { # writes the new record file to stdout, given the current one on stdin
244
+ local phases_json='[]' p name start dur kind state
245
+ for p in ${PHASES[@]+"${PHASES[@]}"}; do
246
+ IFS=':' read -r name start dur kind state <<<"$p"
247
+ phases_json="$(jq -c --arg n "$name" --argjson s "${start:-0}" --argjson d "${dur:-0}" \
248
+ --arg k "${kind:-gate}" --arg st "${state:-unrun}" \
249
+ '. + [{name:$n, start:$s, dur:$d, kind:$k, state:$st}]' <<<"$phases_json")"
250
+ done
251
+ jq --arg door "$DOOR" --arg target "$TARGET" --arg branch "$BRANCH" --arg sha "$SHA" \
252
+ --arg verdict "${VERDICT:-unknown}" --arg note "$NOTE" \
253
+ --argjson wall "${WALL:-0}" --argjson phases "$phases_json" \
254
+ --arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --argjson keep "$KEEP" \
255
+ '{runs: ([{at:$at, door:$door, target:$target, branch:$branch, sha:$sha,
256
+ verdict:$verdict, wall:$wall, note:$note, phases:$phases}]
257
+ + (.runs // []))[0:$keep]}'
258
+ }
259
+
260
+ if (( SELF_TEST )); then
261
+ # The red proof: a recorder that silently writes nothing, or drops the cap, is
262
+ # worse than no recorder — the page would render a stale run as the current one.
263
+ t="$(mktemp -d)"; f="$t/runs.json"; fail=0
264
+ ok(){ printf ' \033[32m✓\033[0m %s\n' "$1"; }
265
+ bad(){ printf ' \033[31m✗\033[0m %s\n' "$1"; fail=1; }
266
+ printf '{"runs":[]}' > "$f"
267
+ for i in 1 2 3; do
268
+ DEPLOY_RECORD_FILE="$f" DEPLOY_RECORD_KEEP=2 bash "${BASH_SOURCE[0]}" \
269
+ --door "d$i" --target t --verdict green --wall "$i" --phase "p:0:1:gate:pass" >/dev/null
270
+ done
271
+ n="$(jq '.runs|length' "$f")"
272
+ [ "$n" = "2" ] && ok "cap holds (3 written, 2 kept)" || bad "cap ignored — kept $n"
273
+ [ "$(jq -r '.runs[0].door' "$f")" = "d3" ] && ok "newest first" || bad "order wrong"
274
+ [ "$(jq -r '.runs[0].phases[0].state' "$f")" = "pass" ] && ok "phase parsed" || bad "phase lost"
275
+ # a run with no phases must still record — a gate that died before phase 1 is a fact
276
+ DEPLOY_RECORD_FILE="$f" bash "${BASH_SOURCE[0]}" --door bare --verdict red --wall 1 >/dev/null
277
+ [ "$(jq -r '.runs[0].phases|length' "$f")" = "0" ] && ok "a phase-less run still records" || bad "dropped a phase-less run"
278
+
279
+ # ── the release-worktree property ────────────────────────────────────────
280
+ # A production ship runs deploy.sh from `.release/`, a LINKED worktree. The
281
+ # bar is two-sided and both sides matter: the tree it ran from must stay
282
+ # CLEAN (its cleanliness is the receipt's whole premise), and the run must
283
+ # still reach the file the /deploy page imports. A recorder that satisfies
284
+ # only one of those is the defect this check exists for.
285
+ #
286
+ # The sandbox is built to look like the real thing: the script and a seeded
287
+ # record are COMMITTED first, then the linked worktree is added, so both
288
+ # trees carry them tracked and clean and `git status --porcelain` can be
289
+ # asserted bare — no pathspec, nothing filtered out.
290
+ _sb() { # <dir> [gut: file|land|pending|expire] — a repo shaped like this one:
291
+ # a primary tree, a release-shaped worktree, and a feature worktree.
292
+ # The seed record carries a row and there is a tracked NEIGHBOUR file,
293
+ # because the staging assertions need something to sweep and the union
294
+ # assertion needs a row trunk already holds.
295
+ local s="$1" gut="${2:-}"
296
+ mkdir -p "$s/main/.claude/scripts" "$s/main/one.ie/web/src/data"
297
+ cp "${BASH_SOURCE[0]}" "$s/main/.claude/scripts/deploy-record.sh"
298
+ case "$gut" in
299
+ file)
300
+ # RED HALF for the release-worktree property. Restore the one line as it
301
+ # stood before 2026-09-08 — the record resolved off $ROOT, so a linked
302
+ # worktree wrote its own tracked copy.
303
+ sed 's|^FILE=.*|FILE="${DEPLOY_RECORD_FILE:-$ROOT/one.ie/web/src/data/deploy-runs.json}"|' \
304
+ "$s/main/.claude/scripts/deploy-record.sh" > "$s/g" && mv "$s/g" "$s/main/.claude/scripts/deploy-record.sh"
305
+ ;;
306
+ land)
307
+ # RED HALF for ONE PATH, EVER. Replace --land's explicit-path stage with
308
+ # the blanket form this door exists in order not to be. Nothing else
309
+ # about --land changes, so a green result here can only mean the staging
310
+ # rule was never what the assertions were reading.
311
+ sed 's|^ git -C "$here" add -- "$REL".*| git -C "$here" add -A|' \
312
+ "$s/main/.claude/scripts/deploy-record.sh" > "$s/g" && mv "$s/g" "$s/main/.claude/scripts/deploy-record.sh"
313
+ ;;
314
+ expire)
315
+ # RED HALF for expiry. Restore the definition this replaced — pending as
316
+ # "what trunk lacks" rather than "what landing would add". A row the cap
317
+ # can never admit then reads as pending, on every deploy, forever.
318
+ sed 's#^ | _union($base; $work)$# | $work#' \
319
+ "$s/main/.claude/scripts/deploy-record.sh" > "$s/g" && mv "$s/g" "$s/main/.claude/scripts/deploy-record.sh"
320
+ ;;
321
+ pending)
322
+ # RED HALF for the nudge. Blind _pending_rows to []. A checker that stays
323
+ # green against a recorder which can never see a pending row measures
324
+ # nothing at all.
325
+ awk '/^_pending_rows\(\) \{$/ {print "_pending_rows() { printf \"[]\\n\"; }"; sk=1; next}
326
+ sk && /^\}$/ {sk=0; next} sk {next} {print}' \
327
+ "$s/main/.claude/scripts/deploy-record.sh" > "$s/g" && mv "$s/g" "$s/main/.claude/scripts/deploy-record.sh"
328
+ ;;
329
+ esac
330
+ printf '%s' '{"runs":[{"at":"2026-01-01T00:00:00Z","door":"seed","target":"t","branch":"b","sha":"seed0001","verdict":"green","wall":1,"note":"","phases":[]}]}' \
331
+ > "$s/main/one.ie/web/src/data/deploy-runs.json"
332
+ printf 'a neighbour, tracked and clean\n' > "$s/main/neighbour.txt"
333
+ git -C "$s/main" init -q -b main >/dev/null 2>&1 || git -C "$s/main" init -q >/dev/null 2>&1
334
+ git -C "$s/main" config user.email t@t >/dev/null 2>&1
335
+ git -C "$s/main" config user.name t >/dev/null 2>&1
336
+ git -C "$s/main" add .claude one.ie neighbour.txt >/dev/null 2>&1
337
+ git -C "$s/main" commit -qm seed >/dev/null 2>&1
338
+ git -C "$s/main" worktree add -q "$s/release" -b release >/dev/null 2>&1
339
+ git -C "$s/main" worktree add -q "$s/feat" -b feat >/dev/null 2>&1
340
+ }
341
+ _ship() { # <sandbox> — run the door the way `release.sh ship` runs it
342
+ ( cd "$1/release" && bash "$1/release/.claude/scripts/deploy-record.sh" \
343
+ --door "deploy full" --target prod --verdict green --wall 92 \
344
+ --phase "vitest:2:2:gate:pass" ) >/dev/null 2>&1
345
+ }
346
+
347
+ g="$(mktemp -d)"; _sb "$g"
348
+ _ship "$g"
349
+ gdirt="$(git -C "$g/release" status --porcelain)"
350
+ gmain="$(jq -r '.runs[0].door // "—"' "$g/main/one.ie/web/src/data/deploy-runs.json")"
351
+ [ -z "$gdirt" ] && ok "a ship from the release worktree leaves it CLEAN" \
352
+ || bad "the release worktree was dirtied: $(printf '%s' "$gdirt" | tr '\n' ';')"
353
+ [ "$gmain" = "deploy full" ] && ok "…and the run still reaches the page's file in the primary tree" \
354
+ || bad "the run was lost — primary tree's newest door is '$gmain'"
355
+
356
+ # The red half. Same two assertions, driven against the behaviour this fix
357
+ # replaced: BOTH must fail. A gut that only breaks one of them can still read
358
+ # green on the other, and a half-red proof proves half of nothing.
359
+ r="$(mktemp -d)"; _sb "$r" file
360
+ _ship "$r"
361
+ rdirt="$(git -C "$r/release" status --porcelain)"
362
+ rmain="$(jq -r '.runs[0].door // "—"' "$r/main/one.ie/web/src/data/deploy-runs.json")"
363
+ if [ -n "$rdirt" ] && [ "$rmain" != "deploy full" ]; then
364
+ ok "red proof — gutted to the old FILE=, the worktree goes dirty ($(printf '%s' "$rdirt" | tr -s ' ' | tr '\n' ';')) and the primary tree records nothing"
365
+ else
366
+ bad "red proof did not bite — dirty='$rdirt' primary-door='$rmain'; the green half above proves nothing"
367
+ fi
368
+ # ── the trunk leg: --pending and --land ──────────────────────────────────
369
+ # The 2026-09-08 fix put the row where a commit CAN pick it up. Nothing
370
+ # commits it, and the primary tree cannot push, so the leg is a worktree and
371
+ # land.sh. What is checked here is that the leg cannot be forgotten (pending
372
+ # is a computable fact with a red exit) and that walking it stages ONE path.
373
+
374
+ # --pending, both directions. Silent and 0 while trunk carries every run;
375
+ # exit 6 naming the ship the moment one is recorded and uncommitted.
376
+ q="$(mktemp -d)"; _sb "$q"
377
+ pq_out="$(cd "$q/feat" && bash "$q/feat/.claude/scripts/deploy-record.sh" --pending 2>&1)"; pq_rc=$?
378
+ { [ "$pq_rc" = 0 ] && [ -z "$pq_out" ]; } \
379
+ && ok "--pending is silent and 0 when trunk carries every run" \
380
+ || bad "--pending fired on a clean record (rc=$pq_rc) — a line that always prints is never read"
381
+ _ship "$q"
382
+ pd_out="$(cd "$q/feat" && bash "$q/feat/.claude/scripts/deploy-record.sh" --pending 2>&1)"; pd_rc=$?
383
+ case "$pd_out" in *"NOT IN TRUNK"*) pd_named=1 ;; *) pd_named=0 ;; esac
384
+ { [ "$pd_rc" = 6 ] && [ "$pd_named" = 1 ]; } \
385
+ && ok "--pending goes RED (exit 6) and names the run once a ship is recorded but uncommitted" \
386
+ || bad "--pending did not report the recorded ship (rc=$pd_rc, out='$(printf '%s' "$pd_out" | tr '\n' ';')')"
387
+
388
+ qr="$(mktemp -d)"; _sb "$qr" pending; _ship "$qr"
389
+ qr_out="$(cd "$qr/feat" && bash "$qr/feat/.claude/scripts/deploy-record.sh" --pending 2>&1)"; qr_rc=$?
390
+ { [ "$qr_rc" = 0 ] && [ -z "$qr_out" ]; } \
391
+ && ok "red proof — blinded to [], --pending calls a real pending ship a clean record" \
392
+ || bad "red proof did not bite — the blinded --pending still exited $qr_rc; the two checks above prove nothing"
393
+
394
+ # A row the cap can never admit is EXPIRED, not pending. Measured on the real
395
+ # repo before this was fixed: --land unioned 12 rows into a file already
396
+ # holding 12 and the cap dropped the 2 oldest, so once the branch merged,
397
+ # --pending reported those 2 on every deploy and every land, forever, and
398
+ # --land answered "already carries all 2" because the union was a no-op.
399
+ _expired_sb() { # <dir> [gut] — trunk holds a FULL cap of rows, all newer
400
+ _sb "$1" "${2:-}"
401
+ jq -n '{runs:[{at:"2026-02-02T00:00:00Z",door:"a",target:"t",branch:"b",sha:"aaa",verdict:"green",wall:1,note:"",phases:[]},
402
+ {at:"2026-02-01T00:00:00Z",door:"b",target:"t",branch:"b",sha:"bbb",verdict:"green",wall:1,note:"",phases:[]}]}' \
403
+ > "$1/main/one.ie/web/src/data/deploy-runs.json"
404
+ git -C "$1/main" add -- one.ie/web/src/data/deploy-runs.json >/dev/null 2>&1
405
+ git -C "$1/main" commit -qm cap >/dev/null 2>&1
406
+ # …and one OLDER row in the working copy, which a cap of 2 can never admit
407
+ jq '.runs += [{at:"2026-01-01T00:00:00Z",door:"old",target:"t",branch:"b",sha:"ooo",verdict:"green",wall:1,note:"",phases:[]}]' \
408
+ "$1/main/one.ie/web/src/data/deploy-runs.json" > "$1/w" \
409
+ && mv "$1/w" "$1/main/one.ie/web/src/data/deploy-runs.json"
410
+ }
411
+ E="$(mktemp -d)"; _expired_sb "$E"
412
+ e_out="$(cd "$E/feat" && DEPLOY_RECORD_KEEP=2 bash "$E/feat/.claude/scripts/deploy-record.sh" --pending 2>&1)"; e_rc=$?
413
+ { [ "$e_rc" = 0 ] && [ -z "$e_out" ]; } \
414
+ && ok "a row the cap can never admit is EXPIRED, not pending — --pending stays silent" \
415
+ || bad "--pending reports a row landing could never add (rc=$e_rc) — it would fire on every deploy forever"
416
+
417
+ E2="$(mktemp -d)"; _expired_sb "$E2" expire
418
+ e2_out="$(cd "$E2/feat" && DEPLOY_RECORD_KEEP=2 bash "$E2/feat/.claude/scripts/deploy-record.sh" --pending 2>&1)"; e2_rc=$?
419
+ { [ "$e2_rc" = 6 ] && [ -n "$e2_out" ]; } \
420
+ && ok "red proof — on the old definition (what trunk LACKS) the expired row reads as pending, exit 6" \
421
+ || bad "red proof did not bite — the old definition exited $e2_rc; the check above proves nothing"
422
+
423
+ # ── ONE PATH, EVER ───────────────────────────────────────────────────────
424
+ # The load-bearing assertion. hook:git-add-guard refuses a blanket stage a
425
+ # HUMAN types; it is PreToolUse and does not police what a script does
426
+ # internally. So this is the enforcement, and it is two-sided on purpose: a
427
+ # mechanism that blanket-stages in a SECOND commit passes a one-sided check.
428
+ L="$(mktemp -d)"; _sb "$L"; _ship "$L"
429
+ printf 'a neighbour, edited by somebody else mid-run\n' >> "$L/feat/neighbour.txt"
430
+ l_out="$(cd "$L/feat" && bash "$L/feat/.claude/scripts/deploy-record.sh" --land 2>&1)"; l_rc=$?
431
+ l_names="$(git -C "$L/feat" show --name-only --pretty=format: HEAD | sed '/^$/d' | tr '\n' ' ' | sed 's/ $//')"
432
+ l_dirty="$(git -C "$L/feat" status --porcelain -- neighbour.txt)"
433
+ l_rec="$L/feat/one.ie/web/src/data/deploy-runs.json"
434
+ l_top="$(jq -r '.runs[0].door // "—"' "$l_rec" 2>/dev/null)"
435
+ l_seed="$(jq -r '[.runs[] | select(.sha == "seed0001")] | length' "$l_rec" 2>/dev/null)"
436
+ [ "$l_rc" = 0 ] && ok "--land walked the leg from a linked worktree" \
437
+ || bad "--land exited $l_rc: $(printf '%s' "$l_out" | tr '\n' ';')"
438
+ [ "$l_names" = "$REL" ] && ok "--land's commit names EXACTLY one path ($l_names)" \
439
+ || bad "--land's commit named '$l_names' — a neighbour was swept in"
440
+ [ -n "$l_dirty" ] && ok "…and the neighbour's edit is STILL uncommitted ($(printf '%s' "$l_dirty" | tr -s ' '))" \
441
+ || bad "the neighbour's edit was swept into the commit — the whole reason git-add-guard exists"
442
+ [ "$l_top" = "deploy full" ] && ok "…and the pending ship reached this tree's record" \
443
+ || bad "the pending ship did not land — newest door here is '$l_top'"
444
+ [ "$l_seed" = "1" ] && ok "…as a UNION — the row trunk already carried survives the merge" \
445
+ || bad "the merge dropped trunk's own row (found $l_seed) — that is a copy, not a union"
446
+
447
+ # The nudge must tell the two states apart, or it prints the wrong instruction
448
+ # on every ship until the PR merges and stops being read. Two-sided: after
449
+ # --land the message names the BRANCH; before it, it must not.
450
+ li_out2="$(cd "$L/release" && bash "$L/release/.claude/scripts/deploy-record.sh" --pending 2>&1)"
451
+ case "$li_out2" in *"already committed on: feat"*) lf=1 ;; *) lf=0 ;; esac
452
+ case "$pd_out" in *"already committed on"*) lg=1 ;; *) lg=0 ;; esac
453
+ { [ "$lf" = 1 ] && [ "$lg" = 0 ]; } \
454
+ && ok "--pending tells the two states apart: 'already committed on: feat' after --land, and not before" \
455
+ || bad "--pending cannot tell a landed row from an unlanded one (after=$lf before=$lg)"
456
+
457
+ # The red half for both staging assertions. BOTH must fail: a gut that breaks
458
+ # only one of them can still read green on the other.
459
+ X="$(mktemp -d)"; _sb "$X" land; _ship "$X"
460
+ printf 'a neighbour, edited by somebody else mid-run\n' >> "$X/feat/neighbour.txt"
461
+ ( cd "$X/feat" && bash "$X/feat/.claude/scripts/deploy-record.sh" --land ) >/dev/null 2>&1
462
+ x_names="$(git -C "$X/feat" show --name-only --pretty=format: HEAD | sed '/^$/d' | tr '\n' ' ' | sed 's/ $//')"
463
+ x_dirty="$(git -C "$X/feat" status --porcelain -- neighbour.txt)"
464
+ if [ "$x_names" != "$REL" ] && [ -z "$x_dirty" ]; then
465
+ ok "red proof — gutted to the blanket stage, the commit names '$x_names' and the neighbour's edit is gone from the working tree"
466
+ else
467
+ bad "red proof did not bite — names='$x_names' neighbour-still-dirty='$x_dirty'; the staging checks above prove nothing"
468
+ fi
469
+
470
+ # --land refuses on the PRIMARY tree. A commit there lands on a stale, pinned
471
+ # main that cannot be pushed — the reason this leg is not automatic at all.
472
+ lp_before="$(git -C "$L/main" rev-parse HEAD)"
473
+ lp_out="$(cd "$L/main" && bash "$L/main/.claude/scripts/deploy-record.sh" --land 2>&1)"; lp_rc=$?
474
+ { [ "$lp_rc" = 7 ] && [ "$(git -C "$L/main" rev-parse HEAD)" = "$lp_before" ]; } \
475
+ && ok "--land refuses on the primary worktree (exit 7) and commits nothing there" \
476
+ || bad "--land ran on the primary tree (rc=$lp_rc) — that commit could never be pushed"
477
+
478
+ # And it refuses rather than commit AROUND another's staged path. The index is
479
+ # not this door's to edit; a partial-commit pathspec would have hidden this.
480
+ I="$(mktemp -d)"; _sb "$I"; _ship "$I"
481
+ printf 'staged by somebody else\n' >> "$I/feat/neighbour.txt"
482
+ git -C "$I/feat" add -- neighbour.txt >/dev/null 2>&1
483
+ li_before="$(git -C "$I/feat" rev-parse HEAD)"
484
+ li_out="$(cd "$I/feat" && bash "$I/feat/.claude/scripts/deploy-record.sh" --land 2>&1)"; li_rc=$?
485
+ { [ "$li_rc" = 9 ] && [ "$(git -C "$I/feat" rev-parse HEAD)" = "$li_before" ]; } \
486
+ && ok "--land refuses (exit 9) rather than commit around another's staged path" \
487
+ || bad "--land committed with a foreign path staged (rc=$li_rc)"
488
+
489
+ rm -rf "$q" "$qr" "$L" "$X" "$I" "$E" "$E2"
490
+ git -C "$g/main" worktree remove --force "$g/release" >/dev/null 2>&1
491
+ git -C "$r/main" worktree remove --force "$r/release" >/dev/null 2>&1
492
+ rm -rf "$g" "$r"
493
+ rm -rf "$t"
494
+ [ "$fail" = 0 ] && { echo "deploy-record.sh: all checks pass"; exit 0; }
495
+ echo "deploy-record.sh: FAILED"; exit 4
496
+ fi
497
+
498
+ # ── --pending ───────────────────────────────────────────────────────────────
499
+ if (( PENDING )); then
500
+ _pending_report; exit $?
501
+ fi
502
+
503
+ # ── --land: the trunk leg ───────────────────────────────────────────────────
504
+ # Union the pending rows into THIS worktree's copy, stage exactly one path,
505
+ # commit. Never pushes: the branch reaches trunk through land.sh, the door the
506
+ # loop already has.
507
+ if (( LAND )); then
508
+ primary="$(_record_root)"
509
+ here="$(git rev-parse --show-toplevel 2>/dev/null)"
510
+ if [ -z "$here" ]; then
511
+ echo "deploy-record.sh --land: not inside a git worktree." >&2; exit 7
512
+ fi
513
+ if [ "$here" = "$primary" ]; then
514
+ {
515
+ echo "deploy-record.sh --land: refusing on the PRIMARY worktree."
516
+ echo " It is pinned to main by hook:branch-pin, permanently dirty, and behind trunk,"
517
+ echo " so a commit made here lands on a stale main and cannot be pushed. Cut a"
518
+ echo " worktree and run this door there:"
519
+ echo " bash .claude/scripts/worktree-up.sh deployrec --no-dev"
520
+ echo " cd .claude/worktrees/deployrec && bash .claude/scripts/deploy-record.sh --land"
521
+ } >&2
522
+ exit 7
523
+ fi
524
+
525
+ rows="$(_pending_rows)"
526
+ n="$(jq 'length' <<<"$rows" 2>/dev/null)" || n=0
527
+ case "$n" in ''|*[!0-9]*) n=0 ;; esac
528
+ if [ "$n" -eq 0 ]; then
529
+ echo "[deploy-record] nothing pending — trunk already carries every recorded run."
530
+ exit 0
531
+ fi
532
+
533
+ # ONE PATH, EVER. If somebody else's work is already staged here, REFUSE
534
+ # rather than commit around it. A partial-commit pathspec would hide the
535
+ # collision instead of surfacing it, and the index is not this door's to edit.
536
+ others=""
537
+ while IFS= read -r stagedpath; do
538
+ [ -z "$stagedpath" ] && continue
539
+ [ "$stagedpath" = "$REL" ] && continue
540
+ others="$others $stagedpath"
541
+ done < <(git -C "$here" diff --cached --name-only 2>/dev/null)
542
+ if [ -n "$others" ]; then
543
+ {
544
+ echo "deploy-record.sh --land: the index here already holds:$others"
545
+ echo " This door stages exactly one path and will not commit around another's work."
546
+ echo " Commit or unstage that first."
547
+ } >&2
548
+ exit 9
549
+ fi
550
+
551
+ target="$here/$REL"
552
+ mkdir -p "$(dirname "$target")"
553
+ [ -f "$target" ] || printf '{"runs":[]}' > "$target"
554
+ # UNION, never copy. The primary tree's working copy has already dropped rows
555
+ # past the cap that trunk still carries — copying it over would DELETE them.
556
+ # Same shape _emit writes: unique, newest first, re-sliced at the cap.
557
+ ltmp="$(mktemp)"
558
+ if jq --argjson add "$rows" --argjson keep "$KEEP" "$_JQ_UNION"'
559
+ {runs: _union((.runs // []); $add)}' \
560
+ "$target" > "$ltmp" && [ -s "$ltmp" ]; then
561
+ mv "$ltmp" "$target"
562
+ else
563
+ rm -f "$ltmp"
564
+ echo "deploy-record.sh --land: could not merge into $target" >&2; exit 8
565
+ fi
566
+
567
+ branch="$(git -C "$here" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)"
568
+ if git -C "$here" diff --quiet HEAD -- "$REL" 2>/dev/null; then
569
+ echo "[deploy-record] this tree already carries all $n run(s) — the branch is what is left:"
570
+ echo " bash .claude/scripts/land.sh $branch"
571
+ exit 0
572
+ fi
573
+
574
+ # THE ONE PATH. --self-test guts this line to the blanket form and asserts a
575
+ # neighbouring dirty file is swept in — that red half is the reason this line
576
+ # is a line and not a convenience.
577
+ git -C "$here" add -- "$REL" || { echo "deploy-record.sh --land: stage failed" >&2; exit 8; }
578
+
579
+ lmsg="$(printf '%s\n\n%s\n%s\n\n%s\n' \
580
+ "chore(deploy): the record's trunk leg — $n run(s) the primary tree was holding" \
581
+ "deploy.sh and land.sh write each run into the PRIMARY worktree's copy of $REL, and nothing commits it. /deploy renders that file at BUILD time — a deploy runs on a laptop and one-prod cannot read that disk — so a row nobody commits never reaches the page." \
582
+ "These are the runs trunk did not carry:" \
583
+ "$(jq -r 'sort_by(.at) | reverse | .[]
584
+ | " \(.at) \(.door) \(.sha // "—") \(.verdict // "—")"' <<<"$rows")")"
585
+ if ! git -C "$here" commit -q -m "$lmsg"; then
586
+ echo "deploy-record.sh --land: commit failed" >&2; exit 8
587
+ fi
588
+ echo "[deploy-record] landed $n run(s) on $branch — staged one path: $REL"
589
+ echo " trunk is one door away: bash .claude/scripts/land.sh $branch"
590
+ exit 0
591
+ fi
592
+
593
+ [ -n "$DOOR" ] || { echo "deploy-record.sh: --door is required" >&2; exit 2; }
594
+ mkdir -p "$(dirname "$FILE")"
595
+ [ -f "$FILE" ] || printf '{"runs":[]}' > "$FILE"
596
+ tmp="$(mktemp)"
597
+ if _emit < "$FILE" > "$tmp" && [ -s "$tmp" ]; then
598
+ mv "$tmp" "$FILE"
599
+ echo "[deploy-record] $DOOR → ${TARGET:-—} · ${VERDICT:-unknown} · ${WALL:-0}s"
600
+ _pending_report || true
601
+ else
602
+ rm -f "$tmp"
603
+ echo "[deploy-record] could not write $FILE — run NOT recorded" >&2
604
+ exit 5
605
+ fi