@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
@@ -101,6 +101,7 @@ function toList(v) {
101
101
  }
102
102
 
103
103
  const promises = []
104
+ const broken = []
104
105
  for (const file of readdirSync(TEXT).sort()) {
105
106
  if (!file.endsWith('.md') || DERIVED.test(file)) continue
106
107
  if (file.startsWith('template-')) continue
@@ -109,7 +110,18 @@ for (const file of readdirSync(TEXT).sort()) {
109
110
  if (!split) continue
110
111
 
111
112
  let fm
112
- try { fm = parseYaml(split.fm) } catch { continue } // an unparseable head is not a contract
113
+ try {
114
+ fm = parseYaml(split.fm)
115
+ } catch (err) {
116
+ // An unparseable head is not a contract — UNLESS it was meant to be one. A file
117
+ // that declares `deliverables:` and then fails to parse is a promise the site
118
+ // would drop on the floor, silently, forever (text/urls.md vanished this way:
119
+ // an unescaped `"` inside a double-quoted accept:). Loud, not skipped.
120
+ if (/^deliverables:/m.test(split.fm)) {
121
+ broken.push({ file, message: String(err.message || err).split('\n')[0] })
122
+ }
123
+ continue
124
+ }
113
125
  if (!fm || typeof fm !== 'object') continue
114
126
  const rows = Array.isArray(fm.deliverables) ? fm.deliverables : []
115
127
  if (rows.length === 0) continue // no schedule = not a promise
@@ -132,6 +144,17 @@ for (const file of readdirSync(TEXT).sort()) {
132
144
  })
133
145
  }
134
146
 
147
+ if (broken.length) {
148
+ console.error(
149
+ `[promise-manifest] ${broken.length} promise file(s) declare deliverables: but their\n` +
150
+ `frontmatter does not parse — each would be MISSING from the site, silently:\n` +
151
+ broken.map((b) => ` text/${b.file} — ${b.message}`).join('\n') +
152
+ `\nFix the YAML (usually an unescaped " or \\ inside a double-quoted accept:,\n` +
153
+ `or an unquoted title: containing ": "). Nothing was written.`,
154
+ )
155
+ process.exit(4)
156
+ }
157
+
135
158
  // The index carries the claim and the count and nothing else — it is imported by
136
159
  // the Astro route, so every field costs worker bundle on every tasks request.
137
160
  const index = promises.map((p) => ({
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env bash
2
+ # release.sh — a deploy worktree of beautifully tested code, shipped without
3
+ # re-running the suite.
4
+ #
5
+ # manifest: monorepo-only
6
+ #
7
+ # THE ASK. "We should have a deploy worktree with beautifully tested code that we
8
+ # can deploy without testing." The second half is the part that needs care: not
9
+ # *without testing* — without RE-RUNNING the tests, because the tree already
10
+ # carries a green full-suite receipt and the receipt is bound to the tree.
11
+ #
12
+ # WHAT THE RECEIPT IS. test-cached.sh memoises a full-suite PASS under a key over
13
+ # the exact inputs: the vitest argv + the content of every selected test file +
14
+ # `git diff HEAD` BY CONTENT + untracked contents + HEAD + the gitignored .env
15
+ # files + the test-file set. Only passes are ever recorded. So a stamp existing
16
+ # for a tree is a proof that this exact tree ran the whole suite green, and it is
17
+ # the same object deploy.sh's own vitest gate looks for. release.sh does not
18
+ # invent a second notion of "tested" — it reads deploy's.
19
+ #
20
+ # promote [<sha>] refuse unless <sha>'s tree carries that receipt; then
21
+ # fast-forward `release` to it and materialise .release/
22
+ # ship run ./deploy FROM .release/, where the suite gate is a
23
+ # memo HIT rather than a 400-second re-run
24
+ # --self-test the red proof (three refusals, sandboxed)
25
+ #
26
+ # WHY .release/ AND NOT .do-worktrees/. do-auto.sh's exit-time --gc sweep deletes
27
+ # worktrees under .do-worktrees/ (project_do_auto_gc_collateral_bug). A release
28
+ # tree that a neighbouring /do cycle can garbage-collect mid-deploy is not a
29
+ # release tree. .release/ is gitignored and swept by nothing.
30
+ #
31
+ # WHY THE KEY IS COMPUTED BY RUNNING THE REAL MACHINERY. The full suite runs as
32
+ # TWO lanes (test-lanes.sh) with different argv, so it has TWO keys — and a third
33
+ # shape if the `real TypeDB` marker ever matches nothing (test-lanes falls back to
34
+ # a single run). Spelling those lanes out here would be a second definition that
35
+ # drifts silently, which is the exact bug test-full.sh was created to kill. So
36
+ # promote runs test-full.sh with TEST_CACHE_KEY_ONLY=1: every lane's test-cached.sh
37
+ # prints its key and exits without running anything. Whatever the lanes are, the
38
+ # keys are theirs.
39
+ #
40
+ # THE DIRTY-MAIN COROLLARY, measured 2026-09-05. The key hashes `git diff HEAD`
41
+ # by content, and main carried 9 uncommitted paths. A receipt minted on main is
42
+ # therefore a receipt for a tree that will never ship. The mint has to happen on
43
+ # a CLEAN tree at the sha — which is what .release/ is. This is also the honest
44
+ # security property: `./deploy --allow-dirty` from main ships uncommitted code;
45
+ # `ship` structurally cannot.
46
+ #
47
+ # Exit codes are distinct and named, because "it refused" is not a finding:
48
+ # 0 ok · 2 usage · 3 no receipt · 4 tsc red · 5 not a fast-forward
49
+ # 6 dirty release tree · 7 tsc could not run (lock/timeout) · 8 deploy failed
50
+ # 9 no receipt key could be computed
51
+ set -uo pipefail
52
+
53
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
54
+ WT="${RELEASE_WORKTREE:-$ROOT/.release}"
55
+ BRANCH="${RELEASE_BRANCH:-release}"
56
+ CACHE_DIR="${TEST_CACHE_DIR:-${TMPDIR:-/tmp}/one-test-cache}"
57
+ # Bounded, unlike tsc-cached's 900s default. deploy.sh deliberately does NOT route
58
+ # its typechecks through tsc-cached because a /do cycle holding the per-folder
59
+ # gate_lock would turn a ship gate red for lock contention rather than for code.
60
+ # promote reads the cache but must never inherit that stall: 120s, then refuse
61
+ # with its own exit code (7), which says "unrun", never "green".
62
+ TSC_WAIT="${RELEASE_TSC_WAIT:-120}"
63
+ TSC_SERVICES=(one.ie/web api sync channels pay/backend)
64
+
65
+ say() { printf '%s\n' "$*"; }
66
+ ok() { printf ' ok %s\n' "$*"; }
67
+ bad() { printf ' RED %s\n' "$*" >&2; }
68
+
69
+ # Everything gitignored that a checkout does not get but the gates read. The
70
+ # node_modules half is do-auto.sh's _link_deps set; the .env half is WIDER than
71
+ # _link_deps on purpose. test-cached.sh's key hashes one.ie/web/.env, .env.local
72
+ # AND .env.test, printing "ABSENT" for a missing one — so a worktree that links
73
+ # .env but not .env.local computes a DIFFERENT key from the main tree for the same
74
+ # commit, and every receipt misses for a reason nothing prints. Measured
75
+ # 2026-09-05: one.ie/web/.env.local exists on this box and _link_deps does not
76
+ # link it.
77
+ _link_deps() {
78
+ local wt="$1" nm
79
+ for nm in node_modules one.ie/web/node_modules packages/node_modules \
80
+ packages/sdk/dist packages/sdk/node_modules \
81
+ pay/backend/node_modules channels/node_modules \
82
+ api/node_modules sync/node_modules \
83
+ one.ie/web/.env one.ie/web/.env.local one.ie/web/.env.test \
84
+ .env.local pay/backend/.env channels/.env; do
85
+ if [ -e "$ROOT/$nm" ] && [ ! -e "$wt/$nm" ]; then
86
+ mkdir -p "$wt/$(dirname "$nm")"
87
+ ln -s "$ROOT/$nm" "$wt/$nm" 2>/dev/null && say " linked $nm"
88
+ fi
89
+ done
90
+ }
91
+
92
+ # The receipt keys for the tree standing in $1. Prints one 64-hex key per lane.
93
+ # TEST_CACHE_KEY_ONLY=1 makes every test-cached.sh in the fan-out print its key
94
+ # and exit 0 without running vitest, so this costs ~1s and cannot start a suite.
95
+ _receipt_keys() {
96
+ ( cd "$1" && TEST_CACHE_KEY_ONLY=1 bash .claude/scripts/test-full.sh 2>/dev/null ) \
97
+ | grep -Eo '^[0-9a-f]{64}$'
98
+ }
99
+
100
+ # The stamp for a key, if one exists. test-cached.sh mangles the whole stamp path
101
+ # into the filename, so match on the key rather than reproducing that mangling —
102
+ # the key is a sha256 and is unique on its own.
103
+ _stamp_for() {
104
+ local k="$1" f
105
+ for f in "$CACHE_DIR"/*"$k".pass; do [ -f "$f" ] && { printf '%s\n' "$f"; return 0; }; done
106
+ return 1
107
+ }
108
+
109
+ # ── promote ─────────────────────────────────────────────────────────────────
110
+ cmd_promote() {
111
+ local sha="${1:-}"
112
+ sha="$(git -C "$ROOT" rev-parse --verify "${sha:-HEAD}^{commit}" 2>/dev/null)" || {
113
+ bad "not a commit: ${1:-HEAD}"; return 2; }
114
+
115
+ say "release: promote ${sha:0:9}"
116
+
117
+ # 1. fast-forward only. A release branch that can move backwards is not a record.
118
+ local prev="" dist="(first release)"
119
+ if prev="$(git -C "$ROOT" rev-parse --verify "$BRANCH" 2>/dev/null)"; then
120
+ if [ "$prev" != "$sha" ] && ! git -C "$ROOT" merge-base --is-ancestor "$prev" "$sha"; then
121
+ bad "$sha is not a fast-forward of $BRANCH (${prev:0:9}) — refusing to rewind the release record"
122
+ return 5
123
+ fi
124
+ dist="$(git -C "$ROOT" rev-list --count "$prev..$sha") commit(s) since ${prev:0:9}"
125
+ fi
126
+
127
+ # 2. materialise the tree. Idempotent: create, or move an existing worktree.
128
+ #
129
+ # THE DIRTY CHECK COMES FIRST, and the ordering is the whole point. The obvious
130
+ # shape — reset --hard, then check clean — can never report a dirty tree,
131
+ # because the reset already destroyed the evidence, and the operator's work
132
+ # with it. Silently discarding what is standing in the tree is not a refusal;
133
+ # it is data loss that reads as success.
134
+ if [ -d "$WT/.git" ] || [ -f "$WT/.git" ]; then
135
+ local dirty; dirty="$(git -C "$WT" status --porcelain)"
136
+ if [ -n "$dirty" ]; then
137
+ bad "release tree is dirty — the receipt would not describe what ships:"
138
+ printf '%s\n' "$dirty" | sed 's/^/ /' >&2
139
+ return 6
140
+ fi
141
+ # -B moves the branch and HEAD together. A plain `git branch -f` is REFUSED
142
+ # by git for a branch checked out in a worktree ("cannot force update the
143
+ # branch ... checked out at"), so the move is made from inside the worktree.
144
+ git -C "$WT" checkout -q -B "$BRANCH" "$sha" \
145
+ || { bad "could not move $WT to $sha"; return 5; }
146
+ else
147
+ git -C "$ROOT" worktree add -B "$BRANCH" "$WT" "$sha" >/dev/null 2>&1 \
148
+ || { bad "could not add worktree $WT on $BRANCH"; return 5; }
149
+ fi
150
+ _link_deps "$WT"
151
+
152
+ # 3. after the move it must still be clean — a checkout that left something
153
+ # behind is not a tree any receipt describes.
154
+ local dirty2; dirty2="$(git -C "$WT" status --porcelain)"
155
+ if [ -n "$dirty2" ]; then
156
+ bad "release tree is dirty after checkout:"
157
+ printf '%s\n' "$dirty2" | sed 's/^/ /' >&2
158
+ return 6
159
+ fi
160
+ ok "release tree clean at $(git -C "$WT" rev-parse --short HEAD)"
161
+
162
+ # 4. the receipt.
163
+ local keys; keys="$(_receipt_keys "$WT")"
164
+ if [ -z "$keys" ]; then
165
+ bad "no receipt key could be computed — test-full.sh printed no lane key"
166
+ return 9
167
+ fi
168
+ local n_keys=0 missing=0 stamps=()
169
+ while IFS= read -r k; do
170
+ [ -n "$k" ] || continue
171
+ n_keys=$((n_keys+1))
172
+ local s
173
+ if s="$(_stamp_for "$k")"; then
174
+ stamps+=("$s")
175
+ ok "receipt ${k:0:12} — full suite PASSED $(cat "$s" 2>/dev/null)"
176
+ else
177
+ bad "receipt ${k:0:12} — NO green full-suite stamp for this tree"
178
+ missing=$((missing+1))
179
+ fi
180
+ done <<< "$keys"
181
+ if (( missing )); then
182
+ say ""
183
+ say " $missing of $n_keys lane(s) have no receipt. Mint one on this exact tree:"
184
+ say " cd $WT && DEPLOY_YES=1 bash .claude/scripts/deploy.sh --gates-only"
185
+ say " (a receipt minted on a DIRTY main describes a tree that never ships —"
186
+ say " the mint has to happen here, on the clean tree at $(git -C "$WT" rev-parse --short HEAD))"
187
+ return 3
188
+ fi
189
+
190
+ # 5. tsc, for the same tree. The suite does not typecheck; deploy does it as a
191
+ # separate gate, and a promoted tree should already know the answer.
192
+ if [ "${RELEASE_SKIP_TSC:-0}" != "1" ]; then
193
+ local svc out rc
194
+ for svc in "${TSC_SERVICES[@]}"; do
195
+ out="$( cd "$WT" && TSC_CACHE_WAIT="$TSC_WAIT" bash .claude/scripts/tsc-cached.sh "$svc" 2>/dev/null )"
196
+ rc=$?
197
+ if (( rc != 0 )); then
198
+ bad "tsc $svc — could NOT run (lock or timeout); an unrun check is not a pass"
199
+ return 7
200
+ fi
201
+ if [ "$out" != "0" ]; then bad "tsc $svc — $out error(s)"; return 4; fi
202
+ ok "tsc $svc — 0 errors"
203
+ done
204
+ fi
205
+
206
+ say ""
207
+ say " promoted: $sha"
208
+ say " branch: $BRANCH -> $(git -C "$ROOT" rev-parse --short "$BRANCH")"
209
+ say " worktree: $WT"
210
+ say " distance: $dist"
211
+ local s; for s in "${stamps[@]}"; do say " receipt: $s"; done
212
+ say ""
213
+ say " ship it: bash .claude/scripts/release.sh ship"
214
+ return 0
215
+ }
216
+
217
+ # ── ship ────────────────────────────────────────────────────────────────────
218
+ cmd_ship() {
219
+ [ -d "$WT" ] || { bad "no release worktree at $WT — run promote first"; return 2; }
220
+ local dirty; dirty="$(git -C "$WT" status --porcelain)"
221
+ [ -z "$dirty" ] || { bad "release tree is dirty — refusing to ship an unproven tree"; return 6; }
222
+ local sha; sha="$(git -C "$WT" rev-parse HEAD)"
223
+ say "release: ship ${sha:0:9} from $WT"
224
+
225
+ # Seconds, ships nothing, and catches a credential ladder that cannot resolve
226
+ # from a worktree BEFORE the seven-minute pipeline dies at the same place.
227
+ say ""; say "── credential pre-flight ──"
228
+ ( cd "$WT" && bash .claude/scripts/deploy.sh --check-creds ) || {
229
+ bad "credential ladder did not verify from $WT"; return 8; }
230
+
231
+ say ""; say "── deploy ──"
232
+ local t0=$SECONDS
233
+ # NO --skip-tests. The whole point is that the suite gate hits the memo and
234
+ # reports "REUSED" — forcing the skip would throw away the proof and ship on a
235
+ # promise instead of a receipt.
236
+ #
237
+ # --changed BY DEFAULT. A service with no commit under its svc_paths since its
238
+ # last clean deploy has nothing to ship; deploy.sh's per-service markers live
239
+ # under $WT/.deploy-logs, so they describe THIS tree's ships, never main's.
240
+ # svc_paths already folds packages/ into astro and channels (an SDK-only
241
+ # commit re-ships both), which is what makes the skip safe. First ship from a
242
+ # fresh .release/ has no markers and ships all five. RELEASE_ALL=1 forces it.
243
+ local changed=(--changed); [ "${RELEASE_ALL:-0}" = "1" ] && changed=(--all)
244
+ ( cd "$WT" && DEPLOY_YES=1 bash .claude/scripts/deploy.sh "${changed[@]}" )
245
+ local rc=$?
246
+ say ""
247
+ say " ship wall-clock: $((SECONDS - t0))s"
248
+ (( rc == 0 )) || { bad "deploy exited $rc"; return 8; }
249
+ return 0
250
+ }
251
+
252
+ # ── --self-test — the red proof ─────────────────────────────────────────────
253
+ # Three refusals, each asserted on its OWN exit code: three refusals that all
254
+ # exit 1 would pass a checker that only asks "did it refuse?".
255
+ #
256
+ # Sandboxed on every axis that could touch the real thing: its own TEST_CACHE_DIR
257
+ # (so a planted stamp can never be replayed by a future deploy), its own worktree
258
+ # path, its own throwaway branch. The last assertion is that the REAL cache gained
259
+ # zero files — a fake receipt leaking into it is the single worst outcome this
260
+ # script could have.
261
+ _self_test() {
262
+ local fails=0
263
+ local sbcache sbwt sbbranch base
264
+ sbcache="$(mktemp -d)"; sbwt="$(mktemp -d)/rel"; sbbranch="release-selftest-$$"
265
+ base="$(git -C "$ROOT" rev-parse --verify HEAD)"
266
+ local real_before real_after
267
+ real_before=$(ls "${TMPDIR:-/tmp}/one-test-cache" 2>/dev/null | wc -l | tr -d ' ')
268
+
269
+ _run() { TEST_CACHE_DIR="$sbcache" RELEASE_WORKTREE="$sbwt" RELEASE_BRANCH="$sbbranch" \
270
+ RELEASE_SKIP_TSC=1 bash "${BASH_SOURCE[0]}" promote "$@" >/dev/null 2>&1; }
271
+ _assert() { # <expected-rc> <actual-rc> <label>
272
+ if [ "$1" = "$2" ]; then echo " ok $3 (exit $2)"
273
+ else echo " FAIL $3 — expected exit $1, got $2"; fails=$((fails+1)); fi
274
+ }
275
+
276
+ # 1. a tree with no receipt is REFUSED (3)
277
+ _run "$base"; _assert 3 $? "no receipt refuses"
278
+
279
+ # 2. plant a PASS stamp for every lane key of that tree — promotion succeeds
280
+ local k
281
+ while IFS= read -r k; do
282
+ [ -n "$k" ] && date -u +%Y-%m-%dT%H:%M:%SZ > "$sbcache/selftest-$k.pass"
283
+ done < <(TEST_CACHE_KEY_ONLY=1 bash -c "cd '$sbwt' && bash .claude/scripts/test-full.sh" 2>/dev/null \
284
+ | grep -Eo '^[0-9a-f]{64}$')
285
+ _run "$base"; _assert 0 $? "a planted receipt promotes"
286
+
287
+ # 3. the receipt is bound to the TREE. Append a byte (never `touch` — the key is
288
+ # content, never mtime, so a touch would move nothing and look like a bug),
289
+ # commit it so the tree is clean again, and the same stamps must no longer
290
+ # answer for the new tree.
291
+ local probe="$sbwt/text/dictionary.md"
292
+ [ -f "$probe" ] || probe="$(git -C "$sbwt" ls-files | head -1 | sed "s|^|$sbwt/|")"
293
+ printf '\n<!-- release.sh self-test -->\n' >> "$probe"
294
+ git -C "$sbwt" add -- "${probe#$sbwt/}" >/dev/null 2>&1
295
+ git -C "$sbwt" -c user.email=selftest@one.ie -c user.name=selftest \
296
+ commit -q -m "release.sh self-test: move the tree" >/dev/null 2>&1
297
+ local moved; moved="$(git -C "$sbwt" rev-parse HEAD)"
298
+ _run "$moved"; _assert 3 $? "a moved tree loses the receipt (key follows content)"
299
+
300
+ # 4. and a DIRTY tree is refused before any key is computed
301
+ printf 'dirty\n' >> "$probe"
302
+ _run "$moved"; _assert 6 $? "a dirty release tree refuses"
303
+
304
+ # 5. nothing leaked into the real cache
305
+ real_after=$(ls "${TMPDIR:-/tmp}/one-test-cache" 2>/dev/null | wc -l | tr -d ' ')
306
+ if [ "$real_before" = "$real_after" ]; then ok "no stamp leaked into the real cache ($real_after files)"
307
+ else echo " FAIL self-test wrote to the REAL cache ($real_before -> $real_after)"; fails=$((fails+1)); fi
308
+
309
+ git -C "$ROOT" worktree remove --force "$sbwt" >/dev/null 2>&1
310
+ git -C "$ROOT" branch -D "$sbbranch" >/dev/null 2>&1
311
+ rm -rf "$sbcache"
312
+ [ "$fails" -eq 0 ] && { echo "release: self-test PASS"; return 0; }
313
+ echo "release: self-test FAILED ($fails)"; return 1
314
+ }
315
+
316
+ case "${1:-}" in
317
+ promote) shift; cmd_promote "${1:-}"; exit $? ;;
318
+ ship) cmd_ship; exit $? ;;
319
+ --self-test) _self_test; exit $? ;;
320
+ -h|--help|"") sed -n '2,30p' "${BASH_SOURCE[0]}"; exit 0 ;;
321
+ *) echo "release.sh: unknown verb '${1}' (try --help)" >&2; exit 2 ;;
322
+ esac