@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,257 @@
1
+ #!/usr/bin/env bash
2
+ # factory-close-check.sh — did the Close actually land on the board?
3
+ #
4
+ # manifest: needs-env
5
+ # Reads GATEWAY_API_KEY through ONE_ENV_FILE (default one.ie/web/.env, plus the
6
+ # monorepo fallbacks). A read it cannot make is exit 3 by name; it never
7
+ # answers MISSING on evidence it did not get.
8
+ #
9
+ # WHY IT EXISTS. On the first factory run the Close agent RETURNED
10
+ # {"status":"done"} while the board row stayed `picked`
11
+ # (task:01a07591e5c350a306c93933, workspace one, measured 2026-09-06). The
12
+ # executor's result JSON is a claim about an agent, not evidence about the
13
+ # substrate. This reads the row back and prints what the BOARD says, one line.
14
+ #
15
+ # WHY NOT /api/factory/board. Its `?slug=` is a PLAN selector from a closed list
16
+ # (board.ts PLAN_SLUGS = ['factory-do','factory']; planSlug coerces everything
17
+ # else to the default), so `?slug=one` answers for factory-do — measured. And the
18
+ # executor's rows are on neither plan board. readWork is
19
+ # `tasks:everywhere {workspace, limit:200}` anyway, and it deliberately never
20
+ # reads the CLOSED queue, which is the half a close-check needs. So: the same
21
+ # source, one hop earlier, both queues.
22
+ #
23
+ # USAGE factory-close-check.sh <tid> <workspace>
24
+ # factory-close-check.sh --self-test [workspace]
25
+ # PRINTS CLOSE tid=<tid> lane=<lane> verdict=<verdict> status=<status>
26
+ # EXITS 0 the row was read
27
+ # 2 both queues answered cleanly and completely, and no such row: MISSING
28
+ # 3 UNREADABLE — a refusal, a truncated answer, an unparseable count, or
29
+ # zero rows in both queues (subscriptions.ts answers `{ok:true,tasks:[]}`
30
+ # when env.DB is absent, so an outage and an empty workspace are the same
31
+ # bytes; that must never become "your task is not there")
32
+ # 4 bad arguments / jq missing
33
+ set -uo pipefail
34
+
35
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
36
+ ONE_ENV_FILE="${ONE_ENV_FILE:-${DO_ENV_FILE:-one.ie/web/.env}}"
37
+ URL="${FACTORY_EMIT_URL:-${ONE_API_URL:-https://one.ie}}"
38
+ TIMEOUT="${FACTORY_CLOSE_TIMEOUT:-15}"
39
+ # The server's own ceiling: subscriptions.ts is Math.min(data.limit ?? 50, 200),
40
+ # so 200 is the most any caller can ask for. `returned == 200` is therefore the
41
+ # only honest truncation signal — measured 2026-09-06, workspace one answered
42
+ # 100 open rows to a limit of 200 and 100 again to a limit of 500, which is the
43
+ # whole visible set, not a cap.
44
+ LIMIT=200
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # The row reader. Mirrors toWorkRow (board.ts): the labelled NOTES lines ride
48
+ # first (WORK_LABELS), the `lane:`/`verdict:` TAGS are the fallback for rows a
49
+ # pre-fix close tagged. The label regex and the normalisation are copied from
50
+ # spec-gate.ts LABEL_RE / normaliseLabel — case- and space-insensitive, markdown
51
+ # furniture tolerated, smart quotes folded.
52
+ #
53
+ # ONE deviation, stated out loud: parseLabelledLines appends CONTINUATION lines
54
+ # to the open field; this takes the label line's own value only. For lane and
55
+ # verdict the executor writes single-line values (closePrompt), and a multi-line
56
+ # lane would print here as its first line rather than as a mangled one.
57
+ #
58
+ # `lane=unrecorded` / `verdict=unrun` are deliberate. board.ts says a NULL
59
+ # verdict "renders as unrun, which is not a pass"; an absent lane is not
60
+ # `lane=none`, which is a value the executor actually writes.
61
+ #
62
+ # It lives in a heredoc file, not inline, so the apostrophe inside the label
63
+ # character class never has to survive shell quoting.
64
+ # ---------------------------------------------------------------------------
65
+ _write_jq() {
66
+ cat > "$1" <<'JQ'
67
+ def lbls:
68
+ (.notes // "")
69
+ | split("\n")
70
+ | map(capture("^[ \t>*#-]*(?<k>[A-Za-z][A-Za-z'‘’ ]{2,24}?)[ \t]*:[ \t]*(?<v>.*)$")?)
71
+ | map(select(. != null))
72
+ | map({key: (.k | gsub("[‘’]"; "'") | ascii_downcase | gsub("\\s+"; " ") | gsub("^\\s+|\\s+$"; "")),
73
+ value: (.v | gsub("^\\s+|\\s+$"; ""))})
74
+ | from_entries;
75
+ def tagv($p): ([.tags[]? | select(startswith($p))] | .[0] // "") | if . == "" then "" else .[($p|length):] end;
76
+ def pick($a; $b; $d): if ($a // "") != "" then $a elif ($b // "") != "" then $b else $d end;
77
+ . as $t | (lbls) as $l
78
+ | "CLOSE tid=" + $t.id
79
+ + " lane=" + ((pick($l["lane"]; ($t | tagv("lane:")); "unrecorded")) | gsub("\\s+"; "_"))
80
+ + " verdict=" + ((pick($l["verdict"]; ($t | tagv("verdict:")); "unrun")) | gsub("\\s+"; "_"))
81
+ + " status=" + (($t.status // "") | if . == "" then "unknown" else . end)
82
+ JQ
83
+ }
84
+
85
+ _gateway_key() {
86
+ local f v
87
+ for f in "$ONE_ENV_FILE" "$ROOT/one.ie/web/.env" "$ROOT/one.ie/web/.dev.vars" "$ROOT/.env"; do
88
+ [ -f "$f" ] || continue
89
+ if [ "${FACTORY_EMIT_SKIP_FALLBACKS:-0}" = "1" ] && [ "$f" != "$ONE_ENV_FILE" ]; then continue; fi
90
+ v="$(grep -E '^GATEWAY_API_KEY=' "$f" 2>/dev/null | head -1 | sed 's/^[^=]*=//;s/^"//;s/"$//')"
91
+ [ -n "$v" ] && { printf '%s' "$v"; return 0; }
92
+ done
93
+ printf ''
94
+ }
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # --self-test — the local guards, the parser against the REAL close notes, and
98
+ # one network assertion: an impossible tid must be MISSING (exit 2), which is
99
+ # only reachable after a clean two-queue read.
100
+ #
101
+ # THE DISCIPLINE: an UNREADABLE (exit 3) here is a FAILED self-test, never a
102
+ # skip. A checker that goes green because it could not reach the substrate is
103
+ # the exact defect this script was written against.
104
+ # ---------------------------------------------------------------------------
105
+ self_test() {
106
+ local me="${BASH_SOURCE[0]}" ws="${1:-${FACTORY_CLOSE_SELFTEST_WS:-one}}" fails=0 rc out
107
+ printf '== factory-close-check --self-test (workspace %s)\n' "$ws"
108
+
109
+ _case() { # label, want-rc, argv...
110
+ local label="$1" want="$2"; shift 2
111
+ out="$(bash "$me" "$@" 2>&1)"; rc=$?
112
+ if [ "$rc" = "$want" ]; then printf ' ok %-52s exit %s\n' "$label" "$rc"
113
+ else printf ' FAIL %-52s exit %s, wanted %s\n %s\n' "$label" "$rc" "$want" "$(printf '%s' "$out" | head -1)" >&2; fails=$((fails+1)); fi
114
+ }
115
+
116
+ _case 'no arguments is REFUSED' 4
117
+ _case 'uppercase workspace is REFUSED, not read' 4 task:x One
118
+ _case 'implausible tid is REFUSED' 4 'task:$(rm -rf /)' one
119
+ # Reachable only because SKIP_FALLBACKS exists; without it the monorepo's own
120
+ # .env answers and the no-credential branch is unreachable.
121
+ out="$(ONE_ENV_FILE=/dev/null FACTORY_EMIT_SKIP_FALLBACKS=1 bash "$me" task:x "$ws" 2>&1)"; rc=$?
122
+ if [ "$rc" = 3 ] && printf '%s' "$out" | grep -q 'reason=no-credential'; then
123
+ printf ' ok %-52s exit 3\n' 'no credential is UNREADABLE, not MISSING'
124
+ else
125
+ printf ' FAIL %-52s exit %s\n %s\n' 'no credential must be UNREADABLE' "$rc" "$out" >&2; fails=$((fails+1))
126
+ fi
127
+
128
+ # The parser, against the FIRST REAL CLOSE, byte-identical to the fixture in
129
+ # one.ie/web/src/lib/factory/close-parse.test.ts. Two parsers drift; this is
130
+ # the line that would go red when WORK_LABELS or LABEL_RE moves.
131
+ local jqf row got
132
+ jqf="$(mktemp)"; _write_jq "$jqf"
133
+ row='{"id":"task:01a07591e5c350a306c93933","status":"picked","tags":[],"notes":"Lane: none\nVerdict: red\nPreview: http://localhost:4325\nCommit: https://github.com/one-ie/one/commit/10a821f97\nHeld: red — see stage lines above\nwalk red (lane none, tier ?) · http://localhost:4325/components · commit 10a821f97 · 2026-09-06T07:18:36Z"}'
134
+ got="$(printf '%s' "$row" | jq -r -f "$jqf" 2>&1)"
135
+ rm -f "$jqf"
136
+ if [ "$got" = "CLOSE tid=task:01a07591e5c350a306c93933 lane=none verdict=red status=picked" ]; then
137
+ printf ' ok %-52s\n' 'the real close notes parse as the TS reader does'
138
+ else
139
+ printf ' FAIL %-52s\n got: %s\n' 'parser disagrees with close-parse.test.ts' "$got" >&2; fails=$((fails+1))
140
+ fi
141
+
142
+ # An absent verdict must read `unrun`, never a pass — board.ts's own rule.
143
+ jqf="$(mktemp)"; _write_jq "$jqf"
144
+ got="$(printf '%s' '{"id":"t","status":"picked","tags":[],"notes":"Preview: x"}' | jq -r -f "$jqf" 2>&1)"; rm -f "$jqf"
145
+ if [ "$got" = "CLOSE tid=t lane=unrecorded verdict=unrun status=picked" ]; then
146
+ printf ' ok %-52s\n' 'an absent verdict reads unrun, never a pass'
147
+ else
148
+ printf ' FAIL %-52s\n got: %s\n' 'absent verdict did not read unrun' "$got" >&2; fails=$((fails+1))
149
+ fi
150
+
151
+ # THE NETWORK HALF. exit 2 is only reachable through a clean read of BOTH
152
+ # queues, so this asserts the read as much as the verdict.
153
+ #
154
+ # A LINKED WORKTREE HAS NO one.ie/web/.env — it is gitignored and never checked
155
+ # out. Say that by name and STILL FAIL: an unreachable substrate is a failed
156
+ # self-test, never a skip, exactly as the exit-3 branch above insists.
157
+ if [ -z "$(_gateway_key)" ]; then
158
+ printf ' FAIL %-52s\n' 'no credential — the read half could not be asserted' >&2
159
+ printf ' SKIP-IMPOSSIBLE: no GATEWAY_API_KEY via %s. This is not a broken script;\n' "$ONE_ENV_FILE" >&2
160
+ printf ' run --self-test from the MAIN tree, or export ONE_ENV_FILE=<main>/one.ie/web/.env\n' >&2
161
+ fails=$((fails + 1))
162
+ printf 'factory-close-check --self-test: RED — %s check(s) failed\n' "$fails" >&2; return 1
163
+ fi
164
+ out="$(bash "$me" 'task:0000000000000000000000000' "$ws" 2>&1)"; rc=$?
165
+ if [ "$rc" = 2 ] && printf '%s' "$out" | grep -q 'MISSING'; then
166
+ printf ' ok %-52s exit 2\n' 'an impossible tid is MISSING after a clean read'
167
+ elif [ "$rc" = 3 ]; then
168
+ printf ' FAIL %-52s exit 3 — the substrate was NOT read, so this test proved nothing\n %s\n' \
169
+ 'impossible tid must be MISSING' "$out" >&2; fails=$((fails+1))
170
+ else
171
+ printf ' FAIL %-52s exit %s\n %s\n' 'impossible tid must be MISSING' "$rc" "$out" >&2; fails=$((fails+1))
172
+ fi
173
+
174
+ if [ "$fails" -eq 0 ]; then printf 'factory-close-check --self-test: PASS (7 checks)\n'; return 0; fi
175
+ printf 'factory-close-check --self-test: RED — %s check(s) failed\n' "$fails" >&2; return 1
176
+ }
177
+
178
+ case "${1:-}" in
179
+ --self-test) command -v jq >/dev/null 2>&1 || { echo "factory-close-check: jq not found" >&2; exit 4; }
180
+ self_test "${2:-}"; exit $? ;;
181
+ -h|--help) sed -n '2,32p' "${BASH_SOURCE[0]}"; exit 0 ;;
182
+ esac
183
+
184
+ TID="${1:-}"; WS="${2:-}"
185
+ [ -n "$TID" ] && [ -n "$WS" ] || { echo "usage: factory-close-check.sh <tid> <workspace>" >&2; exit 4; }
186
+ command -v jq >/dev/null 2>&1 || { echo "factory-close-check: jq not found" >&2; exit 4; }
187
+ # Validated, not escaped — the same shape readWork asserts. The workspace is
188
+ # interpolated into the JSON body below, so this is the injection guard too.
189
+ #
190
+ # ENUMERATED, never a RANGE. Measured on this box 2026-09-06 (bash 3.2,
191
+ # en_US.UTF-8): `case One in *[!a-z0-9-]*)` does NOT match — a glob range uses
192
+ # COLLATION order, which interleaves case, so `One` sailed past this very guard
193
+ # and reached the wire. Caught by --self-test's uppercase case. `-` is LAST in
194
+ # both sets on purpose; anywhere else it forms a range of its own.
195
+ WS_OK='abcdefghijklmnopqrstuvwxyz0123456789-'
196
+ TID_OK='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:_-'
197
+ case "$WS" in *[!$WS_OK]*|'') echo "factory-close-check: workspace must match ^[a-z0-9-]+$ (got '$WS')" >&2; exit 4 ;; esac
198
+ case "$TID" in *[!$TID_OK]*|'') echo "factory-close-check: implausible tid '$TID'" >&2; exit 4 ;; esac
199
+
200
+ KEYVAL="$(_gateway_key)"
201
+ [ -n "$KEYVAL" ] || { printf 'CLOSE tid=%s UNREADABLE reason=no-credential file=%s\n' "$TID" "$ONE_ENV_FILE"; exit 3; }
202
+
203
+ cfg="$(mktemp)"; chmod 600 "$cfg"
204
+ JQF="$(mktemp)"; _write_jq "$JQF"
205
+ trap 'rm -f "$cfg" "$JQF"' EXIT INT TERM
206
+ printf 'header = "Authorization: Bearer %s"\n' "$KEYVAL" > "$cfg"
207
+
208
+ read_queue() { # $1 = false | true (the CLOSED flag)
209
+ curl -sS -X POST "$URL/api/ask/tasks:everywhere" \
210
+ -H 'content-type: application/json' --config "$cfg" --max-time "$TIMEOUT" \
211
+ -d "{\"data\":{\"workspace\":\"$WS\",\"limit\":$LIMIT,\"closed\":$1}}" 2>&1
212
+ }
213
+ open_body="$(read_queue false)"; rc_open=$?
214
+ done_body="$(read_queue true)"; rc_done=$?
215
+ rm -f "$cfg"
216
+
217
+ # One shape is a read; everything else is UNREADABLE. Measured: a forbidden call
218
+ # is HTTP 200 {"outcome":"result","result":{"ok":false,"error":"forbidden: ..."}}.
219
+ check_read() { # $1 = body, $2 = rc, $3 = label
220
+ local why
221
+ if [ "$2" -ne 0 ]; then printf 'CLOSE tid=%s UNREADABLE reason=curl-rc-%s queue=%s\n' "$TID" "$2" "$3"; exit 3; fi
222
+ if [ "$(printf '%s' "$1" | jq -r 'if (.outcome == "result" and (.result.ok == true)) then "yes" else "no" end' 2>/dev/null)" != "yes" ]; then
223
+ why="$(printf '%s' "$1" | jq -r '.reason // .result.error // .result.reason // .error // .hint // empty' 2>/dev/null)"
224
+ [ -n "$why" ] || why="unparseable body: $(printf '%s' "$1" | head -c 160)"
225
+ printf 'CLOSE tid=%s UNREADABLE reason=%s queue=%s\n' "$TID" "$why" "$3"; exit 3
226
+ fi
227
+ }
228
+ check_read "$open_body" "$rc_open" open
229
+ check_read "$done_body" "$rc_done" closed
230
+
231
+ n_open="$(printf '%s' "$open_body" | jq '.result.tasks | length' 2>/dev/null)"
232
+ n_done="$(printf '%s' "$done_body" | jq '.result.tasks | length' 2>/dev/null)"
233
+ # A count that is not a number would make BOTH arithmetic tests below evaluate
234
+ # false and drop this straight through to MISSING — the house bug in my own
235
+ # arithmetic. It is UNREADABLE by name instead.
236
+ case "${n_open}|${n_done}" in
237
+ ''|*[!0-9\|]*|'|'*|*'|') printf 'CLOSE tid=%s UNREADABLE reason=unparseable-count open=%s closed=%s\n' "$TID" "${n_open:-none}" "${n_done:-none}"; exit 3 ;;
238
+ esac
239
+
240
+ row="$(jq -s -c --arg tid "$TID" '[.[] | .result.tasks[]?] | map(select(.id == $tid)) | .[0] // empty' <<EOF
241
+ $open_body
242
+ $done_body
243
+ EOF
244
+ )"
245
+
246
+ if [ -z "$row" ]; then
247
+ if [ "$n_open" -ge "$LIMIT" ] || [ "$n_done" -ge "$LIMIT" ]; then
248
+ printf 'CLOSE tid=%s UNREADABLE reason=answer-truncated open=%s closed=%s limit=%s\n' "$TID" "$n_open" "$n_done" "$LIMIT"; exit 3
249
+ fi
250
+ if [ "$n_open" -eq 0 ] && [ "$n_done" -eq 0 ]; then
251
+ printf 'CLOSE tid=%s UNREADABLE reason=zero-rows-in-both-queues (subscriptions.ts answers ok:true with no rows when env.DB is absent — an outage wears this exact shape)\n' "$TID"; exit 3
252
+ fi
253
+ printf 'CLOSE tid=%s MISSING open=%s closed=%s\n' "$TID" "$n_open" "$n_done"; exit 2
254
+ fi
255
+
256
+ printf '%s' "$row" | jq -r -f "$JQF"
257
+ exit 0
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env bash
2
+ # factory-emit.sh — ONE factory stage, onto the run/event stream every viewer reads.
3
+ #
4
+ # manifest: needs-env
5
+ # Reads GATEWAY_API_KEY through ONE_ENV_FILE (default one.ie/web/.env, plus the
6
+ # monorepo fallbacks do-signal.sh walks). No credential is exit 3 by name —
7
+ # never a quiet success, never a skipped stage that reads as a stage that ran.
8
+ #
9
+ # WHY IT EXISTS. .claude/workflows/factory-executor.js writes to the substrate
10
+ # exactly TWICE per job: tasks:claim at Claim and tasks:notes+tasks:status at
11
+ # Close. Ready · Build · Review · Prove emit nothing, so no surface can draw a
12
+ # job DURING. This is the call that closes that gap: one FactoryEventPayload
13
+ # (one.ie/web/src/lib/factory/event.ts) per stage boundary, posted to the
14
+ # factory:event receiver, which maps it onto the same run/event stream RunTrack
15
+ # already reads.
16
+ #
17
+ # USAGE
18
+ # factory-emit.sh --job <tid> --stage <stage> --status <start|ok|fail>
19
+ # [--slug <workspace>] [--model <rung>] [--reason <text>]
20
+ # [--detail k=v]... [--env dev] [--dry-run]
21
+ # factory-emit.sh --self-test # local red proofs, no network
22
+ #
23
+ # stage : ready | claim | build | review | prove | close (FACTORY_SPINE_STEPS)
24
+ # status: start | ok | fail (FactoryStageStatus)
25
+ #
26
+ # EXITS 0 emitted (or --dry-run printed) · 2 bad arguments · 3 no credential
27
+ # 4 the receiver refused — its own words on stderr · 5 jq missing
28
+ #
29
+ # THE REFUSAL IS THE WHOLE POINT — both measured against prod 2026-09-06:
30
+ # unknown receiver → HTTP 200 {"outcome":"dissolved","reason":"unknown_receiver",...}
31
+ # forbidden call → HTTP 200 {"outcome":"result","result":{"ok":false,"error":"forbidden: ..."}}
32
+ # A caller that reads curl's exit code, the HTTP status, or `outcome` alone
33
+ # records a stage that never landed. Success is ONE shape and nothing else:
34
+ # .outcome == "result" AND .result.ok == true
35
+ #
36
+ # ENV FACTORY_EMIT_URL default $ONE_API_URL, else https://one.ie
37
+ # FACTORY_EVENT_RECEIVER default factory:event
38
+ # ONE_ENV_FILE / DO_ENV_FILE the credential file
39
+ # FACTORY_EMIT_SKIP_FALLBACKS=1 ONE_ENV_FILE is the WHOLE ladder. Without
40
+ # it the no-credential state is unreachable from inside the monorepo
41
+ # (the fallbacks resolve to a tree that carries a real .env), so exit 3
42
+ # could not be driven red. Same reason, same shape, as
43
+ # DO_SIGNAL_SKIP_FALLBACKS in do-signal.sh.
44
+ set -uo pipefail
45
+
46
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
47
+ ONE_ENV_FILE="${ONE_ENV_FILE:-${DO_ENV_FILE:-one.ie/web/.env}}"
48
+ URL="${FACTORY_EMIT_URL:-${ONE_API_URL:-https://one.ie}}"
49
+ RECEIVER="${FACTORY_EVENT_RECEIVER:-factory:event}"
50
+ TIMEOUT="${FACTORY_EMIT_TIMEOUT:-8}"
51
+
52
+ die() { printf 'factory-emit: %s\n' "$1" >&2; exit "$2"; }
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # --self-test — every LOCAL refusal, driven for real. No network, no credential.
56
+ # A stage this script would have posted anyway is the frame nobody meant, so
57
+ # each refusal is asserted by its EXIT CODE, never by its message.
58
+ # ---------------------------------------------------------------------------
59
+ self_test() {
60
+ local me="${BASH_SOURCE[0]}" fails=0 rc out
61
+ _case() { # $1 label, $2 expected rc, rest: argv (run with no credential ladder)
62
+ local label="$1" want="$2"; shift 2
63
+ out="$(ONE_ENV_FILE=/dev/null FACTORY_EMIT_SKIP_FALLBACKS=1 bash "$me" "$@" 2>&1)"; rc=$?
64
+ if [ "$rc" = "$want" ]; then
65
+ printf ' ok %-46s exit %s\n' "$label" "$rc"
66
+ else
67
+ printf ' FAIL %-46s exit %s, wanted %s\n %s\n' "$label" "$rc" "$want" "$(printf '%s' "$out" | head -1)" >&2
68
+ fails=$((fails + 1))
69
+ fi
70
+ }
71
+ printf '== factory-emit --self-test\n'
72
+ # THE broken shape this test exists for: factoryEventToRunEvent returns null
73
+ # for a stage outside the six, and a null posted anyway is a lie on the stream.
74
+ _case 'unknown stage is REFUSED before the wire' 2 --job task:selftest --stage warm --status ok
75
+ _case 'unknown status is REFUSED' 2 --job task:selftest --stage build --status green
76
+ _case 'missing --job is REFUSED' 2 --stage build --status ok
77
+ _case 'missing --stage is REFUSED' 2 --job task:selftest --status ok
78
+ _case 'malformed --detail is REFUSED' 2 --job t --stage build --status ok --detail nokv
79
+ _case 'unknown argument is REFUSED' 2 --job t --stage build --status ok --wat
80
+ # No credential must be a NAMED exit 3, never a quiet skip that reads as a
81
+ # stage that ran. Reachable only because SKIP_FALLBACKS exists.
82
+ _case 'no credential is exit 3, not a quiet pass' 3 --job task:selftest --stage build --status start
83
+ # --dry-run precedes the credential read, so it stays 0 with no key at all.
84
+ _case '--dry-run needs no credential' 0 --job task:selftest --stage prove --status ok --dry-run
85
+
86
+ # And the dry-run payload must actually carry the three required fields plus
87
+ # an optional one, or a green emit could be an empty frame.
88
+ local pl
89
+ pl="$(ONE_ENV_FILE=/dev/null FACTORY_EMIT_SKIP_FALLBACKS=1 bash "$me" \
90
+ --job task:selftest --stage review --status fail --slug one --model opus \
91
+ --reason 'refuted' --detail lens=urls --dry-run 2>/dev/null)"
92
+ if printf '%s' "$pl" | jq -e '.data.job == "task:selftest" and .data.stage == "review"
93
+ and .data.status == "fail" and .data.slug == "one" and .data.model == "opus"
94
+ and .data.reason == "refuted" and .data.detail.lens == "urls"' >/dev/null 2>&1; then
95
+ printf ' ok %-46s\n' 'dry-run payload carries every named field'
96
+ else
97
+ printf ' FAIL %-46s\n %s\n' 'dry-run payload is incomplete' "$pl" >&2
98
+ fails=$((fails + 1))
99
+ fi
100
+
101
+ # The optional fields must be ABSENT rather than empty strings — zod strips
102
+ # undeclared keys silently, and an empty `slug` is not the same as no slug.
103
+ pl="$(ONE_ENV_FILE=/dev/null FACTORY_EMIT_SKIP_FALLBACKS=1 bash "$me" \
104
+ --job t --stage ready --status start --dry-run 2>/dev/null)"
105
+ if printf '%s' "$pl" | jq -e '(.data | has("slug") | not) and (.data | has("detail") | not)' >/dev/null 2>&1; then
106
+ printf ' ok %-46s\n' 'unset optional fields are absent, not empty'
107
+ else
108
+ printf ' FAIL %-46s\n %s\n' 'unset optional fields leaked into the payload' "$pl" >&2
109
+ fails=$((fails + 1))
110
+ fi
111
+
112
+ if [ "$fails" -eq 0 ]; then
113
+ printf 'factory-emit --self-test: PASS (10 checks)\n'; return 0
114
+ fi
115
+ printf 'factory-emit --self-test: RED — %s check(s) failed\n' "$fails" >&2; return 1
116
+ }
117
+
118
+ JOB=""; STAGE=""; STATUS=""; SLUG=""; MODEL=""; REASON=""; ENVW=""; DETAILS=""; DRY=0
119
+ while [ $# -gt 0 ]; do
120
+ case "$1" in
121
+ --job) JOB="${2:-}"; shift 2 ;;
122
+ --stage) STAGE="${2:-}"; shift 2 ;;
123
+ --status) STATUS="${2:-}"; shift 2 ;;
124
+ --slug) SLUG="${2:-}"; shift 2 ;;
125
+ --model) MODEL="${2:-}"; shift 2 ;;
126
+ --reason) REASON="${2:-}"; shift 2 ;;
127
+ --env) ENVW="${2:-}"; shift 2 ;;
128
+ --detail) DETAILS="${DETAILS}${DETAILS:+$'\n'}${2:-}"; shift 2 ;;
129
+ --dry-run) DRY=1; shift ;;
130
+ --self-test) command -v jq >/dev/null 2>&1 || die "jq not found" 5; self_test; exit $? ;;
131
+ -h|--help) sed -n '2,42p' "${BASH_SOURCE[0]}"; exit 0 ;;
132
+ *) die "unknown argument: $1" 2 ;;
133
+ esac
134
+ done
135
+
136
+ command -v jq >/dev/null 2>&1 || die "jq not found — this script builds and reads JSON with it, and a hand-rolled parser is how a refusal becomes a pass" 5
137
+ [ -n "$JOB" ] || die "--job <tid> is required — it is the run key (factory:<tid>, event.ts factoryRunKey)" 2
138
+ case "$STAGE" in
139
+ ready|claim|build|review|prove|close) ;;
140
+ *) die "--stage must be one of ready claim build review prove close (got '$STAGE') — factoryEventToRunEvent returns NULL for anything else, and a null that is posted anyway is a frame nobody meant" 2 ;;
141
+ esac
142
+ case "$STATUS" in
143
+ start|ok|fail) ;;
144
+ *) die "--status must be start|ok|fail (got '$STATUS')" 2 ;;
145
+ esac
146
+
147
+ detail_json='{}'
148
+ if [ -n "$DETAILS" ]; then
149
+ while IFS= read -r kv; do
150
+ [ -n "$kv" ] || continue
151
+ case "$kv" in *=*) ;; *) die "--detail wants k=v, got: $kv" 2 ;; esac
152
+ detail_json="$(jq -n --argjson d "$detail_json" --arg k "${kv%%=*}" --arg v "${kv#*=}" '$d + {($k): $v}')" \
153
+ || die "could not build --detail object" 2
154
+ done <<EOF
155
+ $DETAILS
156
+ EOF
157
+ fi
158
+
159
+ payload="$(jq -n \
160
+ --arg job "$JOB" --arg stage "$STAGE" --arg status "$STATUS" \
161
+ --arg slug "$SLUG" --arg model "$MODEL" --arg reason "$REASON" --arg env "$ENVW" \
162
+ --argjson detail "$detail_json" '
163
+ {data: ({job: $job, stage: $stage, status: $status}
164
+ + (if $slug != "" then {slug: $slug} else {} end)
165
+ + (if $model != "" then {model: $model} else {} end)
166
+ + (if $reason != "" then {reason: $reason} else {} end)
167
+ + (if $env != "" then {env: $env} else {} end)
168
+ + (if ($detail | length) > 0 then {detail: $detail} else {} end))}')" \
169
+ || die "could not build the payload" 2
170
+
171
+ if [ "$DRY" = 1 ]; then printf '%s\n' "$payload"; exit 0; fi
172
+
173
+ # The key never reaches argv (ps) and never reaches stdout. --config, chmod 600,
174
+ # removed on every path. Same shape as do-signal.sh.
175
+ _gateway_key() {
176
+ local f v
177
+ for f in "$ONE_ENV_FILE" "$ROOT/one.ie/web/.env" "$ROOT/one.ie/web/.dev.vars" "$ROOT/.env"; do
178
+ [ -f "$f" ] || continue
179
+ if [ "${FACTORY_EMIT_SKIP_FALLBACKS:-0}" = "1" ] && [ "$f" != "$ONE_ENV_FILE" ]; then continue; fi
180
+ v="$(grep -E '^GATEWAY_API_KEY=' "$f" 2>/dev/null | head -1 | sed 's/^[^=]*=//;s/^"//;s/"$//')"
181
+ [ -n "$v" ] && { printf '%s' "$v"; return 0; }
182
+ done
183
+ printf ''
184
+ }
185
+
186
+ KEYVAL="$(_gateway_key)"
187
+ [ -n "$KEYVAL" ] || die "no GATEWAY_API_KEY in $ONE_ENV_FILE (or the monorepo fallbacks) — an unauthenticated emit is refused at the door and would look exactly like a stage that never happened" 3
188
+
189
+ cfg="$(mktemp)"; chmod 600 "$cfg"
190
+ trap 'rm -f "$cfg"' EXIT INT TERM
191
+ printf 'header = "Authorization: Bearer %s"\n' "$KEYVAL" > "$cfg"
192
+
193
+ resp="$(curl -sS -X POST "$URL/api/ask/$RECEIVER" \
194
+ -H 'content-type: application/json' --config "$cfg" \
195
+ --max-time "$TIMEOUT" -d "$payload" -w $'\n%{http_code}' 2>&1)"; rc=$?
196
+ rm -f "$cfg"; trap - EXIT INT TERM
197
+
198
+ code="$(printf '%s' "$resp" | tail -1)"
199
+ body="$(printf '%s' "$resp" | sed '$d')"
200
+ ok="$(printf '%s' "$body" | jq -r 'if (.outcome == "result" and (.result.ok == true)) then "yes" else "no" end' 2>/dev/null)"
201
+
202
+ if [ "$rc" -ne 0 ] || [ "$ok" != "yes" ]; then
203
+ why="$(printf '%s' "$body" | jq -r '.reason // .result.error // .error // .hint // empty' 2>/dev/null)"
204
+ [ -n "$why" ] || why="curl rc=$rc http=${code:-none} body=$(printf '%s' "$body" | head -c 200)"
205
+ printf 'factory-emit: RED job=%s stage=%s status=%s http=%s — %s\n' \
206
+ "$JOB" "$STAGE" "$STATUS" "${code:-none}" "$why" >&2
207
+ exit 4
208
+ fi
209
+
210
+ printf 'factory-emit: ok job=%s stage=%s status=%s receiver=%s\n' "$JOB" "$STAGE" "$STATUS" "$RECEIVER"
211
+ exit 0