@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
@@ -22,44 +22,130 @@ GC_ONLY=false
22
22
  SETUP_ONLY=false
23
23
  SELF_TEST_LIFECYCLE=false
24
24
  SELF_TEST_KANBAN=false
25
+ SELF_TEST_DAEMON=false
26
+ STATUS_ONLY=false
27
+ DAEMON=false
25
28
 
26
29
  while [ "$#" -gt 0 ]; do
27
30
  case "$1" in
28
31
  --setup-only) SETUP_ONLY=true; shift ;;
32
+ --status) STATUS_ONLY=true; shift ;;
33
+ --daemon) DAEMON=true; shift ;;
29
34
  --max-cycles) MAX_CYCLES="$2"; shift 2 ;;
30
35
  --dry-run) DRY_RUN=true; shift ;;
31
36
  --gc) GC_ONLY=true; shift ;;
32
37
  --self-test-lifecycle) SELF_TEST_LIFECYCLE=true; shift ;;
33
38
  --self-test-kanban) SELF_TEST_KANBAN=true; shift ;;
39
+ --self-test-daemon) SELF_TEST_DAEMON=true; shift ;;
34
40
  -*) echo "unknown flag: $1" >&2; exit 1 ;;
35
41
  *) SLUG="$1"; shift ;;
36
42
  esac
37
43
  done
38
44
 
39
- # Standalone GC: sweep all worktrees whose do/<slug> branch is fully merged into
40
- # trunk (main; DO_BASE_REF overrides). Safe-only: `worktree remove` refuses
41
- # dirty, `branch -d` refuses unmerged. Unmerged/dirty worktrees are listed,
42
- # never touched.
45
+ # Standalone GC: sweep every worktree whose branch is fully merged into the
46
+ # RELEASE trunk. Safe-only: `worktree remove` refuses a dirty tree and
47
+ # `branch -d` refuses an unmerged branch, so live work is listed, never touched.
48
+ #
49
+ # TWO DIRECTORIES, not one. `.do-worktrees/<slug>` carries branch `do/<slug>`;
50
+ # `.claude/worktrees/<name>` carries whatever branch is checked out there, which
51
+ # is the door root CLAUDE.md sends every session through. Only the first was ever
52
+ # swept, and the canon said so out loud — "swept by nothing" — for as long as the
53
+ # second existed. Measured 2026-09-06: 36 worktrees, 24 of them fully landed,
54
+ # 6.4G, and the box at load 16 funding ONE concurrent gate because each one costs
55
+ # a tsserver. Sweeping 17 took load to 3.5.
56
+ #
57
+ # THE BASE IS `main`, DELIBERATELY, AND NOT `dev`. Branches are cut from dev and
58
+ # land into dev (§ the loop), so measuring against dev would delete a worktree
59
+ # the moment its work integrated — which is BEFORE the dev→main PR is reviewed.
60
+ # A reviewer asking for changes would find the branch already gone. A branch is
61
+ # finished when it is released, not when it is integrated. DO_BASE_REF overrides.
43
62
  if $GC_ONLY; then
44
- BASE="${DO_BASE_REF:-main}"
45
- git show-ref --verify --quiet "refs/heads/$BASE" \
46
- || BASE="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)"
63
+ BASE="${DO_GC_BASE:-${DO_BASE_REF_GC:-main}}"
64
+ # RESOLVE ANY COMMIT-ISH, AND NEVER SUBSTITUTE ONE SILENTLY. This used to be
65
+ # `show-ref --verify refs/heads/$BASE`, which accepts only a LOCAL branch — so
66
+ # `DO_GC_BASE=origin/main`, the obvious way to sweep against the real trunk,
67
+ # failed the check and fell back to the invoking branch. That is the branch the
68
+ # override exists to escape, and the run reported `base=main` as if it had
69
+ # worked. Measured 2026-09-07: local main sat 131 commits behind origin/main,
70
+ # so every landed branch read as "142 unmerged commit(s)" and the sweep hoarded
71
+ # 1.0 GB of finished worktrees. A base that cannot be resolved is now a REFUSAL,
72
+ # not a fallback — the whole point of --gc is that it never deletes live work,
73
+ # and measuring against the wrong ref is how it would.
74
+ if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null 2>&1; then
75
+ echo "[do-auto] gc: base '$BASE' does not resolve to a commit — refusing to sweep" >&2
76
+ echo "[do-auto] (a wrong base makes landed work look unmerged, and unlanded work look landed)" >&2
77
+ exit 2
78
+ fi
79
+ # Say what was measured against, so a STALE base is visible in the output
80
+ # rather than inferred from a surprising verdict. `main` 131 commits behind its
81
+ # upstream is not a wrong ref — it is a right ref pointing at the wrong commit,
82
+ # and nothing in the old output could tell those apart.
83
+ BASE_SHA="$(git rev-parse --short "${BASE}^{commit}")"
84
+ BASE_NOTE=""
85
+ if _gc_up="$(git rev-parse --abbrev-ref --symbolic-full-name "${BASE}@{upstream}" 2>/dev/null)"; then
86
+ _gc_behind="$(git rev-list --count "${BASE}..${_gc_up}" 2>/dev/null || echo 0)"
87
+ [ "${_gc_behind:-0}" -gt 0 ] && BASE_NOTE=" — STALE: ${_gc_behind} commit(s) behind ${_gc_up}"
88
+ fi
89
+ echo "[do-auto] gc: measuring against ${BASE} (${BASE_SHA})${BASE_NOTE}"
90
+ # Never sweep the branches the loop itself stands on, whatever their state.
91
+ GC_KEEP="${GC_KEEP:-main dev release}"
47
92
  removed=0; kept=0
48
- for wt in .do-worktrees/*/; do
49
- [ -d "$wt" ] || continue
50
- s=$(basename "$wt"); b="do/${s}"
51
- git show-ref --verify --quiet "refs/heads/$b" || { echo "[do-auto] gc: $wt has no branch $b — inspect manually"; kept=$((kept+1)); continue; }
52
- if [ "$(git rev-list --count "${BASE}..${b}" 2>/dev/null || echo 1)" -eq 0 ]; then
53
- if git worktree remove "$wt" 2>/dev/null && git branch -d "$b" 2>/dev/null; then
54
- echo "[do-auto] gc: removed merged worktree $wt + branch $b"; removed=$((removed+1))
55
- else
56
- echo "[do-auto] gc: $wt merged but dirty — inspect manually"; kept=$((kept+1))
57
- fi
93
+
94
+ # The predicate lives in lib/gc-finished.sh shared, so its red proof
95
+ # (gc-content-check.sh) drives the same code the sweep runs.
96
+ . "$(dirname "${BASH_SOURCE[0]}")/lib/gc-finished.sh"
97
+
98
+ _gc_one() { # <worktree-dir> <branch>
99
+ local wt="$1" b="$2"
100
+ case " $GC_KEEP " in *" $b "*) echo "[do-auto] gc: $wt is $b — infrastructure, kept"; kept=$((kept+1)); return ;; esac
101
+
102
+ if ! gc_finished "$BASE" "$b"; then
103
+ echo "[do-auto] gc: $b carries $(gc_carries "$BASE" "$b") path(s) $BASE does not have — kept"; kept=$((kept+1)); return
104
+ fi
105
+
106
+ # Finished. The only thing that still earns a keep is UNCOMMITTED work — that
107
+ # is content no merge has seen and no ref points at, so losing it loses it.
108
+ local real; real="$(gc_real_dirt "$wt")"
109
+ if [ -n "$real" ]; then
110
+ echo "[do-auto] gc: $b is in $BASE but its worktree has uncommitted work — kept:"
111
+ echo "$real" | sed "s/^/[do-auto] gc: /"
112
+ kept=$((kept+1)); return
113
+ fi
114
+
115
+ # --force gets past ephemeral dirt only: the check above already refused
116
+ # anything that was work. `branch -d` asks "is it merged into HEAD", which is
117
+ # the wrong question here (HEAD is usually main and $BASE is usually dev), so
118
+ # -D is correct once content identity is proven — and it is proven, above.
119
+ if git worktree remove --force "$wt" 2>/dev/null; then
120
+ git branch -D "$b" >/dev/null 2>&1 \
121
+ && echo "[do-auto] gc: removed $wt + branch $b (content already in $BASE)" \
122
+ || echo "[do-auto] gc: removed $wt (branch $b kept — could not delete ref)"
123
+ removed=$((removed+1))
58
124
  else
59
- echo "[do-auto] gc: $b has $(git rev-list --count "${BASE}..${b}") unmerged commit(s)kept"; kept=$((kept+1))
125
+ echo "[do-auto] gc: $b is in $BASE but its worktree would not remove inspect $wt"; kept=$((kept+1))
60
126
  fi
61
- done
62
- echo "[do-auto] gc done: removed=$removed kept=$kept"
127
+ }
128
+
129
+ # ENUMERATE FROM GIT, NOT FROM A GLOB. `for wt in .claude/worktrees/*/` is
130
+ # relative to the INVOKING tree, and a worktree contains no worktrees — so run
131
+ # from anywhere but main and the sweep matches nothing and reports success.
132
+ # Measured 2026-09-06 on the first run of this code: `removed=0 kept=0` with 19
133
+ # worktrees live. It is the same trap the canon already records for land.sh and
134
+ # release.sh ("Run MAIN's copy"), and a glob cannot be told apart from a clean
135
+ # sweep by its output. `git worktree list` answers with absolute paths from any
136
+ # tree, so there is no copy to run.
137
+ SELF="$(git rev-parse --show-toplevel 2>/dev/null)"
138
+ while IFS= read -r wt; do
139
+ [ -d "$wt" ] || continue
140
+ case "$wt" in *"/.do-worktrees/"*|*"/.claude/worktrees/"*) ;; *) continue ;; esac
141
+ # Never saw off the branch you are sitting on.
142
+ [ "$wt" = "$SELF" ] && { echo "[do-auto] gc: $wt is the invoking tree — kept"; kept=$((kept+1)); continue; }
143
+ b="$(git -C "$wt" branch --show-current 2>/dev/null)"
144
+ [ -n "$b" ] || { echo "[do-auto] gc: $wt is detached — kept"; kept=$((kept+1)); continue; }
145
+ _gc_one "$wt" "$b"
146
+ done < <(git worktree list --porcelain | sed -n 's/^worktree //p')
147
+
148
+ echo "[do-auto] gc done: removed=$removed kept=$kept (base=$BASE ${BASE_SHA}${BASE_NOTE})"
63
149
  exit 0
64
150
  fi
65
151
 
@@ -74,10 +160,21 @@ _remaining() {
74
160
  # "C2's collapsed into C1" is never mistaken for a real Status-block line.
75
161
  # `^[[:space:]]*` (zero-or-more, not one-or-more) so the --self-test-lifecycle
76
162
  # fixture (no leading indentation) still matches.
77
- local n; n=$(grep -cE '^[[:space:]]*- \[[ ~]\] C[0-9]+ —' "$TODO" 2>/dev/null) || true
163
+ local n; n=$(grep -cE '^[[:space:]]*- \[[ ~]\] \*{0,2}C[0-9]+\*{0,2} —' "$TODO" 2>/dev/null) || true
78
164
  echo "${n:-0}"
79
165
  }
80
166
 
167
+ # The PLAN cycle this iteration is about to build — the first open C<n> in the Status
168
+ # block, same lines _remaining counts. Every emission keys on this, never on the loop
169
+ # ordinal `$i`: `i` restarts at 0 on a resume, so a resumed run building C3 would emit
170
+ # cycle:1, and the projection's run key `do:<slug>:c1` would silently append C3's trace
171
+ # to C1's closed run (text/do-as-workflow.md). Falls back to the caller's ordinal when
172
+ # the todo carries no Status block.
173
+ _plan_cycle() {
174
+ local n; n=$(grep -oE '^[[:space:]]*- \[[ ~]\] \*{0,2}C[0-9]+\*{0,2} —' "$TODO" 2>/dev/null | grep -oE 'C[0-9]+' | head -1 | tr -d 'C') || true
175
+ echo "${n:-${1:-0}}"
176
+ }
177
+
81
178
  # IC3: keep the Status-block checkbox in sync with each cycle's own wave checkboxes —
82
179
  # a cycle is done iff its `## C<n>` section has zero remaining `- [ ] W` lines.
83
180
  # POSIX-portable (no gawk-only 3-arg match()) — works with BSD awk/sed (macOS) and GNU.
@@ -110,7 +207,7 @@ _sync_status() {
110
207
  if ! printf '%s\n' "$section" | grep -qE '^[[:space:]]*- \[ \] W'; then
111
208
  # No open wave line left in this cycle's section — flip the Status-block
112
209
  # line for this cid from [ ]/[~] to [x] if not already ticked.
113
- sed -i.bak -E "s/^([[:space:]]*- \\[)[ ~](\\] ${cid} —)/\\1x\\2/" "$todo" 2>/dev/null || true
210
+ sed -i.bak -E "s/^([[:space:]]*- \\[)[ ~](\\] \\*{0,2}${cid}\\*{0,2} —)/\\1x\\2/" "$todo" 2>/dev/null || true
114
211
  rm -f "${todo}.bak"
115
212
  fi
116
213
  done <<<"$cid_lines"
@@ -164,6 +261,128 @@ _write_sentinel() {
164
261
  "$pid" "$(_sentinel_port "$slug")" "$iteration" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$path"
165
262
  }
166
263
 
264
+ # --self-test-daemon: prove the liveness guard can go RED. A guard that only ever
265
+ # reports success is the bug it is meant to prevent (this one shipped as
266
+ # `ps aux | grep -c do-auto.sh`, which reported a loop that had already died).
267
+ # True when this conductor run looks like "the API was unreachable": it died fast
268
+ # AND its output carries a transport error. Pure so --self-test-daemon can prove
269
+ # both verdicts — the guard that burned 30 iterations shipped untested.
270
+ _netfail_run() {
271
+ local cond_secs="$1" logfile="$2"
272
+ [ "${cond_secs:-999}" -lt 90 ] || return 1
273
+ [ -f "$logfile" ] || return 1
274
+ tail -c 2000 "$logfile" 2>/dev/null \
275
+ | grep -qE 'Unable to connect to API|ENOTFOUND|ECONNREFUSED|getaddrinfo' || return 1
276
+ return 0
277
+ }
278
+
279
+ # The HEAD of the last commit that touched something OTHER than loop bookkeeping.
280
+ # Plain HEAD is not a progress signal: a conductor that dies instantly still gets a
281
+ # commit, because .do-conductor.out (the heartbeat log) and todo.md's timestamp are
282
+ # inside the worktree. On 2026-08-07 that turned a 2-iteration stall halt into a
283
+ # 30-iteration burn — 28 subprocesses, each dying on ENOTFOUND, each committing one
284
+ # line of error text that the stall guard scored as work.
285
+ _substantive_head() {
286
+ local wt="$1" sha
287
+ for sha in $(git -C "$wt" log --format=%H -200 2>/dev/null); do
288
+ if git -C "$wt" show --name-only --format= "$sha" 2>/dev/null \
289
+ | grep -qvE '^(\.do-|\.w[0-9]|todo\.md$|$)'; then
290
+ echo "$sha"; return 0
291
+ fi
292
+ done
293
+ echo ""
294
+ }
295
+
296
+ # Stalled ONLY if nothing moved on ANY axis we can observe. Cycle checkboxes are
297
+ # too coarse to be the sole signal: a wide cycle (blocks C4) commits real work for
298
+ # an hour — 97 registrations archived, three commits — while its cycle box stays
299
+ # open until W4 closes it, so a box-only guard halts a run that is plainly working.
300
+ # Returns 0 (true, stalled) only when the box count AND the branch head are both
301
+ # unchanged from the previous iteration.
302
+ _stall_check() {
303
+ local remaining="$1" prev_remaining="$2" head="$3" prev_head="$4"
304
+ [ "$remaining" -eq "$prev_remaining" ] || return 1
305
+ [ -n "$prev_head" ] && [ "$head" != "$prev_head" ] && return 1
306
+ return 0
307
+ }
308
+
309
+ if ${SELF_TEST_DAEMON:-false}; then
310
+ rc=0
311
+ _t=$(mktemp -d); _self="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
312
+ cd "$_t" || exit 1
313
+ mkdir -p text .do-worktrees
314
+
315
+ # 1. --status on a slug with no loop and open cycles must say NOT RUNNING (exit 3).
316
+ printf -- '- [ ] C1 — nothing\n' > text/ghost-todo.md
317
+ st=0; out=$(bash "$_self" ghost --status 2>&1) || st=$?
318
+ if [ "$st" -eq 3 ] && grep -q "NOT RUNNING" <<<"$out"; then
319
+ echo "[self-test-daemon] PASS: absent loop reports NOT RUNNING (exit 3)"
320
+ else
321
+ echo "[self-test-daemon] FAIL: absent loop reported '$out' (exit $st, wanted 3)" >&2; rc=1
322
+ fi
323
+
324
+ # 2. A loop that dies on startup must make --daemon exit NON-ZERO. This is the
325
+ # regression: `nohup setsid ...` died instantly and the launch read as success.
326
+ # No text/<slug>-todo.md → the child exits 1 → the owner file never appears.
327
+ st=0; out=$(DO_DAEMON_LOG="$_t/d.log" bash "$_self" nosuchplan --daemon 2>&1) || st=$?
328
+ if [ "$st" -ne 0 ] && grep -q "is NOT running" <<<"$out"; then
329
+ echo "[self-test-daemon] PASS: a dead launch fails loud (exit $st)"
330
+ else
331
+ echo "[self-test-daemon] FAIL: dead launch reported '$out' (exit $st, wanted non-zero)" >&2; rc=1
332
+ fi
333
+
334
+ # 3. A live owner file must read RUNNING — the guard is not merely always-red.
335
+ mkdir -p .do-worktrees/alive
336
+ printf '{"pid":%d,"slug":"alive","started_at":"now"}\n' $$ > .do-worktrees/alive/.do-loop-owner
337
+ printf -- '- [ ] C1 — nothing\n' > text/alive-todo.md
338
+ st=0; out=$(bash "$_self" alive --status 2>&1) || st=$?
339
+ if [ "$st" -eq 0 ] && grep -q "RUNNING" <<<"$out"; then
340
+ echo "[self-test-daemon] PASS: a live loop reports RUNNING (exit 0)"
341
+ else
342
+ echo "[self-test-daemon] FAIL: live loop reported '$out' (exit $st, wanted 0)" >&2; rc=1
343
+ fi
344
+
345
+ # 4. the stall decision itself: a committing cycle is NOT stalled.
346
+ if _stall_check 6 6 "sha1" "sha1"; then
347
+ echo "[self-test-daemon] PASS: no boxes + no commits = stalled"
348
+ else
349
+ echo "[self-test-daemon] FAIL: flat state should count as stalled" >&2; rc=1
350
+ fi
351
+ if _stall_check 6 6 "sha2" "sha1"; then
352
+ echo "[self-test-daemon] FAIL: a new commit must clear the stall" >&2; rc=1
353
+ else
354
+ echo "[self-test-daemon] PASS: same boxes but a new commit = progress"
355
+ fi
356
+ if _stall_check 5 6 "sha1" "sha1"; then
357
+ echo "[self-test-daemon] FAIL: a ticked cycle must clear the stall" >&2; rc=1
358
+ else
359
+ echo "[self-test-daemon] PASS: a closed cycle = progress"
360
+ fi
361
+
362
+ # 5. the connectivity verdict: fast death + transport error = unreachable API.
363
+ printf 'API Error: Unable to connect to API (ENOTFOUND)\n' > "$_t/net.log"
364
+ if _netfail_run 3 "$_t/net.log"; then
365
+ echo "[self-test-daemon] PASS: fast death + ENOTFOUND = unreachable"
366
+ else
367
+ echo "[self-test-daemon] FAIL: should have flagged an unreachable API" >&2; rc=1
368
+ fi
369
+ if _netfail_run 4000 "$_t/net.log"; then
370
+ echo "[self-test-daemon] FAIL: a long run is not a connectivity failure" >&2; rc=1
371
+ else
372
+ echo "[self-test-daemon] PASS: a long run is never scored unreachable"
373
+ fi
374
+ printf 'W4 verify failed: 3 tests red\n' > "$_t/work.log"
375
+ if _netfail_run 3 "$_t/work.log"; then
376
+ echo "[self-test-daemon] FAIL: a real build failure must not read as network" >&2; rc=1
377
+ else
378
+ echo "[self-test-daemon] PASS: a fast REAL failure is not a network failure"
379
+ fi
380
+
381
+ cd /; rm -rf "$_t"
382
+ [ "$rc" -eq 0 ] && echo "[self-test-daemon] all checks passed"
383
+ exit "$rc"
384
+ fi
385
+
167
386
  if $SELF_TEST_LIFECYCLE; then
168
387
  rc=0
169
388
  tmp=$(mktemp -d)
@@ -247,6 +466,31 @@ EOF
247
466
  else
248
467
  echo "[self-test-kanban] FAIL: count wrong before=$before after=$after (expected 1,0)" >&2; rc=1
249
468
  fi
469
+ # _plan_cycle: the run key must name the PLAN cycle, not the loop ordinal. C1 is
470
+ # already ticked here, so a resumed run (loop ordinal 1) building C2 must emit 2 —
471
+ # emitting 1 would append C2's trace to C1's closed run (text/do-as-workflow.md).
472
+ cat > "$TODO" <<'EOF'
473
+ ## Status
474
+
475
+ Batch 1
476
+ - [x] C1 — done state: done
477
+ - [x] W1 · W2 · W3 · W4
478
+ - [ ] C2 — next state: ready
479
+ - [ ] W1 · W2 · W3 · W4
480
+ EOF
481
+ pc=$(_plan_cycle 1)
482
+ if [ "$pc" = "2" ]; then
483
+ echo "[self-test-kanban] PASS: _plan_cycle resolves the open cycle (2), not the loop ordinal (1)"
484
+ else
485
+ echo "[self-test-kanban] FAIL: _plan_cycle=$pc (expected 2)" >&2; rc=1
486
+ fi
487
+ : > "$TODO" # no Status block at all → fall back to the caller's ordinal
488
+ pc=$(_plan_cycle 7)
489
+ if [ "$pc" = "7" ]; then
490
+ echo "[self-test-kanban] PASS: _plan_cycle falls back to the ordinal with no Status block"
491
+ else
492
+ echo "[self-test-kanban] FAIL: _plan_cycle fallback=$pc (expected 7)" >&2; rc=1
493
+ fi
250
494
  rm -rf "$tmp"
251
495
  [ "$rc" -eq 0 ] && echo "[self-test-kanban] all checks passed"
252
496
  exit "$rc"
@@ -260,6 +504,76 @@ if ! printf '%s' "$SLUG" | grep -qE '^[a-zA-Z0-9][a-zA-Z0-9_-]*$'; then
260
504
  echo "[do-auto] unsafe slug (must be alphanumeric + hyphens/underscores): $SLUG" >&2; exit 1
261
505
  fi
262
506
 
507
+ # ── Liveness is READ, never inferred ──────────────────────────────────────────
508
+ # The loop owns `.do-loop-owner` for its whole life (pid = this script's own $$,
509
+ # removed by an EXIT trap). The older `.do-loop-running` sentinel tracks the
510
+ # CONDUCTOR of the current iteration, so it is legitimately absent between
511
+ # iterations — reading it to answer "is the loop up?" reports a false DEAD.
512
+ #
513
+ # WHY THIS EXISTS: a launch was reported as running when it had died instantly
514
+ # (`nohup setsid ...` — setsid does not exist on macOS). The check used was
515
+ # `ps aux | grep -c do-auto.sh`, which cannot go red: it counts its own shell,
516
+ # a stale match, or nothing, and every outcome reads as plausible. A check that
517
+ # cannot fail is not a check. Liveness now has exactly one answer, and the only
518
+ # supported way to start a detached loop (--daemon) refuses to return success
519
+ # until it has SEEN that answer say RUNNING.
520
+ _owner_file() { echo ".do-worktrees/${1}/.do-loop-owner"; }
521
+
522
+ _loop_pid() {
523
+ local f; f=$(_owner_file "$1")
524
+ [ -f "$f" ] || return 1
525
+ local pid; pid=$(jq -r '.pid // empty' "$f" 2>/dev/null || true)
526
+ [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null || return 1
527
+ echo "$pid"
528
+ }
529
+
530
+ if $STATUS_ONLY; then
531
+ [ -n "$SLUG" ] || { echo "usage: do-auto.sh <slug> --status" >&2; exit 1; }
532
+ TODO="text/${SLUG}-todo.md"
533
+ wt=".do-worktrees/${SLUG}"
534
+ [ -f "$wt/text/${SLUG}-todo.md" ] && TODO="$wt/text/${SLUG}-todo.md"
535
+ rem=$(_remaining)
536
+ if pid=$(_loop_pid "$SLUG"); then
537
+ f=$(_owner_file "$SLUG")
538
+ echo "[do-auto] RUNNING — slug=$SLUG pid=$pid started=$(jq -r '.started_at // "?"' "$f" 2>/dev/null) cycles-remaining=$rem"
539
+ exit 0
540
+ fi
541
+ if [ "$rem" -eq 0 ] && [ -f "$TODO" ]; then
542
+ echo "[do-auto] COMPLETE — slug=$SLUG no open cycles"
543
+ exit 4
544
+ fi
545
+ echo "[do-auto] NOT RUNNING — slug=$SLUG cycles-remaining=$rem"
546
+ exit 3
547
+ fi
548
+
549
+ if $DAEMON; then
550
+ [ -n "$SLUG" ] || { echo "usage: do-auto.sh <slug> --daemon" >&2; exit 1; }
551
+ if pid=$(_loop_pid "$SLUG"); then
552
+ echo "[do-auto] already running for $SLUG (pid $pid) — not starting a second loop" >&2
553
+ exit 0
554
+ fi
555
+ log="${DO_DAEMON_LOG:-.do-worktrees/${SLUG}.loop.log}"
556
+ mkdir -p "$(dirname "$log")" 2>/dev/null || true
557
+ # Portable detach: nohup + background + disown. NO setsid — it is Linux-only and
558
+ # its absence is precisely the silent death this guard exists to catch.
559
+ nohup "$0" "$SLUG" ${MAX_CYCLES:+--max-cycles "$MAX_CYCLES"} > "$log" 2>&1 < /dev/null &
560
+ child=$!
561
+ disown "$child" 2>/dev/null || true
562
+ # Do not trust the fork. Wait until the loop PUBLISHES liveness, or fail loud.
563
+ for _ in $(seq 1 60); do
564
+ if _loop_pid "$SLUG" >/dev/null; then
565
+ echo "[do-auto] daemon up — slug=$SLUG pid=$(_loop_pid "$SLUG") log=$log"
566
+ echo "[do-auto] check: bash .claude/scripts/do-auto.sh $SLUG --status"
567
+ exit 0
568
+ fi
569
+ kill -0 "$child" 2>/dev/null || break
570
+ sleep 1
571
+ done
572
+ echo "[do-auto] FATAL: daemon did not come up for $SLUG — the loop is NOT running." >&2
573
+ echo "[do-auto] log tail: $(tail -c 600 "$log" 2>/dev/null | tr '\n' ' ')" >&2
574
+ exit 1
575
+ fi
576
+
263
577
  # Recursion guard: the conductor subprocess runs `/do <slug> --next-cycle` inside
264
578
  # the worktree. A buggy conductor may call do-auto.sh again despite the --next-cycle
265
579
  # flag. If our JSON sentinel exists AND its pid is alive, we're already inside a
@@ -314,7 +628,14 @@ CONDUCTOR_EFFORT="${DO_CONDUCTOR_EFFORT:-medium}"
314
628
  # NO change, with an absolute DO_HARD_CAP backstop against a pathological write-loop.
315
629
  # DO_CONDUCTOR_TIMEOUT is retained as the default hard-cap for back-compat.
316
630
  CONDUCTOR_TIMEOUT="${DO_CONDUCTOR_TIMEOUT:-2400}"
317
- IDLE_SECS="${DO_IDLE_SECS:-600}" # 10 min of total silence = hung
631
+ # 60 min of TOTAL silence = hung. Not 10, and not 30 — both were measured too
632
+ # tight on blocks C4 (killed at 603s, then at 1922s, both times mid-real-work): an apex W2 (Opus/Fable deciding a wide
633
+ # cut — blocks C4 reasons over 376 registrations) legitimately spends 10-25 min
634
+ # inside a single Agent spawn, writing nothing and printing nothing. At 600s the
635
+ # guard killed a working conductor every iteration and the run could never finish
636
+ # that cycle. HARD_CAP (3x CONDUCTOR_TIMEOUT, default 7200s) remains the backstop
637
+ # against a genuinely wedged conductor, so raising this loses no real protection.
638
+ IDLE_SECS="${DO_IDLE_SECS:-3600}"
318
639
  HARD_CAP="${DO_HARD_CAP:-$(( CONDUCTOR_TIMEOUT > 0 ? CONDUCTOR_TIMEOUT * 3 : 7200 ))}" # absolute backstop
319
640
  POLL_SECS="${DO_POLL_SECS:-30}"
320
641
 
@@ -346,12 +667,33 @@ POLL_SECS="${DO_POLL_SECS:-30}"
346
667
  # branch would otherwise seed every new do/<slug> with that branch's commits
347
668
  # (plans cross-contaminating across concurrent sessions, 2026-07-21).
348
669
  # DO_BASE_REF overrides for a deliberate stack on another branch.
349
- BASE="${DO_BASE_REF:-main}"
670
+ # `dev` since 2026-09-06: it is the integration branch branches land into, so
671
+ # cutting from main would start every cycle missing what dev already holds.
672
+ BASE="${DO_BASE_REF:-dev}"
350
673
  git show-ref --verify --quiet "refs/heads/$BASE" \
351
674
  || BASE="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)"
352
675
  BR="do/${SLUG}"
353
676
  WT=".do-worktrees/${SLUG}"
354
677
 
678
+ # Publish liveness the moment the worktree path is known, and retract it on ANY
679
+ # exit (clean, halt, kill, crash). This file — not `ps | grep` — is the single
680
+ # answer to "is the loop running?", and --daemon blocks until it appears.
681
+ _OWNER="$WT/.do-loop-owner"
682
+ _publish_owner() {
683
+ mkdir -p "$WT" 2>/dev/null || true
684
+ printf '{"pid":%d,"slug":"%s","started_at":"%s"}\n' \
685
+ "$$" "$SLUG" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$_OWNER" 2>/dev/null || true
686
+ }
687
+ _retract_owner() {
688
+ [ -f "$_OWNER" ] || return 0
689
+ local own; own=$(jq -r '.pid // empty' "$_OWNER" 2>/dev/null || true)
690
+ [ "$own" = "$$" ] && rm -f "$_OWNER"
691
+ }
692
+ if ! $SETUP_ONLY && ! $DRY_RUN; then
693
+ _publish_owner
694
+ trap _retract_owner EXIT INT TERM
695
+ fi
696
+
355
697
  # Symlink node_modules + built SDK dist into the worktree. A git worktree omits
356
698
  # gitignored dirs (node_modules), so without this a worktree can't typecheck, run
357
699
  # vitest, or build — the kill-switch would fail for lack of deps, not lack of code.
@@ -360,8 +702,33 @@ WT=".do-worktrees/${SLUG}"
360
702
  _link_deps() {
361
703
  local wt="$1" root nm
362
704
  root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0
705
+ # pay/backend + channels are here because one.ie/web's OWN vitest suite reaches
706
+ # across into them (tests/e2e/wallets-refined.test.ts imports
707
+ # pay/backend/src/protocol, tests/unit/chat-block-tool-filter.test.ts imports
708
+ # channels/src/agents/builder). Without their node_modules those two suites fail
709
+ # to COLLECT — zero tests run, vitest exits 1 — so `bun run verify` in a worktree
710
+ # went red for a missing dep rather than a real fault (measured 2026-08-04).
711
+ # .env belongs in this list for exactly the reason node_modules does: it is
712
+ # gitignored, so a worktree never gets one, and the suites that need credentials
713
+ # then fail for lack of a KEY rather than lack of correct code. Measured
714
+ # 2026-08-31: two independent W4 verifiers both reported
715
+ # `fixture write failed (status=0 error=no_gateway_key)` across 4-7 real-TypeDB
716
+ # test files and had to hand-classify them as environmental — noise that makes a
717
+ # verifier argue with its own gate. Symlinked, never copied: one file of truth,
718
+ # and a rotated key is picked up by every live worktree at once.
719
+ #
720
+ # The credential file honours ONE_ENV_FILE (DO_ENV_FILE is the accepted
721
+ # alias), like every other credential-reading script in the harness —
722
+ # factory-repo.sh --check-env-indirection is the gate. A RELATIVE
723
+ # ONE_ENV_FILE is linked at the same relative path inside the worktree; an
724
+ # ABSOLUTE one is reachable from anywhere already and needs no link.
725
+ local env_file="${ONE_ENV_FILE:-${DO_ENV_FILE:-one.ie/web/.env}}"
726
+ local env_links="pay/backend/.env channels/.env"
727
+ case "$env_file" in /*) ;; *) env_links="$env_file $env_links" ;; esac
363
728
  for nm in node_modules one.ie/web/node_modules packages/node_modules \
364
- packages/sdk/dist packages/sdk/node_modules; do
729
+ packages/sdk/dist packages/sdk/node_modules \
730
+ pay/backend/node_modules channels/node_modules \
731
+ $env_links; do
365
732
  if [ -e "$root/$nm" ] && [ ! -e "$wt/$nm" ]; then
366
733
  mkdir -p "$wt/$(dirname "$nm")"
367
734
  ln -s "$root/$nm" "$wt/$nm" 2>/dev/null && echo "[do-auto] linked $nm" || true
@@ -369,18 +736,80 @@ _link_deps() {
369
736
  done
370
737
  }
371
738
 
739
+ # Is $WT a REAL linked worktree, or just a directory that happens to sit inside
740
+ # the repo? `rev-parse --git-dir` cannot tell you: run from any plain directory
741
+ # under the repo root it walks UP, finds the main tree's .git, and exits 0 —
742
+ # verified 2026-08-02, `mkdir .do-worktrees/x && git -C .do-worktrees/x
743
+ # rev-parse --git-dir` prints the MAIN /Users/toc/Server/one-ie/.git.
744
+ #
745
+ # That is not academic. do-orchestrate.sh used to `mkdir -p` the worktree path so
746
+ # it could redirect a log into it BEFORE calling do-auto; this probe then said
747
+ # "resuming worktree", `git worktree add` never ran, and every later
748
+ # `git -C "$WT" commit` targeted the MAIN TREE'S HEAD — where _safe_stage
749
+ # correctly refuses to stage. Orchestrated cycles committed nothing, silently.
750
+ #
751
+ # --show-toplevel is the discriminator: from a real worktree it returns that
752
+ # worktree's own root; from a plain subdirectory it returns the main tree's root.
753
+ # Compare it against the resolved path and the two cases separate cleanly.
754
+ _is_worktree() {
755
+ local wt="$1" abs top
756
+ abs="$(cd "$wt" 2>/dev/null && pwd -P)" || return 1
757
+ [ -n "$abs" ] || return 1
758
+ top="$(git -C "$wt" rev-parse --show-toplevel 2>/dev/null)" || return 1
759
+ [ "$top" = "$abs" ]
760
+ }
761
+
372
762
  _setup_worktree() {
373
763
  # Idempotent: reuse an existing worktree (resume), else attach one to an
374
764
  # existing branch (prior halt), else create branch+worktree fresh from HEAD.
375
- if git -C "$WT" rev-parse --git-dir >/dev/null 2>&1; then
765
+ #
766
+ # A pre-existing PLAIN directory at $WT is fatal, not recoverable: `git
767
+ # worktree add` refuses a non-empty path, and silently proceeding is the bug
768
+ # documented above. Empty is fine — git accepts an existing empty dir — so
769
+ # clear that case and fail loudly on anything else.
770
+ if [ -d "$WT" ] && ! _is_worktree "$WT"; then
771
+ # OUR OWN liveness file is not foreign content. _publish_owner() runs long
772
+ # before this (it fires the moment $WT is known, so --daemon can block on
773
+ # it) and it `mkdir -p "$WT"` + writes .do-loop-owner. On a slug whose
774
+ # worktree does not exist yet, that left a plain directory holding exactly
775
+ # one file — ours — so the rmdir below failed and every NEW slug FATALed
776
+ # here. The file is deleted again on exit, so the directory read as empty
777
+ # to anyone who looked afterwards, which is what made this hard to see.
778
+ # Drop it, then re-publish after `git worktree add` succeeds.
779
+ rm -f "$WT/.do-loop-owner"
780
+ rmdir "$WT" 2>/dev/null || {
781
+ echo "[do-auto] FATAL: $WT exists but is not a git worktree, and is not empty." >&2
782
+ echo "[do-auto] Commits there would land on the MAIN tree's HEAD. Refusing." >&2
783
+ echo "[do-auto] Inspect it, then: rm -rf '$WT' && git worktree prune" >&2
784
+ return 1
785
+ }
786
+ echo "[do-auto] cleared empty non-worktree dir at $WT"
787
+ fi
788
+
789
+ if _is_worktree "$WT"; then
376
790
  echo "[do-auto] resuming worktree $WT on $BR"
377
- elif git show-ref --verify --quiet "refs/heads/$BR"; then
378
- echo "[do-auto] re-attaching worktree $WT to existing branch $BR"
379
- git worktree add "$WT" "$BR" >/dev/null
380
791
  else
381
- echo "[do-auto] creating worktree $WT on new branch $BR (from $BASE)"
382
- git worktree add -b "$BR" "$WT" "$BASE" >/dev/null
792
+ # Both add-paths below fail with "already registered" if a previous run left
793
+ # a stale entry in .git/worktrees/<slug> the directory deleted by hand but
794
+ # never deregistered. prune drops ONLY registrations whose working dir is
795
+ # gone, so it can never touch a live worktree; do-orchestrate.sh:178 already
796
+ # relies on that. Needed here because the clear-empty-dir branch above now
797
+ # routes more cases into `git worktree add` than the old probe ever did.
798
+ git worktree prune 2>/dev/null || true
799
+
800
+ if git show-ref --verify --quiet "refs/heads/$BR"; then
801
+ echo "[do-auto] re-attaching worktree $WT to existing branch $BR"
802
+ git worktree add "$WT" "$BR" >/dev/null
803
+ else
804
+ echo "[do-auto] creating worktree $WT on new branch $BR (from $BASE)"
805
+ git worktree add -b "$BR" "$WT" "$BASE" >/dev/null
806
+ fi
383
807
  fi
808
+
809
+ # Re-publish liveness: the clear-empty-dir branch above deletes our owner file
810
+ # so `git worktree add` sees an empty path. Without this, a fresh slug runs
811
+ # with no liveness file and `--status` reports it dead while it is building.
812
+ _publish_owner
384
813
  _link_deps "$WT"
385
814
 
386
815
  # Carry the plan's own artifacts into the worktree. The spine walk may have
@@ -496,6 +925,12 @@ if ! $DRY_RUN; then
496
925
  if [ ! -f "$WT/.task-created" ]; then
497
926
  _title=$(grep -m1 '^title:' "$TODO" 2>/dev/null | sed 's/^title:[[:space:]]*//' | tr -d '"' || true)
498
927
  bash "$(dirname "$0")/do-signal.sh" --task-create "$SLUG" "${_title:-$SLUG}" "tags=do:cycle" >/dev/null 2>&1 || true
928
+ # ...and one task PER CYCLE, so the substrate holds the whole plan rather than
929
+ # a single row standing in for it. Measured 2026-09-01: lifecycle carries 10
930
+ # cycles and 13 deliverables; exactly ONE task reached the substrate, which is
931
+ # why `signal_tasks_cycle` had zero callers (the tasks it sets status on did
932
+ # not exist) and why do-next.sh re-derives readiness from markdown checkboxes.
933
+ bash "$(dirname "$0")/do-signal.sh" --task-plan "$SLUG" >/dev/null 2>&1 || true
499
934
  touch "$WT/.task-created"
500
935
  fi
501
936
  else
@@ -514,8 +949,16 @@ _tier() {
514
949
  }
515
950
 
516
951
  # Emit a world:do-event signal at a cycle boundary. Fire-and-forget — never blocks the loop.
952
+ # stderr is NOT swallowed: do-signal's dark-cycle guard writes there, and a worktree
953
+ # that emits nothing must say so rather than looking like a cycle that never ran.
517
954
  _signal() {
518
- bash "$(dirname "$0")/do-signal.sh" "$@" 2>/dev/null || true
955
+ bash "$(dirname "$0")/do-signal.sh" "$@" || true
956
+ }
957
+
958
+ # Which wave checkboxes are ticked right now — the same `- [x] W` lines _sync_status
959
+ # reads. Used as the wave-trace fallback when the conductor appended no census lines.
960
+ _waves_ticked() {
961
+ grep -oE '^[[:space:]]*- \[x\] W[0-9]' "$1" 2>/dev/null | grep -oE 'W[0-9]' | sort -u | tr '\n' ' ' || true
519
962
  }
520
963
 
521
964
  _plan_done() {
@@ -584,7 +1027,7 @@ _merge_digest() {
584
1027
  # cycle commits. Written to trunk's root (.do-digest.md) where the human stands.
585
1028
  local digest=".do-digest.md"
586
1029
  local cycles shortstat trust composite open mb behind agents
587
- cycles=$(grep -cE '\[[xX]\] C[0-9]+' "$TODO" 2>/dev/null || true); cycles=${cycles:-0}
1030
+ cycles=$(grep -cE '\[[xX]\] \*{0,2}C[0-9]+' "$TODO" 2>/dev/null || true); cycles=${cycles:-0}
588
1031
  # Diff from the MERGE-BASE, not BASE..BR. A two-dot BASE..BR diff conflates
589
1032
  # "what this branch added" with "what trunk advanced while we built" (the
590
1033
  # latter shows as spurious reversions), inflating the digest. merge-base..BR
@@ -667,25 +1110,45 @@ while [ "$i" -lt "$MAX_CYCLES" ]; do
667
1110
 
668
1111
  # Stall guard: a cycle that ticks no box means the subprocess halted/errored.
669
1112
  # Retrying the identical state forever just burns subprocesses — halt after 2 stalls.
670
- if [ "$remaining" -eq "$prev_remaining" ]; then
1113
+ # Connectivity abort: a conductor that dies in seconds with a transport error
1114
+ # built nothing and will keep building nothing until the network returns. Burning
1115
+ # the whole --max-cycles budget on it (30 dead subprocesses, measured 2026-08-07)
1116
+ # teaches nothing and buries the real state. Three in a row = stop and say why.
1117
+ if _netfail_run "${_cond_secs:-999}" "$WT/.do-conductor.out"; then
1118
+ netfail=$((${netfail:-0} + 1))
1119
+ if [ "$netfail" -ge 3 ]; then
1120
+ echo "[do-auto] FATAL: the conductor failed to reach the API 3 times in a row (last run ${_cond_secs}s)." >&2
1121
+ echo "[do-auto] This is a connectivity problem, not a plan problem — nothing was built." >&2
1122
+ echo "[do-auto] Check the network, then re-run: bash .claude/scripts/do-auto.sh $SLUG --daemon" >&2
1123
+ echo "[do-auto] WIP preserved on branch $BR (worktree $WT)." >&2
1124
+ _signal halt "$SLUG" "$(_tier)" "$(_plan_cycle "$i")" "reason=api-unreachable" 2>/dev/null || true
1125
+ exit 1
1126
+ fi
1127
+ else
1128
+ netfail=0
1129
+ fi
1130
+
1131
+ _cur_head=$(_substantive_head "$WT")
1132
+ if _stall_check "$remaining" "$prev_remaining" "$_cur_head" "${prev_head:-}"; then
671
1133
  stall=$((stall + 1))
672
1134
  if [ "$stall" -ge 2 ]; then
673
1135
  echo "[do-auto] no progress for 2 iterations (${remaining} cycle(s) stuck) — halting." >&2
674
- echo "[do-auto] The last cycle ticked no checkbox. Inspect $TODO, fix the blocker, then re-run /do $SLUG." >&2
1136
+ echo "[do-auto] The last cycle ticked no checkbox AND committed nothing. Inspect $TODO, fix the blocker, then re-run /do $SLUG." >&2
675
1137
  echo "[do-auto] WIP preserved on branch $BR (worktree $WT)." >&2
676
- _signal halt "$SLUG" "$(_tier)" "$i" "reason=stall"
1138
+ _signal halt "$SLUG" "$(_tier)" "$(_plan_cycle "$i")" "reason=stall"
677
1139
  exit 1
678
1140
  fi
679
1141
  else
680
1142
  stall=0
681
1143
  fi
1144
+ prev_head="$_cur_head"
682
1145
  prev_remaining=$remaining
683
1146
 
684
1147
  trust=$(_trust)
685
1148
  if [ "$trust" = "cautious" ]; then
686
1149
  echo "[do-auto] trust=cautious — halting. Fix the issue, then re-run /do $SLUG."
687
1150
  echo "[do-auto] WIP preserved on branch $BR (worktree $WT)."
688
- _signal halt "$SLUG" "$(_tier)" "$i" "reason=trust-cautious"
1151
+ _signal halt "$SLUG" "$(_tier)" "$(_plan_cycle "$i")" "reason=trust-cautious"
689
1152
  exit 1
690
1153
  fi
691
1154
 
@@ -720,11 +1183,19 @@ while [ "$i" -lt "$MAX_CYCLES" ]; do
720
1183
  fi
721
1184
 
722
1185
  # Emit cycle-open signal (world:do-event type:aim) before the conductor starts.
723
- _signal aim "$SLUG" "$(_tier)" "$i"
1186
+ # One plan-cycle number for EVERY emission this iteration — resolved BEFORE the
1187
+ # conductor runs, since the conductor ticks the cycle closed and the post-wait
1188
+ # emissions would otherwise resolve to the NEXT cycle.
1189
+ _cyc=$(_plan_cycle "$i")
1190
+ _signal aim "$SLUG" "$(_tier)" "$_cyc"
724
1191
  # Lease the plan's substrate task (tasks-do C1/C5 — claim = lease, same verb
725
- # do-fleet.sh checks before spawning a worktree). Fire-and-forget: a missing
726
- # task (never seeded) or a claim race never blocks the loop.
727
- bash "$(dirname "$0")/do-signal.sh" --task-claim "$SLUG" >/dev/null 2>&1 || true
1192
+ # do-fleet.sh checks before spawning a worktree). Asserted: a TypeDB-down or
1193
+ # gateway miss is logged (never disguised as ok) and never aborts the cycle.
1194
+ _claim_rc=0
1195
+ bash "$(dirname "$0")/do-signal.sh" --task-claim "$SLUG" >/dev/null || _claim_rc=$?
1196
+ if [ "$_claim_rc" -ne 0 ]; then
1197
+ echo "board-write: claim FAILED (exit ${_claim_rc})" >&2
1198
+ fi
728
1199
 
729
1200
  # C6: Trail confidence hint — query KV trail before spawning the conductor.
730
1201
  # If confidence ≥ 0.8, write .do-trail-hint.json in the worktree so W1 can skip W2.
@@ -764,14 +1235,33 @@ while [ "$i" -lt "$MAX_CYCLES" ]; do
764
1235
  _iter_sha=$(git -C "$WT" rev-parse HEAD 2>/dev/null || echo "")
765
1236
  _iter_remaining="$remaining"
766
1237
 
1238
+ # Wave-trace baselines, captured BEFORE the conductor runs (text/do-as-workflow.md).
1239
+ _census_base=0
1240
+ [ -f "$WT/.do-census.jsonl" ] && _census_base=$(wc -l < "$WT/.do-census.jsonl" 2>/dev/null | tr -d ' ')
1241
+ _waves_base=$(_waves_ticked "$TODO")
1242
+
767
1243
  # Run the conductor in the background with a bash-native, PROGRESS-BASED watchdog
768
1244
  # (macOS has no `timeout`). It kills the conductor only after IDLE_SECS of worktree
769
1245
  # SILENCE (or the HARD_CAP absolute backstop) — never mid-edit on a slow-but-live
770
1246
  # cycle. Matches the claude process by its --next-cycle command line.
771
1247
  # Unset Claude Code session vars so the subprocess starts a fresh session instead
772
1248
  # of being blocked by the anti-nesting guard (CLAUDECODE/CLAUDE_CODE_CHILD_SESSION).
773
- ( cd "$WT" && unset CLAUDECODE CLAUDE_CODE_CHILD_SESSION CLAUDE_CODE_SESSION_ID AI_AGENT && claude --model "$CONDUCTOR_MODEL" --effort "$CONDUCTOR_EFFORT" --dangerously-skip-permissions -p "/do $SLUG --next-cycle${AUTO_FLAG}" ) &
1249
+ # CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0 wait indefinitely for the conductor's
1250
+ # background tasks. The CLI default terminates a -p run whose background work is
1251
+ # still going after 600s, which kills a LIVE conductor mid-decision: a wide W2
1252
+ # (blocks C4 reasons over 376 registrations) always exceeds it, so that cycle can
1253
+ # never close and every iteration burns identically until the stall guard halts.
1254
+ # Bounding the conductor is already do-auto's own job, and its guards are the ones
1255
+ # that can tell alive-but-slow from hung: IDLE_SECS (no worktree write) and
1256
+ # HARD_CAP. Deferring to a print-wait ceiling that cannot see the worktree trades a
1257
+ # real halt for a fake one.
1258
+ # The conductor's own output is a heartbeat: tee it into the worktree so the
1259
+ # find -newer probe below counts "the conductor is still talking" as progress.
1260
+ # Without this, the only progress signal is a file write — and a wide W2 spends
1261
+ # its whole life inside one Agent spawn, writing nothing.
1262
+ ( cd "$WT" && unset CLAUDECODE CLAUDE_CODE_CHILD_SESSION CLAUDE_CODE_SESSION_ID AI_AGENT && CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS="${DO_BG_WAIT_CEILING_MS:-0}" claude --model "$CONDUCTOR_MODEL" --effort "$CONDUCTOR_EFFORT" --dangerously-skip-permissions -p "/do $SLUG --next-cycle${AUTO_FLAG}" ) > >(tee -a "$WT/.do-conductor.out") 2>&1 &
774
1263
  cond_pid=$!
1264
+ _cond_t0=$(date +%s)
775
1265
  _write_sentinel "$WT/.do-loop-running" "$cond_pid" "$i" "$SLUG"
776
1266
  # Progress signal (F11): ANY file written under the worktree since the last poll —
777
1267
  # code edits, new files, commits, AND gitignored state (.w2-spec.json, .w1-cache/,
@@ -807,14 +1297,62 @@ while [ "$i" -lt "$MAX_CYCLES" ]; do
807
1297
  # more common case of "done but not exiting."
808
1298
  TODO="$WT/text/${SLUG}-todo.md" _reap_watcher "$WT" "$cond_pid" "$_iter_sha" "$_iter_remaining" &
809
1299
  reap_pid=$!
1300
+ # Board heartbeat (IC: the lease must outlive the cycle). `tasks:claim` stamps
1301
+ # `updated-at-ms`; a `picked` row older than CLAIM_LEASE_TTL_MS (45 min,
1302
+ # one.ie/web/src/lib/resolvers/tasks.ts:92) is STALE, and both `tasks:reap` (which
1303
+ # tasks-loop.sh runs FIRST on every pass) and `tasks:claim`'s own stale branch will
1304
+ # reopen it and hand the row to another worker. do-auto wrote exactly three board
1305
+ # events per plan — create/claim/link — with nothing between claim and link, while
1306
+ # p100 fleet duration is 79 min. So a live cycle crossed the TTL and its work could
1307
+ # be double-claimed, discovered only at merge. This ticker is what makes the lease
1308
+ # honest; the per-wave heartbeat below is the progress trace, not the keep-alive.
1309
+ # Default 900s = 3x margin inside the TTL. Fire-and-forget like every other signal.
1310
+ ( while kill -0 "$cond_pid" 2>/dev/null; do
1311
+ sleep "${DO_HEARTBEAT_SECS:-900}"
1312
+ kill -0 "$cond_pid" 2>/dev/null || break
1313
+ _hb_waves=$(_waves_ticked "$TODO" 2>/dev/null | tr '\n' ' ' | sed 's/ *$//')
1314
+ bash "$(dirname "$0")/do-signal.sh" --task-heartbeat "$SLUG" \
1315
+ "cycle ${_cyc} in flight — waves: ${_hb_waves:-none yet}" >/dev/null 2>&1 || true
1316
+ done ) &
1317
+ hb_pid=$!
810
1318
  wait "$cond_pid" 2>/dev/null || true
1319
+ _cond_secs=$(( $(date +%s) - ${_cond_t0:-$(date +%s)} ))
811
1320
  kill "$wd_pid" 2>/dev/null || true # conductor finished first — cancel the watchdog
812
1321
  kill "$reap_pid" 2>/dev/null || true # conductor finished first — cancel the reap watcher
1322
+ kill "$hb_pid" 2>/dev/null || true # conductor finished first — cancel the board heartbeat
813
1323
  # Kill any child do-auto.sh the conductor may have spawned despite --next-cycle.
814
1324
  pkill -f "do-auto.sh ${SLUG}" 2>/dev/null || true
815
1325
  sleep 1
816
1326
  rm -f "$WT/.do-loop-running" "${_wd_marker:-}" # marker cleanup (subshell's rm is skipped when the conductor finishes first)
817
1327
 
1328
+ # Wave trace (text/do-as-workflow.md deliverable 1) — one `wave` event per wave this
1329
+ # iteration ticked, in order, so the run record carries the wave-by-wave trace and a
1330
+ # W3.5 reloop shows as repeated w3/w4 entries. Drained in the FOREGROUND after the
1331
+ # wait: the watchdog subshell is killed the instant the conductor exits, which would
1332
+ # systematically drop the closing W4. The census is the primary source (it records
1333
+ # every agent settle, reloops included); it is an instruction to the conductor, not a
1334
+ # mechanical guarantee, so a cycle that appended none falls back to the wave
1335
+ # checkboxes it DID tick. Fire-and-forget, like every other _signal.
1336
+ _census_now=0
1337
+ [ -f "$WT/.do-census.jsonl" ] && _census_now=$(wc -l < "$WT/.do-census.jsonl" 2>/dev/null | tr -d ' ')
1338
+ _new_waves=""
1339
+ if [ "${_census_now:-0}" -gt "${_census_base:-0}" ]; then
1340
+ _new_waves=$(tail -n "+$((_census_base + 1))" "$WT/.do-census.jsonl" 2>/dev/null \
1341
+ | sed -n 's/.*"wave"[[:space:]]*:[[:space:]]*"\(W[0-9]\)".*/\1/p' || true)
1342
+ else
1343
+ for _w in $(_waves_ticked "$TODO"); do
1344
+ case " ${_waves_base} " in *" ${_w} "*) ;; *) _new_waves="${_new_waves} ${_w}" ;; esac
1345
+ done
1346
+ fi
1347
+ for _w in $_new_waves; do
1348
+ _signal wave "$SLUG" "$(_tier)" "$_cyc" "wave=${_w}"
1349
+ # Same wave close, second surface: a tasks:comment bumps `updated-at-ms`, so the
1350
+ # task sheet's timeline becomes the wave-by-wave trace a human watches at
1351
+ # /tasks/board — and each one re-arms the lease. Mirrors do-engine.js:427.
1352
+ bash "$(dirname "$0")/do-signal.sh" --task-heartbeat "$SLUG" \
1353
+ "cycle ${_cyc} · ${_w} close" >/dev/null 2>&1 || true
1354
+ done
1355
+
818
1356
  # Objective security gate (W4 bash-first, zero-LLM): before the cycle lands on
819
1357
  # the branch, secret-scan the worktree + audit deps. A scored security score can
820
1358
  # miss a leaked key or known-vuln dep; this is the deterministic backstop — a
@@ -823,7 +1361,7 @@ while [ "$i" -lt "$MAX_CYCLES" ]; do
823
1361
  if ! _secret_gate "$WT"; then
824
1362
  echo "[do-auto] secret-scan / bun audit found an issue — NOT committing cycle ${i}." >&2
825
1363
  echo "[do-auto] Inspect $WT, remove the secret/vuln, then re-run /do $SLUG. WIP preserved on $BR." >&2
826
- _signal halt "$SLUG" "$(_tier)" "$i" "reason=secret-gate"
1364
+ _signal halt "$SLUG" "$(_tier)" "$_cyc" "reason=secret-gate"
827
1365
  exit 1
828
1366
  fi
829
1367
 
@@ -857,9 +1395,9 @@ while [ "$i" -lt "$MAX_CYCLES" ]; do
857
1395
  # harvest credits these tag-nodes so tasks:mine ranks by proven work (text/reputation.md § A).
858
1396
  _worktags=$(python3 "$(dirname "$0")/do-rank.py" --worktags "$SLUG" 2>/dev/null || true)
859
1397
  if [ -n "$_composite" ]; then
860
- _signal learn "$SLUG" "$(_tier)" "$i" "composite=${_composite}" ${_shape:+"$_shape"} "$_model" ${_worktags:+"worktags=$_worktags"}
1398
+ _signal learn "$SLUG" "$(_tier)" "$_cyc" "composite=${_composite}" ${_shape:+"$_shape"} "$_model" ${_worktags:+"worktags=$_worktags"}
861
1399
  else
862
- _signal learn "$SLUG" "$(_tier)" "$i" ${_shape:+"$_shape"} "$_model" ${_worktags:+"worktags=$_worktags"}
1400
+ _signal learn "$SLUG" "$(_tier)" "$_cyc" ${_shape:+"$_shape"} "$_model" ${_worktags:+"worktags=$_worktags"}
863
1401
  fi
864
1402
  done
865
1403
 
@@ -868,7 +1406,11 @@ if $DRY_RUN; then exit 0; fi
868
1406
  if [ "$i" -ge "$MAX_CYCLES" ]; then
869
1407
  echo "[do-auto] hit --max-cycles $MAX_CYCLES — halting"
870
1408
  echo "[do-auto] WIP preserved on branch $BR (worktree $WT)."
871
- _signal halt "$SLUG" "$(_tier)" "$i" "reason=max-cycles"
1409
+ _signal halt "$SLUG" "$(_tier)" "$(_plan_cycle "$i")" "reason=max-cycles"
1410
+ # A halt still closes. --status timeout is the honest word: the lease stays
1411
+ # held and the task is NOT marked done, so the next run resumes rather than
1412
+ # re-opening finished work. Rule 1 has no exception for halts.
1413
+ bash "$(dirname "$0")/do-close.sh" "$SLUG" --status timeout || true
872
1414
  exit 1
873
1415
  fi
874
1416
 
@@ -881,8 +1423,16 @@ fi
881
1423
  _merge_digest
882
1424
  # Close the loop on the origin task (tasks-do C1): the handoff-completion
883
1425
  # record — provenance line + task-status:done — lands once, at PLAN close,
884
- # not per cycle (a plan's cycles all serve the same origin task).
885
- bash "$(dirname "$0")/do-signal.sh" --task-link "$SLUG" "docs=${SLUG}" >/dev/null 2>&1 || true
1426
+ # not per cycle (a plan's cycles all serve the same origin task). Asserted:
1427
+ # a failed link is logged with its exit, never disguised as ok; the plan
1428
+ # still completed its code.
1429
+ #
1430
+ # do-close.sh is the whole close, not just the link: the board write (through
1431
+ # this same slug door), the receipt check that gates it, the rubric dims, the
1432
+ # feedback pheromone and the promise settle — bounded, in ~5s, and whatever it
1433
+ # cannot close it FILES with the command that finishes it. It exits 0 by design
1434
+ # even with open legs; a close that can fail a plan is a close plans skip.
1435
+ bash "$(dirname "$0")/do-close.sh" "$SLUG" --composite "${_composite:-}" || true
886
1436
  echo ""
887
1437
  echo "[do-auto] ✓ plan complete on branch $BR — trunk ($BASE) untouched."
888
1438
  echo "[do-auto] read: .do-digest.md # what landed + gate verdict (reasoning was discarded)"