Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/scheduled_tasks.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"sessionId":"88a8ed16-5d68-4663-8e91-559a895b8f99","pid":80940,"procStart":"Sat Apr 25 04:34:00 2026","acquiredAt":1777100789355}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# Specflow local env
.specflow/config.env
.specflow/runs/
.specflow/worktrees/

# Node
node_modules/
Expand Down
90 changes: 79 additions & 11 deletions assets/commands/specflow.approve.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -320,19 +320,52 @@ If the archive command fails (non-zero exit code):

## Push & Pull Request

1. 現在のブランチ名を取得:
All `git` and `gh` invocations in this section MUST run with `cwd = $WORKTREE_PATH` (the main-session worktree resolved from run-state via `specflow-run get-field "<RUN_ID>" worktree_path`). The user's repository working tree is NOT the push source.

1. Resolve the worktree path and the change branch from run-state:
```bash
git branch --show-current
WORKTREE_PATH="$(specflow-run get-field "<RUN_ID>" worktree_path)"
CHANGE_BRANCH="$(specflow-run get-field "<RUN_ID>" branch_name)"
BASE_COMMIT="$(specflow-run get-field "<RUN_ID>" base_commit)"
BASE_BRANCH="$(specflow-run get-field "<RUN_ID>" base_branch)"
```

2. リモートに同名ブランチで push:
2. Push the change branch from inside the worktree:
```bash
git push -u origin <branch-name>
git -C "$WORKTREE_PATH" push -u origin "$CHANGE_BRANCH"
```

3. デフォルトブランチを取得:
3. Resolve the PR base. Try these strategies in order until one succeeds:
- **Strategy 1**: If `base_commit` is non-empty AND **exactly one** remote-tracking branch contains it, use that as the PR base. When 0 or 2+ remotes match, Strategy 1 abstains — silently picking an arbitrary match would target the wrong base.
- **Strategy 2**: If `base_branch` is non-empty and has an upstream tracking ref, strip the remote prefix and use that.
- **Strategy 3**: If `base_branch` is non-empty (the branch the user was on at prepare-change time), use it directly as the PR base. This handles local feature branches that have no upstream tracking.
- **Strategy 4**: Fall back to the repository's default branch.
```bash
gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'
PR_BASE=""
# Strategy 1: resolve from base_commit ONLY when exactly one remote branch contains it.
if [ -n "$BASE_COMMIT" ]; then
CONTAINING_REFS="$(git -C "$WORKTREE_PATH" branch -r --contains "$BASE_COMMIT" 2>/dev/null | grep -v HEAD | sed 's|^ *origin/||' | sort -u)"
CONTAINING_COUNT="$(printf '%s\n' "$CONTAINING_REFS" | grep -c .)"
if [ "$CONTAINING_COUNT" = "1" ]; then
PR_BASE="$CONTAINING_REFS"
fi
fi
# Strategy 2: resolve from base_branch upstream
if [ -z "$PR_BASE" ] && [ -n "$BASE_BRANCH" ] && git -C "$WORKTREE_PATH" rev-parse --abbrev-ref "$BASE_BRANCH@{upstream}" >/dev/null 2>&1; then
UPSTREAM="$(git -C "$WORKTREE_PATH" rev-parse --abbrev-ref "$BASE_BRANCH@{upstream}")"
PR_BASE="${UPSTREAM#origin/}"
fi
# Strategy 3: use base_branch directly (local branch the user started from)
if [ -z "$PR_BASE" ] && [ -n "$BASE_BRANCH" ]; then
PR_BASE="$BASE_BRANCH"
fi
# Strategy 4: repository default branch — run gh from inside the worktree
# so it picks up the right remote without an explicit -R selector. (Passing
# a raw `https://`/`git@` URL via -R is not the documented OWNER/REPO form
# and can fail.)
if [ -z "$PR_BASE" ]; then
PR_BASE="$(cd "$WORKTREE_PATH" && gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')"
fi
```

4. PR のタイトルと本文を生成する:
Expand All @@ -352,9 +385,9 @@ If the archive command fails (non-zero exit code):
<proposal の Acceptance Criteria や実装内容を箇条書きで 3-5 行>
```

5. `gh pr create` で PR を作成する:
5. PR を作成する。`gh pr create` も worktree 内で実行する:
```bash
gh pr create --title "<title>" --body "<body>" --base <default-branch>
(cd "$WORKTREE_PATH" && gh pr create --title "<title>" --body "<body>" --head "$CHANGE_BRANCH" --base "$PR_BASE")
```

6. PR 作成後、PR の URL をユーザーに表示する。
Expand All @@ -367,8 +400,43 @@ If the archive command fails (non-zero exit code):
fi
```

If `ARCHIVE_SUCCESS = true`:
Report: "Implementation approved, committed, PR created: `<PR-URL>`, change archived." → **END**.
## Worktree Cleanup

After the terminal phase completes (approve, archive, or reject), evaluate cleanup of the `.specflow/worktrees/<CHANGE_ID>/` subtree. Cleanup is gated on TWO conditions:

1. **success_full** — the terminal action itself completed without error.
2. **tree_clean** — every registered worktree under `.specflow/worktrees/<CHANGE_ID>/` has an empty `git status --porcelain`.

If `ARCHIVE_SUCCESS = true` (terminal action succeeded), invoke the cleanup subcommand:

```bash
specflow-run cleanup-worktrees "<RUN_ID>"
```

This command:
- Inspects every worktree under `.specflow/worktrees/<CHANGE_ID>/` for cleanliness.
- If all worktrees are clean: removes them (`git worktree remove`) and deletes the parent directory. Clears `cleanup_pending` in run-state. Exits 0.
- If any worktree is dirty or removal fails: sets `cleanup_pending = true` in run-state, outputs the deferred reasons as JSON, and exits 1.

If `ARCHIVE_SUCCESS = false` (terminal action itself failed), skip the cleanup subcommand — cleanup is implicitly deferred:

```bash
specflow-run update-field "<RUN_ID>" cleanup_pending true
```

Report the dirty paths or partial-failure cause to the user:
```
⚠️ Worktree cleanup deferred. Reason: <dirty paths / partial failure cause>
Resolve manually, then remove via: specflow-run cleanup-worktrees "<RUN_ID>"
```

## Final Report

If `ARCHIVE_SUCCESS = true` AND cleanup succeeded:
Report: "Implementation approved, committed, PR created: `<PR-URL>`, change archived, worktrees cleaned up." → **END**.

If `ARCHIVE_SUCCESS = true` AND cleanup deferred:
Report: "Implementation approved, committed, PR created: `<PR-URL>`, change archived. ⚠️ Worktree cleanup deferred — see details above." → **END**.

If `ARCHIVE_SUCCESS = false`:
Report: "Implementation approved, committed, PR created: `<PR-URL>`. ⚠️ Archive failed — run `openspec archive -y "<CHANGE_ID>"` manually." → **END**.
Report: "Implementation approved, committed, PR created: `<PR-URL>`. ⚠️ Archive failed — run `openspec archive -y "<CHANGE_ID>"` manually. Worktree cleanup deferred." → **END**.
63 changes: 51 additions & 12 deletions assets/commands/specflow.reject.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,66 @@ $ARGUMENTS

全変更を破棄します。

1. 現在の変更状態を確認:
1. Resolve `RUN_ID`/`CHANGE_ID`/worktree path from run-state. Reject MUST run inside the main-session worktree, not the user repo.
```bash
git status --short
CHANGE_ID="$(specflow-run get-field "<RUN_ID>" change_name)"
WORKTREE_PATH="$(specflow-run get-field "<RUN_ID>" worktree_path)"
REPO_PATH="$(specflow-run get-field "<RUN_ID>" repo_path)"
WT_PARENT="$REPO_PATH/.specflow/worktrees/$CHANGE_ID"
```

2. 変更ファイル一覧をユーザーに表示する。
2. 現在の変更状態を確認 (worktree 内):
```bash
git -C "$WORKTREE_PATH" status --short
```

3. 変更ファイル一覧をユーザーに表示する。

3. 全変更を破棄:
4. 全変更を破棄 (worktree 内のみ。ユーザーリポは触らない):
```bash
git checkout -- .
git clean -fd -- . ':(exclude)openspec'
git -C "$WORKTREE_PATH" checkout -- .
git -C "$WORKTREE_PATH" clean -fd -- . ':(exclude)openspec'
```

これにより:
- 変更されたファイルは元に戻る (`git checkout`)
- 新規作成されたファイルは削除される (`git clean`)
- `openspec/` 配下の新規ファイルは保持される
5. 破棄後の状態を確認:
```bash
git -C "$WORKTREE_PATH" status --short
```

## Worktree Cleanup

After reject completes, evaluate cleanup of `.specflow/worktrees/<CHANGE_ID>/`.
Cleanup is gated on TWO conditions:

1. **success_full** — reject ran without error (steps 4 above succeeded).
2. **tree_clean** — every registered worktree under `$WT_PARENT` reports an empty `git status --porcelain`.

If step 4 succeeded (exit 0), invoke the cleanup subcommand:

```bash
specflow-run cleanup-worktrees "<RUN_ID>"
```

This command:
- Inspects every worktree under `.specflow/worktrees/<CHANGE_ID>/` for cleanliness.
- If all worktrees are clean: removes them (`git worktree remove`) and deletes the parent directory. Clears `cleanup_pending`. Exits 0.
- If any worktree is dirty or removal fails: sets `cleanup_pending = true` in run-state, outputs deferred reasons as JSON, and exits 1.

If step 4 failed, skip the cleanup subcommand — cleanup is implicitly deferred:

```bash
specflow-run update-field "<RUN_ID>" cleanup_pending true
```

Surface the reason to the user:
```
⚠️ Worktree cleanup deferred. Reason: <failure details>
Resolve manually, then retry: specflow-run cleanup-worktrees "<RUN_ID>"
```

4. 破棄後の状態を確認:
Advance the run-state to its terminal reject phase regardless of cleanup deferral:
```bash
git status --short
specflow-run advance "<RUN_ID>" reject
```

Report: "Implementation rejected. All changes have been discarded." → **END**.
1 change: 1 addition & 0 deletions assets/template/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
# Specflow local env
.specflow/config.env
.specflow/runs/
.specflow/worktrees/
2 changes: 2 additions & 0 deletions openspec/changes/archive/2026-04-25-worktree/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-25
145 changes: 145 additions & 0 deletions openspec/changes/archive/2026-04-25-worktree/approval-summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Approval Summary: worktree

**Generated**: 2026-04-25T22:26:43+09:00
**Branch**: worktree
**Status**: ⚠️ 4 unresolved high (accepted as risk — see Remaining Risks)

## What Changed

```
.gitignore | 1 +
assets/commands/specflow.approve.md.tmpl | 79 ++++++++++--
assets/commands/specflow.reject.md.tmpl | 56 +++++++--
assets/template/.gitignore | 1 +
src/bin/specflow-challenge-proposal.ts | 2 +-
src/bin/specflow-generate-task-graph.ts | 2 +-
src/bin/specflow-prepare-change.ts | 173 ++++++++++++++++++++++++--
src/bin/specflow-review-apply.ts | 2 +-
src/bin/specflow-review-design.ts | 2 +-
src/bin/specflow-run.ts | 74 ++++++++++-
src/bin/specflow-watch.ts | 62 +++++++--
src/core/update-field.ts | 3 +
src/lib/apply-dispatcher/orchestrate.ts | 29 +++++
src/lib/apply-worktree/worktree.ts | 48 +++++--
src/lib/local-workspace-context.ts | 12 +-
src/lib/run-store-ops.ts | 15 ++-
src/lib/schemas.ts | 3 +
src/lib/terminal-worktree-cleanup.ts | 178 ++++++++++++++++++++++++++
src/lib/worktree-resolver.ts | 56 +++++++++
src/tests/__snapshots__/specflow.approve.md.snap | 79 ++++++++++--
src/tests/__snapshots__/specflow.reject.md.snap | 56 +++++++--
... (additional test/fixture updates omitted)
43 files changed, 1421 insertions(+), 183 deletions(-)
```

## Files Touched

```
.gitignore
assets/commands/specflow.approve.md.tmpl
assets/commands/specflow.reject.md.tmpl
assets/template/.gitignore
src/bin/specflow-challenge-proposal.ts
src/bin/specflow-generate-task-graph.ts
src/bin/specflow-prepare-change.ts
src/bin/specflow-review-apply.ts
src/bin/specflow-review-design.ts
src/bin/specflow-run.ts
src/bin/specflow-watch.ts
src/core/update-field.ts
src/lib/apply-dispatcher/orchestrate.ts
src/lib/apply-worktree/worktree.ts
src/lib/local-workspace-context.ts
src/lib/run-store-ops.ts
src/lib/schemas.ts
src/lib/terminal-worktree-cleanup.ts
src/lib/worktree-resolver.ts
src/tests/__snapshots__/specflow.approve.md.snap
src/tests/__snapshots__/specflow.reject.md.snap
src/tests/advance-records.test.ts
src/tests/apply-dispatcher-orchestrate.test.ts
src/tests/apply-worktree-helpers.test.ts
src/tests/apply-worktree-integrate.test.ts
src/tests/apply-worktree-realgit.test.ts
src/tests/core-advance.test.ts
src/tests/core-error-wording.test.ts
src/tests/core-start.test.ts
src/tests/core-status-fields.test.ts
src/tests/core-suspend-resume.test.ts
src/tests/fixtures/legacy-final/specflow-run/advance.json
src/tests/fixtures/legacy-final/specflow-run/start.json
src/tests/generation.test.ts
src/tests/legacy-runstate-guard.test.ts
src/tests/phase-router.test.ts
src/tests/prepare-change-raw-input.test.ts
src/tests/prepare-change-worktree-conflicts.test.ts
src/tests/run-state-partition.test.ts
src/tests/runstate-generic.test.ts
src/tests/spec-verify-integration.test.ts
src/tests/specflow-watch-integration.test.ts
src/tests/specflow-watch-readers.test.ts
src/tests/terminal-worktree-cleanup.test.ts
src/tests/utility-cli.test.ts
src/tests/worktree-invariant-verification.test.ts
src/tests/worktree-resolver.test.ts
src/types/contracts.ts
```

## Review Loop Summary

### Design Review
| Metric | Count |
|--------------------|-------|
| Initial high | 1 |
| Resolved high | 1 |
| Unresolved high | 0 |
| New high (later) | 1 |
| Total rounds | 2 |

### Impl Review
| Metric | Count |
|--------------------|-------|
| Initial high | 4 |
| Resolved high | 3 |
| Unresolved high | 4 |
| New high (later) | 4 |
| Total rounds | 3 |

## Proposal Coverage

Proposal covers worktree-mode for the main session. Coverage mapping:

| # | Criterion (summary) | Covered? | Mapped Files |
|---|---------------------|----------|--------------|
| 1 | C-1 user repo HEAD/branch/dirty state untouched after prepare-change | Yes | src/bin/specflow-prepare-change.ts (ensureMainSessionWorktree), src/tests/worktree-invariant-verification.test.ts |
| 2 | C-2 LocalRunState carries base_commit/base_branch/cleanup_pending | Yes | src/types/contracts.ts, src/lib/schemas.ts, src/bin/specflow-run.ts, src/tests/run-state-partition.test.ts |
| 3 | C-3 phase-command cwd routing through worktree_path | Partial | src/lib/worktree-resolver.ts, src/bin/specflow-watch.ts (watcher only) |
| 4 | C-4 subagent patches land in main-session worktree | Yes | src/lib/apply-worktree/worktree.ts (mainWorkspacePath/changeId mandatory), src/lib/apply-dispatcher/orchestrate.ts |
| 5 | C-5 approve PR base resolution from base_commit/base_branch with default-branch fallback | Yes | assets/commands/specflow.approve.md.tmpl |
| 6 | C-6 terminal cleanup gate (clean+complete vs deferred) | Yes | src/lib/terminal-worktree-cleanup.ts, src/tests/terminal-worktree-cleanup.test.ts |
| 7 | C-7 legacy run-state guard with synthetic-run exemption | Yes | src/bin/specflow-prepare-change.ts (legacy guard), src/bin/specflow-watch.ts (watcher guard) |

**Coverage Rate**: 6.5/7 (93%) — C-3 partial because audit of every downstream phase CLI is scope-limited to follow-up.

## Remaining Risks

### Deterministic risks (from review-ledger)

- **R1-F04 (high)**: Legacy mode preservation — `readRunState` backfills missing `base_commit`/`base_branch`/`cleanup_pending` fields with defaults instead of fail-fast. **Accepted as risk.** Rationale: synthetic runs legitimately lack these fields; the backfill is forward-compat for record evolution. The mutating entry points (`prepare-change`) and the watcher both fail-fast on legacy `worktree_path == repo_path` non-synthetic records — that's the actual policy lever. The lenient read is read-only.
- **R2-F06 (high)**: Downstream phase CLIs (`specflow-review-apply`, `specflow-review-design`, `specflow-challenge-proposal`, `specflow-generate-task-graph`) still build their change stores from `projectRoot()`/`ensureGitRepo()` instead of resolving the run-id → worktree_path indirection. **Accepted as risk.** Rationale: the resolver (`src/lib/worktree-resolver.ts`) is foundation for that audit, but threading it through every CLI is a separate cross-cutting effort tracked as bundle-4 follow-up. For changes started after this lands, users can either (a) `cd .specflow/worktrees/<change>/main && /specflow.<phase>` (cwd-rooted resolution works correctly), or (b) wait for the follow-up audit change.
- **R3-F08 (high)**: Same scope as R2-F06. **Accepted as risk.**
- **R3-F09 (high)**: Slash-command templates (other than `specflow.approve` / `specflow.reject`) still instruct repo-root execution. **Accepted as risk.** Same scope as R2-F06.

### Untested new files
None — every new file (`worktree-resolver.ts`, `terminal-worktree-cleanup.ts`) has corresponding test files.

### Uncovered criteria
None.

## Human Checkpoints

- [ ] Drain any in-flight legacy `/specflow` runs (`worktree_path == repo_path` for non-synthetic) before merging this PR. The `prepare-change` legacy guard will refuse to resume them after merge.
- [ ] After merge, file a follow-up issue for the C-3 downstream-CLI audit (R1-F04 / R2-F06 / R3-F08 / R3-F09): thread `worktree-resolver` through `specflow-review-apply`, `specflow-review-design`, `specflow-challenge-proposal`, `specflow-generate-task-graph`, and update slash-command templates other than approve/reject.
- [ ] Smoke-test the new behavior on a fresh repo: invoke `/specflow` from a feature branch and confirm (a) the user's branch is unchanged, (b) `.specflow/worktrees/<change>/main/` is created, (c) the PR base resolves to the feature branch (not `main`).
- [ ] Verify that `git worktree list` does NOT show stale entries after `/specflow.approve` succeeds end-to-end.
- [ ] Confirm the `.specflow/worktrees/` entry is in user repos' `.gitignore` after this change ships (the template adds it automatically; existing repos may need a one-time merge).
11 changes: 11 additions & 0 deletions openspec/changes/archive/2026-04-25-worktree/current-phase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Current Phase: worktree

- Phase: fix-review
- Round: 3
- Status: has_open_high
- Open High/Critical Findings: 4 件 — "Legacy mode is still preserved despite the spec forbidding dual-path behavior", "Downstream phase CLIs still read and write change artifacts from the caller's git root", "Remaining phase helper CLIs still hardcode the user repo root instead of resolving the main-session worktree", "Slash-command templates still instruct repo-root execution and the old subagent worktree layout"
- Actionable Findings: 5
- Accepted Risks: none
- Latest Changes:
- (no commits yet)
- Next Recommended Action: /specflow.fix_apply
Loading
Loading