@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,311 @@
1
+ #!/usr/bin/env bash
2
+ # deploy-emit.sh — ONE deploy gate, onto the run/event stream every viewer reads.
3
+ #
4
+ # manifest: monorepo-only
5
+ # Same classification as deploy-record.sh, and for the same reason: deploy.sh
6
+ # is its only caller and deploy.sh does not ship. It DOES read a credential —
7
+ # GATEWAY_API_KEY through ONE_ENV_FILE (default one.ie/web/.dev.vars, plus the
8
+ # monorepo fallbacks do-signal.sh walks), honoured so that a future ship needs
9
+ # no change — and no credential is exit 3 BY NAME, never a quiet success and
10
+ # never a skipped gate that reads as a gate that ran.
11
+ #
12
+ # WHY IT EXISTS. `deploy.sh` emitted NOTHING — `grep -n signal deploy.sh` returns
13
+ # two comment lines and no call. A deploy's only trace was a committed JSON file,
14
+ # and `/deploy` says on its face what that costs: "nothing here animates a run
15
+ # that is happening now, because nothing here can know about one". The file stays
16
+ # (a page must render with no network); this makes the RUN a row as well.
17
+ #
18
+ # THE CALLER MUST NEVER LET THIS FAIL A DEPLOY. Every call site in deploy.sh ends
19
+ # in `|| true`, exactly as `deploy-record.sh` is called. This script is a
20
+ # reporter: a gate that passed while the network was down is still a gate that
21
+ # passed, and a deploy that goes red because its telemetry could not phone home
22
+ # is a worse failure than the one it was reporting. The other half of that rule
23
+ # lives in the mapper: a stage that never arrives is ABSENT, never a default
24
+ # frame, so a dropped emit degrades to a gap in the trace and never to a lie.
25
+ #
26
+ # USAGE
27
+ # deploy-emit.sh --run <id> --target <worker> [--stage <gate> --status <s>]
28
+ # [--verdict green|red] [--sha <sha>] [--door <text>]
29
+ # [--slug <ws>] [--env dev] [--reason <text>] [--wall-ms <n>]
30
+ # [--detail k=v]... [--dry-run]
31
+ # deploy-emit.sh --self-test # local red proofs, no network
32
+ #
33
+ # stage : tree credentials typecheck tests build smoke approval migrations
34
+ # ship health (DEPLOY_SPINE_STEPS)
35
+ # status : start | ok | fail (DeployStageStatus)
36
+ # verdict: green | red — the FINAL call, and the only thing that closes the
37
+ # run. Separate from the stages because `--gates-only` is a complete,
38
+ # successful run that never reaches ship or health; closing on the
39
+ # last stage would leave every one of those runs open forever.
40
+ #
41
+ # --run IS HALF THE RUN KEY, and the sha cannot serve. On 2026-09-06 the same sha
42
+ # (af3ba5919) ran gates-only, went red, went green, then shipped — four runs.
43
+ # Keyed on sha they would have interleaved into one. deploy.sh already mints a
44
+ # usable id for its own log name: deploy-20260906-225246.log.
45
+ #
46
+ # NEVER CALL THIS ON --dry-run. A run row for a deploy that did not happen is the
47
+ # same theater the page refuses to render. Enforced at the call site, because a
48
+ # script cannot know its caller's flags.
49
+ #
50
+ # EXITS 0 emitted (or --dry-run printed) · 2 bad arguments · 3 no credential
51
+ # 4 the receiver refused — its own words on stderr · 5 jq missing
52
+ #
53
+ # THE REFUSAL IS THE WHOLE POINT — measured against prod 2026-09-06:
54
+ # unknown receiver → HTTP 200 {"outcome":"dissolved","reason":"unknown_receiver"}
55
+ # forbidden call → HTTP 200 {"outcome":"result","result":{"ok":false,...}}
56
+ # A caller reading curl's exit code, the HTTP status, or `outcome` alone records
57
+ # a gate that never landed. Success is ONE shape: .outcome == "result" AND
58
+ # .result.ok == true.
59
+ #
60
+ # ENV DEPLOY_EMIT_URL default $ONE_API_URL, else https://one.ie
61
+ # DEPLOY_EVENT_RECEIVER default deploy:event
62
+ # ONE_ENV_FILE / DO_ENV_FILE the credential file. Defaults to .dev.vars
63
+ # and the ladder tries it FIRST, mirroring do-board.sh:54-60: BOTH
64
+ # files carry a GATEWAY_API_KEY and PROD REFUSES the one in .env. A
65
+ # refused key is exit 4 — but deploy.sh backgrounds every _emit with
66
+ # both streams discarded and `|| true`, so the whole run would have
67
+ # reported nothing while recording nothing. Reordering the ladder
68
+ # alone would not have fixed it: the default above lands on a file
69
+ # before the ladder is ever consulted.
70
+ # DEPLOY_EMIT_SKIP_FALLBACKS=1 ONE_ENV_FILE is the WHOLE ladder. Without it
71
+ # the no-credential state is unreachable from inside the monorepo, so
72
+ # exit 3 could not be driven red. Same reason and shape as
73
+ # FACTORY_EMIT_SKIP_FALLBACKS / DO_SIGNAL_SKIP_FALLBACKS.
74
+ set -uo pipefail
75
+
76
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
77
+ # .dev.vars, NOT .env — prod refuses the .env key (recorded trap; do-board.sh:56
78
+ # pins the same file for the same reason). Relative, as it has always been: from
79
+ # a subdirectory this misses and the ladder below — which now also leads with
80
+ # .dev.vars — picks it up by absolute path.
81
+ ONE_ENV_FILE="${ONE_ENV_FILE:-${DO_ENV_FILE:-one.ie/web/.dev.vars}}"
82
+ URL="${DEPLOY_EMIT_URL:-${ONE_API_URL:-https://one.ie}}"
83
+ RECEIVER="${DEPLOY_EVENT_RECEIVER:-deploy:event}"
84
+ TIMEOUT="${DEPLOY_EMIT_TIMEOUT:-8}"
85
+
86
+ die() { printf 'deploy-emit: %s\n' "$1" >&2; exit "$2"; }
87
+
88
+ STAGES='tree credentials typecheck tests build smoke approval migrations ship health'
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # --self-test — every LOCAL refusal, driven for real. No network, no credential.
92
+ # Each refusal is asserted by its EXIT CODE, never by its message: a gate this
93
+ # script would have posted anyway is the frame nobody meant.
94
+ # ---------------------------------------------------------------------------
95
+ self_test() {
96
+ local me="${BASH_SOURCE[0]}" fails=0 rc out pl
97
+ _case() {
98
+ local label="$1" want="$2"; shift 2
99
+ out="$(ONE_ENV_FILE=/dev/null DEPLOY_EMIT_SKIP_FALLBACKS=1 bash "$me" "$@" 2>&1)"; rc=$?
100
+ if [ "$rc" = "$want" ]; then printf ' ok %-52s exit %s\n' "$label" "$rc"
101
+ else printf ' FAIL %-52s exit %s, wanted %s\n %s\n' "$label" "$rc" "$want" "$(printf '%s' "$out" | head -1)" >&2
102
+ fails=$((fails + 1)); fi
103
+ }
104
+ printf '== deploy-emit --self-test\n'
105
+ # THE broken shape this exists for: deployEventToRunEvents returns [] for a
106
+ # gate outside the ten, and an empty mapping posted anyway is a lie on the stream.
107
+ _case 'unknown stage is REFUSED before the wire' 2 --run r1 --target one-prod --stage lint --status ok
108
+ _case 'unknown status is REFUSED' 2 --run r1 --target one-prod --stage tests --status green
109
+ _case 'unknown verdict is REFUSED' 2 --run r1 --target one-prod --verdict amber
110
+ _case 'missing --run is REFUSED' 2 --target one-prod --stage tests --status ok
111
+ _case 'missing --target is REFUSED' 2 --run r1 --stage tests --status ok
112
+ # Half a stage frame is not a stage frame — the mapper returns [] for either half.
113
+ _case 'stage without status is REFUSED' 2 --run r1 --target one-prod --stage tests
114
+ _case 'status without stage is REFUSED' 2 --run r1 --target one-prod --status ok
115
+ # A payload that names no stage AND no verdict would upsert a run and say nothing.
116
+ _case 'neither stage nor verdict is REFUSED' 2 --run r1 --target one-prod
117
+ _case 'non-numeric --wall-ms is REFUSED' 2 --run r1 --target one-prod --stage tests --status ok --wall-ms fast
118
+ _case 'malformed --detail is REFUSED' 2 --run r1 --target one-prod --stage tests --status ok --detail nokv
119
+ _case 'unknown argument is REFUSED' 2 --run r1 --target one-prod --stage tests --status ok --wat
120
+ _case 'no credential is exit 3, not a quiet pass' 3 --run r1 --target one-prod --stage tests --status ok
121
+ _case '--dry-run needs no credential' 0 --run r1 --target one-prod --stage build --status ok --dry-run
122
+ _case 'verdict alone is a valid final call' 0 --run r1 --target one-prod --verdict green --dry-run
123
+
124
+ pl="$(ONE_ENV_FILE=/dev/null DEPLOY_EMIT_SKIP_FALLBACKS=1 bash "$me" \
125
+ --run 20260906-225246 --target one-prod --stage tests --status ok --sha af3ba5919 \
126
+ --door './deploy' --slug one --reason none --wall-ms 168000 \
127
+ --detail reused=true --detail waived=false --dry-run 2>/dev/null)"
128
+ if printf '%s' "$pl" | jq -e '.data.run == "20260906-225246" and .data.target == "one-prod"
129
+ and .data.stage == "tests" and .data.status == "ok" and .data.sha == "af3ba5919"
130
+ and .data.slug == "one" and .data.wallMs == 168000
131
+ and .data.detail.reused == true and .data.detail.waived == false' >/dev/null 2>&1; then
132
+ printf ' ok %-52s\n' 'dry-run payload carries every named field'
133
+ else
134
+ printf ' FAIL %-52s\n %s\n' 'dry-run payload is incomplete' "$pl" >&2; fails=$((fails + 1))
135
+ fi
136
+
137
+ # wallMs must be a NUMBER and booleans must be BOOLEANS — zod rejects a string
138
+ # for z.number(), so a quoted 168000 is a receiver refusal at deploy time
139
+ # rather than a test failure here.
140
+ if printf '%s' "$pl" | jq -e '(.data.wallMs | type) == "number"
141
+ and (.data.detail.reused | type) == "boolean"' >/dev/null 2>&1; then
142
+ printf ' ok %-52s\n' 'wallMs is a number, booleans are booleans'
143
+ else
144
+ printf ' FAIL %-52s\n %s\n' 'metric types would be refused by zod' "$pl" >&2; fails=$((fails + 1))
145
+ fi
146
+
147
+ # Optional fields ABSENT rather than empty — zod strips undeclared keys
148
+ # silently, and an empty `slug` is not the same as no slug.
149
+ pl="$(ONE_ENV_FILE=/dev/null DEPLOY_EMIT_SKIP_FALLBACKS=1 bash "$me" \
150
+ --run r --target one-prod --stage tree --status start --dry-run 2>/dev/null)"
151
+ if printf '%s' "$pl" | jq -e '(.data | has("slug") | not) and (.data | has("detail") | not)
152
+ and (.data | has("wallMs") | not) and (.data | has("verdict") | not)' >/dev/null 2>&1; then
153
+ printf ' ok %-52s\n' 'unset optional fields are absent, not empty'
154
+ else
155
+ printf ' FAIL %-52s\n %s\n' 'unset optional fields leaked into the payload' "$pl" >&2; fails=$((fails + 1))
156
+ fi
157
+
158
+ # Every stage in STAGES must be accepted — a typo here silently narrows the
159
+ # pipeline to the gates somebody remembered.
160
+ local st
161
+ for st in $STAGES; do
162
+ ONE_ENV_FILE=/dev/null DEPLOY_EMIT_SKIP_FALLBACKS=1 bash "$me" \
163
+ --run r --target t --stage "$st" --status ok --dry-run >/dev/null 2>&1 \
164
+ || { printf ' FAIL %-52s\n' "stage '$st' rejected by its own script" >&2; fails=$((fails + 1)); }
165
+ done
166
+ printf ' ok %-52s\n' "all 10 DEPLOY_SPINE_STEPS accepted"
167
+
168
+ if [ "$fails" -eq 0 ]; then printf 'deploy-emit --self-test: PASS (17 checks)\n'; return 0; fi
169
+ printf 'deploy-emit --self-test: RED — %s check(s) failed\n' "$fails" >&2; return 1
170
+ }
171
+
172
+ RUN=""; TARGET=""; STAGE=""; STATUS=""; VERDICT=""; SHA=""; DOOR=""
173
+ SLUG=""; ENVW=""; REASON=""; WALLMS=""; DETAILS=""; DRY=0
174
+ while [ $# -gt 0 ]; do
175
+ case "$1" in
176
+ --run) RUN="${2:-}"; shift 2 ;;
177
+ --target) TARGET="${2:-}"; shift 2 ;;
178
+ --stage) STAGE="${2:-}"; shift 2 ;;
179
+ --status) STATUS="${2:-}"; shift 2 ;;
180
+ --verdict) VERDICT="${2:-}"; shift 2 ;;
181
+ --sha) SHA="${2:-}"; shift 2 ;;
182
+ --door) DOOR="${2:-}"; shift 2 ;;
183
+ --slug) SLUG="${2:-}"; shift 2 ;;
184
+ --env) ENVW="${2:-}"; shift 2 ;;
185
+ --reason) REASON="${2:-}"; shift 2 ;;
186
+ --wall-ms) WALLMS="${2:-}"; shift 2 ;;
187
+ --detail) DETAILS="${DETAILS}${DETAILS:+$'\n'}${2:-}"; shift 2 ;;
188
+ --dry-run) DRY=1; shift ;;
189
+ --self-test) command -v jq >/dev/null 2>&1 || die "jq not found" 5; self_test; exit $? ;;
190
+ -h|--help) sed -n '2,48p' "${BASH_SOURCE[0]}"; exit 0 ;;
191
+ *) die "unknown argument: $1" 2 ;;
192
+ esac
193
+ done
194
+
195
+ command -v jq >/dev/null 2>&1 || die "jq not found — this script builds and reads JSON with it, and a hand-rolled parser is how a refusal becomes a pass" 5
196
+ [ -n "$RUN" ] || die "--run <id> is required — it is half the run key (deploy:<target>:<run>, event.ts deployRunKey). The sha cannot serve: one sha ships repeatedly and the attempts would interleave" 2
197
+ [ -n "$TARGET" ] || die "--target <worker> is required — the other half of the run key, so 'did pay go out' does not need disambiguating by timestamp" 2
198
+
199
+ # Half a stage frame is not a stage frame. The mapper returns [] for either half
200
+ # alone, so refusing here keeps the wire and the mapper saying the same thing.
201
+ if [ -n "$STAGE" ] || [ -n "$STATUS" ]; then
202
+ [ -n "$STAGE" ] || die "--status without --stage — deployEventToRunEvents returns [] for half a frame, and guessing the other half records a frame nobody meant" 2
203
+ [ -n "$STATUS" ] || die "--stage without --status — same reason" 2
204
+ case " $STAGES " in
205
+ *" $STAGE "*) ;;
206
+ *) die "--stage must be one of: $STAGES (got '$STAGE') — deployEventToRunEvents returns [] for anything else, and an empty mapping posted anyway is a frame nobody meant" 2 ;;
207
+ esac
208
+ case "$STATUS" in
209
+ start|ok|fail) ;;
210
+ *) die "--status must be start|ok|fail (got '$STATUS'). There is deliberately no 'skip': a gate that did not run must be ABSENT, and an unrun gate is never a pass" 2 ;;
211
+ esac
212
+ fi
213
+
214
+ if [ -n "$VERDICT" ]; then
215
+ case "$VERDICT" in
216
+ green|red) ;;
217
+ *) die "--verdict must be green|red (got '$VERDICT')" 2 ;;
218
+ esac
219
+ fi
220
+
221
+ [ -n "$STAGE" ] || [ -n "$VERDICT" ] || \
222
+ die "nothing to say: pass --stage/--status, or --verdict, or both. A payload naming neither would upsert a run and record no frame" 2
223
+
224
+ if [ -n "$WALLMS" ]; then
225
+ case "$WALLMS" in
226
+ ''|*[!0-9]*) die "--wall-ms wants an integer of milliseconds (got '$WALLMS') — the receiver types it z.number() and would refuse a string at deploy time" 2 ;;
227
+ esac
228
+ fi
229
+
230
+ # k=v, with `true`/`false`/integers kept as JSON scalars. `reused` and `waived`
231
+ # are the two metrics this whole feature exists to carry, and both are booleans:
232
+ # quoting them would make `waived:"false"` truthy in every reader.
233
+ detail_json='{}'
234
+ if [ -n "$DETAILS" ]; then
235
+ while IFS= read -r kv; do
236
+ [ -n "$kv" ] || continue
237
+ case "$kv" in *=*) ;; *) die "--detail wants k=v, got: $kv" 2 ;; esac
238
+ k="${kv%%=*}"; v="${kv#*=}"
239
+ case "$v" in
240
+ true|false) detail_json="$(jq -n --argjson d "$detail_json" --arg k "$k" --argjson v "$v" '$d + {($k): $v}')" ;;
241
+ ''|*[!0-9]*) detail_json="$(jq -n --argjson d "$detail_json" --arg k "$k" --arg v "$v" '$d + {($k): $v}')" ;;
242
+ *) detail_json="$(jq -n --argjson d "$detail_json" --arg k "$k" --argjson v "$v" '$d + {($k): $v}')" ;;
243
+ esac
244
+ [ -n "$detail_json" ] || die "could not build --detail object" 2
245
+ done <<EOF
246
+ $DETAILS
247
+ EOF
248
+ fi
249
+
250
+ payload="$(jq -n \
251
+ --arg run "$RUN" --arg target "$TARGET" --arg stage "$STAGE" --arg status "$STATUS" \
252
+ --arg verdict "$VERDICT" --arg sha "$SHA" --arg door "$DOOR" --arg slug "$SLUG" \
253
+ --arg env "$ENVW" --arg reason "$REASON" --arg wallms "$WALLMS" \
254
+ --argjson detail "$detail_json" '
255
+ {data: ({run: $run, target: $target}
256
+ + (if $stage != "" then {stage: $stage} else {} end)
257
+ + (if $status != "" then {status: $status} else {} end)
258
+ + (if $verdict != "" then {verdict: $verdict} else {} end)
259
+ + (if $sha != "" then {sha: $sha} else {} end)
260
+ + (if $door != "" then {door: $door} else {} end)
261
+ + (if $slug != "" then {slug: $slug} else {} end)
262
+ + (if $env != "" then {env: $env} else {} end)
263
+ + (if $reason != "" then {reason: $reason} else {} end)
264
+ + (if $wallms != "" then {wallMs: ($wallms | tonumber)} else {} end)
265
+ + (if ($detail | length) > 0 then {detail: $detail} else {} end))}')" \
266
+ || die "could not build the payload" 2
267
+
268
+ if [ "$DRY" = 1 ]; then printf '%s\n' "$payload"; exit 0; fi
269
+
270
+ # The key never reaches argv (ps) and never reaches stdout. --config, chmod 600,
271
+ # removed on every path. Same shape as factory-emit.sh and do-signal.sh.
272
+ _gateway_key() {
273
+ local f v
274
+ # .dev.vars BEFORE .env, both here and in the default above. Two changes, not
275
+ # one: whichever is consulted first must be the file prod accepts.
276
+ for f in "$ONE_ENV_FILE" "$ROOT/one.ie/web/.dev.vars" "$ROOT/one.ie/web/.env" "$ROOT/.env"; do
277
+ [ -f "$f" ] || continue
278
+ if [ "${DEPLOY_EMIT_SKIP_FALLBACKS:-0}" = "1" ] && [ "$f" != "$ONE_ENV_FILE" ]; then continue; fi
279
+ v="$(grep -E '^GATEWAY_API_KEY=' "$f" 2>/dev/null | head -1 | sed 's/^[^=]*=//;s/^"//;s/"$//')"
280
+ [ -n "$v" ] && { printf '%s' "$v"; return 0; }
281
+ done
282
+ printf ''
283
+ }
284
+
285
+ KEYVAL="$(_gateway_key)"
286
+ [ -n "$KEYVAL" ] || die "no GATEWAY_API_KEY in $ONE_ENV_FILE (or the monorepo fallbacks) — an unauthenticated emit is refused at the door and would look exactly like a gate that never happened" 3
287
+
288
+ cfg="$(mktemp)"; chmod 600 "$cfg"
289
+ trap 'rm -f "$cfg"' EXIT INT TERM
290
+ printf 'header = "Authorization: Bearer %s"\n' "$KEYVAL" > "$cfg"
291
+
292
+ resp="$(curl -sS -X POST "$URL/api/ask/$RECEIVER" \
293
+ -H 'content-type: application/json' --config "$cfg" \
294
+ --max-time "$TIMEOUT" -d "$payload" -w $'\n%{http_code}' 2>&1)"; rc=$?
295
+ rm -f "$cfg"; trap - EXIT INT TERM
296
+
297
+ code="$(printf '%s' "$resp" | tail -1)"
298
+ body="$(printf '%s' "$resp" | sed '$d')"
299
+ ok="$(printf '%s' "$body" | jq -r 'if (.outcome == "result" and (.result.ok == true)) then "yes" else "no" end' 2>/dev/null)"
300
+
301
+ if [ "$rc" -ne 0 ] || [ "$ok" != "yes" ]; then
302
+ why="$(printf '%s' "$body" | jq -r '.reason // .result.error // .error // .hint // empty' 2>/dev/null)"
303
+ [ -n "$why" ] || why="curl rc=$rc http=${code:-none} body=$(printf '%s' "$body" | head -c 200)"
304
+ printf 'deploy-emit: RED run=%s target=%s stage=%s%s http=%s — %s\n' \
305
+ "$RUN" "$TARGET" "${STAGE:-none}" "${VERDICT:+ verdict=$VERDICT}" "${code:-none}" "$why" >&2
306
+ exit 4
307
+ fi
308
+
309
+ printf 'deploy-emit: ok run=%s target=%s stage=%s%s receiver=%s\n' \
310
+ "$RUN" "$TARGET" "${STAGE:-none}" "${VERDICT:+ verdict=$VERDICT}" "$RECEIVER"
311
+ exit 0
@@ -83,6 +83,153 @@ else
83
83
  fi
84
84
  rm -rf "$_mc"
85
85
  fi
86
+ # --- the memo probe: a hit must never queue for a slot ----------------------
87
+ # deploy's vitest gate runs under gate-run.sh, so it TOOK A SLOT and only then
88
+ # asked the memo whether there was anything to run. Measured 2026-09-13: 300s
89
+ # queued, then both lanes reported cache HIT.
90
+ #
91
+ # The block is EXTRACTED VERBATIM from deploy.sh and driven against a fake root
92
+ # and a SANDBOX cache -- a copy here would prove only that the copy works. Every
93
+ # axis that could touch the real thing is sandboxed: TEST_CACHE_DIR (a planted
94
+ # stamp replayed by a real deploy is the worst thing this file could do) and
95
+ # GOVERN_DIR (the only direct evidence that no slot was taken). The last two
96
+ # assertions are that neither real directory gained a file.
97
+ _probe_block="$(awk '/^# >>> vitest-memo-probe/{f=1} f{print} f&&/^# <<< vitest-memo-probe/{exit}' "$DEPLOY")"
98
+ _report_block="$(awk '/elif grep -q .SKIPPED by the memo probe./{f=1} f{print} f&&/^ *ok "\$TESTS_REPORT"$/{exit}' "$DEPLOY")"
99
+ if [ -z "$_probe_block" ] || [ -z "$_report_block" ]; then
100
+ echo "FAIL: could not extract the memo-probe block(s) from deploy.sh — this checker is blind"; fails=$((fails+1))
101
+ else
102
+ _sbcache="$(mktemp -d)"; _sbgov="$(mktemp -d)"; _froot="$(mktemp -d)"
103
+ _real_cache="${TMPDIR:-/tmp}/one-test-cache"
104
+ K1="$(printf 'lane-pool' | shasum -a 256 | cut -d' ' -f1)"
105
+ K2="$(printf 'lane-typedb' | shasum -a 256 | cut -d' ' -f1)"
106
+ mkdir -p "$_froot/.claude/scripts"
107
+ # a stand-in for test-full.sh under TEST_CACHE_KEY_ONLY=1: two lanes, two keys,
108
+ # and the chatter test-lanes.sh prints around them.
109
+ { echo 'echo "test-lanes: pool lane (8 forks) + typedb lane (19 files, serial) — concurrent"'
110
+ echo "echo $K1"; echo "echo $K2"; } > "$_froot/.claude/scripts/test-full.sh"
111
+
112
+ _drive_probe() { # <SKIP_TESTS> <want_astro> <ROOT> — prints a transcript
113
+ local logd; logd="$(mktemp -d)"
114
+ SKIP_TESTS_IN="$1" WANT_IN="$2" ROOT_IN="$3" LOGD_IN="$logd" \
115
+ TEST_CACHE_DIR="$_sbcache" GOVERN_DIR="$_sbgov" \
116
+ bash -c '
117
+ set -uo pipefail
118
+ say(){ printf "SAY %s\n" "$*"; }
119
+ ok(){ printf "OK %s\n" "$*"; }
120
+ _spine_stage(){ [ "$1" = vitest ] && echo tests; }
121
+ _emit(){ printf "EMIT %s\n" "$*"; }
122
+ # the stub records a heavy registration the way the real one does, so
123
+ # HEAVY=0 on the skip path is a fact and not an artefact of the stub.
124
+ gate_start_heavy(){ printf "GATE_STARTED %s\n" "$1"; HEAVY_IDX+=(1); }
125
+ ROOT="$ROOT_IN"; LOG_DIR="$LOGD_IN"; STAMP="probe"; DRY=0
126
+ SKIP_TESTS="$SKIP_TESTS_IN"; want_astro="$WANT_IN"
127
+ GATE_NAMES=(); GATE_PIDS=(); GATE_LOGS=(); GATE_RC=(); GATE_T=(); GATE_DUR=(); HEAVY_IDX=()
128
+ '"$_probe_block"'
129
+ printf "GATES=%s RC=%s HEAVY=%s\n" "${#GATE_NAMES[@]}" "${GATE_RC[0]:-none}" "${#HEAVY_IDX[@]}"
130
+ if [ "${#GATE_LOGS[@]}" -gt 0 ]; then sed "s/^/LOG /" "${GATE_LOGS[0]}"; fi
131
+ exit 0'
132
+ rm -rf "$logd"
133
+ }
134
+
135
+ # (a) every lane stamped ⇒ the gate is SKIPPED, counted, and passed
136
+ date -u +%Y-%m-%dT%H:%M:%SZ > "$_sbcache/one.ie_web-$K1.pass"
137
+ date -u +%Y-%m-%dT%H:%M:%SZ > "$_sbcache/one.ie_web-$K2.pass"
138
+ _p_hit="$(_drive_probe 0 1 "$_froot")"
139
+ case "$_p_hit" in
140
+ *GATE_STARTED*) echo "FAIL: every lane stamped and the gate still took a slot"; fails=$((fails+1)) ;;
141
+ *) echo "ok: every lane stamped — the gate is skipped, no slot taken" ;;
142
+ esac
143
+ case "$_p_hit" in
144
+ *"GATES=1 RC=0"*) echo "ok: the skipped gate is still COUNTED as a passed gate" ;;
145
+ *) echo "FAIL: a memo-skipped gate was not recorded as a passed gate: $_p_hit"; fails=$((fails+1)) ;;
146
+ esac
147
+ # The hazard a stubbed gate_start_heavy would hide: a placeholder pid in
148
+ # HEAVY_IDX makes the NEXT heavy gate `wait 0` (exit 127 — verified) and
149
+ # overwrite this gate's rc with 1. A gate with no process is not a heavy gate.
150
+ case "$_p_hit" in
151
+ *HEAVY=0*) echo "ok: the skipped gate is not in HEAVY_IDX (no wait on a non-child)" ;;
152
+ *) echo "FAIL: a process-less gate entered HEAVY_IDX — the next heavy gate will mark it FAILED"; fails=$((fails+1)) ;;
153
+ esac
154
+ # …and the report it produces must say REUSED, never "all pass", and carry the
155
+ # stamp's timestamp. Driven through deploy.sh's OWN report branch, extracted.
156
+ _skiplog="$(mktemp)"
157
+ printf '%s\n' "$_p_hit" | sed -n 's/^LOG //p' > "$_skiplog"
158
+ _rep="$( glog="$_skiplog" bash -c '
159
+ ok(){ printf "OK %s\n" "$*"; }; TESTS_REPORT=""
160
+ if false; then :
161
+ '"$_report_block"'
162
+ fi
163
+ printf "REPORT=%s\n" "$TESTS_REPORT"' )"
164
+ case "$_rep" in
165
+ *"REUSED — identical inputs already passed"*"already PASSED 20"*)
166
+ echo "ok: the skip reports REUSED with the stamp's timestamp" ;;
167
+ *) echo "FAIL: the skip did not report REUSED with a timestamp: $_rep"; fails=$((fails+1)) ;;
168
+ esac
169
+ case "$_rep" in
170
+ *"all pass"*) echo "FAIL: a memo hit was reported as 'all pass'"; fails=$((fails+1)) ;;
171
+ *) echo "ok: a reused pass is never reported as 'all pass'" ;;
172
+ esac
173
+ # (b) RED HALF — ONE lane unstamped ⇒ the gate RUNS. This is the whole reason
174
+ # the probe does not read test-lanes.sh's exit code: under
175
+ # TYPEDB_LANE_NONBLOCKING=1 that code is the POOL lane's alone, so a pool-only
176
+ # hit would skip a typedb lane that had never run.
177
+ rm -f "$_sbcache/one.ie_web-$K2.pass"
178
+ _p_miss="$(_drive_probe 0 1 "$_froot")"
179
+ case "$_p_miss" in
180
+ *GATE_STARTED*) echo "ok: one lane unstamped — the gate RUNS (pool-only hit cannot skip it)" ;;
181
+ *) echo "FAIL: a pool-only hit skipped the gate — the typedb lane would never run"; fails=$((fails+1)) ;;
182
+ esac
183
+ case "$_p_miss" in *"memo probe: MISS"*) echo "ok: the miss is named in the report" ;;
184
+ *) echo "FAIL: a miss ran the gate without saying why"; fails=$((fails+1)) ;; esac
185
+ # (c) RED HALF — a probe that cannot answer runs the gate. An unrun check is
186
+ # not a pass, and neither is an unanswerable one.
187
+ echo 'exit 1' > "$_froot/.claude/scripts/test-full.sh"
188
+ _p_unk="$(_drive_probe 0 1 "$_froot")"
189
+ case "$_p_unk" in
190
+ *GATE_STARTED*) echo "ok: a probe that cannot answer runs the gate" ;;
191
+ *) echo "FAIL: an unanswerable probe skipped the gate"; fails=$((fails+1)) ;;
192
+ esac
193
+ # (d) --skip-tests keeps its own meaning: no gate at all, even with every lane
194
+ # stamped. A skipped suite is not a reused one.
195
+ { echo "echo $K1"; } > "$_froot/.claude/scripts/test-full.sh"
196
+ date -u +%Y-%m-%dT%H:%M:%SZ > "$_sbcache/one.ie_web-$K1.pass"
197
+ _p_skip="$(_drive_probe 1 1 "$_froot")"
198
+ case "$_p_skip" in
199
+ *GATE_STARTED*|*GATES=1*) echo "FAIL: --skip-tests registered a gate"; fails=$((fails+1)) ;;
200
+ *) echo "ok: --skip-tests still skips distinctly (no gate, no memo report)" ;;
201
+ esac
202
+ # the real test-full.sh must actually print a key per lane, or the probe above
203
+ # answers `2` forever and the change buys nothing. KEY_ONLY runs no vitest.
204
+ if [ -d "$ROOT/one.ie/web" ]; then
205
+ _rkeys="$( cd "$ROOT" && TEST_CACHE_KEY_ONLY=1 TEST_CACHE_DIR="$_sbcache" \
206
+ bash "$ROOT/.claude/scripts/test-full.sh" 2>/dev/null | grep -Ec '^[0-9a-f]{64}$' )"
207
+ if [ "${_rkeys:-0}" -ge 2 ]; then echo "ok: the real full suite prints $_rkeys lane keys (probe can answer)"
208
+ else echo "FAIL: the real full suite printed ${_rkeys:-0} lane key(s) — the probe cannot answer"; fails=$((fails+1)); fi
209
+ else
210
+ echo "n/a: no one.ie/web here — the real lane-key probe cannot run (NOT a pass)"
211
+ fi
212
+ # NOTHING MAY LEAK — and the assertion is by CONTENT, not by count. release.sh's
213
+ # self-test compares file counts either side (release.sh:304-307); here that is a
214
+ # FALSE RED waiting to happen, because both directories are machine-wide and
215
+ # every other session on the box writes to them. Measured while writing this:
216
+ # the real governor dir went 161 -> 162 -> 163 inside one minute under two
217
+ # neighbouring `gate-run.sh verify-fast` processes, with nothing here running.
218
+ # So ask the precise question instead: did a PLANTED stamp reach the real cache?
219
+ _leaked=0
220
+ for _k in "$K1" "$K2"; do
221
+ for _f in "$_real_cache"/*"$_k".pass; do [ -f "$_f" ] && _leaked=$((_leaked+1)); done
222
+ done
223
+ if [ "$_leaked" -eq 0 ]; then echo "ok: no planted stamp leaked into the real cache"
224
+ else echo "FAIL: $_leaked planted stamp(s) reached the REAL cache — a future deploy could replay them"; fails=$((fails+1)); fi
225
+ # …and the slot. GOVERN_DIR is exported into the driven block, so a gate-run
226
+ # underneath it would leave its lock/slot HERE. An empty sandbox is the direct
227
+ # evidence that the hit path took no slot — the whole point of the change.
228
+ if [ -z "$(ls "$_sbgov" 2>/dev/null)" ]; then echo "ok: the probe took no governor slot at all (sandbox GOVERN_DIR empty)"
229
+ else echo "FAIL: the probe touched its governor dir — it is taking a slot"; fails=$((fails+1)); fi
230
+ rm -rf "$_sbcache" "$_sbgov" "$_froot" "$_skiplog"
231
+ fi
232
+
86
233
  # --- the deferred-pin ledger: driven, not grepped ---------------------------
87
234
  # Extract deploy.sh's debt block verbatim and run it with stubbed reporters. The
88
235
  # block is the last chance a deferred pin has to come due: verify-fast.sh settles
@@ -118,6 +265,14 @@ else
118
265
  if _drive_debt "pins deferred at cycle C3" 0 "Tests 10780 passed"; then
119
266
  echo "ok: a green full suite settles the debt and the deploy continues"
120
267
  else echo "FAIL: a green full suite did not settle the debt"; fails=$((fails+1)); fi
268
+ # …and a suite the MEMO PROBE reused pays the debt exactly as a cache HIT does.
269
+ # The probe skips a gate whose lanes are all green stamps, so the pins ran when
270
+ # those stamps were minted — but the debt block reads TESTS_REPORT, so the new
271
+ # wording has to keep settling it or a memo hit would refuse every deploy.
272
+ if _drive_debt "pins deferred at cycle C3" 0 \
273
+ "full suite REUSED — identical inputs already passed (memo probe, no slot taken: already PASSED 2026-09-13T00:00:00Z)"; then
274
+ echo "ok: a memo-probe REUSED suite settles the debt"
275
+ else echo "FAIL: a memo-probe REUSED suite did not settle the debt"; fails=$((fails+1)); fi
121
276
  # and an EMPTY ledger must never block anything
122
277
  if _drive_debt "" 1 "skipped"; then echo "ok: no debt, no refusal"
123
278
  else echo "FAIL: an empty ledger blocked the deploy"; fails=$((fails+1)); fi
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ # deploy-ready.sh — say which branch the next deploy will take, BEFORE it takes it.
3
+ #
4
+ # manifest: monorepo-only
5
+ #
6
+ # WHY THIS EXISTS. deploy.sh prices its heavy gates in memory, not cores:
7
+ # vitest (~6GB) and the astro build (--max-old-space-size=8192) overlap only
8
+ # when `gate_mem_avail_mb` reports >= DEPLOY_HEAVY_NEED_GB (default 14) at the
9
+ # moment the gates start. deploy.sh's own comment carries the measurement:
10
+ #
11
+ # overlapped 176s
12
+ # serialised 1145s
13
+ #
14
+ # That is a 6.5x swing decided by ONE sample taken once, at gate-start, and
15
+ # nothing in the pipeline records which way it went. Two runs on a byte-identical
16
+ # tree can differ by 6.5x and the ledger cannot say why.
17
+ #
18
+ # So this is not a new gate and it changes no behaviour. It reads the same
19
+ # probe deploy.sh reads, through the same function, and prints the branch the
20
+ # deploy WOULD take. It turns a coin-flip into a decision.
21
+ #
22
+ # THE PROBE IS THE HONEST ONE. `memory_pressure`'s "free percentage" counts file
23
+ # cache, purgeable and compressed pages: measured 2026-09-03 it read 74% on a box
24
+ # with 1.3% actually free and 9.4GB in swap. gate_mem_avail_mb is what
25
+ # lib/govern.sh and deploy.sh both use; this reads it rather than inventing a
26
+ # second definition that drifts.
27
+ #
28
+ # Exit codes, so a caller can branch on it:
29
+ # 0 the fast branch — heavy gates will overlap
30
+ # 1 the slow branch — they will serialise (deploy still WORKS, just ~6.5x)
31
+ # 2 the probe could not read memory
32
+ #
33
+ # It never refuses and never kills anything: what to do about a short box is the
34
+ # operator's call, and a pre-flight that took that call would be a gate.
35
+ set -uo pipefail
36
+
37
+ ROOT="${DEPLOY_READY_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
38
+ . "$ROOT/.claude/scripts/lib/govern.sh"
39
+
40
+ NEED_GB="${DEPLOY_HEAVY_NEED_GB:-14}"
41
+
42
+ avail_mb="$(gate_mem_avail_mb 2>/dev/null || echo "")"
43
+ if [ -z "$avail_mb" ]; then
44
+ echo " RED memory probe unreadable — deploy.sh will assume 999GB and OVERLAP"
45
+ echo " (a broken sensor must never silently serialise the pipeline, so"
46
+ echo " the fallback is the fast branch. On a short box that pages.)"
47
+ exit 2
48
+ fi
49
+ free_gb=$(( avail_mb / 1024 ))
50
+
51
+ swap="$(sysctl -n vm.swapusage 2>/dev/null || echo '')"
52
+ load="$(uptime | sed 's/.*load averages*: //')"
53
+ cores="$(sysctl -n hw.ncpu 2>/dev/null || echo '?')"
54
+
55
+ printf '\n deploy-ready — will the heavy gates overlap?\n\n'
56
+ printf ' free memory %sGB (need >= %sGB)\n' "$free_gb" "$NEED_GB"
57
+ printf ' swap %s\n' "$swap"
58
+ printf ' load %s on %s cores\n' "$load" "$cores"
59
+
60
+ # What is holding memory right now, so "unthrash" is actionable rather than advice.
61
+ printf '\n top memory holders:\n'
62
+ ps -Ao rss=,comm=,args= | sort -rn | head -6 | while read -r rss comm rest; do
63
+ printf ' %6s MB %s\n' "$((rss/1024))" "$(printf '%.58s' "$comm $rest")"
64
+ done
65
+
66
+ printf '\n'
67
+ if (( free_gb >= NEED_GB )); then
68
+ printf ' ok OVERLAP — vitest and build run together (measured ~176s)\n\n'
69
+ exit 0
70
+ fi
71
+ printf ' RED SERIALISE — vitest then build, one after the other\n'
72
+ printf ' measured 1145s vs 176s: this is the 6.5x branch\n'
73
+ printf ' short by %sGB. Free memory before shipping — browsers and\n' "$(( NEED_GB - free_gb ))"
74
+ printf ' sessions cost RAM; worktrees cost only disk, so sweeping\n'
75
+ printf ' trees is hygiene, not the fix.\n\n'
76
+ printf ' Do NOT set DEPLOY_HEAVY_PARALLEL=1 to force the fast branch:\n'
77
+ printf ' overlapping ~7GB gates on a short box is what measured 1145s.\n\n'
78
+ exit 1