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
18 changes: 17 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,13 @@ Use research docs for exploratory work that is not yet ready for a plan but may
Location:

```text
docs/research-YYYY-MM-DD-<short-title>.md
docs/researches/research-YYYY-MM-DD-<short-title>.md
```

Notes:

- Use the creation date and a short, kebab-case title.
- Keep all research docs inside `docs/researches/` (not directly under `docs/`).
- Keep scope focused on a single topic or question.
- If research becomes actionable, create a plan doc and link to it.

Expand All @@ -94,11 +95,15 @@ agent: <agent name>
Suggested sections:

- Goal
- Milestone Goal (optional but recommended)
- Key Findings
- Implications or Recommendations
- Open Questions (optional)
- References (use footnote-style links)

Optional metadata:
- `milestone: v0.1.0` (or another release milestone) for feature-track research.

Traceability:

- Research docs should include a short "Related Plans" section when applicable, with links to plan docs.
Expand Down Expand Up @@ -132,6 +137,17 @@ agent: <agent name>

---

### Path Reference Policy (Default + Exceptions)

- Use repository-relative paths for repository files in documentation (for example: `.github/workflows/release.yml`, `docs/plans/jobs/...`, `src/...`).
- Do not include real machine-specific absolute paths that may reveal local user/home details from the current environment.
- When useful for explanation, sanitized OS-specific absolute-path examples are allowed (for example: `/Users/alice/...`, `/home/alice/...`, `C:\Users\Alice\...`, `$HOME/.config/...`, `%USERPROFILE%\...`).
- Prefer placeholder usernames like `alice` or `bob` in examples.
- Keep this case-by-case: prefer clarity for behavior/docs examples, but avoid disclosing actual local paths.
- This policy applies to plan, research, and job documents, including summaries, change lists, and verification notes.

---

### Status Meanings

- `draft` — idea or exploration, not executed
Expand Down
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,10 @@ gwtt list "my-task" --strict
# Filter by branch
gwtt list --branch feature-branch

# Custom path layout still searchable by task name
gwtt create new-task -p ./.worktrees/new-task
gwtt list new-task -o raw

# Show absolute paths
gwtt list --abs

Expand All @@ -344,6 +348,15 @@ gwtt --mode codex list
| `--strict` | | Require exact task match |
| `--grid` | | Render table with grid borders |

**Task lookup behavior (classic mode):**

- `list <task>` first resolves task names from `<repo>_<task>` paths.
- If the path does not match that convention, task lookup falls back to branch-backed inference for non-main, non-detached worktrees.
- `--branch` remains the explicit/authoritative branch filter.

> [!NOTE]
> If you place worktrees under a nested path inside the main repo (for example, `./.worktrees/<task>`), add that root path (for example, `.worktrees/`) to `.gitignore` in the main checkout.

### Checking Status

```bash
Expand Down Expand Up @@ -376,6 +389,11 @@ gwtt --mode codex status
| `--strict` | | Require exact task match |
| `--grid` | | Render table with grid borders |

**Task lookup behavior (classic mode):**

- `status <task>` uses the same task resolution as `list <task>` (path-first, then branch-backed fallback for eligible rows).
- `--branch` remains the explicit/authoritative branch filter.

### Finishing Tasks

```bash
Expand Down Expand Up @@ -572,12 +590,21 @@ gwtt-new() {

When using `--output raw` with `list`:

- If no matching worktree exists but the branch does, returns the main worktree path
- Requires either a task filter or `--branch` flag
- If a matching worktree row exists (including custom path layouts), raw output uses that row.
- If no matching worktree exists but the branch does, raw output falls back to a synthetic main-worktree row.
- Fallback row output respects `--field`:
- `path` -> main worktree path
- `branch` -> fallback branch name
- `task` -> `-`
- Requires either a task filter or `--branch` flag.
- `--branch` remains the explicit/authoritative branch selector.

```bash
# Returns path even if no worktree exists (fallback to main repo)
gwtt list feature-branch -o raw

# Field-aware fallback output
gwtt list feature-branch -o raw -f branch
```

---
Expand Down
28 changes: 28 additions & 0 deletions cli/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,34 @@ func fallbackPathForBranch(ctx context.Context, runner git.Runner, repoRoot, bra
return path, true, nil
}

func deriveClassicTask(repoRoot, mainWorktree, repo string, wt worktree.Worktree) (string, error) {
if task, ok := worktree.TaskFromPath(repo, wt.Path); ok && task != "" {
return task, nil
}

branch := strings.TrimSpace(strings.TrimPrefix(wt.Branch, "refs/heads/"))
if branch == "" {
return "", nil
}

if strings.TrimSpace(mainWorktree) == "" {
mainWorktree = repoRoot
}
mainWorktreeAbs, err := worktree.NormalizePath(repoRoot, mainWorktree)
if err != nil {
return "", err
}
wtAbs, err := worktree.NormalizePath(repoRoot, wt.Path)
if err != nil {
return "", err
}
if wtAbs == mainWorktreeAbs {
return "", nil
}

return worktree.SlugifyTask(branch), nil
}

func normalizeTaskQuery(raw string) (string, error) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
Expand Down
95 changes: 95 additions & 0 deletions cli/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"strings"
"testing"

"github.com/pi2pie/git-worktree-tasks/internal/worktree"
)

type fakeRunner struct {
Expand Down Expand Up @@ -126,6 +128,99 @@ func TestFallbackPathForBranch(t *testing.T) {
}
}

func TestDeriveClassicTask(t *testing.T) {
defaultRepoRoot := "/tmp/repo"
defaultMainWorktree := "/tmp/repo"
repo := "repo"
tests := []struct {
name string
repoRoot string
mainWorktree string
wt worktree.Worktree
want string
}{
{
name: "path naming convention wins",
wt: worktree.Worktree{
Path: "/tmp/repo_task-from-path",
Branch: "refs/heads/other-branch",
},
want: "task-from-path",
},
{
name: "fallback to branch task for custom path",
wt: worktree.Worktree{
Path: "/tmp/repo/.claude/worktrees/new-task",
Branch: "refs/heads/new-task",
},
want: "new-task",
},
{
name: "fallback branch task is slugified",
wt: worktree.Worktree{
Path: "/tmp/repo/.claude/worktrees/release",
Branch: "refs/heads/release/1.0",
},
want: "release/1-0",
},
{
name: "main worktree path stays empty task",
wt: worktree.Worktree{
Path: "/tmp/repo",
Branch: "refs/heads/main",
},
want: "",
},
{
name: "main worktree stays empty when invoked from linked worktree",
repoRoot: "/tmp/repo/.claude/worktrees/new-task",
mainWorktree: "/tmp/repo",
wt: worktree.Worktree{
Path: "/tmp/repo",
Branch: "refs/heads/main",
},
want: "",
},
{
name: "linked worktree still infers task when invoked from linked worktree",
repoRoot: "/tmp/repo/.claude/worktrees/new-task",
mainWorktree: "/tmp/repo",
wt: worktree.Worktree{
Path: "/tmp/repo/.claude/worktrees/new-task",
Branch: "refs/heads/new-task",
},
want: "new-task",
},
{
name: "detached stays empty task",
wt: worktree.Worktree{
Path: "/tmp/repo/.claude/worktrees/detached",
},
want: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repoRoot := tt.repoRoot
if repoRoot == "" {
repoRoot = defaultRepoRoot
}
mainWorktree := tt.mainWorktree
if mainWorktree == "" {
mainWorktree = defaultMainWorktree
}
got, err := deriveClassicTask(repoRoot, mainWorktree, repo, tt.wt)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Fatalf("deriveClassicTask() = %q, want %q", got, tt.want)
}
})
}
}

func TestFormatGitCommand(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading