@oneie/claude 0.2.1 → 0.3.1

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/commands/do.md CHANGED
@@ -122,7 +122,7 @@ For each **enabled** stop (per the pruned spine), in order: check if its artifac
122
122
  | ↳ *ANALYZE* | — | `do-analyze.sh plans/<slug>-todo.md` — CRITICAL exit 1 blocks BUILD | bash · none |
123
123
  | **code** | survey verdict = `build` (≥70% match → `expose`/`extend` instead) | the BUILD engine (Step 3) | sonnet · low–medium |
124
124
  | **tests** | test file in the repo folder | test-first, one assertion per deliverable | sonnet · low |
125
- | **proof** | proof artifact captured | `do-prove.sh` (browser / curl / contract / sync) + `accessibility`; **promise-check** the shipped thing against `text/<slug>.md`; FEATURE/SCHEMA built in a worktree **merge to trunk only on PROVE pass**, abandon on fail | sonnet · low |
125
+ | **proof** | proof artifact captured | `do-prove.sh` (browser / curl / contract / sync) + `accessibility`; **promise-check** the shipped thing against `text/<slug>.md`. A multi-cycle plan already built on its own `do/<slug>` branch (worktree isolation see Step 2); PROVE is the gate that branch must clear before a human merges it to trunk | sonnet · low |
126
126
  | **docs** | feature doc + runbook exist | `tutorial` + `writer` — written against the *proven* behavior | sonnet · medium |
127
127
  | **release** | changelog / README row | `/release` + adoption signal | sonnet · low |
128
128
 
@@ -163,6 +163,8 @@ bun .claude/scripts/do-auto.sh <slug>
163
163
 
164
164
  `do-auto.sh` loops: each iteration spawns a clean `/do <slug> --next-cycle` that runs exactly one batch (W1→W4), ticks its boxes, writes state, and exits. Because each cycle starts from near-zero context, prior-cycle recon/edit/verify chatter never accumulates. The main session just kicks off the loop and reports the close.
165
165
 
166
+ **Worktree isolation comes free with the loop.** The loop's existence *is* the trigger — `do-auto.sh` only runs for multi-cycle plans, which is exactly the FEATURE/SCHEMA work that must not land half-built on trunk. So before the first cycle it cuts a worktree `.do-worktrees/<slug>` on branch `do/<slug>` (reusing it on resume), syncs the plan's spine artifacts into it, and runs **every cycle inside it** — each cycle's edits and box-ticks commit to that branch. Trunk never moves while the loop runs; a halt (trust `cautious` / stall / max-cycles) leaves a clean, inspectable WIP branch, and re-running `/do <slug>` resumes in the same worktree. On plan-complete the loop **reports** the merge (`git merge --no-ff do/<slug>`) rather than running it — landing to trunk is a human decision (commit only when asked), and PROVE is the gate that branch cleared to earn the merge. PATCH/FIX never reach the loop, so they run inline with no worktree.
167
+
166
168
  A **single** incomplete cycle (or a PATCH/FIX) runs inline — there's nothing to isolate from, and spawning a subprocess would cost more than it saves.
167
169
 
168
170
  **`--next-cycle` is internal.** It means "run one batch, tick boxes, exit — do **not** loop." Only `do-auto.sh` passes it; it is the recursion guard that keeps a fresh `/do` from re-entering the loop. A human never types it.
@@ -266,7 +268,7 @@ PATCH clears {1,2,3,8}. FEATURE clears all eight.
266
268
 
267
269
  ## Available to /do — the toolbox
268
270
 
269
- **Scripts** (`.claude/scripts/`): `do-auto.sh` (*internal* — the context-isolated loop `/do` drives for multi-cycle plans) · `do-tier.sh` (tier + pruned spine + classifier + ceiling) · `do-folder.sh` (folder-aware verify/build) · `do-survey.sh` (reuse verdict) · `do-reconcile.sh` (substrate dim/verb/dead-name gate) · `do-analyze.sh` (deliverable↔cycle coverage gate) · `do-prove.sh` (surface-detect proof) · `do-smoke.sh` (deterministic outcome) · `w1-recon.ts` (prompt-cached recon) · `w4-rubric.ts` (cached parallel rubric).
271
+ **Scripts** (`.claude/scripts/`): `do-auto.sh` (*internal* — the context-isolated, worktree-isolated loop `/do` drives for multi-cycle plans; builds on branch `do/<slug>`, merges to trunk only on a human's say-so) · `do-tier.sh` (tier + pruned spine + classifier + ceiling) · `do-folder.sh` (folder-aware verify/build) · `do-survey.sh` (reuse verdict) · `do-reconcile.sh` (substrate dim/verb/dead-name gate) · `do-analyze.sh` (deliverable↔cycle coverage gate) · `do-prove.sh` (surface-detect proof) · `do-smoke.sh` (deterministic outcome) · `w1-recon.ts` (prompt-cached recon) · `w4-rubric.ts` (cached parallel rubric).
270
272
 
271
273
  **Templates**: `text/template-frame.md` (promise) · `plans/template-spec.md` (design + pre-mortem + decisions) · `plans/template-todo.md` (plan + parallel budget + testing policy) · `plans/agent-template.md` (agent definition).
272
274
 
@@ -0,0 +1,100 @@
1
+ # /skill-create — crystallise a cycle's learnings into a reusable skill
2
+
3
+ Run after a `/do` cycle closes (or any meaningful session). Reads the recent git
4
+ diff and the last `learnings.md` entry, then drafts a `SKILL.md` under
5
+ `.claude/skills/<slug>/` — so the pattern compounds instead of evaporating.
6
+
7
+ ---
8
+
9
+ ## When to invoke
10
+
11
+ - After `/do` closes a cycle and `learnings.md` has a new entry
12
+ - When you notice a pattern being repeated across sessions
13
+ - Explicitly: `/skill-create [slug]`
14
+
15
+ If `[slug]` is omitted, derive it from the most recent `learnings.md` entry
16
+ (the slug of the last `/do` cycle).
17
+
18
+ ---
19
+
20
+ ## What it does
21
+
22
+ **Step 1 — Gather raw material (zero LLM)**
23
+
24
+ ```bash
25
+ # Last learnings entry
26
+ tail -5 plans/learnings.md
27
+
28
+ # Recent cycle diff (what actually changed)
29
+ git diff HEAD~1 HEAD --stat
30
+ git diff HEAD~1 HEAD -- $(git diff HEAD~1 HEAD --name-only | grep -v 'plans/\|text/' | head -20)
31
+ ```
32
+
33
+ **Step 2 — Extract the pattern (Haiku · low)**
34
+
35
+ From the diff + learnings entry, identify:
36
+ - What recurring problem this cycle solved
37
+ - The specific shape of the solution (code pattern, file layout, command sequence)
38
+ - When it applies (trigger condition)
39
+ - When it doesn't (anti-patterns / exclusions)
40
+
41
+ Ignore one-off specifics (feature names, slugs). Keep the reusable skeleton.
42
+
43
+ **Step 3 — Write `.claude/skills/<slug>/SKILL.md`**
44
+
45
+ Use this template:
46
+
47
+ ```markdown
48
+ # <title> skill
49
+
50
+ <one-sentence description — what this skill does for the model>
51
+
52
+ ## When to use
53
+ <trigger phrase or condition that should invoke this skill>
54
+
55
+ ## Pattern
56
+ <the reusable shape — code snippet, command, or process>
57
+
58
+ ## Don't
59
+ - <anti-pattern 1>
60
+ - <anti-pattern 2>
61
+
62
+ ## Example
63
+ <minimal concrete example>
64
+ ```
65
+
66
+ **Step 4 — Wire it (if applicable)**
67
+
68
+ - If the skill has a natural trigger phrase, append a row to the skills table in
69
+ `.claude/CLAUDE.md` so it auto-loads.
70
+ - If it extends an existing skill, note the relation.
71
+
72
+ **Step 5 — Close**
73
+
74
+ Emit `signal("learning:harden", 1, "skill=<slug>")` and append one line to
75
+ `plans/learnings.md`:
76
+
77
+ ```
78
+ - YYYY-MM-DD · skill:<slug> · crystallised from <source-slug> · source=skill-create
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Rules
84
+
85
+ - **Extract, don't invent.** The pattern must be in the diff or learnings entry — no
86
+ generalising beyond what the cycle actually did.
87
+ - **One skill = one pattern.** If the cycle contains two independent patterns, create
88
+ two skills.
89
+ - **Lean.** A skill that fits in 30 lines is more likely to be read than one that
90
+ doesn't. Cut examples to the minimum that makes the pattern clear.
91
+ - **No cycle-specific names.** Slug and title describe the pattern, not the feature
92
+ that spawned it (`worktree-isolation`, not `do-auto-fix`).
93
+
94
+ ---
95
+
96
+ ## Close
97
+
98
+ After writing the skill file: `[ ] skill-create:<slug> done` appended to
99
+ `plans/learnings.md`, signal emitted, session stop-reflect will pick it up as
100
+ a new primitive on the next session start.
package/hooks/hooks.json CHANGED
@@ -1,9 +1,35 @@
1
1
  {
2
+ "PreToolUse": [
3
+ {
4
+ "matcher": "Edit|Write|MultiEdit",
5
+ "hooks": [
6
+ {
7
+ "id": "hook:gate-guard",
8
+ "type": "command",
9
+ "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/gate-guard.sh\"",
10
+ "timeout": 5000,
11
+ "blocking": true
12
+ }
13
+ ]
14
+ }
15
+ ],
2
16
  "PostToolUse": [
17
+ {
18
+ "matcher": "Read",
19
+ "hooks": [
20
+ {
21
+ "id": "hook:read-tracker",
22
+ "type": "command",
23
+ "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/read-tracker.sh\"",
24
+ "timeout": 3000
25
+ }
26
+ ]
27
+ },
3
28
  {
4
29
  "matcher": "Write|Edit",
5
30
  "hooks": [
6
31
  {
32
+ "id": "hook:post-edit",
7
33
  "type": "command",
8
34
  "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/post-edit-check.sh\"",
9
35
  "timeout": 15000
@@ -14,6 +40,7 @@
14
40
  "matcher": "Write|Edit|MultiEdit",
15
41
  "hooks": [
16
42
  {
43
+ "id": "hook:sync-todo-docs",
17
44
  "type": "command",
18
45
  "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/sync-todo-docs.sh\"",
19
46
  "timeout": 5000
@@ -24,6 +51,7 @@
24
51
  "matcher": "Write|Edit|MultiEdit",
25
52
  "hooks": [
26
53
  {
54
+ "id": "hook:design-check",
27
55
  "type": "command",
28
56
  "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/design-check.sh\"",
29
57
  "timeout": 5000,
@@ -35,6 +63,7 @@
35
63
  "matcher": "*",
36
64
  "hooks": [
37
65
  {
66
+ "id": "hook:tool-signal",
38
67
  "type": "command",
39
68
  "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/tool-signal.sh\"",
40
69
  "timeout": 3000
@@ -47,6 +76,7 @@
47
76
  "matcher": "*",
48
77
  "hooks": [
49
78
  {
79
+ "id": "hook:task-complete",
50
80
  "type": "command",
51
81
  "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/task-complete-verify.sh\"",
52
82
  "timeout": 120000,
@@ -60,12 +90,14 @@
60
90
  "matcher": "*",
61
91
  "hooks": [
62
92
  {
93
+ "id": "hook:session-end",
63
94
  "type": "command",
64
95
  "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/session-end-verify.sh\"",
65
96
  "timeout": 30000,
66
97
  "blocking": false
67
98
  },
68
99
  {
100
+ "id": "hook:stop-reflect",
69
101
  "type": "command",
70
102
  "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/stop-reflect.sh\"",
71
103
  "timeout": 10000,
@@ -79,6 +111,7 @@
79
111
  "matcher": "*",
80
112
  "hooks": [
81
113
  {
114
+ "id": "hook:session-start",
82
115
  "type": "command",
83
116
  "command": "bash \"$CLAUDE_PLUGIN_ROOT/hooks/scripts/session-start.sh\"",
84
117
  "timeout": 5000,
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # Shared hook utilities. Source from any hook:
3
+ # source "$CLAUDE_PROJECT_DIR/.claude/hooks/lib/hook.sh"
4
+ #
5
+ # is_hook_disabled <id> — returns 0 (skip) or 1 (run)
6
+ # hook_session_key <json-arg> — stable per-session key for temp files
7
+
8
+ # ── Runtime toggles ──────────────────────────────────────────────────────────
9
+ #
10
+ # ECC_DISABLED_HOOKS=hook:gate-guard,hook:stop-reflect
11
+ # Comma-separated hook IDs to skip. Useful in CI or setup scripts.
12
+ #
13
+ # ECC_HOOK_PROFILE=minimal|standard (default: standard)
14
+ # minimal — skips signal emissions, reflect, session-end, post-edit lint
15
+ # standard — all hooks active
16
+ #
17
+ # Per-hook disable: ECC_GATEGUARD=off (alias for hook:gate-guard)
18
+
19
+ is_hook_disabled() {
20
+ local id="$1"
21
+ local profile="${ECC_HOOK_PROFILE:-standard}"
22
+
23
+ # Explicit disable list (comma-separated)
24
+ if [[ -n "${ECC_DISABLED_HOOKS:-}" ]]; then
25
+ local IFS=','
26
+ for entry in $ECC_DISABLED_HOOKS; do
27
+ # trim whitespace
28
+ entry="${entry#"${entry%%[![:space:]]*}"}"
29
+ entry="${entry%"${entry##*[![:space:]]}"}"
30
+ [[ "$entry" == "$id" ]] && return 0
31
+ done
32
+ fi
33
+
34
+ # Profile-based skips
35
+ case "$profile" in
36
+ minimal)
37
+ case "$id" in
38
+ hook:tool-signal|hook:stop-reflect|hook:session-end|hook:post-edit) return 0 ;;
39
+ esac
40
+ ;;
41
+ esac
42
+
43
+ # Per-hook env aliases
44
+ case "$id" in
45
+ hook:gate-guard)
46
+ local v="${ECC_GATEGUARD:-}"
47
+ [[ "$v" =~ ^(0|off|false|disabled|disable)$ ]] && return 0
48
+ ;;
49
+ esac
50
+
51
+ return 1 # not disabled — run
52
+ }
53
+
54
+ # Derive a stable per-session key from the hook JSON payload ($1).
55
+ # Uses session_id from JSON if present; falls back to a cksum of the
56
+ # project dir so the key is consistent across hook invocations in one
57
+ # session even when CLAUDE_SESSION_ID isn't set.
58
+ hook_session_key() {
59
+ local payload="${1:-}"
60
+ local sid
61
+ sid=$(printf '%s' "$payload" | jq -r '.session_id // empty' 2>/dev/null)
62
+ if [[ -n "$sid" ]]; then
63
+ # Sanitise to filesystem-safe chars
64
+ printf '%s' "$sid" | tr -dc 'a-zA-Z0-9_-' | cut -c1-48
65
+ return
66
+ fi
67
+ # Fallback: hash of project dir
68
+ printf '%s' "${CLAUDE_PROJECT_DIR:-$(pwd)}" | cksum | awk '{print $1}'
69
+ }
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # GATE-GUARD — PreToolUse(Edit|Write|MultiEdit): require Read before edit.
3
+ #
4
+ # Blocks the first attempt to Edit/Write an existing file that hasn't been
5
+ # Read in this session. Forces "understand before you change."
6
+ #
7
+ # Rules:
8
+ # - New files (Write creating a file that doesn't exist yet) → always allowed
9
+ # - Settings files (.claude/settings*.json) → always allowed
10
+ # - Subagent calls → allowed (parent session's reads cover the session)
11
+ # - File already in this session's read registry → allowed
12
+ # - Existing file NOT yet read → BLOCK with instructions
13
+ #
14
+ # Disable for the session: ECC_GATEGUARD=off
15
+ # Disable one-off: ECC_DISABLED_HOOKS=hook:gate-guard
16
+ # Skip globally in CI: add hook:gate-guard to ECC_DISABLED_HOOKS in env
17
+
18
+ # shellcheck source=lib/hook.sh
19
+ _HOOK_LIB="${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/lib"
20
+ source "$_HOOK_LIB/hook.sh"
21
+ is_hook_disabled "hook:gate-guard" && exit 0
22
+
23
+ PAYLOAD="${1:-}"
24
+ [[ -z "$PAYLOAD" ]] && exit 0
25
+
26
+ TOOL=$(printf '%s' "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null)
27
+ case "$TOOL" in Edit|Write|MultiEdit) ;; *) exit 0 ;; esac
28
+
29
+ # Subagents: parent session already passed the gate for these files
30
+ SUBAGENT=$(printf '%s' "$PAYLOAD" | jq -r '.parent_tool_use_id // .parentToolUseId // empty' 2>/dev/null)
31
+ [[ -n "$SUBAGENT" ]] && exit 0
32
+
33
+ SESSION_KEY=$(hook_session_key "$PAYLOAD")
34
+ READS_FILE="/tmp/oneie-gate-reads-${SESSION_KEY}"
35
+
36
+ _deny() {
37
+ # Claude Code PreToolUse block format
38
+ printf '%s' "$(jq -nc --arg msg "$1" \
39
+ '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":$msg}}')"
40
+ exit 0
41
+ }
42
+
43
+ _is_settings() {
44
+ [[ "$1" =~ \.claude/settings[^/]*\.json$ ]]
45
+ }
46
+
47
+ _was_read() {
48
+ local fp="$1"
49
+ [[ -f "$READS_FILE" ]] && grep -qF "$fp" "$READS_FILE" 2>/dev/null
50
+ }
51
+
52
+ _block_msg() {
53
+ local fp="$1" verb="$2"
54
+ printf '[Gate-Guard] Read %s before you %s it.\n\nUse the Read tool on this file first to prove you understand it, then retry.\n\nTo disable for this session: set ECC_GATEGUARD=off\nTo disable permanently: add hook:gate-guard to ECC_DISABLED_HOOKS in your environment.' \
55
+ "$fp" "$verb"
56
+ }
57
+
58
+ check() {
59
+ local fp="$1" tool="$2"
60
+ [[ -z "$fp" ]] && return 0
61
+ _is_settings "$fp" && return 0
62
+ # New file being created — no prior read needed
63
+ [[ "$tool" == "Write" && ! -f "$fp" ]] && return 0
64
+ # Previously read this session — allowed
65
+ _was_read "$fp" && return 0
66
+ # Existing file not read → block
67
+ [[ -f "$fp" ]] && { _deny "$(_block_msg "$fp" "$(printf '%s' "$tool" | tr '[:upper:]' '[:lower:]')")"; }
68
+ return 0
69
+ }
70
+
71
+ case "$TOOL" in
72
+ Edit|Write)
73
+ FILE=$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
74
+ check "$FILE" "$TOOL"
75
+ ;;
76
+ MultiEdit)
77
+ while IFS= read -r fp; do
78
+ [[ -z "$fp" ]] && continue
79
+ check "$fp" "Edit"
80
+ done < <(printf '%s' "$PAYLOAD" | jq -r '.tool_input.edits[].file_path // empty' 2>/dev/null)
81
+ ;;
82
+ esac
83
+
84
+ exit 0
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ # READ-TRACKER — PostToolUse(Read): record file_path as read this session.
3
+ #
4
+ # Feeds gate-guard.sh: once a file appears in the reads registry,
5
+ # Edit/Write on that file is allowed. Disable both with ECC_GATEGUARD=off
6
+ # or ECC_DISABLED_HOOKS=hook:gate-guard.
7
+ #
8
+ # Non-blocking, always exits 0. Failure to write the registry is silent
9
+ # (gate-guard falls back to allow on missing registry).
10
+
11
+ # shellcheck source=lib/hook.sh
12
+ _HOOK_LIB="${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/lib"
13
+ source "$_HOOK_LIB/hook.sh"
14
+ is_hook_disabled "hook:gate-guard" && exit 0
15
+
16
+ PAYLOAD="${1:-}"
17
+ [[ -z "$PAYLOAD" ]] && exit 0
18
+
19
+ FILE=$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
20
+ [[ -z "$FILE" ]] && exit 0
21
+
22
+ SESSION_KEY=$(hook_session_key "$PAYLOAD")
23
+ READS_FILE="/tmp/oneie-gate-reads-${SESSION_KEY}"
24
+
25
+ # Append absolute path — idempotent (duplicates are fine, grep is fast)
26
+ echo "$FILE" >> "$READS_FILE" 2>/dev/null || true
27
+ exit 0
@@ -73,6 +73,18 @@ check_drift '^packages/sdk/src/' 'packages/sdk/CLAUDE.md'
73
73
  check_drift '^one\.ie/web/src/' 'one.ie/web/CLAUDE.md'
74
74
  check_drift '^api/src/' 'api/CLAUDE.md'
75
75
 
76
+ # (4) Completed /do cycle → suggest /skill-create to crystallise the pattern.
77
+ # Heuristic: learnings.md was touched AND W3 edited >=3 non-plan/non-text files.
78
+ if echo "$CHANGED" | grep -qF 'plans/learnings.md'; then
79
+ CYCLE_FILES=$(echo "$CHANGED" | grep -vE '^plans/|^text/|^\.claude/' | wc -l | awk '{print $1}')
80
+ if [ "$CYCLE_FILES" -ge 3 ]; then
81
+ # Extract the cycle slug from the last learnings entry
82
+ CYCLE_SLUG=$(grep -oE 'cycle [0-9]+|· [a-z][a-z0-9-]+' plans/learnings.md 2>/dev/null | tail -1 | sed 's/^· //')
83
+ BUF+="- **skill-create** cycle closed with ${CYCLE_FILES} file(s) changed — run \`/skill-create ${CYCLE_SLUG}\` to crystallise the pattern as a reusable skill.\n"
84
+ PROPOSALS=$((PROPOSALS + 1))
85
+ fi
86
+ fi
87
+
76
88
  # Only write the file if we found something worth reviewing
77
89
  if [ "$PROPOSALS" -gt 0 ]; then
78
90
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneie/claude",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "ONE Claude Code plugin — /do lifecycle, W1-W4 BUILD engine, ONE substrate via MCP, signals, and rules",
5
5
  "files": [
6
6
  ".claude-plugin",
@@ -19,11 +19,27 @@
19
19
  "@oneie/cli": ">=0.2.0"
20
20
  },
21
21
  "peerDependenciesMeta": {
22
- "@oneie/sdk": { "optional": true },
23
- "@oneie/mcp": { "optional": true },
24
- "@oneie/cli": { "optional": true }
22
+ "@oneie/sdk": {
23
+ "optional": true
24
+ },
25
+ "@oneie/mcp": {
26
+ "optional": true
27
+ },
28
+ "@oneie/cli": {
29
+ "optional": true
30
+ }
25
31
  },
26
- "keywords": ["claude-code", "plugin", "do", "lifecycle", "mcp", "signals", "typedb", "agents", "one"],
32
+ "keywords": [
33
+ "claude-code",
34
+ "plugin",
35
+ "do",
36
+ "lifecycle",
37
+ "mcp",
38
+ "signals",
39
+ "typedb",
40
+ "agents",
41
+ "one"
42
+ ],
27
43
  "license": "SEE LICENSE IN https://one.ie/free-license",
28
44
  "repository": {
29
45
  "type": "git",
@@ -40,15 +40,73 @@ fi
40
40
  # claude subprocess uses --dangerously-skip-permissions because it is non-interactive
41
41
  # — it cannot prompt the human for tool approvals. The workspace is the isolation
42
42
  # boundary. Do not expose this script to untrusted input or run it in shared environments.
43
- TODO="plans/${SLUG}-todo.md"
44
- TRUST=".do-trust.json"
43
+ [ -f "plans/${SLUG}-todo.md" ] || { echo "[do-auto] plans/${SLUG}-todo.md not found — run /do $SLUG first" >&2; exit 1; }
45
44
 
46
- [ -f "$TODO" ] || { echo "[do-auto] $TODO not found — run /do $SLUG first" >&2; exit 1; }
45
+ # ── Worktree isolation (one per plan) ────────────────────────────────────────
46
+ # The loop's existence IS the trigger: do-auto only runs for multi-cycle plans
47
+ # (>=2 incomplete cycles) = exactly the FEATURE/SCHEMA work that must not land
48
+ # half-built on trunk. So every loop builds in its own worktree on branch
49
+ # do/<slug>, leaving trunk green until a human merges. No tier plumbing, no flag.
50
+ #
51
+ # Why a worktree and not just a branch: the main session keeps observing trunk
52
+ # while the loop's subprocesses build in a separate checkout — they never fight
53
+ # over HEAD or the working tree. A halt leaves a clean, inspectable WIP branch.
54
+ #
55
+ # trunk (BASE) ── never moves during the loop
56
+ # └─ do/<slug> (worktree .do-worktrees/<slug>) ── every cycle commits here
57
+ # plan complete → report `git merge do/<slug>` (human lands it — never auto)
58
+ # halt → worktree persists → re-run /do <slug> resumes in place
59
+ BASE="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)"
60
+ BR="do/${SLUG}"
61
+ WT=".do-worktrees/${SLUG}"
62
+
63
+ _setup_worktree() {
64
+ # Idempotent: reuse an existing worktree (resume), else attach one to an
65
+ # existing branch (prior halt), else create branch+worktree fresh from HEAD.
66
+ if git -C "$WT" rev-parse --git-dir >/dev/null 2>&1; then
67
+ echo "[do-auto] resuming worktree $WT on $BR"
68
+ elif git show-ref --verify --quiet "refs/heads/$BR"; then
69
+ echo "[do-auto] re-attaching worktree $WT to existing branch $BR"
70
+ git worktree add "$WT" "$BR" >/dev/null
71
+ else
72
+ echo "[do-auto] creating worktree $WT on new branch $BR (from $BASE)"
73
+ git worktree add -b "$BR" "$WT" HEAD >/dev/null
74
+ fi
75
+
76
+ # Carry the plan's own artifacts into the worktree. The spine walk may have
77
+ # just written promise/spec/todo uncommitted in the main tree; a worktree cut
78
+ # from HEAD wouldn't have them. Copy only this slug's files — never unrelated
79
+ # working-tree changes — then commit them as the branch baseline.
80
+ local f changed=0
81
+ for f in "plans/${SLUG}.md" "plans/${SLUG}-todo.md" "text/${SLUG}.md" ".w4-improvements.json"; do
82
+ if [ -f "$f" ]; then
83
+ mkdir -p "$WT/$(dirname "$f")"
84
+ if ! cmp -s "$f" "$WT/$f" 2>/dev/null; then cp "$f" "$WT/$f"; changed=1; fi
85
+ fi
86
+ done
87
+ if [ "$changed" -eq 1 ]; then
88
+ git -C "$WT" add -A
89
+ git -C "$WT" commit -q -m "do(${SLUG}): sync spine artifacts" 2>/dev/null || true
90
+ fi
91
+ }
92
+
93
+ if ! $DRY_RUN; then
94
+ _setup_worktree
95
+ # All loop state now lives in the worktree — read the boxes the subprocess ticks.
96
+ TODO="$WT/plans/${SLUG}-todo.md"
97
+ TRUST="$WT/.do-trust.json"
98
+ else
99
+ TODO="plans/${SLUG}-todo.md"
100
+ TRUST=".do-trust.json"
101
+ fi
47
102
 
48
103
  _remaining() {
49
104
  # Count cycles in the Status section that are open ([ ]) or in-flight ([~]) — i.e. not [x].
50
105
  # Pattern starts with '\[' (not '-') so grep never mistakes it for an option flag.
51
- grep -cE '\[[ ~]\] C[0-9]+' "$TODO" 2>/dev/null || echo 0
106
+ # grep -c already prints a count (0 on no match) but exits 1 then — capture it so
107
+ # the `|| true` swallows the exit without appending a second "0" to the output.
108
+ local n; n=$(grep -cE '\[[ ~]\] C[0-9]+' "$TODO" 2>/dev/null) || true
109
+ echo "${n:-0}"
52
110
  }
53
111
 
54
112
  _trust() {
@@ -82,6 +140,7 @@ while [ "$i" -lt "$MAX_CYCLES" ]; do
82
140
  if [ "$stall" -ge 2 ]; then
83
141
  echo "[do-auto] no progress for 2 iterations (${remaining} cycle(s) stuck) — halting." >&2
84
142
  echo "[do-auto] The last cycle ticked no checkbox. Inspect $TODO, fix the blocker, then re-run /do $SLUG." >&2
143
+ echo "[do-auto] WIP preserved on branch $BR (worktree $WT)." >&2
85
144
  exit 1
86
145
  fi
87
146
  else
@@ -92,6 +151,7 @@ while [ "$i" -lt "$MAX_CYCLES" ]; do
92
151
  trust=$(_trust)
93
152
  if [ "$trust" = "cautious" ]; then
94
153
  echo "[do-auto] trust=cautious — halting. Fix the issue, then re-run /do $SLUG."
154
+ echo "[do-auto] WIP preserved on branch $BR (worktree $WT)."
95
155
  exit 1
96
156
  fi
97
157
 
@@ -107,11 +167,31 @@ while [ "$i" -lt "$MAX_CYCLES" ]; do
107
167
  break
108
168
  fi
109
169
 
110
- # Fresh context: no --resume, no --continue. Each cycle is a clean slate.
111
- claude --dangerously-skip-permissions -p "/do $SLUG --next-cycle"
170
+ # Fresh context, isolated tree: the subprocess runs INSIDE the worktree, so
171
+ # every edit/box-tick/state-write lands on branch $BR — trunk is never touched.
172
+ ( cd "$WT" && claude --dangerously-skip-permissions -p "/do $SLUG --next-cycle" )
173
+
174
+ # Commit the cycle on its branch. A cycle closes with a passing rubric, so it
175
+ # is the natural commit unit — and committed progress survives a later halt.
176
+ if [ -n "$(git -C "$WT" status --porcelain)" ]; then
177
+ git -C "$WT" add -A
178
+ git -C "$WT" commit -q -m "do(${SLUG}): cycle ${i}" || true
179
+ fi
112
180
  done
113
181
 
182
+ if $DRY_RUN; then exit 0; fi
183
+
114
184
  if [ "$i" -ge "$MAX_CYCLES" ]; then
115
185
  echo "[do-auto] hit --max-cycles $MAX_CYCLES — halting"
186
+ echo "[do-auto] WIP preserved on branch $BR (worktree $WT)."
116
187
  exit 1
117
188
  fi
189
+
190
+ # Plan complete. The branch holds every cycle, proven and committed; trunk is
191
+ # untouched. Landing it is a human decision (commit/push only when asked), so we
192
+ # report the merge instead of running it — the worktree stays for inspection.
193
+ echo ""
194
+ echo "[do-auto] ✓ plan complete on branch $BR — trunk ($BASE) untouched."
195
+ echo "[do-auto] land: git merge --no-ff $BR # from $BASE"
196
+ echo "[do-auto] inspect: git -C $WT log --oneline $BASE..$BR"
197
+ echo "[do-auto] discard: git worktree remove $WT && git branch -D $BR"