@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.
- package/agents/abm-strategist.md +89 -0
- package/agents/ads-meta.md +97 -0
- package/agents/analyst.md +173 -0
- package/agents/architect.md +280 -0
- package/agents/brand-guardian.md +88 -0
- package/agents/brand-strategist.md +92 -0
- package/agents/campaign-content.md +90 -0
- package/agents/campaign-email.md +88 -0
- package/agents/campaign-sms.md +87 -0
- package/agents/campaign-social.md +87 -0
- package/agents/cco.md +99 -0
- package/agents/ceo.md +106 -0
- package/agents/chairman.md +120 -0
- package/agents/cmo.md +101 -0
- package/agents/community-greeter.md +89 -0
- package/agents/community-moderator.md +92 -0
- package/agents/compliance.md +92 -0
- package/agents/copywriter.md +100 -0
- package/agents/creative-strategist.md +95 -0
- package/agents/cro.md +98 -0
- package/agents/cto.md +267 -0
- package/agents/customer-interviewer.md +93 -0
- package/agents/customer-researcher.md +89 -0
- package/agents/customer-success-manager.md +90 -0
- package/agents/customer-trainer.md +90 -0
- package/agents/cxo.md +100 -0
- package/agents/demand-creator.md +89 -0
- package/agents/demo-mover.md +83 -0
- package/agents/demo-specialist.md +89 -0
- package/agents/demo-thai-family-law.md +87 -0
- package/agents/designer.md +112 -0
- package/agents/discovery-caller.md +98 -0
- package/agents/doctor.md +139 -0
- package/agents/educate-coach.md +100 -0
- package/agents/elevate-tutor.md +83 -0
- package/agents/email-lifecycle-marketer.md +91 -0
- package/agents/engage-specialist.md +99 -0
- package/agents/events-coordinator.md +90 -0
- package/agents/foundation-builder.md +94 -0
- package/agents/funnel-architect.md +89 -0
- package/agents/gift-creator.md +100 -0
- package/agents/google-ads.md +103 -0
- package/agents/guide.md +292 -0
- package/agents/helpdesk-dispatcher.md +88 -0
- package/agents/hook-specialist.md +91 -0
- package/agents/identify-optimizer.md +101 -0
- package/agents/implementer.md +305 -0
- package/agents/incident-commander.md +120 -0
- package/agents/insights-lead.md +93 -0
- package/agents/journey-runner.md +113 -0
- package/agents/linkedin-ads.md +123 -0
- package/agents/live-sales-chat.md +90 -0
- package/agents/market-researcher.md +88 -0
- package/agents/media-buyer.md +96 -0
- package/agents/movers-customer-researcher.md +95 -0
- package/agents/movers-foundation-builder.md +96 -0
- package/agents/movers-market-researcher.md +97 -0
- package/agents/movers-pricing-strategist.md +171 -0
- package/agents/nurture-architect.md +99 -0
- package/agents/offer-architect.md +92 -0
- package/agents/onboarder.md +222 -0
- package/agents/onboarding-specialist.md +92 -0
- package/agents/operations-dashboard.md +98 -0
- package/agents/perf-engineer.md +348 -0
- package/agents/playbook-writer.md +71 -0
- package/agents/plg-strategist.md +91 -0
- package/agents/positioning-architect.md +88 -0
- package/agents/press-officer.md +89 -0
- package/agents/pricing-strategist.md +94 -0
- package/agents/privacy-officer.md +92 -0
- package/agents/referral-manager.md +91 -0
- package/agents/refine-analyst.md +102 -0
- package/agents/release-manager.md +261 -0
- package/agents/renewals-upsell-rep.md +90 -0
- package/agents/review-engineer.md +327 -0
- package/agents/rewards-steward.md +82 -0
- package/agents/sales-call-coach.md +94 -0
- package/agents/sales-closer.md +98 -0
- package/agents/security-auditor.md +313 -0
- package/agents/sell-closer.md +101 -0
- package/agents/share-amplifier.md +99 -0
- package/agents/social-media-manager.md +92 -0
- package/agents/storyteller.md +250 -0
- package/agents/strategist.md +83 -0
- package/agents/strategy-aligner.md +94 -0
- package/agents/support-agent.md +90 -0
- package/agents/tagger.md +245 -0
- package/agents/tech-writer.md +216 -0
- package/agents/test-engineer.md +243 -0
- package/agents/tiktok-ads.md +105 -0
- package/agents/tracking-engineer.md +92 -0
- package/agents/upsell-strategist.md +101 -0
- package/agents/voice.md +85 -0
- package/agents/w4-tools.md +153 -0
- package/agents/w4-verify.md +20 -138
- package/agents/workflow-optimiser.md +84 -0
- package/commands/close.md +814 -160
- package/commands/create.md +2 -2
- package/commands/deploy.md +554 -175
- package/commands/do.md +119 -109
- package/commands/fast.md +67 -0
- package/commands/improve.md +2 -2
- package/commands/one.md +418 -0
- package/commands/see.md +1 -1
- package/commands/sync.md +1 -1
- package/commands/tasks.md +222 -0
- package/commands/vespio.md +110 -0
- package/commands/vespio.remote.md +227 -0
- package/hooks/hooks.json +25 -79
- package/hooks/lib/governor-escape-match.sh +88 -0
- package/hooks/lib/hook.sh +4 -13
- package/hooks/lib/signal.sh +9 -2
- package/hooks/scripts/branch-pin.sh +6 -0
- package/hooks/scripts/config-protect.sh +6 -0
- package/hooks/scripts/do-outcome-gate.sh +19 -2
- package/hooks/scripts/git-add-guard.sh +43 -2
- package/hooks/scripts/governor-escape.sh +96 -0
- package/hooks/scripts/load-guard.sh +127 -0
- package/hooks/scripts/post-edit-check.sh +36 -29
- package/hooks/scripts/session-start.sh +34 -80
- package/hooks/scripts/task-complete-verify.sh +46 -40
- package/package.json +1 -1
- package/rules/documentation.md +9 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agentverse-audit.sh +256 -0
- package/scripts/asi-walk.sh +435 -0
- package/scripts/astro-build-cached.sh +143 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/blocks-manifest.mjs +497 -0
- package/scripts/blocks-ratchet.sh +116 -0
- package/scripts/blocks-render-probe.mjs +529 -0
- package/scripts/blocks-usage.mjs +334 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/close-metrics.sh +558 -0
- package/scripts/close-owner.sh +268 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-dev.sh +35 -0
- package/scripts/deploy-emit.sh +298 -0
- package/scripts/deploy-gate-check.sh +128 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy.sh +1273 -0
- package/scripts/do-auto.sh +598 -48
- package/scripts/do-brief.sh +113 -0
- package/scripts/do-close.sh +1137 -0
- package/scripts/do-commit.sh +75 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-cycle-shape-check.sh +160 -0
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-derive-check.sh +436 -0
- package/scripts/do-fleet.sh +106 -28
- package/scripts/do-folder.sh +10 -1
- package/scripts/do-next.sh +106 -0
- package/scripts/do-orchestrate.sh +17 -5
- package/scripts/do-plan-json.mjs +201 -0
- package/scripts/do-plan-json.sh +8 -0
- package/scripts/do-preflight.sh +117 -0
- package/scripts/do-project.sh +157 -0
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +295 -23
- package/scripts/do-rank.py +31 -1
- package/scripts/do-recon-cache.sh +7 -1
- package/scripts/do-recon-pack.sh +196 -0
- package/scripts/do-reconcile.sh +121 -4
- package/scripts/do-signal.sh +280 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-test-gate.sh +80 -0
- package/scripts/do-tick.sh +102 -0
- package/scripts/do-tier.sh +6 -0
- package/scripts/do-triage.sh +182 -0
- package/scripts/do-ui-gate.sh +1 -1
- package/scripts/do-w4-gates.sh +451 -0
- package/scripts/do-walk.sh +12 -1
- package/scripts/env-sync.sh +173 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +68 -14
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +118 -3
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-ship.sh +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/factory-turn.sh +326 -0
- package/scripts/factory-walk.sh +396 -0
- package/scripts/factory-width.sh +57 -0
- package/scripts/fade-toxic.sh +4 -3
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/fixtures/triage-dupe.md +5 -0
- package/scripts/fleet-manifest.mjs +108 -0
- package/scripts/fleet-status.sh +110 -0
- package/scripts/full-suite-paths-check.sh +144 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +125 -0
- package/scripts/gate-run.sh +73 -0
- package/scripts/gc-content-check.sh +140 -0
- package/scripts/gen-dev-config.py +20 -0
- package/scripts/govern-bound-check.sh +60 -0
- package/scripts/govern-claims-check.sh +233 -0
- package/scripts/govern-mem-check.sh +290 -0
- package/scripts/governor-doors-check.sh +362 -0
- package/scripts/governor-escape-check.sh +171 -0
- package/scripts/health.sh +413 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/land.sh +551 -0
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/lib/govern.sh +361 -0
- package/scripts/lib/govern.ts +756 -0
- package/scripts/lighthouse-run.sh +187 -0
- package/scripts/livekit-live-check.sh +61 -0
- package/scripts/livekit-ratchet.sh +46 -0
- package/scripts/load-guard-check.sh +49 -0
- package/scripts/machine-check.sh +102 -0
- package/scripts/machine-watch.sh +177 -0
- package/scripts/one-agents.mjs +415 -0
- package/scripts/one-resume.sh +78 -0
- package/scripts/orphan-baseline.json +182 -0
- package/scripts/orphan-modules.mjs +179 -0
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/promise-manifest.mjs +24 -1
- package/scripts/release.sh +322 -0
- package/scripts/roles-check.sh +946 -0
- package/scripts/sdk-build-cached.sh +64 -0
- package/scripts/signal-watch.sh +241 -0
- package/scripts/skills-publish.sh +94 -0
- package/scripts/speed-cache-check.sh +173 -0
- package/scripts/speed-check.mjs +907 -0
- package/scripts/speed-parity-check.sh +648 -0
- package/scripts/speed-waterfall-check.sh +355 -0
- package/scripts/substrate-env-parity.mjs +156 -0
- package/scripts/tasks-claim-race.mjs +108 -0
- package/scripts/tasks-loop.sh +185 -0
- package/scripts/test-cached.sh +255 -0
- package/scripts/test-full.sh +87 -0
- package/scripts/test-honesty.mjs +137 -0
- package/scripts/test-lanes.sh +166 -0
- package/scripts/test-speed.sh +94 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +179 -0
- package/scripts/typedb-flake-check.sh +197 -0
- package/scripts/urls-lint.sh +15 -0
- package/scripts/verify-fast.sh +445 -0
- package/scripts/vespio-sync.sh +149 -0
- package/scripts/wf-check.mjs +104 -1
- package/scripts/worktree-preview.sh +879 -0
- package/scripts/worktree-up.sh +208 -0
- package/skills/livekit-agents/SKILL.md +285 -0
- package/skills/livekit-agents/references/freshness-rules.md +168 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/signal/SKILL.md +0 -1
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/hooks/scripts/compact-hint.sh +0 -35
- package/hooks/scripts/gate-guard.sh +0 -83
- package/hooks/scripts/read-tracker.sh +0 -26
- package/hooks/scripts/session-end-verify.sh +0 -51
- package/hooks/scripts/stop-reflect.sh +0 -140
- package/hooks/scripts/sync-priority-todo.sh +0 -57
- package/hooks/scripts/sync-todo-docs.sh +0 -46
- package/hooks/scripts/tool-signal.sh +0 -48
- 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
|