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/01dec93295af21787a594de9479acc16e0f85bba/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/f740356bfc30b59038162ed3c7ca849f77c76e7f/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
25 changes: 13 additions & 12 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "01ec94c6a5b13837bcfa11b05bb1a9be58bd643fb144c1d4f17e3b6287e88c92",
"CONTRIBUTING.md": "9d31155e08bf2ec29b66a839c0a320e1e30f2c1f69448b89622b74ade3b0c5ed",
"README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
Expand Down Expand Up @@ -53,7 +53,7 @@
"boatstack/context.go": "02510af176d2d040c0080086f06d1235e76a1d47fef5176f96f740ad18d27660",
"boatstack/decision.go": "257ca328da6ae19ab252f10ee5d06bd7daf49dd8141d083ab1b32f106ea7a94c",
"boatstack/decision_test.go": "1a92ff832610f9559bd47ccac7fc1755a8b4f8261c35bc72a092830dff05f7c0",
"boatstack/delivery.go": "8a4dd6b7dd553e9544879b0489ef51231f2c16ae49dfb98b526bb07fec2b6e96",
"boatstack/delivery.go": "1cbc917eaa7df569f09c71352db157dff743827d5310dccb63acb7be72ccf9d5",
"boatstack/delivery_boundary_conformance_test.go": "c374eddf49b4597db78c0621f65f87199de9a26f1872ed88d9d790edf21fe3f2",
"boatstack/delivery_migrate.go": "7566e49f9c1838d4d563866e941c7aacd61ac918c9e886222282398d287ca780",
"boatstack/delivery_migrate_conformance_test.go": "b8ba53681e1d0361ac62b06586c62b7763d55a65b5427976b5289e1fb1503bdc",
Expand Down Expand Up @@ -120,9 +120,9 @@
"boatstack/mutation_test.go": "68d5049c7f96c1ac558e4c781151f67e8deee2f8d6b9bf293b90d44e769ef7c6",
"boatstack/mutation_undo.go": "697d11b600a276ddbcabe6a9f8040d4f7283e017a0e8fd689ef53a274638946c",
"boatstack/mutation_undo_test.go": "39540e717e3f2136bf975594043a3db9072b28ebe61c6cb0b982cea5e8b1e14e",
"boatstack/next.go": "47e01558a04922a9743ac63ee934945906e17efde54ba3b163721d5f8fbc71e8",
"boatstack/next.go": "c133dbf907dc86ca5aacec154f6e4a63e7aa9f5f0ebdd76e1803375b5700675a",
"boatstack/next_banner_test.go": "c431a6987ed1e479442fc9f5db4371632880b92aa790fa9dd0f5285293352c41",
"boatstack/next_test.go": "d442d22023831ba39fcfbbf73f1a2da4170a83fc2aab5ce1b44f10cf9d88e173",
"boatstack/next_test.go": "6b5ec46ecf1a197d7644846cecbb6d99873a06b7c4e5562772b5016fa0a4cb11",
"boatstack/operation.go": "073113e1e7b6349417e70b704bd1a342b460604cbd97a7fab06b1a6494604112",
"boatstack/operation_test.go": "59d3dc37319aa4d334c0cacbe886e2f757842e6a28dee8781448e528fecbde11",
"boatstack/paths.go": "bc14901f9497bfa87f64eab80ff30cc7c3a31781e3fdb60826df2dc21eb2253b",
Expand All @@ -139,7 +139,7 @@
"boatstack/provision_test.go": "214e9edb991a66d5bbb696a7c1b63876d2f799f2cab4e3f40785f4e8f1eac57b",
"boatstack/publication_ignored_repro_test.go": "b6f3aeb8ba22949ff9af7ac5afe8fb828385d9708d5d5893ef41f33a3de873e1",
"boatstack/published_slice_routing_test.go": "ea7e7351018bc13dcd31c4b96f50f8bc230e8a1dbf7806fba32a12ae58923e7e",
"boatstack/recovery.go": "43a87bf4453f5533d1e3ed97f63bc3f8f1cbe95dee27e3480a63c1b6f72a8870",
"boatstack/recovery.go": "8e35cf7f0d73ec9708e00a5a9bfd5f30ec832537cb0bffc254581bb6b8ae33ea",
"boatstack/recovery_test.go": "29490e7477ba602491330036a491289dd9117b99ff862f66dae421ba17e04c9f",
"boatstack/reexec.go": "fed55416479d7bd3e0c3637057ffe8eb58a032f93fc358f76df906ab7acc677b",
"boatstack/reexec_unix.go": "ff86157a9aa20c82a56fcd859b70669b7eacf4e0a9f61a4546ef33808437939e",
Expand All @@ -160,12 +160,12 @@
"boatstack/runtime_cache.go": "e026ffc1906f7e1e98b768bae63e6658164d2826c07169c9121ce0f23c73faf8",
"boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85",
"boatstack/runtime_provenance_test.go": "1d52f1e6b0691cf4667729cc9b9f3c55c128f0aa3321f3a2843a9aa6fd0e73dc",
"boatstack/safety.go": "3f02b6be7d0a209da5afb2d23a5e5f1cb1c2578f1c4b430cea1d5a30a96e06ad",
"boatstack/safety.go": "56055f93cd9fe0f308b244111f2282d191c3ed522731047a194b06f21df64247",
"boatstack/safety_test.go": "02260654d0b93ad48585c40391b310810876a6abcafc3d6c074f1f4e4e633f76",
"boatstack/safety_update_publisher_test.go": "ed3f8187036623694dfe7c395cdae00fdae14609bab6124d1fdfc6fe73fa2196",
"boatstack/skill_frontmatter.go": "73364df463ce828c2d005aab55f72bb92f7a34d99cf3f53d4e0cd5a4da9dbd0e",
"boatstack/skill_frontmatter_test.go": "a3ec52e7df357a72265c95dd66db15d9c0effc7e5f90f14ce69c27792ce394eb",
"boatstack/supervisory_control_test.go": "af64106118ab31061e3f7335a2e981a4c831db3483962b6bc54e7e37dc4b7b45",
"boatstack/supervisory_control_test.go": "c7ea4bcd678e8ec211dac772c834981c4e21762914be2770a5e181bc24605e06",
"boatstack/testdata/reviewer-pr-body.md": "4c64e3788e5d61a377aeb0f797f7fc8d2316ab6e49572d15636eea7ba9e34ac4",
"boatstack/testdata/safety/safe_apply.py.txt": "c9ec7fb932cf21b6aa8df597c4d4c54d6ec65e796240e49118d699f583383975",
"boatstack/testdata/safety/unsafe_apply.py.txt": "42db1751865cc15c4dd69a03146b5deca8f21f916d258e433b27bbef5f884ab1",
Expand All @@ -187,10 +187,10 @@
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "060775c73431f28bd16066bdf9e0f89034d2855c7ca0f5544f660d24b91211d0",
"docs/evidence-engineered-coding.md": "36de4b7c7f2bbff5e250a3613e36a7e756b350f4bf44cf9f55feb944df746b3b",
"docs/evidence-engineered-coding.md": "c3301e5ef81642029935970eb4270c3667eeeacba3d7994d98407f65409dcac4",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8",
"docs/public-claims.json": "05bce9b3fab43563ca89db5e69b685bb2ee75b510cc7cc4eabfcac7754035209",
"docs/public-claims.json": "6c631d91ce3579a4a4fcf09a0d5c902a102de62b9793136e18e94032f67f73aa",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -204,7 +204,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": "d537e3b15bfa7312f12892f2a1c59fd34078fec2dea37c28362785c3fd17d1c3",
"labs/diagram-json/plan.lock.json": "e059bbc4a5ab29c1dc16a3ae00a1486b57a245e767e2ebaef4cd572002e959a7",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand Down Expand Up @@ -317,12 +317,13 @@
"release-notes/2026-07-26-guard-etxtbsy-retry.md": "4238591804be62f8b9a76dd5cda18923af60ef67932d40d70cab0d815124bcaf",
"release-notes/2026-07-26-guard-hydrate-double-check.md": "83a5591aba6bf30c9f4008ba8d26bf1994ef3fd61145f46fcdd1678912b9990b",
"release-notes/2026-07-26-hidden-jflow-design-note.md": "f60ed9dbbfb46a172ac9d33dd758a3166f820007b1673029f29f0fbefa0e5c0a",
"release-notes/2026-07-26-workspace-reap.md": "e691d6a1c232cf218157880655413005fcb2c4f3113ededffdb80899a5054bb8"
"release-notes/2026-07-26-workspace-reap.md": "e691d6a1c232cf218157880655413005fcb2c4f3113ededffdb80899a5054bb8",
"release-notes/2026-07-27-coreachable-recovery.md": "6ffc6b0e9a7d46c0f99a64112813c33d19571c73d02e98ac5573924f1663fd54"
},
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "01dec93295af21787a594de9479acc16e0f85bba",
"commit": "f740356bfc30b59038162ed3c7ca849f77c76e7f",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
58 changes: 58 additions & 0 deletions boatstack/delivery.go
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,55 @@ type DiscardDeliveryResult struct {
// present without force (fail closed).
// Release condition: the named delivery exists and either bears no published
// authority or force is set.
//
// discardOrphanFeatureArtifacts archives an orphaned product-loop feature
// directory — one carrying a pr.md but no plan.lock.json, with no managed delivery
// state — reversibly to a dotted .discarded sibling (which the slug pattern skips,
// so it is never re-scanned as live). It is the accepting side of the Coreachability
// contract for the orphan cause: ResolveNext prescribes discard-delivery for an
// orphan, so discard-delivery must clear it. It refuses a dir carrying a
// plan.lock.json (a registered, live feature) so it never touches active work.
func discardOrphanFeatureArtifacts(repo, feature string) (DiscardDeliveryResult, bool, error) {
dir := filepath.Join(repo, ".product-loop", "features", feature)
info, statErr := os.Stat(dir)
if os.IsNotExist(statErr) {
return DiscardDeliveryResult{}, false, nil
}
if statErr != nil {
return DiscardDeliveryResult{}, false, statErr
}
if !info.IsDir() {
return DiscardDeliveryResult{}, false, nil
}
if !fileExists(filepath.Join(dir, "pr.md")) || fileExists(filepath.Join(dir, "plan.lock.json")) {
return DiscardDeliveryResult{}, false, nil
}
archiveDir := filepath.Join(filepath.Dir(dir), ".discarded")
destination := filepath.Join(archiveDir, feature)
for suffix := 2; ; suffix++ {
if _, existErr := os.Stat(destination); os.IsNotExist(existErr) {
break
} else if existErr != nil {
return DiscardDeliveryResult{}, false, existErr
}
destination = filepath.Join(archiveDir, fmt.Sprintf("%s-%d", feature, suffix))
}
if err := os.MkdirAll(archiveDir, 0o755); err != nil {
return DiscardDeliveryResult{}, false, err
}
if err := os.Rename(dir, destination); err != nil {
return DiscardDeliveryResult{}, false, err
}
archive := destination
if rel, relErr := filepath.Rel(repo, destination); relErr == nil {
archive = filepath.ToSlash(rel)
}
return DiscardDeliveryResult{
Feature: feature, Action: "discarded", ArchivePath: archive,
Reason: "orphaned feature artifacts (pr.md without a plan lock) archived; the feature can be re-planned",
}, true, nil
}

func DiscardDelivery(repoPath, feature string, force bool) (DiscardDeliveryResult, error) {
repo, err := ResolveRepository(repoPath)
if err != nil {
Expand All @@ -1359,6 +1408,15 @@ func DiscardDelivery(repoPath, feature string, force bool) (DiscardDeliveryResul
}
featureDir := filepath.Dir(statePath)
if info, statErr := os.Stat(featureDir); os.IsNotExist(statErr) {
// No delivery-state dir. The resolver also prescribes discard-delivery for an
// ORPHAN — a product-loop feature dir carrying a pr.md but no plan.lock.json —
// so discard-delivery must accept and archive that too (Coreachability: the
// verb accepts every state that prescribes it).
if archived, ok, orphanErr := discardOrphanFeatureArtifacts(repo, feature); orphanErr != nil {
return DiscardDeliveryResult{}, orphanErr
} else if ok {
return archived, nil
}
return DiscardDeliveryResult{
Feature: feature, Action: "none",
Reason: "no managed delivery state exists for this feature",
Expand Down
26 changes: 20 additions & 6 deletions boatstack/next.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,10 @@ func ResolveNext(repoPath, explicitFeature string) (NextStatus, error) {
}
config, _, configErr := LoadConfig(WorkspaceFor(repo).ProjectConfigPath())
if configErr != nil {
return blockedNextStatus("INVALID_STATE", "repair-state", "Boatstack project configuration is invalid: "+configErr.Error()), nil
// Channel fault: an invalid config cannot be cleared by any mutation verb —
// the operator repairs the file. Route to the read-only doctor to diagnose,
// not repair-state (which quarantines a draft and would not help). Coreachability.
return blockedNextStatus("INVALID_STATE", "doctor", "Boatstack project configuration is invalid; fix the config file, then re-run (doctor diagnoses): "+configErr.Error()), nil
}

// Read-only boundary: apply the ignored-deliveries filter BEFORE a single
Expand All @@ -230,7 +233,9 @@ func ResolveNext(repoPath, explicitFeature string) (NextStatus, error) {
// unrelated new feature. control-law: stale-delivery-cannot-block-unrelated-feature
active, invalidDeliveries, scanErr := scanManagedDeliveries(repo)
if scanErr != nil {
return blockedNextStatus("INVALID_STATE", "repair-state", "Boatstack could not read the managed delivery store: "+scanErr.Error()), nil
// Channel fault reading the store: observation loss, diagnosed by doctor —
// not repaired by quarantining a draft. Coreachability.
return blockedNextStatus("INVALID_STATE", "doctor", "Boatstack could not read the managed delivery store; diagnose the channel with doctor: "+scanErr.Error()), nil
}
active = withoutIgnoredDeliveries(active, config.Workflow.IgnoredDeliveries)
invalidDeliveries = withoutIgnoredDeliveries(invalidDeliveries, config.Workflow.IgnoredDeliveries)
Expand All @@ -251,7 +256,9 @@ func ResolveNext(repoPath, explicitFeature string) (NextStatus, error) {
} else if completedState, completedErr := CurrentDeliveryState(repo, explicitFeature); completedErr == nil && completedState.ActiveIndex >= len(completedState.Slices) {
return nextForPublished(repo, completedState), nil
} else {
return blockedNextStatus("INVALID_STATE", "repair-state", fmt.Sprintf("Feature %s is not a verifiable active or published managed delivery.", explicitFeature)), nil
// Unverifiable named delivery: discard-delivery accepts and archives it
// (repair-state refuses registered/tracked dirs). Coreachability.
return blockedNextStatus("INVALID_STATE", "discard-delivery", fmt.Sprintf("Feature %s is not a verifiable active or published managed delivery; clear it with discard-delivery.", explicitFeature), explicitFeature), nil
}
}

Expand All @@ -267,7 +274,9 @@ func ResolveNext(repoPath, explicitFeature string) (NextStatus, error) {
if len(active) == 1 {
status, deliveryErr := nextForDelivery(repo, active[0])
if deliveryErr != nil {
return blockedNextStatus("INVALID_STATE", "repair-state", "Boatstack could not verify the active managed delivery. Preserve the artifacts and restore its evidence before continuing: "+deliveryErr.Error()), nil
// Unverifiable active delivery state: discard-delivery archives it
// (reversibly); repair-state would refuse the registered dir. Coreachability.
return blockedNextStatus("INVALID_STATE", "discard-delivery", "Boatstack could not verify the active managed delivery; restore its evidence, or archive it with discard-delivery to continue: "+deliveryErr.Error(), active[0]), nil
}
return status, nil
}
Expand All @@ -277,7 +286,10 @@ func ResolveNext(repoPath, explicitFeature string) (NextStatus, error) {
return NextStatus{}, err
}
if len(orphans) > 0 {
return blockedNextStatus("INVALID_STATE", "repair-state", "Boatstack found a PR preview without the plan lock required to verify it. Preserve the artifacts and restore the feature evidence before continuing.", orphans...), nil
// An orphan (pr.md, no plan.lock) is a published-then-unlinked delivery.
// repair-state refuses it (pr.md is a durable-authority blocker); discard-delivery
// accepts and archives the orphaned artifacts. Coreachability.
return blockedNextStatus("INVALID_STATE", "discard-delivery", "Boatstack found a PR preview without the plan lock required to verify it; restore the feature evidence, or archive the orphan with discard-delivery.", orphans...), nil
}

candidates, err := featurePlanCandidates(repo)
Expand Down Expand Up @@ -325,7 +337,9 @@ func ResolveNext(repoPath, explicitFeature string) (NextStatus, error) {

completed, err := completedManagedStates(repo)
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
// Invalid completed delivery state: discard-delivery archives it reversibly;
// repair-state refuses a delivery-bearing dir. Coreachability.
return blockedNextStatus("INVALID_STATE", "discard-delivery", "Boatstack found invalid completed delivery state; restore its evidence, or archive it with discard-delivery to continue: "+err.Error()), nil
}
completed = withoutIgnoredDeliveryStates(completed, config.Workflow.IgnoredDeliveries)
if len(completed) > 0 {
Expand Down
6 changes: 3 additions & 3 deletions boatstack/next_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func TestResolveNextOrphanedEvidenceBlocks(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if status.VerificationStatus != "BLOCKED" || status.ObservedStage != "INVALID_STATE" || status.NextOperation != "repair-state" {
if status.VerificationStatus != "BLOCKED" || status.ObservedStage != "INVALID_STATE" || status.NextOperation != "discard-delivery" {
t.Fatalf("orphaned evidence did not block: %+v", status)
}
}
Expand Down Expand Up @@ -521,7 +521,7 @@ func TestResolveNextBlocksStaleManagedState(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if status.VerificationStatus != "BLOCKED" || status.ObservedStage != "INVALID_STATE" || status.NextOperation != "repair-state" {
if status.VerificationStatus != "BLOCKED" || status.ObservedStage != "INVALID_STATE" || status.NextOperation != "discard-delivery" {
t.Fatalf("stale managed state was accepted: %+v", status)
}
}
Expand Down Expand Up @@ -561,7 +561,7 @@ func TestResolveNextBlocksMissingLockAndOrphanPreview(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if status.VerificationStatus != "BLOCKED" || status.ObservedStage != "INVALID_STATE" || status.NextOperation != "repair-state" {
if status.VerificationStatus != "BLOCKED" || status.ObservedStage != "INVALID_STATE" || status.NextOperation != "discard-delivery" {
t.Fatalf("unexpected invalid state: %+v", status)
}
if !reflect.DeepEqual(before, after) {
Expand Down
4 changes: 4 additions & 0 deletions boatstack/recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,10 @@ func RepairState(repoPath, feature string) (RepairStateResult, error) {
return refusedRepairState(feature, "no plan.md exists for this feature; nothing to repair"), nil
}
if _, checkErr := CheckPlan(planPath); checkErr == nil {
// Coreachability: repair-state accepts only a malformed unregistered draft, so
// the resolver must never PRESCRIBE it for a valid one. A valid draft advances
// (plan-gate/activate); an orphan or unverifiable delivery is prescribed
// discard-delivery, not repair-state — see preActivationFinding / ResolveNext.
return refusedRepairState(feature, "the saved plan is valid; repair-state only quarantines a malformed unregistered draft"), nil
}

Expand Down
Loading
Loading