diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24c40dd..cffbf18 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/b0e65b129cc80a5cb43543ac4b7f89563b64b45e/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/7214b6013406e37567c5ebd6c46a23c78f2446a0/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 901b414..019af31 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -12,12 +12,12 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "9cf8d9e3f15f2247c5967a189d649392d3edc18d92d2964a30d5069a489ebb52", + "CONTRIBUTING.md": "cb692f2ba4ccccb546100be7734f3c94602b8c29c8b6f63b8b058dcdc80ef4ec", "README.md": "8d481f8e395346400726d02f760f831a8b11062de18b7a76fe4cf00e5e12ca08", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", "assets/boatstack-portability.svg": "66dfdfa85db857b3bd18b32047a6975f1fbbfc4dc091158e8277193f9969a346", - "boatstack/SKILL.md": "b8a3bf396c29e5e728d50837c4546bd500a40cc24009217c2f7b80ed267d6bf1", + "boatstack/SKILL.md": "ab0182a73bf42cf01ef0bb99486dd48d83673a60377bae2b5ecdf27022fb5395", "boatstack/agents/gemini.yaml": "cbf43b387399e456fa6178f86d83e6e35567e6142ff800f8de6ffca306fa963e", "boatstack/agents/openai.yaml": "68a30a60859556c5a26e16d184594ca243a6043d99c8cf7d66b5dd6d50a93cd1", "boatstack/assets/templates/adr.md": "c577a3c1c1319061f61deb053597e6e853657022185fe28b8f733327e2a78565", @@ -34,7 +34,7 @@ "boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f", "boatstack/changelog.go": "c5e1f31440b44d61e6037ad27af0333540af3545d655e35819a0241cbbebd8ec", "boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7", - "boatstack/cmd/boatstack-helper/main.go": "3504ea23b4792ac1c3cc1aacbdc245be61217378add98ee52e3dbb342c0240b1", + "boatstack/cmd/boatstack-helper/main.go": "91f869e9dd8b9b19a07c161e170f720ef4d51bc7fba67606d66cf973fcf0422f", "boatstack/cmd/boatstack-helper/main_test.go": "ff73003b6a5157202fa09ddf1129fb13c3d79702b2e05a8721ce5a11bf5ab779", "boatstack/decision.go": "257ca328da6ae19ab252f10ee5d06bd7daf49dd8141d083ab1b32f106ea7a94c", "boatstack/decision_test.go": "1a92ff832610f9559bd47ccac7fc1755a8b4f8261c35bc72a092830dff05f7c0", @@ -53,8 +53,8 @@ "boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c", "boatstack/migrate.go": "eaf589e2b266238068e42c6d78e01dc040266d28e342cb24f09e33e8541749b3", "boatstack/migrate_test.go": "b5c2dad2ab762aa202c5c99a8445774bedd982d280ebbb8dd713e7e9f43299d2", - "boatstack/next.go": "d8bb695964ccefc0fc5682230d20b4ab3e7cbb9041ae828fc5103ccd8d83f9d1", - "boatstack/next_test.go": "57828f76257ad7344c383084a97399f6523cbbced28ac2bbe4c14b9da84ccf32", + "boatstack/next.go": "a4262b2dd310db4e92903ea09cd5f7bd9236cc725341081f6a1a236a46bcb06e", + "boatstack/next_test.go": "d158288d66628ab868061f85503663974c0b3cfe3fce0828d8ec5c85ed3ed81c", "boatstack/plan.go": "4ff1b6bf8b187aa26e4851d30c32496a8e979814a5ce2b4b2a1f1d656f7735d0", "boatstack/plan_test.go": "878dd9086bb583328a7eba1cf45318baeef3d55693be745d5520f07c6ace5e3e", "boatstack/plan_validation.go": "848f895e323ae8a428e57f57233e068a8d71c2a7c8716d2ff60e60d23188fd1b", @@ -72,8 +72,8 @@ "boatstack/references/workflow.md": "410f31698a25fcbf969dd00a28dd48f2f59e1fd3de89ac1cff2e8d513913c6f4", "boatstack/release.go": "fa2ac926df89c90c5844e938a2e02d4b8dbbaefbf85bb7a1a89fc51690bea520", "boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690", - "boatstack/run.go": "a9afb239c5e2cb80f96dab594f0dc94852f2f16625b1de05c5110ee85f61cfaa", - "boatstack/run_test.go": "fdc416f15e787b5c8401fc0f0e3aeb58b4891c828a8a869c8dce4e8f1541d809", + "boatstack/run.go": "fbdbf583c862c41f23d1a200f53d042842db72f19c29fe94e4288a69b0ac4a6b", + "boatstack/run_test.go": "34484285fd2457b84faf6d5353117b74af73d4601cd60a96e270b7fd0a7a6a8e", "boatstack/runtime.go": "60fa6e78d1a7d99a591a9dea3537f3d3a0192ab7b85a798573d5afdc0922f7b4", "boatstack/runtime_cache.go": "60c4eb0c7dde91d40d6ef3f05adc1a1282d17ff1ca12470d0a008454f7ca7489", "boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85", @@ -87,14 +87,14 @@ "boatstack/update.go": "792ba4481503590e1412c0d2ddbfdd912899218a8e3ebcf323e385698b498ab0", "boatstack/update_test.go": "b7d989c3c74a9e4d80ad14f9f74d94b617798dbcc36359327a4233524677db22", "boatstack/workspace.go": "91b343400b3506a6f516c28fabc3f1575f22024a5b19f934a020be660a20482e", - "boatstack/workspace_test.go": "d99ee417954f529a333b8330c32104d96b0368c1b07223a42cbf304d5363ad0d", + "boatstack/workspace_test.go": "bb9293d770c70b9a77b26277539623e6601001766d70487359980ce45f658154", "docs/account-recovery-walkthrough.md": "676034974594a7d1a559b24dbed31d7ccc429eb81404b203ca07bbdaa19ec3d3", "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", - "docs/evidence-engineered-coding.md": "4996b3e4639827757734a6fef96a2d6f29ea83d01426292254d1efd73861ab85", + "docs/evidence-engineered-coding.md": "699203766af7a93584caad922656422a90c1c309ea814da1c7126d4574468229", "docs/generated-files.md": "136422baf0c7fc2bd5100cfe0ebdb3d9d0705dfd7e7d54bf745dd1037e63492c", "docs/getting-started.md": "eacc814fdffdfa3c7d8052b7cd99a79c04da5c75d88d8b44f3fb68d9afec0316", - "docs/public-claims.json": "2f18d8de941840fd6a39661db60120ddfd9e3be391875570086050f94c3d4502", + "docs/public-claims.json": "8371581f75f7eff19840137322476e7a58b507093b8fda1c177abf30b85d2e69", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "d65c66e323037bda5d45aacef5d48afa6bf93da55901378891d235aca3a5684f", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -108,7 +108,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": "e5541208e20e872bc371459e349656bced442c4de4213f091f108260f961cf82", + "labs/diagram-json/plan.lock.json": "632437055949355b724b581da4171a928fe95aeec57f7eebc18f1f6e23bef49b", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -150,6 +150,7 @@ "release-notes/2026-07-20-speak-software-standards.md": "a8890ed7eb38868bdf3035572d71785ca89abd3ffc3a314a5dc5315150afe397", "release-notes/2026-07-21-blueprint-diagrams-value-first-readme.md": "11eb70cb814a99606b4f7761401d3670074fe6bd1af8557f95f573b04a3195af", "release-notes/2026-07-21-e2e-architecture-grounding.md": "7fa7e99fc6fd4e0688009bb736e6506830bf0216f3fa19757cbd9f5679c7ac3b", + "release-notes/2026-07-21-multi-feature-avoidance-dx.md": "a1196a3e1a19466c5c4c86782b3db1ca9a078b0146b9199466bff8a2c263ed0c", "release-notes/2026-07-21-plan-decision-operator.md": "c2a7416ef17a6042583dd60f1f4e847cb1e0f06b28dbde885479efd566bb5cae", "release-notes/2026-07-21-prevent-hallucinated-approver-names.md": "a5fd08bc3d8b983340a2ddd67b71c78b2b34a915fdfe6eb7cdffd0f1d52e3427", "release-notes/2026-07-21-prevent-worktree-dirty-state.md": "d3ebac81a14565461fd7f3c5bd520d881d70b584408ba35a1022cd917c7bf132", @@ -158,7 +159,7 @@ "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "b0e65b129cc80a5cb43543ac4b7f89563b64b45e", + "commit": "7214b6013406e37567c5ebd6c46a23c78f2446a0", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/SKILL.md b/boatstack/SKILL.md index 4acc81c..a1d56d4 100644 --- a/boatstack/SKILL.md +++ b/boatstack/SKILL.md @@ -79,6 +79,14 @@ Use the global, state-scoped reply shortcuts for finite input: `a` approves the Shortcuts do not bypass fingerprints, committed-diff checks, evidence, authentication, or manual commit/push prerequisites. Never interpret `r` as approval, publication, identity, secret input, permission escalation, policy bypass, destructive recovery authorization, or another safety exception. Free-text and operation-command prompts remain explicit. Use an explicit approval identity first; otherwise use the authenticated GitHub login when available. Ask once for a name or handle only when no trustworthy identity can be resolved. Never invent a placeholder name (e.g., Sam, Eve). Never infer the approver from the filesystem username, commit history, or agent identity. If identity is missing after approval, preserve the current approval intent and ask only for identity; do not make the human approve the unchanged plan again. +## Handle new intent during active deliveries + +Before starting `/auto-plan` for a new feature, check `next-status --repo . --json`. If there is already an active managed delivery on the current branch (e.g., Status is `BUILD`): +1. Stop and clarify the developer's intent. Ask: *"You have an active delivery (``). Are these new ideas amendments to this feature, or a completely separate feature?"* +2. If the developer confirms it is an **amendment**, do not start a new feature. Route to the `repair` operation, classify it as a `requirement_amendment`, and update the existing plan. +3. If the developer confirms it is a **completely separate feature**, proactively suggest worktree isolation to avoid branch entanglement. Ask: *"Since `` is still active here, do you want to cut a new worktree (`feat/`) to keep this work isolated? (Recommended)"* +4. If they accept isolation, route to the `workspace-cut` operation. If they explicitly choose to stack both features on the same branch, only then proceed with `/auto-plan` for the new feature. + ## Run `auto-plan` 0. Require exactly one saved plan file created in the active host's Plan mode. First use the active plan path exposed in host/system conversation context, when available, and validate it with `.product-loop/bin/boatstack-helper check-source-plan --repo . --plan `. Otherwise run `check-source-plan --repo .` to search only `.product-loop/intake/` and bounded repo-local host plan directories. If the result is missing or ambiguous, return `BLOCKED`; never choose by recency alone. An explicit `/auto-plan ` is only the ambiguity fallback. Do not write the missing source plan inside `auto-plan`. diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go index 1bf2550..39e1cf5 100644 --- a/boatstack/cmd/boatstack-helper/main.go +++ b/boatstack/cmd/boatstack-helper/main.go @@ -334,11 +334,12 @@ func deliveryStatusCommand(arguments []string) int { func nextStatusCommand(arguments []string) int { flags := flag.NewFlagSet("next-status", flag.ContinueOnError) repo := flags.String("repo", ".", "repository whose Boatstack stage should be inspected") + feature := flags.String("feature", "", "optional specific managed feature to inspect") jsonOutput := flags.Bool("json", false, "print the versioned structured status") if err := flags.Parse(arguments); err != nil { return 2 } - status, err := boatstack.ResolveNext(*repo) + status, err := boatstack.ResolveNext(*repo, *feature) if err != nil { return fail(err) } @@ -357,11 +358,12 @@ func nextStatusCommand(arguments []string) int { func runPreflightCommand(arguments []string) int { flags := flag.NewFlagSet("run-preflight", flag.ContinueOnError) repo := flags.String("repo", ".", "repository whose Git state should be verified before boatstack run") + feature := flags.String("feature", "", "optional specific managed feature to verify") jsonOutput := flags.Bool("json", false, "print the versioned structured preflight") if err := flags.Parse(arguments); err != nil { return 2 } - status := boatstack.CheckRunPreflight(*repo) + status := boatstack.CheckRunPreflight(*repo, *feature) if *jsonOutput { value, err := boatstack.MarshalJSON(status) if err != nil { diff --git a/boatstack/next.go b/boatstack/next.go index 889576d..ca4645f 100644 --- a/boatstack/next.go +++ b/boatstack/next.go @@ -14,16 +14,17 @@ const nextStatusSchemaVersion = 1 // workflow position. Conversation and terminal context are deliberately absent: // adapters may present them as context, but they are not workflow evidence. type NextStatus struct { - SchemaVersion int `json:"schema_version"` - VerificationStatus string `json:"verification_status"` - Feature string `json:"feature,omitempty"` - ActiveSlice string `json:"active_slice,omitempty"` - SliceIndex int `json:"slice_index,omitempty"` - TotalSlices int `json:"total_slices,omitempty"` - ObservedStage string `json:"observed_stage"` - NextOperation string `json:"next_operation"` - Reason string `json:"reason"` - BlockingAmbiguity []string `json:"blocking_ambiguity,omitempty"` + SchemaVersion int `json:"schema_version"` + VerificationStatus string `json:"verification_status"` + Feature string `json:"feature,omitempty"` + ActiveSlice string `json:"active_slice,omitempty"` + SliceIndex int `json:"slice_index,omitempty"` + TotalSlices int `json:"total_slices,omitempty"` + ObservedStage string `json:"observed_stage"` + NextOperation string `json:"next_operation"` + Operator DecisionOperator `json:"operator,omitempty"` + Reason string `json:"reason"` + BlockingAmbiguity []string `json:"blocking_ambiguity,omitempty"` } func blockedNextStatus(stage, operation, reason string, ambiguity ...string) NextStatus { @@ -193,7 +194,7 @@ func completedManagedStates(repo string) ([]DeliveryState, error) { // ResolveNext performs bounded, local, read-only state inspection. It never // contacts GitHub and never treats process or conversation history as evidence. -func ResolveNext(repoPath string) (NextStatus, error) { +func ResolveNext(repoPath, explicitFeature string) (NextStatus, error) { repo, err := ResolveRepository(repoPath) if err != nil { return NextStatus{}, err @@ -211,10 +212,27 @@ func ResolveNext(repoPath string) (NextStatus, error) { if err != nil { return blockedNextStatus("INVALID_STATE", "repair-state", "Boatstack found invalid managed delivery state. Preserve the artifacts and restore the missing or stale evidence before continuing: "+err.Error()), nil } + + if explicitFeature != "" { + found := false + for _, f := range active { + if f == explicitFeature { + found = true + break + } + } + if found { + active = []string{explicitFeature} + } else { + return blockedNextStatus("INVALID_STATE", "repair-state", fmt.Sprintf("Feature %s is not currently an active managed delivery.", explicitFeature)), nil + } + } + if len(active) > 1 { base.VerificationStatus = "BLOCKED" base.ObservedStage = "AMBIGUOUS" base.NextOperation = "resolve-ambiguity" + base.Operator = OperatorQuery base.Reason = "More than one managed delivery is active; Boatstack will not choose by recency." base.BlockingAmbiguity = active return base, nil diff --git a/boatstack/next_test.go b/boatstack/next_test.go index c7cdacd..7880152 100644 --- a/boatstack/next_test.go +++ b/boatstack/next_test.go @@ -69,7 +69,7 @@ func writeIntakePlan(t *testing.T, repo, name string) { func TestResolveNextReportsNotStartedWhenNoFeatureExists(t *testing.T) { repo := nextTestRepo(t) - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -81,7 +81,7 @@ func TestResolveNextReportsNotStartedWhenNoFeatureExists(t *testing.T) { func TestResolveNextReportsSavedSourcePlan(t *testing.T) { repo := nextTestRepo(t) writeIntakePlan(t, repo, "feature.md") - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -96,7 +96,7 @@ func TestResolveNextPrefersUniqueSourcePlanOverHistoricalPlans(t *testing.T) { writeSavedFeaturePlan(t, repo, "historical-two") writeIntakePlan(t, repo, "current.md") - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -112,7 +112,7 @@ func TestResolveNextBlocksMultipleSourcePlansBeforeHistoricalPlans(t *testing.T) writeIntakePlan(t, repo, "first.md") writeIntakePlan(t, repo, "second.md") - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -127,7 +127,7 @@ func TestResolveNextActiveDeliveryOutranksSourcePlan(t *testing.T) { writeNextDelivery(t, repo, "active-feature", "BUILD", 0) writeIntakePlan(t, repo, "new-feature.md") - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -147,7 +147,7 @@ func TestResolveNextOrphanedEvidenceOutranksSourcePlan(t *testing.T) { } writeIntakePlan(t, repo, "new-feature.md") - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -161,7 +161,7 @@ func TestResolveNextBlocksHistoricalPlansWithoutSourceIntent(t *testing.T) { writeSavedFeaturePlan(t, repo, "historical-one") writeSavedFeaturePlan(t, repo, "historical-two") - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -192,7 +192,7 @@ func TestResolveNextPlanningStates(t *testing.T) { t.Fatal(err) } } - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -212,7 +212,7 @@ func TestResolveNextDeliveryTransitions(t *testing.T) { t.Run(test.state, func(t *testing.T) { repo := nextTestRepo(t) writeNextDelivery(t, repo, "recovery", test.state, 0) - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -237,7 +237,7 @@ func TestResolveNextReportsFeatureCompleteAfterPublication(t *testing.T) { plan["feature_id"] = "recovery" plan["source_plan_path"] = "../../intake/source-plan.md" writeMarkdownPlan(t, filepath.Join(repo, ".product-loop", "features", "recovery", "plan.md"), plan, true) - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -262,7 +262,7 @@ func TestResolveNextPrefersNewDraftOverCompletedHistory(t *testing.T) { if err := os.WriteFile(filepath.Join(directory, "plan.md"), []byte("# Plan\n"), 0o644); err != nil { t.Fatal(err) } - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -279,7 +279,7 @@ func TestResolveNextBlocksMultipleActiveFeaturesWithoutMutation(t *testing.T) { if err != nil { t.Fatal(err) } - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -302,7 +302,7 @@ func TestResolveNextBlocksStaleManagedState(t *testing.T) { if err := os.WriteFile(lockPath, []byte("changed\n"), 0o644); err != nil { t.Fatal(err) } - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -338,7 +338,7 @@ func TestResolveNextBlocksMissingLockAndOrphanPreview(t *testing.T) { if err != nil { t.Fatal(err) } - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } diff --git a/boatstack/run.go b/boatstack/run.go index c4fb385..190a06c 100644 --- a/boatstack/run.go +++ b/boatstack/run.go @@ -30,7 +30,7 @@ func blockedRunPreflight(base, head, upstream, relation, reason string) RunPrefl } } -func runBranches(repo string) (string, string, error) { +func runBranches(repo, explicitFeature string) (string, string, error) { base := defaultPRBase(repo) head, err := runGitCommand(repo, "branch", "--show-current") if err != nil || strings.TrimSpace(head) == "" { @@ -41,6 +41,22 @@ func runBranches(repo string) (string, string, error) { if err != nil { return "", "", err } + + if explicitFeature != "" { + found := false + for _, f := range active { + if f == explicitFeature { + found = true + break + } + } + if found { + active = []string{explicitFeature} + } else { + return base, head, fmt.Errorf("feature %s is not currently an active managed delivery", explicitFeature) + } + } + if len(active) > 1 { return base, head, fmt.Errorf("more than one managed delivery is active; Boatstack run will not choose by recency") } @@ -69,7 +85,7 @@ func runBranches(repo string) (string, string, error) { // CheckRunPreflight fetches origin and proves that the current branch contains // the fetched base and is not behind or diverged from its configured upstream. // It never merges, rebases, switches branches, discards changes, or pushes. -func CheckRunPreflight(repoPath string) RunPreflight { +func CheckRunPreflight(repoPath, explicitFeature string) RunPreflight { repo, err := ResolveRepository(repoPath) if err != nil { return blockedRunPreflight("", "", "", "INVALID_REPOSITORY", err.Error()) @@ -84,7 +100,7 @@ func CheckRunPreflight(repoPath string) RunPreflight { return blockedRunPreflight("", "", "", "FETCH_FAILED", "Boatstack could not fetch origin: "+err.Error()) } - base, head, err := runBranches(repo) + base, head, err := runBranches(repo, explicitFeature) if err != nil { return blockedRunPreflight(base, head, "", "BRANCH_MISMATCH", err.Error()) } diff --git a/boatstack/run_test.go b/boatstack/run_test.go index 9b56b15..1ed398f 100644 --- a/boatstack/run_test.go +++ b/boatstack/run_test.go @@ -63,7 +63,7 @@ func TestCheckRunPreflightRequiresOriginBeforeMutation(t *testing.T) { if err != nil { t.Fatal(err) } - status := CheckRunPreflight(repo) + status := CheckRunPreflight(repo, "") after, err := os.ReadFile(filepath.Join(repo, ".product-loop", "project.json")) if err != nil { t.Fatal(err) @@ -91,7 +91,7 @@ func TestCheckRunPreflightFetchesAndAcceptsFreshUnpublishedBranch(t *testing.T) t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, output) } } - status := CheckRunPreflight(repo) + status := CheckRunPreflight(repo, "") if status.VerificationStatus != "VERIFIED" || status.Relation != "UNPUBLISHED" || status.BaseBranch != "main" || status.HeadBranch != "feature" { t.Fatalf("unexpected preflight: %+v", status) } @@ -136,7 +136,7 @@ func TestCheckRunPreflightBlocksFetchAndFreshnessFailures(t *testing.T) { t.Run(test.name, func(t *testing.T) { repo := runTestRepo(t) withRunGit(t, test.responses) - status := CheckRunPreflight(repo) + status := CheckRunPreflight(repo, "") if status.VerificationStatus != "BLOCKED" || status.Relation != test.relation { t.Fatalf("unexpected preflight: %+v", status) } @@ -167,7 +167,7 @@ func TestCheckRunPreflightClassifiesUpstreamRelations(t *testing.T) { "rev-parse --abbrev-ref --symbolic-full-name @{upstream}": {value: "origin/feature"}, "rev-list --left-right --count HEAD...@{upstream}": {value: test.counts}, }) - status := CheckRunPreflight(repo) + status := CheckRunPreflight(repo, "") if status.VerificationStatus != test.verification || status.Relation != test.relation { t.Fatalf("unexpected preflight: %+v", status) } @@ -209,7 +209,7 @@ func TestCheckRunPreflightBlocksConstrainedDeliveryBranchMismatch(t *testing.T) "fetch origin": {}, "branch --show-current": {value: "wrong-feature"}, }) - status := CheckRunPreflight(repo) + status := CheckRunPreflight(repo, "") if status.VerificationStatus != "BLOCKED" || status.Relation != "BRANCH_MISMATCH" || !strings.Contains(status.Reason, "expected-feature") { t.Fatalf("unexpected preflight: %+v", status) } diff --git a/boatstack/workspace_test.go b/boatstack/workspace_test.go index cbddd55..9fb10c6 100644 --- a/boatstack/workspace_test.go +++ b/boatstack/workspace_test.go @@ -345,7 +345,7 @@ func writeApprovedFeature(t *testing.T, repo, feature string) { func TestResolveNextRoutesToWorkspaceCutWhenApprovedOnBase(t *testing.T) { repo := workspaceRepo(t, defaultWorkspace()) writeApprovedFeature(t, repo, "newthing") - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -360,7 +360,7 @@ func TestResolveNextApprovedBuildsWhenWorkspaceExists(t *testing.T) { if _, err := CutFeatureWorkspace(WorkspaceCutOptions{Repo: repo, Feature: "cutdone"}); err != nil { t.Fatal(err) } - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -372,7 +372,7 @@ func TestResolveNextApprovedBuildsWhenWorkspaceExists(t *testing.T) { func TestResolveNextApprovedBuildsWhenWorkspaceDisabled(t *testing.T) { repo := workspaceRepo(t, Workspace{Enabled: false}) writeApprovedFeature(t, repo, "plainfeat") - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -410,7 +410,7 @@ func TestResolveNextRoutesToWorkspaceCleanupAfterPublication(t *testing.T) { t.Fatal(err) } writeCompletedDelivery(t, repo, "shipped", "feat/shipped") - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -422,7 +422,7 @@ func TestResolveNextRoutesToWorkspaceCleanupAfterPublication(t *testing.T) { func TestResolveNextFeatureCompleteStaysNoneWithoutWorktree(t *testing.T) { repo := workspaceRepo(t, defaultWorkspace()) writeCompletedDelivery(t, repo, "shipped", "feat/no-worktree") - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } @@ -436,7 +436,7 @@ func TestResolveNextFeatureCompleteStaysNoneWhenWorkspaceDisabled(t *testing.T) // A worktree exists on disk, but management is off, so cleanup is not surfaced. workspaceGitDo(t, repo, "worktree", "add", "-b", "feat/manual", filepath.Join(repo, "wt-manual")) writeCompletedDelivery(t, repo, "shipped", "feat/manual") - status, err := ResolveNext(repo) + status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 2a633d9..dc619d3 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 [`b0e65b129cc80a5cb43543ac4b7f89563b64b45e`](https://github.com/operatorstack/intelligence-flow/tree/b0e65b129cc80a5cb43543ac4b7f89563b64b45e/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 [`7214b6013406e37567c5ebd6c46a23c78f2446a0`](https://github.com/operatorstack/intelligence-flow/tree/7214b6013406e37567c5ebd6c46a23c78f2446a0/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 45d497e..c8ab53f 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "b0e65b129cc80a5cb43543ac4b7f89563b64b45e", + "source_commit": "7214b6013406e37567c5ebd6c46a23c78f2446a0", "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:b0e65b129cc80a5cb43543ac4b7f89563b64b45e" + "last_verified_version": "source:7214b6013406e37567c5ebd6c46a23c78f2446a0" }, { "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:b0e65b129cc80a5cb43543ac4b7f89563b64b45e" + "last_verified_version": "source:7214b6013406e37567c5ebd6c46a23c78f2446a0" }, { "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:b0e65b129cc80a5cb43543ac4b7f89563b64b45e" + "last_verified_version": "source:7214b6013406e37567c5ebd6c46a23c78f2446a0" }, { "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:b0e65b129cc80a5cb43543ac4b7f89563b64b45e" + "last_verified_version": "source:7214b6013406e37567c5ebd6c46a23c78f2446a0" }, { "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:b0e65b129cc80a5cb43543ac4b7f89563b64b45e" + "last_verified_version": "source:7214b6013406e37567c5ebd6c46a23c78f2446a0" }, { "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:b0e65b129cc80a5cb43543ac4b7f89563b64b45e" + "last_verified_version": "source:7214b6013406e37567c5ebd6c46a23c78f2446a0" }, { "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:b0e65b129cc80a5cb43543ac4b7f89563b64b45e" + "last_verified_version": "source:7214b6013406e37567c5ebd6c46a23c78f2446a0" }, { "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:b0e65b129cc80a5cb43543ac4b7f89563b64b45e" + "last_verified_version": "source:7214b6013406e37567c5ebd6c46a23c78f2446a0" }, { "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:b0e65b129cc80a5cb43543ac4b7f89563b64b45e" + "last_verified_version": "source:7214b6013406e37567c5ebd6c46a23c78f2446a0" }, { "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:b0e65b129cc80a5cb43543ac4b7f89563b64b45e" + "last_verified_version": "source:7214b6013406e37567c5ebd6c46a23c78f2446a0" }, { "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:b0e65b129cc80a5cb43543ac4b7f89563b64b45e" + "last_verified_version": "source:7214b6013406e37567c5ebd6c46a23c78f2446a0" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index ce58ecd..68a6629 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": "b0e65b129cc80a5cb43543ac4b7f89563b64b45e", + "source_commit": "7214b6013406e37567c5ebd6c46a23c78f2446a0", "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-21-multi-feature-avoidance-dx.md b/release-notes/2026-07-21-multi-feature-avoidance-dx.md new file mode 100644 index 0000000..22d212b --- /dev/null +++ b/release-notes/2026-07-21-multi-feature-avoidance-dx.md @@ -0,0 +1,3 @@ +### Avoid multiple active deliveries via intent clarification + +Updated the developer experience to prevent blindly tangling the active branch with multiple concurrent features. When a user presents new ideas while a delivery is active, the agent now clarifies intent: if it's an amendment, it repairs the existing plan; if it's a new feature, it proactively suggests cutting a new worktree. For intentional same-branch stacking, `next-status` and `run-preflight` now accept a `--feature` flag, allowing the agent to resolve ambiguity via an `OperatorQuery` prompt and target a specific delivery without crashing.