@oneie/claude 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/agents/abm-strategist.md +89 -0
  2. package/agents/ads-meta.md +97 -0
  3. package/agents/analyst.md +173 -0
  4. package/agents/architect.md +280 -0
  5. package/agents/brand-guardian.md +88 -0
  6. package/agents/brand-strategist.md +92 -0
  7. package/agents/campaign-content.md +90 -0
  8. package/agents/campaign-email.md +88 -0
  9. package/agents/campaign-sms.md +87 -0
  10. package/agents/campaign-social.md +87 -0
  11. package/agents/cco.md +99 -0
  12. package/agents/ceo.md +106 -0
  13. package/agents/chairman.md +120 -0
  14. package/agents/cmo.md +101 -0
  15. package/agents/community-greeter.md +89 -0
  16. package/agents/community-moderator.md +92 -0
  17. package/agents/compliance.md +92 -0
  18. package/agents/copywriter.md +100 -0
  19. package/agents/creative-strategist.md +95 -0
  20. package/agents/cro.md +98 -0
  21. package/agents/cto.md +267 -0
  22. package/agents/customer-interviewer.md +93 -0
  23. package/agents/customer-researcher.md +89 -0
  24. package/agents/customer-success-manager.md +90 -0
  25. package/agents/customer-trainer.md +90 -0
  26. package/agents/cxo.md +100 -0
  27. package/agents/demand-creator.md +89 -0
  28. package/agents/demo-mover.md +83 -0
  29. package/agents/demo-specialist.md +89 -0
  30. package/agents/demo-thai-family-law.md +87 -0
  31. package/agents/designer.md +112 -0
  32. package/agents/discovery-caller.md +98 -0
  33. package/agents/doctor.md +139 -0
  34. package/agents/educate-coach.md +100 -0
  35. package/agents/elevate-tutor.md +83 -0
  36. package/agents/email-lifecycle-marketer.md +91 -0
  37. package/agents/engage-specialist.md +99 -0
  38. package/agents/events-coordinator.md +90 -0
  39. package/agents/foundation-builder.md +94 -0
  40. package/agents/funnel-architect.md +89 -0
  41. package/agents/gift-creator.md +100 -0
  42. package/agents/google-ads.md +103 -0
  43. package/agents/guide.md +292 -0
  44. package/agents/helpdesk-dispatcher.md +88 -0
  45. package/agents/hook-specialist.md +91 -0
  46. package/agents/identify-optimizer.md +101 -0
  47. package/agents/implementer.md +305 -0
  48. package/agents/incident-commander.md +120 -0
  49. package/agents/insights-lead.md +93 -0
  50. package/agents/journey-runner.md +113 -0
  51. package/agents/linkedin-ads.md +123 -0
  52. package/agents/live-sales-chat.md +90 -0
  53. package/agents/market-researcher.md +88 -0
  54. package/agents/media-buyer.md +96 -0
  55. package/agents/movers-customer-researcher.md +95 -0
  56. package/agents/movers-foundation-builder.md +96 -0
  57. package/agents/movers-market-researcher.md +97 -0
  58. package/agents/movers-pricing-strategist.md +171 -0
  59. package/agents/nurture-architect.md +99 -0
  60. package/agents/offer-architect.md +92 -0
  61. package/agents/onboarder.md +222 -0
  62. package/agents/onboarding-specialist.md +92 -0
  63. package/agents/operations-dashboard.md +98 -0
  64. package/agents/perf-engineer.md +348 -0
  65. package/agents/playbook-writer.md +71 -0
  66. package/agents/plg-strategist.md +91 -0
  67. package/agents/positioning-architect.md +88 -0
  68. package/agents/press-officer.md +89 -0
  69. package/agents/pricing-strategist.md +94 -0
  70. package/agents/privacy-officer.md +92 -0
  71. package/agents/referral-manager.md +91 -0
  72. package/agents/refine-analyst.md +102 -0
  73. package/agents/release-manager.md +261 -0
  74. package/agents/renewals-upsell-rep.md +90 -0
  75. package/agents/review-engineer.md +327 -0
  76. package/agents/rewards-steward.md +82 -0
  77. package/agents/sales-call-coach.md +94 -0
  78. package/agents/sales-closer.md +98 -0
  79. package/agents/security-auditor.md +313 -0
  80. package/agents/sell-closer.md +101 -0
  81. package/agents/share-amplifier.md +99 -0
  82. package/agents/social-media-manager.md +92 -0
  83. package/agents/storyteller.md +250 -0
  84. package/agents/strategist.md +83 -0
  85. package/agents/strategy-aligner.md +94 -0
  86. package/agents/support-agent.md +90 -0
  87. package/agents/tagger.md +245 -0
  88. package/agents/tech-writer.md +216 -0
  89. package/agents/test-engineer.md +243 -0
  90. package/agents/tiktok-ads.md +105 -0
  91. package/agents/tracking-engineer.md +92 -0
  92. package/agents/upsell-strategist.md +101 -0
  93. package/agents/voice.md +85 -0
  94. package/agents/w4-tools.md +153 -0
  95. package/agents/w4-verify.md +20 -138
  96. package/agents/workflow-optimiser.md +84 -0
  97. package/commands/close.md +814 -160
  98. package/commands/create.md +2 -2
  99. package/commands/deploy.md +554 -175
  100. package/commands/do.md +119 -109
  101. package/commands/fast.md +67 -0
  102. package/commands/improve.md +2 -2
  103. package/commands/one.md +418 -0
  104. package/commands/see.md +1 -1
  105. package/commands/sync.md +1 -1
  106. package/commands/tasks.md +222 -0
  107. package/commands/vespio.md +110 -0
  108. package/commands/vespio.remote.md +227 -0
  109. package/hooks/hooks.json +25 -79
  110. package/hooks/lib/governor-escape-match.sh +88 -0
  111. package/hooks/lib/hook.sh +4 -13
  112. package/hooks/lib/signal.sh +9 -2
  113. package/hooks/scripts/branch-pin.sh +6 -0
  114. package/hooks/scripts/config-protect.sh +6 -0
  115. package/hooks/scripts/do-outcome-gate.sh +19 -2
  116. package/hooks/scripts/git-add-guard.sh +43 -2
  117. package/hooks/scripts/governor-escape.sh +96 -0
  118. package/hooks/scripts/load-guard.sh +127 -0
  119. package/hooks/scripts/post-edit-check.sh +36 -29
  120. package/hooks/scripts/session-start.sh +34 -80
  121. package/hooks/scripts/task-complete-verify.sh +46 -40
  122. package/package.json +1 -1
  123. package/rules/documentation.md +9 -0
  124. package/scripts/ad-copy-lint.sh +656 -0
  125. package/scripts/agentverse-audit.sh +256 -0
  126. package/scripts/asi-walk.sh +435 -0
  127. package/scripts/astro-build-cached.sh +143 -0
  128. package/scripts/blocks-manifest-cached.sh +100 -0
  129. package/scripts/blocks-manifest.mjs +497 -0
  130. package/scripts/blocks-ratchet.sh +116 -0
  131. package/scripts/blocks-render-probe.mjs +529 -0
  132. package/scripts/blocks-usage.mjs +334 -0
  133. package/scripts/chat-context-check.sh +89 -0
  134. package/scripts/close-metrics.sh +558 -0
  135. package/scripts/close-owner.sh +268 -0
  136. package/scripts/db-sync-lock-check.sh +116 -0
  137. package/scripts/deploy-dev.sh +35 -0
  138. package/scripts/deploy-emit.sh +298 -0
  139. package/scripts/deploy-gate-check.sh +128 -0
  140. package/scripts/deploy-ready.sh +78 -0
  141. package/scripts/deploy-record.sh +605 -0
  142. package/scripts/deploy.sh +1273 -0
  143. package/scripts/do-auto.sh +598 -48
  144. package/scripts/do-brief.sh +113 -0
  145. package/scripts/do-close.sh +1137 -0
  146. package/scripts/do-commit.sh +75 -0
  147. package/scripts/do-consumer-sweep.sh +18 -1
  148. package/scripts/do-cycle-shape-check.sh +160 -0
  149. package/scripts/do-decide.sh +476 -0
  150. package/scripts/do-derive-check.sh +436 -0
  151. package/scripts/do-fleet.sh +106 -28
  152. package/scripts/do-folder.sh +10 -1
  153. package/scripts/do-next.sh +106 -0
  154. package/scripts/do-orchestrate.sh +17 -5
  155. package/scripts/do-plan-json.mjs +201 -0
  156. package/scripts/do-plan-json.sh +8 -0
  157. package/scripts/do-preflight.sh +117 -0
  158. package/scripts/do-project.sh +157 -0
  159. package/scripts/do-prove-selftest.sh +108 -0
  160. package/scripts/do-prove.sh +295 -23
  161. package/scripts/do-rank.py +31 -1
  162. package/scripts/do-recon-cache.sh +7 -1
  163. package/scripts/do-recon-pack.sh +196 -0
  164. package/scripts/do-reconcile.sh +121 -4
  165. package/scripts/do-signal.sh +280 -23
  166. package/scripts/do-smoke.sh +18 -1
  167. package/scripts/do-test-gate.sh +80 -0
  168. package/scripts/do-tick.sh +102 -0
  169. package/scripts/do-tier.sh +6 -0
  170. package/scripts/do-triage.sh +182 -0
  171. package/scripts/do-ui-gate.sh +1 -1
  172. package/scripts/do-w4-gates.sh +451 -0
  173. package/scripts/do-walk.sh +12 -1
  174. package/scripts/env-sync.sh +173 -0
  175. package/scripts/factory-brief-check.sh +330 -0
  176. package/scripts/factory-check.sh +68 -14
  177. package/scripts/factory-close-check.sh +257 -0
  178. package/scripts/factory-emit.sh +211 -0
  179. package/scripts/factory-executor-check.mjs +353 -0
  180. package/scripts/factory-peak.sh +301 -0
  181. package/scripts/factory-repo.sh +118 -3
  182. package/scripts/factory-review-check.mjs +61 -0
  183. package/scripts/factory-ship.sh +61 -0
  184. package/scripts/factory-tasks-check.sh +18 -1
  185. package/scripts/factory-turn.sh +326 -0
  186. package/scripts/factory-walk.sh +396 -0
  187. package/scripts/factory-width.sh +57 -0
  188. package/scripts/fade-toxic.sh +4 -3
  189. package/scripts/fixtures/factory-brief-real.md +44 -0
  190. package/scripts/fixtures/triage-dupe.md +5 -0
  191. package/scripts/fleet-manifest.mjs +108 -0
  192. package/scripts/fleet-status.sh +110 -0
  193. package/scripts/full-suite-paths-check.sh +144 -0
  194. package/scripts/gate-reaper-check.sh +98 -0
  195. package/scripts/gate-reaper.sh +125 -0
  196. package/scripts/gate-run.sh +73 -0
  197. package/scripts/gc-content-check.sh +140 -0
  198. package/scripts/gen-dev-config.py +20 -0
  199. package/scripts/govern-bound-check.sh +60 -0
  200. package/scripts/govern-claims-check.sh +233 -0
  201. package/scripts/govern-mem-check.sh +290 -0
  202. package/scripts/governor-doors-check.sh +362 -0
  203. package/scripts/governor-escape-check.sh +171 -0
  204. package/scripts/health.sh +413 -0
  205. package/scripts/id-inventory.mjs +418 -0
  206. package/scripts/land.sh +551 -0
  207. package/scripts/lib/gc-finished.sh +77 -0
  208. package/scripts/lib/govern.sh +361 -0
  209. package/scripts/lib/govern.ts +756 -0
  210. package/scripts/lighthouse-run.sh +187 -0
  211. package/scripts/livekit-live-check.sh +61 -0
  212. package/scripts/livekit-ratchet.sh +46 -0
  213. package/scripts/load-guard-check.sh +49 -0
  214. package/scripts/machine-check.sh +102 -0
  215. package/scripts/machine-watch.sh +177 -0
  216. package/scripts/one-agents.mjs +415 -0
  217. package/scripts/one-resume.sh +78 -0
  218. package/scripts/orphan-baseline.json +182 -0
  219. package/scripts/orphan-modules.mjs +179 -0
  220. package/scripts/pr-body.sh +335 -0
  221. package/scripts/preview-fd-check.sh +289 -0
  222. package/scripts/promise-manifest.mjs +24 -1
  223. package/scripts/release.sh +322 -0
  224. package/scripts/roles-check.sh +946 -0
  225. package/scripts/sdk-build-cached.sh +64 -0
  226. package/scripts/signal-watch.sh +241 -0
  227. package/scripts/skills-publish.sh +94 -0
  228. package/scripts/speed-cache-check.sh +173 -0
  229. package/scripts/speed-check.mjs +907 -0
  230. package/scripts/speed-parity-check.sh +648 -0
  231. package/scripts/speed-waterfall-check.sh +355 -0
  232. package/scripts/substrate-env-parity.mjs +156 -0
  233. package/scripts/tasks-claim-race.mjs +108 -0
  234. package/scripts/tasks-loop.sh +185 -0
  235. package/scripts/test-cached.sh +255 -0
  236. package/scripts/test-full.sh +87 -0
  237. package/scripts/test-honesty.mjs +137 -0
  238. package/scripts/test-lanes.sh +166 -0
  239. package/scripts/test-speed.sh +94 -0
  240. package/scripts/triage-shape-check.sh +149 -0
  241. package/scripts/tsc-cached.sh +179 -0
  242. package/scripts/typedb-flake-check.sh +197 -0
  243. package/scripts/urls-lint.sh +15 -0
  244. package/scripts/verify-fast.sh +445 -0
  245. package/scripts/vespio-sync.sh +149 -0
  246. package/scripts/wf-check.mjs +104 -1
  247. package/scripts/worktree-preview.sh +879 -0
  248. package/scripts/worktree-up.sh +208 -0
  249. package/skills/livekit-agents/SKILL.md +285 -0
  250. package/skills/livekit-agents/references/freshness-rules.md +168 -0
  251. package/skills/shadcn/SKILL.md +1 -1
  252. package/skills/signal/SKILL.md +0 -1
  253. package/skills/voice/SKILL.md +94 -6
  254. package/skills/voice/corpus-check.sh +87 -0
  255. package/hooks/scripts/compact-hint.sh +0 -35
  256. package/hooks/scripts/gate-guard.sh +0 -83
  257. package/hooks/scripts/read-tracker.sh +0 -26
  258. package/hooks/scripts/session-end-verify.sh +0 -51
  259. package/hooks/scripts/stop-reflect.sh +0 -140
  260. package/hooks/scripts/sync-priority-todo.sh +0 -57
  261. package/hooks/scripts/sync-todo-docs.sh +0 -46
  262. package/hooks/scripts/tool-signal.sh +0 -48
  263. package/scripts/do-tasks-bridge.py +0 -366
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env bash
2
+ # speed-waterfall-check.sh — proves a serial-hop waterfall was actually
3
+ # collapsed into a fan-out, structurally, on both surfaces this checker will
4
+ # eventually cover:
5
+ #
6
+ # --chat (C9, THIS cycle) api/chat.ts's pre-flight ladder: three
7
+ # independent D1/crypto reads (loadPublishedPage, getThread,
8
+ # visitorHash) that used to run one `await` at a time before the
9
+ # `...P` parallel block even opened. Ships in THIS cycle.
10
+ # --tenant (C8) the tenant page round trip. Checks THREE files together —
11
+ # u/[slug]/index.astro, u/[slug]/p/[pageSlug].astro and the loader
12
+ # module lib/tenant/page-data.ts they both delegate their data wave
13
+ # to. Checking the frontmatters ALONE would be fooled by a
14
+ # "collapse" that merely relocated the round trips one level down
15
+ # into a helper, so each of the three carries its own await-line
16
+ # ceiling (5 / 5 / 4) as well as a required fan-out shape. That
17
+ # pair — required join + per-file ceiling — is what makes the
18
+ # ceiling TRANSITIVE: 3 dependency levels per route, counted across
19
+ # frontmatter + loader, leaf helpers one node each.
20
+ #
21
+ # The two declared exceptions are visible in the numbers: the
22
+ # [pageSlug] draft-preview authz gate (a security gate; asserted to
23
+ # be EXACTLY ONE authorizeWorkspace call, never zero, never two) and
24
+ # index.astro's invite-token early-return branch.
25
+ # --check-gate SELF-TEST: plant the pre-fix serial shape in a scratch file,
26
+ # prove --chat's structural check flags it RED, then prove the
27
+ # real working tree is GREEN. Same discipline as
28
+ # speed-cache-check.sh and speed-check.mjs --check-gate: a checker
29
+ # that has never been observed failing is not evidence of anything.
30
+ #
31
+ # WHY STRUCTURAL, NOT A LIVE TIMING DIFF: chat.ts's pre-flight reads D1 and
32
+ # crypto.subtle inside a live Worker request — there is no cheap way to
33
+ # dependency-inject fake latency into 1,200+ lines of route handler from a
34
+ # shell script, and a live prod A/B (the C6 method) needs a funded turn +
35
+ # billing side-effects, which this checker must not trigner on every run.
36
+ # So this proves the SHAPE that makes concurrency possible (three `P` promises
37
+ # created before any of them is awaited, joined with Promise.all/await),
38
+ # and the real timing readback lives in text/speed-results.md (W4, manual).
39
+ #
40
+ # Usage:
41
+ # speed-waterfall-check.sh --chat structural pass/fail, exit 0/1
42
+ # speed-waterfall-check.sh --tenant NOT YET SHIPPED — exits 2
43
+ # speed-waterfall-check.sh --check-gate self-test, exits 0/1
44
+
45
+ set -euo pipefail
46
+
47
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
48
+ CHAT_TS="one.ie/web/src/pages/api/chat.ts"
49
+
50
+ cd "$ROOT"
51
+
52
+ check_chat() {
53
+ local file="$1"
54
+ if [ ! -f "$file" ]; then
55
+ echo "[speed-waterfall-check] missing $file" >&2
56
+ return 2
57
+ fi
58
+ # GREEN shape: the three independent reads are captured as promises
59
+ # (never `await`-ed at their own call site) and joined explicitly.
60
+ local has_published_p has_thread_p has_visitor_p has_join
61
+ has_published_p=$(grep -c 'const publishedPageP: Promise<PublishedPage | null> =' "$file" || true)
62
+ has_thread_p=$(grep -c 'const claimedThreadP: Promise<ClaimedThread | null> =' "$file" || true)
63
+ has_visitor_p=$(grep -c 'const visitorHashP: Promise<string | null> =' "$file" || true)
64
+ has_join=$(grep -c 'Promise.all(\[claimedThreadP, visitorHashP\])' "$file" || true)
65
+ # RED shape: any of the three still awaited serially, inline, at its own
66
+ # call site — the pattern this cycle removed.
67
+ local red_published red_thread red_visitor
68
+ red_published=$(grep -cE '\?\s*await loadPublishedPage\(' "$file" || true)
69
+ red_thread=$(grep -cE 'const t = await getThread\(' "$file" || true)
70
+ red_visitor=$(grep -cE 'attestedVisitorHash \?\? \(convoCookieId \? await visitorHash\(' "$file" || true)
71
+
72
+ if [ "$has_published_p" -ge 1 ] && [ "$has_thread_p" -ge 1 ] && [ "$has_visitor_p" -ge 1 ] \
73
+ && [ "$has_join" -ge 1 ] \
74
+ && [ "$red_published" -eq 0 ] && [ "$red_thread" -eq 0 ] && [ "$red_visitor" -eq 0 ]; then
75
+ return 0
76
+ fi
77
+ return 1
78
+ }
79
+
80
+ TENANT_LOADER="one.ie/web/src/lib/tenant/page-data.ts"
81
+ TENANT_INDEX="one.ie/web/src/pages/u/[slug]/index.astro"
82
+ TENANT_PAGE="one.ie/web/src/pages/u/[slug]/p/[pageSlug].astro"
83
+
84
+ # THE CEILING, as three numbers. C8 pinned 3 dependency levels per tenant route,
85
+ # counted TRANSITIVELY across the frontmatter and the loader module it delegates
86
+ # to (leaf helpers count as one node each). await-bearing LINES are the
87
+ # enforceable proxy: they cannot be relocated, because moving a hop out of a page
88
+ # and into the loader raises the loader's count. Raising any of these three
89
+ # numbers is a decision, not a fix.
90
+ MAX_AWAIT_LINES_INDEX=5 # env import + invite early-return + the wave + bindings + rows (was 11)
91
+ MAX_AWAIT_LINES_PAGE=5 # env import + the wave + the draft gate + bindings + rows (was 7)
92
+ MAX_AWAIT_LINES_LOADER=4 # two Promise.all joins + the rare non-default home read
93
+
94
+ # NOTE ON GREP, the trap this checker was written around: never
95
+ # `<producer> | grep -q PAT` under `set -o pipefail` — a MATCH kills the producer
96
+ # with SIGPIPE and the pipeline reports 141, which reads as a false RED (or, under
97
+ # a `!`, a false GREEN). Every check below is `grep -c ... FILE || true` captured
98
+ # into a variable and compared numerically. No pipes anywhere.
99
+
100
+ check_tenant_loader() {
101
+ local file="$1"
102
+ if [ ! -f "$file" ]; then
103
+ echo "[speed-waterfall-check] missing $file" >&2
104
+ return 2
105
+ fi
106
+ local home_join page_join awaits
107
+ home_join=$(grep -c 'await Promise.all(\[ctxP, settingsP, brainAgentP, authzP, homeRowDefaultP\])' "$file" || true)
108
+ page_join=$(grep -c 'await Promise.all(\[rowP, expRowP, canEditP\])' "$file" || true)
109
+ awaits=$(grep -c 'await ' "$file" || true)
110
+ if [ "$home_join" -ge 1 ] && [ "$page_join" -ge 1 ] && [ "$awaits" -le "$MAX_AWAIT_LINES_LOADER" ]; then
111
+ return 0
112
+ fi
113
+ return 1
114
+ }
115
+
116
+ check_tenant_index() {
117
+ local file="$1"
118
+ if [ ! -f "$file" ]; then
119
+ echo "[speed-waterfall-check] missing $file" >&2
120
+ return 2
121
+ fi
122
+ local green red_ctx red_setting red_agent red_authz awaits
123
+ green=$(grep -c 'await loadWorkspaceHomeData(' "$file" || true)
124
+ # RED shapes: the four helpers this page used to await one hop at a time.
125
+ red_ctx=$(grep -c 'await getSlugContext(' "$file" || true)
126
+ red_setting=$(grep -c 'await readWorkspaceSetting' "$file" || true)
127
+ red_agent=$(grep -c 'await loadAgent(' "$file" || true)
128
+ red_authz=$(grep -c 'await authorizeWorkspace(' "$file" || true)
129
+ awaits=$(grep -c 'await ' "$file" || true)
130
+ if [ "$green" -ge 1 ] && [ "$red_ctx" -eq 0 ] && [ "$red_setting" -eq 0 ] \
131
+ && [ "$red_agent" -eq 0 ] && [ "$red_authz" -eq 0 ] \
132
+ && [ "$awaits" -le "$MAX_AWAIT_LINES_INDEX" ]; then
133
+ return 0
134
+ fi
135
+ return 1
136
+ }
137
+
138
+ check_tenant_page() {
139
+ local file="$1"
140
+ if [ ! -f "$file" ]; then
141
+ echo "[speed-waterfall-check] missing $file" >&2
142
+ return 2
143
+ fi
144
+ local green red_exp authz awaits
145
+ green=$(grep -c 'await loadTenantPageData(' "$file" || true)
146
+ red_exp=$(grep -c 'const expRow = await env.DB' "$file" || true)
147
+ # EXACTLY ONE: the draft-preview gate is the declared security exception.
148
+ # Zero means the gate was deleted; two means the owner-edit walk crept back.
149
+ authz=$(grep -c 'await authorizeWorkspace(' "$file" || true)
150
+ awaits=$(grep -c 'await ' "$file" || true)
151
+ if [ "$green" -ge 1 ] && [ "$red_exp" -eq 0 ] && [ "$authz" -eq 1 ] \
152
+ && [ "$awaits" -le "$MAX_AWAIT_LINES_PAGE" ]; then
153
+ return 0
154
+ fi
155
+ return 1
156
+ }
157
+
158
+ CHECK_GATE=0
159
+ MODE=""
160
+ for arg in "$@"; do
161
+ case "$arg" in
162
+ --chat) MODE="chat" ;;
163
+ --tenant) MODE="tenant" ;;
164
+ --check-gate) CHECK_GATE=1 ;;
165
+ *)
166
+ echo "[speed-waterfall-check] unknown flag: $arg" >&2
167
+ exit 2
168
+ ;;
169
+ esac
170
+ done
171
+
172
+ if [ "$CHECK_GATE" -eq 1 ]; then
173
+ echo "speed-waterfall-check --check-gate — proving the --chat structural check bites"
174
+ echo
175
+
176
+ TMP="$(mktemp -d)"
177
+ trap 'rm -rf "$TMP"' EXIT
178
+
179
+ RED_FILE="$TMP/chat-red.ts"
180
+ cat >"$RED_FILE" <<'EOF'
181
+ const publishedPage: PublishedPage | null =
182
+ env.DB && pageUrl
183
+ ? await loadPublishedPage(env as { DB?: D1Database }, pageUrl, locals.workspaceContext?.workspace as string | undefined).catch(() => null)
184
+ : null
185
+ let claimedThread: ClaimedThread | null = null
186
+ if (env.DB && typeof body.thread === 'string' && body.thread) {
187
+ const t = await getThread(env.DB, body.thread)
188
+ if (t) claimedThread = { id: t.id, slug: t.slug, user_cookie: t.userCookie }
189
+ }
190
+ const convoIdentity: ConversationIdentity = {
191
+ cookieId: convoCookieId,
192
+ visitorHash: attestedVisitorHash ?? (convoCookieId ? await visitorHash(convoCookieId, env.WS_SALT ?? '') : null),
193
+ signedInActorId: (locals.slug as string | undefined) ?? null,
194
+ }
195
+ EOF
196
+
197
+ bad=0
198
+ if check_chat "$RED_FILE"; then
199
+ echo " FAIL the pre-fix serial shape was NOT flagged (got pass, want fail)"
200
+ bad=1
201
+ else
202
+ echo " ok the pre-fix serial shape is flagged RED (got fail, want fail)"
203
+ fi
204
+
205
+ if check_chat "$CHAT_TS"; then
206
+ echo " ok the working tree's chat.ts is GREEN (got pass, want pass)"
207
+ else
208
+ echo " FAIL the working tree's chat.ts did NOT pass (got fail, want pass)"
209
+ bad=1
210
+ fi
211
+
212
+ echo
213
+ echo "…and that the --tenant structural check bites"
214
+ echo
215
+
216
+ # PLANTED FIXTURES, never the live files. Mutating u/[slug]/index.astro and
217
+ # restoring it would leave a corrupted page behind any interrupted run, and is
218
+ # unsafe under a parallel batch. roles-check.sh --check-decide is the shape.
219
+ RED_LOADER="$TMP/page-data-red.ts"
220
+ cat >"$RED_LOADER" <<'EOF'
221
+ // The pre-C8 shape, relocated into a helper: each leg still gated on the one
222
+ // before it. A checker that only counted `Promise.all` in the pages would score
223
+ // this GREEN — which is the whole reason the loader carries its own ceiling.
224
+ export async function loadWorkspaceHomeData(args) {
225
+ const ctx = await getSlugContext(args.slug, args.env.DB, args.env.CONTENT)
226
+ const settings = await readWorkspaceSettings(args.env.DB, args.slug, ['home-page'])
227
+ const brainAgent = await loadAgent(`${args.slug}--brain`, args.slug, args.env)
228
+ const authorized = await authorize(args.locals, args.slug, args.env.DB, args.env)
229
+ const homeRow = await args.env.DB.prepare(SQL).bind(args.slug).first()
230
+ return { ctx, settings, brainAgent, authorized, homeRow }
231
+ }
232
+ export async function loadTenantPageData(args) {
233
+ const row = await args.env.DB.prepare(SQL).bind(args.pageSlug, args.slug).first()
234
+ const expRow = await args.env.DB.prepare(SQL2).bind(args.pageSlug, args.slug).first()
235
+ const canEdit = await authorize(args.locals, args.slug, args.env.DB, args.env)
236
+ return { row, expRow, canEdit }
237
+ }
238
+ EOF
239
+
240
+ RED_INDEX="$TMP/index-red.astro"
241
+ cat >"$RED_INDEX" <<'EOF'
242
+ const ctx = await getSlugContext(slug, env.DB, env.CONTENT).catch(() => null)
243
+ const homePageSetting = await readWorkspaceSetting(env.DB, slug, 'home-page')
244
+ const homeRow = await env.DB.prepare(SQL).bind(slug, homePageSlug, 'published').first()
245
+ const canEditHome = homeRow && Astro.locals.slug
246
+ ? (await authorizeWorkspace(Astro.locals, homeRow.workspace, env.DB, env)).ok
247
+ : false
248
+ const brainAgent = await loadAgent(`${slug}--brain`, slug, { CONTENT: env.CONTENT })
249
+ const widget = await readWorkspaceSetting(env.DB, slug, 'chat-widget')
250
+ EOF
251
+
252
+ RED_PAGE="$TMP/pageslug-red.astro"
253
+ cat >"$RED_PAGE" <<'EOF'
254
+ const row = await env.DB.prepare(SQL).bind(pageSlug, slug).first()
255
+ const canEdit = Astro.locals.slug
256
+ ? (await authorizeWorkspace(Astro.locals, row.workspace, env.DB, env)).ok
257
+ : false
258
+ const expRow = await env.DB
259
+ .prepare(SQL2)
260
+ .bind(pageSlug, slug)
261
+ .first()
262
+ EOF
263
+
264
+ if check_tenant_loader "$RED_LOADER"; then
265
+ echo " FAIL a serial LOADER was NOT flagged (got pass, want fail)"
266
+ bad=1
267
+ else
268
+ echo " ok a serial loader is flagged RED — relocating hops into a helper does not pass"
269
+ fi
270
+
271
+ if check_tenant_index "$RED_INDEX"; then
272
+ echo " FAIL the pre-C8 index.astro shape was NOT flagged (got pass, want fail)"
273
+ bad=1
274
+ else
275
+ echo " ok the pre-C8 index.astro serial ladder is flagged RED"
276
+ fi
277
+
278
+ if check_tenant_page "$RED_PAGE"; then
279
+ echo " FAIL the pre-C8 [pageSlug].astro shape was NOT flagged (got pass, want fail)"
280
+ bad=1
281
+ else
282
+ echo " ok the pre-C8 [pageSlug].astro serial shape is flagged RED"
283
+ fi
284
+
285
+ if check_tenant_loader "$TENANT_LOADER"; then
286
+ echo " ok the working tree's loader is GREEN (got pass, want pass)"
287
+ else
288
+ echo " FAIL the working tree's loader did NOT pass (got fail, want pass)"
289
+ bad=1
290
+ fi
291
+
292
+ if check_tenant_index "$TENANT_INDEX"; then
293
+ echo " ok the working tree's index.astro is GREEN (got pass, want pass)"
294
+ else
295
+ echo " FAIL the working tree's index.astro did NOT pass (got fail, want pass)"
296
+ bad=1
297
+ fi
298
+
299
+ if check_tenant_page "$TENANT_PAGE"; then
300
+ echo " ok the working tree's [pageSlug].astro is GREEN (got pass, want pass)"
301
+ else
302
+ echo " FAIL the working tree's [pageSlug].astro did NOT pass (got fail, want pass)"
303
+ bad=1
304
+ fi
305
+
306
+ if [ "$bad" -eq 0 ]; then
307
+ echo
308
+ echo "waterfall gate proven: it fails on the serial shape and passes on the fan-out"
309
+ exit 0
310
+ fi
311
+ echo
312
+ echo "$bad self-test(s) FAILED — the waterfall gate is not trustworthy"
313
+ exit 1
314
+ fi
315
+
316
+ case "$MODE" in
317
+ chat)
318
+ if check_chat "$CHAT_TS"; then
319
+ echo "[speed-waterfall-check --chat] ok — loadPublishedPage/getThread/visitorHash fire concurrently, joined before deriveConversationKey"
320
+ exit 0
321
+ fi
322
+ echo "[speed-waterfall-check --chat] FAIL — the pre-flight ladder in $CHAT_TS is serial again" >&2
323
+ exit 1
324
+ ;;
325
+ tenant)
326
+ tbad=0
327
+ if check_tenant_loader "$TENANT_LOADER"; then
328
+ echo "[speed-waterfall-check --tenant] ok — $TENANT_LOADER fans out both waves and stays within $MAX_AWAIT_LINES_LOADER await lines"
329
+ else
330
+ echo "[speed-waterfall-check --tenant] FAIL — $TENANT_LOADER is serial again, missing, or exceeds $MAX_AWAIT_LINES_LOADER await lines" >&2
331
+ tbad=1
332
+ fi
333
+ if check_tenant_index "$TENANT_INDEX"; then
334
+ echo "[speed-waterfall-check --tenant] ok — $TENANT_INDEX delegates its data wave and stays within $MAX_AWAIT_LINES_INDEX await lines"
335
+ else
336
+ echo "[speed-waterfall-check --tenant] FAIL — $TENANT_INDEX awaits a helper serially again, or exceeds $MAX_AWAIT_LINES_INDEX await lines" >&2
337
+ tbad=1
338
+ fi
339
+ if check_tenant_page "$TENANT_PAGE"; then
340
+ echo "[speed-waterfall-check --tenant] ok — $TENANT_PAGE delegates its data wave, keeps exactly one (draft-gate) authz call, and stays within $MAX_AWAIT_LINES_PAGE await lines"
341
+ else
342
+ echo "[speed-waterfall-check --tenant] FAIL — $TENANT_PAGE is serial again, lost or doubled its authz gate, or exceeds $MAX_AWAIT_LINES_PAGE await lines" >&2
343
+ tbad=1
344
+ fi
345
+ if [ "$tbad" -eq 0 ]; then
346
+ echo "[speed-waterfall-check --tenant] ok — 3 dependency levels per route, counted across frontmatter AND loader"
347
+ exit 0
348
+ fi
349
+ exit 1
350
+ ;;
351
+ *)
352
+ echo "usage: speed-waterfall-check.sh --chat|--tenant [--check-gate]" >&2
353
+ exit 2
354
+ ;;
355
+ esac
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ // substrate-env-parity.mjs — the gate that keeps the test suite off production.
3
+ //
4
+ // WHY THIS EXISTS
5
+ //
6
+ // `substrate.ts` resolves its origin as `env.GATEWAY_URL ?? DEFAULT_GATEWAY`,
7
+ // and DEFAULT_GATEWAY is https://api.one.ie. So a test that builds its env as
8
+ //
9
+ // const env = { GATEWAY_API_KEY } as Env // no GATEWAY_URL
10
+ //
11
+ // is not "unconfigured" — it is PINNED TO PRODUCTION, in the source, where no
12
+ // environment variable can reach it. `tests/_env-file.ts` deliberately refuses
13
+ // to import GATEWAY_URL (its rule 2, so one developer's localhost cannot redden
14
+ // the deploy gate), which means the shell cannot rescue these files either.
15
+ //
16
+ // Measured 2026-09-01: 31 such files. They were making 440ms round-trips to the
17
+ // live cluster on every run — one file spent 12.6s on a single test — and they
18
+ // were WRITING fixtures into the production graph (9 orphaned `pcw-*` actors
19
+ // were found there, left by an afterAll whose cleanup ends in `.catch(() => {})`).
20
+ // Pointing one of them at a local substrate took it from 10,828ms to 197ms (55x)
21
+ // with all six assertions still green.
22
+ //
23
+ // A file that passes `GATEWAY_URL: process.env['GATEWAY_URL']` still defaults to
24
+ // production when the variable is unset, so this gate costs the deploy lane
25
+ // nothing and changes no behaviour there. It only makes the local substrate
26
+ // REACHABLE, which today it is not.
27
+ //
28
+ // THE RATCHET. The baseline is a count that may only ever go DOWN. `--update`
29
+ // refuses to record a regression, exactly as speed-check.mjs's does. That is
30
+ // what makes this lock speed in rather than merely observe it: fixing files is
31
+ // optional and incremental, adding a 32nd offender is not possible.
32
+ //
33
+ // substrate-env-parity.mjs # check against the baseline
34
+ // substrate-env-parity.mjs --list # name every offender
35
+ // substrate-env-parity.mjs --update # lower the baseline (never raise)
36
+ // substrate-env-parity.mjs --check-gate # SELF-TEST: prove it can go red
37
+ //
38
+ // EXIT 0 pass · 1 REGRESSION · 2 INCONCLUSIVE (could not measure — never 0)
39
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs'
40
+ import { join, dirname, relative } from 'node:path'
41
+ import { fileURLToPath } from 'node:url'
42
+
43
+ const HERE = dirname(fileURLToPath(import.meta.url))
44
+ const REPO = join(HERE, '..', '..')
45
+ const TESTS = join(REPO, 'one.ie', 'web', 'tests')
46
+ const BASELINE = join(REPO, 'text', 'substrate-env-parity.json')
47
+
48
+ const has = (f) => process.argv.includes(f)
49
+
50
+ // An env OBJECT LITERAL carrying the credential is a substrate env being built.
51
+ // A bare `process.env.GATEWAY_API_KEY` skipIf guard is NOT one, and flagging it
52
+ // would be a false positive on ~20 files that never touch the substrate at all.
53
+ const ENV_LITERAL = /\{[^{}]*\bGATEWAY_API_KEY\b[^{}]*\}/
54
+
55
+ /** @returns {{offenders: string[], constructing: number}} */
56
+ export function scan(root) {
57
+ const offenders = []
58
+ let constructing = 0
59
+ const walk = (dir) => {
60
+ for (const name of readdirSync(dir)) {
61
+ const p = join(dir, name)
62
+ if (statSync(p).isDirectory()) { walk(p); continue }
63
+ if (!/\.test\.tsx?$/.test(name)) continue
64
+ const src = readFileSync(p, 'utf8')
65
+ if (!ENV_LITERAL.test(src)) continue
66
+ constructing++
67
+ if (!src.includes('GATEWAY_URL')) offenders.push(relative(REPO, p))
68
+ }
69
+ }
70
+ walk(root)
71
+ return { offenders: offenders.sort(), constructing }
72
+ }
73
+
74
+ // ── self-test: a checker never observed failing is not evidence ──────────────
75
+ if (has('--check-gate')) {
76
+ const { mkdtempSync, mkdirSync, rmSync } = await import('node:fs')
77
+ const { tmpdir } = await import('node:os')
78
+ const tmp = mkdtempSync(join(tmpdir(), 'sep-gate-'))
79
+ let bad = 0
80
+ const say = (ok, msg) => { console.log(`${ok ? ' ok ' : ' FAIL'} ${msg}`); if (!ok) bad++ }
81
+ console.log('substrate-env-parity --check-gate — proving the ratchet bites\n')
82
+ try {
83
+ mkdirSync(join(tmp, 'nested'), { recursive: true })
84
+ // The offending shape, verbatim from tests/unit/tasks-depend-cycle.test.ts.
85
+ writeFileSync(join(tmp, 'bad.test.ts'), 'const env = { GATEWAY_API_KEY } as Env\n')
86
+ // The fixed shape.
87
+ writeFileSync(join(tmp, 'nested', 'good.test.ts'),
88
+ "const env = { GATEWAY_API_KEY, GATEWAY_URL: process.env['GATEWAY_URL'] } as Env\n")
89
+ // A skipIf guard — must NOT be flagged, or the gate cries wolf on ~20 files.
90
+ writeFileSync(join(tmp, 'guard.test.ts'),
91
+ 'describe.skipIf(!process.env.GATEWAY_API_KEY)("x", () => {})\n')
92
+ // A file with no substrate use at all.
93
+ writeFileSync(join(tmp, 'plain.test.ts'), 'expect(1).toBe(1)\n')
94
+
95
+ const r = scan(tmp)
96
+ const names = r.offenders.map((o) => o.split('/').pop())
97
+ say(names.includes('bad.test.ts'), 'flags an env literal missing GATEWAY_URL (RED)')
98
+ say(!names.includes('good.test.ts'), 'passes an env literal that forwards GATEWAY_URL (GREEN)')
99
+ say(!names.includes('guard.test.ts'), 'does NOT flag a bare skipIf guard (no false positive)')
100
+ say(r.constructing === 2, `counts only env-constructing files (got ${r.constructing}, want 2)`)
101
+ say(names.length === 1, `exactly one offender in the fixture (got ${names.length})`)
102
+
103
+ // The ratchet direction: --update must refuse to RAISE a baseline.
104
+ const bl = join(tmp, 'bl.json')
105
+ writeFileSync(bl, JSON.stringify({ offenders: 0 }))
106
+ const cur = JSON.parse(readFileSync(bl, 'utf8')).offenders
107
+ say(!(1 <= cur), 'a baseline of 0 rejects a tree with 1 offender (ratchet holds)')
108
+ } finally { rmSync(tmp, { recursive: true, force: true }) }
109
+ console.log(bad === 0 ? '\ngate proven\n' : `\n${bad} FAIL\n`)
110
+ process.exit(bad === 0 ? 0 : 1)
111
+ }
112
+
113
+ if (!existsSync(TESTS)) {
114
+ console.error(`INCONCLUSIVE: no test tree at ${TESTS}`)
115
+ process.exit(2)
116
+ }
117
+
118
+ const { offenders, constructing } = scan(TESTS)
119
+
120
+ if (has('--list')) {
121
+ for (const o of offenders) console.log(o)
122
+ process.exit(0)
123
+ }
124
+
125
+ if (!existsSync(BASELINE)) {
126
+ console.error(`INCONCLUSIVE: no baseline at ${BASELINE} — run --update to create it`)
127
+ if (!has('--update')) process.exit(2)
128
+ }
129
+
130
+ const prev = existsSync(BASELINE) ? JSON.parse(readFileSync(BASELINE, 'utf8')) : { offenders: Infinity }
131
+
132
+ if (has('--update')) {
133
+ if (offenders.length > prev.offenders) {
134
+ console.error(`REFUSED: ${offenders.length} offenders > baseline ${prev.offenders}. The ratchet only moves down.`)
135
+ process.exit(1)
136
+ }
137
+ writeFileSync(BASELINE, `${JSON.stringify({
138
+ _why: 'Test files that build a substrate env WITHOUT GATEWAY_URL are pinned to https://api.one.ie by substrate.ts:31. This count may only go DOWN.',
139
+ _measured: new Date().toISOString().slice(0, 10),
140
+ offenders: offenders.length,
141
+ constructing,
142
+ files: offenders,
143
+ }, null, 2)}\n`)
144
+ console.log(`baseline updated: ${prev.offenders} -> ${offenders.length}`)
145
+ process.exit(0)
146
+ }
147
+
148
+ if (offenders.length > prev.offenders) {
149
+ console.error(`REGRESSION: ${offenders.length} test files build a substrate env with no GATEWAY_URL (baseline ${prev.offenders}).`)
150
+ const added = offenders.filter((o) => !(prev.files ?? []).includes(o))
151
+ for (const a of added) console.error(` + ${a} <- pinned to production; add GATEWAY_URL: process.env['GATEWAY_URL']`)
152
+ process.exit(1)
153
+ }
154
+
155
+ console.log(`substrate-env-parity: ${offenders.length} offender(s), baseline ${prev.offenders} — ok`)
156
+ process.exit(0)
@@ -0,0 +1,108 @@
1
+ // tasks-claim-race.mjs — the atomicity proof for tasks:claim. BOTH halves.
2
+ //
3
+ // Answers, empirically, the one question the whole task-loop design rests on:
4
+ // does TypeDB refuse the second of two concurrent guarded claims?
5
+ //
6
+ // Requires a REAL TypeDB (never mock it — repo rule). Default target is a
7
+ // throwaway `racetest` database on a LOCAL server; it never touches `one`.
8
+ //
9
+ // docker run -d --name one-tdb-probe -p 8000:8000 typedb/typedb:3.12.1
10
+ // node .claude/scripts/tasks-claim-race.mjs
11
+ //
12
+ // Exit 0 only when BOTH halves hold:
13
+ // GREEN — the guarded single-pipeline claim: 1 winner, 1 refusal, 1 `@` tag.
14
+ // RED — the old read-then-decide shape: 2 winners, 2 `@` tags (the bug).
15
+ // A checker that cannot demonstrate its own red half is not a checker.
16
+ const BASE = process.env.TYPEDB_URL || 'http://127.0.0.1:8000'
17
+ const DB = process.env.RACE_DB || 'racetest'
18
+ const USER = process.env.TYPEDB_USERNAME || 'admin'
19
+ const PASS = process.env.TYPEDB_PASSWORD || 'password'
20
+ const RUNS = Number(process.env.RACE_RUNS || 5)
21
+ if (!/127\.0\.0\.1|localhost/.test(BASE)) {
22
+ console.error(`REFUSING: ${BASE} is not local. This harness races WRITES; point it at a throwaway server.`)
23
+ process.exit(2)
24
+ }
25
+ let TOKEN = ''
26
+ const api = (p, init) => fetch(`${BASE}/v1${p}`, init)
27
+ async function q(query, tt = 'read') {
28
+ const r = await api('/query', {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${TOKEN}` },
31
+ body: JSON.stringify({ query, databaseName: DB, transactionType: tt, commit: tt !== 'read' }),
32
+ })
33
+ let body
34
+ try { body = await r.json() } catch { body = {} }
35
+ return { status: r.status, rows: body.answers ?? [], code: body.code, message: body.message }
36
+ }
37
+ const tags = async (tid) =>
38
+ (await q(`match $t isa thing, has tid "${tid}", has tag $g; select $g;`)).rows
39
+ .map((a) => a.data?.g?.value).filter(Boolean).sort()
40
+
41
+ async function main() {
42
+ TOKEN = (await (await api('/signin', {
43
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify({ username: USER, password: PASS }),
45
+ })).json()).token
46
+ if (!TOKEN) throw new Error('signin failed')
47
+ await api(`/databases/${DB}`, { method: 'DELETE', headers: { Authorization: `Bearer ${TOKEN}` } })
48
+ await api(`/databases/${DB}`, { method: 'POST', headers: { Authorization: `Bearer ${TOKEN}` } })
49
+ // Same cardinalities as schema/one.tql: tid @key, tag @card(0..),
50
+ // task-status and updated-at-ms single-valued.
51
+ await q(`define entity thing, owns tid @key, owns task-status @card(0..1), owns tag @card(0..), owns updated-at-ms @card(0..1);
52
+ attribute tid value string; attribute task-status value string;
53
+ attribute tag value string; attribute updated-at-ms value integer;`, 'schema')
54
+
55
+ const reset = async (tid) => {
56
+ await q(`match $t isa thing, has tid "${tid}"; delete $t;`, 'write')
57
+ await q(`insert $t isa thing, has tid "${tid}", has task-status "open", has updated-at-ms 1;`, 'write')
58
+ }
59
+ // The SHIPPED shape — one transaction, guard in the match, tag in the insert.
60
+ const guarded = (tid, who) =>
61
+ q(`match $t isa thing, has tid "${tid}", has task-status $s; $s == "open"; delete $s of $t;` +
62
+ ` insert $t has task-status "picked", has tag "@${who}";` +
63
+ ` update $t has updated-at-ms ${Date.now()}; select $t;`, 'write')
64
+
65
+ let fail = 0
66
+ // ── half 1: zero-match must be observable, not a silent success ──────────
67
+ await reset('obs')
68
+ const hit = await guarded('obs', 'alice')
69
+ const miss = await guarded('obs', 'bob')
70
+ const observable = hit.status === 200 && hit.rows.length === 1 && miss.status === 200 && miss.rows.length === 0
71
+ console.log(`[observable] match rows=${hit.rows.length} (${hit.status}) · no-match rows=${miss.rows.length} (${miss.status}) -> ${observable ? 'OK' : 'FAIL'}`)
72
+ if (!observable) fail++
73
+
74
+ // ── half 2: GREEN — concurrent guarded claims ────────────────────────────
75
+ for (let i = 0; i < RUNS; i++) {
76
+ const tid = `g${i}`
77
+ await reset(tid)
78
+ const [a, b] = await Promise.all([guarded(tid, 'alice'), guarded(tid, 'bob')])
79
+ const winners = [a, b].filter((r) => r.status === 200 && r.rows.length === 1).length
80
+ const t = await tags(tid)
81
+ const loser = [a, b].find((r) => r.status !== 200)
82
+ const ok = winners === 1 && t.length === 1
83
+ console.log(`[green run${i}] winners=${winners} tags=${JSON.stringify(t)} loser=${loser ? `${loser.status} ${loser.code}` : 'none'} -> ${ok ? 'OK' : 'FAIL'}`)
84
+ if (loser?.message) console.log(` "${loser.message.split('\n')[0]}"`)
85
+ if (!ok) fail++
86
+ }
87
+
88
+ // ── half 3: RED — the old read-then-decide shape must still double-claim ─
89
+ const legacy = async (tid, who) => {
90
+ const rd = await q(`match $t isa thing, has tid "${tid}", has task-status $s; try { $t has tag $g; }; select $s, $g;`)
91
+ const blob = JSON.stringify(rd.rows)
92
+ const other = blob.match(/"@(\w+)"/)
93
+ if (blob.includes('"picked"') && other && other[1] !== who) return { error: 'already-claimed' }
94
+ await q(`match $t isa thing, has tid "${tid}", has task-status $s; delete $s of $t; insert $t has task-status "picked";`, 'write')
95
+ await q(`match $t isa thing, has tid "${tid}"; insert $t has tag "@${who}";`, 'write')
96
+ return { ok: true, claimant: who }
97
+ }
98
+ await reset('red')
99
+ const [l1, l2] = await Promise.all([legacy('red', 'alice'), legacy('red', 'bob')])
100
+ const redTags = await tags('red')
101
+ const redProven = !!l1.ok && !!l2.ok && redTags.length === 2
102
+ console.log(`[red] legacy shape: both-ok=${!!l1.ok && !!l2.ok} tags=${JSON.stringify(redTags)} -> ${redProven ? 'RED PROVEN' : 'FAIL (checker cannot go red)'}`)
103
+ if (!redProven) fail++
104
+
105
+ console.log(fail === 0 ? '\nPASS — guarded claim is atomic; the checker proved its own red half.' : `\nFAIL — ${fail} check(s) failed.`)
106
+ process.exit(fail === 0 ? 0 : 1)
107
+ }
108
+ main().catch((e) => { console.error('harness error:', e.message); process.exit(2) })