Skip to content

Commit 58a0d80

Browse files
Sync Boatstack from Intelligence Flow Labs @ 60f76062f721 (#99)
Co-authored-by: operator-stack-publisher[bot] <operator-stack-publisher[bot]@users.noreply.github.com>
1 parent 091202d commit 58a0d80

12 files changed

Lines changed: 363 additions & 35 deletions

File tree

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/bf68921a54fdb3139401ce4a91241c799887e5b6/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/60f76062f72185efddad1c22f6c1fc088aff0005/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: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"canonical_context": {
3-
"characters": 77946,
4-
"estimated_tokens": 19487,
3+
"characters": 78586,
4+
"estimated_tokens": 19647,
55
"estimator": "ceil(total characters / 4); compactness signal, not provider billing",
66
"files": [
77
"product-engineering-loop/references/workflow.md",
@@ -12,7 +12,7 @@
1212
},
1313
"files": {
1414
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
15-
"CONTRIBUTING.md": "b5ed7bc801e78adc910b1467cd0be364fe39c44a4c6e4547200debc1d9f74517",
15+
"CONTRIBUTING.md": "2e8bb6baa538f3ea22f5d31d03511eac4ffea83842733ec2f2de731f66baae76",
1616
"README.md": "6b7402c5cef5b3b9b739281d3d4d576cdc995796ff127fc6aefb97c5743e0bac",
1717
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
1818
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
@@ -40,14 +40,14 @@
4040
"boatstack/capture_test.go": "63fa1177738081f1e862364d7a4257f5e259f8e9c36276ba1775b8085b277105",
4141
"boatstack/changelog.go": "6b06be7cd9738de29ba6e87aa2569f3b027a2e618b04524f5abd7abaa17945bf",
4242
"boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7",
43-
"boatstack/cmd/boatstack-helper/main.go": "6e9cb69ef962974f9893b1b4211df5e5fbf15f50207e958d6458b64dc042a683",
43+
"boatstack/cmd/boatstack-helper/main.go": "ac174deb556f9a0b5e033154f2fc966715b3bff81d26ef97a23df2f901c56652",
4444
"boatstack/cmd/boatstack-helper/main_test.go": "ff73003b6a5157202fa09ddf1129fb13c3d79702b2e05a8721ce5a11bf5ab779",
4545
"boatstack/command.go": "4726ac515dedab4947be7eb48f88c6cb8b53d674124504b69f03e6396b080ee8",
4646
"boatstack/command_test.go": "9f707abba3640add81c3e97ba7e72fedbf98f3394b1c060a9ca4b4a28e919968",
4747
"boatstack/config_documentation_test.go": "0632366edc5e88145bb080083ea03c6515da07b0162ce404d63e51bb5bc0774e",
4848
"boatstack/decision.go": "257ca328da6ae19ab252f10ee5d06bd7daf49dd8141d083ab1b32f106ea7a94c",
4949
"boatstack/decision_test.go": "1a92ff832610f9559bd47ccac7fc1755a8b4f8261c35bc72a092830dff05f7c0",
50-
"boatstack/delivery.go": "10098124cfb34d0c1d99885b7293ecaccb64c5b5cd80c08f1e2bd1ef43f8a756",
50+
"boatstack/delivery.go": "4b4e870335b4b45c1fbfdc5a2daeb65b14c41671bc489f7e75189b9fc2ae6cb9",
5151
"boatstack/delivery_boundary_conformance_test.go": "800cd722d8d2a696a0529e8343d3523e453bb052f0917c8a2cad2990296ac1b3",
5252
"boatstack/delivery_reactivation_test.go": "573a2dba0034bc4290478414e3bdd8670b06a326128eb0295d77e748ecc8689e",
5353
"boatstack/delivery_test.go": "45c48ff7581c911bcaf821c3e4241d4ae2a9bb4aa682485cc58b6ad8fe1c85bf",
@@ -89,14 +89,15 @@
8989
"boatstack/provision.go": "4882d49681f99b11ba9d182ca13772131b7f9a11a6c2b560800654ca14f5111e",
9090
"boatstack/provision_test.go": "214e9edb991a66d5bbb696a7c1b63876d2f799f2cab4e3f40785f4e8f1eac57b",
9191
"boatstack/publication_ignored_repro_test.go": "b6f3aeb8ba22949ff9af7ac5afe8fb828385d9708d5d5893ef41f33a3de873e1",
92-
"boatstack/recovery.go": "6939747f3725a6dd2de933d0248571d7f21a9ee017568306fe11f08fdbba413e",
92+
"boatstack/published_slice_routing_test.go": "ea7e7351018bc13dcd31c4b96f50f8bc230e8a1dbf7806fba32a12ae58923e7e",
93+
"boatstack/recovery.go": "7c06cdb52a31125cf3b944c304eca1df2273edc763242112527798bfb114874f",
9394
"boatstack/recovery_test.go": "29490e7477ba602491330036a491289dd9117b99ff862f66dae421ba17e04c9f",
9495
"boatstack/reexec.go": "fed55416479d7bd3e0c3637057ffe8eb58a032f93fc358f76df906ab7acc677b",
9596
"boatstack/reexec_unix.go": "ff86157a9aa20c82a56fcd859b70669b7eacf4e0a9f61a4546ef33808437939e",
9697
"boatstack/reexec_windows.go": "f5335c8c28cb4e89048b058b1c4d12f78644f99acb4f6167ff60e622dfb9e742",
9798
"boatstack/references/artifacts.md": "5fa888ac519085d65cee1d04df5902761651bcf2d7af81711fa0f8ecd1fc0f59",
9899
"boatstack/references/config-schema.md": "0170b90f1d0a592f58e255ffeff642fa037676042443f74a0f1b6e39be5dbbb8",
99-
"boatstack/references/failure-moves.md": "04cf53609c355f93df20ae7650bab49183d612b8b04ef425ea564b808039192b",
100+
"boatstack/references/failure-moves.md": "35ac99fdf19eac823313b684b4a8a92990fb42392dc1a94447621d538c637fc7",
100101
"boatstack/references/host-hook-contracts.md": "d68ae1556e7b1e29e9ac7cb4db767809d510aabf0be52e60e44665ea7abb980e",
101102
"boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441",
102103
"boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae",
@@ -110,7 +111,7 @@
110111
"boatstack/runtime_cache.go": "e40c8c43f410d781a7a4e7ebf68a1caa597ea9005190fd6cea02884f863e091e",
111112
"boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85",
112113
"boatstack/runtime_provenance_test.go": "1d52f1e6b0691cf4667729cc9b9f3c55c128f0aa3321f3a2843a9aa6fd0e73dc",
113-
"boatstack/safety.go": "9e1dd0a40524304b6eb3e0ad9f24ebe12bd1a72794b77bd74b4f79e81992765c",
114+
"boatstack/safety.go": "4f8f6e2dc2596ab28e3cb7f15c5634c6580d4306f039e2bacb85c7a584b5d255",
114115
"boatstack/safety_test.go": "01f28bc3bfcb6bdd47b307e309e36bbc1921b6426ad0b777d81fe4131200c37e",
115116
"boatstack/skill_frontmatter.go": "73364df463ce828c2d005aab55f72bb92f7a34d99cf3f53d4e0cd5a4da9dbd0e",
116117
"boatstack/skill_frontmatter_test.go": "a3ec52e7df357a72265c95dd66db15d9c0effc7e5f90f14ce69c27792ce394eb",
@@ -134,10 +135,10 @@
134135
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
135136
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
136137
"docs/configuration.md": "df054f49d532c8b1b7d94184810d1b3b5bf18cdc30eb985b4b6d0639162e341a",
137-
"docs/evidence-engineered-coding.md": "0eb1c59316939ebb951e28c9a508743be4d55a7ab49a15e80c237dbd1715232d",
138+
"docs/evidence-engineered-coding.md": "8da580e5b910db5255944ba15aa23bea9356b9545e52430962abc265810f871c",
138139
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
139140
"docs/getting-started.md": "1dd4f4e2e636cc5adfc2f79939629701e171087c3d5e558cf919548b9224adfd",
140-
"docs/public-claims.json": "8d9a5258882cdc22df696000a44fdb1d0185c9eae853a3b31f29d49d75fe9cb8",
141+
"docs/public-claims.json": "8fd1c004d69856c2426263f96ce5c9c25cda37ac3d43f8f39bffec0f0913908c",
141142
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
142143
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
143144
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
@@ -151,7 +152,7 @@
151152
"labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d",
152153
"labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71",
153154
"labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39",
154-
"labs/diagram-json/plan.lock.json": "54138d8f25086643c31032262bfe9b6d2f7fec4eabd3fcbfd41c92923c283987",
155+
"labs/diagram-json/plan.lock.json": "617523321b82841a59652e680bd163416507c84ddb727fc226ff5727175cef16",
155156
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
156157
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
157158
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
@@ -233,12 +234,13 @@
233234
"release-notes/2026-07-24-repair-state-recovery.md": "daaa12deb51a5f647178d6164ea5b4bcd29bf5482b77d90002429f69e0da5dd0",
234235
"release-notes/2026-07-24-transactional-mutation-boundary.md": "38819a4811edbc99a9d8a77983aedbd0589bdbbf849da4991d3e21a1b319a65c",
235236
"release-notes/2026-07-25-boatstack-banner.md": "28e83f294de606211cfdc91b2586aa834e004dee76d5c4bee08859986ae86b5b",
237+
"release-notes/2026-07-25-published-slice-correction-routing.md": "129cdd62c80c8b93060726027d68ba3abdb0bca1a1ce9e64d6053271af3fd082",
236238
"release-notes/2026-07-25-root-cause-operation.md": "5bf1f082e9123c5a7bcc8bc01b12e97b24b5ae15958577b4ff2a358994fca891"
237239
},
238240
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
239241
"schema_version": 1,
240242
"source": {
241-
"commit": "bf68921a54fdb3139401ce4a91241c799887e5b6",
243+
"commit": "60f76062f72185efddad1c22f6c1fc088aff0005",
242244
"path": "labs/12-product-engineering-loop",
243245
"repository": "operatorstack/intelligence-flow"
244246
}

boatstack/cmd/boatstack-helper/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,7 @@ func recordChangeCommand(arguments []string) int {
757757
flags.StringVar(&options.Actual, "actual", "", "observed behavior")
758758
flags.StringVar(&options.Evidence, "evidence", "", "bounded evidence or reproduction reference")
759759
flags.StringVar(&options.Classification, "classification", "", "implementation_repair, verification_repair, review_repair, requirement_amendment, needs_clarification, or plan_invalid")
760+
flags.StringVar(&options.SliceID, "slice", "", "delivery slice id the correction targets; redirects to the named active or published-open slice (default: the correction's branch, then the active slice)")
760761
if err := flags.Parse(arguments); err != nil {
761762
return 2
762763
}

boatstack/delivery.go

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ type ChangeObservationOptions struct {
113113
Actual string
114114
Evidence string
115115
Classification string
116+
// SliceID optionally targets a specific addressable slice — the active slice or
117+
// a published-but-open earlier slice. Empty resolves to the correction's branch
118+
// and then the active slice, preserving the ordinary repair path.
119+
SliceID string
116120
}
117121

118122
type ChangeObservation struct {
@@ -546,14 +550,52 @@ func RecordChangeObservation(options ChangeObservationOptions) (ChangeObservatio
546550
return ChangeObservation{}, DeliveryState{}, fmt.Errorf("change observation requires the user message and source stage")
547551
}
548552
published := state.ActiveIndex >= len(state.Slices)
553+
554+
// Resolve which addressable slice this correction targets before mutating any
555+
// state. An explicit --slice, or a correction pushed on a published-but-open
556+
// earlier slice's branch, must bind to that slice — historically every non-
557+
// published correction was bound to the active slice, which corrupted the wrong
558+
// slice when the real target was an already-published slice inside the delivery.
559+
targetIndex := -1
560+
var targetSlice DeliverySlice
561+
publishedOpen := false
549562
if !published {
563+
sliceHint := strings.TrimSpace(options.SliceID)
564+
if sliceHint == "" {
565+
if branch, _ := gitCommand(repo, "branch", "--show-current"); strings.TrimSpace(branch) != "" {
566+
if idx, _, ok := resolveAddressableSliceByBranch(state, strings.TrimSpace(branch)); ok && idx < state.ActiveIndex {
567+
sliceHint = state.Slices[idx].ID
568+
}
569+
}
570+
}
571+
idx, slice, resolveErr := resolveAddressableSlice(state, sliceHint)
572+
if resolveErr != nil {
573+
return ChangeObservation{}, DeliveryState{}, resolveErr
574+
}
575+
targetIndex, targetSlice = idx, slice
576+
publishedOpen = idx < state.ActiveIndex
577+
}
578+
579+
// A published-but-open slice may only be corrected in place through a gate
580+
// repair; a requirement or plan change to it is a new decision that requires an
581+
// independently approved corrective child, not an in-place re-gate.
582+
if publishedOpen {
583+
switch classification {
584+
case "implementation_repair", "verification_repair", "review_repair":
585+
default:
586+
return ChangeObservation{}, DeliveryState{}, fmt.Errorf("delivery slice %s is published with an open pull request; a %s change must be handled by a corrective child delivery, not an in-place re-gate", targetSlice.ID, classification)
587+
}
588+
}
589+
590+
// The delivery-level repair budget governs only the active slice's build loop.
591+
if !published && !publishedOpen {
550592
if state.RepairAttempt >= 3 {
551593
return ChangeObservation{}, DeliveryState{}, fmt.Errorf("persistent repair budget exhausted after %d attempts; preserve current state and require a reviewed recovery decision", state.RepairAttempt)
552594
}
553595
state.RepairAttempt++
554596
}
555597
id := fmt.Sprintf("CHG-%03d", state.RepairAttempt)
556-
if published {
598+
if published || publishedOpen {
557599
id = nextChangeObservationID(repo, options.Feature, state.RepairAttempt+1)
558600
}
559601
observation := ChangeObservation{
@@ -562,10 +604,8 @@ func RecordChangeObservation(options ChangeObservationOptions) (ChangeObservatio
562604
Message: strings.TrimSpace(options.Message), Classification: classification, ResumeStage: resume,
563605
RecordedAt: time.Now().UTC().Truncate(time.Second).Format(time.RFC3339),
564606
}
565-
if !published {
566-
observation.SliceID = state.Slices[state.ActiveIndex].ID
567-
observation.Outcome = "RESUME_ACTIVE"
568-
} else {
607+
switch {
608+
case published:
569609
if len(state.Slices) > 0 {
570610
observation.SliceID = state.Slices[len(state.Slices)-1].ID
571611
}
@@ -576,13 +616,50 @@ func RecordChangeObservation(options ChangeObservationOptions) (ChangeObservatio
576616
observation.Outcome = "CORRECTIVE_CHILD_REQUIRED"
577617
observation.ParentDelivery = state.Feature
578618
observation.SuggestedFeatureID = suggestedCorrectionFeature(states, state.Feature)
619+
case publishedOpen:
620+
observation.SliceID = targetSlice.ID
621+
observation.Outcome = "RESUME_PUBLISHED_SLICE"
622+
default:
623+
observation.SliceID = targetSlice.ID
624+
observation.Outcome = "RESUME_ACTIVE"
579625
}
580626
if err := appendChangeObservation(repo, observation); err != nil {
581627
return ChangeObservation{}, DeliveryState{}, err
582628
}
583629
if published {
584630
return observation, state, nil
585631
}
632+
if publishedOpen {
633+
// In-place re-gate of a published-but-open slice: archive its stale gate
634+
// receipts and reset its Status so test/review re-run against the fix, while
635+
// its PRState="OPEN" preserves the published identity that publish-pr --action
636+
// update targets. The active slice's build loop (RepairAttempt, Mode,
637+
// ResumeStage, ActiveObservationID) is deliberately left untouched — the
638+
// correction belongs to the published slice, not the active one.
639+
slice := &state.Slices[targetIndex]
640+
gates := []string{"review"}
641+
if resume == "BUILD" || resume == "TEST_GATE" {
642+
gates = []string{"test", "review"}
643+
}
644+
for _, gate := range gates {
645+
archived, archiveErr := archiveDeliveryReceipt(repo, options.Feature, slice.ID, gate, id)
646+
if archiveErr != nil {
647+
return ChangeObservation{}, DeliveryState{}, archiveErr
648+
}
649+
if archived != "" {
650+
state.SupersededReceipts = append(state.SupersededReceipts, archived)
651+
}
652+
}
653+
if resume == "REVIEW_GATE" {
654+
slice.Status = "TEST_PASSED"
655+
} else {
656+
slice.Status = "BUILD"
657+
}
658+
if err := saveDeliveryState(repo, state); err != nil {
659+
return ChangeObservation{}, DeliveryState{}, err
660+
}
661+
return observation, state, nil
662+
}
586663
state.ActiveObservationID = id
587664
state.ResumeStage = resume
588665
if classification == "needs_clarification" || classification == "requirement_amendment" {
@@ -688,6 +765,40 @@ func resolveAddressableSlice(state DeliveryState, sliceID string) (int, Delivery
688765
return -1, DeliverySlice{}, fmt.Errorf("delivery slice %s does not exist", sliceID)
689766
}
690767

768+
// resolveAddressableSliceByBranch selects the addressable slice a correction on
769+
// the given branch may act on. It applies the same addressable set as
770+
// resolveAddressableSlice — {active slice} ∪ {PUBLISHED slices whose PR is not
771+
// terminal} — but keys off the slice's recorded head branch rather than its id.
772+
//
773+
// Advisors (recovery routing, safety findings, change recording) start from the
774+
// current git branch, not a slice id, and historically resolved corrections
775+
// against state.ActiveIndex alone. That left the advisor layer blind to a
776+
// published-but-open earlier slice that the actuator layer (resolveAddressableSlice)
777+
// could still correct in place, so a correction pushed on the published slice's
778+
// branch was mis-routed to the active slice. Routing every advisor through this one
779+
// resolver keeps "which slice this correction may act on" defined in a single place,
780+
// so the advisor layer cannot drift from the actuator layer again. ok is false when
781+
// no addressable slice owns the branch (including a slice whose PR is terminal — the
782+
// caller then falls through to its corrective-child path unchanged).
783+
func resolveAddressableSliceByBranch(state DeliveryState, branch string) (int, DeliverySlice, bool) {
784+
branch = strings.TrimSpace(branch)
785+
if branch == "" {
786+
return -1, DeliverySlice{}, false
787+
}
788+
for i, s := range state.Slices {
789+
if strings.TrimSpace(s.HeadBranch) != branch {
790+
continue
791+
}
792+
if i == state.ActiveIndex {
793+
return i, s, true
794+
}
795+
if i < state.ActiveIndex && strings.TrimSpace(s.PRState) != "" && !isTerminalPRState(s.PRState) {
796+
return i, s, true
797+
}
798+
}
799+
return -1, DeliverySlice{}, false
800+
}
801+
691802
func checkDeliveryPlanLock(repo, feature string, state DeliveryState) error {
692803
lockPath := filepath.Join(repo, ".product-loop", "features", feature, "plan.lock.json")
693804
lockHash, err := SHA256File(lockPath)

0 commit comments

Comments
 (0)