@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
@@ -36,7 +36,11 @@ fi
36
36
 
37
37
  # 3. longest-prefix match against known buildable folders (one.ie/web BEFORE one.ie).
38
38
  # bash 3.2-safe (macOS): no associative arrays — dedup a newline list with sort -u.
39
- folders="one.ie/web one.ie packages agents api schema sync backup"
39
+ # channels/ and pay/ were ABSENT until 2026-09-03, and they are two of the five
40
+ # deployed services. A changed set touching only them answered "unmapped", which
41
+ # do-w4-gates then dropped on the floor -- no verify row at all, in either lane.
42
+ # pay/backend before pay, one.ie/web before one.ie: longest prefix must win.
43
+ folders="one.ie/web one.ie packages agents api schema sync backup channels pay/backend pay"
40
44
  matched=""
41
45
  for p in "${paths[@]}"; do
42
46
  for f in $folders; do
@@ -56,7 +60,12 @@ printf '%s\n' "$matched" | while IFS= read -r f; do
56
60
  pj="$ROOT/$f/package.json"
57
61
  verify="null"; build="null"
58
62
  if [ -f "$pj" ]; then
63
+ # Dev lane by default: verify:fast (tsc + related tests + pinned gates).
64
+ # FULL_VERIFY=1 (set by /close and ./deploy) asks for the whole suite.
59
65
  jq -e '.scripts.verify' "$pj" >/dev/null 2>&1 && verify='"bun run verify"'
66
+ if [ "${FULL_VERIFY:-0}" != "1" ] && jq -e '.scripts["verify:fast"]' "$pj" >/dev/null 2>&1; then
67
+ verify='"bun run verify:fast"'
68
+ fi
60
69
  jq -e '.scripts.build' "$pj" >/dev/null 2>&1 && build='"bun run build"'
61
70
  fi
62
71
  printf '{"folder":"%s","verify":%s,"build":%s,"doc_only":false}\n' "$f" "$verify" "$build"
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bash
2
+ # do-next.sh <slug> [--claim] [--all] — stream the next READY cycle(s) of a plan.
3
+ #
4
+ # manifest: portable
5
+ #
6
+ # WHY. do-engine runs a plan as BATCHES: every cycle in a batch must finish before
7
+ # any cycle of the next one starts, so the batch costs its SLOWEST member and a
8
+ # cycle whose own dependencies closed an hour ago still waits at the barrier. The
9
+ # batch is not a real constraint — the DAG is. This emits the cycles whose
10
+ # dependencies are ACTUALLY closed, right now, so a worker can take one, do it,
11
+ # verify it, close it, and immediately ask for the next.
12
+ #
13
+ # Readiness is DERIVED, never read. The todo's `state: ready` / `state: blocked-on-C1,C2`
14
+ # words are written once when the plan is authored and nothing updates them as
15
+ # cycles close — the same second-source-of-truth seam that let a verified cycle
16
+ # stay `- [ ]` forever. The `blocked-on-` list is the edge set (authored, static,
17
+ # correct); whether those edges are SATISFIED is computed from the checkboxes,
18
+ # which are the live record do-tick.sh writes.
19
+ #
20
+ # --claim makes the emission ATOMIC across every session and worktree on this box:
21
+ # claim_take is mkdir(2)-arbitrated, so N workers racing the same plan each get a
22
+ # DIFFERENT cycle and never the same one. A claim evaporates when its owner dies
23
+ # or its lease expires, so a killed worker's cycle returns to the stream by itself
24
+ # — no coordinator, no queue, nothing to deadlock.
25
+ set -euo pipefail
26
+
27
+ SLUG=""; DO_CLAIM=0; ALL=0
28
+ while [ $# -gt 0 ]; do
29
+ case "$1" in
30
+ --claim) DO_CLAIM=1 ;;
31
+ --all) ALL=1 ;;
32
+ -*) echo "do-next: unknown flag $1" >&2; exit 2 ;;
33
+ *) [ -n "$SLUG" ] && { echo "do-next: one slug only" >&2; exit 2; }; SLUG="$1" ;;
34
+ esac
35
+ shift
36
+ done
37
+ [ -n "$SLUG" ] || { echo "usage: do-next.sh <slug> [--claim] [--all]" >&2; exit 2; }
38
+
39
+ ROOT="${DO_NEXT_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
40
+ TODO="$ROOT/text/${SLUG}-todo.md"
41
+ [ -f "$TODO" ] || { echo "do-next: no such plan: $TODO" >&2; exit 3; }
42
+
43
+ # The cycle-line shape. Identical to do-tick.sh's reader and do-auto.sh's
44
+ # _remaining(); parity across all four implementations is proven behaviourally by
45
+ # do-cycle-shape-check.sh, which reads this file rather than a copy of it.
46
+ OPEN_RE='^[[:space:]]*- \[[ ~]\] \*{0,2}C[0-9]+\*{0,2} —'
47
+ CLOSED_RE='^[[:space:]]*- \[x\] \*{0,2}C[0-9]+\*{0,2} —'
48
+
49
+ closed_raw=$(grep -oE "$CLOSED_RE" "$TODO" 2>/dev/null || true)
50
+ closed=" $(printf '%s' "$closed_raw" | grep -oE 'C[0-9]+' 2>/dev/null | tr '\n' ' ' || true)"
51
+ open_lines=$(grep -E "$OPEN_RE" "$TODO" 2>/dev/null || true)
52
+ [ -n "$open_lines" ] || { echo "do-next: ${SLUG} — no open cycles; plan complete" >&2; exit 0; }
53
+
54
+ ready=""
55
+ while IFS= read -r line; do
56
+ [ -n "$line" ] || continue
57
+ cid=$(printf '%s' "$line" | grep -oE 'C[0-9]+' | head -1)
58
+ [ -n "$cid" ] || continue
59
+ # Edge set: `state: blocked-on-C1,C2`. No blocked-on clause ⇒ no edges ⇒ ready.
60
+ deps=$(printf '%s' "$line" | sed -nE 's/.*blocked-on-([C0-9,]+).*/\1/p' | tr ',' ' ')
61
+ blocked=0
62
+ for d in $deps; do
63
+ case "$closed" in *" $d "*) ;; *) blocked=1; break ;; esac
64
+ done
65
+ [ "$blocked" -eq 0 ] && ready="$ready $cid"
66
+ done <<EOF
67
+ $open_lines
68
+ EOF
69
+
70
+ ready=$(printf '%s' "$ready" | tr ' ' '\n' | grep -v '^$' || true)
71
+ [ -n "$ready" ] || { echo "do-next: ${SLUG} — cycles remain OPEN but every one is blocked on an unclosed dependency. Nothing to stream." >&2; exit 1; }
72
+
73
+ if [ "$DO_CLAIM" -eq 0 ]; then
74
+ [ "$ALL" -eq 1 ] && printf '%s\n' "$ready" || printf '%s\n' "$ready" | head -1
75
+ exit 0
76
+ fi
77
+
78
+ # shellcheck source=lib/govern.sh
79
+ . "$(dirname "${BASH_SOURCE[0]}")/lib/govern.sh"
80
+ # Claim on behalf of the WORKER, not of this script. do-next.sh exits in
81
+ # milliseconds; if the claim tracked its lifetime it would evaporate immediately
82
+ # and the next racer would be handed the very cycle we just emitted. Default to
83
+ # the caller (PPID) — the shell, agent, or fleet loop that will actually do the
84
+ # work and whose death SHOULD return the cycle to the stream.
85
+ export GOVERN_CLAIM_PID="${DO_NEXT_OWNER_PID:-$PPID}"
86
+ # PPID is only the real worker when do-next is called directly. Called the
87
+ # natural way — `cid=$(do-next.sh <slug> --claim)` — the parent IS the command
88
+ # substitution's subshell, which dies the instant the value is assigned, so the
89
+ # claim would evaporate before the worker ever saw the cycle it was handed.
90
+ # minhold keeps the ground held for a grace window regardless of owner liveness;
91
+ # the worker adopts it by re-claiming under its own pid (claim_take is idempotent
92
+ # for the same owner), and the lease TTL still bounds the whole thing.
93
+ export GOVERN_CLAIM_MIN_HOLD_SECS="${DO_NEXT_MIN_HOLD_SECS:-300}"
94
+ took=""
95
+ for cid in $ready; do
96
+ if claim_take "do-${SLUG}-${cid}" "${SLUG}/${cid}" >/dev/null 2>&1; then
97
+ took="$took $cid"
98
+ [ "$ALL" -eq 1 ] || break
99
+ fi
100
+ done
101
+ took=$(printf '%s' "$took" | tr ' ' '\n' | grep -v '^$' || true)
102
+ if [ -z "$took" ]; then
103
+ echo "do-next: ${SLUG} — every ready cycle is already claimed by a live worker. Nothing to take." >&2
104
+ exit 1
105
+ fi
106
+ printf '%s\n' "$took"
@@ -278,11 +278,23 @@ while [ "$i" -lt "$ITERS" ]; do
278
278
  fi
279
279
 
280
280
  # LIVE: build the plan (do-auto worktrees + cycles + W4 + digest)
281
- echo " ▶ building (log: $WT_BASE/$pick/.orchestrate.log)" | tee -a "$RUNLOG"
282
- mkdir -p "$WT_BASE/$pick"
283
- _link_deps "$WT_BASE/$pick" # worktree can verify before do-auto even runs
284
- if "$AUTO" "$pick" >"$WT_BASE/$pick/.orchestrate.log" 2>&1; then
285
- _link_deps "$WT_BASE/$pick" # re-link in case do-auto recreated the worktree
281
+ #
282
+ # The log lives BESIDE the worktrees, never inside one. It used to be
283
+ # "$WT_BASE/$pick/.orchestrate.log", which forced a `mkdir -p` of the worktree
284
+ # path before do-auto ran — and that plain directory made do-auto's old
285
+ # `rev-parse --git-dir` probe report "resuming worktree". `git worktree add`
286
+ # then never ran, and every cycle commit targeted the MAIN tree's HEAD, where
287
+ # _safe_stage refuses to stage: orchestrated cycles committed NOTHING, with no
288
+ # error. do-auto now discriminates properly (see _is_worktree there); this side
289
+ # removes the trigger, so neither half depends on the other being correct.
290
+ #
291
+ # Keeping it outside also means the log survives `git worktree remove`, which
292
+ # is when you most want to read it.
293
+ log="$WT_BASE/.logs/$pick.log" # not `local` — this loop is top-level, not a function
294
+ mkdir -p "$WT_BASE/.logs"
295
+ echo " ▶ building (log: $log)" | tee -a "$RUNLOG"
296
+ if "$AUTO" "$pick" >"$log" 2>&1; then
297
+ _link_deps "$WT_BASE/$pick" # do-auto cut the worktree; ensure deps are linked
286
298
  if _reflect "$pick" | tee -a "$RUNLOG"; then
287
299
  echo " ✅ $pick — VERIFIED, merge-ready on branch do/$pick" | tee -a "$RUNLOG"
288
300
  BUILT+=("$pick")
@@ -0,0 +1,201 @@
1
+ // do-plan-json.mjs — parse a /do plan into JSON, deterministically. No LLM.
2
+ //
3
+ // WHY (2026-08-31): do-engine.js runs with no filesystem access, so it asked an
4
+ // LLM agent for "the raw, verbatim content of text/<slug>-todo.md". An LLM will
5
+ // not return a 30KB file verbatim — it returned 6.8KB ending "(Full todo text
6
+ // continues...)". The guard reading open cycles out of that text saw NONE, and
7
+ // four launches burned ~3M subagent tokens building nothing.
8
+ //
9
+ // The plan is a deterministic fact on disk. Reading it through a probabilistic
10
+ // channel is the defect. This is the deterministic channel; the agent's only job
11
+ // becomes "run this, paste stdout". Same discipline as do-auto.sh's _remaining(),
12
+ // which is grep and has never had this failure.
13
+ //
14
+ // Exits non-zero and emits NOTHING when the plan is unreadable — a partial plan
15
+ // must never read as a runnable one.
16
+ import fs from 'node:fs'
17
+ import path from 'node:path'
18
+ import crypto from 'node:crypto'
19
+
20
+ const slug = process.argv[2]
21
+ if (!slug) { console.error('usage: do-plan-json.mjs <slug>'); process.exit(2) }
22
+
23
+ const repo = path.resolve(new URL('../..', import.meta.url).pathname)
24
+
25
+ // READ THE TODO THE ENGINE WRITES.
26
+ //
27
+ // do-tick.sh runs inside the plan WORKTREE, so a closed cycle is ticked in
28
+ // .do-worktrees/<slug>/text/<slug>-todo.md. This parser read main's copy, where
29
+ // those cycles are still `- [ ]` until the branch merges. So a relaunch computed
30
+ // openCycles from main and rebuilt work the worktree had already proven --
31
+ // measured 2026-09-01: a relaunch of `lifecycle` re-ran C0, C2 and C3 (all three
32
+ // closed and committed on do/lifecycle) before it could reach the C1/C5 it was
33
+ // launched for.
34
+ //
35
+ // Same read/write divergence that stalled the engine this morning, one level up:
36
+ // the tick and the reader were looking at two different files. The worktree copy
37
+ // is the live record while a plan is in flight; main's is the record after merge.
38
+ // Prefer the worktree when it exists, and SAY which one was read so a surprising
39
+ // openCycles is one line of output away from being explained.
40
+ const wtTodo = path.join(repo, '.do-worktrees', slug, 'text', `${slug}-todo.md`)
41
+ const mainTodo = path.join(repo, 'text', `${slug}-todo.md`)
42
+ const todo = (process.env.DO_PLAN_TODO) || (fs.existsSync(wtTodo) ? wtTodo : mainTodo)
43
+ if (!fs.existsSync(todo)) { console.error(`do-plan-json: no such plan: text/${slug}-todo.md`); process.exit(3) }
44
+
45
+ const t = fs.readFileSync(todo, 'utf8')
46
+ const fail = (m) => { console.error(`do-plan-json: ${m}`); process.exit(4) }
47
+
48
+ const fm = t.match(/^---\n([\s\S]*?)\n---/)
49
+ if (!fm) fail('no frontmatter')
50
+ const F = fm[1]
51
+
52
+ // batches: `- [C0, C2]` rows under `batches:`
53
+ //
54
+ // DEFECT this shape exists to prevent (measured 2026-09-08 on text/ads-loop-todo.md):
55
+ // the capture used to be /^batches:\n((?:[ \t]+- \[.*\]\n?)+)/m. A trailing
56
+ // `# comment` after the closing `]` is legal YAML and carried by most plans in
57
+ // text/ — it leaves text after `]`, so `\n?` matched empty, the `+` quantifier
58
+ // stopped at that row, and EVERY batch from there on was dropped. Nothing
59
+ // called fail(): the script still exited 0 and still emitted a well-formed
60
+ // object. ads-loop parsed as [["C1"…"C9"]] instead of [["C1"…"C9"],["C10"]],
61
+ // so /do would have run nine cycles of a ten-cycle plan and reported complete.
62
+ //
63
+ // Two changes, and the second is the load-bearing one:
64
+ // 1. a row may carry a trailing comment and trailing whitespace;
65
+ // 2. the block is delimited by INDENTATION (YAML's own rule) and every line
66
+ // inside it must parse — a line that does not is a REFUSAL, never a
67
+ // shorter list. Comment-tolerance alone would leave the next unreadable
68
+ // row failing in exactly the same silent way.
69
+ // Blank lines and whole-line `#` comments inside the block are legal and skipped.
70
+ // Inline flow style (`batches: [[C0]]`) still yields [] and fails at the
71
+ // `no batches:` invariant below, exactly as before.
72
+ const BATCH_ROW = /^[ \t]+-[ \t]*\[([^\]]*)\][ \t]*(?:#.*)?$/
73
+ const fLines = F.split('\n')
74
+ const bStart = fLines.findIndex((l) => /^batches:[ \t]*$/.test(l))
75
+ let batches = []
76
+ if (bStart >= 0) {
77
+ const rows = []
78
+ const bad = []
79
+ for (let i = bStart + 1; i < fLines.length; i++) {
80
+ const line = fLines[i]
81
+ if (!line.trim()) continue // blank line inside the block
82
+ if (!/^[ \t]/.test(line)) break // a column-0 line is the next key: block over
83
+ if (/^[ \t]*#/.test(line)) continue // whole-line comment
84
+ const m = line.match(BATCH_ROW)
85
+ if (m) rows.push(m[1])
86
+ else bad.push(line)
87
+ }
88
+ if (bad.length) {
89
+ fail(
90
+ `\`batches:\` carries ${bad.length} row(s) this parser cannot read. ` +
91
+ 'Refusing: a truncated batch list must never read as a complete one. ' +
92
+ `A row is \`- [C1, C2]\` with an optional trailing # comment. First offender: ${JSON.stringify(bad[0])}`,
93
+ )
94
+ }
95
+ batches = rows.map((r) => r.split(',').map((s) => s.trim()).filter(Boolean))
96
+ }
97
+
98
+ // outcome: the kill-switch. A YAML double-quoted scalar carries \" and \\ escapes;
99
+ // stripping the outer quotes WITHOUT unescaping leaves a stray backslash that
100
+ // JSON.stringify then doubles, emitting invalid JSON (measured: lifecycle-money,
101
+ // lifecycle-human, both of whose outcomes contain a quoted grep pattern).
102
+ function unquote(raw) {
103
+ if (raw.length > 1 && raw.startsWith('"') && raw.endsWith('"')) {
104
+ return raw.slice(1, -1).replace(/\\(["\\])/g, '$1')
105
+ }
106
+ if (raw.length > 1 && raw.startsWith("'") && raw.endsWith("'")) {
107
+ return raw.slice(1, -1).replace(/''/g, "'")
108
+ }
109
+ return raw
110
+ }
111
+ let outcome = ''
112
+ const om = F.match(/^outcome:[ \t]*(?:\|[-+]?|>[-+]?)?[ \t]*(.*)$/m)
113
+ if (om) {
114
+ if (om[1].trim()) outcome = unquote(om[1].trim())
115
+ else {
116
+ const rest = F.slice(F.indexOf(om[0]) + om[0].length)
117
+ const blk = rest.match(/^\n((?:[ \t]+.*\n?)+)/)
118
+ if (blk) outcome = blk[1].split('\n').map((l) => l.trim()).filter(Boolean).join(' ')
119
+ }
120
+ }
121
+
122
+ // The Status kanban — the same fact do-auto.sh's _remaining() greps.
123
+ const openCycles = []
124
+ for (const m of t.matchAll(/^[ \t]*- \[[ ~]\] \*{0,2}(C\d+)\*{0,2} —/gm)) {
125
+ if (!openCycles.includes(m[1])) openCycles.push(m[1])
126
+ }
127
+
128
+ // closed + deps — the REAL DAG, so a scheduler can stream instead of marching.
129
+ // `batches:` is the author's flattening of this graph into rounds, and a round
130
+ // costs its slowest member: a cycle whose own dependencies closed an hour ago
131
+ // still waits at the barrier for an unrelated sibling. The edges themselves live
132
+ // in the kanban's `state: blocked-on-C1,C2` clause. Emitting them lets the engine
133
+ // start a cycle the moment ITS dependencies pass. Same source do-next.sh reads.
134
+ const closedCycles = []
135
+ for (const m of t.matchAll(/^[ \t]*- \[x\] \*{0,2}(C\d+)\*{0,2} —/gm)) {
136
+ if (!closedCycles.includes(m[1])) closedCycles.push(m[1])
137
+ }
138
+ const deps = {}
139
+ for (const m of t.matchAll(/^[ \t]*- \[[x ~]\] \*{0,2}(C\d+)\*{0,2} —(.*)$/gm)) {
140
+ const d = m[2].match(/blocked-on-([C0-9,]+)/)
141
+ deps[m[1]] = d ? d[1].split(',').map((x) => x.trim()).filter(Boolean) : []
142
+ }
143
+
144
+ // Cycle headers: "## C3 — title ⚡/★ marker"
145
+ const cycles = {}
146
+ const heads = [...t.matchAll(/^##[ \t]+(C\d+)[ \t]+—[ \t]+(.+)$/gm)]
147
+ heads.forEach((m, i) => {
148
+ const body = t.slice(m.index, i + 1 < heads.length ? heads[i + 1].index : t.length)
149
+ // Repo-relative paths named anywhere in the cycle body — W1's recon list.
150
+ // ALWAYS repo-relative: an absolute path double-prefixes ${WT} downstream, and
151
+ // an LLM asked for "the file list" returns absolute paths about half the time.
152
+ const files = [...new Set(
153
+ [...body.matchAll(/(?<![\w/.-])((?:one\.ie|packages|channels|api|schema|pay|sync|backup|text|\.claude)\/[A-Za-z0-9._\/\[\]-]+\.[a-z]{2,5})/g)]
154
+ .map((f) => f[1].replace(/[.,)`]+$/, '')),
155
+ )]
156
+ // A path is only a recon hint if it EXISTS. Plan prose abbreviates
157
+ // ("api/events.ts" for one.ie/web/src/pages/api/events.ts), and a hint that
158
+ // resolves to nothing sends W1 hunting for a file that was never there.
159
+ // Existence-filtering makes the list self-validating instead of plausible.
160
+ .filter((f) => fs.existsSync(path.join(repo, f)))
161
+ .slice(0, 12)
162
+ cycles[m[1]] = {
163
+ title: m[2].replace(/\s*[⚡★].*$/, '').trim(),
164
+ // schema tier: touches persistent/irreversible shape (same patterns as do-tier.sh)
165
+ schema: /\.tql\b|migrations\/\d|\.sql\b|\.move\b|\.sol\b/.test(body),
166
+ fableMandatory: /\[Fable\s*—\s*mandatory\]/i.test(body),
167
+ files,
168
+ }
169
+ })
170
+
171
+ // Invariants — refuse to emit a plan that cannot be run.
172
+ if (!batches.length) fail('frontmatter has no `batches:` — nothing to schedule')
173
+ if (!openCycles.length) {
174
+ fail('no open cycles in the Status kanban. Lines must match /^[ \\t]*- \\[[ ~]\\] C<n> — /; ' +
175
+ 'a "Batch N - [ ] C0 — ..." prefix breaks the ^ anchor and hides the cycle')
176
+ }
177
+ const orphan = batches.flat().filter((c) => !cycles[c])
178
+ if (orphan.length) fail(`batches name cycles with no "## Cn —" header: ${orphan.join(', ')}`)
179
+ if (!outcome) fail('frontmatter has no `outcome:` kill-switch')
180
+
181
+ // shared_recon: the author's own explicit file list, existence-checked the same way.
182
+ const srm = F.match(/^shared_recon:\n((?:[ \t]+- .*\n?)+)/m)
183
+ const sharedRecon = srm
184
+ ? srm[1].trim().split('\n').map((l) => l.replace(/^[\s-]+/, '').trim())
185
+ .filter((f) => f && fs.existsSync(path.join(repo, f)))
186
+ : []
187
+
188
+ process.stdout.write(JSON.stringify({
189
+ slug,
190
+ sharedRecon,
191
+ todo: path.relative(repo, todo),
192
+ todoSource: todo === mainTodo ? 'main' : 'worktree',
193
+ worktree: path.join(repo, '.do-worktrees', slug),
194
+ outcome,
195
+ batches,
196
+ openCycles,
197
+ closedCycles,
198
+ deps,
199
+ cycles,
200
+ sha256: crypto.createHash('sha256').update(t).digest('hex').slice(0, 16),
201
+ }, null, 2) + '\n')
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ # do-plan-json.sh <slug> — thin wrapper over do-plan-json.mjs (the parser).
3
+ # manifest: portable
4
+ # Kept as a .sh so callers match every other script in this dir; the parser is a
5
+ # real .mjs file rather than a quoted heredoc, because embedding it inline is how
6
+ # a stray backslash silently became invalid JSON.
7
+ set -euo pipefail
8
+ exec node "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/do-plan-json.mjs" "$@"
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ # do-preflight.sh <slug> [--fix] — refuse to launch a plan into a broken environment.
3
+ #
4
+ # manifest: needs-env
5
+ #
6
+ # WHY. Every expensive failure on 2026-08-31 was an ENVIRONMENT fault discovered
7
+ # only AFTER the run had spent itself:
8
+ #
9
+ # - an unreachable model made W2 return nothing; W3 edited nothing, W4 verified
10
+ # an empty diff, and the plan reported "batch total loss". 41 agents and 3.7M
11
+ # tokens to learn that a model name was wrong.
12
+ # - `.env` was not linked into the worktree, so real-TypeDB tests failed with
13
+ # no_gateway_key and two verifiers hand-classified 4-7 red files as
14
+ # "environmental" before they could score their gate.
15
+ # - four abandoned gates held the box at load 138 and one held a governor slot,
16
+ # so live cycles starved and verifiers began SKIPPING their gates and passing
17
+ # cycles anyway.
18
+ # - a cycle line the writers could tick was invisible to the engine's reader.
19
+ #
20
+ # Not one of those is a code defect, and every one is checkable in seconds. This
21
+ # is the cheap deterministic gate that runs first, so a run is never spent
22
+ # discovering something a grep could have said.
23
+ #
24
+ # Exit 0 = safe to launch. Exit 1 = a blocker (do not launch). Exit 2 = usage.
25
+ set -uo pipefail
26
+
27
+ SLUG="${1:-}"; shift 2>/dev/null || true
28
+ FIX=0
29
+ for f in "$@"; do case "$f" in --fix) FIX=1 ;; esac; done
30
+ [ -n "$SLUG" ] || { echo "usage: do-preflight.sh <slug> [--fix]" >&2; exit 2; }
31
+
32
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
33
+ WT="$ROOT/.do-worktrees/$SLUG"
34
+ bad=0; warn=0
35
+ ok() { printf ' ok %s\n' "$1"; }
36
+ fail() { printf ' FAIL %s\n' "$1"; bad=$((bad+1)); }
37
+ note() { printf ' warn %s\n' "$1"; warn=$((warn+1)); }
38
+
39
+ echo "do-preflight: $SLUG"
40
+
41
+ # 1. The plan must PARSE. do-plan-json refuses (non-zero, no stdout) on a plan
42
+ # missing worktree/outcome/batches/cycles, which is the failure that used to
43
+ # surface as an LLM summarising a 30KB file down to 6.8KB.
44
+ if plan=$(node "$ROOT/.claude/scripts/do-plan-json.mjs" "$SLUG" 2>&1); then
45
+ ok "plan parses ($(printf '%s' "$plan" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(len(d["cycles"]), "cycles,", len(d["batches"]), "batches")' 2>/dev/null || echo 'ok'))"
46
+ else
47
+ fail "plan does not parse: $(printf '%s' "$plan" | head -1)"
48
+ fi
49
+
50
+ # 2. Worktree + the gitignored deps a worktree cannot inherit. A missing .env is
51
+ # not a slow test -- it makes a verifier argue with its own gate and teaches
52
+ # whoever reads the report that red is normal.
53
+ if [ -d "$WT" ]; then
54
+ ok "worktree present"
55
+ for dep in .env node_modules; do
56
+ for where in "one.ie/web/$dep" "$dep"; do
57
+ [ -e "$ROOT/$where" ] || continue
58
+ if [ -e "$WT/$where" ]; then ok "linked: $where"; else
59
+ if [ "$FIX" = "1" ]; then
60
+ mkdir -p "$(dirname "$WT/$where")" && ln -s "$ROOT/$where" "$WT/$where" 2>/dev/null \
61
+ && ok "linked: $where (repaired)" || fail "could not link $where"
62
+ else
63
+ fail "MISSING in worktree: $where — real-infra tests will fail as 'environmental'. Re-run with --fix"
64
+ fi
65
+ fi
66
+ break
67
+ done
68
+ done
69
+ else
70
+ note "no worktree yet at .do-worktrees/$SLUG (do-auto --setup-only will cut one)"
71
+ fi
72
+
73
+ # 3. The cycle-line shape must agree across its four implementations, or the
74
+ # engine reads a different set of open cycles than the writers can tick.
75
+ if bash "$ROOT/.claude/scripts/do-cycle-shape-check.sh" >/dev/null 2>&1; then
76
+ ok "cycle-line shape parity (reader == writer)"
77
+ else
78
+ fail "cycle-shape parity RED — run .claude/scripts/do-cycle-shape-check.sh"
79
+ fi
80
+
81
+ # 4. A workflow with a syntax error does not fail loudly; it never launches.
82
+ if node "$ROOT/.claude/scripts/wf-check.mjs" >/dev/null 2>&1; then
83
+ ok "workflow scripts parse + seam checks"
84
+ else
85
+ fail "wf-check RED — run node .claude/scripts/wf-check.mjs"
86
+ fi
87
+
88
+ # 5. The box. A saturated box is why verifiers started skipping gates and
89
+ # reporting passes anyway, so this is an ACCURACY check, not only a speed one.
90
+ orph=$(bash "$ROOT/.claude/scripts/gate-reaper.sh" --once --dry-run 2>/dev/null | grep -c 'WOULD reap' || true)
91
+ if [ "${orph:-0}" -gt 0 ]; then
92
+ if [ "$FIX" = "1" ]; then
93
+ bash "$ROOT/.claude/scripts/gate-reaper.sh" --once >/dev/null 2>&1
94
+ ok "reaped ${orph} abandoned gate(s)"
95
+ else
96
+ fail "${orph} abandoned gate(s) burning the box — re-run with --fix, or gate-reaper.sh --once"
97
+ fi
98
+ else
99
+ ok "no abandoned gates"
100
+ fi
101
+
102
+ head=$(bash -c ". '$ROOT/.claude/scripts/lib/govern.sh'; gate_headroom" 2>/dev/null || echo 99)
103
+ loadi=$(uptime | sed -E 's/.*load averages?: ([0-9.]+).*/\1/' | cut -d. -f1)
104
+ cores=$(sysctl -n hw.ncpu 2>/dev/null || echo 8)
105
+ if [ "${loadi:-0}" -gt $(( cores * 3 )) ]; then
106
+ note "load ${loadi} on ${cores} cores — gates will run slow; the governor will queue them"
107
+ else
108
+ ok "load ${loadi} on ${cores} cores, memory funds ${head} concurrent cycle(s)"
109
+ fi
110
+
111
+ echo
112
+ if [ "$bad" -gt 0 ]; then
113
+ echo "do-preflight: $bad blocker(s) — DO NOT LAUNCH. Every one of these is cheaper to fix now than to discover mid-run."
114
+ exit 1
115
+ fi
116
+ echo "do-preflight: clear${warn:+ ($warn warning(s))} — safe to launch $SLUG"
117
+ exit 0
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bash
2
+ # do-project.sh — project a promise's deliverables: into an idempotent subtask spec.
3
+ # Parent = the promise (born with tags slug:<slug> — resolveTaskBySlug cannot retrofit).
4
+ # One child per deliverable; notes first line is accept: <that accept:> verbatim.
5
+ # Emits JSON. Does not POST. Re-projecting with --existing <prior.json> yields new_rows=0.
6
+ # Usage: do-project.sh <slug> [--existing prior.json] | do-project.sh --self-test
7
+ set -euo pipefail
8
+
9
+ ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
10
+
11
+ # Lifted from do-promise-lint.sh — do not fork a second YAML parser.
12
+ unquote() {
13
+ local line="$1"
14
+ case "$line" in
15
+ \"*) printf '%s' "$line" | sed -E 's/^"(.*)"[[:space:]]*(#.*)?$/\1/' | sed 's/\\"/"/g' ;;
16
+ \'*) printf '%s' "$line" | sed -E "s/^'(.*)'[[:space:]]*(#.*)?\$/\1/" | sed "s/''/'/g" ;;
17
+ *) printf '%s' "$line" | sed -E 's/[[:space:]]+#.*$//' ;;
18
+ esac
19
+ }
20
+ fm() { awk '/^---[[:space:]]*$/{c++;next} c==1{print} c>=2{exit}' "$1"; }
21
+ extract_proof() {
22
+ local line
23
+ line="$(fm "$1" | awk '/^proof:/{print;exit}')"
24
+ [ -z "$line" ] && return 1
25
+ line="${line#proof:}"
26
+ unquote "$(printf '%s' "$line" | sed -E 's/^[[:space:]]+//')"
27
+ }
28
+ deliverables_block() {
29
+ fm "$1" | awk '/^deliverables:/{on=1;next} on && /^[a-zA-Z_-]+:/{exit} on{print}'
30
+ }
31
+
32
+ if [ "${1:-}" = "--self-test" ]; then
33
+ S="$(cd "$(dirname "$0")" && pwd)"; f=0
34
+ dir="$(mktemp -d)"; trap 'rm -rf "$dir"' EXIT
35
+ cat >"$dir/proj.md" <<'F'
36
+ ---
37
+ title: Project fixture
38
+ slug: proj
39
+ deliverables:
40
+ - item: "alpha row"
41
+ accept: "test -f a"
42
+ - item: "beta row"
43
+ accept: "test -f b"
44
+ proof: "test -f a && test -f b"
45
+ ---
46
+ F
47
+ cat >"$dir/gap.md" <<'F'
48
+ ---
49
+ title: Gap
50
+ slug: gap
51
+ deliverables:
52
+ - item: "alpha row"
53
+ proof: "true"
54
+ ---
55
+ F
56
+ out1="$(PROMISE_FILE="$dir/proj.md" "$S/do-project.sh" proj)" || { echo " FAIL first project exited $?"; exit 1; }
57
+ echo "$out1" >"$dir/once.json"
58
+ case "$out1" in *"\"slug:proj\""*) printf ' ok parent carries slug:proj\n' ;;
59
+ *) printf ' FAIL parent missing slug:proj: %s\n' "$out1"; f=1 ;; esac
60
+ case "$out1" in *"accept: test -f a"*) printf ' ok child carries accept: verbatim\n' ;;
61
+ *) printf ' FAIL child missing accept: %s\n' "$out1"; f=1 ;; esac
62
+ case "$out1" in *"\"new_rows\":3"*) printf ' ok first project inserts 3 rows\n' ;;
63
+ *) printf ' FAIL first new_rows: %s\n' "$out1"; f=1 ;; esac
64
+ out2="$(PROMISE_FILE="$dir/proj.md" "$S/do-project.sh" proj --existing "$dir/once.json")" || { echo " FAIL second project exited $?"; f=1; out2=""; }
65
+ case "$out2" in *"\"new_rows\":0"*) printf ' ok second project new_rows=0\n' ;;
66
+ *) printf ' FAIL second diff not empty: %s\n' "$out2"; f=1 ;; esac
67
+ set +e
68
+ PROMISE_FILE="$dir/gap.md" "$S/do-project.sh" gap >/dev/null 2>&1; rc=$?
69
+ set -e
70
+ [ "$rc" -ne 0 ] && printf ' ok missing accept: refused (exit %s)\n' "$rc" \
71
+ || { printf ' FAIL missing accept: should refuse\n'; f=1; }
72
+ [ "$f" -eq 0 ] && echo "do-project: self-test green" || echo "do-project: self-test FAILED"
73
+ exit "$f"
74
+ fi
75
+
76
+ slug="${1:-}"
77
+ [ -z "$slug" ] && { echo "usage: do-project.sh <slug> [--existing prior.json] | --self-test" >&2; exit 4; }
78
+ shift
79
+ existing=""
80
+ while [ $# -gt 0 ]; do
81
+ case "$1" in
82
+ --existing) existing="${2:-}"; shift 2 ;;
83
+ *) echo "unknown flag $1" >&2; exit 4 ;;
84
+ esac
85
+ done
86
+
87
+ file="${PROMISE_FILE:-$ROOT/text/$slug.md}"
88
+ [ -f "$file" ] || { echo "do-project: no promise at ${file#"$ROOT"/}" >&2; exit 3; }
89
+
90
+ block="$(deliverables_block "$file")"
91
+ [ -z "$block" ] && { echo "do-project: no deliverables: in ${file#"$ROOT"/}" >&2; exit 1; }
92
+
93
+ items=""; accepts=""
94
+ while IFS= read -r raw; do
95
+ [ -z "$raw" ] && continue
96
+ val="$(unquote "$(printf '%s' "$raw" | sed -E 's/^[[:space:]]*-[[:space:]]*item:[[:space:]]*//')")"
97
+ items="${items}${val}
98
+ "
99
+ done <<EOF
100
+ $(printf '%s\n' "$block" | grep '^[[:space:]]*-[[:space:]]*item:' || true)
101
+ EOF
102
+ while IFS= read -r raw; do
103
+ [ -z "$raw" ] && continue
104
+ val="$(unquote "$(printf '%s' "$raw" | sed -E 's/^[[:space:]]*accept:[[:space:]]*//')")"
105
+ accepts="${accepts}${val}
106
+ "
107
+ done <<EOF
108
+ $(printf '%s\n' "$block" | grep '^[[:space:]]*accept:' || true)
109
+ EOF
110
+
111
+ n_items="$(printf '%s' "$items" | grep -c . || true)"
112
+ n_acc="$(printf '%s' "$accepts" | grep -c . || true)"
113
+ if [ "$n_items" -lt 1 ] || [ "$n_items" -ne "$n_acc" ]; then
114
+ echo "do-project: refuse — $n_items item(s) but $n_acc accept(s); every deliverable needs accept:" >&2
115
+ exit 1
116
+ fi
117
+
118
+ title="$(fm "$file" | awk '/^title:/{sub(/^title:[[:space:]]*/,"");print;exit}')"
119
+ title="$(unquote "$title")"
120
+ [ -z "$title" ] && title="$slug"
121
+ proof="$(extract_proof "$file" || true)"
122
+
123
+ pairf="$(mktemp)"
124
+ paste -d $'\t' <(printf '%s' "$items" | grep -v '^$') <(printf '%s' "$accepts" | grep -v '^$') >"$pairf"
125
+ python3 - "$slug" "$title" "$proof" "$existing" "$pairf" <<'PY'
126
+ import json, sys
127
+ slug, title, proof, existing_path, pairf = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]
128
+ pairs = [ln.rstrip("\n").split("\t", 1) for ln in open(pairf) if ln.strip()]
129
+ if not proof:
130
+ proof = " && ".join(p[1] for p in pairs)
131
+ prior = {"parent": None, "children": []}
132
+ if existing_path:
133
+ prior = json.load(open(existing_path))
134
+ prior_tags = (prior.get("parent") or {}).get("tags") or []
135
+ parent_exists = ("slug:%s" % slug) in prior_tags
136
+ prior_titles = {c.get("title") for c in (prior.get("children") or []) if c.get("title")}
137
+ parent = {
138
+ "title": title,
139
+ "tags": ["slug:%s" % slug],
140
+ "notes": "accept: %s" % proof,
141
+ "insert": not parent_exists,
142
+ "create": "tasks:create",
143
+ }
144
+ children, new_rows = [], 1 if parent["insert"] else 0
145
+ for item, accept in pairs:
146
+ ins = item not in prior_titles
147
+ children.append({
148
+ "title": item,
149
+ "notes": "accept: %s" % accept,
150
+ "insert": ins,
151
+ "steps": ["tasks:subtask", "tasks:notes"],
152
+ })
153
+ if ins:
154
+ new_rows += 1
155
+ print(json.dumps({"parent": parent, "children": children, "new_rows": new_rows}, separators=(",", ":")))
156
+ PY
157
+ rm -f "$pairf"