@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
@@ -9,7 +9,11 @@
9
9
  # do-signal.sh [--print] <type> <slug> <tier> <cycle> [key=value ...]
10
10
  # do-signal.sh [--print] --task-create <slug> <title> [tags=a,b,c] [notes=...]
11
11
  # do-signal.sh [--print] --task-claim <slug>
12
+ # do-signal.sh [--print] --task-plan <slug> # one task per cycle
13
+ # do-signal.sh [--print] --task-reap
12
14
  # do-signal.sh [--print] --task-link <slug> [docs=a,b,c]
15
+ # do-signal.sh [--print] --task-heartbeat <slug> <body>
16
+ # do-signal.sh [--print] --outcome <slug> tags=a,b actor=<aid> success=<bool> [weight=<n>]
13
17
  #
14
18
  # --print: write JSON to stdout without POSTing (tests + dry runs).
15
19
  # Extra key=value pairs are merged into the world:do-event payload.
@@ -23,7 +27,9 @@
23
27
  # ONE_ENV_FILE default: one.ie/web/.env (read SERVER_SECRET for auth)
24
28
  # DO_ENV_FILE accepted as an alias for ONE_ENV_FILE (kept for callers)
25
29
  #
26
- # Signal failure never blocks the build loop — callers wrap with `|| true`.
30
+ # world:do-event failure never blocks the build loop — callers wrap with `|| true`.
31
+ # tasks:* (create/claim/link/heartbeat) propagate failure so callers can log it;
32
+ # they must still not abort the build (capture the exit, continue).
27
33
  set -euo pipefail
28
34
 
29
35
  PRINT=false
@@ -45,6 +51,10 @@ _server_secret() {
45
51
  local _f _v
46
52
  for _f in "$ONE_ENV_FILE" "$_ROOT/.env" $_MONOREPO_ENV_CANDIDATES; do
47
53
  [ -f "$_f" ] || continue
54
+ # DO_SIGNAL_SKIP_FALLBACKS=1 → ONE_ENV_FILE is the WHOLE ladder. Without it the
55
+ # no-secret state is unreachable from inside the monorepo (_ROOT resolves to a
56
+ # tree that carries a real .env), so the dark-cycle guard could not be proven.
57
+ if [ "${DO_SIGNAL_SKIP_FALLBACKS:-0}" = "1" ] && [ "$_f" != "$ONE_ENV_FILE" ]; then continue; fi
48
58
  _v=$(grep -E '^SERVER_SECRET=' "$_f" 2>/dev/null | head -1 | sed 's/^[^=]*=//;s/^"//;s/"$//' || true)
49
59
  [ -n "$_v" ] && { printf '%s' "$_v"; return 0; }
50
60
  done
@@ -66,24 +76,39 @@ _gateway_key() {
66
76
  printf ''
67
77
  }
68
78
 
69
- # POST a JSON payload to /api/ask/<receiver> with bearer auth. Fire-and-forget:
70
- # a failure is silent so the build loop is never blocked (same stance as the
71
- # world:do-event POST below). Prints the response body (empty on failure).
79
+ # POST a JSON payload to /api/ask/<receiver> with bearer auth. Board writes
80
+ # (tasks:*) must not disguise failure as ok: curl errors and a body without
81
+ # `"ok":true` (HTTP 200 wrapping {error:...} is the TypeDB-down case) exit
82
+ # non-zero. Prints the response body. Callers log and continue.
72
83
  _post_ask() {
73
- local _receiver="$1" _payload="$2" _secret _authcfg _url _resp
84
+ local _receiver="$1" _payload="$2" _secret _authcfg _url _resp _rc=0
74
85
  _secret="$(_gateway_key)"
75
86
  _url="${DO_SIGNAL_URL}/api/ask/${_receiver}"
76
87
  if [ -n "$_secret" ]; then
77
88
  _authcfg="$(mktemp)"; chmod 600 "$_authcfg"
78
89
  printf 'header = "Authorization: Bearer %s"\n' "$_secret" > "$_authcfg"
79
- _resp=$(curl -sf -X POST "$_url" -H "Content-Type: application/json" --config "$_authcfg" -d "$_payload" 2>/dev/null || true)
90
+ _resp=$(curl -sf -X POST "$_url" -H "Content-Type: application/json" --config "$_authcfg" -d "$_payload" 2>/dev/null) || _rc=$?
80
91
  rm -f "$_authcfg"
81
92
  else
82
- _resp=$(curl -sf -X POST "$_url" -H "Content-Type: application/json" -d "$_payload" 2>/dev/null || true)
93
+ _resp=$(curl -sf -X POST "$_url" -H "Content-Type: application/json" -d "$_payload" 2>/dev/null) || _rc=$?
83
94
  fi
84
95
  printf '%s' "$_resp"
96
+ if [ "$_rc" -eq 0 ]; then
97
+ case "$_resp" in
98
+ *'"ok":true'*) ;;
99
+ *) _rc=1 ;;
100
+ esac
101
+ fi
102
+ return "$_rc"
85
103
  }
86
104
 
105
+ # _jsonesc — JSON-escape a value before it is interpolated into a payload
106
+ # string. EVERY free-text field must go through this. Measured 2026-08-26: a
107
+ # task filed with `notes=accept: ! grep -q "x" f.ts` produced
108
+ # "notes":"accept: ! grep -q "x" f.ts"
109
+ # — malformed JSON, and the create failed. Titles carry quotes just as often.
110
+ _jsonesc() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g' | tr -d '\n\r'; }
111
+
87
112
  # Every task this harness files lands in one workspace — /u/one/tasks — so a
88
113
  # human never has to hunt across slugs for Claude Code's own work. Overridable
89
114
  # for a differently-branded harness fork; unset means "one" (same convention
@@ -93,6 +118,23 @@ _post_ask() {
93
118
  # session happened to be attested.
94
119
  CC_TASKS_WORKSPACE="${CC_TASKS_WORKSPACE:-one}"
95
120
 
121
+ # CC_TASKS_AUTHOR — WHO the comment is from, when it is not simply "the
122
+ # workspace". tasks:comment records an author on the message and the inbox draws
123
+ # it (three directors on one task read as three speakers, not one anonymous
124
+ # run), but a field no caller supplies is a field production never sets: this
125
+ # script and the MCP door are the two writers, and both built their payload with
126
+ # tid/workspace/body only, so every /do close and every wave heartbeat landed
127
+ # `authorKind:"staff"`. The receiver honours the field only for an ATTESTED
128
+ # staff caller — which is exactly what GATEWAY_API_KEY auth above makes this
129
+ # script — and validates the slug shape; a caller that cannot be attested names
130
+ # nobody. UNSET is the default and emits the byte-identical payload this script
131
+ # has always sent, so nothing changes for a harness that does not set it.
132
+ CC_TASKS_AUTHOR="${CC_TASKS_AUTHOR:-}"
133
+ _author_json() { # emits `,"author":"<slug>"` or nothing
134
+ [ -n "$CC_TASKS_AUTHOR" ] || return 0
135
+ printf ',"author":"%s"' "$CC_TASKS_AUTHOR"
136
+ }
137
+
96
138
  # signal_tasks_create — plan arm: files an open task tagged slug:<slug>, so the
97
139
  # plan appears in tasks:everywhere the moment it's armed (C1/C2 interface contract).
98
140
  signal_tasks_create() { # signal_tasks_create <slug> <title> [tags=a,b,c] [notes=...]
@@ -105,26 +147,184 @@ signal_tasks_create() { # signal_tasks_create <slug> <title> [tags=a,b,c] [note
105
147
  esac
106
148
  done
107
149
  local tags_json; tags_json=$(printf '%s' "$tags" | awk -F',' '{for(i=1;i<=NF;i++){printf "%s\"%s\"", (i>1?",":""), $i}}')
108
- local payload="{\"data\":{\"title\":\"${title}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"tags\":[${tags_json}]${notes:+,\"notes\":\"${notes}\"}}}"
150
+ local title_esc notes_esc
151
+ title_esc="$(_jsonesc "$title")"; notes_esc="$(_jsonesc "$notes")"
152
+ local payload="{\"data\":{\"title\":\"${title_esc}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"tags\":[${tags_json}]${notes:+,\"notes\":\"${notes_esc}\"}}}"
109
153
  if $PRINT; then printf '%s\n' "$payload"; return 0; fi
110
154
  _post_ask "tasks:create" "$payload"
111
155
  }
112
156
 
157
+ # signal_tasks_plan — file EVERY cycle of a plan as its own task, once.
158
+ #
159
+ # WHY. The loop filed exactly ONE task per plan (do-auto.sh's `.task-created`
160
+ # sentinel) and nothing else. Measured 2026-09-01: text/lifecycle-todo.md carries
161
+ # 10 cycles and its promise 13 deliverables -- 1 task reached the substrate.
162
+ # `signal_tasks_cycle` (the per-cycle status writer, and its --task-cycle flag)
163
+ # was already written and had ZERO callers, because the tasks it sets status on
164
+ # were never created: _tid_for_slug found nothing and it returned 1 every time.
165
+ #
166
+ # So the substrate could not answer "which cycles are ready" -- and that is
167
+ # exactly why do-next.sh re-derives readiness by PARSING MARKDOWN CHECKBOXES, a
168
+ # second source of truth whose divergences are most of this repo's scars.
169
+ #
170
+ # The cycle list is read from do-plan-json.sh, which is a parser, not a model.
171
+ # tasks:create dedupes on the slug tag, so re-running a plan is idempotent.
172
+ signal_tasks_plan() { # signal_tasks_plan <slug>
173
+ local slug="$1" plan cid title n=0
174
+ plan="$(bash "$(dirname "${BASH_SOURCE[0]}")/do-plan-json.sh" "$slug" 2>/dev/null)" || {
175
+ printf 'do-signal: cannot parse plan %s — no cycle tasks filed\n' "$slug" >&2; return 1; }
176
+ # one task per cycle, tagged so the router can fan on any of them
177
+ while IFS=$'\t' read -r cid title; do
178
+ [ -n "$cid" ] && [ "$cid" != "null" ] || continue
179
+ if $PRINT; then
180
+ signal_tasks_create "${slug}-${cid}" "${title:-$slug $cid}" "tags=do:cycle,cycle:${cid#C},slug:${slug}"
181
+ else
182
+ signal_tasks_create "${slug}-${cid}" "${title:-$slug $cid}" "tags=do:cycle,cycle:${cid#C},slug:${slug}" >/dev/null || true
183
+ fi
184
+ n=$((n+1))
185
+ # `.cycles` is an OBJECT KEYED BY CYCLE ID, not an array. The first draft
186
+ # used `.cycles[]? | .id` and got null for every id -- it still reported
187
+ # "filed 2 cycle task(s)", so the count looked right while every task was
188
+ # named `<slug>-` and tagged `cycle:`. A count is not a check.
189
+ done < <(printf '%s' "$plan" | jq -r '.cycles | to_entries[] | "\(.key)\t\(.value.title // .key)"' 2>/dev/null)
190
+ [ "$n" -gt 0 ] || { printf 'do-signal: %s parsed but yielded ZERO cycles — refusing to report success\n' "$slug" >&2; return 1; }
191
+ printf 'do-signal: filed %d cycle task(s) for %s\n' "$n" "$slug"
192
+ }
193
+
113
194
  # signal_tasks_claim — cycle open: leases the plan's substrate task so a fleet
114
195
  # run (or a human) never double-builds the same slug (do-fleet.sh mirrors this).
196
+ # _tid_for_slug <slug> [title] — resolve a plan slug to its task tid, and cache it.
197
+ #
198
+ # The SERVER's slug→tid resolver is broken in prod: measured 2026-08-31, every
199
+ # tasks:claim/link/heartbeat addressed by `slug` returns not_found, while the
200
+ # same task claimed by `tid` succeeds. That cost a whole campaign's board
201
+ # visibility — the /do engine's claim, six heartbeats and link all failed exit 1
202
+ # on every run, silently, because nothing treats a board write as load-bearing.
203
+ #
204
+ # `tasks:create` is idempotent — it dedupes on the `slug:` tag and returns the
205
+ # EXISTING tid — so create IS a working slug→tid resolver. One call adds the task
206
+ # if missing and resolves it if present, which is also the simplest thing for a
207
+ # human: there is no separate "add" step to forget.
208
+ #
209
+ # Cached under .claude/.task-tids/<slug> so the steady state is zero round-trips.
210
+ _tid_for_slug() {
211
+ local slug="$1" title="${2:-}" dir="$_ROOT/.claude/.task-tids" f tid resp
212
+ f="$dir/$slug"
213
+ if [ -f "$f" ]; then tid=$(cat "$f" 2>/dev/null); [ -n "$tid" ] && { printf '%s' "$tid"; return 0; }; fi
214
+ [ -n "$title" ] || title="$slug — /do plan"
215
+ resp=$(signal_tasks_create "$slug" "$title" 2>/dev/null) || true
216
+ tid=$(printf '%s' "$resp" | sed -n 's/.*"tid":"\([^"]*\)".*/\1/p')
217
+ [ -n "$tid" ] || { printf ''; return 1; }
218
+ mkdir -p "$dir" && printf '%s' "$tid" > "$f"
219
+ printf '%s' "$tid"
220
+ }
221
+
222
+ # signal_tasks_cycle <slug> <cid> <status> — the per-CYCLE board row.
223
+ #
224
+ # The plan-level task says "lifecycle is being worked on". It cannot show the
225
+ # thing an operator actually wants to watch: which cycle is in flight and which
226
+ # ones are finished. This files one row per cycle (slug `<plan>-<cid>`, tagged
227
+ # with the plan) and moves it open -> picked -> done as the engine runs.
228
+ #
229
+ # create is idempotent (dedupes on the slug: tag), so calling this on every cycle
230
+ # open is safe and needs no separate "add" step. Status is addressed by tid,
231
+ # because prod's slug resolver returns not_found (see _tid_for_slug).
232
+ signal_tasks_cycle() { # signal_tasks_cycle <slug> <cid> <status>
233
+ local slug="$1" cid="$2" st="${3:-picked}" cslug tid
234
+ cslug="$(printf '%s-%s' "$slug" "$cid" | tr '[:upper:]' '[:lower:]')"
235
+ tid=$(_tid_for_slug "$cslug" "${slug} ${cid}") || true
236
+ [ -n "$tid" ] || return 1
237
+ _post_ask "tasks:status" "{\"data\":{\"tid\":\"${tid}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"status\":\"${st}\"}}"
238
+ }
239
+
115
240
  signal_tasks_claim() { # signal_tasks_claim <slug>
116
241
  # Address the lease by the plan SLUG, not a guessed tid: create returns a random
117
242
  # task:<traceId>, so "task:<slug>" never matched — claim silently 404'd for every /do
118
243
  # run until this. The server resolves slug → tid (resolvers/tasks.ts resolveTaskBySlug).
119
- local slug="$1"
120
- local payload="{\"data\":{\"slug\":\"${slug}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\"}}"
244
+ local slug="$1" tid
245
+ tid=$(_tid_for_slug "$slug") || true
246
+ local payload
247
+ if [ -n "$tid" ]; then
248
+ payload="{\"data\":{\"tid\":\"${tid}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\"}}"
249
+ else
250
+ payload="{\"data\":{\"slug\":\"${slug}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\"}}"
251
+ fi
121
252
  if $PRINT; then printf '%s\n' "$payload"; return 0; fi
122
253
  _post_ask "tasks:claim" "$payload"
123
254
  }
124
255
 
256
+ # signal_tasks_reap — the stranded-lease sweep. `tasks:claim` is atomic, so no
257
+ # two workers double-lease; this is the OTHER half — a lease whose owner died is
258
+ # `picked` forever, invisible to the claimable view and blocking every dependent
259
+ # via ready-tasks. Nothing else in the system goes looking for those rows.
260
+ signal_tasks_reap() { # signal_tasks_reap
261
+ local payload="{\"data\":{\"workspace\":\"${CC_TASKS_WORKSPACE}\"}}"
262
+ if $PRINT; then printf '%s\n' "$payload"; return 0; fi
263
+ _post_ask "tasks:reap" "$payload"
264
+ }
265
+
125
266
  # signal_tasks_link — cycle close: records the handoff-completion back on the
126
267
  # origin task (provenance line + task-status:done), same shape lifecycle.ts's
127
268
  # tasks_link MCP tool posts.
269
+ # signal_outcome — /close's loop-closer, and the reason it is world:outcome.
270
+ #
271
+ # THE NAMING GAP, measured. `/api/receivers/signals` ranks every emitted signal
272
+ # name as declared | candidate | unhomed against packages/sdk/src/receivers.ts,
273
+ # because 35 distinct event names were emitted in 90 days and exactly ONE
274
+ # (`world:in`) was a declared receiver. /close was a top contributor: measured
275
+ # 2026-09-09, `do:close` 0, `loop:feedback` 0, `world:do-event` 0 declarations —
276
+ # three of the four names it emitted could not be answered by anything, and it
277
+ # reported success anyway.
278
+ #
279
+ # `world:outcome` is not a new contract written for this. It already says exactly
280
+ # what /close does: "whoever consumed a route reports success/failure, moving the
281
+ # same tag->receiver paths the router reads - success marks, failure warns."
282
+ # So the fix converges four names to one and designs nothing.
283
+ #
284
+ # THE VERB IS READ BACK, NEVER ASSUMED. The receiver answers
285
+ # {ok, outcome:"marked"|"warned", ...}; a caller that prints "marked" because it
286
+ # sent success:true is reporting its own intent, which is the class of defect the
287
+ # whole four-outcome grammar exists to stop.
288
+ #
289
+ # TAGS ARE BARE WORDS. `marketing`, never `lifecycle:marketing` - a namespaced
290
+ # tag matches zero subscribers. `slug:`/`cycle:` are provenance, not stakes, and
291
+ # are appended separately so they can never be mistaken for a routable tag.
292
+ signal_outcome() { # signal_outcome <slug> tags=a,b actor=<aid> success=<bool> weight=<n> [workspace=<ws>]
293
+ local slug="$1"; shift
294
+ local tags="" actor="" success="true" weight="" ws="${CC_TASKS_WORKSPACE}"
295
+ local kv
296
+ for kv in "$@"; do
297
+ case "$kv" in
298
+ tags=*) tags="${kv#*=}" ;;
299
+ actor=*) actor="${kv#*=}" ;;
300
+ success=*) success="${kv#*=}" ;;
301
+ weight=*) weight="${kv#*=}" ;;
302
+ workspace=*) ws="${kv#*=}" ;;
303
+ esac
304
+ done
305
+ [ -n "$slug" ] || { echo "signal_outcome: slug required" >&2; return 2; }
306
+ [ -n "$actor" ] || { echo "signal_outcome: actor= required (world:outcome moves that actor's paths)" >&2; return 2; }
307
+ case "$success" in true|false) ;; *) echo "signal_outcome: success must be true|false" >&2; return 2 ;; esac
308
+
309
+ # A namespaced word here would route to nobody. Refuse it loudly rather than
310
+ # emit a signal that matches zero subscribers and still exits 0.
311
+ local t
312
+ for t in ${tags//,/ }; do
313
+ case "$t" in
314
+ slug:*|cycle:*) ;;
315
+ *:*) echo "signal_outcome: '$t' is namespaced - stake tags are BARE words (see subscribe-tags-parity.test.ts)" >&2; return 2 ;;
316
+ esac
317
+ done
318
+
319
+ tags="${tags:+${tags},}slug:${slug}"
320
+ local tags_json
321
+ tags_json=$(printf '%s' "$tags" | awk -F',' '{for(i=1;i<=NF;i++){if($i!=""){printf "%s\"%s\"", (n++?",":""), $i}}}')
322
+
323
+ local payload="{\"data\":{\"tags\":[${tags_json}],\"actor\":\"$(_jsonesc "$actor")\",\"success\":${success}${weight:+,\"weight\":${weight}},\"slug\":\"$(_jsonesc "$slug")\",\"workspace\":\"$(_jsonesc "$ws")\"}}"
324
+ if $PRINT; then printf '%s\n' "$payload"; return 0; fi
325
+ _post_ask "world:outcome" "$payload"
326
+ }
327
+
128
328
  signal_tasks_link() { # signal_tasks_link <slug> [docs=a,b,c]
129
329
  # Slug-addressed like claim — the server resolves slug → the origin tid create wrote.
130
330
  local slug="$1" docs=""
@@ -136,9 +336,38 @@ signal_tasks_link() { # signal_tasks_link <slug> [docs=a,b,c]
136
336
  done
137
337
  local docs_json=""
138
338
  [ -n "$docs" ] && docs_json=$(printf '%s' "$docs" | awk -F',' '{for(i=1;i<=NF;i++){printf "%s\"%s\"", (i>1?",":""), $i}}')
139
- local payload="{\"data\":{\"slug\":\"${slug}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\"${docs_json:+,\"docs\":[${docs_json}]}}}"
339
+ local tid; tid=$(_tid_for_slug "$slug") || true
340
+ local payload="{\"data\":{\"slug\":\"${slug}\"${tid:+,\"tid\":\"${tid}\"},\"workspace\":\"${CC_TASKS_WORKSPACE}\"${docs_json:+,\"docs\":[${docs_json}]}}}"
140
341
  if $PRINT; then printf '%s\n' "$payload"; return 0; fi
141
- _post_ask "tasks:link" "$payload"
342
+ # Prod's tasks:link still resolves by slug and ignores an explicit tid, so it
343
+ # returns not_found for every /do close (measured 2026-08-31). Fall back to the
344
+ # two tid-addressed verbs that DO work, so a close is never silently lost:
345
+ # status:done is the state the board renders, the comment is the provenance line.
346
+ if _post_ask "tasks:link" "$payload"; then return 0; fi
347
+ [ -n "$tid" ] || return 1
348
+ _post_ask "tasks:status" "{\"data\":{\"tid\":\"${tid}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"status\":\"done\"}}" >/dev/null || return 1
349
+ _post_ask "tasks:comment" "{\"data\":{\"tid\":\"${tid}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"body\":\"/do close — ${slug}${docs:+ (docs: ${docs})}\"$(_author_json)}}"
350
+ }
351
+
352
+ # signal_tasks_heartbeat — batch/wave close: posts tasks:comment so the lease
353
+ # stays live (the comment resolver bumps updated-at-ms). Slug-addressed like
354
+ # claim/link; body is the wave-close note (e.g. "wave 2 close").
355
+ signal_tasks_heartbeat() { # signal_tasks_heartbeat <slug> <body>
356
+ local slug="$1"; shift
357
+ local body="$*" body_esc
358
+ body_esc="${body//\\/\\\\}"
359
+ body_esc="${body_esc//\"/\\\"}"
360
+ local tid; tid=$(_tid_for_slug "$slug") || true
361
+ # tasks:comment lets `slug` OVERRIDE `tid`, so the slug must be omitted, not merely
362
+ # accompanied, when the tid is known — otherwise the broken resolver runs anyway.
363
+ local payload
364
+ if [ -n "$tid" ]; then
365
+ payload="{\"data\":{\"tid\":\"${tid}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"body\":\"${body_esc}\"$(_author_json)}}"
366
+ else
367
+ payload="{\"data\":{\"slug\":\"${slug}\",\"workspace\":\"${CC_TASKS_WORKSPACE}\",\"body\":\"${body_esc}\"$(_author_json)}}"
368
+ fi
369
+ if $PRINT; then printf '%s\n' "$payload"; return 0; fi
370
+ _post_ask "tasks:comment" "$payload"
142
371
  }
143
372
 
144
373
  while [ "$#" -gt 0 ]; do
@@ -156,16 +385,22 @@ while [ "$#" -gt 0 ]; do
156
385
  done
157
386
  printf 'resolved-from: %s\n' "${_which:-<none>}"
158
387
  exit 0 ;;
159
- --task-create) shift; signal_tasks_create "$@"; exit 0 ;;
160
- --task-claim) shift; signal_tasks_claim "$@"; exit 0 ;;
161
- --task-link) shift; signal_tasks_link "$@"; exit 0 ;;
388
+ --task-create) shift; signal_tasks_create "$@"; exit $? ;;
389
+ --task-claim) shift; signal_tasks_claim "$@"; exit $? ;;
390
+ --task-cycle) shift; signal_tasks_cycle "$@"; exit $? ;;
391
+ --task-plan) shift; signal_tasks_plan "$@"; exit $? ;;
392
+ --task-reap) shift; signal_tasks_reap "$@"; exit $? ;;
393
+ --task-link) shift; signal_tasks_link "$@"; exit $? ;;
394
+ --task-heartbeat) shift; signal_tasks_heartbeat "$@"; exit $? ;;
395
+ --outcome) shift; signal_outcome "$@"; exit $? ;;
162
396
  *) break ;;
163
397
  esac
164
398
  done
165
399
 
166
400
  if [ "$#" -lt 4 ]; then
167
401
  echo "usage: do-signal.sh [--print] <type> <slug> <tier> <cycle> [key=value ...]" >&2
168
- echo " do-signal.sh [--print] --task-create|--task-claim|--task-link <slug> ..." >&2
402
+ echo " do-signal.sh [--print] --task-create|--task-claim|--task-link|--task-heartbeat <slug> ..." >&2
403
+ echo " do-signal.sh [--print] --outcome <slug> tags=a,b actor=<aid> success=<bool> [weight=<n>]" >&2
169
404
  exit 1
170
405
  fi
171
406
 
@@ -218,20 +453,42 @@ for kv in "$@"; do
218
453
  esac
219
454
  done
220
455
 
456
+ # is_test discriminator. An emission aimed anywhere but production is a test run by
457
+ # construction, so it defaults to dev; a real prod cycle is a real run and stays
458
+ # is_test=0. There is no automatic way to tell a developer's experimental prod cycle
459
+ # from a genuine one, so that case is the operator's explicit DO_SIGNAL_ENV=dev —
460
+ # which this respects rather than overrides.
461
+ case "$DO_SIGNAL_URL" in
462
+ https://one.ie|https://one.ie/) : ;;
463
+ *) DO_SIGNAL_ENV="${DO_SIGNAL_ENV:-dev}" ;;
464
+ esac
465
+ # DO_SIGNAL_ENV rides in the payload so the receiver can flag the projected run
466
+ # is_test — those cycles are then filterable instead of polluting the operator's list.
467
+ [ -n "${DO_SIGNAL_ENV:-}" ] && EXTRA_JSON="${EXTRA_JSON},\"env\":\"${DO_SIGNAL_ENV}\""
468
+
221
469
  PAYLOAD="{\"type\":\"${TYPE}\",\"slug\":\"${SLUG}\",\"tier\":\"${TIER}\",\"cycle\":${CYCLE},\"tags\":[${TAGS}]${EXTRA_JSON}}"
222
470
 
471
+ # SERVER_SECRET for the Authorization header — _server_secret() (defined above) is the
472
+ # one lookup, shared with the tasks:* verbs. .env historically lacks it;
473
+ # .secrets.generated.local is what push-prod-secrets.sh pushed to the worker, so it
474
+ # matches prod. Without a matching secret the POST is silently 401/403-dropped at the
475
+ # gateway (verified 2026-07-04 — zero do-* threads had ever landed in prod D1).
476
+ SERVER_SECRET="$(_server_secret)"
477
+
478
+ # Dark-cycle guard — resolved BEFORE the --print exit so a dry run reports the same
479
+ # state a real emit would hit. A worktree with no secret emits nothing, and the
480
+ # resulting gap in the run trace reads as "no cycles ran" rather than "the wire was
481
+ # down". One loud stderr line; stdout is untouched, so every existing --print
482
+ # consumer is unaffected.
483
+ if [ -z "$SERVER_SECRET" ]; then
484
+ echo "[do-signal] dark: no secret (SERVER_SECRET unresolved from ${ONE_ENV_FILE}) — this cycle emits nothing" >&2
485
+ fi
486
+
223
487
  if $PRINT; then
224
488
  printf '%s\n' "$PAYLOAD"
225
489
  exit 0
226
490
  fi
227
491
 
228
- # SERVER_SECRET for the Authorization header — _server_secret() (defined above)
229
- # is the one lookup, shared with the tasks:* verbs. .env historically lacks it;
230
- # .secrets.generated.local is what push-prod-secrets.sh pushed to the worker, so
231
- # it matches prod. Without a matching secret the POST is silently 401/403-dropped
232
- # at the gateway (verified 2026-07-04 — zero do-* threads had ever landed in prod D1).
233
- SERVER_SECRET="$(_server_secret)"
234
-
235
492
  # POST — fire-and-forget; failures are silent so the build loop is never blocked.
236
493
  _url="${DO_SIGNAL_URL}/api/signal/world:do-event"
237
494
  if [ -n "$SERVER_SECRET" ]; then
@@ -4,6 +4,23 @@
4
4
  # the seven-canon predicate (do-reconcile.sh) + navigation reachable/orphan fixtures.
5
5
  # Exits 0 only if ALL pass.
6
6
  set -uo pipefail
7
+
8
+ # _GATE — route a heavy compute through the machine governor. A gate_lock only
9
+ # dedupes IDENTICAL work; a SLOT is what bounds N worktrees each running one of
10
+ # these at once (measured 2026-09-07: three concurrent 2.5GB typecheckers, every
11
+ # lock uncontended, load 171). Empty when already inside a gate, so a nested call
12
+ # inherits the outer slot rather than taking a second one.
13
+ #
14
+ # Resolves gate-run.sh from its OWN directory, deliberately: an earlier version
15
+ # keyed off $ROOT and got inserted above the line that sets it, so _GATE was
16
+ # silently empty and every call ran ungoverned -- a fail-OPEN, which is the exact
17
+ # defect this preamble exists to close.
18
+ _GATE=()
19
+ if [ "${GOVERN_IN_GATE:-0}" != "1" ]; then
20
+ _GR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/gate-run.sh"
21
+ [ -f "$_GR" ] && _GATE=( bash "$_GR" "compute:$(basename "${BASH_SOURCE[0]}")" -- )
22
+ fi
23
+
7
24
  cd "$(dirname "${BASH_SOURCE[0]}")/../.." || exit 2
8
25
  S=.claude/scripts
9
26
  fail=0
@@ -61,7 +78,7 @@ echo "== C2 do-reconcile — types canon =="
61
78
  # Use a temp file to capture grep output — avoids pipefail swallowing the value when tsc
62
79
  # exits non-zero (which it does whenever there are any type errors).
63
80
  _tsc_tmp=$(mktemp)
64
- ( cd one.ie/web 2>/dev/null && bunx tsc --noEmit 2>&1 | grep -c 'error TS' > "$_tsc_tmp" ) 2>/dev/null || true
81
+ ( cd one.ie/web 2>/dev/null && "${_GATE[@]}" bunx tsc --noEmit 2>&1 | grep -c 'error TS' > "$_tsc_tmp" ) 2>/dev/null || true
65
82
  _tsc_now=$(cat "$_tsc_tmp" 2>/dev/null | tr -d '[:space:]'); rm -f "$_tsc_tmp"
66
83
  echo "{\"tscErrors\": ${_tsc_now:-0}}" > .w0-baseline.json
67
84
  "$S/do-reconcile.sh" types >/dev/null 2>&1 && ok "types delta=0 → exit 0" || no "types should pass when no delta"
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bash
2
+ # do-test-gate.sh — a vitest accept clause that cannot pass vacuously.
3
+ #
4
+ # WHY THIS EXISTS. A promise `accept:` of the form
5
+ # ( cd <workdir> && bunx vitest run tests/unit/<slug>.test.ts )
6
+ # is not proof. Measured on vitest 4.1.7 in this repo (2026-08-18):
7
+ # · a test file full of `it.skip(...)` → exit 0
8
+ # · `-t "name-that-does-not-exist"` → exit 0, "10 skipped"
9
+ # · a file containing only `it('x', () => {})` → exit 0
10
+ # The only shape that DOES go red is a missing file ("No test files found",
11
+ # exit 1). So the clause proves the file exists — the same class of claim as a
12
+ # presence grep, and the same trap: the cycle that must pass the check is the
13
+ # cycle that authors the file.
14
+ #
15
+ # This gate asserts the run actually EXERCISED something: at least <min> tests
16
+ # passed, zero failed, and zero were pending/skipped. A skipped test is a hole,
17
+ # not a pass — `it.skip` in a security accept is the failure mode this closes.
18
+ #
19
+ # Usage: do-test-gate.sh <workdir> <min-passed> <test-file> [<test-file>...]
20
+ # Example (inside a promise accept:):
21
+ # bash .claude/scripts/do-test-gate.sh <workdir> 4 tests/unit/<slug>-persona.test.ts
22
+ #
23
+ # Exit 0 only when: numFailedTests == 0 AND numPendingTests == 0 AND
24
+ # numPassedTests >= min.
25
+ set -euo pipefail
26
+
27
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
28
+ workdir="${1:?usage: do-test-gate.sh <workdir> <min-passed> <test-file>...}"
29
+ min="${2:?usage: do-test-gate.sh <workdir> <min-passed> <test-file>...}"
30
+ shift 2
31
+ [ "$#" -gt 0 ] || { echo "[test-gate] no test files given" >&2; exit 2; }
32
+
33
+ case "$min" in ''|*[!0-9]*) echo "[test-gate] min-passed must be an integer, got '$min'" >&2; exit 2 ;; esac
34
+
35
+ out="$(mktemp -t do-test-gate.XXXXXX)"
36
+ trap 'rm -f "$out"' EXIT
37
+
38
+ # Missing files must fail here rather than inside vitest's include-glob, whose
39
+ # "No test files found" message is easy to mistake for a red test.
40
+ for f in "$@"; do
41
+ [ -f "$ROOT/$workdir/$f" ] || { echo "[test-gate] FAIL missing test file: $workdir/$f" >&2; exit 1; }
42
+ done
43
+
44
+ # vitest's own exit code is deliberately IGNORED — it is 0 for the vacuous
45
+ # shapes above. The json report is the verdict.
46
+ #
47
+ # GOVERNED. This gate is written to be called from many parallel /do cycles at
48
+ # once, and a bare `bunx vitest` is exactly the ungoverned heavy gate that melted
49
+ # a 10-core box (see .claude/CLAUDE.md § Machine governor): no slot, no wall
50
+ # clock, no process-group reap. Routing through gate-run.sh means N parallel
51
+ # cycles QUEUE on GOVERN_MAX_GATES instead of thrashing, and it also satisfies
52
+ # hook:load-guard, which would otherwise refuse a direct `vitest run` on a
53
+ # saturated box and fail the accept for a reason that has nothing to do with the
54
+ # code. Degrades to a plain exec when the governor lib is absent (CI, fresh clone).
55
+ GATE_RUN="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/gate-run.sh"
56
+ runner=( bash "$GATE_RUN" "test-gate:$workdir" -- )
57
+ [ -x "$GATE_RUN" ] || runner=()
58
+ ( cd "$ROOT/$workdir" && "${runner[@]}" bunx vitest run "$@" --reporter=json --outputFile="$out" ) >/dev/null 2>&1 || true
59
+
60
+ python3 - "$out" "$min" "$workdir" "$@" <<'PY'
61
+ import json, sys
62
+ out, min_passed, workdir = sys.argv[1], int(sys.argv[2]), sys.argv[3]
63
+ files = sys.argv[4:]
64
+ try:
65
+ d = json.load(open(out))
66
+ except Exception as e:
67
+ print(f"[test-gate] FAIL no json report ({e}) — the run did not start", file=sys.stderr)
68
+ sys.exit(1)
69
+ passed = int(d.get("numPassedTests", 0))
70
+ failed = int(d.get("numFailedTests", 0))
71
+ pending = int(d.get("numPendingTests", 0))
72
+ label = " ".join(files)
73
+ if failed:
74
+ print(f"[test-gate] FAIL {label}: {failed} failing", file=sys.stderr); sys.exit(1)
75
+ if pending:
76
+ print(f"[test-gate] FAIL {label}: {pending} skipped — a skipped assertion is a hole, not a pass", file=sys.stderr); sys.exit(1)
77
+ if passed < min_passed:
78
+ print(f"[test-gate] FAIL {label}: {passed} passed, expected >= {min_passed}", file=sys.stderr); sys.exit(1)
79
+ print(f"[test-gate] OK {workdir}/{label} — {passed} passed, 0 skipped, 0 failed (floor {min_passed})")
80
+ PY
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ # do-tick.sh <slug> <C1> [C2 ...] — tick a cycle's Status-kanban line after a
3
+ # green W4. Run from the plan worktree (or pass DO_TICK_ROOT).
4
+ #
5
+ # manifest: portable
6
+ #
7
+ # WHY (2026-08-31). do-engine had no writer for the tick, so a cycle that PASSED
8
+ # stayed `- [ ]` forever. openCyclesFromTodo/_remaining then reported it open on
9
+ # the next launch, the batch rebuilt work that was already verified green, and
10
+ # because a rebuilt batch never got further the run reported stoppedAt:1 every
11
+ # time. Measured: lifecycle-money C2 was verified green at 15:43 and was being
12
+ # rebuilt from scratch at 16:02. Two W2 agents diagnosed it independently and
13
+ # named it ROOT CAUSE in the census.
14
+ #
15
+ # Deterministic on purpose: ticking is a two-character edit on a line whose exact
16
+ # shape is already the contract. Nothing about it needs a model.
17
+ set -euo pipefail
18
+ SLUG="${1:-}"; shift || true
19
+ [ -n "$SLUG" ] && [ "$#" -gt 0 ] || { echo "usage: do-tick.sh <slug> <C1> [C2 ...]" >&2; exit 2; }
20
+ ROOT="${DO_TICK_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
21
+ TODO="$ROOT/text/${SLUG}-todo.md"
22
+ [ -f "$TODO" ] || { echo "do-tick: no such plan: $TODO" >&2; exit 3; }
23
+
24
+ # _is_open <cid> — the ONE reader in this script. Same shape do-auto.sh's
25
+ # _remaining() and do-engine's openCyclesFromTodo use; parity across all four
26
+ # is proven behaviourally by do-cycle-shape-check.sh, not by eyeballing them.
27
+ _is_open() { grep -qE "^[[:space:]]*- \[[ ~]\] \*{0,2}${1}\*{0,2} —" "$TODO"; }
28
+
29
+ # ── the OTHER half of closing a cycle ────────────────────────────────────────
30
+ # This script was the single writer of the markdown tick and touched the
31
+ # substrate ZERO times, so a cycle went `[x]` in a file while the platform task
32
+ # stayed open forever. Two records of the same fact, one of them always stale --
33
+ # and the stale one is what a human or an agent reads on the board.
34
+ #
35
+ # So the tick writes BOTH, here, in the one place a cycle is declared complete.
36
+ #
37
+ # It must not fail the tick. The markdown tick is the ratchet the engine reads to
38
+ # avoid rebuilding proven work; losing it because a gateway was unreachable would
39
+ # trade a stale board for a rebuilt cycle, which is far worse. Instead an
40
+ # unreachable substrate PARKS the update in a machine-keyed pending file and every
41
+ # later tick flushes it first -- so "as soon as complete" degrades to "at the next
42
+ # tick", never to "never". Same debt-ledger shape as the deferred test pins.
43
+ PENDING="${DO_TICK_PENDING:-${TMPDIR:-/tmp}/one-do-tick-pending}"
44
+ _sig() { bash "$(dirname "${BASH_SOURCE[0]}")/do-signal.sh" --task-cycle "$1" "$2" done >/dev/null 2>&1; }
45
+
46
+ _flush_pending() {
47
+ [ -f "$PENDING" ] || return 0
48
+ local left; left="$(mktemp)"
49
+ while IFS=$'\t' read -r pslug pcid; do
50
+ [ -n "$pcid" ] || continue
51
+ _sig "$pslug" "$pcid" || printf '%s\t%s\n' "$pslug" "$pcid" >> "$left"
52
+ done < "$PENDING"
53
+ if [ -s "$left" ]; then mv "$left" "$PENDING"
54
+ echo "do-tick: $(wc -l < "$PENDING" | tr -d ' ') substrate update(s) still pending"
55
+ else rm -f "$left" "$PENDING"; fi
56
+ }
57
+
58
+ _mark_done_in_substrate() {
59
+ local cid="$1"
60
+ if _sig "$SLUG" "$cid"; then
61
+ echo "do-tick: ${SLUG} ${cid} -> task done (substrate)"
62
+ else
63
+ printf '%s\t%s\n' "$SLUG" "$cid" >> "$PENDING"
64
+ echo "do-tick: substrate unreachable — ${SLUG} ${cid} PARKED in $PENDING (flushed on the next tick)" >&2
65
+ fi
66
+ }
67
+
68
+ _flush_pending
69
+
70
+ ticked=0; failed=0
71
+ for cid in "$@"; do
72
+ case "$cid" in C[0-9]|C[0-9][0-9]) ;; *) echo "do-tick: skip bad cycle id '$cid'" >&2; continue ;; esac
73
+ # Only ever [ ] or [~] -> [x]. Never the reverse: the ratchet cannot regress.
74
+ if _is_open "$cid"; then
75
+ # SUBSTRATE FIRST, checkbox second. The task is the record; the checkbox is a
76
+ # projection of it. Writing the file first made these two peers that could
77
+ # disagree -- a dual write, which is the same two-sources-of-truth shape that
78
+ # produced every engine scar here. Written in this order the box can only
79
+ # reach [x] downstream of a task that is already `done`.
80
+ _mark_done_in_substrate "$cid"
81
+ perl -i -pe "s/^(\s*- )\[[ ~]\](\s\*{0,2}${cid}\*{0,2}\s—)/\${1}[x]\${2}/" "$TODO"
82
+ # POST-CONDITION. A fire-and-forget write is how the original bug survived:
83
+ # the engine called a ticker, nobody checked the box actually moved, and the
84
+ # next launch rebuilt proven work. Asserting the write LANDED — with this
85
+ # script's own reader — is the guard that works even when do-engine runs from
86
+ # a cached snapshot, because it runs inside whatever code actually executed.
87
+ if _is_open "$cid"; then
88
+ echo "do-tick: FAILED — ${SLUG} ${cid} is STILL open after the write. The plan will rebuild proven work. Line shape in $TODO does not match the writer." >&2
89
+ failed=$((failed+1))
90
+ else
91
+ echo "do-tick: ${SLUG} ${cid} -> [x]"; ticked=$((ticked+1))
92
+ fi
93
+ else
94
+ echo "do-tick: ${SLUG} ${cid} already closed or absent — no change"
95
+ fi
96
+ done
97
+
98
+ # Report what the ENGINE's next launch will see. A silent success that leaves the
99
+ # same count open is the failure mode this whole file exists to end.
100
+ open_now=$(grep -cE '^[[:space:]]*- \[[ ~]\] \*{0,2}C[0-9]+\*{0,2} —' "$TODO" 2>/dev/null || true)
101
+ echo "do-tick: ${SLUG} ticked=${ticked} open=${open_now:-0}"
102
+ [ "$failed" -eq 0 ] || exit 4
@@ -24,6 +24,8 @@ if [ "${1:-}" = "--self-test" ]; then
24
24
  chk SCHEMA channels/migrations/0001_init.sql
25
25
  chk SCHEMA pay/contracts/sui/sources/one_token.move
26
26
  chk FIX pay/contracts/sui/tests/gateway_tests.move
27
+ chk FIX .claude/scripts/x.sh
28
+ chk FIX .claude/workflows/x.js
27
29
  chk FEATURE --intent "add billing" one.ie/web/src/api/b.ts
28
30
  # Absence of recon must not read as simplicity — no paths → UNSIZED, never PATCH.
29
31
  # Intent is ignored by design: "add a null check" and "add a settings page" share a verb.
@@ -82,6 +84,10 @@ for p in "${paths[@]}"; do
82
84
  */migrations/*.sql|migrations/*.sql) schema=true; code=true; doconly=false ;;
83
85
  */tests/*.move) code=true; doconly=false ;; # contract tests are code, not shape
84
86
  *.move|*.sol) schema=true; code=true; doconly=false ;;
87
+ # .claude/scripts/*.sh and .claude/workflows/*.js are code that runs gates and
88
+ # fleets, not .claude/'s usual prose arm — must precede the .claude/* PATCH
89
+ # fallback below or they fall through as doc-only.
90
+ .claude/scripts/*.sh|.claude/workflows/*.js) code=true; doconly=false ;;
85
91
  *.md|.claude/*|text/*|docs/*) ;;
86
92
  "") ;;
87
93
  *) code=true; doconly=false ;;