diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 136e3bb..d37fdee 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/d589911ccb9138013c8693d85b96be0e7c903be3/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/91e33a95add752894fb532a67b980c109ecd28e8/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 bb7cad1..533f311 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -1,7 +1,7 @@ { "canonical_context": { - "characters": 60215, - "estimated_tokens": 15054, + "characters": 60587, + "estimated_tokens": 15147, "estimator": "ceil(total characters / 4); compactness signal, not provider billing", "files": [ "product-engineering-loop/references/workflow.md", @@ -12,12 +12,12 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "35fa5831dec2a7df663652bab27a2bf31b16f04f4ea9263cd780ce4bb9df8166", + "CONTRIBUTING.md": "f9ed388745c12187dcc3e39fc4821ec5094085182157005cae561df38a3ab2c8", "README.md": "125b47671a68556df382f19756fb61fa18925606cbbaf54d6bc9df8872b36870", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", "assets/boatstack-portability.svg": "66dfdfa85db857b3bd18b32047a6975f1fbbfc4dc091158e8277193f9969a346", - "boatstack/SKILL.md": "80c4599f2ffeb686d45fdfc1bb4eac4635bded5a5621fa71c41eed7160f4d125", + "boatstack/SKILL.md": "1fbdab536732aca6425d657acfe0eb2f37743cb542c07e92f8e06ddafdb910ff", "boatstack/agents/gemini.yaml": "cbf43b387399e456fa6178f86d83e6e35567e6142ff800f8de6ffca306fa963e", "boatstack/agents/openai.yaml": "68a30a60859556c5a26e16d184594ca243a6043d99c8cf7d66b5dd6d50a93cd1", "boatstack/assets/templates/adr.md": "c577a3c1c1319061f61deb053597e6e853657022185fe28b8f733327e2a78565", @@ -34,15 +34,15 @@ "boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f", "boatstack/changelog.go": "c5e1f31440b44d61e6037ad27af0333540af3545d655e35819a0241cbbebd8ec", "boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7", - "boatstack/cmd/boatstack-helper/main.go": "3b75e71f201825d5c9b83309ec551f79ca27e6de4a58175841e404e74c106931", + "boatstack/cmd/boatstack-helper/main.go": "718803eb4e0c1ca1bb0ca2923f64cf055d35dab6e3ad7f6a9f62b0cec724b610", "boatstack/cmd/boatstack-helper/main_test.go": "ff73003b6a5157202fa09ddf1129fb13c3d79702b2e05a8721ce5a11bf5ab779", "boatstack/command.go": "94d2117c6e390d5a644afc5cd90f7e712e3f8b1032f8c9e3b253cc134524c28a", "boatstack/command_test.go": "9f707abba3640add81c3e97ba7e72fedbf98f3394b1c060a9ca4b4a28e919968", - "boatstack/config_documentation_test.go": "bd34ac570d08c8d1d1e4f5e86c327c02a55dd5e96bd785b895a05f28fcd3f7a2", + "boatstack/config_documentation_test.go": "1fa56768409824afaff61bbe4ae2733ca854a4cbe87254a7954c6aaa2a6d75ac", "boatstack/decision.go": "257ca328da6ae19ab252f10ee5d06bd7daf49dd8141d083ab1b32f106ea7a94c", "boatstack/decision_test.go": "1a92ff832610f9559bd47ccac7fc1755a8b4f8261c35bc72a092830dff05f7c0", - "boatstack/delivery.go": "96b55c27d320c0a218b1db4ea9c61a8687dfcff8c6f9d3f1126f5b7ae7ed35bd", - "boatstack/delivery_test.go": "5ce806257307af5d2e4c74c07c77c3db119398f60953890d9b72f95cb168cae2", + "boatstack/delivery.go": "ea53af0e702ec3668a563a5f786dcac2e095362285ca6093b7ed71ec495a0a48", + "boatstack/delivery_test.go": "564ad2029a8412de7953967b1acdf377e6c87b6f6e3465d1f8741a4813f2949f", "boatstack/evidence.go": "497a31e6ff632cb1d7c3adfc9f269af3f6aa84e948dd5d417c162767542a27df", "boatstack/export.go": "1cb10bd4efc0906c22220a139da1b10ff7729881501b17ac369dd9dfedac97eb", "boatstack/export_test.go": "50c372ad5713107a8d5d34edb8d65bb56b21b97e3c63ceddf98e099bd9290d71", @@ -58,8 +58,8 @@ "boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c", "boatstack/migrate.go": "eaf589e2b266238068e42c6d78e01dc040266d28e342cb24f09e33e8541749b3", "boatstack/migrate_test.go": "9f4bda2fb158c5e54bcc0242dace1da3c1965f9846a213c573956a35b7d1724e", - "boatstack/next.go": "6bf119965704e8979921aaf514b205f85082716f34a5d1901ed63c175103227b", - "boatstack/next_test.go": "641bf32cf160c426531134a4756b0c41c6a6d876d515bb151cd01fd9c1ae5056", + "boatstack/next.go": "b7852451069764a486fb01b297b83c7882622e62505abf622559a8edcd49490c", + "boatstack/next_test.go": "6d5acd01311a1c357580040d5a14804cc3bb12706f5c997b66f5e1198271a9d6", "boatstack/operation.go": "62f97bf2091f33eb2ca91915bf08bee73d53387611b673e849355bfd516ca467", "boatstack/operation_test.go": "2d624eaba342b2c81b45cdf50918a65a9c002b5376a02041b24180658ee6a25a", "boatstack/plan.go": "8189ee42902bce62dcd39ce7a2e423cecb4c9e0d1dd34a5497e5a732a45c851f", @@ -73,17 +73,17 @@ "boatstack/recovery.go": "dd816b18b54a0085b8d8276a93ee98d2b1e90099059a0d85cf6e24edf6f37d5b", "boatstack/recovery_test.go": "29490e7477ba602491330036a491289dd9117b99ff862f66dae421ba17e04c9f", "boatstack/references/artifacts.md": "5fa888ac519085d65cee1d04df5902761651bcf2d7af81711fa0f8ecd1fc0f59", - "boatstack/references/config-schema.md": "c07c2d532ef95ea6ae538a1fffefb92ded1f8dc6a06eb3b8d463e371d1ed8416", + "boatstack/references/config-schema.md": "4df91e9769125bf21654595a749483b8db7d2a27399db346e9e194d525a328f7", "boatstack/references/failure-moves.md": "34a39aefb282b1b5d9ea387f8536bdc5e0145a240f102ae3d01c7ada6d4abebc", "boatstack/references/host-hook-contracts.md": "d68ae1556e7b1e29e9ac7cb4db767809d510aabf0be52e60e44665ea7abb980e", "boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441", "boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae", - "boatstack/references/workflow.md": "4d8331ff2ffc4cbcaba27818342fa11daa3b11fb7db6cbdce1b099a5175f6f52", + "boatstack/references/workflow.md": "5d9959ea1fdabec568472d08c14d47657ece6cdf108c231f1397baba200861b1", "boatstack/release.go": "82dcb4ca59e8c79a68d5333d650f90e64abd448d04e0c6f504fdf07f42b5ed76", "boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690", - "boatstack/run.go": "fbdbf583c862c41f23d1a200f53d042842db72f19c29fe94e4288a69b0ac4a6b", - "boatstack/run_test.go": "34484285fd2457b84faf6d5353117b74af73d4601cd60a96e270b7fd0a7a6a8e", - "boatstack/runtime.go": "687b3543c22acbbed91c94e798c2a813bd828e087ac180ab29a21e3a429f9971", + "boatstack/run.go": "74967ad5b3ed3847baffec1231aae69a81a70f9cce3a9412fa05bcfdc4eca6d1", + "boatstack/run_test.go": "5b291510fa90cefdc26eb89e18a3443385456a6ebc73408325ac1945b7c084d6", + "boatstack/runtime.go": "007f38f0631200b448f27f79b8cefb9874dd767b500dc389f2c9a663d0d0f9b0", "boatstack/runtime_cache.go": "60c4eb0c7dde91d40d6ef3f05adc1a1282d17ff1ca12470d0a008454f7ca7489", "boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85", "boatstack/safety.go": "994baf314fe1fda41a70cf7b0696a8ce8c7bfc666c39fe773ec7c21c1c8b8afe", @@ -106,11 +106,11 @@ "docs/account-recovery-walkthrough.md": "676034974594a7d1a559b24dbed31d7ccc429eb81404b203ca07bbdaa19ec3d3", "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", - "docs/configuration.md": "f530c5dcbacf32dcb6fdab590901d8f658a4f29bd93f6264cec5d4f449c2cbd1", - "docs/evidence-engineered-coding.md": "b888b8231789d7c24b12902a62e4a8c63527cbf88662acc3afac9d33fe0d89b1", + "docs/configuration.md": "4d8f207b415a5a1e3b9b1698ee7bb1221aa0e5496a061bb8054294df2f347ad1", + "docs/evidence-engineered-coding.md": "2f9d42da150a4552e59af30aaa83b3ff1dd7aadc5e2ba4135296aae2a4fab202", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", "docs/getting-started.md": "d5f0b170209e61518810a23b851bf9eb50b6755906703ca313e6e9faab20e1cd", - "docs/public-claims.json": "4990ea85732e1bdeb24cd5b8a1bb8ec875c758c069645ec43e4c5023918e9e7f", + "docs/public-claims.json": "35181ffc37a69bec15347a024a6e49a0eacb03efcfc1c06dd17a477bfb90dca9", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "d65c66e323037bda5d45aacef5d48afa6bf93da55901378891d235aca3a5684f", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -124,7 +124,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": "303844a2016d64a2c636708c83e62e33f03bd286eb6f5e3276e52d15a5624c62", + "labs/diagram-json/plan.lock.json": "9828551b5812c00108df19faf9b6faabc2e9efea6c2af10b83f1139e28637f9e", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -184,12 +184,13 @@ "release-notes/2026-07-22-value-translation-boundary.md": "9cf168ff7caaf3906b78533935bdfb2c86e753984ed1e5ec8204cb373d083390", "release-notes/2026-07-23-bootstrap-safe-update-repair.md": "d8e66c46ae05e3d228dfea45879f4d1166d5e3a253ac24bababd4c3e396c214b", "release-notes/2026-07-23-canonical-update-ownership.md": "7f34f890b252493797519389b23f1b56ec7ec16db7547aac8ea196f75f3b8c2c", + "release-notes/2026-07-23-ignore-ambiguous-deliveries.md": "9b1b9fd48db340b91fcced1c282297de0ecad8744723f2b1fdd94033927a88c4", "release-notes/2026-07-23-recoverable-repository-sync.md": "3afc4f6220ae76df3bd6dcd15fc180135274c808729e9c512a2c53462ec690c2" }, "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "d589911ccb9138013c8693d85b96be0e7c903be3", + "commit": "91e33a95add752894fb532a67b980c109ecd28e8", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/SKILL.md b/boatstack/SKILL.md index 097de38..3b6de1f 100644 --- a/boatstack/SKILL.md +++ b/boatstack/SKILL.md @@ -29,7 +29,7 @@ For the full state machine, read [workflow.md](references/workflow.md). For arti ## Report what is next -Run the project-local helper's read-only `next-status --repo . --json` inspection. Repository artifacts, managed delivery state, gate receipts, and the recorded PR identity are evidence; conversation, terminal, worktree, and process observations are context only. Never run the returned operation automatically. `NOT_STARTED` and `SOURCE_PLAN_READY` point to `auto-plan`; `PUBLISHED` means a PR exists but is not a verified merge; only `FEATURE_COMPLETE` requires no action. If state is ambiguous, stale, or invalid, name the blocker instead of choosing by recency or clearing artifacts. +Run the project-local helper's read-only `next-status --repo . --json` inspection. Repository artifacts, managed delivery state, gate receipts, and the recorded PR identity are evidence; conversation, terminal, worktree, and process observations are context only. Never run the returned operation automatically. `NOT_STARTED` and `SOURCE_PLAN_READY` point to `auto-plan`; `PUBLISHED` means a PR exists but is not a verified merge; only `FEATURE_COMPLETE` requires no action. If state is ambiguous, stale, or invalid, name the blocker instead of choosing by recency or clearing artifacts. When an `AMBIGUOUS` block names only past deliveries the user no longer cares about, name the ignorable delivery slug(s) and offer to exclude them from ambiguity resolution; only after explicit user confirmation, add each slug with `.product-loop/bin/boatstack-helper ignore-delivery --repo . --feature ` (a bounded, provenance-safe write to `workflow.ignored_deliveries` — never hand-edit config or delivery state). Any new, unlisted ambiguous delivery still pauses the workflow. ## Run through ship diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go index 3aa8f39..074de4c 100644 --- a/boatstack/cmd/boatstack-helper/main.go +++ b/boatstack/cmd/boatstack-helper/main.go @@ -598,6 +598,28 @@ func recordChangeCommand(arguments []string) int { return 0 } +func ignoreDeliveryCommand(arguments []string) int { + flags := flag.NewFlagSet("ignore-delivery", flag.ContinueOnError) + repo := flags.String("repo", ".", "repository containing the Boatstack installation") + feature := flags.String("feature", "", "feature slug of the past delivery to ignore") + if err := flags.Parse(arguments); err != nil { + return 2 + } + if *feature == "" { + return fail(fmt.Errorf("ignore-delivery requires --feature")) + } + added, err := boatstack.IgnoreDelivery(*repo, *feature) + if err != nil { + return fail(err) + } + if added { + fmt.Printf("PASS: delivery %s added to workflow.ignored_deliveries\n", *feature) + } else { + fmt.Printf("PASS: delivery %s already ignored\n", *feature) + } + return 0 +} + func doctorCommand(arguments []string) int { flags := flag.NewFlagSet("doctor", flag.ContinueOnError) repo := flags.String("repo", ".", "repository whose Boatstack installation should be checked") @@ -955,7 +977,7 @@ func workspaceSyncCommand(arguments []string) int { func run() int { if len(os.Args) < 2 { - fmt.Fprintln(os.Stderr, "usage: boatstack-helper ") + fmt.Fprintln(os.Stderr, "usage: boatstack-helper ") return 2 } switch os.Args[1] { @@ -999,6 +1021,8 @@ func run() int { return runPreflightCommand(os.Args[2:]) case "record-change": return recordChangeCommand(os.Args[2:]) + case "ignore-delivery": + return ignoreDeliveryCommand(os.Args[2:]) case "record-delivery-gate": return recordDeliveryGateCommand(os.Args[2:]) case "record-pr-visual-evidence": diff --git a/boatstack/config_documentation_test.go b/boatstack/config_documentation_test.go index 2a94d07..59f3547 100644 --- a/boatstack/config_documentation_test.go +++ b/boatstack/config_documentation_test.go @@ -120,6 +120,7 @@ func TestPublicConfigurationGuideContainsOnlySupportedUserControls(t *testing.T) "workflow.allow_pass_with_gaps", "workflow.boundary_analysis", "workflow.human_plan_approval", + "workflow.ignored_deliveries", "workflow.independent_review_for_high_risk", "workflow.maintain_changelog", "workflow.pr_visual_evidence", diff --git a/boatstack/delivery.go b/boatstack/delivery.go index 95e3d51..3213b14 100644 --- a/boatstack/delivery.go +++ b/boatstack/delivery.go @@ -879,3 +879,82 @@ func ActiveManagedDeliveries(repo string) ([]string, error) { sort.Strings(active) return active, nil } + +// withoutIgnoredDeliveries returns the feature slugs in features that are not +// listed in ignored. Order is preserved. It is used to scope delivery-ambiguity +// resolution to the deliveries the user still cares about; new, unlisted +// deliveries are unaffected. +func withoutIgnoredDeliveries(features []string, ignored []string) []string { + if len(ignored) == 0 { + return features + } + skip := make(map[string]bool, len(ignored)) + for _, slug := range ignored { + skip[slug] = true + } + kept := make([]string, 0, len(features)) + for _, f := range features { + if !skip[f] { + kept = append(kept, f) + } + } + return kept +} + +// withoutIgnoredDeliveryStates is the DeliveryState-slice variant of +// withoutIgnoredDeliveries, filtering by each state's Feature slug. +func withoutIgnoredDeliveryStates(states []DeliveryState, ignored []string) []DeliveryState { + if len(ignored) == 0 { + return states + } + skip := make(map[string]bool, len(ignored)) + for _, slug := range ignored { + skip[slug] = true + } + kept := make([]DeliveryState, 0, len(states)) + for _, state := range states { + if !skip[state.Feature] { + kept = append(kept, state) + } + } + return kept +} + +// IgnoreDelivery appends a feature slug to workflow.ignored_deliveries in the +// repository's project.json, deduplicating and preserving all other config. It +// is the bounded, provenance-safe write behind the ignore-delivery helper +// subcommand: the config round-trips through LoadConfig -> GeneratedJSON so the +// serialization contract and generator metadata are preserved. It returns +// whether the slug was newly added. +func IgnoreDelivery(repo, feature string) (bool, error) { + feature = strings.TrimSpace(feature) + if feature == "" { + return false, fmt.Errorf("ignore-delivery requires a feature slug") + } + if !featureSlugPattern.MatchString(feature) { + return false, fmt.Errorf("feature slug %q is not a valid Boatstack feature slug", feature) + } + resolved, err := ResolveRepository(repo) + if err != nil { + return false, err + } + configPath := filepath.Join(resolved, ".product-loop", "project.json") + config, _, err := LoadConfig(configPath) + if err != nil { + return false, err + } + for _, existing := range config.Workflow.IgnoredDeliveries { + if existing == feature { + return false, nil + } + } + config.Workflow.IgnoredDeliveries = append(config.Workflow.IgnoredDeliveries, feature) + value, err := GeneratedJSON(config) + if err != nil { + return false, err + } + if err := atomicWriteMode(configPath, value, 0o644); err != nil { + return false, err + } + return true, nil +} diff --git a/boatstack/delivery_test.go b/boatstack/delivery_test.go index 12deb3e..6dac3a9 100644 --- a/boatstack/delivery_test.go +++ b/boatstack/delivery_test.go @@ -3,10 +3,50 @@ package boatstack import ( "os" "path/filepath" + "reflect" "strings" "testing" ) +func TestIgnoreDeliveryAppendsDedupsAndPreservesConfig(t *testing.T) { + repo := nextTestRepo(t) + + added, err := IgnoreDelivery(repo, "old-feature") + if err != nil { + t.Fatal(err) + } + if !added { + t.Fatal("first ignore-delivery should report the slug as newly added") + } + + config, _, err := LoadConfig(filepath.Join(repo, ".product-loop", "project.json")) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(config.Workflow.IgnoredDeliveries, []string{"old-feature"}) { + t.Fatalf("ignored delivery not persisted: %+v", config.Workflow.IgnoredDeliveries) + } + // Other config must survive the round-trip. + if config.Project.Name != "fixture" || config.Workflow.HumanPlanApproval != true { + t.Fatalf("ignore-delivery clobbered unrelated config: %+v", config) + } + + added, err = IgnoreDelivery(repo, "old-feature") + if err != nil { + t.Fatal(err) + } + if added { + t.Fatal("repeat ignore-delivery should be a no-op") + } + config, _, err = LoadConfig(filepath.Join(repo, ".product-loop", "project.json")) + if err != nil { + t.Fatal(err) + } + if len(config.Workflow.IgnoredDeliveries) != 1 { + t.Fatalf("ignore-delivery duplicated the slug: %+v", config.Workflow.IgnoredDeliveries) + } +} + func twoSlicePlan() map[string]any { plan := validPlan() plan["acceptance_criteria"] = []any{ diff --git a/boatstack/next.go b/boatstack/next.go index 9d73fff..52df601 100644 --- a/boatstack/next.go +++ b/boatstack/next.go @@ -245,6 +245,7 @@ func ResolveNext(repoPath, explicitFeature 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 } + active = withoutIgnoredDeliveries(active, config.Workflow.IgnoredDeliveries) if explicitFeature != "" { found := false @@ -355,6 +356,7 @@ func ResolveNext(repoPath, explicitFeature string) (NextStatus, error) { if err != nil { return blockedNextStatus("INVALID_STATE", "repair-state", "Boatstack found invalid completed delivery state. Preserve the artifacts and restore its evidence before continuing: "+err.Error()), nil } + completed = withoutIgnoredDeliveryStates(completed, config.Workflow.IgnoredDeliveries) if len(completed) > 0 { if len(completed) == 1 { base = nextForPublished(repo, completed[0]) diff --git a/boatstack/next_test.go b/boatstack/next_test.go index 6c2a04f..aeecf41 100644 --- a/boatstack/next_test.go +++ b/boatstack/next_test.go @@ -354,6 +354,71 @@ func TestResolveNextBlocksMultipleActiveFeaturesWithoutMutation(t *testing.T) { } } +func setIgnoredDeliveries(t *testing.T, repo string, ignored ...string) { + t.Helper() + config := testConfig() + config.Workflow.IgnoredDeliveries = ignored + value, err := MarshalJSON(config) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(repo, ".product-loop", "project.json"), value, 0o644); err != nil { + t.Fatal(err) + } +} + +func TestResolveNextIgnoredActiveDeliveryClearsAmbiguity(t *testing.T) { + repo := nextTestRepo(t) + writeNextDelivery(t, repo, "first", "BUILD", 0) + writeNextDelivery(t, repo, "second", "BUILD", 0) + setIgnoredDeliveries(t, repo, "first") + + status, err := ResolveNext(repo, "") + if err != nil { + t.Fatal(err) + } + if status.ObservedStage == "AMBIGUOUS" { + t.Fatalf("ignored active delivery did not clear ambiguity: %+v", status) + } + if status.Feature != "second" || status.NextOperation != "build" { + t.Fatalf("remaining active delivery did not resolve uniquely: %+v", status) + } +} + +func TestResolveNextIgnoredPublishedDeliveryClearsAmbiguity(t *testing.T) { + repo := nextTestRepo(t) + writeNextDelivery(t, repo, "published-one", "PUBLISHED", 1) + writeNextDelivery(t, repo, "published-two", "PUBLISHED", 1) + setIgnoredDeliveries(t, repo, "published-one") + + status, err := ResolveNext(repo, "") + if err != nil { + t.Fatal(err) + } + if status.VerificationStatus == "BLOCKED" || status.ObservedStage == "AMBIGUOUS" { + t.Fatalf("ignored published delivery did not clear ambiguity: %+v", status) + } + if status.Feature != "published-two" { + t.Fatalf("remaining published delivery did not resolve uniquely: %+v", status) + } +} + +func TestResolveNextNewUnignoredActiveDeliveryStillBlocks(t *testing.T) { + repo := nextTestRepo(t) + writeNextDelivery(t, repo, "first", "BUILD", 0) + writeNextDelivery(t, repo, "second", "BUILD", 0) + // Ignoring an unrelated slug must not clear a genuinely ambiguous pair. + setIgnoredDeliveries(t, repo, "unrelated") + + status, err := ResolveNext(repo, "") + if err != nil { + t.Fatal(err) + } + if status.VerificationStatus != "BLOCKED" || status.ObservedStage != "AMBIGUOUS" || !reflect.DeepEqual(status.BlockingAmbiguity, []string{"first", "second"}) { + t.Fatalf("un-ignored ambiguous deliveries should still block: %+v", status) + } +} + func TestResolveNextBlocksStaleManagedState(t *testing.T) { repo := nextTestRepo(t) writeNextDelivery(t, repo, "recovery", "BUILD", 0) diff --git a/boatstack/references/config-schema.md b/boatstack/references/config-schema.md index 64f193b..f443a55 100644 --- a/boatstack/references/config-schema.md +++ b/boatstack/references/config-schema.md @@ -15,6 +15,7 @@ boatstack-config-field:workflow.allow_pass_with_gaps boatstack-config-field:workflow.maintain_changelog boatstack-config-field:workflow.boundary_analysis boatstack-config-field:workflow.pr_visual_evidence +boatstack-config-field:workflow.ignored_deliveries boatstack-config-field:workspace boatstack-config-field:workspace.enabled boatstack-config-field:workspace.mode @@ -65,6 +66,7 @@ This is the exhaustive serialization contract, not a list of recommended user ed - `maintain_changelog` (boolean, optional): Whether a reader-visible `CHANGELOG.md` entry is required for each delivery slice. - `boundary_analysis` (boolean, optional): Agent-mediated planning guidance that presents local repair versus programmatic enforcement as a material product decision. - `pr_visual_evidence` (string, optional): `off`, `suggest`, or `require`. Omission is `off`. Relevant PRs use machine-local PNG evidence without committing media to Git; `suggest` records missing evidence as a visible gap and `require` blocks completed publication. +- `ignored_deliveries` (array of strings, optional): Deterministic ambiguity control. Feature slugs of past deliveries to exclude from delivery-ambiguity resolution so historical work no longer blocks new work. New, unlisted ambiguous deliveries still pause the workflow. ### workspace Fields diff --git a/boatstack/references/workflow.md b/boatstack/references/workflow.md index 7c5cb08..1e67d46 100644 --- a/boatstack/references/workflow.md +++ b/boatstack/references/workflow.md @@ -98,7 +98,7 @@ Lead with a plain outcome, never a machine code such as `PASS`, `PLAN_APPROVED`, | State | Outcome -> one next action | |---|---| -| `next`, `/boatstack-next`, `$boatstack next` not started / active / complete / ambiguous | **Start a Boatstack feature** -> save a Plan-mode file or run `auto-plan`; **Next Boatstack stage** -> run the one repository-backed operation; **Feature complete** -> no action required; **Boatstack state needs attention** -> resolve the named ambiguity or invalid evidence | +| `next`, `/boatstack-next`, `$boatstack next` not started / active / complete / ambiguous | **Start a Boatstack feature** -> save a Plan-mode file or run `auto-plan`; **Next Boatstack stage** -> run the one repository-backed operation; **Feature complete** -> no action required; **Boatstack state needs attention** -> resolve the named ambiguity (address the invalid evidence, or, when the block names only past deliveries, ignore a named past delivery after explicit user confirmation) | | `run`, `/boatstack-run`, `$boatstack run` not started / complete / paused / blocked | **Start a Boatstack feature** -> save a Plan-mode file; **Feature ready for review** -> review the published PRs; **Boatstack run paused** -> provide the one required approval, confirmation, or product answer; **Boatstack run needs attention** -> resolve the named freshness, safety, state, or repair blocker | | `auto-plan` ready / needs answers | **Plan ready** -> run `/plan-gate`; **I need your input** -> answer with the displayed choice keys or `r` for all recommendations | | `plan-gate` pending / approved | **Ready for your approval** -> reply `a` to approve; **Approved — ready to build** -> enter execution mode and run `/build` | @@ -116,7 +116,7 @@ Lead with a plain outcome, never a machine code such as `PASS`, `PLAN_APPROVED`, `run` is an opt-in foreground coordinator over the existing operations, not a second state machine. It first resolves the read-only repository state, enters `auto-plan` when one saved source plan exists, asks for a saved Plan-mode file when none exists, returns **Feature complete** without requiring a remote only for completed work, and stops on unverified or blocked state. Before the first delivery-stage mutation it runs the versioned Git preflight, which fetches `origin`, requires the fetched remote base, verifies that the current named branch contains that base, rejects a behind or diverged upstream, and enforces any active slice branch constraints. Planning and approval remain local and do not require a remote. It never merges, rebases, switches or creates constrained branches, discards changes, force-pushes, merges a PR, or deploys. -After preflight, resolve the repository-backed next operation, execute exactly that canonical operation, verify the resulting state, and resolve again through all declared delivery slices. Pause for `a`, a material product answer, and `o` or `u`; after the valid state-scoped reply, continue in the current host session. The invocation does not replace either human authorization. Automatically record and repair same-intent test or review failures for at most three complete repair-and-gate cycles per active slice per invocation. Stop immediately for requirement amendments, ambiguous or stale state, unsafe capability, unsupported recovery, branch mismatch, or exhausted repairs. Store no durable run/autopilot mode; re-invocation reconstructs progress from canonical repository state. +After preflight, resolve the repository-backed next operation, execute exactly that canonical operation, verify the resulting state, and resolve again through all declared delivery slices. When the resolved block names only past deliveries, the coordinator may offer to ignore a named past delivery (adding its slug to `workflow.ignored_deliveries`) only after explicit user confirmation; any new, unlisted ambiguous delivery still pauses. Pause for `a`, a material product answer, and `o` or `u`; after the valid state-scoped reply, continue in the current host session. The invocation does not replace either human authorization. Automatically record and repair same-intent test or review failures for at most three complete repair-and-gate cycles per active slice per invocation. Stop immediately for requirement amendments, ambiguous or stale state, unsafe capability, unsupported recovery, branch mismatch, or exhausted repairs. Store no durable run/autopilot mode; re-invocation reconstructs progress from canonical repository state. ### Reply shortcuts diff --git a/boatstack/run.go b/boatstack/run.go index 190a06c..f47a56e 100644 --- a/boatstack/run.go +++ b/boatstack/run.go @@ -41,6 +41,12 @@ func runBranches(repo, explicitFeature string) (string, string, error) { if err != nil { return "", "", err } + // Scope the ambiguity check to un-ignored deliveries so the foreground + // coordinator matches ResolveNext. A config that fails to load leaves active + // unfiltered, preserving the prior >1-active behavior. + if config, _, configErr := LoadConfig(filepath.Join(repo, ".product-loop", "project.json")); configErr == nil { + active = withoutIgnoredDeliveries(active, config.Workflow.IgnoredDeliveries) + } if explicitFeature != "" { found := false diff --git a/boatstack/run_test.go b/boatstack/run_test.go index 1ed398f..bc0ee45 100644 --- a/boatstack/run_test.go +++ b/boatstack/run_test.go @@ -57,6 +57,52 @@ func withRunGit(t *testing.T, responses map[string]struct { t.Cleanup(func() { runGitCommand = old }) } +func writeRunConfig(t *testing.T, repo string, ignored ...string) { + t.Helper() + config := testConfig() + config.Project.DefaultBranch = "main" + config.Adapters = []string{"cursor"} + config.Workflow.IgnoredDeliveries = ignored + value, err := MarshalJSON(config) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(repo, ".product-loop", "project.json"), value, 0o644); err != nil { + t.Fatal(err) + } +} + +func TestRunBranchesIgnoredActiveDeliveryClearsAmbiguity(t *testing.T) { + repo := runTestRepo(t) + writeNextDelivery(t, repo, "first", "BUILD", 0) + writeNextDelivery(t, repo, "second", "BUILD", 0) + writeRunConfig(t, repo, "first") + withRunGit(t, map[string]struct { + value string + err error + }{"branch --show-current": {value: "feature"}}) + + if _, _, err := runBranches(repo, ""); err != nil { + t.Fatalf("ignored active delivery should clear run ambiguity: %v", err) + } +} + +func TestRunBranchesNewUnignoredActiveDeliveryStillBlocks(t *testing.T) { + repo := runTestRepo(t) + writeNextDelivery(t, repo, "first", "BUILD", 0) + writeNextDelivery(t, repo, "second", "BUILD", 0) + writeRunConfig(t, repo, "unrelated") + withRunGit(t, map[string]struct { + value string + err error + }{"branch --show-current": {value: "feature"}}) + + _, _, err := runBranches(repo, "") + if err == nil || !strings.Contains(err.Error(), "more than one managed delivery is active") { + t.Fatalf("un-ignored ambiguous deliveries should still block run: %v", err) + } +} + func TestCheckRunPreflightRequiresOriginBeforeMutation(t *testing.T) { repo := runTestRepo(t) before, err := os.ReadFile(filepath.Join(repo, ".product-loop", "project.json")) diff --git a/boatstack/runtime.go b/boatstack/runtime.go index 8d63abf..b1a2947 100644 --- a/boatstack/runtime.go +++ b/boatstack/runtime.go @@ -53,6 +53,10 @@ type Workflow struct { MaintainChangelog bool `json:"maintain_changelog"` BoundaryAnalysis bool `json:"boundary_analysis,omitempty"` PRVisualEvidence string `json:"pr_visual_evidence,omitempty"` + // IgnoredDeliveries lists feature slugs of past deliveries to exclude from + // delivery-ambiguity resolution. New, unlisted ambiguous deliveries still + // pause the workflow. Persisted via the LoadConfig -> GeneratedJSON round-trip. + IgnoredDeliveries []string `json:"ignored_deliveries,omitempty"` } type IntegrationState struct { diff --git a/docs/configuration.md b/docs/configuration.md index 296ab2d..1af7698 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,6 +11,7 @@ boatstack-user-config-field:workflow.allow_pass_with_gaps boatstack-user-config-field:workflow.maintain_changelog boatstack-user-config-field:workflow.boundary_analysis boatstack-user-config-field:workflow.pr_visual_evidence +boatstack-user-config-field:workflow.ignored_deliveries boatstack-user-config-field:workspace.enabled boatstack-user-config-field:workspace.mode boatstack-user-config-field:workspace.cleanup @@ -34,6 +35,7 @@ Boatstack's installer owns the complete `.boatstack-project.json` shape. Edit on | Maintain reader-facing history | `workflow.maintain_changelog` | Managed delivery and Boatstack-prepared PRs require a categorized `CHANGELOG.md` entry. | | Check for a systemic boundary | `workflow.boundary_analysis` | Planning guidance asks whether the request is a local symptom before scope expands. | | Add frontend PR screenshots | `workflow.pr_visual_evidence` | `suggest` exposes missing screenshots as a gap; `require` blocks completed publication. | +| Ignore old ambiguous deliveries | `workflow.ignored_deliveries` | Listed feature slugs are excluded from delivery-ambiguity resolution so past work stops blocking new work; new, unlisted ambiguous deliveries still pause. | | Use fresh feature workspaces | `workspace.*` | Boatstack creates and cleans branches or linked worktrees under the selected policy. | | Limit generated host surfaces | `adapters` | Export generates only the selected supported adapters. | @@ -93,6 +95,16 @@ Changelog enforcement is mechanical. Boundary analysis is model-mediated plannin Visual-evidence values are `off`, `suggest`, and `require`. Screenshot bytes stay outside Git history until explicitly attached to the PR. +```json +{ + "workflow": { + "ignored_deliveries": ["old-feature-slug", "another-past-feature"] + } +} +``` + +List feature slugs here to drop past deliveries from the ambiguity check so historical work no longer blocks new work. Any new, unlisted ambiguous delivery still pauses the workflow. + ## Workspace and adapter controls ```json diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 9658d40..3d4d1ef 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -96,7 +96,7 @@ subject to acceptance criteria pass approval is current ``` -That is why context trimming is not automatically an optimization. If removing state increases rework or false acceptance, total cost rises. The canonical runtime references are approximately **15054 estimated tokens**, while host adapters point to one operation at a time. +That is why context trimming is not automatically an optimization. If removing state increases rework or false acceptance, total cost rises. The canonical runtime references are approximately **15147 estimated tokens**, while host adapters point to one operation at a time. ## Control appears at transitions @@ -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 [`d589911ccb9138013c8693d85b96be0e7c903be3`](https://github.com/operatorstack/intelligence-flow/tree/d589911ccb9138013c8693d85b96be0e7c903be3/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 [`91e33a95add752894fb532a67b980c109ecd28e8`](https://github.com/operatorstack/intelligence-flow/tree/91e33a95add752894fb532a67b980c109ecd28e8/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 936c241..f060a82 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "d589911ccb9138013c8693d85b96be0e7c903be3", + "source_commit": "91e33a95add752894fb532a67b980c109ecd28e8", "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:d589911ccb9138013c8693d85b96be0e7c903be3" + "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" }, { "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:d589911ccb9138013c8693d85b96be0e7c903be3" + "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" }, { "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:d589911ccb9138013c8693d85b96be0e7c903be3" + "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" }, { "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:d589911ccb9138013c8693d85b96be0e7c903be3" + "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" }, { "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:d589911ccb9138013c8693d85b96be0e7c903be3" + "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" }, { "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:d589911ccb9138013c8693d85b96be0e7c903be3" + "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" }, { "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:d589911ccb9138013c8693d85b96be0e7c903be3" + "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" }, { "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:d589911ccb9138013c8693d85b96be0e7c903be3" + "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" }, { "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:d589911ccb9138013c8693d85b96be0e7c903be3" + "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" }, { "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:d589911ccb9138013c8693d85b96be0e7c903be3" + "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" }, { "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:d589911ccb9138013c8693d85b96be0e7c903be3" + "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 2de37d2..a212c2b 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": "d589911ccb9138013c8693d85b96be0e7c903be3", + "source_commit": "91e33a95add752894fb532a67b980c109ecd28e8", "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-ignore-ambiguous-deliveries.md b/release-notes/2026-07-23-ignore-ambiguous-deliveries.md new file mode 100644 index 0000000..f27c147 --- /dev/null +++ b/release-notes/2026-07-23-ignore-ambiguous-deliveries.md @@ -0,0 +1,5 @@ +### Resolve delivery ambiguity for known past deliveries + +Boatstack no longer hard-stops new work when historical published or active deliveries make the delivery state ambiguous. A new `workflow.ignored_deliveries` configuration field lists feature slugs to exclude from ambiguity resolution, and both `next` and `run` honor it deterministically. + +New, un-ignored ambiguous deliveries still pause the workflow, so the safety interlock is preserved. A bounded `ignore-delivery --repo . --feature ` helper appends a slug to the list after explicit user confirmation, persisting it through the standard configuration round-trip.