Skip to content

Commit 6c1b8a0

Browse files
Sync Boatstack from Intelligence Flow Labs @ 91e33a95add7
1 parent 169b764 commit 6c1b8a0

19 files changed

Lines changed: 327 additions & 40 deletions

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Contributing
44

5-
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).
5+
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).
66

77
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.
88

UPSTREAM.json

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"canonical_context": {
3-
"characters": 60215,
4-
"estimated_tokens": 15054,
3+
"characters": 60587,
4+
"estimated_tokens": 15147,
55
"estimator": "ceil(total characters / 4); compactness signal, not provider billing",
66
"files": [
77
"product-engineering-loop/references/workflow.md",
@@ -12,12 +12,12 @@
1212
},
1313
"files": {
1414
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
15-
"CONTRIBUTING.md": "35fa5831dec2a7df663652bab27a2bf31b16f04f4ea9263cd780ce4bb9df8166",
15+
"CONTRIBUTING.md": "f9ed388745c12187dcc3e39fc4821ec5094085182157005cae561df38a3ab2c8",
1616
"README.md": "125b47671a68556df382f19756fb61fa18925606cbbaf54d6bc9df8872b36870",
1717
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
1818
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
1919
"assets/boatstack-portability.svg": "66dfdfa85db857b3bd18b32047a6975f1fbbfc4dc091158e8277193f9969a346",
20-
"boatstack/SKILL.md": "80c4599f2ffeb686d45fdfc1bb4eac4635bded5a5621fa71c41eed7160f4d125",
20+
"boatstack/SKILL.md": "1fbdab536732aca6425d657acfe0eb2f37743cb542c07e92f8e06ddafdb910ff",
2121
"boatstack/agents/gemini.yaml": "cbf43b387399e456fa6178f86d83e6e35567e6142ff800f8de6ffca306fa963e",
2222
"boatstack/agents/openai.yaml": "68a30a60859556c5a26e16d184594ca243a6043d99c8cf7d66b5dd6d50a93cd1",
2323
"boatstack/assets/templates/adr.md": "c577a3c1c1319061f61deb053597e6e853657022185fe28b8f733327e2a78565",
@@ -34,15 +34,15 @@
3434
"boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f",
3535
"boatstack/changelog.go": "c5e1f31440b44d61e6037ad27af0333540af3545d655e35819a0241cbbebd8ec",
3636
"boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7",
37-
"boatstack/cmd/boatstack-helper/main.go": "3b75e71f201825d5c9b83309ec551f79ca27e6de4a58175841e404e74c106931",
37+
"boatstack/cmd/boatstack-helper/main.go": "718803eb4e0c1ca1bb0ca2923f64cf055d35dab6e3ad7f6a9f62b0cec724b610",
3838
"boatstack/cmd/boatstack-helper/main_test.go": "ff73003b6a5157202fa09ddf1129fb13c3d79702b2e05a8721ce5a11bf5ab779",
3939
"boatstack/command.go": "94d2117c6e390d5a644afc5cd90f7e712e3f8b1032f8c9e3b253cc134524c28a",
4040
"boatstack/command_test.go": "9f707abba3640add81c3e97ba7e72fedbf98f3394b1c060a9ca4b4a28e919968",
41-
"boatstack/config_documentation_test.go": "bd34ac570d08c8d1d1e4f5e86c327c02a55dd5e96bd785b895a05f28fcd3f7a2",
41+
"boatstack/config_documentation_test.go": "1fa56768409824afaff61bbe4ae2733ca854a4cbe87254a7954c6aaa2a6d75ac",
4242
"boatstack/decision.go": "257ca328da6ae19ab252f10ee5d06bd7daf49dd8141d083ab1b32f106ea7a94c",
4343
"boatstack/decision_test.go": "1a92ff832610f9559bd47ccac7fc1755a8b4f8261c35bc72a092830dff05f7c0",
44-
"boatstack/delivery.go": "96b55c27d320c0a218b1db4ea9c61a8687dfcff8c6f9d3f1126f5b7ae7ed35bd",
45-
"boatstack/delivery_test.go": "5ce806257307af5d2e4c74c07c77c3db119398f60953890d9b72f95cb168cae2",
44+
"boatstack/delivery.go": "ea53af0e702ec3668a563a5f786dcac2e095362285ca6093b7ed71ec495a0a48",
45+
"boatstack/delivery_test.go": "564ad2029a8412de7953967b1acdf377e6c87b6f6e3465d1f8741a4813f2949f",
4646
"boatstack/evidence.go": "497a31e6ff632cb1d7c3adfc9f269af3f6aa84e948dd5d417c162767542a27df",
4747
"boatstack/export.go": "1cb10bd4efc0906c22220a139da1b10ff7729881501b17ac369dd9dfedac97eb",
4848
"boatstack/export_test.go": "50c372ad5713107a8d5d34edb8d65bb56b21b97e3c63ceddf98e099bd9290d71",
@@ -58,8 +58,8 @@
5858
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
5959
"boatstack/migrate.go": "eaf589e2b266238068e42c6d78e01dc040266d28e342cb24f09e33e8541749b3",
6060
"boatstack/migrate_test.go": "9f4bda2fb158c5e54bcc0242dace1da3c1965f9846a213c573956a35b7d1724e",
61-
"boatstack/next.go": "6bf119965704e8979921aaf514b205f85082716f34a5d1901ed63c175103227b",
62-
"boatstack/next_test.go": "641bf32cf160c426531134a4756b0c41c6a6d876d515bb151cd01fd9c1ae5056",
61+
"boatstack/next.go": "b7852451069764a486fb01b297b83c7882622e62505abf622559a8edcd49490c",
62+
"boatstack/next_test.go": "6d5acd01311a1c357580040d5a14804cc3bb12706f5c997b66f5e1198271a9d6",
6363
"boatstack/operation.go": "62f97bf2091f33eb2ca91915bf08bee73d53387611b673e849355bfd516ca467",
6464
"boatstack/operation_test.go": "2d624eaba342b2c81b45cdf50918a65a9c002b5376a02041b24180658ee6a25a",
6565
"boatstack/plan.go": "8189ee42902bce62dcd39ce7a2e423cecb4c9e0d1dd34a5497e5a732a45c851f",
@@ -73,17 +73,17 @@
7373
"boatstack/recovery.go": "dd816b18b54a0085b8d8276a93ee98d2b1e90099059a0d85cf6e24edf6f37d5b",
7474
"boatstack/recovery_test.go": "29490e7477ba602491330036a491289dd9117b99ff862f66dae421ba17e04c9f",
7575
"boatstack/references/artifacts.md": "5fa888ac519085d65cee1d04df5902761651bcf2d7af81711fa0f8ecd1fc0f59",
76-
"boatstack/references/config-schema.md": "c07c2d532ef95ea6ae538a1fffefb92ded1f8dc6a06eb3b8d463e371d1ed8416",
76+
"boatstack/references/config-schema.md": "4df91e9769125bf21654595a749483b8db7d2a27399db346e9e194d525a328f7",
7777
"boatstack/references/failure-moves.md": "34a39aefb282b1b5d9ea387f8536bdc5e0145a240f102ae3d01c7ada6d4abebc",
7878
"boatstack/references/host-hook-contracts.md": "d68ae1556e7b1e29e9ac7cb4db767809d510aabf0be52e60e44665ea7abb980e",
7979
"boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441",
8080
"boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae",
81-
"boatstack/references/workflow.md": "4d8331ff2ffc4cbcaba27818342fa11daa3b11fb7db6cbdce1b099a5175f6f52",
81+
"boatstack/references/workflow.md": "5d9959ea1fdabec568472d08c14d47657ece6cdf108c231f1397baba200861b1",
8282
"boatstack/release.go": "82dcb4ca59e8c79a68d5333d650f90e64abd448d04e0c6f504fdf07f42b5ed76",
8383
"boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690",
84-
"boatstack/run.go": "fbdbf583c862c41f23d1a200f53d042842db72f19c29fe94e4288a69b0ac4a6b",
85-
"boatstack/run_test.go": "34484285fd2457b84faf6d5353117b74af73d4601cd60a96e270b7fd0a7a6a8e",
86-
"boatstack/runtime.go": "687b3543c22acbbed91c94e798c2a813bd828e087ac180ab29a21e3a429f9971",
84+
"boatstack/run.go": "74967ad5b3ed3847baffec1231aae69a81a70f9cce3a9412fa05bcfdc4eca6d1",
85+
"boatstack/run_test.go": "5b291510fa90cefdc26eb89e18a3443385456a6ebc73408325ac1945b7c084d6",
86+
"boatstack/runtime.go": "007f38f0631200b448f27f79b8cefb9874dd767b500dc389f2c9a663d0d0f9b0",
8787
"boatstack/runtime_cache.go": "60c4eb0c7dde91d40d6ef3f05adc1a1282d17ff1ca12470d0a008454f7ca7489",
8888
"boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85",
8989
"boatstack/safety.go": "994baf314fe1fda41a70cf7b0696a8ce8c7bfc666c39fe773ec7c21c1c8b8afe",
@@ -106,11 +106,11 @@
106106
"docs/account-recovery-walkthrough.md": "676034974594a7d1a559b24dbed31d7ccc429eb81404b203ca07bbdaa19ec3d3",
107107
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
108108
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
109-
"docs/configuration.md": "f530c5dcbacf32dcb6fdab590901d8f658a4f29bd93f6264cec5d4f449c2cbd1",
110-
"docs/evidence-engineered-coding.md": "b888b8231789d7c24b12902a62e4a8c63527cbf88662acc3afac9d33fe0d89b1",
109+
"docs/configuration.md": "4d8f207b415a5a1e3b9b1698ee7bb1221aa0e5496a061bb8054294df2f347ad1",
110+
"docs/evidence-engineered-coding.md": "2f9d42da150a4552e59af30aaa83b3ff1dd7aadc5e2ba4135296aae2a4fab202",
111111
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
112112
"docs/getting-started.md": "d5f0b170209e61518810a23b851bf9eb50b6755906703ca313e6e9faab20e1cd",
113-
"docs/public-claims.json": "4990ea85732e1bdeb24cd5b8a1bb8ec875c758c069645ec43e4c5023918e9e7f",
113+
"docs/public-claims.json": "35181ffc37a69bec15347a024a6e49a0eacb03efcfc1c06dd17a477bfb90dca9",
114114
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
115115
"docs/research-and-design.md": "d65c66e323037bda5d45aacef5d48afa6bf93da55901378891d235aca3a5684f",
116116
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
@@ -124,7 +124,7 @@
124124
"labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d",
125125
"labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71",
126126
"labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39",
127-
"labs/diagram-json/plan.lock.json": "303844a2016d64a2c636708c83e62e33f03bd286eb6f5e3276e52d15a5624c62",
127+
"labs/diagram-json/plan.lock.json": "9828551b5812c00108df19faf9b6faabc2e9efea6c2af10b83f1139e28637f9e",
128128
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
129129
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
130130
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
@@ -184,12 +184,13 @@
184184
"release-notes/2026-07-22-value-translation-boundary.md": "9cf168ff7caaf3906b78533935bdfb2c86e753984ed1e5ec8204cb373d083390",
185185
"release-notes/2026-07-23-bootstrap-safe-update-repair.md": "d8e66c46ae05e3d228dfea45879f4d1166d5e3a253ac24bababd4c3e396c214b",
186186
"release-notes/2026-07-23-canonical-update-ownership.md": "7f34f890b252493797519389b23f1b56ec7ec16db7547aac8ea196f75f3b8c2c",
187+
"release-notes/2026-07-23-ignore-ambiguous-deliveries.md": "9b1b9fd48db340b91fcced1c282297de0ecad8744723f2b1fdd94033927a88c4",
187188
"release-notes/2026-07-23-recoverable-repository-sync.md": "3afc4f6220ae76df3bd6dcd15fc180135274c808729e9c512a2c53462ec690c2"
188189
},
189190
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
190191
"schema_version": 1,
191192
"source": {
192-
"commit": "d589911ccb9138013c8693d85b96be0e7c903be3",
193+
"commit": "91e33a95add752894fb532a67b980c109ecd28e8",
193194
"path": "labs/12-product-engineering-loop",
194195
"repository": "operatorstack/intelligence-flow"
195196
}

boatstack/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ For the full state machine, read [workflow.md](references/workflow.md). For arti
2929

3030
## Report what is next
3131

32-
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.
32+
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 <slug>` (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.
3333

3434
## Run through ship
3535

boatstack/cmd/boatstack-helper/main.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,28 @@ func recordChangeCommand(arguments []string) int {
598598
return 0
599599
}
600600

601+
func ignoreDeliveryCommand(arguments []string) int {
602+
flags := flag.NewFlagSet("ignore-delivery", flag.ContinueOnError)
603+
repo := flags.String("repo", ".", "repository containing the Boatstack installation")
604+
feature := flags.String("feature", "", "feature slug of the past delivery to ignore")
605+
if err := flags.Parse(arguments); err != nil {
606+
return 2
607+
}
608+
if *feature == "" {
609+
return fail(fmt.Errorf("ignore-delivery requires --feature"))
610+
}
611+
added, err := boatstack.IgnoreDelivery(*repo, *feature)
612+
if err != nil {
613+
return fail(err)
614+
}
615+
if added {
616+
fmt.Printf("PASS: delivery %s added to workflow.ignored_deliveries\n", *feature)
617+
} else {
618+
fmt.Printf("PASS: delivery %s already ignored\n", *feature)
619+
}
620+
return 0
621+
}
622+
601623
func doctorCommand(arguments []string) int {
602624
flags := flag.NewFlagSet("doctor", flag.ContinueOnError)
603625
repo := flags.String("repo", ".", "repository whose Boatstack installation should be checked")
@@ -955,7 +977,7 @@ func workspaceSyncCommand(arguments []string) int {
955977

956978
func run() int {
957979
if len(os.Args) < 2 {
958-
fmt.Fprintln(os.Stderr, "usage: boatstack-helper <init|update|check-update|repair-status|operation-status|prepare-update-pr|publish-update-pr|release-classify|next-patch|export|check-source-plan|planning-write|check-plan|record-approval|activate-plan|delivery-status|next-status|recovery-status|run-preflight|record-change|record-delivery-gate|record-pr-visual-evidence|record-pr-visual-publication|check-safety|migrate-config|safety-hook|diagnose-hook|pr-context|check-pr|publish-pr|workspace-cut|workspace-cleanup|workspace-status|workspace-sync|doctor|version>")
980+
fmt.Fprintln(os.Stderr, "usage: boatstack-helper <init|update|check-update|repair-status|operation-status|prepare-update-pr|publish-update-pr|release-classify|next-patch|export|check-source-plan|planning-write|check-plan|record-approval|activate-plan|delivery-status|next-status|recovery-status|run-preflight|record-change|ignore-delivery|record-delivery-gate|record-pr-visual-evidence|record-pr-visual-publication|check-safety|migrate-config|safety-hook|diagnose-hook|pr-context|check-pr|publish-pr|workspace-cut|workspace-cleanup|workspace-status|workspace-sync|doctor|version>")
959981
return 2
960982
}
961983
switch os.Args[1] {
@@ -999,6 +1021,8 @@ func run() int {
9991021
return runPreflightCommand(os.Args[2:])
10001022
case "record-change":
10011023
return recordChangeCommand(os.Args[2:])
1024+
case "ignore-delivery":
1025+
return ignoreDeliveryCommand(os.Args[2:])
10021026
case "record-delivery-gate":
10031027
return recordDeliveryGateCommand(os.Args[2:])
10041028
case "record-pr-visual-evidence":

boatstack/config_documentation_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ func TestPublicConfigurationGuideContainsOnlySupportedUserControls(t *testing.T)
120120
"workflow.allow_pass_with_gaps",
121121
"workflow.boundary_analysis",
122122
"workflow.human_plan_approval",
123+
"workflow.ignored_deliveries",
123124
"workflow.independent_review_for_high_risk",
124125
"workflow.maintain_changelog",
125126
"workflow.pr_visual_evidence",

boatstack/delivery.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,3 +879,82 @@ func ActiveManagedDeliveries(repo string) ([]string, error) {
879879
sort.Strings(active)
880880
return active, nil
881881
}
882+
883+
// withoutIgnoredDeliveries returns the feature slugs in features that are not
884+
// listed in ignored. Order is preserved. It is used to scope delivery-ambiguity
885+
// resolution to the deliveries the user still cares about; new, unlisted
886+
// deliveries are unaffected.
887+
func withoutIgnoredDeliveries(features []string, ignored []string) []string {
888+
if len(ignored) == 0 {
889+
return features
890+
}
891+
skip := make(map[string]bool, len(ignored))
892+
for _, slug := range ignored {
893+
skip[slug] = true
894+
}
895+
kept := make([]string, 0, len(features))
896+
for _, f := range features {
897+
if !skip[f] {
898+
kept = append(kept, f)
899+
}
900+
}
901+
return kept
902+
}
903+
904+
// withoutIgnoredDeliveryStates is the DeliveryState-slice variant of
905+
// withoutIgnoredDeliveries, filtering by each state's Feature slug.
906+
func withoutIgnoredDeliveryStates(states []DeliveryState, ignored []string) []DeliveryState {
907+
if len(ignored) == 0 {
908+
return states
909+
}
910+
skip := make(map[string]bool, len(ignored))
911+
for _, slug := range ignored {
912+
skip[slug] = true
913+
}
914+
kept := make([]DeliveryState, 0, len(states))
915+
for _, state := range states {
916+
if !skip[state.Feature] {
917+
kept = append(kept, state)
918+
}
919+
}
920+
return kept
921+
}
922+
923+
// IgnoreDelivery appends a feature slug to workflow.ignored_deliveries in the
924+
// repository's project.json, deduplicating and preserving all other config. It
925+
// is the bounded, provenance-safe write behind the ignore-delivery helper
926+
// subcommand: the config round-trips through LoadConfig -> GeneratedJSON so the
927+
// serialization contract and generator metadata are preserved. It returns
928+
// whether the slug was newly added.
929+
func IgnoreDelivery(repo, feature string) (bool, error) {
930+
feature = strings.TrimSpace(feature)
931+
if feature == "" {
932+
return false, fmt.Errorf("ignore-delivery requires a feature slug")
933+
}
934+
if !featureSlugPattern.MatchString(feature) {
935+
return false, fmt.Errorf("feature slug %q is not a valid Boatstack feature slug", feature)
936+
}
937+
resolved, err := ResolveRepository(repo)
938+
if err != nil {
939+
return false, err
940+
}
941+
configPath := filepath.Join(resolved, ".product-loop", "project.json")
942+
config, _, err := LoadConfig(configPath)
943+
if err != nil {
944+
return false, err
945+
}
946+
for _, existing := range config.Workflow.IgnoredDeliveries {
947+
if existing == feature {
948+
return false, nil
949+
}
950+
}
951+
config.Workflow.IgnoredDeliveries = append(config.Workflow.IgnoredDeliveries, feature)
952+
value, err := GeneratedJSON(config)
953+
if err != nil {
954+
return false, err
955+
}
956+
if err := atomicWriteMode(configPath, value, 0o644); err != nil {
957+
return false, err
958+
}
959+
return true, nil
960+
}

boatstack/delivery_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,50 @@ package boatstack
33
import (
44
"os"
55
"path/filepath"
6+
"reflect"
67
"strings"
78
"testing"
89
)
910

11+
func TestIgnoreDeliveryAppendsDedupsAndPreservesConfig(t *testing.T) {
12+
repo := nextTestRepo(t)
13+
14+
added, err := IgnoreDelivery(repo, "old-feature")
15+
if err != nil {
16+
t.Fatal(err)
17+
}
18+
if !added {
19+
t.Fatal("first ignore-delivery should report the slug as newly added")
20+
}
21+
22+
config, _, err := LoadConfig(filepath.Join(repo, ".product-loop", "project.json"))
23+
if err != nil {
24+
t.Fatal(err)
25+
}
26+
if !reflect.DeepEqual(config.Workflow.IgnoredDeliveries, []string{"old-feature"}) {
27+
t.Fatalf("ignored delivery not persisted: %+v", config.Workflow.IgnoredDeliveries)
28+
}
29+
// Other config must survive the round-trip.
30+
if config.Project.Name != "fixture" || config.Workflow.HumanPlanApproval != true {
31+
t.Fatalf("ignore-delivery clobbered unrelated config: %+v", config)
32+
}
33+
34+
added, err = IgnoreDelivery(repo, "old-feature")
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
if added {
39+
t.Fatal("repeat ignore-delivery should be a no-op")
40+
}
41+
config, _, err = LoadConfig(filepath.Join(repo, ".product-loop", "project.json"))
42+
if err != nil {
43+
t.Fatal(err)
44+
}
45+
if len(config.Workflow.IgnoredDeliveries) != 1 {
46+
t.Fatalf("ignore-delivery duplicated the slug: %+v", config.Workflow.IgnoredDeliveries)
47+
}
48+
}
49+
1050
func twoSlicePlan() map[string]any {
1151
plan := validPlan()
1252
plan["acceptance_criteria"] = []any{

0 commit comments

Comments
 (0)