@oneie/claude 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/abm-strategist.md +67 -1
- package/agents/ads-meta.md +67 -1
- package/agents/analyst.md +67 -1
- package/agents/animator.md +108 -0
- package/agents/architect.md +269 -20
- package/agents/brand-guardian.md +67 -1
- package/agents/brand-strategist.md +67 -1
- package/agents/campaign-content.md +67 -1
- package/agents/campaign-email.md +67 -1
- package/agents/campaign-sms.md +67 -1
- package/agents/campaign-social.md +67 -1
- package/agents/cco.md +83 -2
- package/agents/ceo.md +108 -11
- package/agents/chairman.md +197 -0
- package/agents/cmo.md +82 -2
- package/agents/community-greeter.md +67 -1
- package/agents/community-moderator.md +67 -1
- package/agents/compliance.md +67 -1
- package/agents/copywriter.md +67 -1
- package/agents/creative-strategist.md +67 -1
- package/agents/cro.md +81 -1
- package/agents/cto.md +266 -28
- package/agents/customer-interviewer.md +67 -1
- package/agents/customer-researcher.md +67 -1
- package/agents/customer-success-manager.md +67 -1
- package/agents/customer-trainer.md +67 -1
- package/agents/cxo.md +82 -1
- package/agents/demand-creator.md +67 -1
- package/agents/demo-mover.md +67 -1
- package/agents/demo-specialist.md +67 -1
- package/agents/demo-thai-family-law.md +67 -1
- package/agents/designer.md +67 -1
- package/agents/discovery-caller.md +67 -1
- package/agents/doctor.md +269 -0
- package/agents/educate-coach.md +67 -1
- package/agents/elevate-tutor.md +67 -1
- package/agents/email-lifecycle-marketer.md +67 -1
- package/agents/engage-specialist.md +67 -1
- package/agents/events-coordinator.md +67 -1
- package/agents/foundation-builder.md +67 -1
- package/agents/funnel-architect.md +67 -1
- package/agents/gift-creator.md +67 -1
- package/agents/google-ads.md +67 -1
- package/agents/guide.md +67 -1
- package/agents/helpdesk-dispatcher.md +67 -1
- package/agents/hook-specialist.md +67 -1
- package/agents/identify-optimizer.md +67 -1
- package/agents/implementer.md +313 -45
- package/agents/incident-commander.md +67 -1
- package/agents/insights-lead.md +87 -1
- package/agents/journey-runner.md +67 -1
- package/agents/linkedin-ads.md +67 -1
- package/agents/live-sales-chat.md +67 -1
- package/agents/market-researcher.md +67 -1
- package/agents/media-buyer.md +67 -1
- package/agents/memory-keeper.md +195 -0
- package/agents/movers-customer-researcher.md +67 -1
- package/agents/movers-foundation-builder.md +67 -1
- package/agents/movers-market-researcher.md +67 -1
- package/agents/movers-pricing-strategist.md +67 -1
- package/agents/nurture-architect.md +67 -1
- package/agents/offer-architect.md +67 -1
- package/agents/onboarder.md +67 -1
- package/agents/onboarding-specialist.md +67 -1
- package/agents/operations-dashboard.md +87 -1
- package/agents/perf-engineer.md +333 -37
- package/agents/playbook-writer.md +67 -1
- package/agents/plg-strategist.md +67 -1
- package/agents/positioning-architect.md +67 -1
- package/agents/press-officer.md +67 -1
- package/agents/pricing-strategist.md +67 -1
- package/agents/privacy-officer.md +67 -1
- package/agents/referral-manager.md +67 -1
- package/agents/refine-analyst.md +67 -1
- package/agents/release-manager.md +446 -39
- package/agents/renewals-upsell-rep.md +67 -1
- package/agents/review-engineer.md +319 -45
- package/agents/rewards-steward.md +67 -1
- package/agents/sales-call-coach.md +67 -1
- package/agents/sales-closer.md +67 -1
- package/agents/security-auditor.md +343 -48
- package/agents/sell-closer.md +67 -1
- package/agents/share-amplifier.md +67 -1
- package/agents/social-media-manager.md +67 -1
- package/agents/storyteller.md +301 -0
- package/agents/strategist.md +67 -1
- package/agents/strategy-aligner.md +67 -1
- package/agents/support-agent.md +67 -1
- package/agents/tagger.md +327 -0
- package/agents/tech-writer.md +195 -22
- package/agents/test-engineer.md +398 -29
- package/agents/tiktok-ads.md +67 -1
- package/agents/tracking-engineer.md +67 -1
- package/agents/trailkeeper.md +181 -0
- package/agents/upsell-strategist.md +67 -1
- package/agents/voice.md +67 -1
- package/agents/w1-recon.md +1 -1
- package/agents/w2-decide.md +1 -1
- package/agents/w3-edit.md +8 -2
- package/agents/w4-verify.md +13 -0
- package/agents/workflow-optimiser.md +81 -1
- package/commands/close.md +916 -160
- package/commands/deploy.md +102 -724
- package/commands/do.md +58 -2
- package/commands/sweep.md +159 -0
- package/commands/tasks.md +222 -0
- package/hooks/scripts/dev-only.sh +135 -0
- package/hooks/scripts/git-add-guard.sh +37 -2
- package/hooks/scripts/session-start.sh +32 -4
- package/package.json +1 -1
- package/rules/scripts.md +85 -0
- package/scripts/CLAUDE.md +315 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agent-actor-parity.sh +129 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/chrome.mjs +18 -0
- package/scripts/close-metrics.sh +587 -0
- package/scripts/close-owner.sh +326 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-emit.sh +311 -0
- package/scripts/deploy-gate-check.sh +155 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy-schema-check.sh +58 -0
- package/scripts/deploy.sh +393 -243
- package/scripts/do-auto.sh +127 -26
- package/scripts/do-board.sh +429 -0
- package/scripts/do-close.sh +1184 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-fleet.sh +8 -2
- package/scripts/do-plan-json.mjs +110 -12
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +86 -10
- package/scripts/do-rank.py +200 -3
- package/scripts/do-reconcile.sh +73 -12
- package/scripts/do-signal.sh +101 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-w4-gates.sh +11 -1
- package/scripts/do-world-check.sh +153 -0
- package/scripts/download-stats.sh +172 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +18 -1
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +71 -0
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/flywheel-outcome.sh +63 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +9 -0
- package/scripts/gate-watchdog.sh +619 -0
- package/scripts/gc-content-check.sh +142 -0
- package/scripts/gh-traffic-capture.sh +153 -0
- package/scripts/govern-order-check.sh +202 -0
- package/scripts/governor-doors-check.sh +86 -5
- package/scripts/health.sh +448 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/incident.sh +212 -0
- package/scripts/land.sh +755 -45
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/livekit-ratchet.sh +18 -1
- package/scripts/machine-check.sh +1 -1
- package/scripts/memory-index-budget.sh +79 -0
- package/scripts/npm-downloads.sh +109 -0
- package/scripts/one-agents.mjs +204 -8
- package/scripts/one-resume.sh +31 -3
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/redirect-lint.sh +169 -0
- package/scripts/release.sh +40 -6
- package/scripts/resume-lost-sessions.sh +68 -0
- package/scripts/shoot-pages.mjs +140 -0
- package/scripts/signal-meta-backfill.ts +451 -0
- package/scripts/signal-watch.sh +63 -6
- package/scripts/speed-cache-check.sh +12 -2
- package/scripts/sweep.sh +426 -0
- package/scripts/task-titles-dump.ts +101 -0
- package/scripts/test-cached.sh +47 -10
- package/scripts/test-lanes.sh +14 -0
- package/scripts/thread-name-backfill.ts +215 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +155 -8
- package/scripts/typedb-flake-check.sh +3 -1
- package/scripts/urls-lint.sh +8 -0
- package/scripts/verify-board-doors.sh +80 -0
- package/scripts/verify-fast.sh +159 -6
- package/scripts/worktree-up.sh +21 -3
- package/skills/astro/SKILL.md +9 -3
- package/skills/astro/optimize-performance.md +3 -2
- package/skills/cloudflare/SKILL.md +3 -2
- package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
- package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
- package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
- package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
- package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
- package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
- package/skills/cloudflare-security-audit/HUNTING.md +251 -0
- package/skills/cloudflare-security-audit/LICENSE +21 -0
- package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
- package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
- package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
- package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
- package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
- package/skills/cloudflare-security-audit/SKILL.md +192 -0
- package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
- package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
- package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
- package/skills/cloudflare-security-audit/report-schema.json +461 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
- package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
- package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
- package/skills/deploy/REFERENCE.md +713 -0
- package/skills/deploy/SKILL.md +140 -0
- package/skills/fleet-audit/SKILL.md +58 -0
- package/skills/meeting/SKILL.md +220 -0
- package/skills/planning/SKILL.md +256 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/typedb/SKILL.md +7 -0
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/templates/template-agent.md +7 -1
- package/templates/template-feature.md +9 -0
- package/templates/template-todo.md +29 -0
package/scripts/do-auto.sh
CHANGED
|
@@ -42,30 +42,123 @@ while [ "$#" -gt 0 ]; do
|
|
|
42
42
|
esac
|
|
43
43
|
done
|
|
44
44
|
|
|
45
|
-
# Standalone GC: sweep
|
|
46
|
-
# trunk
|
|
47
|
-
#
|
|
48
|
-
#
|
|
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.
|
|
49
62
|
if $GC_ONLY; then
|
|
50
|
-
BASE="${
|
|
51
|
-
|
|
52
|
-
|
|
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}"
|
|
53
92
|
removed=0; kept=0
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
# A DRY RUN THAT DELETES IS NOT A DRY RUN. `--dry-run` is parsed by the same
|
|
116
|
+
# flag loop that serves the cycle path, where it is honoured in four places —
|
|
117
|
+
# but nothing here read it, so `--gc --dry-run` removed the worktree AND
|
|
118
|
+
# `branch -D`'d the ref while printing a line that reads like a preview.
|
|
119
|
+
# Measured 2026-09-10: it reported `removed=1 kept=2` and `feat/panel-resize`
|
|
120
|
+
# and its worktree were actually gone. The content was safely in the trunk
|
|
121
|
+
# that time; the flag composes in the usage string, so next time it need not
|
|
122
|
+
# be. Report exactly what it WOULD do, then return without touching the disk.
|
|
123
|
+
if $DRY_RUN; then
|
|
124
|
+
echo "[do-auto] gc: WOULD remove $wt + branch $b (content already in $BASE) — dry run, nothing deleted"
|
|
125
|
+
removed=$((removed+1)); return
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# --force gets past ephemeral dirt only: the check above already refused
|
|
129
|
+
# anything that was work. `branch -d` asks "is it merged into HEAD", which is
|
|
130
|
+
# the wrong question here (HEAD is usually main and $BASE is usually dev), so
|
|
131
|
+
# -D is correct once content identity is proven — and it is proven, above.
|
|
132
|
+
if git worktree remove --force "$wt" 2>/dev/null; then
|
|
133
|
+
git branch -D "$b" >/dev/null 2>&1 \
|
|
134
|
+
&& echo "[do-auto] gc: removed $wt + branch $b (content already in $BASE)" \
|
|
135
|
+
|| echo "[do-auto] gc: removed $wt (branch $b kept — could not delete ref)"
|
|
136
|
+
removed=$((removed+1))
|
|
64
137
|
else
|
|
65
|
-
echo "[do-auto] gc: $b
|
|
138
|
+
echo "[do-auto] gc: $b is in $BASE but its worktree would not remove — inspect $wt"; kept=$((kept+1))
|
|
66
139
|
fi
|
|
67
|
-
|
|
68
|
-
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# ENUMERATE FROM GIT, NOT FROM A GLOB. `for wt in .claude/worktrees/*/` is
|
|
143
|
+
# relative to the INVOKING tree, and a worktree contains no worktrees — so run
|
|
144
|
+
# from anywhere but main and the sweep matches nothing and reports success.
|
|
145
|
+
# Measured 2026-09-06 on the first run of this code: `removed=0 kept=0` with 19
|
|
146
|
+
# worktrees live. It is the same trap the canon already records for land.sh and
|
|
147
|
+
# release.sh ("Run MAIN's copy"), and a glob cannot be told apart from a clean
|
|
148
|
+
# sweep by its output. `git worktree list` answers with absolute paths from any
|
|
149
|
+
# tree, so there is no copy to run.
|
|
150
|
+
SELF="$(git rev-parse --show-toplevel 2>/dev/null)"
|
|
151
|
+
while IFS= read -r wt; do
|
|
152
|
+
[ -d "$wt" ] || continue
|
|
153
|
+
case "$wt" in *"/.do-worktrees/"*|*"/.claude/worktrees/"*) ;; *) continue ;; esac
|
|
154
|
+
# Never saw off the branch you are sitting on.
|
|
155
|
+
[ "$wt" = "$SELF" ] && { echo "[do-auto] gc: $wt is the invoking tree — kept"; kept=$((kept+1)); continue; }
|
|
156
|
+
b="$(git -C "$wt" branch --show-current 2>/dev/null)"
|
|
157
|
+
[ -n "$b" ] || { echo "[do-auto] gc: $wt is detached — kept"; kept=$((kept+1)); continue; }
|
|
158
|
+
_gc_one "$wt" "$b"
|
|
159
|
+
done < <(git worktree list --porcelain | sed -n 's/^worktree //p')
|
|
160
|
+
|
|
161
|
+
echo "[do-auto] gc done: removed=$removed kept=$kept (base=$BASE ${BASE_SHA}${BASE_NOTE})"
|
|
69
162
|
exit 0
|
|
70
163
|
fi
|
|
71
164
|
|
|
@@ -587,7 +680,9 @@ POLL_SECS="${DO_POLL_SECS:-30}"
|
|
|
587
680
|
# branch would otherwise seed every new do/<slug> with that branch's commits
|
|
588
681
|
# (plans cross-contaminating across concurrent sessions, 2026-07-21).
|
|
589
682
|
# DO_BASE_REF overrides for a deliberate stack on another branch.
|
|
590
|
-
|
|
683
|
+
# `dev` since 2026-09-06: it is the integration branch branches land into, so
|
|
684
|
+
# cutting from main would start every cycle missing what dev already holds.
|
|
685
|
+
BASE="${DO_BASE_REF:-dev}"
|
|
591
686
|
git show-ref --verify --quiet "refs/heads/$BASE" \
|
|
592
687
|
|| BASE="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)"
|
|
593
688
|
BR="do/${SLUG}"
|
|
@@ -1325,6 +1420,10 @@ if [ "$i" -ge "$MAX_CYCLES" ]; then
|
|
|
1325
1420
|
echo "[do-auto] hit --max-cycles $MAX_CYCLES — halting"
|
|
1326
1421
|
echo "[do-auto] WIP preserved on branch $BR (worktree $WT)."
|
|
1327
1422
|
_signal halt "$SLUG" "$(_tier)" "$(_plan_cycle "$i")" "reason=max-cycles"
|
|
1423
|
+
# A halt still closes. --status timeout is the honest word: the lease stays
|
|
1424
|
+
# held and the task is NOT marked done, so the next run resumes rather than
|
|
1425
|
+
# re-opening finished work. Rule 1 has no exception for halts.
|
|
1426
|
+
bash "$(dirname "$0")/do-close.sh" "$SLUG" --status timeout || true
|
|
1328
1427
|
exit 1
|
|
1329
1428
|
fi
|
|
1330
1429
|
|
|
@@ -1340,11 +1439,13 @@ _merge_digest
|
|
|
1340
1439
|
# not per cycle (a plan's cycles all serve the same origin task). Asserted:
|
|
1341
1440
|
# a failed link is logged with its exit, never disguised as ok; the plan
|
|
1342
1441
|
# still completed its code.
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1442
|
+
#
|
|
1443
|
+
# do-close.sh is the whole close, not just the link: the board write (through
|
|
1444
|
+
# this same slug door), the receipt check that gates it, the rubric dims, the
|
|
1445
|
+
# feedback pheromone and the promise settle — bounded, in ~5s, and whatever it
|
|
1446
|
+
# cannot close it FILES with the command that finishes it. It exits 0 by design
|
|
1447
|
+
# even with open legs; a close that can fail a plan is a close plans skip.
|
|
1448
|
+
bash "$(dirname "$0")/do-close.sh" "$SLUG" --composite "${_composite:-}" || true
|
|
1348
1449
|
echo ""
|
|
1349
1450
|
echo "[do-auto] ✓ plan complete on branch $BR — trunk ($BASE) untouched."
|
|
1350
1451
|
echo "[do-auto] read: .do-digest.md # what landed + gate verdict (reasoning was discarded)"
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# do-board.sh <slug> [--dry-run | --status | --self-test] — the ▸board stop's executor,
|
|
3
|
+
# and the ONE minter of a plan's cycle rows.
|
|
4
|
+
#
|
|
5
|
+
# manifest: needs-env (reads GATEWAY_API_KEY; writes rows through one.ie)
|
|
6
|
+
#
|
|
7
|
+
# Mirrors a plan's batch DAG onto the board as CHAINED rows and writes the tids
|
|
8
|
+
# back into the todo's frontmatter (`anchor:` + `cycle_rows:`), so the plan and
|
|
9
|
+
# the board are one graph rendered twice.
|
|
10
|
+
#
|
|
11
|
+
# WHY THE JOIN MATTERS. `tasks:claim` and `tasks:status picked` are BLOCKER-GATED:
|
|
12
|
+
# they read the very `blocks` edge this script writes. A mirrored plan cannot be
|
|
13
|
+
# claimed out of order; an unmirrored one hands every head every cycle at once.
|
|
14
|
+
#
|
|
15
|
+
# ONE MINTER. do-signal.sh's `--task-plan` used to file the same rows flat
|
|
16
|
+
# (tasks:create, no edges, tagged `slug:<slug>-C1` in UPPER case) while
|
|
17
|
+
# `--task-cycle` closed rows named `slug:<slug>-c1` in lower case — SLUG_RE is
|
|
18
|
+
# lowercase-only, so the armed rows never deduped and never closed. Both now meet
|
|
19
|
+
# here: `--task-plan` delegates to this script, and the rows it mints carry the
|
|
20
|
+
# exact lower-case slug tag `--task-cycle` resolves, so the engine's existing
|
|
21
|
+
# close (do-tick.sh, do-engine.js, do-close.sh) moves THESE rows to done and the
|
|
22
|
+
# gate unblocks the next batch by itself. No new return leg was written.
|
|
23
|
+
#
|
|
24
|
+
# IDEMPOTENT AND RESUMABLE. tasks:subtask does not dedupe, so before minting a
|
|
25
|
+
# cycle the walk looks the row up by its slug tag (tasks:list) and, if it exists,
|
|
26
|
+
# only re-asserts the edges with tasks:depend (a negated-pattern insert — safe to
|
|
27
|
+
# repeat). Frontmatter is written after EVERY batch, so a walk that dies at C7
|
|
28
|
+
# resumes at C7 instead of twinning C1..C6.
|
|
29
|
+
#
|
|
30
|
+
# SEQUENTIAL BY CONTRACT. blockedBy takes tids, so batch N+1 needs batch N's rows
|
|
31
|
+
# to exist. Within a batch nothing depends on anything, but 14 rows cost ~20s in
|
|
32
|
+
# series and the order is the proof — not worth a parallel shape.
|
|
33
|
+
#
|
|
34
|
+
# THE LEASE MOVES TO THE CYCLE. Containment children gate their parent's claim
|
|
35
|
+
# (blockersResolved reads `containment` too), so once cycles hang off the anchor
|
|
36
|
+
# the plan-level lease (`tasks:claim` on the anchor) is refused as `blocked`.
|
|
37
|
+
# do-signal.sh --task-claim and do-fleet.sh claim_task therefore claim the FIRST
|
|
38
|
+
# OPEN CYCLE's row when the plan has cycle_rows — the row actually in flight.
|
|
39
|
+
#
|
|
40
|
+
# The deterministic-fact rule (.claude/CLAUDE.md): a fact the spine needs gets a
|
|
41
|
+
# script; the model executes it, never substitutes for it.
|
|
42
|
+
#
|
|
43
|
+
# DO_BOARD_FAKE=1 swaps the door for an in-process stub that mints deterministic
|
|
44
|
+
# tids — the dry run and the self-test walk the REAL loop, not a summary of it.
|
|
45
|
+
set -euo pipefail
|
|
46
|
+
|
|
47
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
48
|
+
API="${ONE_API_URL:-https://one.ie}"
|
|
49
|
+
FAKE="${DO_BOARD_FAKE:-0}"
|
|
50
|
+
|
|
51
|
+
die() { echo "do-board: $*" >&2; exit 1; }
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------- the door
|
|
54
|
+
_key() {
|
|
55
|
+
[ -n "${GATEWAY_API_KEY:-}" ] && { printf '%s' "$GATEWAY_API_KEY"; return; }
|
|
56
|
+
# .dev.vars, NOT .env — prod refuses the .env key (recorded trap).
|
|
57
|
+
local f="${ONE_ENV_FILE:-${DO_ENV_FILE:-$ROOT/one.ie/web/.dev.vars}}"
|
|
58
|
+
[ -f "$f" ] || die "no GATEWAY_API_KEY and no $f"
|
|
59
|
+
grep -o 'GATEWAY_API_KEY=.*' "$f" | head -1 | cut -d= -f2-
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# ask <receiver> <json-data> -> stdout is the raw envelope. No `-f`: a refusal's
|
|
63
|
+
# body is the diagnosis, and a silent empty string is how a mint reads as a hang.
|
|
64
|
+
ask() {
|
|
65
|
+
if [ "$FAKE" = 1 ]; then
|
|
66
|
+
case "$1" in
|
|
67
|
+
# deterministic per payload (ask runs in a $(...) subshell, so a counter cannot climb)
|
|
68
|
+
tasks:create|tasks:subtask) printf '{"ok":true,"tid":"task:%024x"}' "$(printf '%s' "$2" | cksum | cut -d' ' -f1)" ;;
|
|
69
|
+
tasks:list) printf '{"ok":true,"tasks":[]}' ;;
|
|
70
|
+
# The bulk door answers a ref→tid map, one entry per `creates` row, so the
|
|
71
|
+
# self-test walks the REAL bulk loop (chunking, cross-chunk ref resolution,
|
|
72
|
+
# the frontmatter write) instead of a summary of it. A stub that only knew
|
|
73
|
+
# the one-at-a-time receivers would have left the whole bulk path unproven
|
|
74
|
+
# while printing PASS.
|
|
75
|
+
tasks:bulk)
|
|
76
|
+
local _r _t _out=""
|
|
77
|
+
for _r in $(jq -r '.creates[]?.ref // empty' <<<"$2"); do
|
|
78
|
+
_t=$(printf '%s%s' "$_r" "$2" | cksum | cut -d' ' -f1)
|
|
79
|
+
_out="$_out,\"$_r\":\"$(printf 'task:%024x' "$_t")\""
|
|
80
|
+
done
|
|
81
|
+
printf '{"ok":true,"attempted":%s,"applied":%s,"failed":0,"created":{%s}}' \
|
|
82
|
+
"$(jq -r '.creates|length' <<<"$2")" "$(jq -r '.creates|length' <<<"$2")" "${_out#,}"
|
|
83
|
+
;;
|
|
84
|
+
*) printf '{"ok":true}' ;;
|
|
85
|
+
esac
|
|
86
|
+
return
|
|
87
|
+
fi
|
|
88
|
+
curl -sS -m 30 -X POST "$API/api/ask/$1" \
|
|
89
|
+
-H "Authorization: Bearer $(_key)" -H 'Content-Type: application/json' \
|
|
90
|
+
-d "{\"data\":$2}"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# tid_of <envelope> — the id a task receiver returns, whatever it calls it.
|
|
94
|
+
tid_of() { jq -r '.result.tid // .result.id // .result.taskId // .tid // empty' <<<"$1"; }
|
|
95
|
+
|
|
96
|
+
# row_by_slug <cslug> <ws> — an existing row's tid by its exact `slug:` tag, or empty.
|
|
97
|
+
# Same rule as resolveTaskBySlug: exact tag, and the sort-last tid wins if there are twins.
|
|
98
|
+
row_by_slug() {
|
|
99
|
+
local env; env=$(ask tasks:list "{\"workspace\":\"$2\",\"tag\":\"slug:$1\"}")
|
|
100
|
+
jq -r '[(.result.tasks // .tasks // [])[].tid] | sort | last // empty' <<<"$env"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# cycle_notes <todo> <cid> <slug> — what a head reads when it opens the row: the
|
|
104
|
+
# cycle's goal delta + deliverable, and where the whole frame lives. Capped so a
|
|
105
|
+
# silent 4000-byte truncation (tasks:comment's, and notes' by the same discipline)
|
|
106
|
+
# can never eat the pointer at the end.
|
|
107
|
+
cycle_notes() {
|
|
108
|
+
local body
|
|
109
|
+
body=$(awk -v c="$2" '
|
|
110
|
+
$0 ~ "^## "c" — " {on=1; next}
|
|
111
|
+
on && /^## / {exit}
|
|
112
|
+
on && /^\*\*(Goal delta|Deliverable|Files|Accept)/ {print}
|
|
113
|
+
' "$1" | sed 's/\*\*//g' | head -c 1200)
|
|
114
|
+
printf '%s\n\nPlan: text/%s-todo.md § %s\nClose: bash .claude/scripts/do-signal.sh --task-cycle %s %s done' "$body" "$3" "$2" "$3" "$2"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# write_front <todo> <anchor> <ws> <rows-block> — replace-or-insert the BOARD
|
|
118
|
+
# CONTRACT block. ONE definition, used by the walk AND the self-test, so the red
|
|
119
|
+
# proof exercises the writer that ships and not a copy of it.
|
|
120
|
+
write_front() {
|
|
121
|
+
python3 - "$1" "$2" "$3" "$4" <<'PY'
|
|
122
|
+
import re,sys
|
|
123
|
+
p,anchor,ws,rows = sys.argv[1],sys.argv[2],sys.argv[3],sys.argv[4]
|
|
124
|
+
s=open(p).read()
|
|
125
|
+
# strip any previous block first — running ▸board twice must not leave two answers
|
|
126
|
+
# `[ \t]*(?:#.*)?` on the cycle_rows line is load-bearing: template-todo.md ships
|
|
127
|
+
# `cycle_rows: [] # WRITTEN BY do-board.sh, never by hand`, and without
|
|
128
|
+
# the comment clause that group does not match. The group is OPTIONAL, so the strip
|
|
129
|
+
# then silently removed only anchor+board_workspace and LEFT the empty list — and a
|
|
130
|
+
# second `cycle_rows:` is last-wins in YAML, so every YAML consumer read ZERO tids
|
|
131
|
+
# while the file visibly held six. --status never saw it (it parses by line, not by
|
|
132
|
+
# YAML), so writer and checker agreed and only the consumers were wrong. Measured on
|
|
133
|
+
# workflow-genesis, 2026-09-12.
|
|
134
|
+
s=re.sub(r'\n(anchor:.*\n)(board_workspace:.*\n)?(cycle_rows:[ \t]*(?:\[\])?[ \t]*(?:#.*)?\n(?:[ \t]+-.*\n)*)?','\n',s,count=1)
|
|
135
|
+
blk=f'anchor: "{anchor}"\nboard_workspace: {ws}\ncycle_rows:\n{rows}'
|
|
136
|
+
if not blk.endswith('\n'): blk += '\n'
|
|
137
|
+
if re.search(r'^depends_on:.*\n',s,flags=re.M):
|
|
138
|
+
s=re.sub(r'^(depends_on:.*\n)',lambda m:m.group(1)+blk,s,count=1,flags=re.M)
|
|
139
|
+
else:
|
|
140
|
+
s=re.sub(r'^(slug:.*\n)',lambda m:m.group(1)+blk,s,count=1,flags=re.M)
|
|
141
|
+
open(p,'w').write(s)
|
|
142
|
+
PY
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------- self-test
|
|
146
|
+
if [ "${1:-}" = "--self-test" ]; then
|
|
147
|
+
fail=0
|
|
148
|
+
ok() { if [ "$2" = 1 ]; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi; }
|
|
149
|
+
|
|
150
|
+
ok "tid_of reads .result.tid" "$([ "$(tid_of '{"result":{"tid":"task:a"}}')" = "task:a" ] && echo 1 || echo 0)"
|
|
151
|
+
ok "tid_of reads .result.id" "$([ "$(tid_of '{"result":{"id":"task:b"}}')" = "task:b" ] && echo 1 || echo 0)"
|
|
152
|
+
ok "tid_of reads bare .tid" "$([ "$(tid_of '{"ok":true,"tid":"task:c"}')" = "task:c" ] && echo 1 || echo 0)"
|
|
153
|
+
ok "tid_of empty on an error" "$([ -z "$(tid_of '{"error":"forbidden"}')" ] && echo 1 || echo 0)"
|
|
154
|
+
|
|
155
|
+
tmp="$(mktemp -d)"; f="$tmp/x-todo.md"
|
|
156
|
+
# A fixture do-plan-json accepts: frontmatter, three cycles in two batches, a kanban.
|
|
157
|
+
cat > "$f" <<'FIX'
|
|
158
|
+
---
|
|
159
|
+
title: fixture plan
|
|
160
|
+
slug: x
|
|
161
|
+
outcome: "three rows chained"
|
|
162
|
+
depends_on: []
|
|
163
|
+
batches:
|
|
164
|
+
- [C1]
|
|
165
|
+
- [C2, C3]
|
|
166
|
+
---
|
|
167
|
+
## Status
|
|
168
|
+
- [ ] C1 — first
|
|
169
|
+
- [ ] C2 — second
|
|
170
|
+
- [ ] C3 — third
|
|
171
|
+
|
|
172
|
+
## C1 — first [tier: trivial]
|
|
173
|
+
**Goal delta:** one is true.
|
|
174
|
+
**Deliverable:** row one.
|
|
175
|
+
|
|
176
|
+
## C2 — second [tier: trivial]
|
|
177
|
+
**Goal delta:** two is true.
|
|
178
|
+
|
|
179
|
+
## C3 — third [tier: trivial]
|
|
180
|
+
**Goal delta:** three is true.
|
|
181
|
+
FIX
|
|
182
|
+
# A pristine copy: the walk below rewrites $f, and the bulk cases need a plan
|
|
183
|
+
# that is still UNMIRRORED (bulk is gated on drift == the cycle count).
|
|
184
|
+
cp "$f" "$tmp/orig-todo.md"
|
|
185
|
+
# THE WALK, under /bin/bash 3.2 — the shell this box actually runs. The first
|
|
186
|
+
# cut used `mapfile` (bash 4), died on the first batch, and was never caught
|
|
187
|
+
# because --dry-run exited before the loop. The fake door makes the loop the thing
|
|
188
|
+
# under test.
|
|
189
|
+
out=$(cd "$ROOT" && DO_PLAN_TODO="$f" DO_BOARD_FAKE=1 /bin/bash "$ROOT/.claude/scripts/do-board.sh" x 2>&1) || true
|
|
190
|
+
ok "walk runs under /bin/bash 3.2" "$(grep -q 'drift is now 0' <<<"$out" && echo 1 || echo 0)"
|
|
191
|
+
ok "C1 minted with no blockers" "$(grep -q '^ C1 .*blockedBy=0' <<<"$out" && echo 1 || echo 0)"
|
|
192
|
+
ok "C2 and C3 both blocked by batch 1" "$([ "$(grep -c '^ C[23] .*blockedBy=1' <<<"$out")" = 2 ] && echo 1 || echo 0)"
|
|
193
|
+
ok "anchor + 3 rows written to frontmatter" "$([ "$(grep -c 'cycle: C[123], tid: "task:' "$f")" = 3 ] && grep -q '^anchor: "task:' "$f" && echo 1 || echo 0)"
|
|
194
|
+
# Second run: every cycle has a row → nothing minted, drift 0, ONE block.
|
|
195
|
+
out2=$(cd "$ROOT" && DO_PLAN_TODO="$f" DO_BOARD_FAKE=1 /bin/bash "$ROOT/.claude/scripts/do-board.sh" x 2>&1) || true
|
|
196
|
+
ok "second run mints nothing (idempotent)" "$(grep -q 'already mirrored' <<<"$out2" && echo 1 || echo 0)"
|
|
197
|
+
ok "second run leaves ONE anchor:" "$([ "$(grep -c '^anchor:' "$f")" = 1 ] && echo 1 || echo 0)"
|
|
198
|
+
ok "second run leaves ONE cycle_rows:" "$([ "$(grep -c '^cycle_rows:' "$f")" = 1 ] && echo 1 || echo 0)"
|
|
199
|
+
# Resume: drop C3's row from the frontmatter → only C3 is re-minted, chained to batch 1.
|
|
200
|
+
sed -i '' '/cycle: C3,/d' "$f"
|
|
201
|
+
out3=$(cd "$ROOT" && DO_PLAN_TODO="$f" DO_BOARD_FAKE=1 /bin/bash "$ROOT/.claude/scripts/do-board.sh" x 2>&1) || true
|
|
202
|
+
ok "resume re-mints ONLY the missing cycle" "$([ "$(grep '^ C[0-9]* ' <<<"$out3" | grep -vc existing)" = 1 ] && grep -q '^ C3 .*blockedBy=1' <<<"$out3" && echo 1 || echo 0)"
|
|
203
|
+
ok "fake tids are distinct per row" "$([ "$(grep -o 'task:[0-9a-f]\{24\}' "$f" | sort -u | wc -l | tr -d ' ')" = 4 ] && echo 1 || echo 0)"
|
|
204
|
+
# Notes carry the pointer a head needs.
|
|
205
|
+
ok "cycle_notes ends with the close command" "$(cycle_notes "$f" C1 x | grep -q -- '--task-cycle x C1 done' && echo 1 || echo 0)"
|
|
206
|
+
|
|
207
|
+
# ── the BULK path, walked end to end against the fake bulk door ────────────
|
|
208
|
+
# A fresh fixture, because bulk is gated on drift == the cycle count. This
|
|
209
|
+
# drives the real loop: the creates array, the ref chain, the receipt's
|
|
210
|
+
# ref→tid map, and the SAME write_front the per-row walk uses.
|
|
211
|
+
g="$tmp/bz-todo.md"; sed 's/^slug: x$/slug: bz/' "$tmp/orig-todo.md" > "$g"
|
|
212
|
+
outb=$(cd "$ROOT" && DO_PLAN_TODO="$g" DO_BOARD_FAKE=1 DO_BOARD_BULK=1 /bin/bash "$ROOT/.claude/scripts/do-board.sh" bz 2>&1) || true
|
|
213
|
+
ok "bulk mints the tree in ONE tasks:bulk call" "$(grep -q 'in 1 tasks:bulk call' <<<"$outb" && echo 1 || echo 0)"
|
|
214
|
+
ok "bulk chains batch 2 behind batch 1" "$([ "$(grep -c '^ C[23] .*blockedBy=1' <<<"$outb")" = 2 ] && grep -q '^ C1 .*blockedBy=0' <<<"$outb" && echo 1 || echo 0)"
|
|
215
|
+
ok "bulk writes all 3 tids to frontmatter" "$([ "$(grep -c 'cycle: C[123], tid: \"task:' "$g")" = 3 ] && echo 1 || echo 0)"
|
|
216
|
+
ok "bulk tids are distinct per row" "$([ "$(grep -o 'task:[0-9a-f]\{24\}' "$g" | sort -u | wc -l | tr -d ' ')" = 4 ] && echo 1 || echo 0)"
|
|
217
|
+
# drift=0 is not the proof, and the script must say so rather than imply it.
|
|
218
|
+
ok "bulk points at the claim REFUSAL as proof" "$(grep -q -- '--task-claim bz' <<<"$outb" && echo 1 || echo 0)"
|
|
219
|
+
# RED HALF: a half-mirrored plan must DECLINE bulk — `creates` does not dedupe,
|
|
220
|
+
# so a second bulk run over these rows would twin every one of them.
|
|
221
|
+
outb2=$(cd "$ROOT" && DO_PLAN_TODO="$g" DO_BOARD_FAKE=1 DO_BOARD_BULK=1 /bin/bash "$ROOT/.claude/scripts/do-board.sh" bz 2>&1) || true
|
|
222
|
+
ok "fully mirrored plan mints nothing under --bulk" "$(grep -q 'already mirrored' <<<"$outb2" && echo 1 || echo 0)"
|
|
223
|
+
sed -i '' '/cycle: C3,/d' "$g"
|
|
224
|
+
outb3=$(cd "$ROOT" && DO_PLAN_TODO="$g" DO_BOARD_FAKE=1 DO_BOARD_BULK=1 /bin/bash "$ROOT/.claude/scripts/do-board.sh" bz 2>&1) || true
|
|
225
|
+
ok "half-mirrored plan DECLINES bulk (no twins)" "$(grep -q 'bulk declined' <<<"$outb3" && [ "$(grep -c '^ C[0-9]* ' <<<"$outb3" | tr -d ' ')" = 3 ] && echo 1 || echo 0)"
|
|
226
|
+
# Empty block form (`cycle_rows: []`, the template's default) is replaced, not doubled.
|
|
227
|
+
printf -- '---\ntitle: t\nslug: y\ndepends_on: []\nanchor: ""\nboard_workspace: one\ncycle_rows: []\n---\nbody\n' > "$f"
|
|
228
|
+
write_front "$f" "task:aaa" "one" ' - {cycle: C1, tid: "task:111"}'
|
|
229
|
+
ok "template default block is replaced" "$([ "$(grep -c '^cycle_rows' "$f")" = 1 ] && grep -q 'tid: "task:111"' "$f" && echo 1 || echo 0)"
|
|
230
|
+
|
|
231
|
+
# THE TEMPLATE'S ACTUAL SHAPE — every one of the three keys carries a trailing
|
|
232
|
+
# `# comment`, which is what text/template-todo.md ships and therefore what every
|
|
233
|
+
# real plan looks like. The comment-free fixture above passed while this one wrote
|
|
234
|
+
# a DUPLICATE cycle_rows (last-wins in YAML ⇒ consumers read zero tids). A fixture
|
|
235
|
+
# tidier than the artefact it stands for is not a fixture. Parsed with a real YAML
|
|
236
|
+
# reader, not grep, because grep is the parser that missed it.
|
|
237
|
+
printf -- '---\ntitle: t\nslug: y\ndepends_on: []\nanchor: "" # filled by do-board.sh\nboard_workspace: one # where the rows live\ncycle_rows: [] # WRITTEN BY do-board.sh, never by hand\n---\nbody\n' > "$f"
|
|
238
|
+
write_front "$f" "task:bbb" "one" ' - {cycle: C1, tid: "task:222"}'
|
|
239
|
+
ok "commented template form leaves ONE cycle_rows" "$([ "$(grep -c '^cycle_rows' "$f")" = 1 ] && echo 1 || echo 0)"
|
|
240
|
+
ok "and a YAML reader sees the tid, not []" "$(python3 -c "
|
|
241
|
+
import yaml,sys
|
|
242
|
+
d=yaml.safe_load(open(sys.argv[1]).read().split('---')[1])
|
|
243
|
+
r=d.get('cycle_rows') or []
|
|
244
|
+
print(1 if len(r)==1 and r[0]['tid']=='task:222' else 0)" "$f")"
|
|
245
|
+
# RED PROOF: a naive appender fails the idempotence check the way the real writer must not.
|
|
246
|
+
printf -- '---\ntitle: t\nslug: y\ndepends_on: []\n---\nbody\n' > "$f"
|
|
247
|
+
printf 'anchor: "task:aaa"\n' >> "$f"; printf 'anchor: "task:aaa"\n' >> "$f"
|
|
248
|
+
ok "gutted writer is caught (2 anchors reads red)" "$([ "$(grep -c '^anchor:' "$f")" = 2 ] && echo 1 || echo 0)"
|
|
249
|
+
|
|
250
|
+
rm -rf "$tmp"
|
|
251
|
+
[ "$fail" = 0 ] && echo "PASS" || echo "FAIL"
|
|
252
|
+
exit "$fail"
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
# ---------------------------------------------------------------- read the plan
|
|
256
|
+
SLUG="${1:-}"; [ -n "$SLUG" ] || die "usage: do-board.sh <slug> [--dry-run|--status|--bulk]"
|
|
257
|
+
MODE="${2:-}"
|
|
258
|
+
|
|
259
|
+
PLAN="$(bash "$ROOT/.claude/scripts/do-plan-json.sh" "$SLUG")" || die "plan will not parse — fix it before mirroring it"
|
|
260
|
+
TODO="${DO_PLAN_TODO:-$ROOT/$(jq -r '.todo' <<<"$PLAN")}"
|
|
261
|
+
WS="$(jq -r '.boardWorkspace' <<<"$PLAN")"
|
|
262
|
+
ANCHOR="$(jq -r '.boardAnchor' <<<"$PLAN")"
|
|
263
|
+
DRIFT="$(jq -r '.boardDrift | length' <<<"$PLAN")"
|
|
264
|
+
TITLE="$(sed -n 's/^title:[ \t]*//p' "$TODO" | head -1 | tr -d '"')"
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------- --status: the instrument
|
|
267
|
+
if [ "$MODE" = "--status" ]; then
|
|
268
|
+
# ONE read for every cycle row (they all carry plan:<slug>), one for the anchor.
|
|
269
|
+
rows=$(ask tasks:list "{\"workspace\":\"$WS\",\"tag\":\"plan:$SLUG\"}")
|
|
270
|
+
arow=$(ask tasks:list "{\"workspace\":\"$WS\",\"tag\":\"slug:$SLUG\"}")
|
|
271
|
+
echo "do-board: $SLUG — workspace=$WS anchor=${ANCHOR:-none} $(jq -r --arg a "$ANCHOR" '(.result.tasks//.tasks//[])[] | select(.tid==$a) | "(\(.status))"' <<<"$arow")"
|
|
272
|
+
printf ' %-4s %-30s %-9s %s\n' cycle tid status title
|
|
273
|
+
jq -r '.batches | to_entries[] | .value[] as $c | "\(.key+1)\t\($c)"' <<<"$PLAN" | while IFS=$'\t' read -r b c; do
|
|
274
|
+
tid=$(jq -r --arg c "$c" '.cycleRows[$c] // "-"' <<<"$PLAN")
|
|
275
|
+
st=$(jq -r --arg t "$tid" '(.result.tasks//.tasks//[])[] | select(.tid==$t) | .status' <<<"$rows" | head -1)
|
|
276
|
+
nm=$(jq -r --arg t "$tid" '(.result.tasks//.tasks//[])[] | select(.tid==$t) | .name // ""' <<<"$rows" | head -1)
|
|
277
|
+
[ "$tid" = "-" ] && st="NO ROW"
|
|
278
|
+
[ -n "$st" ] || st="not-on-board"
|
|
279
|
+
printf ' %-4s %-30s %-9s %s\n' "$c" "$tid" "$st" "${nm:-$(sed -n "s/^## $c — //p" "$TODO" | head -1 | sed 's/ *\[.*//')}"
|
|
280
|
+
done
|
|
281
|
+
n_open=$(jq -r '[(.result.tasks//.tasks//[])[] | select(.status=="open")] | length' <<<"$rows")
|
|
282
|
+
n_done=$(jq -r '[(.result.tasks//.tasks//[])[] | select(.status=="done" or .status=="verified")] | length' <<<"$rows")
|
|
283
|
+
n_pick=$(jq -r '[(.result.tasks//.tasks//[])[] | select(.status=="picked")] | length' <<<"$rows")
|
|
284
|
+
echo " rows: open=$n_open picked=$n_pick done=$n_done drift=$DRIFT (door: tasks:list tag=plan:$SLUG — exact, uncapped)"
|
|
285
|
+
exit 0
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
echo "do-board: $SLUG — workspace=$WS drift=$DRIFT cycle(s) with no row"
|
|
289
|
+
[ "$DRIFT" = 0 ] && { echo "do-board: already mirrored — nothing to mint"; exit 0; }
|
|
290
|
+
|
|
291
|
+
if [ "$MODE" = "--dry-run" ]; then
|
|
292
|
+
# The dry run walks the SAME loop against the fake door, in a scratch copy of the
|
|
293
|
+
# todo, so what it prints is what the live run does — not a summary of it.
|
|
294
|
+
tmp="$(mktemp -d)"; cp "$TODO" "$tmp/$SLUG-todo.md"
|
|
295
|
+
echo "do-board: DRY RUN (fake door, scratch copy) —"
|
|
296
|
+
DO_PLAN_TODO="$tmp/$SLUG-todo.md" DO_BOARD_FAKE=1 bash "$ROOT/.claude/scripts/do-board.sh" "$SLUG" | sed 's/^/ /'
|
|
297
|
+
rm -rf "$tmp"; exit 0
|
|
298
|
+
fi
|
|
299
|
+
[ -z "$MODE" ] || [ "$MODE" = "--bulk" ] || die "unknown mode $MODE"
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------- the walk
|
|
302
|
+
# The anchor IS the plan row do-auto.sh already files (tasks:create dedupes on the
|
|
303
|
+
# `slug:` tag and returns the existing tid) — never a second parent.
|
|
304
|
+
if [ -z "$ANCHOR" ]; then
|
|
305
|
+
env=$(ask tasks:create "{\"title\":$(jq -Rn --arg t "$TITLE" '$t'),\"workspace\":\"$WS\",\"tags\":[\"slug:$SLUG\",\"do:cycle\"]}")
|
|
306
|
+
ANCHOR="$(tid_of "$env")"
|
|
307
|
+
[ -n "$ANCHOR" ] || die "anchor mint failed: $env"
|
|
308
|
+
echo " anchor $ANCHOR"
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
# ---------------------------------------------------------------- the bulk mint
|
|
312
|
+
# ONE call files a whole plan tree. tasks:bulk's `creates` resolves `ref` handles
|
|
313
|
+
# INSIDE the call, so C2's blockedBy names C1's ref and the receiver wires the
|
|
314
|
+
# edge itself — the batch DAG arrives as one request instead of one per row plus
|
|
315
|
+
# one per edge (14 rows cost ~20s in series; this costs one round trip per 25).
|
|
316
|
+
#
|
|
317
|
+
# GATED ON AN UNMIRRORED PLAN, and that is correctness, not caution: `creates`
|
|
318
|
+
# does NOT dedupe — which is the same reason `row_by_slug` exists at all, since
|
|
319
|
+
# tasks:subtask never deduped either. Run over a half-mirrored plan it would TWIN
|
|
320
|
+
# every row that already exists. So bulk fires only when drift == the cycle count
|
|
321
|
+
# (nothing on the board yet); every other state — including the resume after a
|
|
322
|
+
# bulk run that died mid-way — takes the per-row walk below, which looks each row
|
|
323
|
+
# up by its slug tag first. The fast path never becomes the only path.
|
|
324
|
+
#
|
|
325
|
+
# Opt in with `--bulk` or DO_BOARD_BULK=1. The default is unchanged.
|
|
326
|
+
BULK="${DO_BOARD_BULK:-0}"
|
|
327
|
+
[ "$MODE" = "--bulk" ] && BULK=1
|
|
328
|
+
NCYC=$(jq -r '[.batches[][]] | length' <<<"$PLAN")
|
|
329
|
+
if [ "$BULK" = 1 ] && [ "$DRIFT" = "$NCYC" ]; then
|
|
330
|
+
echo " bulk $NCYC row(s) with no board row — minting the tree with tasks:bulk creates"
|
|
331
|
+
rowsf="$(mktemp)"; ordf="$(mktemp)"
|
|
332
|
+
prevrefs='[]'
|
|
333
|
+
nb=$(jq -r '.batches | length' <<<"$PLAN"); b=0
|
|
334
|
+
while [ "$b" -lt "$nb" ]; do
|
|
335
|
+
currefs='[]'
|
|
336
|
+
while IFS= read -r c; do
|
|
337
|
+
[ -n "$c" ] || continue
|
|
338
|
+
n="${c#C}"; cslug="$(printf '%s-c%s' "$SLUG" "$n" | tr '[:upper:]' '[:lower:]')"
|
|
339
|
+
ctitle="$(sed -n "s/^## $c — //p" "$TODO" | head -1 | sed 's/ *\[.*//' | tr -d '"')"
|
|
340
|
+
notes="$(cycle_notes "$TODO" "$c" "$SLUG")"
|
|
341
|
+
jq -cn --arg ref "c$n" --arg p "$ANCHOR" --arg t "$c — $ctitle" --argjson bb "$prevrefs" \
|
|
342
|
+
--arg ws "$WS" --arg s "slug:$cslug" --arg pl "plan:$SLUG" --arg cy "cycle:$n" --arg notes "$notes" \
|
|
343
|
+
'{ref:$ref,parent:$p,title:$t,blockedBy:$bb,tags:[$s,$pl,"do:cycle",$cy],notes:$notes,workspace:$ws}' >> "$rowsf"
|
|
344
|
+
currefs=$(jq -c --arg r "c$n" '. + [$r]' <<<"$currefs")
|
|
345
|
+
printf '%s\tc%s\n' "$c" "$n" >> "$ordf"
|
|
346
|
+
done < <(jq -r ".batches[$b][]" <<<"$PLAN")
|
|
347
|
+
prevrefs="$currefs"; b=$((b+1))
|
|
348
|
+
done
|
|
349
|
+
|
|
350
|
+
ALL=$(jq -cs '.' "$rowsf"); MAP='{}'; off=0; ncall=0
|
|
351
|
+
# 25 rows per call is the receiver's declared ceiling. A ref can only be named
|
|
352
|
+
# by a row LATER IN THE SAME array, so a blockedBy pointing at an earlier CHUNK
|
|
353
|
+
# is rewritten to the real tid that chunk returned — otherwise the edge would
|
|
354
|
+
# silently not form and the plan would hand every head every cycle at once,
|
|
355
|
+
# which is the exact failure this whole script exists to prevent.
|
|
356
|
+
while [ "$off" -lt "$NCYC" ]; do
|
|
357
|
+
chunk=$(jq -c --argjson o "$off" '.[$o:$o+25]' <<<"$ALL")
|
|
358
|
+
chunk=$(jq -c --argjson here "$(jq -c '[.[].ref]' <<<"$chunk")" --argjson map "$MAP" \
|
|
359
|
+
'map(.blockedBy = [ .blockedBy[]? | if (. as $r | $here | index($r)) then . else ($map[.] // .) end ])' <<<"$chunk")
|
|
360
|
+
env=$(ask tasks:bulk "$(jq -cn --argjson c "$chunk" --arg ws "$WS" '{creates:$c,workspace:$ws}')")
|
|
361
|
+
# ok is the CALL's outcome, never the rows' — read applied/failed (receivers.ts).
|
|
362
|
+
fail=$(jq -r '(.result//.).failed // 0' <<<"$env")
|
|
363
|
+
[ "$fail" = 0 ] || die "tasks:bulk: $fail row(s) failed — $(jq -c '(.result//.).results // .' <<<"$env")"
|
|
364
|
+
created=$(jq -c '(.result//.).created // {}' <<<"$env")
|
|
365
|
+
[ "$(jq -r 'length' <<<"$created")" -gt 0 ] || die "tasks:bulk returned no created map: $env"
|
|
366
|
+
MAP=$(jq -cn --argjson a "$MAP" --argjson b "$created" '$a * $b')
|
|
367
|
+
off=$((off+25)); ncall=$((ncall+1))
|
|
368
|
+
done
|
|
369
|
+
|
|
370
|
+
ROWS=""
|
|
371
|
+
while IFS=$'\t' read -r c ref; do
|
|
372
|
+
tid=$(jq -r --arg r "$ref" '.[$r] // empty' <<<"$MAP")
|
|
373
|
+
[ -n "$tid" ] || die "$c: tasks:bulk never returned a tid for ref $ref"
|
|
374
|
+
nb_=$(jq -r --arg r "$ref" '.[] | select(.ref==$r) | .blockedBy | length' <<<"$ALL")
|
|
375
|
+
echo " $c $tid blockedBy=$nb_"
|
|
376
|
+
ROWS="$ROWS - {cycle: $c, tid: \"$tid\"}
|
|
377
|
+
"
|
|
378
|
+
done < "$ordf"
|
|
379
|
+
write_front "$TODO" "$ANCHOR" "$WS" "$ROWS"
|
|
380
|
+
rm -f "$rowsf" "$ordf"
|
|
381
|
+
echo "do-board: wrote anchor + $NCYC cycle row(s) into $(basename "$TODO") in $ncall tasks:bulk call(s)"
|
|
382
|
+
DO_PLAN_TODO="$TODO" bash "$ROOT/.claude/scripts/do-plan-json.sh" "$SLUG" | jq -r '"do-board: drift is now \(.boardDrift|length)"'
|
|
383
|
+
# drift=0 is NOT proof the chain formed — it only says every cycle has a tid.
|
|
384
|
+
# The edge is proven by a REFUSAL: tasks:claim on a blocked cycle answers
|
|
385
|
+
# `blocked`, and that is the check to run (recorded trap, 2026-09-12).
|
|
386
|
+
echo "do-board: prove the chain with a refusal, not a count —"
|
|
387
|
+
echo " bash .claude/scripts/do-signal.sh --task-claim $SLUG # a later cycle must answer 'blocked'"
|
|
388
|
+
exit 0
|
|
389
|
+
fi
|
|
390
|
+
[ "$BULK" != 1 ] || echo " bulk declined — $DRIFT of $NCYC cycle(s) missing; creates does not dedupe, taking the per-row walk"
|
|
391
|
+
|
|
392
|
+
PREV=""; ROWS=""
|
|
393
|
+
nb=$(jq -r '.batches | length' <<<"$PLAN")
|
|
394
|
+
b=0
|
|
395
|
+
while [ "$b" -lt "$nb" ]; do
|
|
396
|
+
CUR=""
|
|
397
|
+
while IFS= read -r c; do
|
|
398
|
+
[ -n "$c" ] || continue
|
|
399
|
+
n="${c#C}"; cslug="$(printf '%s-c%s' "$SLUG" "$n" | tr '[:upper:]' '[:lower:]')"
|
|
400
|
+
tid=$(jq -r --arg c "$c" '.cycleRows[$c] // empty' <<<"$PLAN")
|
|
401
|
+
blocked=$(printf '%s' "$PREV" | tr ' ' '\n' | jq -Rsc 'split("\n")|map(select(length>0))')
|
|
402
|
+
if [ -z "$tid" ]; then tid="$(row_by_slug "$cslug" "$WS")"; fi
|
|
403
|
+
if [ -n "$tid" ]; then
|
|
404
|
+
# existing row: only the edges are (re)asserted — depend is idempotent
|
|
405
|
+
k=0
|
|
406
|
+
for p in $PREV; do ask tasks:depend "{\"tid\":\"$tid\",\"blockedBy\":\"$p\",\"workspace\":\"$WS\"}" >/dev/null; k=$((k+1)); done
|
|
407
|
+
echo " $c $tid (existing) blockedBy=$k"
|
|
408
|
+
else
|
|
409
|
+
ctitle="$(sed -n "s/^## $c — //p" "$TODO" | head -1 | sed 's/ *\[.*//' | tr -d '"')"
|
|
410
|
+
notes="$(cycle_notes "$TODO" "$c" "$SLUG")"
|
|
411
|
+
body=$(jq -cn --arg p "$ANCHOR" --arg t "$c — $ctitle" --argjson b "$blocked" --arg ws "$WS" \
|
|
412
|
+
--arg s "slug:$cslug" --arg pl "plan:$SLUG" --arg cy "cycle:$n" --arg notes "$notes" \
|
|
413
|
+
'{parent:$p,title:$t,blockedBy:$b,tags:[$s,$pl,"do:cycle",$cy],notes:$notes,workspace:$ws}')
|
|
414
|
+
env=$(ask tasks:subtask "$body")
|
|
415
|
+
tid="$(tid_of "$env")"
|
|
416
|
+
[ -n "$tid" ] || die "$c mint failed: $env"
|
|
417
|
+
echo " $c $tid blockedBy=$(jq -r 'length' <<<"$blocked")"
|
|
418
|
+
fi
|
|
419
|
+
CUR="$CUR $tid"
|
|
420
|
+
ROWS="$ROWS - {cycle: $c, tid: \"$tid\"}
|
|
421
|
+
"
|
|
422
|
+
done < <(jq -r ".batches[$b][]" <<<"$PLAN")
|
|
423
|
+
# write after EVERY batch — a walk that dies at batch 3 resumes at batch 3
|
|
424
|
+
write_front "$TODO" "$ANCHOR" "$WS" "$ROWS"
|
|
425
|
+
PREV="$CUR"; b=$((b+1))
|
|
426
|
+
done
|
|
427
|
+
|
|
428
|
+
echo "do-board: wrote anchor + $(printf '%s' "$ROWS" | grep -c 'cycle:') cycle row(s) into $(basename "$TODO")"
|
|
429
|
+
DO_PLAN_TODO="$TODO" bash "$ROOT/.claude/scripts/do-plan-json.sh" "$SLUG" | jq -r '"do-board: drift is now \(.boardDrift|length)"'
|