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/d950cb53d33f4f2571a62ce05e10a85a3da7756b/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/14534849fadddf0dddd8b5c9c48291b36a0a4daa/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
31 changes: 16 additions & 15 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"canonical_context": {
"characters": 91083,
"estimated_tokens": 22771,
"characters": 92427,
"estimated_tokens": 23107,
"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": "cfeb3c333bd52524eccb76ccdda2b39c7b4944b15b92c0bd422f5cc9a94d740d",
"CONTRIBUTING.md": "25686dc0fda396906f4a1f57c7c1f5fb3019a0de6e37a1e1fbff1a927097e85a",
"README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
Expand Down Expand Up @@ -106,13 +106,13 @@
"boatstack/hooks_hydrate_test.go": "7beeb26b2b1398741e8a28963a9686e974047016cc736f233024004add1afc32",
"boatstack/hooks_test.go": "fb75e3aabf2204871b3e6d16de98d26fb33b0ec19e41aae761cf1f34397c31f4",
"boatstack/hydrate_runtime_test.go": "dbd5eae2ba85701e4af0430ba3a0d70ea98e028b66992bd4fc05f3f582398627",
"boatstack/init.go": "7caef2bb2e40cd75a9e940b04deef242812f5f971332b86b00739662435bcae3",
"boatstack/init.go": "1fc4392a76de4ab2bdc9a0493d5df8ccc36bf2b7fdbbf1ce3f484fc9ac90c8af",
"boatstack/init_test.go": "5fdf687205e7a5984a98a87336b7127e4ae9b651d57e21ec2dc8ca7e653ee602",
"boatstack/init_transaction.go": "112456c4e1c4db54c4137bcf4f7a9a9e63399a6f5971e9b3dc952d0c4b2aa4b6",
"boatstack/insight.go": "7ec492b65043f7b10dae9dd55104d22bc56775759b8c3fb288545dede01b9f89",
"boatstack/insight_conformance_test.go": "6093b5496338e52995c231847d3938c4fa97b0f6b4ccbc49161b625d69e9c90e",
"boatstack/installation_repair.go": "f6889e3d21102d2aedfe1af900945d0a3ea2a772b3fddad787cef99803ef91aa",
"boatstack/installation_repair_test.go": "3194d3e8a737d5df09fcba2baef5f7b665db678eae181abbd2c35c462365477e",
"boatstack/installation_repair_test.go": "fe831f15458c10057654a296ee4472177ec150ca937462d169787566525a3976",
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
"boatstack/internal/deliverycontrol/advise.go": "4f3a53a785a34f6c34858236a57d4114091141a463b51e5aaefae3336557ae5a",
"boatstack/internal/deliverycontrol/advise_test.go": "6e0d3bda302cb5d26dd42a0253281ac6d1bb6749496b1a7df9c2aa9f53ea13a1",
Expand Down Expand Up @@ -157,8 +157,8 @@
"boatstack/next_response.go": "11decf2e3b236cbaa183980946ec17ffbbbb1af9c08bd11a466a8487bf229d5f",
"boatstack/next_response_conformance_test.go": "be4f3bc7507abfb0ae9f86310eb29e34b166dcc40b6fa103e05babb81f2bd928",
"boatstack/next_test.go": "6b5ec46ecf1a197d7644846cecbb6d99873a06b7c4e5562772b5016fa0a4cb11",
"boatstack/operation.go": "a51ee8d20cfb98d0e595e1420ed3277275229cf3ab0f2afa4c690c648a464853",
"boatstack/operation_test.go": "7c1c7f1b69359457a133b0b0d82310b9c5190eaff6f19738c3ca839807ca0a79",
"boatstack/operation.go": "1eac601c216282983dceb6f2f7c0be58b06312fb9659447bba02df896d23929a",
"boatstack/operation_test.go": "9580b71ed4fa70cf73f02975737c824484341fa6751e670e7d183e898e1ffdde",
"boatstack/paths.go": "84b56d031389dd872dfb88d08cfbdf178befe6df3ef2ac6d75369da8d827e8c6",
"boatstack/plan.go": "a130c9e587f97a280b6cfa531bcaab46fc6e58772f27921031be03eb98f04439",
"boatstack/plan_test.go": "1b01e7d9d7794eb11c998e19a2f3532d3b8509d984eca934cf5f1662a0a7e573",
Expand Down Expand Up @@ -186,11 +186,11 @@
"boatstack/reexec_windows.go": "f5335c8c28cb4e89048b058b1c4d12f78644f99acb4f6167ff60e622dfb9e742",
"boatstack/references/artifacts.md": "7f410a5c99273bdf45d70e86c226ac4d13d78a7bee563ec77a07793b8d46fb33",
"boatstack/references/config-schema.md": "1da1326cef1573d60e5df6fb3e0ae656f324fb4f7ad1df6f57c5c51664616b46",
"boatstack/references/failure-moves.md": "e990abca11035f412039d1366bff1bf052d1ff229cbd5d0be97f1e608684fe6d",
"boatstack/references/failure-moves.md": "2be252c8de61380712e351edaeb51625bf5c7eb23ebced04bc2bf0f5b4fb3be6",
"boatstack/references/host-hook-contracts.md": "2a89d44d0e418a53f2e3b6300fed957cdf878f45ea97ce24b55b66065f0eaa1d",
"boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441",
"boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae",
"boatstack/references/workflow.md": "b64c7d3168fa99055fdae798357110f11b496ce8a6f2a6237f17523ce6eeac48",
"boatstack/references/workflow.md": "b04c3c716793f9af85fa72f5cc882c282547811f6c705a2e5a76750ad7f1080a",
"boatstack/release.go": "82dcb4ca59e8c79a68d5333d650f90e64abd448d04e0c6f504fdf07f42b5ed76",
"boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690",
"boatstack/repair_budget_conformance_test.go": "05793600dac06bbf39075b15bc1262a9bd1e916738d801dcd6ff515771262b4c",
Expand Down Expand Up @@ -235,14 +235,14 @@
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "ed62dfd360377f7a15826ef5a2d3c9798d25c272168c65480961f9e0d011ccf1",
"docs/evidence-engineered-coding.md": "cae640d8fcb89edab69ecbee5148b336b4cb77c752cab79e6ce63925669876c3",
"docs/evidence-engineered-coding.md": "79f639f49b793e057fede99cd16ae869f8c6aca3c016da6e22a0488b79702691",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8",
"docs/public-claims.json": "6f93df0664dbaf0fee6aed003559ac6b0e606753cafd76298d68623a6c5c9f2c",
"docs/public-claims.json": "f4e703a4b2176b34fb2c8ac794b071a686aa6714904e1a5c09188b890cfc0587",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
"docs/troubleshooting.md": "10841d10b51d551cc49d6f9d1c7ee997a8aaf091a10e8f74a570b5cf445d6ada",
"docs/troubleshooting.md": "84f61b8f61d0ca02274f2e91a4da17802b99bd4bbfe0d116674e1b9398e8192a",
"docs/validation-and-evidence.md": "e7d91ad49c6adb44784ebe7d94feceb6abd445857f9a0716f0758bf6b55296c5",
"docs/why-these-steps.md": "cbe0d769db11ef15bb1dff888009378d6783776ad020e6f5139847a1dd62fa09",
"install.ps1": "f48d0f26a26e806b780d10fa916c261ff9f84ab39758cf9e229f647836845e86",
Expand All @@ -252,7 +252,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": "4e3130b0203c93077a3dfdaaf167dcd6adf50155d4477a41046f018c14461ddf",
"labs/diagram-json/plan.lock.json": "1c803010b3f61d0ab71fc1bf9898e3ea767c8fde1d47f77b3ec803796b5b2e35",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand Down Expand Up @@ -401,12 +401,13 @@
"release-notes/2026-07-30-visual-evidence-survives-preview-commit.md": "71d19e9fabb40e8cb1e939f26bf288911d227979926f43f337046c6cf6b66551",
"release-notes/2026-08-01-detached-update-recovery.md": "3c8e5b36ba5a561f0dccd67863f93981be7820c434ac37ed2aeeb708aa1d0fff",
"release-notes/2026-08-01-independent-insight-captures.md": "5d18f26fced9589fc7cfd70eac1bfe28038f99b921a5ea7766c310d6595349c3",
"release-notes/2026-08-01-readable-projection-history.md": "93c4db5c3935b4c4c251f2f8a5dd2925a7241f34f6dd7bb9602badca6ad367bc"
"release-notes/2026-08-01-readable-projection-history.md": "93c4db5c3935b4c4c251f2f8a5dd2925a7241f34f6dd7bb9602badca6ad367bc",
"release-notes/2026-08-01-terminal-update-postcondition.md": "65a502bfc4ee8002614e555189b082c3859dbfc2d0611c3f598dc951353c41a1"
},
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "d950cb53d33f4f2571a62ce05e10a85a3da7756b",
"commit": "14534849fadddf0dddd8b5c9c48291b36a0a4daa",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
63 changes: 60 additions & 3 deletions boatstack/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,8 @@ func RunUpdate(options InitOptions) error {
return reexecUpdate(options.BinaryPath, options)
}
}
config, _, configErr := LoadConfig(WorkspaceFor(repo).SourceConfigPath())
configPath := WorkspaceFor(repo).SourceConfigPath()
config, rawConfig, configErr := LoadConfig(configPath)
if configErr != nil {
return configErr
}
Expand Down Expand Up @@ -827,15 +828,27 @@ func RunUpdate(options InitOptions) error {
if err != nil {
return err
}
if receipt.State == OperationSucceeded {
postconditionErr := verifyInstalledUpdatePostcondition(repo, configPath, config, rawConfig, preflight.PreservedIntegrations)
if postconditionErr == nil {
return nil
}
receipt, err = reconcileSucceededInstallUpdate(repo, receipt.OperationID,
"the previously successful local update no longer matches its target postcondition",
postconditionErr.Error())
if err != nil {
return err
}
}
begin, err := BeginOperation(repo, receipt.OperationID, SHA256Bytes([]byte("install-update\x00"+packageFingerprint)), "boatstack-helper update")
if err != nil {
if begin.Receipt.State == OperationSucceeded {
return nil
return verifyInstalledUpdatePostcondition(repo, configPath, config, rawConfig, preflight.PreservedIntegrations)
}
return err
}
if begin.Receipt.State == OperationSucceeded {
return nil
return verifyInstalledUpdatePostcondition(repo, configPath, config, rawConfig, preflight.PreservedIntegrations)
}
options.Update = true
if err := RunInit(options); err != nil {
Expand All @@ -845,3 +858,47 @@ func RunUpdate(options InitOptions) error {
_, err = CompleteOperation(repo, receipt.OperationID, begin.LeaseToken, "SUCCEEDED", "post-install doctor and generated projections passed", Version)
return err
}

func verifyInstalledUpdatePostcondition(repo, configPath string, config ProjectConfig, rawConfig []byte, expectedIntegrations map[string]IntegrationState) error {
bundle, err := BuildExportBundle(configPath, config, rawConfig, "boatstack")
if err != nil {
return err
}
if err := CheckExport(repo, bundle.Files); err != nil {
return err
}
if err := CheckHostHooks(repo, config.Adapters); err != nil {
return err
}
for _, host := range config.Adapters {
path := executionInterceptorPath(host)
if path == "" {
continue
}
items := classifyExecutionInterceptor(repo, host)
if len(items) != 1 || items[0].Classification != RepairCurrent {
return fmt.Errorf("target execution interceptor is not current: %s", path)
}
}
if err := verifyGeneratedRuntime(repo); err != nil {
return err
}
manifest, _, err := loadSharedRuntime(repo)
if err != nil {
return err
}
if err := verifyLocalRuntime(repo); err != nil {
return err
}
installedIntegrations, err := readInstalledIntegrations(repo, config)
if err != nil {
return err
}
if expectedIntegrations == nil {
expectedIntegrations = config.Integrations
}
if !sameJSON(installedIntegrations, expectedIntegrations) || !sameJSON(manifest.Integrations, expectedIntegrations) {
return fmt.Errorf("installed integration state does not match the update postcondition")
}
return nil
}
93 changes: 93 additions & 0 deletions boatstack/installation_repair_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,99 @@ func TestDetachedUpdateRepairsInvalidLocalProvenanceEndToEnd(t *testing.T) {
}
}

func TestDetachedUpdateReconcilesSucceededReceiptAfterCommittedPinIsRestored(t *testing.T) {
now := time.Date(2026, 8, 1, 13, 0, 0, 0, time.UTC)
withUpdateGlobals(t, "v0.4.0", now, func() (ReleaseInfo, error) { return ReleaseInfo{}, nil })
repo, _ := updateInstalledRepo(t)
t.Setenv(stateRootEnv, t.TempDir())
invalidateWorkspaceCache()
if _, err := AttachDetached(AttachOptions{Repo: repo}); err != nil {
t.Fatal(err)
}
runGit(t, repo, "switch", "-c", "chore/update-boatstack-v0.5.0")
Version = "v0.5.0"
SourceCommit = "update-test-0.5.0"
options := InitOptions{Repo: repo, Repair: true, Yes: true, Input: strings.NewReader(""), Output: &bytes.Buffer{}}
if err := RunUpdate(options); err != nil {
t.Fatal(err)
}
receipts, err := operationReceipts(repo)
if err != nil || len(receipts) == 0 {
t.Fatalf("first update receipt missing: %#v %v", receipts, err)
}
var updateID string
for _, receipt := range receipts {
if receipt.Kind == "install-update" && receipt.State == OperationSucceeded {
updateID = receipt.OperationID
}
}
if updateID == "" {
t.Fatalf("successful update receipt missing: %#v", receipts)
}

// Simulate an operator discarding the generated update diff while ignored
// runtime state and the detached terminal receipt survive.
runGit(t, repo, "restore", ".")
if status := gitPorcelain(t, repo); status != "" {
t.Fatalf("fixture did not restore a clean old-pin worktree: %s", status)
}
if err := verifyGeneratedRuntime(repo); err == nil {
t.Fatal("restored old pin unexpectedly matched the target runtime")
}

options.Input = strings.NewReader("")
options.Output = &bytes.Buffer{}
if err := RunUpdate(options); err != nil {
t.Fatalf("clean retry did not reconcile the stale terminal receipt: %v", err)
}
if err := verifyGeneratedRuntime(repo); err != nil {
t.Fatalf("clean retry did not regenerate the target pin: %v", err)
}
reconciled, err := loadOperation(repo, updateID)
if err != nil || reconciled.State != OperationSucceeded || reconciled.Attempt != 1 || reconciled.Observation.Status != "SUCCEEDED" {
t.Fatalf("reconciled update did not finish as a fresh bounded attempt: %#v %v", reconciled, err)
}
}

func TestUpdatePostconditionVerifierIsReadOnlyWhenCurrent(t *testing.T) {
now := time.Date(2026, 8, 1, 13, 0, 0, 0, time.UTC)
withUpdateGlobals(t, "v0.4.0", now, func() (ReleaseInfo, error) { return ReleaseInfo{}, nil })
repo, _ := updateInstalledRepo(t)
runGit(t, repo, "switch", "-c", "chore/update-boatstack-v0.5.0")
Version = "v0.5.0"
SourceCommit = "update-test-0.5.0"
options := InitOptions{Repo: repo, Yes: true, Input: strings.NewReader(""), Output: &bytes.Buffer{}}
if err := RunUpdate(options); err != nil {
t.Fatal(err)
}
receipts, err := operationReceipts(repo)
if err != nil {
t.Fatal(err)
}
var before OperationReceipt
for _, receipt := range receipts {
if receipt.Kind == "install-update" {
before = receipt
}
}
configPath := WorkspaceFor(repo).SourceConfigPath()
config, rawConfig, err := LoadConfig(configPath)
if err != nil {
t.Fatal(err)
}
preflight, err := ClassifyInstallationRepair(repo, config.Adapters, false)
if err != nil {
t.Fatal(err)
}
if err := verifyInstalledUpdatePostcondition(repo, configPath, config, rawConfig, preflight.PreservedIntegrations); err != nil {
t.Fatalf("current update postcondition did not verify: %v", err)
}
after, err := loadOperation(repo, before.OperationID)
if err != nil || after.Attempt != before.Attempt || after.UpdatedAt != before.UpdatedAt {
t.Fatalf("current terminal receipt was needlessly reopened: before=%#v after=%#v err=%v", before, after, err)
}
}

func TestRepairReconstructsCorruptGeneratedProvenance(t *testing.T) {
now := time.Date(2026, 7, 23, 12, 0, 0, 0, time.UTC)
withUpdateGlobals(t, "v0.4.0", now, func() (ReleaseInfo, error) { return ReleaseInfo{}, nil })
Expand Down
55 changes: 52 additions & 3 deletions boatstack/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -253,16 +254,28 @@ func withOperationLock(repo, id string, apply func() error) error {
return fmt.Errorf("operation %s is busy", id)
}

// Windows can report ERROR_ACCESS_DENIED when another process owns an O_EXCL
// lock file. Treat that as contention only when the lock path actually exists;
// genuine directory/ACL permission failures still fail closed.
// Windows can report ERROR_ACCESS_DENIED while another process owns or has just
// released an O_EXCL lock file. Retry that condition within the caller's bounded
// budget; other platforms require the lock path to exist.
func isLockContention(openErr error, lock string) bool {
return isLockContentionForOS(openErr, lock, runtime.GOOS)
}

func isLockContentionForOS(openErr error, lock, goos string) bool {
if os.IsExist(openErr) {
return true
}
if !os.IsPermission(openErr) {
return false
}
// Windows can keep an exclusive lock handle alive briefly after the owner
// removes its directory entry. During that interval OpenFile reports
// ERROR_ACCESS_DENIED while a following Stat can already report not-exist.
// Retry within the caller's fixed budget; a real ACL failure still exhausts
// that budget without entering the critical section.
if goos == "windows" {
return true
}
_, statErr := os.Stat(lock)
return statErr == nil
}
Expand Down Expand Up @@ -442,6 +455,42 @@ func BeginOperation(repoPath, id, attemptKey, tool string) (OperationBeginResult
return result, err
}

// reconcileSucceededInstallUpdate reopens only a local atomic Boatstack update
// whose previously observed postcondition no longer holds. A terminal receipt is
// evidence about an observation in time, not permanent authority to suppress a
// later explicit update after the repository was restored or otherwise regressed.
// Other operation kinds keep their existing terminal replay semantics.
func reconcileSucceededInstallUpdate(repoPath, id, detail, evidence string) (OperationReceipt, error) {
repo, err := ResolveRepository(repoPath)
if err != nil {
return OperationReceipt{}, err
}
var result OperationReceipt
err = withOperationLock(repo, id, func() error {
receipt, loadErr := loadOperation(repo, id)
if loadErr != nil {
return loadErr
}
if receipt.State != OperationSucceeded {
return fmt.Errorf("operation %s is no longer a succeeded update", id)
}
if receipt.Kind != "install-update" || receipt.RetryClass != "ATOMIC_LOCAL" {
return fmt.Errorf("operation %s does not support terminal postcondition reconciliation", id)
}
receipt.State = OperationRetryable
receipt.Attempt = 0
receipt.Lease = nil
receipt.Observation = OperationObservation{
Status: "POSTCONDITION_MISSING", Detail: boundedObservation(detail),
Evidence: boundedObservation(evidence), At: operationTimestamp(),
}
receipt.UpdatedAt = operationTimestamp()
result = receipt
return saveOperation(repo, receipt)
})
return result, err
}

func completeOperation(repoPath, id, leaseToken, attemptKey, outcome, detail, evidence string, trustedAttempt bool) (OperationReceipt, error) {
repo, err := ResolveRepository(repoPath)
if err != nil {
Expand Down
Loading
Loading