Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/bf68921a54fdb3139401ce4a91241c799887e5b6/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/60f76062f72185efddad1c22f6c1fc088aff0005/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.

Expand Down
26 changes: 14 additions & 12 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"canonical_context": {
"characters": 77946,
"estimated_tokens": 19487,
"characters": 78586,
"estimated_tokens": 19647,
"estimator": "ceil(total characters / 4); compactness signal, not provider billing",
"files": [
"product-engineering-loop/references/workflow.md",
Expand All @@ -12,7 +12,7 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "b5ed7bc801e78adc910b1467cd0be364fe39c44a4c6e4547200debc1d9f74517",
"CONTRIBUTING.md": "2e8bb6baa538f3ea22f5d31d03511eac4ffea83842733ec2f2de731f66baae76",
"README.md": "6b7402c5cef5b3b9b739281d3d4d576cdc995796ff127fc6aefb97c5743e0bac",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
Expand Down Expand Up @@ -40,14 +40,14 @@
"boatstack/capture_test.go": "63fa1177738081f1e862364d7a4257f5e259f8e9c36276ba1775b8085b277105",
"boatstack/changelog.go": "6b06be7cd9738de29ba6e87aa2569f3b027a2e618b04524f5abd7abaa17945bf",
"boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7",
"boatstack/cmd/boatstack-helper/main.go": "6e9cb69ef962974f9893b1b4211df5e5fbf15f50207e958d6458b64dc042a683",
"boatstack/cmd/boatstack-helper/main.go": "ac174deb556f9a0b5e033154f2fc966715b3bff81d26ef97a23df2f901c56652",
"boatstack/cmd/boatstack-helper/main_test.go": "ff73003b6a5157202fa09ddf1129fb13c3d79702b2e05a8721ce5a11bf5ab779",
"boatstack/command.go": "4726ac515dedab4947be7eb48f88c6cb8b53d674124504b69f03e6396b080ee8",
"boatstack/command_test.go": "9f707abba3640add81c3e97ba7e72fedbf98f3394b1c060a9ca4b4a28e919968",
"boatstack/config_documentation_test.go": "0632366edc5e88145bb080083ea03c6515da07b0162ce404d63e51bb5bc0774e",
"boatstack/decision.go": "257ca328da6ae19ab252f10ee5d06bd7daf49dd8141d083ab1b32f106ea7a94c",
"boatstack/decision_test.go": "1a92ff832610f9559bd47ccac7fc1755a8b4f8261c35bc72a092830dff05f7c0",
"boatstack/delivery.go": "10098124cfb34d0c1d99885b7293ecaccb64c5b5cd80c08f1e2bd1ef43f8a756",
"boatstack/delivery.go": "4b4e870335b4b45c1fbfdc5a2daeb65b14c41671bc489f7e75189b9fc2ae6cb9",
"boatstack/delivery_boundary_conformance_test.go": "800cd722d8d2a696a0529e8343d3523e453bb052f0917c8a2cad2990296ac1b3",
"boatstack/delivery_reactivation_test.go": "573a2dba0034bc4290478414e3bdd8670b06a326128eb0295d77e748ecc8689e",
"boatstack/delivery_test.go": "45c48ff7581c911bcaf821c3e4241d4ae2a9bb4aa682485cc58b6ad8fe1c85bf",
Expand Down Expand Up @@ -89,14 +89,15 @@
"boatstack/provision.go": "4882d49681f99b11ba9d182ca13772131b7f9a11a6c2b560800654ca14f5111e",
"boatstack/provision_test.go": "214e9edb991a66d5bbb696a7c1b63876d2f799f2cab4e3f40785f4e8f1eac57b",
"boatstack/publication_ignored_repro_test.go": "b6f3aeb8ba22949ff9af7ac5afe8fb828385d9708d5d5893ef41f33a3de873e1",
"boatstack/recovery.go": "6939747f3725a6dd2de933d0248571d7f21a9ee017568306fe11f08fdbba413e",
"boatstack/published_slice_routing_test.go": "ea7e7351018bc13dcd31c4b96f50f8bc230e8a1dbf7806fba32a12ae58923e7e",
"boatstack/recovery.go": "7c06cdb52a31125cf3b944c304eca1df2273edc763242112527798bfb114874f",
"boatstack/recovery_test.go": "29490e7477ba602491330036a491289dd9117b99ff862f66dae421ba17e04c9f",
"boatstack/reexec.go": "fed55416479d7bd3e0c3637057ffe8eb58a032f93fc358f76df906ab7acc677b",
"boatstack/reexec_unix.go": "ff86157a9aa20c82a56fcd859b70669b7eacf4e0a9f61a4546ef33808437939e",
"boatstack/reexec_windows.go": "f5335c8c28cb4e89048b058b1c4d12f78644f99acb4f6167ff60e622dfb9e742",
"boatstack/references/artifacts.md": "5fa888ac519085d65cee1d04df5902761651bcf2d7af81711fa0f8ecd1fc0f59",
"boatstack/references/config-schema.md": "0170b90f1d0a592f58e255ffeff642fa037676042443f74a0f1b6e39be5dbbb8",
"boatstack/references/failure-moves.md": "04cf53609c355f93df20ae7650bab49183d612b8b04ef425ea564b808039192b",
"boatstack/references/failure-moves.md": "35ac99fdf19eac823313b684b4a8a92990fb42392dc1a94447621d538c637fc7",
"boatstack/references/host-hook-contracts.md": "d68ae1556e7b1e29e9ac7cb4db767809d510aabf0be52e60e44665ea7abb980e",
"boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441",
"boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae",
Expand All @@ -110,7 +111,7 @@
"boatstack/runtime_cache.go": "e40c8c43f410d781a7a4e7ebf68a1caa597ea9005190fd6cea02884f863e091e",
"boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85",
"boatstack/runtime_provenance_test.go": "1d52f1e6b0691cf4667729cc9b9f3c55c128f0aa3321f3a2843a9aa6fd0e73dc",
"boatstack/safety.go": "9e1dd0a40524304b6eb3e0ad9f24ebe12bd1a72794b77bd74b4f79e81992765c",
"boatstack/safety.go": "4f8f6e2dc2596ab28e3cb7f15c5634c6580d4306f039e2bacb85c7a584b5d255",
"boatstack/safety_test.go": "01f28bc3bfcb6bdd47b307e309e36bbc1921b6426ad0b777d81fe4131200c37e",
"boatstack/skill_frontmatter.go": "73364df463ce828c2d005aab55f72bb92f7a34d99cf3f53d4e0cd5a4da9dbd0e",
"boatstack/skill_frontmatter_test.go": "a3ec52e7df357a72265c95dd66db15d9c0effc7e5f90f14ce69c27792ce394eb",
Expand All @@ -134,10 +135,10 @@
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "df054f49d532c8b1b7d94184810d1b3b5bf18cdc30eb985b4b6d0639162e341a",
"docs/evidence-engineered-coding.md": "0eb1c59316939ebb951e28c9a508743be4d55a7ab49a15e80c237dbd1715232d",
"docs/evidence-engineered-coding.md": "8da580e5b910db5255944ba15aa23bea9356b9545e52430962abc265810f871c",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "1dd4f4e2e636cc5adfc2f79939629701e171087c3d5e558cf919548b9224adfd",
"docs/public-claims.json": "8d9a5258882cdc22df696000a44fdb1d0185c9eae853a3b31f29d49d75fe9cb8",
"docs/public-claims.json": "8fd1c004d69856c2426263f96ce5c9c25cda37ac3d43f8f39bffec0f0913908c",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -151,7 +152,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": "54138d8f25086643c31032262bfe9b6d2f7fec4eabd3fcbfd41c92923c283987",
"labs/diagram-json/plan.lock.json": "617523321b82841a59652e680bd163416507c84ddb727fc226ff5727175cef16",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand Down Expand Up @@ -233,12 +234,13 @@
"release-notes/2026-07-24-repair-state-recovery.md": "daaa12deb51a5f647178d6164ea5b4bcd29bf5482b77d90002429f69e0da5dd0",
"release-notes/2026-07-24-transactional-mutation-boundary.md": "38819a4811edbc99a9d8a77983aedbd0589bdbbf849da4991d3e21a1b319a65c",
"release-notes/2026-07-25-boatstack-banner.md": "28e83f294de606211cfdc91b2586aa834e004dee76d5c4bee08859986ae86b5b",
"release-notes/2026-07-25-published-slice-correction-routing.md": "129cdd62c80c8b93060726027d68ba3abdb0bca1a1ce9e64d6053271af3fd082",
"release-notes/2026-07-25-root-cause-operation.md": "5bf1f082e9123c5a7bcc8bc01b12e97b24b5ae15958577b4ff2a358994fca891"
},
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "bf68921a54fdb3139401ce4a91241c799887e5b6",
"commit": "60f76062f72185efddad1c22f6c1fc088aff0005",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
1 change: 1 addition & 0 deletions boatstack/cmd/boatstack-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ func recordChangeCommand(arguments []string) int {
flags.StringVar(&options.Actual, "actual", "", "observed behavior")
flags.StringVar(&options.Evidence, "evidence", "", "bounded evidence or reproduction reference")
flags.StringVar(&options.Classification, "classification", "", "implementation_repair, verification_repair, review_repair, requirement_amendment, needs_clarification, or plan_invalid")
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)")
if err := flags.Parse(arguments); err != nil {
return 2
}
Expand Down
121 changes: 116 additions & 5 deletions boatstack/delivery.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ type ChangeObservationOptions struct {
Actual string
Evidence string
Classification string
// SliceID optionally targets a specific addressable slice — the active slice or
// a published-but-open earlier slice. Empty resolves to the correction's branch
// and then the active slice, preserving the ordinary repair path.
SliceID string
}

type ChangeObservation struct {
Expand Down Expand Up @@ -546,14 +550,52 @@ func RecordChangeObservation(options ChangeObservationOptions) (ChangeObservatio
return ChangeObservation{}, DeliveryState{}, fmt.Errorf("change observation requires the user message and source stage")
}
published := state.ActiveIndex >= len(state.Slices)

// Resolve which addressable slice this correction targets before mutating any
// state. An explicit --slice, or a correction pushed on a published-but-open
// earlier slice's branch, must bind to that slice — historically every non-
// published correction was bound to the active slice, which corrupted the wrong
// slice when the real target was an already-published slice inside the delivery.
targetIndex := -1
var targetSlice DeliverySlice
publishedOpen := false
if !published {
sliceHint := strings.TrimSpace(options.SliceID)
if sliceHint == "" {
if branch, _ := gitCommand(repo, "branch", "--show-current"); strings.TrimSpace(branch) != "" {
if idx, _, ok := resolveAddressableSliceByBranch(state, strings.TrimSpace(branch)); ok && idx < state.ActiveIndex {
sliceHint = state.Slices[idx].ID
}
}
}
idx, slice, resolveErr := resolveAddressableSlice(state, sliceHint)
if resolveErr != nil {
return ChangeObservation{}, DeliveryState{}, resolveErr
}
targetIndex, targetSlice = idx, slice
publishedOpen = idx < state.ActiveIndex
}

// A published-but-open slice may only be corrected in place through a gate
// repair; a requirement or plan change to it is a new decision that requires an
// independently approved corrective child, not an in-place re-gate.
if publishedOpen {
switch classification {
case "implementation_repair", "verification_repair", "review_repair":
default:
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)
}
}

// The delivery-level repair budget governs only the active slice's build loop.
if !published && !publishedOpen {
if state.RepairAttempt >= 3 {
return ChangeObservation{}, DeliveryState{}, fmt.Errorf("persistent repair budget exhausted after %d attempts; preserve current state and require a reviewed recovery decision", state.RepairAttempt)
}
state.RepairAttempt++
}
id := fmt.Sprintf("CHG-%03d", state.RepairAttempt)
if published {
if published || publishedOpen {
id = nextChangeObservationID(repo, options.Feature, state.RepairAttempt+1)
}
observation := ChangeObservation{
Expand All @@ -562,10 +604,8 @@ func RecordChangeObservation(options ChangeObservationOptions) (ChangeObservatio
Message: strings.TrimSpace(options.Message), Classification: classification, ResumeStage: resume,
RecordedAt: time.Now().UTC().Truncate(time.Second).Format(time.RFC3339),
}
if !published {
observation.SliceID = state.Slices[state.ActiveIndex].ID
observation.Outcome = "RESUME_ACTIVE"
} else {
switch {
case published:
if len(state.Slices) > 0 {
observation.SliceID = state.Slices[len(state.Slices)-1].ID
}
Expand All @@ -576,13 +616,50 @@ func RecordChangeObservation(options ChangeObservationOptions) (ChangeObservatio
observation.Outcome = "CORRECTIVE_CHILD_REQUIRED"
observation.ParentDelivery = state.Feature
observation.SuggestedFeatureID = suggestedCorrectionFeature(states, state.Feature)
case publishedOpen:
observation.SliceID = targetSlice.ID
observation.Outcome = "RESUME_PUBLISHED_SLICE"
default:
observation.SliceID = targetSlice.ID
observation.Outcome = "RESUME_ACTIVE"
}
if err := appendChangeObservation(repo, observation); err != nil {
return ChangeObservation{}, DeliveryState{}, err
}
if published {
return observation, state, nil
}
if publishedOpen {
// In-place re-gate of a published-but-open slice: archive its stale gate
// receipts and reset its Status so test/review re-run against the fix, while
// its PRState="OPEN" preserves the published identity that publish-pr --action
// update targets. The active slice's build loop (RepairAttempt, Mode,
// ResumeStage, ActiveObservationID) is deliberately left untouched — the
// correction belongs to the published slice, not the active one.
slice := &state.Slices[targetIndex]
gates := []string{"review"}
if resume == "BUILD" || resume == "TEST_GATE" {
gates = []string{"test", "review"}
}
for _, gate := range gates {
archived, archiveErr := archiveDeliveryReceipt(repo, options.Feature, slice.ID, gate, id)
if archiveErr != nil {
return ChangeObservation{}, DeliveryState{}, archiveErr
}
if archived != "" {
state.SupersededReceipts = append(state.SupersededReceipts, archived)
}
}
if resume == "REVIEW_GATE" {
slice.Status = "TEST_PASSED"
} else {
slice.Status = "BUILD"
}
if err := saveDeliveryState(repo, state); err != nil {
return ChangeObservation{}, DeliveryState{}, err
}
return observation, state, nil
}
state.ActiveObservationID = id
state.ResumeStage = resume
if classification == "needs_clarification" || classification == "requirement_amendment" {
Expand Down Expand Up @@ -688,6 +765,40 @@ func resolveAddressableSlice(state DeliveryState, sliceID string) (int, Delivery
return -1, DeliverySlice{}, fmt.Errorf("delivery slice %s does not exist", sliceID)
}

// resolveAddressableSliceByBranch selects the addressable slice a correction on
// the given branch may act on. It applies the same addressable set as
// resolveAddressableSlice — {active slice} ∪ {PUBLISHED slices whose PR is not
// terminal} — but keys off the slice's recorded head branch rather than its id.
//
// Advisors (recovery routing, safety findings, change recording) start from the
// current git branch, not a slice id, and historically resolved corrections
// against state.ActiveIndex alone. That left the advisor layer blind to a
// published-but-open earlier slice that the actuator layer (resolveAddressableSlice)
// could still correct in place, so a correction pushed on the published slice's
// branch was mis-routed to the active slice. Routing every advisor through this one
// resolver keeps "which slice this correction may act on" defined in a single place,
// so the advisor layer cannot drift from the actuator layer again. ok is false when
// no addressable slice owns the branch (including a slice whose PR is terminal — the
// caller then falls through to its corrective-child path unchanged).
func resolveAddressableSliceByBranch(state DeliveryState, branch string) (int, DeliverySlice, bool) {
branch = strings.TrimSpace(branch)
if branch == "" {
return -1, DeliverySlice{}, false
}
for i, s := range state.Slices {
if strings.TrimSpace(s.HeadBranch) != branch {
continue
}
if i == state.ActiveIndex {
return i, s, true
}
if i < state.ActiveIndex && strings.TrimSpace(s.PRState) != "" && !isTerminalPRState(s.PRState) {
return i, s, true
}
}
return -1, DeliverySlice{}, false
}

func checkDeliveryPlanLock(repo, feature string, state DeliveryState) error {
lockPath := filepath.Join(repo, ".product-loop", "features", feature, "plan.lock.json")
lockHash, err := SHA256File(lockPath)
Expand Down
Loading
Loading