@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,326 @@
1
+ #!/usr/bin/env bash
2
+ # factory-turn.sh — drive ONE turn of the factory, locally, from the DOCUMENT.
3
+ #
4
+ # manifest: needs-env
5
+ # Reads GATEWAY_API_KEY through the ONE_ENV_FILE ladder (DO_ENV_FILE alias) and
6
+ # talks to the receivers over HTTPS. With --replay it needs neither and runs
7
+ # from a fixture, which is how tests/unit/factory/factory-turn.test.ts proves it.
8
+ #
9
+ # WHAT IT IS. R8 of text/factory-do.md: `one.ie/ai/workflows/factory-turn.tql`
10
+ # is the turn — the six funs asked, one handler each, and a close that refuses
11
+ # an empty board dressed as a finished one. This script is that document's LOCAL
12
+ # DRIVER and NOT a second definition of it: it parses the .tql, walks the steps
13
+ # in edge order with the executor's own semantics (condition grammar, `each` +
14
+ # `limit` + `concurrency`, pruned branches, a human step that parks), and
15
+ # dispatches every `receiver` over the one door — POST /api/ask/<receiver>, the
16
+ # payload wrapped in "data", `workspace` on every call because a rung elaborated
17
+ # with slug X lives in workspace X (measured 2026-09-04). Change the turn by
18
+ # editing the document; this file has nothing to say about WHICH funs or WHICH
19
+ # handlers.
20
+ #
21
+ # The one thing it adds is a verdict the document's grammar cannot express: a
22
+ # turn that reached `ran` with N ready rows and launched NONE of them is RED here
23
+ # (a condition step cannot count ok:true receipts inside an each result).
24
+ #
25
+ # bash .claude/scripts/factory-turn.sh --plan objective:<slug>:1 [--workspace <slug>]
26
+ # bash .claude/scripts/factory-turn.sh --plan ... --dry-run reads live, writes nothing
27
+ # bash .claude/scripts/factory-turn.sh --plan ... --replay f.json [--log calls.jsonl]
28
+ # bash .claude/scripts/factory-turn.sh --self-test
29
+ #
30
+ # Exit codes — the verdict, and nothing else on stdout's last line:
31
+ # 0 the close was a MARK (ran · settled)
32
+ # 1 the close was a WARN (blind · stalled · halted), or ran-but-launched-nothing
33
+ # 2 PARKED at a human step (deadlocked — a person, now)
34
+ # 3 CANNOT RUN (no key, no plan, unreadable document, a step failed)
35
+ # 1 vs 3 is load-bearing (same as factory-check.sh): red sends someone to the
36
+ # board, cannot-run sends them to the environment. Never widen a 3 into a 1.
37
+ set -uo pipefail
38
+
39
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
40
+ DOC="$ROOT/one.ie/ai/workflows/factory-turn.tql"
41
+
42
+ # Credentials: the same ladder do-signal.sh walks. ONE_ENV_FILE is the whole
43
+ # story in a generated factory clone; the monorepo fallbacks are extras.
44
+ ONE_ENV_FILE="${ONE_ENV_FILE:-${DO_ENV_FILE:-$ROOT/one.ie/web/.env}}"
45
+ if [ -z "${GATEWAY_API_KEY:-}" ]; then
46
+ for _f in "$ONE_ENV_FILE" "$ROOT/.env" "$ROOT/one.ie/.env"; do
47
+ [ -f "$_f" ] || continue
48
+ _v=$(grep -E '^GATEWAY_API_KEY=' "$_f" 2>/dev/null | head -1 | sed 's/^[^=]*=//;s/^"//;s/"$//' || true)
49
+ if [ -n "$_v" ]; then GATEWAY_API_KEY="$_v"; break; fi
50
+ done
51
+ fi
52
+ export GATEWAY_API_KEY="${GATEWAY_API_KEY:-}"
53
+ export ONE_API_URL="${ONE_API_URL:-https://one.ie}"
54
+ export FACTORY_TURN_DOC="$DOC"
55
+
56
+ if [ "${1:-}" = "--self-test" ]; then
57
+ # The replay fixtures the unit test drives, run here so a bare shell can see
58
+ # the four verdicts without vitest. Each line is <expected exit> <fixture>.
59
+ tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
60
+ fails=0
61
+ mk() { printf '%s' "$2" >"$tmp/$1.json"; }
62
+ fun() { printf '{"ok":true,"fn":"%s","rows":%s}' "$1" "$2"; }
63
+ ROWS1='[{"r0":"0x1"}]'; ROWS0='[]'
64
+ mk ran "{\"fn:run\":{\"incomplete\":$(fun incomplete '[{"r0":"0x1"},{"r0":"0x2"}]'),\"ready-tasks\":$(fun ready-tasks "$ROWS1"),\"unrealised\":$(fun unrealised "$ROWS0"),\"uncovered\":$(fun uncovered "$ROWS0"),\"covered-unproven\":$(fun covered-unproven "$ROWS1"),\"deadlocked\":$(fun deadlocked "$ROWS0")},\"tasks:list\":{\"ok\":true,\"tasks\":[{\"tid\":\"task:t1\",\"status\":\"open\"}]},\"tasks:launch\":{\"ok\":true,\"results\":[{\"tid\":\"task:t1\",\"ok\":true}]},\"world:outcome\":{\"ok\":true}}"
65
+ mk stalled "{\"fn:run\":{\"incomplete\":$(fun incomplete '[{"r0":"0x9"}]'),\"ready-tasks\":$(fun ready-tasks "$ROWS0"),\"unrealised\":$(fun unrealised "$ROWS1"),\"uncovered\":$(fun uncovered "$ROWS0"),\"covered-unproven\":$(fun covered-unproven "$ROWS0"),\"deadlocked\":$(fun deadlocked "$ROWS0")},\"tasks:list\":{\"ok\":true,\"tasks\":[]},\"world:outcome\":{\"ok\":true}}"
66
+ mk settled "{\"fn:run\":{\"incomplete\":$(fun incomplete "$ROWS0"),\"ready-tasks\":$(fun ready-tasks "$ROWS0"),\"unrealised\":$(fun unrealised "$ROWS0"),\"uncovered\":$(fun uncovered "$ROWS0"),\"covered-unproven\":$(fun covered-unproven "$ROWS0"),\"deadlocked\":$(fun deadlocked "$ROWS0")},\"tasks:list\":{\"ok\":true,\"tasks\":[]},\"world:outcome\":{\"ok\":true}}"
67
+ mk blind "{\"fn:run\":{\"incomplete\":{\"ok\":false,\"fn\":\"incomplete\",\"rows\":[],\"error\":\"substrate_unavailable (status 503)\"},\"ready-tasks\":$(fun ready-tasks "$ROWS0"),\"unrealised\":$(fun unrealised "$ROWS0"),\"uncovered\":$(fun uncovered "$ROWS0"),\"covered-unproven\":$(fun covered-unproven "$ROWS0"),\"deadlocked\":$(fun deadlocked "$ROWS0")},\"tasks:list\":{\"ok\":true,\"tasks\":[]},\"world:outcome\":{\"ok\":true}}"
68
+ mk parked "{\"fn:run\":{\"incomplete\":$(fun incomplete "$ROWS1"),\"ready-tasks\":$(fun ready-tasks "$ROWS0"),\"unrealised\":$(fun unrealised "$ROWS0"),\"uncovered\":$(fun uncovered "$ROWS0"),\"covered-unproven\":$(fun covered-unproven "$ROWS0"),\"deadlocked\":$(fun deadlocked "$ROWS1")},\"tasks:list\":{\"ok\":true,\"tasks\":[]},\"world:outcome\":{\"ok\":true}}"
69
+ for case in "0 ran" "1 stalled" "0 settled" "1 blind" "2 parked"; do
70
+ want="${case%% *}"; name="${case##* }"
71
+ out=$(bash "$0" --plan objective:self-test:1 --workspace self-test --replay "$tmp/$name.json" 2>&1); rc=$?
72
+ last=$(printf '%s\n' "$out" | tail -1)
73
+ if [ "$rc" -eq "$want" ] && [[ "$last" == *"$name"* ]]; then
74
+ echo "ok $name → exit $rc · $last"
75
+ else
76
+ echo "FAIL $name → wanted exit $want with '$name' on the last line, got exit $rc · $last"; fails=$((fails+1))
77
+ fi
78
+ done
79
+ [ "$fails" -eq 0 ] && { echo "factory-turn --self-test: 5/5"; exit 0; }
80
+ echo "factory-turn --self-test: $fails FAIL"; exit 1
81
+ fi
82
+
83
+ exec python3 - "$@" <<'PY'
84
+ import json, os, re, sys, urllib.request, urllib.error
85
+
86
+ DOC = os.environ["FACTORY_TURN_DOC"]
87
+ API = os.environ.get("ONE_API_URL", "https://one.ie").rstrip("/")
88
+ KEY = os.environ.get("GATEWAY_API_KEY", "")
89
+
90
+ # ── args ─────────────────────────────────────────────────────────────────────
91
+ opts = {"plan": "", "workspace": "", "replay": "", "log": "", "dry": False}
92
+ a = sys.argv[1:]
93
+ i = 0
94
+ while i < len(a):
95
+ k = a[i]
96
+ if k == "--plan": opts["plan"] = a[i + 1]; i += 2
97
+ elif k == "--workspace": opts["workspace"] = a[i + 1]; i += 2
98
+ elif k == "--replay": opts["replay"] = a[i + 1]; i += 2
99
+ elif k == "--log": opts["log"] = a[i + 1]; i += 2
100
+ elif k == "--dry-run": opts["dry"] = True; i += 1
101
+ else: print(f"CANNOT RUN: unknown arg {k}", file=sys.stderr); sys.exit(3)
102
+
103
+ def cannot(msg):
104
+ print(f"factory-turn: CANNOT RUN — {msg}")
105
+ sys.exit(3)
106
+
107
+ m = re.match(r"^objective:([a-z0-9-]+):\d+$", opts["plan"])
108
+ if not m: cannot("--plan must be an objective tid (objective:<slug>:<n>)")
109
+ slug = m.group(1)
110
+ ws = opts["workspace"] or slug
111
+ if not opts["replay"] and not KEY: cannot("no GATEWAY_API_KEY resolved (ONE_ENV_FILE ladder) — use --replay for an offline run")
112
+
113
+ # ── the document, parsed the way the executor reads it ───────────────────────
114
+ try:
115
+ src = open(DOC, encoding="utf-8").read()
116
+ except OSError as e:
117
+ cannot(f"document unreadable: {e}")
118
+
119
+ def unq(s): # a TQL double-quoted literal → its text (same escapes as workflow-tql.ts)
120
+ return s.replace('\\"', '"').replace("\\\\", "\\")
121
+
122
+ steps, order, var2tid = {}, [], {}
123
+ # A statement ends at the first `;` OUTSIDE a quoted literal — an intent string
124
+ # may carry one (step:promise does), and a non-greedy `.*?;` stopped inside it.
125
+ STMT = r'((?:[^;"]|"(?:[^"\\]|\\.)*")*);'
126
+ for mm in re.finditer(r'(\$s\d+) isa thing,' + STMT, src, re.S):
127
+ var, body = mm.group(1), mm.group(2)
128
+ tid = re.search(r'has tid "step:([^"]+)"', body)
129
+ kind = re.search(r'has step-kind "([^"]+)"', body)
130
+ cfg = re.search(r'has step-config "((?:[^"\\]|\\.)*)"', body)
131
+ if not (tid and kind and cfg): cannot(f"step {var} is missing tid/kind/config")
132
+ try:
133
+ config = json.loads(unq(cfg.group(1)))
134
+ except json.JSONDecodeError as e:
135
+ cannot(f"step:{tid.group(1)} step-config is not JSON: {e}")
136
+ steps[tid.group(1)] = {"id": tid.group(1), "kind": kind.group(1), "config": config}
137
+ order.append(tid.group(1)); var2tid[var] = tid.group(1)
138
+
139
+ edges = []
140
+ for mm in re.finditer(r'\$e\d+ isa path, links \(source: (\$s\d+), target: (\$s\d+)\)' + STMT, src, re.S):
141
+ cond = re.search(r'has edge-condition "([^"]*)"', mm.group(3))
142
+ edges.append({"source": var2tid[mm.group(1)], "target": var2tid[mm.group(2)], "condition": cond.group(1) if cond else ""})
143
+ if not steps or not edges: cannot("document has no steps or no edges")
144
+
145
+ # ── dispatch: replay · dry · live ─────────────────────────────────────────────
146
+ replay = json.load(open(opts["replay"])) if opts["replay"] else None
147
+ calls = []
148
+ READS = {"fn:run", "tasks:list"}
149
+
150
+ def dispatch(receiver, data):
151
+ payload = dict(data); payload["workspace"] = ws
152
+ calls.append({"receiver": receiver, "data": payload})
153
+ if replay is not None:
154
+ canned = replay.get(receiver)
155
+ if receiver == "fn:run" and isinstance(canned, dict): canned = canned.get(payload.get("fn"))
156
+ if isinstance(canned, list): canned = canned.pop(0) if canned else None
157
+ if canned is None: return {"ok": False, "error": f"replay has no answer for {receiver}"}
158
+ return canned
159
+ if opts["dry"] and receiver not in READS:
160
+ print(f"dry {receiver} {json.dumps(payload)[:160]}")
161
+ return {"ok": True, "dry": True}
162
+ # A named User-Agent: the edge refuses urllib's default with a 403 while the same
163
+ # key by curl gets 200 (measured 2026-09-04) — that 403 was the door, not the key.
164
+ req = urllib.request.Request(f"{API}/api/ask/{receiver}", data=json.dumps({"data": payload}).encode(),
165
+ headers={"Authorization": f"Bearer {KEY}", "content-type": "application/json", "User-Agent": "factory-turn.sh (curl-shaped; ONE factory R8)"}, method="POST")
166
+ try:
167
+ with urllib.request.urlopen(req, timeout=60) as r:
168
+ body = json.loads(r.read().decode() or "{}")
169
+ except urllib.error.HTTPError as e:
170
+ try: body = json.loads(e.read().decode() or "{}")
171
+ except Exception: return {"ok": False, "error": f"http {e.code}"}
172
+ except Exception as e:
173
+ return {"ok": False, "error": f"unreachable: {e}"}
174
+ # The door answers an ENVELOPE — {"outcome":"result","result":{...},"signalId",...}
175
+ # — exactly what a workflow step's dispatch sees. Unwrap it the same way:
176
+ # `result` is the receiver's answer; any other outcome (dissolved, failure) is
177
+ # a refusal the receiver never saw, reported as such rather than as its answer.
178
+ if isinstance(body, dict) and "outcome" in body:
179
+ if body.get("outcome") in ("result", "success"): return body.get("result") if isinstance(body.get("result"), dict) else {"ok": True, "result": body.get("result")}
180
+ return {"ok": False, "error": f"outcome {body.get('outcome')}: {body.get('error') or body.get('reason') or ''}".strip()}
181
+ return body
182
+
183
+ # ── substitution + guard grammar, mirroring workflow-executor.ts ─────────────
184
+ # `tag` is carried WHOLE: a string arg substitutes only in whole, so the document
185
+ # reads $trigger.tag rather than composing "slug:$trigger.slug" (which ships the
186
+ # literal — the first live dry-run proved it with an honest zero from tasks:list).
187
+ trigger = {"plan": opts["plan"], "slug": slug, "tag": f"slug:{slug}", "workspace": ws}
188
+ results = {}
189
+
190
+ def sub(v, item=None):
191
+ if isinstance(v, dict): return {k: sub(x, item) for k, x in v.items()}
192
+ if isinstance(v, list): return [sub(x, item) for x in v]
193
+ if not isinstance(v, str) or not v.startswith("$"): return v
194
+ if v.startswith("$trigger."): return trigger.get(v[9:], v)
195
+ if v.startswith("$step."):
196
+ rest = v[6:]; sid, _, field = rest.partition(".")
197
+ if sid not in results: return v
198
+ r = results[sid]
199
+ if not field: return r
200
+ return r.get(field, v) if isinstance(r, dict) else v
201
+ if v == "$item": return item if item is not None else v
202
+ if v.startswith("$item."): return item.get(v[6:], v) if isinstance(item, dict) else v
203
+ return v
204
+
205
+ def getpath(obj, path):
206
+ for p in path.split("."):
207
+ if isinstance(obj, dict): obj = obj.get(p)
208
+ elif isinstance(obj, list) and p == "length": obj = len(obj)
209
+ else: return None
210
+ return obj
211
+
212
+ def resolve_ref(ref):
213
+ if ref.startswith("$trigger."): return getpath(trigger, ref[9:])
214
+ if ref.startswith("$step."):
215
+ sid, _, field = ref[6:].partition(".")
216
+ return getpath(results.get(sid), field) if field else results.get(sid)
217
+ return getpath(trigger, ref)
218
+
219
+ def evaluate(expr):
220
+ mm = re.match(r"^(\S+)\s*(===|!==|>=|<=|>|<|includes)\s*(.+)$", expr.strip())
221
+ if not mm: return None
222
+ lhs = resolve_ref(mm.group(1)); raw = mm.group(3).strip().strip("'\"")
223
+ rhs = True if raw == "true" else False if raw == "false" else None if raw == "null" else float(raw) if re.match(r"^-?\d+(\.\d+)?$", raw) else raw
224
+ op = mm.group(2)
225
+ try:
226
+ if op == "===": return lhs == rhs and type(lhs) == type(rhs) if isinstance(rhs, bool) or isinstance(lhs, bool) else lhs == rhs
227
+ if op == "!==": return lhs != rhs
228
+ if op == ">": return float(lhs) > float(rhs)
229
+ if op == "<": return float(lhs) < float(rhs)
230
+ if op == ">=": return float(lhs) >= float(rhs)
231
+ if op == "<=": return float(lhs) <= float(rhs)
232
+ if op == "includes": return rhs in lhs
233
+ except (TypeError, ValueError):
234
+ return False
235
+ return None
236
+
237
+ # ── the walk ─────────────────────────────────────────────────────────────────
238
+ done, failed, pruned, decision = set(), set(), set(), {}
239
+ incoming = {sid: [e for e in edges if e["target"] == sid] for sid in steps}
240
+ closes, dark, parked = [], [], None
241
+
242
+ def active(e):
243
+ return e["source"] in done and (e["condition"] == "" or decision.get(e["source"]) == e["condition"])
244
+
245
+ progress = True
246
+ while progress:
247
+ progress = False
248
+ for sid in order:
249
+ if sid in done or sid in failed or sid in pruned: continue
250
+ ins = incoming[sid]
251
+ if any(e["source"] in failed for e in ins): failed.add(sid); progress = True; continue
252
+ if any(e["source"] not in done and e["source"] not in pruned for e in ins): continue
253
+ if ins and not any(active(e) for e in ins): pruned.add(sid); progress = True; continue
254
+ st = steps[sid]; cfg = st["config"]; kind = st["kind"]
255
+ if kind == "trigger":
256
+ results[sid] = trigger
257
+ elif kind == "condition":
258
+ v = evaluate(cfg.get("expr", ""))
259
+ if v is None: failed.add(sid); print(f"failed step:{sid} — unparseable guard {cfg.get('expr')!r}"); progress = True; continue
260
+ decision[sid] = "true" if v else "false"
261
+ print(f"guard step:{sid} {cfg.get('expr')} → {decision[sid]}")
262
+ elif kind == "human":
263
+ parked = sid
264
+ print(f"park step:{sid} — {cfg.get('question', 'a person, now')}")
265
+ break
266
+ elif kind == "tool":
267
+ receiver = cfg.get("receiver"); args = cfg.get("args", {})
268
+ if not receiver:
269
+ dark.append(sid)
270
+ print(f"dark step:{sid} — {cfg.get('blocked', 'no receiver')}")
271
+ results[sid] = None
272
+ elif isinstance(cfg.get("each"), str):
273
+ items = sub(cfg["each"]); limit = cfg.get("limit")
274
+ if not isinstance(items, list) or not isinstance(limit, int) or len(items) > limit:
275
+ failed.add(sid); print(f"failed step:{sid} — each_limit (items={type(items).__name__}, limit={limit})"); progress = True; continue
276
+ out = [dispatch(receiver, sub(args, it)) for it in items]
277
+ results[sid] = out
278
+ for it, r in zip(items, out):
279
+ rec = (r.get("results") or [{}])[0] if isinstance(r, dict) and isinstance(r.get("results"), list) else (r if isinstance(r, dict) else {})
280
+ print(f"action {receiver} {it.get('tid') if isinstance(it, dict) else it} → {'ok' if rec.get('ok') else rec.get('reason') or rec.get('error') or r.get('error') if isinstance(r, dict) else '?'}")
281
+ if not items: print(f"action {receiver} — 0 items")
282
+ else:
283
+ r = dispatch(receiver, sub(args))
284
+ results[sid] = r
285
+ if receiver == "world:outcome": closes.append((sid, bool(args.get("success"))))
286
+ elif receiver == "fn:run": print(f"fun {args.get('fn')}: {'ok' if r.get('ok') else 'FAILED ' + str(r.get('error'))} · {len(r.get('rows') or [])} row(s)")
287
+ else: print(f"call {receiver} → {'ok' if r.get('ok') else r.get('error')}")
288
+ else:
289
+ failed.add(sid); print(f"failed step:{sid} — kind {kind} is not driven locally"); progress = True; continue
290
+ done.add(sid); progress = True
291
+ if parked: break
292
+
293
+ if opts["log"]:
294
+ with open(opts["log"], "w") as f:
295
+ for c in calls: f.write(json.dumps(c) + "\n")
296
+
297
+ # ── the verdict ──────────────────────────────────────────────────────────────
298
+ counts = {k: len((results.get(k) or {}).get("rows") or []) if isinstance(results.get(k), dict) else "?" for k in ("ready", "unrealised", "uncovered", "unproven", "deadlocked", "incomplete")}
299
+ print("turn: plan=%s ws=%s ready=%s unrealised=%s uncovered=%s unproven=%s deadlocked=%s incomplete=%s · dark=%s" % (
300
+ opts["plan"], ws, counts["ready"], counts["unrealised"], counts["uncovered"], counts["unproven"], counts["deadlocked"], counts["incomplete"], ",".join(dark) or "-"))
301
+
302
+ if parked:
303
+ print(f"factory-turn: ESCALATE parked at step:{parked} — deadlocked={counts['deadlocked']}, a person, now")
304
+ sys.exit(2)
305
+ if failed:
306
+ print(f"factory-turn: CANNOT RUN — step(s) failed: {', '.join(sorted(failed))}")
307
+ sys.exit(3)
308
+ if not closes:
309
+ print("factory-turn: CANNOT RUN — the walk reached no close (document shape)")
310
+ sys.exit(3)
311
+ close, marked = closes[-1]
312
+ if close == "ran":
313
+ launched = sum(1 for r in (results.get("execute") or []) if isinstance(r, dict) for rec in (r.get("results") or []) if rec.get("ok"))
314
+ ready = counts["ready"]
315
+ if launched == 0 and not opts["dry"]:
316
+ print(f"factory-turn: RED ran but launched 0 of {ready} ready — every launch was refused; read the receipts above")
317
+ sys.exit(1)
318
+ print(f"factory-turn: OK ran — launched {launched} of {ready} ready{' (dry-run: nothing written)' if opts['dry'] else ''}")
319
+ sys.exit(0)
320
+ if marked:
321
+ print(f"factory-turn: OK {close} — ready and incomplete both empty; the plan is established")
322
+ sys.exit(0)
323
+ why = {"blind": "the substrate did not answer", "stalled": f"empty stream with incomplete={counts['incomplete']} — nothing to run, work remains", "halted": "a person halted the plan"}.get(close, close)
324
+ print(f"factory-turn: RED {close} — {why}")
325
+ sys.exit(1)
326
+ PY