diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 0000000..681311e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 0349124..9dbd8a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,3 +119,14 @@ Test patterns: ## Releases Releases are automated via GitHub Actions. When PRs are merged to main, a release is created automatically. Do not create releases manually. + +## Decision Log + +At the start of a session, read `decision-log.md` in the repo root for past architectural and design decisions. + +When making an architectural or design decision, add an entry to `decision-log.md` with: + +- **Date** (YYYY-MM-DD) +- **Decision** — what was decided +- **Context** — why it came up, what the problem was +- **Resolution** — what was done and how diff --git a/cmd/status.go b/cmd/status.go index 3b7098d..3a51c92 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -315,6 +315,23 @@ func detectSyncIssues(gitClient git.GitClient, stackBranches []stack.StackBranch continue } + // Check if parent PR is merged (or parent branch merged via git history) + parentPR := prCache[branch.Parent] + if parentPR != nil && parentPR.State == "MERGED" { + if verbose { + fmt.Printf(" ✗ Parent PR #%d has been merged\n", parentPR.Number) + } + issues = append(issues, fmt.Sprintf(" - Branch '%s': parent '%s' has been merged — run '%s' to reparent", ui.Branch(branch.Name), ui.Branch(branch.Parent), ui.Command("stack sync"))) + } else if parentPR == nil && branch.Parent != baseBranch { + // No PR found for parent - check if parent was merged via git history + if merged, err := gitClient.IsAncestor(branch.Parent, "origin/"+baseBranch); err == nil && merged { + if verbose { + fmt.Printf(" ✗ Parent %s appears merged into %s via git history\n", branch.Parent, baseBranch) + } + issues = append(issues, fmt.Sprintf(" - Branch '%s': parent '%s' has been merged — run '%s' to reparent", ui.Branch(branch.Name), ui.Branch(branch.Parent), ui.Command("stack sync"))) + } + } + // Check if PR base matches the configured parent (if PR exists) if pr, exists := prCache[branch.Name]; exists { if verbose { diff --git a/cmd/status_test.go b/cmd/status_test.go index 21a1620..bd59dec 100644 --- a/cmd/status_test.go +++ b/cmd/status_test.go @@ -148,6 +148,38 @@ func TestDetectSyncIssues(t *testing.T) { }, expectedIssues: 1, }, + { + name: "parent PR is merged", + stackBranches: []stack.StackBranch{ + {Name: "feature-b", Parent: "feature-a"}, + }, + prCache: map[string]*github.PRInfo{ + "feature-a": {Number: 10, State: "MERGED", Base: "main"}, + "feature-b": {Number: 11, State: "OPEN", Base: "feature-a"}, + }, + setupMocks: func(mockGit *testutil.MockGitClient) { + // PR base still matches parent, but parent is merged — should report merged parent issue + mockGit.On("IsCommitsBehind", "feature-b", "feature-a").Return(false, nil) + mockGit.On("RemoteBranchExists", "feature-b").Return(false) + }, + expectedIssues: 1, // merged parent + }, + { + name: "parent branch merged via git history (no PR for parent)", + stackBranches: []stack.StackBranch{ + {Name: "feature-b", Parent: "feature-a"}, + }, + prCache: map[string]*github.PRInfo{ + "feature-b": {Number: 11, State: "OPEN", Base: "feature-a"}, + }, + setupMocks: func(mockGit *testutil.MockGitClient) { + // Parent has no PR, but is ancestor of origin/main + mockGit.On("IsAncestor", "feature-a", "origin/main").Return(true, nil) + mockGit.On("IsCommitsBehind", "feature-b", "feature-a").Return(false, nil) + mockGit.On("RemoteBranchExists", "feature-b").Return(false) + }, + expectedIssues: 1, // merged parent via git history + }, } for _, tt := range tests { diff --git a/decision-log.md b/decision-log.md new file mode 100644 index 0000000..202fe38 --- /dev/null +++ b/decision-log.md @@ -0,0 +1,11 @@ +# Decision Log + +Architectural and design decisions for Stackinator. + +## 2026-03-06 — Add merged-parent detection to `stack status` + +**Decision**: Add merged-parent detection to `stack status`. + +**Context**: `stack status` reported "perfectly synced" even when a parent branch's PR had been merged. `stack sync` already had this detection logic (checking PR state + git ancestor fallback) but `status` did not, so users had no warning until they ran sync. + +**Resolution**: Mirror `sync.go`'s merged-parent detection (PR state check + git ancestor fallback) in `status.go:detectSyncIssues()`. This lets `status` report when a parent branch has been merged and the child needs re-parenting.