@oneie/claude 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/abm-strategist.md +67 -1
  3. package/agents/ads-meta.md +67 -1
  4. package/agents/analyst.md +67 -1
  5. package/agents/animator.md +108 -0
  6. package/agents/architect.md +269 -20
  7. package/agents/brand-guardian.md +67 -1
  8. package/agents/brand-strategist.md +67 -1
  9. package/agents/campaign-content.md +67 -1
  10. package/agents/campaign-email.md +67 -1
  11. package/agents/campaign-sms.md +67 -1
  12. package/agents/campaign-social.md +67 -1
  13. package/agents/cco.md +83 -2
  14. package/agents/ceo.md +108 -11
  15. package/agents/chairman.md +197 -0
  16. package/agents/cmo.md +82 -2
  17. package/agents/community-greeter.md +67 -1
  18. package/agents/community-moderator.md +67 -1
  19. package/agents/compliance.md +67 -1
  20. package/agents/copywriter.md +67 -1
  21. package/agents/creative-strategist.md +67 -1
  22. package/agents/cro.md +81 -1
  23. package/agents/cto.md +266 -28
  24. package/agents/customer-interviewer.md +67 -1
  25. package/agents/customer-researcher.md +67 -1
  26. package/agents/customer-success-manager.md +67 -1
  27. package/agents/customer-trainer.md +67 -1
  28. package/agents/cxo.md +82 -1
  29. package/agents/demand-creator.md +67 -1
  30. package/agents/demo-mover.md +67 -1
  31. package/agents/demo-specialist.md +67 -1
  32. package/agents/demo-thai-family-law.md +67 -1
  33. package/agents/designer.md +67 -1
  34. package/agents/discovery-caller.md +67 -1
  35. package/agents/doctor.md +269 -0
  36. package/agents/educate-coach.md +67 -1
  37. package/agents/elevate-tutor.md +67 -1
  38. package/agents/email-lifecycle-marketer.md +67 -1
  39. package/agents/engage-specialist.md +67 -1
  40. package/agents/events-coordinator.md +67 -1
  41. package/agents/foundation-builder.md +67 -1
  42. package/agents/funnel-architect.md +67 -1
  43. package/agents/gift-creator.md +67 -1
  44. package/agents/google-ads.md +67 -1
  45. package/agents/guide.md +67 -1
  46. package/agents/helpdesk-dispatcher.md +67 -1
  47. package/agents/hook-specialist.md +67 -1
  48. package/agents/identify-optimizer.md +67 -1
  49. package/agents/implementer.md +313 -45
  50. package/agents/incident-commander.md +67 -1
  51. package/agents/insights-lead.md +87 -1
  52. package/agents/journey-runner.md +67 -1
  53. package/agents/linkedin-ads.md +67 -1
  54. package/agents/live-sales-chat.md +67 -1
  55. package/agents/market-researcher.md +67 -1
  56. package/agents/media-buyer.md +67 -1
  57. package/agents/memory-keeper.md +195 -0
  58. package/agents/movers-customer-researcher.md +67 -1
  59. package/agents/movers-foundation-builder.md +67 -1
  60. package/agents/movers-market-researcher.md +67 -1
  61. package/agents/movers-pricing-strategist.md +67 -1
  62. package/agents/nurture-architect.md +67 -1
  63. package/agents/offer-architect.md +67 -1
  64. package/agents/onboarder.md +67 -1
  65. package/agents/onboarding-specialist.md +67 -1
  66. package/agents/operations-dashboard.md +87 -1
  67. package/agents/perf-engineer.md +333 -37
  68. package/agents/playbook-writer.md +67 -1
  69. package/agents/plg-strategist.md +67 -1
  70. package/agents/positioning-architect.md +67 -1
  71. package/agents/press-officer.md +67 -1
  72. package/agents/pricing-strategist.md +67 -1
  73. package/agents/privacy-officer.md +67 -1
  74. package/agents/referral-manager.md +67 -1
  75. package/agents/refine-analyst.md +67 -1
  76. package/agents/release-manager.md +446 -39
  77. package/agents/renewals-upsell-rep.md +67 -1
  78. package/agents/review-engineer.md +319 -45
  79. package/agents/rewards-steward.md +67 -1
  80. package/agents/sales-call-coach.md +67 -1
  81. package/agents/sales-closer.md +67 -1
  82. package/agents/security-auditor.md +343 -48
  83. package/agents/sell-closer.md +67 -1
  84. package/agents/share-amplifier.md +67 -1
  85. package/agents/social-media-manager.md +67 -1
  86. package/agents/storyteller.md +301 -0
  87. package/agents/strategist.md +67 -1
  88. package/agents/strategy-aligner.md +67 -1
  89. package/agents/support-agent.md +67 -1
  90. package/agents/tagger.md +327 -0
  91. package/agents/tech-writer.md +195 -22
  92. package/agents/test-engineer.md +398 -29
  93. package/agents/tiktok-ads.md +67 -1
  94. package/agents/tracking-engineer.md +67 -1
  95. package/agents/trailkeeper.md +181 -0
  96. package/agents/upsell-strategist.md +67 -1
  97. package/agents/voice.md +67 -1
  98. package/agents/w1-recon.md +1 -1
  99. package/agents/w2-decide.md +1 -1
  100. package/agents/w3-edit.md +8 -2
  101. package/agents/w4-verify.md +13 -0
  102. package/agents/workflow-optimiser.md +81 -1
  103. package/commands/close.md +916 -160
  104. package/commands/deploy.md +102 -724
  105. package/commands/do.md +58 -2
  106. package/commands/sweep.md +159 -0
  107. package/commands/tasks.md +222 -0
  108. package/hooks/scripts/dev-only.sh +135 -0
  109. package/hooks/scripts/git-add-guard.sh +37 -2
  110. package/hooks/scripts/session-start.sh +32 -4
  111. package/package.json +1 -1
  112. package/rules/scripts.md +85 -0
  113. package/scripts/CLAUDE.md +315 -0
  114. package/scripts/ad-copy-lint.sh +656 -0
  115. package/scripts/agent-actor-parity.sh +129 -0
  116. package/scripts/blocks-manifest-cached.sh +100 -0
  117. package/scripts/chat-context-check.sh +89 -0
  118. package/scripts/chrome.mjs +18 -0
  119. package/scripts/close-metrics.sh +587 -0
  120. package/scripts/close-owner.sh +326 -0
  121. package/scripts/db-sync-lock-check.sh +116 -0
  122. package/scripts/deploy-emit.sh +311 -0
  123. package/scripts/deploy-gate-check.sh +155 -0
  124. package/scripts/deploy-ready.sh +78 -0
  125. package/scripts/deploy-record.sh +605 -0
  126. package/scripts/deploy-schema-check.sh +58 -0
  127. package/scripts/deploy.sh +393 -243
  128. package/scripts/do-auto.sh +127 -26
  129. package/scripts/do-board.sh +429 -0
  130. package/scripts/do-close.sh +1184 -0
  131. package/scripts/do-consumer-sweep.sh +18 -1
  132. package/scripts/do-decide.sh +476 -0
  133. package/scripts/do-fleet.sh +8 -2
  134. package/scripts/do-plan-json.mjs +110 -12
  135. package/scripts/do-prove-selftest.sh +108 -0
  136. package/scripts/do-prove.sh +86 -10
  137. package/scripts/do-rank.py +200 -3
  138. package/scripts/do-reconcile.sh +73 -12
  139. package/scripts/do-signal.sh +101 -23
  140. package/scripts/do-smoke.sh +18 -1
  141. package/scripts/do-w4-gates.sh +11 -1
  142. package/scripts/do-world-check.sh +153 -0
  143. package/scripts/download-stats.sh +172 -0
  144. package/scripts/factory-brief-check.sh +330 -0
  145. package/scripts/factory-check.sh +18 -1
  146. package/scripts/factory-close-check.sh +257 -0
  147. package/scripts/factory-emit.sh +211 -0
  148. package/scripts/factory-executor-check.mjs +353 -0
  149. package/scripts/factory-peak.sh +301 -0
  150. package/scripts/factory-repo.sh +71 -0
  151. package/scripts/factory-review-check.mjs +61 -0
  152. package/scripts/factory-tasks-check.sh +18 -1
  153. package/scripts/fixtures/factory-brief-real.md +44 -0
  154. package/scripts/flywheel-outcome.sh +63 -0
  155. package/scripts/gate-reaper-check.sh +98 -0
  156. package/scripts/gate-reaper.sh +9 -0
  157. package/scripts/gate-watchdog.sh +619 -0
  158. package/scripts/gc-content-check.sh +142 -0
  159. package/scripts/gh-traffic-capture.sh +153 -0
  160. package/scripts/govern-order-check.sh +202 -0
  161. package/scripts/governor-doors-check.sh +86 -5
  162. package/scripts/health.sh +448 -0
  163. package/scripts/id-inventory.mjs +418 -0
  164. package/scripts/incident.sh +212 -0
  165. package/scripts/land.sh +755 -45
  166. package/scripts/lib/gc-finished.sh +77 -0
  167. package/scripts/livekit-ratchet.sh +18 -1
  168. package/scripts/machine-check.sh +1 -1
  169. package/scripts/memory-index-budget.sh +79 -0
  170. package/scripts/npm-downloads.sh +109 -0
  171. package/scripts/one-agents.mjs +204 -8
  172. package/scripts/one-resume.sh +31 -3
  173. package/scripts/pr-body.sh +335 -0
  174. package/scripts/preview-fd-check.sh +289 -0
  175. package/scripts/redirect-lint.sh +169 -0
  176. package/scripts/release.sh +40 -6
  177. package/scripts/resume-lost-sessions.sh +68 -0
  178. package/scripts/shoot-pages.mjs +140 -0
  179. package/scripts/signal-meta-backfill.ts +451 -0
  180. package/scripts/signal-watch.sh +63 -6
  181. package/scripts/speed-cache-check.sh +12 -2
  182. package/scripts/sweep.sh +426 -0
  183. package/scripts/task-titles-dump.ts +101 -0
  184. package/scripts/test-cached.sh +47 -10
  185. package/scripts/test-lanes.sh +14 -0
  186. package/scripts/thread-name-backfill.ts +215 -0
  187. package/scripts/triage-shape-check.sh +149 -0
  188. package/scripts/tsc-cached.sh +155 -8
  189. package/scripts/typedb-flake-check.sh +3 -1
  190. package/scripts/urls-lint.sh +8 -0
  191. package/scripts/verify-board-doors.sh +80 -0
  192. package/scripts/verify-fast.sh +159 -6
  193. package/scripts/worktree-up.sh +21 -3
  194. package/skills/astro/SKILL.md +9 -3
  195. package/skills/astro/optimize-performance.md +3 -2
  196. package/skills/cloudflare/SKILL.md +3 -2
  197. package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
  198. package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
  199. package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
  200. package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  201. package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  202. package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  203. package/skills/cloudflare-security-audit/HUNTING.md +251 -0
  204. package/skills/cloudflare-security-audit/LICENSE +21 -0
  205. package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  206. package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  207. package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
  208. package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
  209. package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  210. package/skills/cloudflare-security-audit/SKILL.md +192 -0
  211. package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  212. package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
  213. package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  214. package/skills/cloudflare-security-audit/report-schema.json +461 -0
  215. package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
  216. package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
  217. package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
  218. package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
  219. package/skills/deploy/REFERENCE.md +713 -0
  220. package/skills/deploy/SKILL.md +140 -0
  221. package/skills/fleet-audit/SKILL.md +58 -0
  222. package/skills/meeting/SKILL.md +220 -0
  223. package/skills/planning/SKILL.md +256 -0
  224. package/skills/shadcn/SKILL.md +1 -1
  225. package/skills/typedb/SKILL.md +7 -0
  226. package/skills/voice/SKILL.md +94 -6
  227. package/skills/voice/corpus-check.sh +87 -0
  228. package/templates/template-agent.md +7 -1
  229. package/templates/template-feature.md +9 -0
  230. package/templates/template-todo.md +29 -0
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env bun
2
+ // task-titles-dump.ts — the offline `{ tid: title }` map the thread-name backfill reads.
3
+ //
4
+ // manifest: monorepo-only (reads this repo's one.ie/web/.dev.vars and calls one.ie's
5
+ // own receivers by name)
6
+ //
7
+ // WHY IT EXISTS. A task thread's name is the task's TITLE, and the D1 `entity_meta`
8
+ // overlay that will carry it is written by `tasks:create`/`subtask`/`rename` FROM NOW
9
+ // ON — measured 2026-09-13, production's overlay held 2 rows and 0 titles. So for
10
+ // every task that already exists the title lives only in the brain, and
11
+ // `thread-name-backfill.ts --titles` is the door. Without a map it names 15 of 1055
12
+ // rows in `one`; with one it names 411.
13
+ //
14
+ // READ-ONLY, AND IT NEVER TOUCHES THE BRAIN DIRECTLY. Two shipped receivers over
15
+ // HTTP: `tasks:everywhere` once, only to learn which BARE tags this workspace
16
+ // actually uses, then `tasks:list` per tag — the one door that returns every row for
17
+ // a tag with no cap. `tasks:board` would answer in a single call and is the right
18
+ // door the day it reaches production; it dissolves as `unknown_receiver` there today
19
+ // (measured 2026-09-13), which is the whole reason this unions tag pages instead.
20
+ //
21
+ // THE COUNT IS A FLOOR, NOT A TOTAL, and the file says so. `tasks:everywhere` on
22
+ // production still clamps at 200 rows with no `total`/`truncated` beside it, so the
23
+ // TAG SET it yields is whatever those 200 rows carry — a tag used only by older rows
24
+ // is invisible, and its tasks are simply absent from the map. A thread whose title is
25
+ // missing is COUNTED AND LEFT by the backfill, never guessed at, so an incomplete map
26
+ // under-names and never mis-names. Add `--tag <t>` to force one in.
27
+ //
28
+ // USAGE
29
+ // bun .claude/scripts/task-titles-dump.ts --slug ehc --out .backfill/titles-ehc.json
30
+ // bun .claude/scripts/task-titles-dump.ts --slug one --tag legacy --tag archived
31
+
32
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
33
+ import { dirname, resolve } from 'node:path'
34
+
35
+ const ROOT = new URL('../..', import.meta.url).pathname
36
+ const API = process.env['ONE_API_URL'] || 'https://one.ie'
37
+
38
+ const argv = process.argv.slice(2)
39
+ const flag = (name: string, fallback = ''): string => {
40
+ const i = argv.indexOf(`--${name}`)
41
+ return i >= 0 && argv[i + 1] && !argv[i + 1]!.startsWith('--') ? argv[i + 1]! : fallback
42
+ }
43
+ const flags = (name: string): string[] =>
44
+ argv.reduce<string[]>((acc, a, i) => (a === `--${name}` && argv[i + 1] && !argv[i + 1]!.startsWith('--') ? [...acc, argv[i + 1]!] : acc), [])
45
+
46
+ const slug = flag('slug')
47
+ const out = resolve(ROOT, flag('out', `.backfill/titles-${slug}.json`))
48
+ const extraTags = flags('tag')
49
+
50
+ if (!slug) {
51
+ console.error('usage: bun .claude/scripts/task-titles-dump.ts --slug <workspace> [--out FILE] [--tag <bare-word>]…')
52
+ process.exit(2)
53
+ }
54
+
55
+ // The service key prod accepts is the one in `.dev.vars`; the copy in `.env` is
56
+ // REFUSED by production (measured 2026-09-06). Never printed.
57
+ const devVars = readFileSync(`${ROOT}/one.ie/web/.dev.vars`, 'utf8')
58
+ const KEY = (devVars.match(/^GATEWAY_API_KEY=(.*)$/m)?.[1] ?? '').trim().replace(/^"|"$/g, '')
59
+ if (!KEY) {
60
+ console.error('no GATEWAY_API_KEY in one.ie/web/.dev.vars — stopping rather than calling the door unauthenticated')
61
+ process.exit(3)
62
+ }
63
+
64
+ async function ask<T>(receiver: string, data: unknown): Promise<T | null> {
65
+ const res = await fetch(`${API}/api/ask/${receiver}`, {
66
+ method: 'POST',
67
+ headers: { Authorization: `Bearer ${KEY}`, 'content-type': 'application/json' },
68
+ body: JSON.stringify({ data }),
69
+ })
70
+ // BOTH refusals answer HTTP 200 — success is `outcome=='result'` AND `result.ok`.
71
+ const body = (await res.json()) as { outcome?: string; result?: { ok?: boolean } & T; reason?: string }
72
+ if (body.outcome !== 'result' || !body.result?.ok) {
73
+ console.error(` ${receiver} refused: ${body.outcome ?? res.status}${body.reason ? ` (${body.reason})` : ''}`)
74
+ return null
75
+ }
76
+ return body.result as T
77
+ }
78
+
79
+ // ── which tags this workspace uses ──────────────────────────────────────────
80
+ const seed = await ask<{ tasks?: Array<{ tags?: string[] }> }>('tasks:everywhere', { workspace: slug, limit: 5000 })
81
+ const tags = new Set(extraTags)
82
+ for (const t of seed?.tasks ?? []) {
83
+ // BARE words only. A namespaced tag is a routing lane, not a tag, and `tasks:list`
84
+ // filters on the bare word.
85
+ for (const g of t.tags ?? []) if (!g.includes(':') && !g.startsWith('@')) tags.add(g)
86
+ }
87
+ console.log(`${slug}: ${seed?.tasks?.length ?? 0} seed row(s) → ${tags.size} bare tag(s)`)
88
+
89
+ // ── one page per tag, unioned ───────────────────────────────────────────────
90
+ const titles: Record<string, string> = {}
91
+ let refused = 0
92
+ for (const tag of [...tags].sort()) {
93
+ const page = await ask<{ tasks?: Array<{ tid?: string; name?: string }> }>('tasks:list', { workspace: slug, tag })
94
+ if (!page) { refused++; continue }
95
+ for (const t of page.tasks ?? []) if (t.tid && t.name) titles[t.tid] = t.name
96
+ }
97
+
98
+ mkdirSync(dirname(out), { recursive: true })
99
+ writeFileSync(out, `${JSON.stringify(titles, null, 1)}\n`, 'utf8')
100
+ console.log(`${Object.keys(titles).length} title(s) → ${out}${refused ? ` (${refused} tag page(s) refused — the map is short by however many rows they held)` : ''}`)
101
+ console.log('This is a FLOOR: a tag absent from the seed page contributes nothing, and a missing title is left unnamed by the backfill rather than guessed.')
@@ -185,6 +185,24 @@ if [ "${TEST_CACHE_KEY_ONLY:-0}" = "1" ]; then echo "$KEY"; exit 0; fi
185
185
  STAMP="$CACHE_DIR/$folder-${KEY}.pass"
186
186
  STAMP="${STAMP//\//_}"; STAMP="$CACHE_DIR/$(basename "$STAMP")"
187
187
 
188
+ # TEST_CACHE_PROBE=1 answers "would this HIT?" and runs nothing: exit 0 on a
189
+ # hit, 1 on a miss. It sits AFTER the STAMP lines above on purpose -- the
190
+ # probe and the real check must resolve the same path or the probe is a
191
+ # second, drifting definition of a hit. KEY_ONLY prints the key; PROBE
192
+ # answers the question the caller actually has.
193
+ #
194
+ # WHY IT EXISTS. verify-fast.sh took a GOVERNOR SLOT and then asked whether
195
+ # there was anything to run. Measured 2026-09-09 on a saturated box: 855s
196
+ # and 1324s queued for slots whose work was a memo hit costing ~0. A cache
197
+ # hit must never wait behind a real gate -- it needs no memory, no forks and
198
+ # no slot. Probe first, take the slot only on a MISS.
199
+ if [ "${TEST_CACHE_PROBE:-0}" = "1" ]; then
200
+ if [ "${TEST_CACHE_DISABLE:-0}" != "1" ] && [ -f "$STAMP" ]; then
201
+ echo "[test-cached] probe: HIT ${KEY:0:12}"; exit 0
202
+ fi
203
+ echo "[test-cached] probe: MISS ${KEY:0:12}"; exit 1
204
+ fi
205
+
188
206
  mkdir -p "$CACHE_DIR" 2>/dev/null
189
207
  find "$CACHE_DIR" -name '*.pass' -mtime "+${MAX_AGE_DAYS}" -delete 2>/dev/null
190
208
 
@@ -197,17 +215,36 @@ fi
197
215
  # __probe__ is the self-test's fake folder: no vitest, deterministic outcome.
198
216
  if [ "$folder" = "__probe__" ]; then
199
217
  case " ${ARGS[*]} " in *--probe-fail*) st=1 ;; *) st=0 ;; esac
200
- elif [ "${TEST_CACHE_PTY:-0}" = "1" ]; then
201
- # The deploy gate's vitest HANGS when stdout is not a TTY -- a fork is spawned
202
- # and never given work (see .claude/commands/deploy.md "The vitest gate hangs").
203
- # `script -qeF /dev/null` allocates a pty and flushes. The -e is load-bearing:
204
- # it propagates the CHILD's exit status. Without it `script` exits 0 over a red
205
- # suite and this cache would record a PASS for a failing run.
206
- ( cd "$ROOT/$folder" && script -qeF /dev/null bunx vitest run "${ARGS[@]}" )
207
- st=$?
208
218
  else
209
- ( cd "$ROOT/$folder" && bunx vitest run "${ARGS[@]}" )
210
- st=$?
219
+ # THE SLOT. This script takes no lock, so unlike tsc-cached.sh there is no
220
+ # ordering hazard -- adding a slot here is purely additive.
221
+ #
222
+ # verify-fast.sh already wraps ITS calls to this script in gate-run, and
223
+ # gate-run exports GOVERN_IN_GATE=1, so _gate below collapses to a plain `env`
224
+ # on that path: the outer slot is INHERITED, never doubled (a second slot held
225
+ # by one logical gate is what deadlocks the queue at cap 2).
226
+ #
227
+ # What this closes is the other caller: `bun run test` -> test-lanes.sh -> here
228
+ # reached vitest with no slot at all. Measured 2026-09-07 in one worktree at
229
+ # 256% CPU and ~800MB, with the governor reporting "1 of 2 slots held".
230
+ _gate=( env )
231
+ if [ "${TEST_CACHE_GATE:-1}" = "1" ] && [ "${GOVERN_IN_GATE:-0}" != "1" ] \
232
+ && [ -f "$ROOT/.claude/scripts/gate-run.sh" ]; then
233
+ _gate=( env "GOVERN_GATE_TIMEOUT=${TEST_CACHE_TIMEOUT:-1800}"
234
+ bash "$ROOT/.claude/scripts/gate-run.sh" "test-cache:$folder" -- )
235
+ fi
236
+ if [ "${TEST_CACHE_PTY:-0}" = "1" ]; then
237
+ # The deploy gate's vitest HANGS when stdout is not a TTY -- a fork is spawned
238
+ # and never given work (see .claude/commands/deploy.md "The vitest gate hangs").
239
+ # `script -qeF /dev/null` allocates a pty and flushes. The -e is load-bearing:
240
+ # it propagates the CHILD's exit status. Without it `script` exits 0 over a red
241
+ # suite and this cache would record a PASS for a failing run.
242
+ ( cd "$ROOT/$folder" && "${_gate[@]}" script -qeF /dev/null bunx vitest run "${ARGS[@]}" )
243
+ st=$?
244
+ else
245
+ ( cd "$ROOT/$folder" && "${_gate[@]}" bunx vitest run "${ARGS[@]}" )
246
+ st=$?
247
+ fi
211
248
  fi
212
249
 
213
250
  # ONLY a pass is recorded. A red gate must re-run until it is green.
@@ -145,6 +145,20 @@ echo "───── pool lane (rc=$RC_A) ─────"; cat "$LOG_A"
145
145
  echo "───── typedb lane (rc=$RC_B) ─────"; cat "$LOG_B"
146
146
  rm -f "$LOG_A" "$LOG_B"
147
147
 
148
+ # TYPEDB_LANE_NONBLOCKING: the typedb lane still runs (signal is preserved) but
149
+ # its exit code is never a gate. Set in deploy.sh because the deploy must not
150
+ # block on the shared TypeDB Cloud cluster — the request path goes through
151
+ # Cloudflare (BrainDO / KV), TypeDB is writes + sync only (§ The brain and the edge).
152
+ if [[ "${TYPEDB_LANE_NONBLOCKING:-0}" == "1" ]]; then
153
+ if [ "$RC_B" -ne 0 ]; then
154
+ echo "test-lanes: typedb lane FAILED (rc=$RC_B) — non-blocking, gate is pool lane only"
155
+ fi
156
+ if [ "$RC_A" -eq 0 ]; then
157
+ echo "test-lanes: pool lane green — PASS (typedb lane non-blocking)"; exit 0
158
+ fi
159
+ echo "test-lanes: FAILED (pool=$RC_A typedb=$RC_B, typedb non-blocking)"; exit 1
160
+ fi
161
+
148
162
  # Both must pass. A lane that could not run is a failure, never a pass.
149
163
  if [ "$RC_A" -eq 0 ] && [ "$RC_B" -eq 0 ]; then
150
164
  echo "test-lanes: BOTH lanes green"; exit 0
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env bun
2
+ // thread-name-backfill.ts — name the threads that were written before there was a
3
+ // name to write.
4
+ //
5
+ // manifest: monorepo-only (binds the `one-owners` D1 by name through wrangler and
6
+ // imports the namer out of one.ie/web/src)
7
+ //
8
+ // MEASURED 2026-09-13, production D1 `one-owners`: 1042 of 1053 threads in workspace
9
+ // `one` and 125 of 132 in `ehc` carry an empty `name`, so the inbox renders their KEY
10
+ // (`space:ehc`, `ehc-chairman`) or a hex id. The write door now names every thread it
11
+ // mints (lib/threads.ts) — this is the other half, for the rows already written.
12
+ //
13
+ // IT APPLIES EXACTLY THE SAME FUNCTION THE LIVE PATH DOES — `deriveThreadName` from
14
+ // one.ie/web/src/lib/in/thread-name.ts. A backfill with its own naming rules is a
15
+ // second vocabulary for the same keys, and the day they disagree nobody can tell
16
+ // which row was named by which.
17
+ //
18
+ // FOUR SAFETIES, and each is a thing that can go wrong once and not be undone:
19
+ // 1. DRY RUN IS THE DEFAULT. `--apply` is required to write, and it is not a flag
20
+ // any read-only session should pass.
21
+ // 2. It only ever writes `name`. A thread KEY is identity — `listThreads` filters
22
+ // `agent_id = ?` exactly, and rewriting one orphans the conversation.
23
+ // 3. `WHERE name IS NULL OR name = ''` appears in the SELECT **and again in the
24
+ // UPDATE**, so a name a human typed between the two is never overwritten, and
25
+ // running it twice changes nothing the second time.
26
+ // 4. Batched by `--limit`, one workspace at a time — and the APPLY is a generated
27
+ // SQL FILE (`--out`, chunked by `--chunk`), run with `wrangler … --file`, not
28
+ // one `--command` spawn per row. 411 sequential remote calls is ~15-30 minutes
29
+ // during which a concurrent writer can change a row read in the SELECT above;
30
+ // a handful of file round trips is not. The SQL is generated on a DRY RUN too,
31
+ // so the operator reads statements rather than a description of them, and the
32
+ // ids are written beside it because a rollback cannot be re-derived: run the
33
+ // namer again later and it names rows this pass never touched.
34
+ //
35
+ // A task thread's name is its TITLE, which no key can supply. Titles come from the
36
+ // D1 `entity_meta` overlay (written by tasks:create/subtask/rename) and, for tasks
37
+ // created before that landed, from an offline `--titles <file.json>` map of
38
+ // `{ "<tid>": "<title>" }`. Rows with neither are COUNTED AND LEFT, never guessed at.
39
+ //
40
+ // USAGE
41
+ // bun .claude/scripts/thread-name-backfill.ts --slug one # dry run
42
+ // bun .claude/scripts/thread-name-backfill.ts --slug one --limit 500
43
+ // bun .claude/scripts/thread-name-backfill.ts --slug one --titles t.json
44
+ // bun .claude/scripts/thread-name-backfill.ts --slug one --out .backfill --chunk 400
45
+ // bun .claude/scripts/thread-name-backfill.ts --slug one --apply # runs the SQL
46
+ // …add --local to work against the miniflare copy instead of production.
47
+
48
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
49
+ import { resolve } from 'node:path'
50
+ import { spawnSync } from 'node:child_process'
51
+ import { deriveThreadName, taskIdOf } from '../../one.ie/web/src/lib/in/thread-name.ts'
52
+
53
+ const DB = 'one-owners'
54
+ const WEB = new URL('../../one.ie/web', import.meta.url).pathname
55
+
56
+ const argv = process.argv.slice(2)
57
+ const flag = (name: string, fallback = ''): string => {
58
+ const i = argv.indexOf(`--${name}`)
59
+ return i >= 0 && argv[i + 1] && !argv[i + 1]!.startsWith('--') ? argv[i + 1]! : fallback
60
+ }
61
+ const has = (name: string) => argv.includes(`--${name}`)
62
+
63
+ const slug = flag('slug')
64
+ const limit = Number(flag('limit', '200'))
65
+ const chunk = Number(flag('chunk', '400'))
66
+ const outDir = resolve(new URL('../..', import.meta.url).pathname, flag('out', '.backfill'))
67
+ const apply = has('apply')
68
+ const remote = !has('local')
69
+ const titlesFile = flag('titles')
70
+
71
+ if (!slug || !Number.isFinite(limit) || limit <= 0 || !Number.isFinite(chunk) || chunk <= 0) {
72
+ console.error('usage: bun .claude/scripts/thread-name-backfill.ts --slug <workspace> [--limit N] [--chunk N] [--out DIR] [--titles f.json] [--local] [--apply]')
73
+ process.exit(2)
74
+ }
75
+
76
+ const sq = (s: string) => `'${s.replace(/'/g, "''")}'`
77
+
78
+ function wrangler(mode: '--command' | '--file', value: string): Array<Record<string, unknown>> {
79
+ const args = ['wrangler', 'd1', 'execute', DB, remote ? '--remote' : '--local', '--json', mode, value]
80
+ const r = spawnSync('bunx', args, { cwd: WEB, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 })
81
+ if (r.status !== 0) {
82
+ // A failed READ must never read as "no rows to name" — that is the shape that
83
+ // turns an outage into a clean-looking pass. Stop instead.
84
+ console.error(r.stderr || r.stdout)
85
+ throw new Error(`d1 command failed (exit ${r.status})`)
86
+ }
87
+ const start = r.stdout.indexOf('[')
88
+ if (start < 0) return []
89
+ const parsed = JSON.parse(r.stdout.slice(start)) as Array<{ results?: Array<Record<string, unknown>> }>
90
+ return parsed[0]?.results ?? []
91
+ }
92
+
93
+ // ── titles ──────────────────────────────────────────────────────────────────
94
+ const titles = new Map<string, string>()
95
+ if (titlesFile) {
96
+ const raw = JSON.parse(readFileSync(titlesFile, 'utf8')) as Record<string, string>
97
+ for (const [tid, t] of Object.entries(raw)) if (typeof t === 'string' && t.trim()) titles.set(tid, t.trim())
98
+ }
99
+ // The overlay the live writers mirror to — the same source `tasks:comment` reads.
100
+ for (const row of wrangler('--command', `SELECT entity_id, meta FROM entity_meta WHERE workspace_slug = ${sq(slug)}`)) {
101
+ const id = String(row['entity_id'] ?? '')
102
+ if (!id || titles.has(id)) continue
103
+ try {
104
+ const m = JSON.parse(String(row['meta'] ?? '{}')) as Record<string, unknown>
105
+ if (typeof m['title'] === 'string' && m['title'].trim()) titles.set(id, m['title'].trim())
106
+ } catch { /* a malformed overlay row names nothing */ }
107
+ }
108
+
109
+ // ── the nameless ────────────────────────────────────────────────────────────
110
+ const rows = wrangler('--command',
111
+ `SELECT id, slug, agent_id, name FROM threads WHERE slug = ${sq(slug)} AND (name IS NULL OR name = '') ORDER BY last_msg_at DESC LIMIT ${limit}`,
112
+ )
113
+
114
+ type Plan = { id: string; key: string | null; name: string; via: string }
115
+ const planned: Plan[] = []
116
+ const unnamed: Array<{ id: string; key: string | null; why: string }> = []
117
+
118
+ for (const r of rows) {
119
+ const id = String(r['id'] ?? '')
120
+ const key = r['agent_id'] === null || r['agent_id'] === undefined ? null : String(r['agent_id'])
121
+ if (!id) continue
122
+ if (key && key.startsWith('task:')) {
123
+ // Both stored shapes resolve to one tid, so a `task:task:` row and a `task:` row
124
+ // find the same title.
125
+ const tid = taskIdOf(key)
126
+ const title = titles.get(tid) ?? titles.get(`task:${tid}`) ?? ''
127
+ if (title) planned.push({ id, key, name: title.slice(0, 200), via: 'task title' })
128
+ else unnamed.push({ id, key, why: 'no title for this tid in the overlay or --titles' })
129
+ continue
130
+ }
131
+ const derived = deriveThreadName(key, slug)
132
+ if (derived) planned.push({ id, key, name: derived, via: 'key' })
133
+ else unnamed.push({ id, key, why: key ? 'key shape carries no name' : 'null key — the client derives from the body' })
134
+ }
135
+
136
+ // ── report ──────────────────────────────────────────────────────────────────
137
+ console.log(`${apply ? 'APPLY' : 'DRY RUN'} · workspace ${slug} · ${remote ? 'remote' : 'local'} · limit ${limit}`)
138
+ console.log(`nameless rows read: ${rows.length} nameable: ${planned.length} left unnamed: ${unnamed.length}`)
139
+ console.log(`titles available: ${titles.size}`)
140
+ console.log('\n── would name (sample) ──')
141
+ for (const p of planned.slice(0, 15)) console.log(` ${p.id} ${String(p.key ?? '<null>').padEnd(34)} '' → '${p.name}' [${p.via}]`)
142
+ if (planned.length > 15) console.log(` … ${planned.length - 15} more`)
143
+ if (unnamed.length) {
144
+ console.log('\n── left unnamed (sample) ──')
145
+ for (const u of unnamed.slice(0, 10)) console.log(` ${u.id} ${String(u.key ?? '<null>').padEnd(34)} ${u.why}`)
146
+ if (unnamed.length > 10) console.log(` … ${unnamed.length - 10} more`)
147
+ }
148
+
149
+ // ── generate the SQL ────────────────────────────────────────────────────────
150
+ // ALWAYS, dry run included — the apply runs a FILE, not 411 `--command` spawns,
151
+ // and the operator reads the statements before any of them run. The blank
152
+ // predicate is repeated in every statement, not only in the SELECT above:
153
+ // between the read and the write a human may have named the row, and a backfill
154
+ // that clobbers a human is worse than one that leaves an id on screen.
155
+ const now = Math.floor(Date.now() / 1000)
156
+ const statementFor = (p: Plan) =>
157
+ `UPDATE threads SET name = ${sq(p.name)}, updated_at = ${now}\n` +
158
+ ` WHERE id = ${sq(p.id)} AND (name IS NULL OR name = '');`
159
+
160
+ mkdirSync(outDir, { recursive: true })
161
+ const files: string[] = []
162
+ for (let i = 0; i < planned.length; i += chunk) {
163
+ const slice = planned.slice(i, i + chunk)
164
+ const path = `${outDir}/thread-name-${slug}-${String(i / chunk + 1).padStart(3, '0')}.sql`
165
+ writeFileSync(
166
+ path,
167
+ `-- thread-name-backfill · ${slug} · rows ${i + 1}-${i + slice.length} of ${planned.length}\n` +
168
+ `-- Re-runnable: every statement keeps the blank predicate, so a name typed since is never clobbered.\n` +
169
+ `-- Rollback: the ids this pass would write are listed beside this file.\n\n` +
170
+ slice.map(statementFor).join('\n'),
171
+ 'utf8',
172
+ )
173
+ files.push(path)
174
+ }
175
+ // THE ROLLBACK CANNOT BE DERIVED, so it is written down. Re-running the namer
176
+ // later would name rows this pass never touched, and could not tell a name this
177
+ // wrote from one a human typed afterwards. The id list is the only honest undo.
178
+ //
179
+ // AND A RUN THAT PLANS NOTHING WRITES NOTHING HERE. Measured 2026-09-13, minutes
180
+ // after the real apply: re-running the same command to PROVE idempotence read 8
181
+ // nameless rows, planned 0, and truncated the list of the 107 rows the first run had
182
+ // just named — the no-op proof destroyed the undo for the work it was proving.
183
+ // Recovering it meant parsing the ids back out of the generated `.sql`. An empty
184
+ // plan now leaves the file exactly as it is.
185
+ const idsPath = `${outDir}/thread-name-${slug}.ids.txt`
186
+ if (planned.length) writeFileSync(idsPath, `${planned.map((p) => `${p.id}\t${p.via}\t${p.name}`).join('\n')}\n`, 'utf8')
187
+
188
+ console.log(`\n── generated SQL (${planned.length} statements, ${files.length} file(s) of ≤${chunk}) ──`)
189
+ for (const f of files) console.log(` ${f}`)
190
+ console.log(
191
+ planned.length
192
+ ? ` ${idsPath} (id, via, name — the rollback's row list)`
193
+ : ` nothing to do — ${idsPath} and any SQL already in ${outDir} were LEFT AS THEY WERE (an earlier pass's rollback list is not this run's to overwrite)`,
194
+ )
195
+ if (planned.length) {
196
+ console.log('\n first statement:')
197
+ for (const line of statementFor(planned[0]).split('\n')) console.log(` ${line.slice(0, 200)}`)
198
+ }
199
+
200
+ if (!apply) {
201
+ console.log('\nDRY RUN — nothing was written to the database. Read the SQL above, then re-run with --apply.')
202
+ console.log(`Rollback after an apply: UPDATE threads SET name = '' WHERE id IN (<the ids in ${idsPath}>);`)
203
+ process.exit(0)
204
+ }
205
+
206
+ // ── write ───────────────────────────────────────────────────────────────────
207
+ for (const f of files) {
208
+ wrangler('--file', f)
209
+ console.log(` ran ${f}`)
210
+ }
211
+ const after = wrangler('--command',
212
+ `SELECT COUNT(*) AS n FROM threads WHERE slug = ${sq(slug)} AND (name IS NULL OR name = '')`,
213
+ )
214
+ console.log(`\nran ${files.length} file(s), ${planned.length} guarded statement(s). still nameless in ${slug}: ${after[0]?.['n'] ?? '?'}`)
215
+ console.log(`Rollback list: ${idsPath}`)
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env bash
2
+ # triage-shape-check.sh — prove the `shape` half of .claude/workflows/triage.js
3
+ # writes a sane, non-destructive plan. Same idiom as do-tier.sh --self-test and
4
+ # do-triage.sh --self-test: one command, exits non-zero on failure, and carries
5
+ # a RED PROOF so a checker that stays green against gutted code is caught.
6
+ #
7
+ # manifest: monorepo-only
8
+ #
9
+ # WHY THIS EXISTS. The shape phase emits a LIST of board writes, and the list is
10
+ # where the damage lives, not the words. Two of these invariants were broken in
11
+ # the first version and neither would have failed a syntax check:
12
+ # · TWO tasks:notes writes. tasks:notes SETS the body, it does not append, so
13
+ # the second silently destroyed the first — and the survivor said
14
+ # "accept: see above" pointing at the line it had just deleted. Exactly the
15
+ # defect the board already carries one layer down (an absent notes key is a
16
+ # silent DELETE returning ok:true).
17
+ # · tasks:assign, which is not in the receiver registry and would dissolve as
18
+ # unknown_receiver.
19
+ # Both are invisible to the eye and obvious to this file.
20
+ #
21
+ # Usage: bash .claude/scripts/triage-shape-check.sh [--red]
22
+ # --red gut the guard and assert this checker goes RED
23
+
24
+ set -uo pipefail
25
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
26
+ WF="$ROOT/.claude/workflows/triage.js"
27
+ SDK="$ROOT/packages/sdk/src/receivers.ts"
28
+ RED=0
29
+ [ "${1:-}" = "--red" ] && RED=1
30
+
31
+ [ -f "$WF" ] || { echo "FAIL: no $WF"; exit 2; }
32
+
33
+ run_cases() { # <workflow file>
34
+ node - "$1" "$SDK" <<'NODE'
35
+ // eval() IS DELIBERATE AND BOUNDED HERE, and JSON.parse cannot replace it: the
36
+ // tables it lifts contain REGEX LITERALS (ROUTES) and unquoted keys (SPLIT),
37
+ // neither of which is JSON. The input is not untrusted — it is a tracked file
38
+ // in this repo, read by path, and the whole point of the checker is to test the
39
+ // SHIPPING definitions rather than a copy that can drift. A copy is the failure
40
+ // mode this file exists to prevent: two definitions of one rule is how the
41
+ // routing table and its fixtures disagree silently. Never point this at a path
42
+ // a caller supplies.
43
+ const fs = require('fs')
44
+ const src = fs.readFileSync(process.argv[2], 'utf8')
45
+ const sdk = fs.existsSync(process.argv[3]) ? fs.readFileSync(process.argv[3], 'utf8') : ''
46
+ const grab = (re) => { const m = src.match(re); if (!m) { console.log('FAIL: could not find ' + re); process.exit(3) } return m[0] }
47
+ const ROUTES = eval('(' + grab(/const ROUTES = \[[\s\S]*?\n\]/).replace('const ROUTES = ', '') + ')')
48
+ const SPLIT = eval('(' + grab(/const SPLIT = \{[\s\S]*?\n\}/).replace('const SPLIT = ', '') + ')')
49
+ eval(grab(/function routeFor[\s\S]*?\n\}/))
50
+ eval(grab(/function plannedWrites[\s\S]*?\n return writes\n\}/))
51
+
52
+ let bad = 0
53
+ const fail = (m) => { console.log(' FAIL ' + m); bad++ }
54
+ const ok = (m) => console.log(' ok ' + m)
55
+
56
+ // ── 1. routing: what a file IS beats what it is ABOUT ───────────────────────
57
+ // The first draft put the money nouns first and sent a wallet COMPONENT and a
58
+ // wallet TEST to security-auditor. These fixtures are that bug, frozen.
59
+ for (const [p, want] of [
60
+ ['one.ie/web/src/components/wallet/AllWallets.tsx', 'implementer'],
61
+ ['one.ie/web/tests/unit/wallet/x.test.ts', 'test-engineer'],
62
+ ['one.ie/web/src/lib/auth/passkey-webauthn.ts', 'security-auditor'],
63
+ ['pay/backend/src/signing-key.ts', 'security-auditor'],
64
+ ['schema/one.tql', 'architect'],
65
+ ['one.ie/web/src/lib/resolvers/tasks.ts', 'architect'],
66
+ ['text/story.md', 'tech-writer'],
67
+ ['one.ie/web/wrangler.toml', 'release-manager'],
68
+ ['one.ie/web/src/pages/about.astro', 'implementer'],
69
+ ['somewhere/random.go', 'cto'],
70
+ ]) {
71
+ const got = routeFor([p])
72
+ got === want ? ok(`route ${want.padEnd(17)}${p}`) : fail(`route ${p} → ${got}, want ${want}`)
73
+ }
74
+
75
+ // ── 2. the write plan ───────────────────────────────────────────────────────
76
+ const C = {
77
+ FIX: { title: 'send button does nothing', triage: { verdict: 'sizeable', paths: ['one.ie/web/src/components/wallet/AllWallets.tsx'] }, size: { tier: 'FIX' },
78
+ shape: { want: 'Stop the Send button pretending it moved money', why: 'It emits telemetry and signs nothing.', subtasks: ['a', 'b', 'c'], chain: true, assignee: 'implementer' } },
79
+ UNSIZED: { title: 'make chat nicer', triage: { verdict: 'idea', paths: [] }, size: { tier: 'UNSIZED' }, shape: null },
80
+ PATCH: { title: 'typo', triage: { verdict: 'sizeable', paths: ['text/readme.md'] }, size: { tier: 'PATCH' },
81
+ shape: { want: 'Fix the misspelled word', why: '', subtasks: [], chain: false, assignee: 'tech-writer' } },
82
+ }
83
+ const plans = Object.fromEntries(Object.entries(C).map(([k, t]) => [k, plannedWrites(t)]))
84
+
85
+ for (const [k, w] of Object.entries(plans)) {
86
+ const ops = w.map((x) => x.op)
87
+ // EXACTLY ONE notes write. Two means the second clobbers the first.
88
+ const notes = w.filter((x) => x.op === 'tasks:notes')
89
+ notes.length <= 1 ? ok(`${k}: one notes write`) : fail(`${k}: ${notes.length} notes writes — the later SETS over the earlier`)
90
+ if (notes[0] && /accept:\s*see above/i.test(notes[0].text)) fail(`${k}: notes back-reference a line this plan deletes`)
91
+ // The capture lands FIRST, before anything can overwrite the title.
92
+ ops[0] === 'tasks:comment' ? ok(`${k}: capture first`) : fail(`${k}: first write is ${ops[0]}, not tasks:comment`)
93
+ // Every emitted receiver must exist.
94
+ if (sdk) for (const op of new Set(ops)) {
95
+ if (!sdk.includes('"' + op + '"')) fail(`${k}: ${op} is not in the receiver registry — would dissolve as unknown_receiver`)
96
+ }
97
+ }
98
+ // UNSIZED is shaped by nobody.
99
+ {
100
+ const ops = plans.UNSIZED.map((x) => x.op)
101
+ ops.some((o) => ['tasks:rename', 'tasks:reassign', 'tasks:subtask'].includes(o))
102
+ ? fail('UNSIZED row was renamed/assigned/split — absence of recon must not read as simplicity')
103
+ : ok('UNSIZED: no title, no owner, no children')
104
+ }
105
+ // PATCH is one piece.
106
+ plans.PATCH.some((x) => x.op === 'tasks:subtask') ? fail('PATCH got children') : ok('PATCH: no children')
107
+ // The chain leaves exactly one sibling ready.
108
+ {
109
+ const subs = plans.FIX.filter((x) => x.op === 'tasks:subtask')
110
+ subs.length === 3 ? ok('FIX: 3 children') : fail(`FIX: ${subs.length} children, want 3`)
111
+ subs[0] && subs[0].blockedBy ? fail('first sibling is blocked — nothing would be ready') : ok('FIX: first sibling ready')
112
+ subs.slice(1).every((s) => s.blockedBy) ? ok('FIX: later siblings chained') : fail('FIX: a later sibling is unchained — siblings race')
113
+ }
114
+ process.exit(bad ? 1 : 0)
115
+ NODE
116
+ }
117
+
118
+ if [ "$RED" -eq 0 ]; then
119
+ echo "triage-shape-check — invariants"
120
+ run_cases "$WF"; rc=$?
121
+ [ $rc -eq 0 ] && echo "triage-shape: self-test green" || echo "triage-shape: self-test FAILED"
122
+ exit $rc
123
+ fi
124
+
125
+ # ── RED PROOF ────────────────────────────────────────────────────────────────
126
+ # A checker that stays green against gutted code proves nothing. Re-introduce
127
+ # the two defects that were actually shipped and assert this file catches both.
128
+ tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
129
+ gut="$tmp/triage.js"
130
+
131
+ echo "RED PROOF 1 — second notes write restored (the clobber)"
132
+ sed 's|if (body) writes.splice(1, 0, { op: .tasks:notes., text: body })|if (body) writes.splice(1, 0, { op: "tasks:notes", text: body }); if (task.shape \&\& task.shape.why) writes.push({ op: "tasks:notes", text: task.shape.why + "\\n\\naccept: see above" })|' "$WF" > "$gut"
133
+ if run_cases "$gut" >/dev/null 2>&1; then echo " FAIL: checker stayed GREEN against a double notes write"; exit 1
134
+ else echo " ok checker went RED"; fi
135
+
136
+ echo "RED PROOF 2 — tasks:assign restored (the absent receiver)"
137
+ sed "s|op: 'tasks:reassign'|op: 'tasks:assign'|" "$WF" > "$gut"
138
+ if run_cases "$gut" >/dev/null 2>&1; then echo " FAIL: checker stayed GREEN against an unwired receiver"; exit 1
139
+ else echo " ok checker went RED"; fi
140
+
141
+ echo "RED PROOF 3 — money rule moved back to the front (the mis-route)"
142
+ node -e '
143
+ const fs=require("fs");let s=fs.readFileSync(process.argv[1],"utf8");
144
+ s=s.replace(/const ROUTES = \[[\s\S]*?\n\]/,`const ROUTES = [\n [/(^|\\\\/)(auth|passkey|vault|signer|wallet|escrow|payment|credits)/i, "security-auditor"],\n [/\\\\.(astro|tsx|jsx|css)$|components?\\\\/|pages?\\\\//i, "implementer"],\n]`);
145
+ fs.writeFileSync(process.argv[2],s)' "$WF" "$gut"
146
+ if run_cases "$gut" >/dev/null 2>&1; then echo " FAIL: checker stayed GREEN against a greedy money rule"; exit 1
147
+ else echo " ok checker went RED"; fi
148
+
149
+ echo "triage-shape: red proofs green (all three defects caught)"