feat(start): add --worktree flag with .gitignore and doctor wiring (#62)#63
Conversation
Closes #62. - .gitignore: add /.worktrees/ so fallback worktrees stay untracked. - start.md step 1: accept --worktree as a known flag, carried through as WORKTREE boolean alongside the existing flags. - start.md step 13: split into 13a (in-place checkout, default) and 13b (worktree). 13b probes for superpowers via the same glob doctor uses (~/.claude/plugins/cache/superpowers*), delegates when present, falls back to `git worktree add .worktrees/<branch>` with an explicit clean-abort + `git worktree remove` hint when the target directory already exists. - start.md state schema + step 16 recap: persist worktree_path and render a `cd <path>` hint driven off the resolved branch, so --worktree + --branch=<override> lands on the right path. - doctor.md: new Recommended check 11 (superpowers plugin via PMRP) and new Informational check 18 (.gitignore has /.worktrees/). Renumbered existing informational checks 12-17. - README.md + README.ja.md: "Parallel development (--worktree)" section covering both paths, cleanup after merge, and the when-to- use split between start --worktree and calling superpowers directly. - CONTRIBUTING.md: "Design principles" section codifying three phases (flag-on-start vs new command) and the drift-prevention rule for shared presence checks. No behavior change when --worktree is absent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds opt-in git worktree support to /gh-issue-driven:start via a new --worktree flag, with supporting documentation and doctor checks so operators can work in isolated directories while keeping the main working tree free.
Changes:
- Extend
/gh-issue-driven:startto optionally create/delegate a git worktree and persistworktree_pathin state. - Add
/gh-issue-driven:doctorchecks forsuperpowerspresence and/.worktrees/.gitignoreentry (informational). - Document parallel development workflow in README (EN/JA) and codify related design principles in CONTRIBUTING.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| commands/start.md | Adds --worktree flag behavior, worktree creation/delegation steps, and persists worktree_path in the state schema/docs. |
| commands/doctor.md | Adds superpowers optional plugin check and .gitignore informational check for /.worktrees/. |
| README.md | Documents start --worktree parallel development workflow and cleanup guidance. |
| README.ja.md | Japanese equivalent documentation for --worktree. |
| CONTRIBUTING.md | Adds design principles for phase/flag structure and probe consistency. |
| .gitignore | Ignores /.worktrees/ created by the worktree fallback path. |
| ## 並行開発 (`--worktree`) | ||
|
|
||
| `/gh-issue-driven:start --worktree <issue>` は、in-place で checkout する代わりに、新しい feature branch を別の [git worktree](https://git-scm.com/docs/git-worktree) の中に作成します。これによりメインの作業ディレクトリを `main`(または別の feature branch)のままにしつつ、新しい branch での実装を別ディレクトリで並行して進められます。 | ||
|
|
||
| ### 1 つのフラグに 2 系統のパス | ||
|
|
||
| - **`superpowers` プラグインが入っている場合**: `superpowers:using-git-worktrees` に委譲されます。smart directory selection によって、通常はリポジトリ外の sibling path に worktree が配置されます。 | ||
| - **入っていない場合**: フォールバックとしてリポジトリ内の `.worktrees/<branch>` に作成します。このディレクトリは `/.worktrees/` エントリで gitignore 済み(`/gh-issue-driven:doctor --fix` で自動追記されます)。`--branch=<override>` と併用すると `.worktrees/<override>` に配置されます。 | ||
|
|
||
| どちらのパスでも、`start` の recap に `cd <worktree-path>` のヒントが表示されるので、次のシェルプロンプトで正しい作業ディレクトリに移動できます。 |
There was a problem hiding this comment.
The Japanese README uses /gh-issue-driven:start --worktree <issue>, but start parses issue identifiers first and flags after (per commands/start.md step 1). Update the example to /gh-issue-driven:start <issue> --worktree (and adjust nearby phrasing if needed) so it matches the actual invocation syntax.
| - `PMRP_GLOB=superpowers*` | ||
| - `PMRP_SKILL=superpowers` | ||
| - `PMRP_OFFICIAL=false` | ||
| - If `PLUGIN_FOUND=false`: emit `⚠️ superpowers: not installed — /gh-issue-driven:start --worktree will fall back to direct `git worktree add .worktrees/<branch>`, which still works (no hard requirement)`. |
There was a problem hiding this comment.
This status message wraps the whole sentence in backticks, but also uses backticks again around git worktree add ..., which will break Markdown rendering (nested inline-code delimiters). Rephrase so there's only one level of inline-code formatting (e.g., either remove the outer backticks or avoid backticks around the inner command).
| - If `PLUGIN_FOUND=false`: emit `⚠️ superpowers: not installed — /gh-issue-driven:start --worktree will fall back to direct `git worktree add .worktrees/<branch>`, which still works (no hard requirement)`. | |
| - If `PLUGIN_FOUND=false`: emit ⚠️ superpowers: not installed — `/gh-issue-driven:start --worktree` will fall back to direct `git worktree add .worktrees/<branch>`, which still works (no hard requirement). |
|
|
||
| - **Three phases, not four.** The plugin has exactly three user-facing phases: `/start` → `/ship` → `/tag`. When a feature adds a new capability to a phase, it lands as a **flag on the existing command**, not a new top-level command. For example, `/gh-issue-driven:start --worktree` (issue #62) is a flag, not a hypothetical `/gh-issue-driven:worktree` command — a new top-level command would fragment the mental model and break the `start → ship → tag` progression. New commands are reserved for genuinely new phases (like `/propose` and `/review`, which each represent a distinct phase of work). | ||
| - **Plugin integration is opt-in, never required.** Integrations with other plugins (`superpowers`, `feature-dev`, `claude-c-suite`, `kagura-memory`) degrade to a safe fallback when the plugin is missing. Hard dependencies are limited to `gh`, `git`, `jq`, and (optionally) `python3`. | ||
| - **Presence checks belong in one place.** If two commands need to answer the same "is plugin X installed?" question, they use the **same probe method** (see `commands/start.md` step 13b and `commands/doctor.md` step 11 for the `superpowers` example — both use `ls ~/.claude/plugins/cache/superpowers*`). Drift here silently produces inconsistent behavior. |
There was a problem hiding this comment.
The “MUST NOT drift” note says both commands use the same probe method but cites ls ~/.claude/plugins/cache/superpowers* (without -d), while commands/start.md/commands/doctor.md explicitly use ls -d ... in the actual probe. To keep this principle precise (and avoid future divergence), align the cited probe string here with the exact command used in those steps.
| - **Presence checks belong in one place.** If two commands need to answer the same "is plugin X installed?" question, they use the **same probe method** (see `commands/start.md` step 13b and `commands/doctor.md` step 11 for the `superpowers` example — both use `ls ~/.claude/plugins/cache/superpowers*`). Drift here silently produces inconsistent behavior. | |
| - **Presence checks belong in one place.** If two commands need to answer the same "is plugin X installed?" question, they use the **same probe method** (see `commands/start.md` step 13b and `commands/doctor.md` step 11 for the `superpowers` example — both use `ls -d ~/.claude/plugins/cache/superpowers*`). Drift here silently produces inconsistent behavior. |
| "branch_type": "<type>", | ||
| "is_batch": <IS_BATCH>, | ||
| "worktree_path": "<WORKTREE_PATH or null>", | ||
| "started_at": "<UTC ISO-8601>", | ||
| "phase": "designed", |
There was a problem hiding this comment.
worktree_path is documented as "<WORKTREE_PATH or null>" (quoted). If WORKTREE_PATH is null, this would serialize as the string "null" rather than JSON null, and it differs from how other nullable fields like gate1.escalated_to are represented. Update the schema/example so worktree_path is emitted as either a JSON string or null (unquoted), and align the instructions accordingly (e.g., set a JSON-encoded variable for this field).
| ## Parallel development (`--worktree`) | ||
|
|
||
| `/gh-issue-driven:start --worktree <issue>` creates the feature branch inside a separate [git worktree](https://git-scm.com/docs/git-worktree) instead of checking out in-place. This lets you keep the main working tree on `main` (or on another feature branch) while implementation proceeds in the new one. | ||
|
|
||
| ### Two paths, same flag | ||
|
|
||
| - **With `superpowers` plugin installed**: delegates to `superpowers:using-git-worktrees`, which performs smart directory selection outside the repo (typically a sibling path). | ||
| - **Without `superpowers`**: fallback creates `.worktrees/<branch>` inside the repo. The directory is gitignored via `/.worktrees/` (entry added for you if you run `/gh-issue-driven:doctor --fix`). When combined with `--branch=<override>`, the fallback places the worktree at `.worktrees/<override>`. | ||
|
|
||
| In both cases, the `start` recap prints a `cd <worktree-path>` hint so you land in the new worktree on your next shell prompt. |
There was a problem hiding this comment.
The usage example shows /gh-issue-driven:start --worktree <issue>, but start's argument parser expects one or more issue identifiers first, then flags (step 1). This example won't parse as intended; swap it to /gh-issue-driven:start <issue> --worktree (and similarly in nearby prose like “Use start --worktree …”).
- README.md + README.ja.md: swap usage example to `start <issue> --worktree` since `start` parses issue IDs first then flags (Copilot flag #1, #5). - doctor.md step 11: flatten nested inline-code backticks in the "not installed" status message so Markdown renders cleanly (#2). - CONTRIBUTING.md Design principles: cite the actual probe as `ls -d ~/.claude/plugins/cache/superpowers*` to match start.md step 13b and doctor.md step 11 verbatim (#3). - start.md state schema: change `worktree_path` template from quoted placeholder to unquoted `<WORKTREE_PATH or null>` and clarify serialization convention (JSON string when populated, unquoted `null` when absent) to match other nullable fields like `gate1.escalated_to` (#4). All 5 findings are documentation/spec clarity; no behavior change. Tests remain green: check-frontmatter 8/8, all 4 tests/*.sh pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
||
| - **`SUPERPOWERS_PRESENT=true` — delegate to superpowers**: | ||
|
|
||
| > **Invoke the `superpowers:using-git-worktrees` skill via the Skill tool**, asking it to create a worktree for branch `<branch>` off `<DEFAULT_BRANCH>`. Wait for the skill to complete. Capture the resulting worktree path the skill reports (it performs its own smart directory selection and safety checks — the path may or may not be under `.worktrees/`). |
There was a problem hiding this comment.
In the superpowers delegation path, the Skill invocation name doesn’t follow the repo’s established “Invoke the /plugin:command skill via the Skill tool” convention (e.g. /kagura-memory:session-start, /claude-c-suite:ask, and CONTRIBUTING.md’s guidance). Using superpowers:using-git-worktrees without the leading / may cause the model to fail to route the call through the Skill tool or to treat it as plain text. Align this to the slash-prefixed skill name and the existing phrasing pattern used elsewhere in this file.
| > **Invoke the `superpowers:using-git-worktrees` skill via the Skill tool**, asking it to create a worktree for branch `<branch>` off `<DEFAULT_BRANCH>`. Wait for the skill to complete. Capture the resulting worktree path the skill reports (it performs its own smart directory selection and safety checks — the path may or may not be under `.worktrees/`). | |
| > **Invoke the `/superpowers:using-git-worktrees` skill via the Skill tool**, asking it to create a worktree for branch `<branch>` off `<DEFAULT_BRANCH>`. Wait for the skill to complete. Capture the resulting worktree path the skill reports (it performs its own smart directory selection and safety checks — the path may or may not be under `.worktrees/`). |
| WORKTREE_PATH=".worktrees/<branch>" | ||
| if [ -e "$WORKTREE_PATH" ]; then | ||
| echo "error: '$WORKTREE_PATH' already exists — a stale worktree may be present." | ||
| echo " Clean it up first, then re-run: git worktree remove '$WORKTREE_PATH' && git worktree prune" | ||
| echo " If git reports the worktree is already registered but the directory was manually deleted," | ||
| echo " 'git worktree prune' alone is enough." | ||
| exit 6 | ||
| fi | ||
| mkdir -p "$(dirname "$WORKTREE_PATH")" | ||
| git worktree add "$WORKTREE_PATH" -b "<branch>" "$DEFAULT_BRANCH" | ||
| ``` |
There was a problem hiding this comment.
The fallback stale-worktree guard only checks whether the target path exists ([ -e "$WORKTREE_PATH" ]). A common failure mode is a worktree being registered in git while the directory was manually deleted; in that case the path doesn’t exist, git worktree add fails, and the command will still surface the raw git error—contradicting the instruction immediately below to abort with the custom recovery hint. Consider also checking git worktree list --porcelain for an existing worktree at that path (or trapping git worktree add failure and mapping the “already registered/checked out” cases to the friendly recovery message).
- start.md step 13b: prefix the superpowers skill name with `/` to match the repo's `Invoke the /plugin:command skill via the Skill tool` convention used by every other Skill invocation in this file (`/kagura-memory:session-start`, `/claude-c-suite:ask`). Without the leading slash the Skill tool may fail to route the call or treat it as plain text. Add a cross-reference to CONTRIBUTING.md so future readers know why this phrasing is load-bearing. - start.md step 13b: expand the stale-worktree guard to also query `git worktree list --porcelain`. The existing `[ -e "$WORKTREE_PATH" ]` check missed the failure mode where the directory was manually deleted but git still has the worktree registered — `-e` returns false, the custom recovery hint is skipped, and `git worktree add` surfaces the raw "already registered" error. Now both failure modes (disk clash OR stale registration) route to the same recovery commands. Uses `grep -qxF` for fixed-string exact-line match to avoid substring false positives between registered paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| grep -qE '^/?\.worktrees/?$' .gitignore 2>/dev/null | ||
| ``` | ||
| - Match → `✅ worktree gitignore: /.worktrees/ entry present in .gitignore` | ||
| - No match AND `.gitignore` exists → `ℹ️ worktree gitignore: .gitignore has no /.worktrees/ entry — add one if you plan to use /gh-issue-driven:start --worktree and superpowers is not installed (the fallback path creates worktrees under .worktrees/<branch> inside this repo and they should not be committed)` | ||
| - `.gitignore` absent → `ℹ️ worktree gitignore: .gitignore not present — skipped` | ||
|
|
||
| This is informational only, never `⚠️` or `❌`. The entry only matters for the superpowers-less fallback in `start.md` step 13b; with superpowers installed, worktrees typically live outside the repo (smart directory selection) and the entry is moot. When `fix` flag is set AND the entry is missing AND `.gitignore` exists, append a single `try:` line: | ||
| ``` | ||
| try: printf '%s\n' '' '# Git worktrees from /gh-issue-driven:start --worktree (local-only)' '/.worktrees/' >> .gitignore |
There was a problem hiding this comment.
This .gitignore check reads .gitignore from the current working directory. Since doctor only verifies git rev-parse --is-inside-work-tree, running /gh-issue-driven:doctor from a subdirectory will incorrectly report the worktree ignore entry as missing (even if it exists at the repo root). Consider grepping the repo-root .gitignore (resolve via git rev-parse --show-toplevel) or explicitly cd/instruct running doctor from the repo root for path-sensitive checks.
| grep -qE '^/?\.worktrees/?$' .gitignore 2>/dev/null | |
| ``` | |
| - Match → `✅ worktree gitignore: /.worktrees/ entry present in .gitignore` | |
| - No match AND `.gitignore` exists → `ℹ️ worktree gitignore: .gitignore has no /.worktrees/ entry — add one if you plan to use /gh-issue-driven:start --worktree and superpowers is not installed (the fallback path creates worktrees under .worktrees/<branch> inside this repo and they should not be committed)` | |
| - `.gitignore` absent → `ℹ️ worktree gitignore: .gitignore not present — skipped` | |
| This is informational only, never `⚠️` or `❌`. The entry only matters for the superpowers-less fallback in `start.md` step 13b; with superpowers installed, worktrees typically live outside the repo (smart directory selection) and the entry is moot. When `fix` flag is set AND the entry is missing AND `.gitignore` exists, append a single `try:` line: | |
| ``` | |
| try: printf '%s\n' '' '# Git worktrees from /gh-issue-driven:start --worktree (local-only)' '/.worktrees/' >> .gitignore | |
| repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" && | |
| grep -qE '^/?\.worktrees/?$' "$repo_root/.gitignore" 2>/dev/null | |
| ``` | |
| - Match → `✅ worktree gitignore: /.worktrees/ entry present in repo-root .gitignore` | |
| - No match AND repo-root `.gitignore` exists → `ℹ️ worktree gitignore: repo-root .gitignore has no /.worktrees/ entry — add one if you plan to use /gh-issue-driven:start --worktree and superpowers is not installed (the fallback path creates worktrees under .worktrees/<branch> inside this repo and they should not be committed)` | |
| - Repo-root `.gitignore` absent → `ℹ️ worktree gitignore: repo-root .gitignore not present — skipped` | |
| This is informational only, never `⚠️` or `❌`. The entry only matters for the superpowers-less fallback in `start.md` step 13b; with superpowers installed, worktrees typically live outside the repo (smart directory selection) and the entry is moot. When `fix` flag is set AND the entry is missing AND repo-root `.gitignore` exists, append a single `try:` line: | |
| ``` | |
| try: repo_root="$(git rev-parse --show-toplevel)" && printf '%s\n' '' '# Git worktrees from /gh-issue-driven:start --worktree (local-only)' '/.worktrees/' >> "$repo_root/.gitignore" |
| 13. **Configuration file** | ||
| ```bash | ||
| test -f ~/.claude/gh-issue-driven-config.json && jq empty ~/.claude/gh-issue-driven-config.json | ||
| ``` | ||
| - Missing → informational, defaults will be used. Hint: `/gh-issue-driven:config init`. | ||
| - Present but unparseable → warn with the `jq` error. | ||
|
|
There was a problem hiding this comment.
After inserting the new superpowers check as step 11, the “Configuration file” check is now numbered 13, but the early “Output language” section still says “Configuration file health check at step 11…”. Please update that cross-reference so the step number matches the current numbering (otherwise operators following the instructions will look at the wrong check).
| - **`SUPERPOWERS_PRESENT=false` — direct fallback**: create the worktree under the repo-local `.worktrees/` convention (gitignored via `/.worktrees/`). | ||
|
|
||
| Two stale-state footguns exist and must both be detected before calling `git worktree add`: | ||
| 1. The target directory already exists on disk (operator left a stale copy behind). | ||
| 2. The directory was manually deleted but the worktree is still registered with git (no filesystem entry, but `git worktree add` will reject with "already registered"). Checking only `[ -e "$WORKTREE_PATH" ]` misses this — the registry must be queried explicitly. | ||
|
|
||
| ```bash | ||
| WORKTREE_PATH=".worktrees/<branch>" | ||
| WORKTREE_ABS="$(pwd)/$WORKTREE_PATH" | ||
| STALE="" | ||
| [ -e "$WORKTREE_PATH" ] && STALE="directory exists on disk" | ||
| if [ -z "$STALE" ] && git worktree list --porcelain 2>/dev/null \ | ||
| | awk '/^worktree /{print $2}' | grep -qxF "$WORKTREE_ABS"; then | ||
| STALE="still registered in git worktree list (directory missing on disk)" | ||
| fi | ||
| if [ -n "$STALE" ]; then | ||
| echo "error: '$WORKTREE_PATH' is in a stale state — $STALE." | ||
| echo " Recover with: git worktree remove '$WORKTREE_PATH' && git worktree prune" | ||
| echo " If only the registration is stale (directory already deleted)," | ||
| echo " 'git worktree prune' alone is enough." | ||
| exit 6 | ||
| fi | ||
| mkdir -p "$(dirname "$WORKTREE_PATH")" | ||
| git worktree add "$WORKTREE_PATH" -b "<branch>" "$DEFAULT_BRANCH" | ||
| ``` | ||
|
|
||
| Abort cleanly with the structured error above rather than surfacing the raw `git worktree add` error — both failure modes (filesystem clash, stale registration) need to route the operator to the same recovery commands. Use `grep -qxF` so the comparison is a fixed-string exact match on the whole line, avoiding regex / substring false matches when another registered worktree path contains `WORKTREE_ABS` as a substring. | ||
|
|
||
| In both sub-paths, the branch name (`<branch>`) is the same value computed in step 6 — it already accounts for `--branch=<override>`, so when both `--worktree` and `--branch=<override>` are set the worktree directory is `.worktrees/<override>` (fallback path) or whatever superpowers picked for that branch name (delegated path). |
There was a problem hiding this comment.
In the fallback worktree path, WORKTREE_PATH and WORKTREE_ABS are derived from the current working directory (.worktrees/<branch> + $(pwd)), but /start only verifies “inside a git work tree” and does not ensure it’s being run from the repo root. If the operator runs /gh-issue-driven:start --worktree from a subdirectory, this will create <subdir>/.worktrees/<branch> and the new directory will not be covered by the repo’s /.worktrees/ .gitignore rule (root-anchored), plus the stale-registration check may compare against the wrong absolute path. Suggest resolving the repo root (e.g., via git rev-parse --show-toplevel) and constructing both the filesystem path and the registration-check absolute path from that root; keep the recap WORKTREE_PATH consistent with the actual location (ideally relative to repo root).
| - **`SUPERPOWERS_PRESENT=false` — direct fallback**: create the worktree under the repo-local `.worktrees/` convention (gitignored via `/.worktrees/`). | |
| Two stale-state footguns exist and must both be detected before calling `git worktree add`: | |
| 1. The target directory already exists on disk (operator left a stale copy behind). | |
| 2. The directory was manually deleted but the worktree is still registered with git (no filesystem entry, but `git worktree add` will reject with "already registered"). Checking only `[ -e "$WORKTREE_PATH" ]` misses this — the registry must be queried explicitly. | |
| ```bash | |
| WORKTREE_PATH=".worktrees/<branch>" | |
| WORKTREE_ABS="$(pwd)/$WORKTREE_PATH" | |
| STALE="" | |
| [ -e "$WORKTREE_PATH" ] && STALE="directory exists on disk" | |
| if [ -z "$STALE" ] && git worktree list --porcelain 2>/dev/null \ | |
| | awk '/^worktree /{print $2}' | grep -qxF "$WORKTREE_ABS"; then | |
| STALE="still registered in git worktree list (directory missing on disk)" | |
| fi | |
| if [ -n "$STALE" ]; then | |
| echo "error: '$WORKTREE_PATH' is in a stale state — $STALE." | |
| echo " Recover with: git worktree remove '$WORKTREE_PATH' && git worktree prune" | |
| echo " If only the registration is stale (directory already deleted)," | |
| echo " 'git worktree prune' alone is enough." | |
| exit 6 | |
| fi | |
| mkdir -p "$(dirname "$WORKTREE_PATH")" | |
| git worktree add "$WORKTREE_PATH" -b "<branch>" "$DEFAULT_BRANCH" | |
| ``` | |
| Abort cleanly with the structured error above rather than surfacing the raw `git worktree add` error — both failure modes (filesystem clash, stale registration) need to route the operator to the same recovery commands. Use `grep -qxF` so the comparison is a fixed-string exact match on the whole line, avoiding regex / substring false matches when another registered worktree path contains `WORKTREE_ABS` as a substring. | |
| In both sub-paths, the branch name (`<branch>`) is the same value computed in step 6 — it already accounts for `--branch=<override>`, so when both `--worktree` and `--branch=<override>` are set the worktree directory is `.worktrees/<override>` (fallback path) or whatever superpowers picked for that branch name (delegated path). | |
| - **`SUPERPOWERS_PRESENT=false` — direct fallback**: create the worktree under the repo-local `.worktrees/` convention (gitignored via `/.worktrees/`), anchored at the repository root even when this command is run from a subdirectory. | |
| Two stale-state footguns exist and must both be detected before calling `git worktree add`: | |
| 1. The target directory already exists on disk (operator left a stale copy behind). | |
| 2. The directory was manually deleted but the worktree is still registered with git (no filesystem entry, but `git worktree add` will reject with "already registered"). Checking only `[ -e "$WORKTREE_ABS" ]` misses this — the registry must be queried explicitly. | |
| ```bash | |
| REPO_ROOT_ABS="$(git rev-parse --show-toplevel)" | |
| WORKTREE_PATH=".worktrees/<branch>" | |
| WORKTREE_ABS="$REPO_ROOT_ABS/$WORKTREE_PATH" | |
| STALE="" | |
| [ -e "$WORKTREE_ABS" ] && STALE="directory exists on disk" | |
| if [ -z "$STALE" ] && git -C "$REPO_ROOT_ABS" worktree list --porcelain 2>/dev/null \ | |
| | awk '/^worktree /{print $2}' | grep -qxF "$WORKTREE_ABS"; then | |
| STALE="still registered in git worktree list (directory missing on disk)" | |
| fi | |
| if [ -n "$STALE" ]; then | |
| echo "error: '$WORKTREE_PATH' is in a stale state — $STALE." | |
| echo " Recover with: git -C '$REPO_ROOT_ABS' worktree remove '$WORKTREE_PATH' && git -C '$REPO_ROOT_ABS' worktree prune" | |
| echo " If only the registration is stale (directory already deleted)," | |
| echo " 'git -C \"$REPO_ROOT_ABS\" worktree prune' alone is enough." | |
| exit 6 | |
| fi | |
| mkdir -p "$(dirname "$WORKTREE_ABS")" | |
| git -C "$REPO_ROOT_ABS" worktree add "$WORKTREE_PATH" -b "<branch>" "$DEFAULT_BRANCH" |
Abort cleanly with the structured error above rather than surfacing the raw git worktree add error — both failure modes (filesystem clash, stale registration) need to route the operator to the same recovery commands. Use grep -qxF so the comparison is a fixed-string exact match on the whole line, avoiding regex / substring false matches when another registered worktree path contains WORKTREE_ABS as a substring. Keep WORKTREE_PATH repo-root-relative (for recap and operator commands) and derive WORKTREE_ABS from git rev-parse --show-toplevel for all filesystem checks.
In both sub-paths, the branch name (<branch>) is the same value computed in step 6 — it already accounts for --branch=<override>, so when both --worktree and --branch=<override> are set the fallback worktree directory is repo-root/.worktrees/<override> and the recap path remains .worktrees/<override>, while the delegated path is whatever superpowers picked for that branch name.
- doctor.md step 18: anchor the .gitignore check at `git rev-parse --show-toplevel` instead of the current working directory. Running `/gh-issue-driven:doctor` from a subdirectory would previously false-negative (and offer a misleading fix hint) because `.gitignore` is root-anchored. The `--fix` hint is updated to append to the repo-root `.gitignore` explicitly. - doctor.md Output language section: update the "Configuration file health check at step 11" cross-reference to step 13 — the renumber from the previous commit left this stale pointer in place. - start.md step 13b fallback: pin the worktree path to `$(git rev-parse --show-toplevel)/.worktrees/<branch>` so `/start --worktree` works correctly regardless of which subdirectory it was invoked from. A cwd-relative `.worktrees/` created under a subdir would NOT be covered by the repo-root `/.worktrees/` gitignore rule, and the stale-registration check (which compares against absolute paths from `git worktree list --porcelain`) would silently miss matches. The state file now records the absolute path; the recap `cd` hint works from any shell. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| STALE="" | ||
| [ -e "$WORKTREE_PATH" ] && STALE="directory exists on disk" | ||
| if [ -z "$STALE" ] && git worktree list --porcelain 2>/dev/null \ | ||
| | awk '/^worktree /{print $2}' | grep -qxF "$WORKTREE_PATH"; then |
There was a problem hiding this comment.
The stale-registration probe parses git worktree list --porcelain using awk '{print $2}', which breaks if the worktree path contains spaces (awk will truncate at the first space). Use a parser that preserves the full path after the worktree prefix (e.g., strip the prefix with sed/cut -f2-), so the exact-match comparison remains correct.
| | awk '/^worktree /{print $2}' | grep -qxF "$WORKTREE_PATH"; then | |
| | sed -n 's/^worktree //p' | grep -qxF "$WORKTREE_PATH"; then |
- start.md step 13b fallback: switch the stale-registration parser from
`awk '/^worktree /{print $2}'` to `sed -n 's/^worktree //p'` so worktree
paths containing spaces are preserved verbatim. The awk form truncated
at the first whitespace, which silently broke the exact-match `grep -qxF`
comparison for any path with a space — an unlikely but real failure
mode (e.g., operators working under "Work Projects/<repo>").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
||
| Two stale-state footguns exist and must both be detected before calling `git worktree add`: | ||
| 1. The target directory already exists on disk (operator left a stale copy behind). | ||
| 2. The directory was manually deleted but the worktree is still registered with git (no filesystem entry, but `git worktree add` will reject with "already registered"). Checking only `[ -e "$WORKTREE_ABS" ]` misses this — the registry must be queried explicitly. |
There was a problem hiding this comment.
This paragraph references [ -e "$WORKTREE_ABS" ], but the actual variable used in the fallback is WORKTREE_PATH. Since this is an instruction file, that mismatch can cause confusion when someone follows/edits the procedure; please align the variable name in the prose with the code block.
| 2. The directory was manually deleted but the worktree is still registered with git (no filesystem entry, but `git worktree add` will reject with "already registered"). Checking only `[ -e "$WORKTREE_ABS" ]` misses this — the registry must be queried explicitly. | |
| 2. The directory was manually deleted but the worktree is still registered with git (no filesystem entry, but `git worktree add` will reject with "already registered"). Checking only `[ -e "$WORKTREE_PATH" ]` misses this — the registry must be queried explicitly. |
| This is informational only, never `⚠️` or `❌`. The entry only matters for the superpowers-less fallback in `start.md` step 13b; with superpowers installed, worktrees typically live outside the repo (smart directory selection) and the entry is moot. When `fix` flag is set AND the entry is missing AND repo-root `.gitignore` exists, append a single `try:` line: | ||
| ``` | ||
| try: printf '%s\n' '' '# Git worktrees from /gh-issue-driven:start --worktree (local-only)' '/.worktrees/' >> "$(git rev-parse --show-toplevel)/.gitignore" | ||
| ``` |
There was a problem hiding this comment.
The --fix suggestion for adding the /.worktrees/ entry appends unconditionally and can duplicate the comment/entry if run more than once (or if the grep check false-negatives due to formatting). Since the PR description calls this idempotent, consider making the suggested command guard against existing entries before appending.
- start.md step 13b: align the prose's `[ -e "$WORKTREE_ABS" ]` reference with the actual `WORKTREE_PATH` variable used in the code block. The prior `WORKTREE_ABS` was a leftover from an earlier draft and was confusing without changing behavior. - doctor.md step 18: make the `--fix` `try:` suggestion idempotent by grep-guarding against existing entries before appending. Running the hint twice (or after a grep false-negative on a formatting variant) now produces zero duplication rather than stacking the comment+entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| Bring the local default branch up to date in both sub-paths: | ||
|
|
||
| ```bash | ||
| DEFAULT_BRANCH=<from config> | ||
| git fetch origin "$DEFAULT_BRANCH" | ||
| git checkout "$DEFAULT_BRANCH" | ||
| git pull --ff-only origin "$DEFAULT_BRANCH" |
There was a problem hiding this comment.
In the --worktree path, step 13 still checks out and fast-forwards the local default branch in the current working tree (git checkout "$DEFAULT_BRANCH" + git pull). That defeats the stated goal of keeping the primary working directory free (and can unexpectedly move the operator off whatever branch they were on). Consider limiting checkout/pull to the in-place branch path (13a), and for 13b base the worktree off the remote-tracking ref (e.g., after git fetch, use origin/<DEFAULT_BRANCH> as the worktree start point) so the main worktree’s branch doesn’t change.
- start.md step 13: restructure so the checkout/fast-forward of the default branch only happens in 13a (in-place path). Previously the pre-work ran `git checkout "$DEFAULT_BRANCH"` and `git pull --ff-only` unconditionally, which silently moved the operator's PRIMARY working directory onto the default branch even when --worktree was set — defeating the whole point of --worktree (keeping the primary working directory free for concurrent tasks on whatever branch the operator was on). - 13b now only fetches `origin/<DEFAULT_BRANCH>`, and the fallback `git worktree add` uses the remote-tracking ref as the start point (`origin/$DEFAULT_BRANCH`) instead of the local `$DEFAULT_BRANCH` (which may be behind). The superpowers-delegation ask is updated the same way. - Local `<DEFAULT_BRANCH>` fast-forward for --worktree runs is deferred to whenever the operator chooses to update it themselves (typically after merging this PR). This is the first loop-6 comment that surfaced a genuine design issue rather than a doc/robustness nit — filing as a behavioral fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| Fetch the default branch first (both sub-paths need an up-to-date remote ref): | ||
|
|
||
| ```bash | ||
| DEFAULT_BRANCH=<from config> | ||
| git fetch origin "$DEFAULT_BRANCH" | ||
| ``` | ||
|
|
||
| Then branch on `WORKTREE`: | ||
|
|
||
| #### 13a. In-place branch (default — `WORKTREE=false`) | ||
|
|
||
| Move the current working tree to the default branch, fast-forward, and branch from there: | ||
|
|
||
| ```bash | ||
| git checkout "$DEFAULT_BRANCH" | ||
| git pull --ff-only origin "$DEFAULT_BRANCH" | ||
| git checkout -b <branch> |
There was a problem hiding this comment.
Step 13 now unconditionally runs git fetch origin "$DEFAULT_BRANCH" even for the default in-place path (13a), and 13a still does git pull --ff-only origin "$DEFAULT_BRANCH" which already performs a fetch. This adds an extra network round-trip and is a behavior change for the no---worktree flow (the PR description says non-worktree is unchanged). Consider either (a) moving the git fetch into 13b only, or (b) keeping the shared fetch but replacing the subsequent git pull with an ff-only update that doesn’t re-fetch (e.g., checkout + merge/ff against the fetched remote ref).
| - The `try:` hint commands themselves (e.g. `apt install jq`, `/plugin install ...`) — only the surrounding prose is localized | ||
|
|
||
| The Configuration file health check at step 11 reads `~/.claude/gh-issue-driven-config.json` as part of its informational scan; doctor's own `lang` read in this section is just-in-time, separate from step 11. (Same just-in-time pattern as the Copilot setup section's read of `copilot.skip_setup_prompt`.) | ||
| The Configuration file health check at step 13 reads `~/.claude/gh-issue-driven-config.json` as part of its informational scan; doctor's own `lang` read in this section is just-in-time, separate from step 13. (Same just-in-time pattern as the Copilot setup section's read of `copilot.skip_setup_prompt`.) |
There was a problem hiding this comment.
After renumbering the informational checks, the step reference for the Configuration file health check is inconsistent within this file: this paragraph correctly says step 13, but later the Copilot setup section still says “the broader Configuration file health check at step 11”. Please update that later reference to step 13 as well so the internal cross-references stay accurate.
- start.md step 13: remove the shared `git fetch origin "$DEFAULT_BRANCH"` introduced in loop 6. Each sub-path now owns its own remote-refresh: 13a stays bit-for-bit identical to the pre-`--worktree` pre-work (pull already fetches, so no separate fetch), 13b runs an explicit `git fetch` just before `git worktree add ... origin/<DEFAULT_BRANCH>`. This restores the "no behavior change when --worktree is absent" contract — 13a no longer incurs the extra network round-trip that loop 6's shared fetch introduced. - doctor.md Copilot setup section: update the remaining "step 11" cross-reference to "step 13" to match the step renumber. Loop 3 fixed the Output-language section; this loop catches the pair in the "Check logic" paragraph that was still pointing at the old number. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
||
| This is informational only, never `⚠️` or `❌`. The entry only matters for the superpowers-less fallback in `start.md` step 13b; with superpowers installed, worktrees typically live outside the repo (smart directory selection) and the entry is moot. When `fix` flag is set AND the entry is missing AND repo-root `.gitignore` exists, append a single `try:` line. The suggested command is **idempotent** — it grep-guards against existing entries before appending, so re-running it (or running it after the grep check false-negatives on a formatting variant) is safe: | ||
| ``` | ||
| try: gitignore="$(git rev-parse --show-toplevel)/.gitignore" && grep -qxE '/?\.worktrees/?' "$gitignore" 2>/dev/null || printf '%s\n' '' '# Git worktrees from /gh-issue-driven:start --worktree (local-only)' '/.worktrees/' >> "$gitignore" |
There was a problem hiding this comment.
The --fix try-line’s guard grep -qxE '/?\.worktrees/?' is not equivalent to the check regex (^/?\.worktrees/?$). Because it’s unanchored, it can false-positive on unrelated lines (e.g. comments or foo/.worktrees/) and then skip appending the required /.worktrees/ entry. Make the guard use the same anchored pattern as the check (and ideally ignore commented lines) so --fix is reliably idempotent without accidental matches.
| try: gitignore="$(git rev-parse --show-toplevel)/.gitignore" && grep -qxE '/?\.worktrees/?' "$gitignore" 2>/dev/null || printf '%s\n' '' '# Git worktrees from /gh-issue-driven:start --worktree (local-only)' '/.worktrees/' >> "$gitignore" | |
| try: gitignore="$(git rev-parse --show-toplevel)/.gitignore" && grep -vE '^[[:space:]]*#' "$gitignore" 2>/dev/null | grep -qxE '^/?\.worktrees/?$' || printf '%s\n' '' '# Git worktrees from /gh-issue-driven:start --worktree (local-only)' '/.worktrees/' >> "$gitignore" |
|
|
||
| Skip this step entirely if `DRY_RUN` is true. | ||
|
|
||
| Each sub-path owns its own remote-refresh step — 13a's `git pull` already performs a fetch, so no shared pre-fetch is needed. Keeping the fetch inside each sub-path also preserves the "no behavior change when `--worktree` is absent" contract for 13a (network calls identical to the pre-`--worktree` version of this step). |
There was a problem hiding this comment.
Step 13 intro says keeping the fetch inside each sub-path preserves “no behavior change when --worktree is absent” because network calls are identical to the pre---worktree version, but 13a no longer runs the explicit git fetch origin "$DEFAULT_BRANCH" line (only git pull, which fetches internally). Either restore the explicit fetch (if identical network behavior is important) or adjust this rationale to avoid claiming identical network calls when the sequence has changed.
| Each sub-path owns its own remote-refresh step — 13a's `git pull` already performs a fetch, so no shared pre-fetch is needed. Keeping the fetch inside each sub-path also preserves the "no behavior change when `--worktree` is absent" contract for 13a (network calls identical to the pre-`--worktree` version of this step). | |
| Each sub-path owns its own remote-refresh step. In 13a, `git pull` already performs the required fetch, so no shared pre-fetch is needed. Keeping the remote refresh inside each sub-path preserves the default-path flow when `--worktree` is absent without requiring a separate top-level fetch step. |
| Resolve the repo root first so the check works regardless of which subdirectory doctor is invoked from — `.gitignore` is anchored to the repo root, so reading the cwd-relative file would false-negative for any invocation outside the top level. | ||
|
|
||
| ```bash | ||
| REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) | ||
| if [ -n "$REPO_ROOT" ] && [ -f "$REPO_ROOT/.gitignore" ]; then | ||
| grep -qE '^/?\.worktrees/?$' "$REPO_ROOT/.gitignore" | ||
| fi | ||
| ``` | ||
|
|
||
| - Match → `✅ worktree gitignore: /.worktrees/ entry present in repo-root .gitignore` | ||
| - No match AND repo-root `.gitignore` exists → `ℹ️ worktree gitignore: .gitignore has no /.worktrees/ entry — add one if you plan to use /gh-issue-driven:start --worktree and superpowers is not installed (the fallback path creates worktrees under .worktrees/<branch> inside this repo and they should not be committed)` | ||
| - `.gitignore` absent at the repo root → `ℹ️ worktree gitignore: repo-root .gitignore not present — skipped` |
There was a problem hiding this comment.
The step 18 probe block uses an if ...; then grep ...; fi with no else. In bash, when the condition is false (e.g. repo-root .gitignore missing), the compound if typically exits 0, which can be misinterpreted as “match found” if the caller is using exit status to choose ✅ vs ℹ️. Consider restructuring the probe so its exit status clearly distinguishes: (a) entry present, (b) entry missing, and (c) .gitignore absent/skipped (e.g., explicit test -f + grep pipeline, or add an else that returns non-zero / prints a sentinel).
| Resolve the repo root first so the check works regardless of which subdirectory doctor is invoked from — `.gitignore` is anchored to the repo root, so reading the cwd-relative file would false-negative for any invocation outside the top level. | |
| ```bash | |
| REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) | |
| if [ -n "$REPO_ROOT" ] && [ -f "$REPO_ROOT/.gitignore" ]; then | |
| grep -qE '^/?\.worktrees/?$' "$REPO_ROOT/.gitignore" | |
| fi | |
| ``` | |
| - Match → `✅ worktree gitignore: /.worktrees/ entry present in repo-root .gitignore` | |
| - No match AND repo-root `.gitignore` exists → `ℹ️ worktree gitignore: .gitignore has no /.worktrees/ entry — add one if you plan to use /gh-issue-driven:start --worktree and superpowers is not installed (the fallback path creates worktrees under .worktrees/<branch> inside this repo and they should not be committed)` | |
| - `.gitignore` absent at the repo root → `ℹ️ worktree gitignore: repo-root .gitignore not present — skipped` | |
| Resolve the repo root first so the check works regardless of which subdirectory doctor is invoked from — `.gitignore` is anchored to the repo root, so reading the cwd-relative file would false-negative for any invocation outside the top level. Make the probe emit an explicit sentinel so the caller can distinguish “present”, “missing”, and “absent/skipped” without depending on ambiguous `if ... fi` exit status. | |
| ```bash | |
| REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) | |
| if [ -n "$REPO_ROOT" ] && [ -f "$REPO_ROOT/.gitignore" ]; then | |
| if grep -qE '^/?\.worktrees/?$' "$REPO_ROOT/.gitignore"; then | |
| printf 'present\n' | |
| else | |
| printf 'missing\n' | |
| fi | |
| else | |
| printf 'absent\n' | |
| fi | |
| ``` | |
| - Probe output `present` → `✅ worktree gitignore: /.worktrees/ entry present in repo-root .gitignore` | |
| - Probe output `missing` → `ℹ️ worktree gitignore: .gitignore has no /.worktrees/ entry — add one if you plan to use /gh-issue-driven:start --worktree and superpowers is not installed (the fallback path creates worktrees under .worktrees/<branch> inside this repo and they should not be committed)` | |
| - Probe output `absent` → `ℹ️ worktree gitignore: repo-root .gitignore not present — skipped` |
| ### Two paths, same flag | ||
|
|
||
| - **With `superpowers` plugin installed**: delegates to `superpowers:using-git-worktrees`, which performs smart directory selection outside the repo (typically a sibling path). | ||
| - **Without `superpowers`**: fallback creates `.worktrees/<branch>` inside the repo. The directory is gitignored via `/.worktrees/` (entry added for you if you run `/gh-issue-driven:doctor --fix`). When combined with `--branch=<override>`, the fallback places the worktree at `.worktrees/<override>`. |
There was a problem hiding this comment.
This line says the /.worktrees/ entry is “added for you” by running /gh-issue-driven:doctor --fix, but doctor is explicitly non-mutating (it only prints try: hints) and its args are fix/verbose (no leading --). Please reword to reflect the actual contract (e.g., running /gh-issue-driven:doctor fix prints an idempotent try: command to append the entry, which the operator runs manually).
| - **Without `superpowers`**: fallback creates `.worktrees/<branch>` inside the repo. The directory is gitignored via `/.worktrees/` (entry added for you if you run `/gh-issue-driven:doctor --fix`). When combined with `--branch=<override>`, the fallback places the worktree at `.worktrees/<override>`. | |
| - **Without `superpowers`**: fallback creates `.worktrees/<branch>` inside the repo. The directory should be gitignored via `/.worktrees/`; running `/gh-issue-driven:doctor fix` prints an idempotent `try:` command to append that entry, which the operator runs manually. When combined with `--branch=<override>`, the fallback places the worktree at `.worktrees/<override>`. |
| ### 1 つのフラグに 2 系統のパス | ||
|
|
||
| - **`superpowers` プラグインが入っている場合**: `superpowers:using-git-worktrees` に委譲されます。smart directory selection によって、通常はリポジトリ外の sibling path に worktree が配置されます。 | ||
| - **入っていない場合**: フォールバックとしてリポジトリ内の `.worktrees/<branch>` に作成します。このディレクトリは `/.worktrees/` エントリで gitignore 済み(`/gh-issue-driven:doctor --fix` で自動追記されます)。`--branch=<override>` と併用すると `.worktrees/<override>` に配置されます。 |
There was a problem hiding this comment.
ここでは /.worktrees/ が /gh-issue-driven:doctor --fix で「自動追記される」と書かれていますが、doctor は基本 read-only で try: ヒントを出すだけ(実際の追記はオペレータが手動実行)であり、引数も fix(-- なし)です。コマンド仕様に合わせて、/gh-issue-driven:doctor fix が /.worktrees/ 追記用の try: コマンドを表示する、という表現に直してください。
| - **入っていない場合**: フォールバックとしてリポジトリ内の `.worktrees/<branch>` に作成します。このディレクトリは `/.worktrees/` エントリで gitignore 済み(`/gh-issue-driven:doctor --fix` で自動追記されます)。`--branch=<override>` と併用すると `.worktrees/<override>` に配置されます。 | |
| - **入っていない場合**: フォールバックとしてリポジトリ内の `.worktrees/<branch>` に作成します。このディレクトリは `/.worktrees/` エントリで gitignore しておく想定で、`/gh-issue-driven:doctor fix` を実行すると `/.worktrees/` 追記用の `try:` コマンドが表示されます。`--branch=<override>` と併用すると `.worktrees/<override>` に配置されます。 |
- doctor.md step 18 probe: restructure as if/elif/else with an explicit WORKTREE_GITIGNORE_STATUS variable (match | missing | absent) so the three outcomes are each driven by a distinct code path. The prior `if ... then grep ...; fi` leaned on nested-grep exit status which is ambiguous (a missing .gitignore exits 0 from the outer `if`, making it look like a match to callers that check status alone). - doctor.md step 18 --fix try-line: tighten the idempotency guard to use the same anchored `^/?\.worktrees/?$` pattern as the check, and strip comment lines before grepping. The previous unanchored `grep -qxE '/?\.worktrees/?'` could false-positive on comments or on substrings like `foo/.worktrees/bar` and silently skip appending. - start.md step 13 intro: drop the "network calls identical to the pre-`--worktree` version" claim. That wasn't strictly true after loop 7's restructure (the pre-edit code ran an explicit `git fetch origin "$DEFAULT_BRANCH"` which 13a no longer does separately — pull does its own fetch). New rationale focuses on sequence preservation rather than byte-identical network calls. - README.md + README.ja.md Parallel-development section: correct the claim that doctor auto-edits .gitignore. `doctor fix` is read-only; it prints an idempotent `try:` command which the operator runs manually. Also drop the non-existent `--fix` dash-dash syntax (actual arg is `fix`, no leading `--`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| STALE="" | ||
| [ -e "$WORKTREE_PATH" ] && STALE="directory exists on disk" | ||
| if [ -z "$STALE" ] && git worktree list --porcelain 2>/dev/null \ | ||
| | sed -n 's/^worktree //p' | grep -qxF "$WORKTREE_PATH"; then | ||
| STALE="still registered in git worktree list (directory missing on disk)" | ||
| fi | ||
| if [ -n "$STALE" ]; then | ||
| echo "error: '$WORKTREE_PATH' is in a stale state — $STALE." | ||
| echo " Recover with: git worktree remove '$WORKTREE_PATH' && git worktree prune" | ||
| echo " If only the registration is stale (directory already deleted)," | ||
| echo " 'git worktree prune' alone is enough." | ||
| exit 6 | ||
| fi | ||
| mkdir -p "$(dirname "$WORKTREE_PATH")" | ||
| git worktree add "$WORKTREE_PATH" -b "<branch>" "origin/$DEFAULT_BRANCH" | ||
| ``` | ||
|
|
||
| `WORKTREE_PATH` is stored in the state file and rendered in the step 16 recap exactly as computed above — i.e. an absolute path under the repo root. The `cd <WORKTREE_PATH>` hint then works from any shell regardless of the operator's current directory. Abort cleanly with the structured error above rather than surfacing the raw `git worktree add` error — both failure modes (filesystem clash, stale registration) need to route the operator to the same recovery commands. Use `grep -qxF` so the comparison is a fixed-string exact match on the whole line, avoiding regex / substring false matches when another registered worktree path contains `WORKTREE_PATH` as a substring. |
There was a problem hiding this comment.
In the --worktree fallback stale-state handling, the recovery hint always recommends git worktree remove '$WORKTREE_PATH' && git worktree prune even when the directory merely exists on disk but is NOT registered as a git worktree. In that case git worktree remove will fail and the operator won’t have a correct remediation path. Consider checking registration regardless of the on-disk existence and tailoring the error/recovery guidance: if registered → git worktree remove … (+ prune), if unregistered → instruct to delete/rename the directory (or choose a different path), and keep git worktree prune for the “registered but missing on disk” case. Also update the failure-mode row for “--worktree target already exists” to match the refined recovery guidance.
| STALE="" | |
| [ -e "$WORKTREE_PATH" ] && STALE="directory exists on disk" | |
| if [ -z "$STALE" ] && git worktree list --porcelain 2>/dev/null \ | |
| | sed -n 's/^worktree //p' | grep -qxF "$WORKTREE_PATH"; then | |
| STALE="still registered in git worktree list (directory missing on disk)" | |
| fi | |
| if [ -n "$STALE" ]; then | |
| echo "error: '$WORKTREE_PATH' is in a stale state — $STALE." | |
| echo " Recover with: git worktree remove '$WORKTREE_PATH' && git worktree prune" | |
| echo " If only the registration is stale (directory already deleted)," | |
| echo " 'git worktree prune' alone is enough." | |
| exit 6 | |
| fi | |
| mkdir -p "$(dirname "$WORKTREE_PATH")" | |
| git worktree add "$WORKTREE_PATH" -b "<branch>" "origin/$DEFAULT_BRANCH" | |
| ``` | |
| `WORKTREE_PATH` is stored in the state file and rendered in the step 16 recap exactly as computed above — i.e. an absolute path under the repo root. The `cd <WORKTREE_PATH>` hint then works from any shell regardless of the operator's current directory. Abort cleanly with the structured error above rather than surfacing the raw `git worktree add` error — both failure modes (filesystem clash, stale registration) need to route the operator to the same recovery commands. Use `grep -qxF` so the comparison is a fixed-string exact match on the whole line, avoiding regex / substring false matches when another registered worktree path contains `WORKTREE_PATH` as a substring. | |
| ON_DISK="" | |
| REGISTERED="" | |
| [ -e "$WORKTREE_PATH" ] && ON_DISK="yes" | |
| if git worktree list --porcelain 2>/dev/null \ | |
| | sed -n 's/^worktree //p' | grep -qxF "$WORKTREE_PATH"; then | |
| REGISTERED="yes" | |
| fi | |
| if [ -n "$ON_DISK" ] || [ -n "$REGISTERED" ]; then | |
| if [ -n "$ON_DISK" ] && [ -n "$REGISTERED" ]; then | |
| echo "error: '$WORKTREE_PATH' is in a stale state — directory exists on disk and is still registered in git worktree list." | |
| echo " Recover with: git worktree remove '$WORKTREE_PATH' && git worktree prune" | |
| elif [ -n "$REGISTERED" ]; then | |
| echo "error: '$WORKTREE_PATH' is in a stale state — still registered in git worktree list (directory missing on disk)." | |
| echo " Recover with: git worktree prune" | |
| else | |
| echo "error: '$WORKTREE_PATH' already exists on disk and is not registered as a git worktree." | |
| echo " Recover by deleting or renaming that directory, or choose a different --worktree path." | |
| fi | |
| exit 6 | |
| fi | |
| mkdir -p "$(dirname "$WORKTREE_PATH")" | |
| git worktree add "$WORKTREE_PATH" -b "<branch>" "origin/$DEFAULT_BRANCH" |
WORKTREE_PATH is stored in the state file and rendered in the step 16 recap exactly as computed above — i.e. an absolute path under the repo root. The cd <WORKTREE_PATH> hint then works from any shell regardless of the operator's current directory. Abort cleanly with the structured error above rather than surfacing the raw git worktree add error, but tailor the recovery guidance to the detected state: if the path is still registered as a git worktree, use git worktree remove '$WORKTREE_PATH' && git worktree prune when the directory still exists and git worktree prune when the registration is stale and the directory is already gone; if the path merely exists on disk and is not registered, instruct the operator to delete or rename that directory (or choose a different path). Use grep -qxF so the comparison is a fixed-string exact match on the whole line, avoiding regex / substring false matches when another registered worktree path contains WORKTREE_PATH as a substring.
- start.md step 13b fallback: replace the single STALE string variable
with independent ON_DISK and REGISTERED probes, then dispatch the
correct recovery message per the 4 possible (disk, registry) shapes.
The prior logic conflated "dir on disk but not registered" with "dir
on disk AND registered" — both emitted
`git worktree remove $PATH && git worktree prune`
but `git worktree remove` FAILS with "not a working tree" when the
path is merely a stray directory that git doesn't know about. The
operator was being pointed at a command that couldn't possibly work.
- Add a shape table documenting the 4 (disk × registry) cases for
future readers: yes/yes → remove+prune, yes/no → manual rm, no/yes →
prune, no/no → proceed.
- Update the Failure-modes table row for "--worktree target already
exists" to mention the shape-specific recovery (was generic).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| ```bash | ||
| REPO_ROOT=$(git rev-parse --show-toplevel) | ||
| WORKTREE_PATH="$REPO_ROOT/.worktrees/<branch>" # absolute, anchored to the repo root | ||
| ON_DISK="" |
There was a problem hiding this comment.
WORKTREE_PATH is derived from <branch> (which can be set verbatim via --branch=<name> in step 6a). Because the override allow-list permits . and /, a value containing .. path segments can escape $REPO_ROOT/.worktrees/ (e.g. --branch=../tmp/x), causing directories/worktrees to be created outside the intended gitignored area. Add an explicit safety check before constructing/using WORKTREE_PATH (e.g., validate with git check-ref-format --branch and reject .. path elements / normalize+assert the resolved path stays under $REPO_ROOT/.worktrees/).
| <if WORKTREE=true:> | ||
| Worktree <WORKTREE_PATH> | ||
| → cd <WORKTREE_PATH> to work on this branch | ||
| </if> |
There was a problem hiding this comment.
The recap prints the Worktree <WORKTREE_PATH> block whenever WORKTREE=true, but step 13 (where WORKTREE_PATH is computed) is skipped entirely when DRY_RUN=true. As a result, dry-run combined with --worktree will reference an unset/unknown WORKTREE_PATH in the recap. Either suppress the worktree recap when DRY_RUN is true, or compute a no-side-effects WORKTREE_PATH value for dry-run runs (without creating the worktree / invoking superpowers).
- start.md step 1a: add `git check-ref-format --branch` as a second gate
on BRANCH_OVERRIDE, after the existing regex. The regex
`^[a-zA-Z0-9._/-]{1,100}$` permits `..` (both dots allowed, `/` allowed)
which means `--branch=../foo` would slip through, then step 13b's
`WORKTREE_PATH="$REPO_ROOT/.worktrees/<branch>"` resolves to a path
outside `.worktrees/` — escaping the gitignored zone. git's own
ref-name rules reject `..`, leading `.`, `@{`, etc. so adding
`git check-ref-format` closes the gap. The CSO gate2 advisor had
flagged this earlier as a low-severity observation; Copilot loop 10
made the exploit path concrete.
- start.md step 16 recap: guard the Worktree block on
`WORKTREE=true AND NOT DRY_RUN`. Step 13 (which sets WORKTREE_PATH)
is skipped in dry-run, so printing the block would emit a literal
placeholder for an unset variable. The `[DRY RUN]` banner plus the
Branch line convey the preview intent; the operator re-runs without
`dry-run` to get the actual worktree location.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| | Default branch fast-forward fails | Abort. Tell the user to reconcile manually. Never auto-rebase. | | ||
| | Branch already exists | Auto-suffix with today's UTC date and inform the user. | | ||
| | `--worktree` target already exists (fallback path) | Abort with the shape-specific recovery hint from step 13b: `git worktree remove` + `prune` if both on disk and registered; manual `rm`/rename if on disk only (unregistered); `git worktree prune` alone if only the registration is stale. Do not auto-clean. | | ||
| | `--worktree` + superpowers delegation fails | Abort with the skill's error. Operator can retry without `--worktree` for in-place checkout, or uninstall superpowers to hit the fallback. | |
There was a problem hiding this comment.
The failure-mode for “--worktree + superpowers delegation fails” currently aborts and suggests uninstalling superpowers to hit the fallback. Since superpowers is meant to be an optional enhancement, consider degrading to the direct git worktree add .worktrees/<branch> fallback when the Skill invocation fails (especially for “skill not found” / transient errors), so start --worktree remains usable even if the plugin is installed but broken.
| | `--worktree` + superpowers delegation fails | Abort with the skill's error. Operator can retry without `--worktree` for in-place checkout, or uninstall superpowers to hit the fallback. | | |
| | `--worktree` + superpowers delegation fails | Degrade: surface the skill error as a warning, then fall back to the direct native path (`git worktree add .worktrees/<branch>`). Only abort if the fallback path itself fails; in that case, use the same shape-specific recovery guidance as the fallback-path failure in step 13b. | |
| Each sub-path owns its own remote-refresh step. In 13a, `git pull` already performs the required fetch, so no shared pre-fetch is needed. Keeping the remote refresh inside each sub-path preserves the default-path flow when `--worktree` is absent without requiring a separate top-level fetch step — 13a's sequence `checkout → pull → checkout -b` is unchanged from the pre-`--worktree` version. | ||
|
|
||
| Branch on `WORKTREE`: | ||
|
|
||
| #### 13a. In-place branch (default — `WORKTREE=false`) | ||
|
|
||
| Move the current working tree to the default branch, fast-forward, and branch from there. `git pull --ff-only` does its own fetch — no separate `git fetch` needed. | ||
|
|
||
| ```bash | ||
| DEFAULT_BRANCH=<from config> | ||
| git fetch origin "$DEFAULT_BRANCH" | ||
| git checkout "$DEFAULT_BRANCH" | ||
| git pull --ff-only origin "$DEFAULT_BRANCH" | ||
| git checkout -b <branch> |
There was a problem hiding this comment.
Step 13a removed the explicit git fetch origin "$DEFAULT_BRANCH" line and now relies on git pull --ff-only to fetch. Since the PR description/notes claim the non---worktree path was moved “verbatim” / is unchanged, either restore the original fetch (if you want truly identical semantics) or update the surrounding narrative/PR description to reflect that this is an intentional behavior change (even if functionally equivalent).
- start.md step 13b: superpowers-delegation failure now degrades to the native fallback (with a warning), rather than aborting and asking the operator to uninstall a plugin. Fallback + superpowers are functionally equivalent for this plugin's purposes, so a broken superpowers skill shouldn't block --worktree. The Failure-modes table row is updated to match. - start.md step 13a: restore the explicit `git fetch origin "$DEFAULT_BRANCH"` before `git checkout`, reverting the loop-7 optimization. True byte-for-byte parity with the pre-`--worktree` version matters more than shaving one redundant network call — the "no behavior change when --worktree is absent" guarantee is the load- bearing promise of this PR. The intro narrative is updated to reflect the intentional redundancy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes #62
Summary
--worktreeflag to/gh-issue-driven:startso implementation on a new issue can happen in an isolated git worktree rather than in-place, leaving the primary working directory free for concurrent tasks.superpowers:using-git-worktreesis installed, delegate path selection to it; otherwise create.worktrees/<branch>inside the repo (gitignored)./gh-issue-driven:doctorgains two aligned checks:superpowersplugin presence (same glob probe asstart.md) and.gitignoreentry for/.worktrees/.--worktreeis absent — the existingstart→ship→tagflow is untouched.Implementation notes
commands/start.mdstep 13 splits into13a(existing in-place path, semantically identical) and13b(new worktree path). The bash for the non-worktree case is moved verbatim under the new sub-header — not rewritten.13bprobes superpowers vials -d ~/.claude/plugins/cache/superpowers*.doctor.mdstep 11 uses the identical glob with aMUST NOT driftnote in both files. CONTRIBUTING.md codifies this as a design principle ("Presence checks belong in one place").worktree_path(string or null). Absent is equivalent to null, so existing v1/v2 state files remain compatible without migration.Worktree <path>+cd <path>line is resolved fromWORKTREE_PATH, so--worktree+--branch=<override>produces.worktrees/<override>(fallback path) and thecdhint always matches reality.doctor.mdcheck 18 (.gitignorehas/.worktrees/) is strictly informational — never the warning / error emoji. It only matters when the operator uses--worktreewithoutsuperpowers, and even then the fallback still works.Manual verification
Per the issue's AC (new automated tests are out of scope —
tests/*.shis static verification only):a—/gh-issue-driven:start <new-issue> --worktreeon a fresh issue withoutsuperpowersinstalled: branch created at.worktrees/<branch>, recap prints thecdhint, state file hasworktree_pathpopulated.b—/gh-issue-driven:start <new-issue> --worktree --branch=custom: worktree lands at.worktrees/custom, recap hint matches.c—/gh-issue-driven:doctorwith and without/.worktrees/in.gitignore: check 18 reports present vs. informational-missing.--fixhint appends the entry idempotently.d—/gh-issue-driven:start <new-issue>without the flag: existing in-place behavior is unchanged (non-regression proof).All existing CI checks pass locally:
check-frontmatter.py→ 8/8 command files OKtests/copilot-detection.sh→ 5/5 PASStests/enum-sync-check.sh→ 24/24 in synctests/jq-sync-check.sh→ 5/5 in synctests/test_state_schema.sh→ 39/39 PASSPre-PR review summary
Full reviews are saved in the plugin cache:
Gate2 advisors also surfaced three low-severity future-observation items (none blocking this PR):
git check-ref-format --branchinstart.mdstep 1a as belt-and-suspenders validation for--branch=<override>.tests/could gain a trivial glob-sync check acrossstart.mdstep 13b anddoctor.mdstep 11 to enforce the "MUST NOT drift" contract automatically.commands/_shared/.Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
🤖 Generated via /gh-issue-driven:ship