Skip to content

Commit c404290

Browse files
Sync Boatstack from Intelligence Flow Labs @ 14534849fadd
1 parent 343c374 commit c404290

13 files changed

Lines changed: 272 additions & 37 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/d950cb53d33f4f2571a62ce05e10a85a3da7756b/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/14534849fadddf0dddd8b5c9c48291b36a0a4daa/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: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"canonical_context": {
3-
"characters": 91083,
4-
"estimated_tokens": 22771,
3+
"characters": 92427,
4+
"estimated_tokens": 23107,
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": "cfeb3c333bd52524eccb76ccdda2b39c7b4944b15b92c0bd422f5cc9a94d740d",
15+
"CONTRIBUTING.md": "25686dc0fda396906f4a1f57c7c1f5fb3019a0de6e37a1e1fbff1a927097e85a",
1616
"README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad",
1717
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
1818
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
@@ -106,13 +106,13 @@
106106
"boatstack/hooks_hydrate_test.go": "7beeb26b2b1398741e8a28963a9686e974047016cc736f233024004add1afc32",
107107
"boatstack/hooks_test.go": "fb75e3aabf2204871b3e6d16de98d26fb33b0ec19e41aae761cf1f34397c31f4",
108108
"boatstack/hydrate_runtime_test.go": "dbd5eae2ba85701e4af0430ba3a0d70ea98e028b66992bd4fc05f3f582398627",
109-
"boatstack/init.go": "7caef2bb2e40cd75a9e940b04deef242812f5f971332b86b00739662435bcae3",
109+
"boatstack/init.go": "1fc4392a76de4ab2bdc9a0493d5df8ccc36bf2b7fdbbf1ce3f484fc9ac90c8af",
110110
"boatstack/init_test.go": "5fdf687205e7a5984a98a87336b7127e4ae9b651d57e21ec2dc8ca7e653ee602",
111111
"boatstack/init_transaction.go": "112456c4e1c4db54c4137bcf4f7a9a9e63399a6f5971e9b3dc952d0c4b2aa4b6",
112112
"boatstack/insight.go": "7ec492b65043f7b10dae9dd55104d22bc56775759b8c3fb288545dede01b9f89",
113113
"boatstack/insight_conformance_test.go": "6093b5496338e52995c231847d3938c4fa97b0f6b4ccbc49161b625d69e9c90e",
114114
"boatstack/installation_repair.go": "f6889e3d21102d2aedfe1af900945d0a3ea2a772b3fddad787cef99803ef91aa",
115-
"boatstack/installation_repair_test.go": "3194d3e8a737d5df09fcba2baef5f7b665db678eae181abbd2c35c462365477e",
115+
"boatstack/installation_repair_test.go": "fe831f15458c10057654a296ee4472177ec150ca937462d169787566525a3976",
116116
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
117117
"boatstack/internal/deliverycontrol/advise.go": "4f3a53a785a34f6c34858236a57d4114091141a463b51e5aaefae3336557ae5a",
118118
"boatstack/internal/deliverycontrol/advise_test.go": "6e0d3bda302cb5d26dd42a0253281ac6d1bb6749496b1a7df9c2aa9f53ea13a1",
@@ -157,8 +157,8 @@
157157
"boatstack/next_response.go": "11decf2e3b236cbaa183980946ec17ffbbbb1af9c08bd11a466a8487bf229d5f",
158158
"boatstack/next_response_conformance_test.go": "be4f3bc7507abfb0ae9f86310eb29e34b166dcc40b6fa103e05babb81f2bd928",
159159
"boatstack/next_test.go": "6b5ec46ecf1a197d7644846cecbb6d99873a06b7c4e5562772b5016fa0a4cb11",
160-
"boatstack/operation.go": "a51ee8d20cfb98d0e595e1420ed3277275229cf3ab0f2afa4c690c648a464853",
161-
"boatstack/operation_test.go": "7c1c7f1b69359457a133b0b0d82310b9c5190eaff6f19738c3ca839807ca0a79",
160+
"boatstack/operation.go": "1eac601c216282983dceb6f2f7c0be58b06312fb9659447bba02df896d23929a",
161+
"boatstack/operation_test.go": "9580b71ed4fa70cf73f02975737c824484341fa6751e670e7d183e898e1ffdde",
162162
"boatstack/paths.go": "84b56d031389dd872dfb88d08cfbdf178befe6df3ef2ac6d75369da8d827e8c6",
163163
"boatstack/plan.go": "a130c9e587f97a280b6cfa531bcaab46fc6e58772f27921031be03eb98f04439",
164164
"boatstack/plan_test.go": "1b01e7d9d7794eb11c998e19a2f3532d3b8509d984eca934cf5f1662a0a7e573",
@@ -186,11 +186,11 @@
186186
"boatstack/reexec_windows.go": "f5335c8c28cb4e89048b058b1c4d12f78644f99acb4f6167ff60e622dfb9e742",
187187
"boatstack/references/artifacts.md": "7f410a5c99273bdf45d70e86c226ac4d13d78a7bee563ec77a07793b8d46fb33",
188188
"boatstack/references/config-schema.md": "1da1326cef1573d60e5df6fb3e0ae656f324fb4f7ad1df6f57c5c51664616b46",
189-
"boatstack/references/failure-moves.md": "e990abca11035f412039d1366bff1bf052d1ff229cbd5d0be97f1e608684fe6d",
189+
"boatstack/references/failure-moves.md": "2be252c8de61380712e351edaeb51625bf5c7eb23ebced04bc2bf0f5b4fb3be6",
190190
"boatstack/references/host-hook-contracts.md": "2a89d44d0e418a53f2e3b6300fed957cdf878f45ea97ce24b55b66065f0eaa1d",
191191
"boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441",
192192
"boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae",
193-
"boatstack/references/workflow.md": "b64c7d3168fa99055fdae798357110f11b496ce8a6f2a6237f17523ce6eeac48",
193+
"boatstack/references/workflow.md": "b04c3c716793f9af85fa72f5cc882c282547811f6c705a2e5a76750ad7f1080a",
194194
"boatstack/release.go": "82dcb4ca59e8c79a68d5333d650f90e64abd448d04e0c6f504fdf07f42b5ed76",
195195
"boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690",
196196
"boatstack/repair_budget_conformance_test.go": "05793600dac06bbf39075b15bc1262a9bd1e916738d801dcd6ff515771262b4c",
@@ -235,14 +235,14 @@
235235
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
236236
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
237237
"docs/configuration.md": "ed62dfd360377f7a15826ef5a2d3c9798d25c272168c65480961f9e0d011ccf1",
238-
"docs/evidence-engineered-coding.md": "cae640d8fcb89edab69ecbee5148b336b4cb77c752cab79e6ce63925669876c3",
238+
"docs/evidence-engineered-coding.md": "79f639f49b793e057fede99cd16ae869f8c6aca3c016da6e22a0488b79702691",
239239
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
240240
"docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8",
241-
"docs/public-claims.json": "6f93df0664dbaf0fee6aed003559ac6b0e606753cafd76298d68623a6c5c9f2c",
241+
"docs/public-claims.json": "f4e703a4b2176b34fb2c8ac794b071a686aa6714904e1a5c09188b890cfc0587",
242242
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
243243
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
244244
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
245-
"docs/troubleshooting.md": "10841d10b51d551cc49d6f9d1c7ee997a8aaf091a10e8f74a570b5cf445d6ada",
245+
"docs/troubleshooting.md": "84f61b8f61d0ca02274f2e91a4da17802b99bd4bbfe0d116674e1b9398e8192a",
246246
"docs/validation-and-evidence.md": "e7d91ad49c6adb44784ebe7d94feceb6abd445857f9a0716f0758bf6b55296c5",
247247
"docs/why-these-steps.md": "cbe0d769db11ef15bb1dff888009378d6783776ad020e6f5139847a1dd62fa09",
248248
"install.ps1": "f48d0f26a26e806b780d10fa916c261ff9f84ab39758cf9e229f647836845e86",
@@ -252,7 +252,7 @@
252252
"labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d",
253253
"labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71",
254254
"labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39",
255-
"labs/diagram-json/plan.lock.json": "4e3130b0203c93077a3dfdaaf167dcd6adf50155d4477a41046f018c14461ddf",
255+
"labs/diagram-json/plan.lock.json": "1c803010b3f61d0ab71fc1bf9898e3ea767c8fde1d47f77b3ec803796b5b2e35",
256256
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
257257
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
258258
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
@@ -401,12 +401,13 @@
401401
"release-notes/2026-07-30-visual-evidence-survives-preview-commit.md": "71d19e9fabb40e8cb1e939f26bf288911d227979926f43f337046c6cf6b66551",
402402
"release-notes/2026-08-01-detached-update-recovery.md": "3c8e5b36ba5a561f0dccd67863f93981be7820c434ac37ed2aeeb708aa1d0fff",
403403
"release-notes/2026-08-01-independent-insight-captures.md": "5d18f26fced9589fc7cfd70eac1bfe28038f99b921a5ea7766c310d6595349c3",
404-
"release-notes/2026-08-01-readable-projection-history.md": "93c4db5c3935b4c4c251f2f8a5dd2925a7241f34f6dd7bb9602badca6ad367bc"
404+
"release-notes/2026-08-01-readable-projection-history.md": "93c4db5c3935b4c4c251f2f8a5dd2925a7241f34f6dd7bb9602badca6ad367bc",
405+
"release-notes/2026-08-01-terminal-update-postcondition.md": "65a502bfc4ee8002614e555189b082c3859dbfc2d0611c3f598dc951353c41a1"
405406
},
406407
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
407408
"schema_version": 1,
408409
"source": {
409-
"commit": "d950cb53d33f4f2571a62ce05e10a85a3da7756b",
410+
"commit": "14534849fadddf0dddd8b5c9c48291b36a0a4daa",
410411
"path": "labs/12-product-engineering-loop",
411412
"repository": "operatorstack/intelligence-flow"
412413
}

boatstack/init.go

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,8 @@ func RunUpdate(options InitOptions) error {
793793
return reexecUpdate(options.BinaryPath, options)
794794
}
795795
}
796-
config, _, configErr := LoadConfig(WorkspaceFor(repo).SourceConfigPath())
796+
configPath := WorkspaceFor(repo).SourceConfigPath()
797+
config, rawConfig, configErr := LoadConfig(configPath)
797798
if configErr != nil {
798799
return configErr
799800
}
@@ -827,15 +828,27 @@ func RunUpdate(options InitOptions) error {
827828
if err != nil {
828829
return err
829830
}
831+
if receipt.State == OperationSucceeded {
832+
postconditionErr := verifyInstalledUpdatePostcondition(repo, configPath, config, rawConfig, preflight.PreservedIntegrations)
833+
if postconditionErr == nil {
834+
return nil
835+
}
836+
receipt, err = reconcileSucceededInstallUpdate(repo, receipt.OperationID,
837+
"the previously successful local update no longer matches its target postcondition",
838+
postconditionErr.Error())
839+
if err != nil {
840+
return err
841+
}
842+
}
830843
begin, err := BeginOperation(repo, receipt.OperationID, SHA256Bytes([]byte("install-update\x00"+packageFingerprint)), "boatstack-helper update")
831844
if err != nil {
832845
if begin.Receipt.State == OperationSucceeded {
833-
return nil
846+
return verifyInstalledUpdatePostcondition(repo, configPath, config, rawConfig, preflight.PreservedIntegrations)
834847
}
835848
return err
836849
}
837850
if begin.Receipt.State == OperationSucceeded {
838-
return nil
851+
return verifyInstalledUpdatePostcondition(repo, configPath, config, rawConfig, preflight.PreservedIntegrations)
839852
}
840853
options.Update = true
841854
if err := RunInit(options); err != nil {
@@ -845,3 +858,47 @@ func RunUpdate(options InitOptions) error {
845858
_, err = CompleteOperation(repo, receipt.OperationID, begin.LeaseToken, "SUCCEEDED", "post-install doctor and generated projections passed", Version)
846859
return err
847860
}
861+
862+
func verifyInstalledUpdatePostcondition(repo, configPath string, config ProjectConfig, rawConfig []byte, expectedIntegrations map[string]IntegrationState) error {
863+
bundle, err := BuildExportBundle(configPath, config, rawConfig, "boatstack")
864+
if err != nil {
865+
return err
866+
}
867+
if err := CheckExport(repo, bundle.Files); err != nil {
868+
return err
869+
}
870+
if err := CheckHostHooks(repo, config.Adapters); err != nil {
871+
return err
872+
}
873+
for _, host := range config.Adapters {
874+
path := executionInterceptorPath(host)
875+
if path == "" {
876+
continue
877+
}
878+
items := classifyExecutionInterceptor(repo, host)
879+
if len(items) != 1 || items[0].Classification != RepairCurrent {
880+
return fmt.Errorf("target execution interceptor is not current: %s", path)
881+
}
882+
}
883+
if err := verifyGeneratedRuntime(repo); err != nil {
884+
return err
885+
}
886+
manifest, _, err := loadSharedRuntime(repo)
887+
if err != nil {
888+
return err
889+
}
890+
if err := verifyLocalRuntime(repo); err != nil {
891+
return err
892+
}
893+
installedIntegrations, err := readInstalledIntegrations(repo, config)
894+
if err != nil {
895+
return err
896+
}
897+
if expectedIntegrations == nil {
898+
expectedIntegrations = config.Integrations
899+
}
900+
if !sameJSON(installedIntegrations, expectedIntegrations) || !sameJSON(manifest.Integrations, expectedIntegrations) {
901+
return fmt.Errorf("installed integration state does not match the update postcondition")
902+
}
903+
return nil
904+
}

boatstack/installation_repair_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,99 @@ func TestDetachedUpdateRepairsInvalidLocalProvenanceEndToEnd(t *testing.T) {
375375
}
376376
}
377377

378+
func TestDetachedUpdateReconcilesSucceededReceiptAfterCommittedPinIsRestored(t *testing.T) {
379+
now := time.Date(2026, 8, 1, 13, 0, 0, 0, time.UTC)
380+
withUpdateGlobals(t, "v0.4.0", now, func() (ReleaseInfo, error) { return ReleaseInfo{}, nil })
381+
repo, _ := updateInstalledRepo(t)
382+
t.Setenv(stateRootEnv, t.TempDir())
383+
invalidateWorkspaceCache()
384+
if _, err := AttachDetached(AttachOptions{Repo: repo}); err != nil {
385+
t.Fatal(err)
386+
}
387+
runGit(t, repo, "switch", "-c", "chore/update-boatstack-v0.5.0")
388+
Version = "v0.5.0"
389+
SourceCommit = "update-test-0.5.0"
390+
options := InitOptions{Repo: repo, Repair: true, Yes: true, Input: strings.NewReader(""), Output: &bytes.Buffer{}}
391+
if err := RunUpdate(options); err != nil {
392+
t.Fatal(err)
393+
}
394+
receipts, err := operationReceipts(repo)
395+
if err != nil || len(receipts) == 0 {
396+
t.Fatalf("first update receipt missing: %#v %v", receipts, err)
397+
}
398+
var updateID string
399+
for _, receipt := range receipts {
400+
if receipt.Kind == "install-update" && receipt.State == OperationSucceeded {
401+
updateID = receipt.OperationID
402+
}
403+
}
404+
if updateID == "" {
405+
t.Fatalf("successful update receipt missing: %#v", receipts)
406+
}
407+
408+
// Simulate an operator discarding the generated update diff while ignored
409+
// runtime state and the detached terminal receipt survive.
410+
runGit(t, repo, "restore", ".")
411+
if status := gitPorcelain(t, repo); status != "" {
412+
t.Fatalf("fixture did not restore a clean old-pin worktree: %s", status)
413+
}
414+
if err := verifyGeneratedRuntime(repo); err == nil {
415+
t.Fatal("restored old pin unexpectedly matched the target runtime")
416+
}
417+
418+
options.Input = strings.NewReader("")
419+
options.Output = &bytes.Buffer{}
420+
if err := RunUpdate(options); err != nil {
421+
t.Fatalf("clean retry did not reconcile the stale terminal receipt: %v", err)
422+
}
423+
if err := verifyGeneratedRuntime(repo); err != nil {
424+
t.Fatalf("clean retry did not regenerate the target pin: %v", err)
425+
}
426+
reconciled, err := loadOperation(repo, updateID)
427+
if err != nil || reconciled.State != OperationSucceeded || reconciled.Attempt != 1 || reconciled.Observation.Status != "SUCCEEDED" {
428+
t.Fatalf("reconciled update did not finish as a fresh bounded attempt: %#v %v", reconciled, err)
429+
}
430+
}
431+
432+
func TestUpdatePostconditionVerifierIsReadOnlyWhenCurrent(t *testing.T) {
433+
now := time.Date(2026, 8, 1, 13, 0, 0, 0, time.UTC)
434+
withUpdateGlobals(t, "v0.4.0", now, func() (ReleaseInfo, error) { return ReleaseInfo{}, nil })
435+
repo, _ := updateInstalledRepo(t)
436+
runGit(t, repo, "switch", "-c", "chore/update-boatstack-v0.5.0")
437+
Version = "v0.5.0"
438+
SourceCommit = "update-test-0.5.0"
439+
options := InitOptions{Repo: repo, Yes: true, Input: strings.NewReader(""), Output: &bytes.Buffer{}}
440+
if err := RunUpdate(options); err != nil {
441+
t.Fatal(err)
442+
}
443+
receipts, err := operationReceipts(repo)
444+
if err != nil {
445+
t.Fatal(err)
446+
}
447+
var before OperationReceipt
448+
for _, receipt := range receipts {
449+
if receipt.Kind == "install-update" {
450+
before = receipt
451+
}
452+
}
453+
configPath := WorkspaceFor(repo).SourceConfigPath()
454+
config, rawConfig, err := LoadConfig(configPath)
455+
if err != nil {
456+
t.Fatal(err)
457+
}
458+
preflight, err := ClassifyInstallationRepair(repo, config.Adapters, false)
459+
if err != nil {
460+
t.Fatal(err)
461+
}
462+
if err := verifyInstalledUpdatePostcondition(repo, configPath, config, rawConfig, preflight.PreservedIntegrations); err != nil {
463+
t.Fatalf("current update postcondition did not verify: %v", err)
464+
}
465+
after, err := loadOperation(repo, before.OperationID)
466+
if err != nil || after.Attempt != before.Attempt || after.UpdatedAt != before.UpdatedAt {
467+
t.Fatalf("current terminal receipt was needlessly reopened: before=%#v after=%#v err=%v", before, after, err)
468+
}
469+
}
470+
378471
func TestRepairReconstructsCorruptGeneratedProvenance(t *testing.T) {
379472
now := time.Date(2026, 7, 23, 12, 0, 0, 0, time.UTC)
380473
withUpdateGlobals(t, "v0.4.0", now, func() (ReleaseInfo, error) { return ReleaseInfo{}, nil })

boatstack/operation.go

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"path/filepath"
1010
"regexp"
11+
"runtime"
1112
"sort"
1213
"strings"
1314
"time"
@@ -253,16 +254,28 @@ func withOperationLock(repo, id string, apply func() error) error {
253254
return fmt.Errorf("operation %s is busy", id)
254255
}
255256

256-
// Windows can report ERROR_ACCESS_DENIED when another process owns an O_EXCL
257-
// lock file. Treat that as contention only when the lock path actually exists;
258-
// genuine directory/ACL permission failures still fail closed.
257+
// Windows can report ERROR_ACCESS_DENIED while another process owns or has just
258+
// released an O_EXCL lock file. Retry that condition within the caller's bounded
259+
// budget; other platforms require the lock path to exist.
259260
func isLockContention(openErr error, lock string) bool {
261+
return isLockContentionForOS(openErr, lock, runtime.GOOS)
262+
}
263+
264+
func isLockContentionForOS(openErr error, lock, goos string) bool {
260265
if os.IsExist(openErr) {
261266
return true
262267
}
263268
if !os.IsPermission(openErr) {
264269
return false
265270
}
271+
// Windows can keep an exclusive lock handle alive briefly after the owner
272+
// removes its directory entry. During that interval OpenFile reports
273+
// ERROR_ACCESS_DENIED while a following Stat can already report not-exist.
274+
// Retry within the caller's fixed budget; a real ACL failure still exhausts
275+
// that budget without entering the critical section.
276+
if goos == "windows" {
277+
return true
278+
}
266279
_, statErr := os.Stat(lock)
267280
return statErr == nil
268281
}
@@ -442,6 +455,42 @@ func BeginOperation(repoPath, id, attemptKey, tool string) (OperationBeginResult
442455
return result, err
443456
}
444457

458+
// reconcileSucceededInstallUpdate reopens only a local atomic Boatstack update
459+
// whose previously observed postcondition no longer holds. A terminal receipt is
460+
// evidence about an observation in time, not permanent authority to suppress a
461+
// later explicit update after the repository was restored or otherwise regressed.
462+
// Other operation kinds keep their existing terminal replay semantics.
463+
func reconcileSucceededInstallUpdate(repoPath, id, detail, evidence string) (OperationReceipt, error) {
464+
repo, err := ResolveRepository(repoPath)
465+
if err != nil {
466+
return OperationReceipt{}, err
467+
}
468+
var result OperationReceipt
469+
err = withOperationLock(repo, id, func() error {
470+
receipt, loadErr := loadOperation(repo, id)
471+
if loadErr != nil {
472+
return loadErr
473+
}
474+
if receipt.State != OperationSucceeded {
475+
return fmt.Errorf("operation %s is no longer a succeeded update", id)
476+
}
477+
if receipt.Kind != "install-update" || receipt.RetryClass != "ATOMIC_LOCAL" {
478+
return fmt.Errorf("operation %s does not support terminal postcondition reconciliation", id)
479+
}
480+
receipt.State = OperationRetryable
481+
receipt.Attempt = 0
482+
receipt.Lease = nil
483+
receipt.Observation = OperationObservation{
484+
Status: "POSTCONDITION_MISSING", Detail: boundedObservation(detail),
485+
Evidence: boundedObservation(evidence), At: operationTimestamp(),
486+
}
487+
receipt.UpdatedAt = operationTimestamp()
488+
result = receipt
489+
return saveOperation(repo, receipt)
490+
})
491+
return result, err
492+
}
493+
445494
func completeOperation(repoPath, id, leaseToken, attemptKey, outcome, detail, evidence string, trustedAttempt bool) (OperationReceipt, error) {
446495
repo, err := ResolveRepository(repoPath)
447496
if err != nil {

0 commit comments

Comments
 (0)