diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 401e599..b2287a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ # Contributing -Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/dc7d340e0028db5df232d75b4c69ba866aa133f9/labs/12-product-engineering-loop). +Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/c288814c89139fa36e7b914ce14d9917f12d32d0/labs/12-product-engineering-loop). The Boatstack repository receives product/runtime changes through a generated pull request. Review the PR's `UPSTREAM.json`, tests, adapter diff, and context-size change; do not hand-edit generated output on `main`. `.github/workflows` is the exception: it is Boatstack's executable control plane, excluded from scheduled projection and changed only through a separate manually reviewed Boatstack PR. diff --git a/UPSTREAM.json b/UPSTREAM.json index 26b432c..2993200 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -12,11 +12,12 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "e73050efeefb479d4e2694977de316dd0d6068bcf71ab61e69aaa1514c376cb3", + "CONTRIBUTING.md": "47f98fbd40e12646c9314b53ce5a1b297cfd154bae919b5751bf93db28557bd0", "README.md": "125b47671a68556df382f19756fb61fa18925606cbbaf54d6bc9df8872b36870", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", "assets/boatstack-portability.svg": "66dfdfa85db857b3bd18b32047a6975f1fbbfc4dc091158e8277193f9969a346", + "boatstack/BUG-worktree-delivery-state.md": "02469cf51c3849dad5743783e248e5c04583e4240507fbef0e3f890cd6a95724", "boatstack/SKILL.md": "7c7b3568d836cb176c92762a8315fa834cd61a531a629c97a5cd98d6f7689be0", "boatstack/agents/gemini.yaml": "cbf43b387399e456fa6178f86d83e6e35567e6142ff800f8de6ffca306fa963e", "boatstack/agents/openai.yaml": "68a30a60859556c5a26e16d184594ca243a6043d99c8cf7d66b5dd6d50a93cd1", @@ -58,8 +59,8 @@ "boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c", "boatstack/migrate.go": "eaf589e2b266238068e42c6d78e01dc040266d28e342cb24f09e33e8541749b3", "boatstack/migrate_test.go": "9f4bda2fb158c5e54bcc0242dace1da3c1965f9846a213c573956a35b7d1724e", - "boatstack/next.go": "86ec196ac966c13b92b6302ded846d99a4e0e37d9d584bd80490e4e1047249e5", - "boatstack/next_test.go": "24d8c86da4b320af085117117b8032fa59d0f78896393cf1c8b0c2194a430090", + "boatstack/next.go": "ce2660e1d7649e0356ee4708aff8ce9ee5961165cf3026df4b8974528c703f71", + "boatstack/next_test.go": "d442d22023831ba39fcfbbf73f1a2da4170a83fc2aab5ce1b44f10cf9d88e173", "boatstack/operation.go": "62f97bf2091f33eb2ca91915bf08bee73d53387611b673e849355bfd516ca467", "boatstack/operation_test.go": "2d624eaba342b2c81b45cdf50918a65a9c002b5376a02041b24180658ee6a25a", "boatstack/plan.go": "6854d744a14e60eb285216ddc90c41085d6dde08ca5cae497e6863a703123b2f", @@ -107,10 +108,10 @@ "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", "docs/configuration.md": "4d8f207b415a5a1e3b9b1698ee7bb1221aa0e5496a061bb8054294df2f347ad1", - "docs/evidence-engineered-coding.md": "1104a171aa4dcfcf881bc59adcc0018a90fa94868f08f5a596129aa4f4b8e344", + "docs/evidence-engineered-coding.md": "295e908297c7a1c63d9e4784e7f5a5299ab6f515f9e6364f825b2a2b8fa3688d", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", "docs/getting-started.md": "f314270c5ed1a55bbef5f3ddbcb5596693dbee9374e5f0d3df8838cefbd68052", - "docs/public-claims.json": "baa8dbebf79506bc4071bbdeb122148055a634085b56eea138886cba66c38cae", + "docs/public-claims.json": "dff1654cb0901c026770f0358918b29e11a5836a6fbc8668f7449b10ee208e53", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -124,7 +125,7 @@ "labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d", "labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71", "labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39", - "labs/diagram-json/plan.lock.json": "f7d44717599db90f46da9556ca4d7195fa43c65db28fa532c9e7f5b6ba738589", + "labs/diagram-json/plan.lock.json": "100ee5d8381c04b3f11b4ffe3462a214883b85ddb1df73beb88a61251952cd41", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -186,12 +187,13 @@ "release-notes/2026-07-23-canonical-update-ownership.md": "7f34f890b252493797519389b23f1b56ec7ec16db7547aac8ea196f75f3b8c2c", "release-notes/2026-07-23-explicit-source-plan.md": "ec1f97434f9263f6db4bc3b83ae83213053cd2bc6b7465e4fb2d67cbea5cf731", "release-notes/2026-07-23-ignore-ambiguous-deliveries.md": "9b1b9fd48db340b91fcced1c282297de0ecad8744723f2b1fdd94033927a88c4", - "release-notes/2026-07-23-recoverable-repository-sync.md": "3afc4f6220ae76df3bd6dcd15fc180135274c808729e9c512a2c53462ec690c2" + "release-notes/2026-07-23-recoverable-repository-sync.md": "3afc4f6220ae76df3bd6dcd15fc180135274c808729e9c512a2c53462ec690c2", + "release-notes/2026-07-23-shipped-feature-candidate-resolution.md": "bd8ee8e7f3f216b356b121a83ef10cb0c8131a90b9ab803edebf23b882d9cf89" }, "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "dc7d340e0028db5df232d75b4c69ba866aa133f9", + "commit": "c288814c89139fa36e7b914ce14d9917f12d32d0", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/BUG-worktree-delivery-state.md b/boatstack/BUG-worktree-delivery-state.md new file mode 100644 index 0000000..9dd6842 --- /dev/null +++ b/boatstack/BUG-worktree-delivery-state.md @@ -0,0 +1,136 @@ +# Bug: shipped features re-register as ambiguous plan candidates from worktrees + +**Component:** `boatstack-helper` (labs/12-product-engineering-loop/product-engineering-loop) +**Observed in:** v0.7.45 (commit 91e33a95) +**Severity:** blocks `next-status` (BLOCKED / AMBIGUOUS) for any repo that has shipped >1 feature and uses worktree delivery mode. +**Goal of fix:** stop miscounting shipped features as unshipped, WITHOUT deleting the committed +`plan.md` / `plan.lock.json` / `pr.md` context. (A prune workaround was applied in taxweave PR #304 — +this fix should let that be reverted.) + +## Symptom + +From a fresh build worktree, `next-status --repo . --json` returns: + +``` +BLOCKED / AMBIGUOUS "More than one saved feature plan is available" +``` + +listing every historically shipped feature as a candidate — in taxweave, 22 shipped features + the +1 genuinely-active feature = 23 candidates. + +## Root cause + +Two facts combine: + +### 1. Delivery state is resolved against the worktree-local git dir + +`deliveryStateDirectory` (delivery.go ~L250) uses `git rev-parse --git-dir`, which inside a linked +worktree returns the **worktree-local** dir, not the shared repo dir: + +``` +--git-dir → /.git/worktrees/ (per-worktree, ephemeral) +--git-common-dir → /.git (shared, durable) +``` + +So delivery state is written to and read from: + +``` +/.git/worktrees//boatstack/deliveries//state.json +``` + +Both `saveDeliveryState` and `deliveryStatePath` go through `deliveryStateDirectory`, so **write and +read both target the ephemeral per-worktree location.** + +### 2. Shipping a feature deletes the worktree that holds its state.json + +`workspace.cleanup_after = merge` removes the feature's worktree on ship. That deletes +`/.git/worktrees//…/state.json` along with it — i.e. **shipping a feature destroys its +own "I am shipped" record.** The committed artifacts (`plan.md`, `plan.lock.json`, `pr.md`) live in +the repo tree and survive forever. + +### 3. The candidate check reads "planned + no state.json" as "unshipped" + +`featurePlanCandidates` (next.go ~L42): + +```go +if fileExists(/plan.md) && !fileExists(deliveryStatePath(repo, name)) { + features = append(features, name) // counted as an open candidate +} +``` + +Every shipped feature now matches (`plan.md` present, `state.json` gone), so from any new worktree +they all re-register as open candidates → ambiguity. `ignored_deliveries` (PR #301) filters the +*active-delivery* ambiguity path, NOT this feature-plan-candidate path. + +## Evidence + +- `git -C rev-parse --git-dir` → `/.git/worktrees/wt-firm-status` +- `git -C rev-parse --git-common-dir` → `/.git` +- The 22 blocked dirs each have `plan.lock.json`; 15 also have `pr.md`. The 1 legitimately-open + feature (`firm-status-finance-aesthetic`) has neither. +- Precedent: `hooks.go` (L120, L175) already uses `--git-common-dir` for exactly this "shared, + survives worktrees" reason. Delivery state is the outlier still on `--git-dir`. + +## Proposed fix — Part B only (Part A REJECTED, see below) + +### DECISION (2026-07-23): do Part B alone. Do NOT do Part A. + +An earlier draft of this spec proposed Part A (move delivery state to `--git-common-dir`) **and** +Part B. Investigation in the source rejected Part A: + +- `ActiveManagedDeliveries` (delivery.go:854) feeds `ClassifyCommand` push-denial + (safety.go:222/269/543), `next`, `run`, and `pr`. +- Per-worktree isolation of delivery state is **intentional**: + `TestManagedDeliveryStateDoesNotBlockUnrelatedWorktrees` (delivery_test.go:550) asserts an + unrelated worktree neither sees another feature's active delivery nor has its pushes denied. + Operations, by contrast, are deliberately shared/serialized (operation_test.go:267). +- Part A would make every active delivery visible in **all** worktrees — breaking that test and + changing cross-worktree push-denial semantics. + +Part A is also **unnecessary**: its only purpose was durability of the "shipped" record. Part B +derives shipped-ness from the **committed, durable** `plan.lock.json`/`pr.md` instead of the +ephemeral `state.json`, so the shipped record is durable by construction and the disappearance of a +shipped feature's `state.json` on cleanup becomes harmless. Do not move delivery state. + +### Part B — treat locked/shipped dirs as not-candidates + +Make `featurePlanCandidates` exclude any feature that is demonstrably past planning — +i.e. carries a `plan.lock.json` (locked/built) and/or `pr.md` (shipped): + +```go +if !fileExists(plan.md) { continue } +if fileExists(plan.lock.json) || fileExists(pr.md) { continue } // shipped/locked, not an open plan +if !fileExists(statePath) { features = append(features, name) } +``` + +This is consistent with the existing `orphanedFeatureArtifacts` helper (next.go), which already +distinguishes `pr.md` + `plan.lock.json` presence. It also lets taxweave **revert PR #304** and keep +all the rich shipped-feature context in-repo. + +`firm-status-finance-aesthetic` has no `plan.lock.json`/`pr.md`, so it correctly stays the single +open candidate. + +## Status (2026-07-23): FIXED via Part B + +`featurePlanCandidates` (next.go) now skips any feature dir carrying `plan.lock.json` or `pr.md` +before the `state.json` check, so locked/shipped features can no longer re-register as open plan +candidates. No change was made to `deliveryStateDirectory`, worktree isolation, or +`ActiveManagedDeliveries` / push-denial. Part A was **not** implemented (see rejection above). + +## Tests added (`next_test.go`) + +- `TestFeaturePlanCandidatesExcludesLockedAndShippedFeatures` — unit: a `plan.md`+`plan.lock.json` + dir and a `plan.md`+`plan.lock.json`+`pr.md` dir (both without `state.json`) are NOT returned; a + `plan.md`-only dir IS returned. +- `TestResolveNextIgnoresShippedFeatureCandidates` — reproduction: one open feature plus several + shipped dirs resolves to the single open candidate (`DRAFT_PLAN` / `plan-gate`), not `AMBIGUOUS`. +- `TestResolveNextIgnoresShippedFeaturesFromLinkedWorktree` — worktree conformance: same result from + a fresh linked build worktree (the exact reported symptom). +- `TestManagedDeliveryStateDoesNotBlockUnrelatedWorktrees` (delivery_test.go:550) — unchanged and + still green, confirming delivery-state locality was not touched. + +## Rollout + +1. Land Part B + tests, cut a new helper version. +2. In taxweave: bump the helper, revert PR #304 (restore the 22 shipped feature dirs). +3. Confirm `next-status` resolves `firm-status-finance-aesthetic` as the single candidate. diff --git a/boatstack/next.go b/boatstack/next.go index 0d26708..0d287ca 100644 --- a/boatstack/next.go +++ b/boatstack/next.go @@ -50,7 +50,19 @@ func featurePlanCandidates(repo string) ([]string, error) { } features := []string{} for _, entry := range entries { - if !entry.IsDir() || !featureSlugPattern.MatchString(entry.Name()) || !fileExists(filepath.Join(root, entry.Name(), "plan.md")) { + if !entry.IsDir() || !featureSlugPattern.MatchString(entry.Name()) { + continue + } + directory := filepath.Join(root, entry.Name()) + if !fileExists(filepath.Join(directory, "plan.md")) { + continue + } + // A feature that has been locked (built) or shipped is past planning and + // must never re-register as an open plan candidate, even when its + // ephemeral per-worktree delivery state.json was destroyed by worktree + // cleanup on ship. plan.lock.json / pr.md are the durable committed + // signals, mirroring orphanedFeatureArtifacts. + if fileExists(filepath.Join(directory, "plan.lock.json")) || fileExists(filepath.Join(directory, "pr.md")) { continue } statePath, stateErr := deliveryStatePath(repo, entry.Name()) diff --git a/boatstack/next_test.go b/boatstack/next_test.go index 877de49..f04c6fd 100644 --- a/boatstack/next_test.go +++ b/boatstack/next_test.go @@ -62,6 +62,26 @@ func writeSavedFeaturePlan(t *testing.T, repo, feature string) { } } +// writeShippedFeatureArtifacts models a feature that was built and shipped, then +// had its worktree (and the per-worktree delivery state.json) removed by cleanup: +// only the committed plan.md, plan.lock.json, and pr.md survive. +func writeShippedFeatureArtifacts(t *testing.T, repo, feature string) { + t.Helper() + directory := filepath.Join(repo, ".product-loop", "features", feature) + if err := os.MkdirAll(directory, 0o755); err != nil { + t.Fatal(err) + } + for name, body := range map[string]string{ + "plan.md": "# Plan\n", + "plan.lock.json": "lock\n", + "pr.md": "# PR\n", + } { + if err := os.WriteFile(filepath.Join(directory, name), []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } +} + func TestResolveNextReportsNotStartedWhenNoFeatureExists(t *testing.T) { repo := nextTestRepo(t) status, err := ResolveNext(repo, "") @@ -154,6 +174,104 @@ func TestResolveNextBlocksHistoricalPlansWithoutSourceIntent(t *testing.T) { } } +// TestFeaturePlanCandidatesExcludesLockedAndShippedFeatures is the unit-level +// guard for the shipped-feature ambiguity bug: locked (plan.lock.json) and +// shipped (pr.md) feature dirs must never re-register as open plan candidates +// even after their ephemeral per-worktree state.json was destroyed on cleanup. +func TestFeaturePlanCandidatesExcludesLockedAndShippedFeatures(t *testing.T) { + repo := nextTestRepo(t) + writeSavedFeaturePlan(t, repo, "open-feature") + + locked := filepath.Join(repo, ".product-loop", "features", "locked-feature") + if err := os.MkdirAll(locked, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(locked, "plan.md"), []byte("# Plan\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(locked, "plan.lock.json"), []byte("lock\n"), 0o644); err != nil { + t.Fatal(err) + } + + writeShippedFeatureArtifacts(t, repo, "shipped-feature") + + candidates, err := featurePlanCandidates(repo) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(candidates, []string{"open-feature"}) { + t.Fatalf("locked/shipped features leaked into plan candidates: %v", candidates) + } +} + +// TestResolveNextIgnoresShippedFeatureCandidates reproduces the taxweave scenario +// through ResolveNext: one genuinely open feature plus several shipped dirs whose +// state.json was destroyed by worktree cleanup must resolve to the single open +// candidate, not AMBIGUOUS. +func TestResolveNextIgnoresShippedFeatureCandidates(t *testing.T) { + repo := nextTestRepo(t) + writeSavedFeaturePlan(t, repo, "open-feature") + writeShippedFeatureArtifacts(t, repo, "shipped-one") + writeShippedFeatureArtifacts(t, repo, "shipped-two") + writeShippedFeatureArtifacts(t, repo, "shipped-three") + + status, err := ResolveNext(repo, "") + if err != nil { + t.Fatal(err) + } + if status.ObservedStage == "AMBIGUOUS" { + t.Fatalf("shipped features re-registered as ambiguous candidates: %+v", status) + } + if status.VerificationStatus != "VERIFIED" || status.Feature != "open-feature" || + status.ObservedStage != "DRAFT_PLAN" || status.NextOperation != "plan-gate" { + t.Fatalf("single open feature did not resolve cleanly: %+v", status) + } +} + +// TestResolveNextIgnoresShippedFeaturesFromLinkedWorktree reproduces the exact +// reported symptom: from a fresh linked build worktree, shipped feature dirs +// (committed plan.md/plan.lock.json/pr.md, no worktree-local state.json) must not +// re-register as open candidates. Guards the durable-committed-artifact contract +// under real worktree conditions. +func TestResolveNextIgnoresShippedFeaturesFromLinkedWorktree(t *testing.T) { + repo := t.TempDir() + runGit(t, repo, "init", "-b", "main") + runGit(t, repo, "config", "user.name", "Boatstack Test") + runGit(t, repo, "config", "user.email", "boatstack@example.invalid") + + if err := os.MkdirAll(filepath.Join(repo, ".product-loop", "features"), 0o755); err != nil { + t.Fatal(err) + } + value, err := MarshalJSON(testConfig()) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(repo, ".product-loop", "project.json"), value, 0o644); err != nil { + t.Fatal(err) + } + writeSavedFeaturePlan(t, repo, "open-feature") + writeShippedFeatureArtifacts(t, repo, "shipped-one") + writeShippedFeatureArtifacts(t, repo, "shipped-two") + + runGit(t, repo, "add", "-A") + runGit(t, repo, "commit", "-m", "seed shipped and open features") + + linked := filepath.Join(t.TempDir(), "linked") + runGit(t, repo, "worktree", "add", "-b", "build-work", linked) + + status, err := ResolveNext(linked, "") + if err != nil { + t.Fatal(err) + } + if status.ObservedStage == "AMBIGUOUS" { + t.Fatalf("shipped features re-registered as ambiguous from linked worktree: %+v", status) + } + if status.VerificationStatus != "VERIFIED" || status.Feature != "open-feature" || + status.ObservedStage != "DRAFT_PLAN" || status.NextOperation != "plan-gate" { + t.Fatalf("linked worktree did not resolve to the single open feature: %+v", status) + } +} + func TestResolveNextPlanningStates(t *testing.T) { for _, test := range []struct { name, approval, stage, next string diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 7cf899d..ed3a630 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -146,6 +146,6 @@ Delivery and system improvement also remain separate. A failed task may suggest ## What is evidence-backed -The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`dc7d340e0028db5df232d75b4c69ba866aa133f9`](https://github.com/operatorstack/intelligence-flow/tree/dc7d340e0028db5df232d75b4c69ba866aa133f9/labs/12-product-engineering-loop). +The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`c288814c89139fa36e7b914ce14d9917f12d32d0`](https://github.com/operatorstack/intelligence-flow/tree/c288814c89139fa36e7b914ce14d9917f12d32d0/labs/12-product-engineering-loop). The evidence supports specific failure mechanisms and guardrails. It does not establish that Boatstack is optimal, that control-theory notation proves software quality, or that one workflow dominates every team. Those are evaluation questions, so the distribution preserves measurements, provenance, gaps, and negative results. diff --git a/docs/public-claims.json b/docs/public-claims.json index 09b6101..f1a3538 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "dc7d340e0028db5df232d75b4c69ba866aa133f9", + "source_commit": "c288814c89139fa36e7b914ce14d9917f12d32d0", "statuses": ["verified", "observed", "still_being_evaluated"], "claims": [ { @@ -12,7 +12,7 @@ "readable_evidence": "why-these-steps.md#portable-workflow-and-state", "implementation": ["../boatstack/export.go", "../boatstack/references/artifacts.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go"], - "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" + "last_verified_version": "source:c288814c89139fa36e7b914ce14d9917f12d32d0" }, { "id": "human-decisions", @@ -23,7 +23,7 @@ "readable_evidence": "why-these-steps.md#human-decisions", "implementation": ["../boatstack/references/workflow.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" + "last_verified_version": "source:c288814c89139fa36e7b914ce14d9917f12d32d0" }, { "id": "validation-provenance", @@ -34,7 +34,7 @@ "readable_evidence": "why-these-steps.md#validation-provenance", "implementation": ["validation-and-evidence.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go"], - "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" + "last_verified_version": "source:c288814c89139fa36e7b914ce14d9917f12d32d0" }, { "id": "irreversible-operations", @@ -46,7 +46,7 @@ "readable_evidence": "why-these-steps.md#irreversible-operations", "implementation": ["safety.md", "../boatstack/safety.go", "../boatstack/hooks.go"], "verification": ["../boatstack/safety_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" + "last_verified_version": "source:c288814c89139fa36e7b914ce14d9917f12d32d0" }, { "id": "reviewer-ready-pr", @@ -57,7 +57,7 @@ "readable_evidence": "why-these-steps.md#reviewer-ready-pr", "implementation": ["../boatstack/pr.go", "getting-started.md"], "verification": ["../boatstack/pr_test.go"], - "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" + "last_verified_version": "source:c288814c89139fa36e7b914ce14d9917f12d32d0" }, { "id": "phase-scoped-delivery", @@ -68,7 +68,7 @@ "readable_evidence": "why-these-steps.md#phase-scoped-delivery", "implementation": ["../boatstack/delivery.go", "../boatstack/safety.go", "../boatstack/hooks.go", "../boatstack/references/workflow.md"], "verification": ["../boatstack/delivery_test.go", "../boatstack/pr_test.go"], - "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" + "last_verified_version": "source:c288814c89139fa36e7b914ce14d9917f12d32d0" }, { "id": "model-neutral-contract", @@ -79,7 +79,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" + "last_verified_version": "source:c288814c89139fa36e7b914ce14d9917f12d32d0" }, { "id": "cross-model-failures", @@ -90,7 +90,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" + "last_verified_version": "source:c288814c89139fa36e7b914ce14d9917f12d32d0" }, { "id": "lower-cost-outcomes", @@ -101,7 +101,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" + "last_verified_version": "source:c288814c89139fa36e7b914ce14d9917f12d32d0" }, { "id": "git-worktree-activation", @@ -112,7 +112,7 @@ "readable_evidence": "why-these-steps.md#git-worktree-activation", "implementation": ["../boatstack/runtime_cache.go", "../boatstack/hooks.go"], "verification": ["../boatstack/runtime_cache_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" + "last_verified_version": "source:c288814c89139fa36e7b914ce14d9917f12d32d0" }, { "id": "visible-updates", @@ -123,7 +123,7 @@ "readable_evidence": "why-these-steps.md#visible-updates", "implementation": ["../boatstack/update.go", "../boatstack/init.go"], "verification": ["../boatstack/update_test.go", "../boatstack/init_test.go", "../boatstack/export_test.go"], - "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" + "last_verified_version": "source:c288814c89139fa36e7b914ce14d9917f12d32d0" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index a1700db..568866e 100644 --- a/labs/diagram-json/plan.lock.json +++ b/labs/diagram-json/plan.lock.json @@ -6,7 +6,7 @@ "plan_path": "labs/diagram-json/plan.md", "plan_sha256": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "schema_version": 1, - "source_commit": "dc7d340e0028db5df232d75b4c69ba866aa133f9", + "source_commit": "c288814c89139fa36e7b914ce14d9917f12d32d0", "source_plan_path": "labs/diagram-json/source-plan.md", "source_plan_sha256": "e10593ddaa7522ab80cc991d0a09399257139799e37f737794cd49d68a39985b", "spec_path": "labs/diagram-json/spec.md", diff --git a/release-notes/2026-07-23-shipped-feature-candidate-resolution.md b/release-notes/2026-07-23-shipped-feature-candidate-resolution.md new file mode 100644 index 0000000..1af98cb --- /dev/null +++ b/release-notes/2026-07-23-shipped-feature-candidate-resolution.md @@ -0,0 +1,5 @@ +### Stop miscounting shipped features as open plans + +Boatstack no longer treats already-shipped or locked features as unresolved plan candidates. Previously, in repositories that use worktree delivery mode with `workspace.cleanup_after = merge`, shipping a feature removed the worktree that held its per-worktree delivery `state.json`, while the committed `plan.md` / `plan.lock.json` / `pr.md` survived. From a fresh build worktree, `next-status` then re-registered every historically shipped feature as an open plan and blocked with `AMBIGUOUS`. + +The plan-candidate check now relies on the durable committed artifacts: any feature carrying a `plan.lock.json` (locked/built) or `pr.md` (shipped) is past planning and is excluded from open-plan candidates, regardless of whether its ephemeral `state.json` still exists. A feature with only `plan.md` correctly remains the single open candidate. Delivery-state locality and cross-worktree push-denial are unchanged.