From 197b0ccfd39ee10853a74c55d776316fafede024 Mon Sep 17 00:00:00 2001 From: "operator-stack-publisher[bot]" Date: Sat, 1 Aug 2026 21:23:22 +0000 Subject: [PATCH] Sync Boatstack from Intelligence Flow Labs @ d950cb53d33f --- CONTRIBUTING.md | 2 +- UPSTREAM.json | 35 +++++---- boatstack/installation_repair.go | 51 ++++++++++-- boatstack/installation_repair_test.go | 78 +++++++++++++++++++ boatstack/operation.go | 33 ++++---- boatstack/operation_test.go | 64 +++++++++++++++ boatstack/paths.go | 54 +++++++++++++ boatstack/references/failure-moves.md | 2 + boatstack/references/workflow.md | 2 + boatstack/runtime_cache.go | 74 ++++++++---------- boatstack/update.go | 2 +- docs/evidence-engineered-coding.md | 4 +- docs/public-claims.json | 24 +++--- docs/troubleshooting.md | 2 + labs/diagram-json/plan.lock.json | 2 +- .../2026-08-01-detached-update-recovery.md | 3 + 16 files changed, 328 insertions(+), 104 deletions(-) create mode 100644 release-notes/2026-08-01-detached-update-recovery.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aafd80e..cb9f921 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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/6149e9333caa3d936f69e03015252b712cf453ee/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/d950cb53d33f4f2571a62ce05e10a85a3da7756b/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. diff --git a/UPSTREAM.json b/UPSTREAM.json index b1bd93a..72753cc 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -1,7 +1,7 @@ { "canonical_context": { - "characters": 89360, - "estimated_tokens": 22340, + "characters": 91083, + "estimated_tokens": 22771, "estimator": "ceil(total characters / 4); compactness signal, not provider billing", "files": [ "product-engineering-loop/references/workflow.md", @@ -12,7 +12,7 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "5972bb34fea69f854044bd649c6e5a61e0668461946cfcb67e0ce10fa87cc914", + "CONTRIBUTING.md": "cfeb3c333bd52524eccb76ccdda2b39c7b4944b15b92c0bd422f5cc9a94d740d", "README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", @@ -111,8 +111,8 @@ "boatstack/init_transaction.go": "112456c4e1c4db54c4137bcf4f7a9a9e63399a6f5971e9b3dc952d0c4b2aa4b6", "boatstack/insight.go": "7ec492b65043f7b10dae9dd55104d22bc56775759b8c3fb288545dede01b9f89", "boatstack/insight_conformance_test.go": "6093b5496338e52995c231847d3938c4fa97b0f6b4ccbc49161b625d69e9c90e", - "boatstack/installation_repair.go": "c9698e134094873642a60300342b3d1c2be30dc86b15dc73aa859605e83d2019", - "boatstack/installation_repair_test.go": "ae5a5ea1110836bd78cf20ade863a4d32cfd63d282559f92786f57b31869bd14", + "boatstack/installation_repair.go": "f6889e3d21102d2aedfe1af900945d0a3ea2a772b3fddad787cef99803ef91aa", + "boatstack/installation_repair_test.go": "3194d3e8a737d5df09fcba2baef5f7b665db678eae181abbd2c35c462365477e", "boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c", "boatstack/internal/deliverycontrol/advise.go": "4f3a53a785a34f6c34858236a57d4114091141a463b51e5aaefae3336557ae5a", "boatstack/internal/deliverycontrol/advise_test.go": "6e0d3bda302cb5d26dd42a0253281ac6d1bb6749496b1a7df9c2aa9f53ea13a1", @@ -157,9 +157,9 @@ "boatstack/next_response.go": "11decf2e3b236cbaa183980946ec17ffbbbb1af9c08bd11a466a8487bf229d5f", "boatstack/next_response_conformance_test.go": "be4f3bc7507abfb0ae9f86310eb29e34b166dcc40b6fa103e05babb81f2bd928", "boatstack/next_test.go": "6b5ec46ecf1a197d7644846cecbb6d99873a06b7c4e5562772b5016fa0a4cb11", - "boatstack/operation.go": "86c6d1a82cfd0b3f2aadef044a8047fb49c612131ef8ba5f44e92c9f18e45162", - "boatstack/operation_test.go": "59d3dc37319aa4d334c0cacbe886e2f757842e6a28dee8781448e528fecbde11", - "boatstack/paths.go": "e1f3b42cda713997b6b6dcd585e0b8d06d0b1c82e61a9afaa33a14c223818c40", + "boatstack/operation.go": "a51ee8d20cfb98d0e595e1420ed3277275229cf3ab0f2afa4c690c648a464853", + "boatstack/operation_test.go": "7c1c7f1b69359457a133b0b0d82310b9c5190eaff6f19738c3ca839807ca0a79", + "boatstack/paths.go": "84b56d031389dd872dfb88d08cfbdf178befe6df3ef2ac6d75369da8d827e8c6", "boatstack/plan.go": "a130c9e587f97a280b6cfa531bcaab46fc6e58772f27921031be03eb98f04439", "boatstack/plan_test.go": "1b01e7d9d7794eb11c998e19a2f3532d3b8509d984eca934cf5f1662a0a7e573", "boatstack/plan_validation.go": "48c4fd061257f821ed54a0892b291cc04450e93e3db48f358f8c6599b21f9e41", @@ -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": "b65ef72035afa6ad0dce589a0b38f84bc40cde3864c9ecf973f08fc687f001c3", + "boatstack/references/failure-moves.md": "e990abca11035f412039d1366bff1bf052d1ff229cbd5d0be97f1e608684fe6d", "boatstack/references/host-hook-contracts.md": "2a89d44d0e418a53f2e3b6300fed957cdf878f45ea97ce24b55b66065f0eaa1d", "boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441", "boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae", - "boatstack/references/workflow.md": "5e43b1f82d9be55166f70606b34f20d1fdc61ebe1183338a405503d0975ec59a", + "boatstack/references/workflow.md": "b64c7d3168fa99055fdae798357110f11b496ce8a6f2a6237f17523ce6eeac48", "boatstack/release.go": "82dcb4ca59e8c79a68d5333d650f90e64abd448d04e0c6f504fdf07f42b5ed76", "boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690", "boatstack/repair_budget_conformance_test.go": "05793600dac06bbf39075b15bc1262a9bd1e916738d801dcd6ff515771262b4c", @@ -200,7 +200,7 @@ "boatstack/run.go": "3127e8c054b80e41413011e423ba9463fe06a7e6c9e1e71750ab98587871d89d", "boatstack/run_test.go": "5b291510fa90cefdc26eb89e18a3443385456a6ebc73408325ac1945b7c084d6", "boatstack/runtime.go": "cd77c34910de45ca382275df1a36ce39fe88497169618096009962d2bcb8f591", - "boatstack/runtime_cache.go": "3bff97f1a7db1389cecd00c5ac3584e5202d2caf9c6ea42a0ba427ab538aeac7", + "boatstack/runtime_cache.go": "6f6b023170cce982bf155e7c2fc7752cca2f7acff771967b4a523c1c13ea876f", "boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85", "boatstack/runtime_provenance_test.go": "1d52f1e6b0691cf4667729cc9b9f3c55c128f0aa3321f3a2843a9aa6fd0e73dc", "boatstack/safety.go": "f0af393b87bbc82d4b9fea3ebf40eee8674bed16b17aa0e9ea18e35be5810625", @@ -216,7 +216,7 @@ "boatstack/testdata/reviewer-pr-body.md": "4c64e3788e5d61a377aeb0f797f7fc8d2316ab6e49572d15636eea7ba9e34ac4", "boatstack/testdata/safety/safe_apply.py.txt": "c9ec7fb932cf21b6aa8df597c4d4c54d6ec65e796240e49118d699f583383975", "boatstack/testdata/safety/unsafe_apply.py.txt": "42db1751865cc15c4dd69a03146b5deca8f21f916d258e433b27bbef5f884ab1", - "boatstack/update.go": "042c7e8141423e2cdb71c92f93cf73cc81d916116d76ab9928c56cf18bc6827a", + "boatstack/update.go": "cebd3a3300055fe1fa7646e8c8182662a066771afd44e35cb5d64530f52e26e8", "boatstack/update_publication.go": "5c1ac8445345c6546d165b9321a736453b313ced96a0059465b8ad637498bac6", "boatstack/update_publication_test.go": "c5f32578db53be65e35452d5e8b4520884354e4e80a370dcec19860ea644d091", "boatstack/update_test.go": "bf5f19f8499db6dd7356917d867b113b790548ab89bfdea59e2adf4999a82a6a", @@ -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": "d5d34eee3d7af847ab4eabf3a7be21709b7b0619b44bde505f744aa6a337129c", + "docs/evidence-engineered-coding.md": "cae640d8fcb89edab69ecbee5148b336b4cb77c752cab79e6ce63925669876c3", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", "docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8", - "docs/public-claims.json": "78230b15a4c7716e2ebbd3b864795cbbad8c25e48e2f7c1e50e9ad97dae23947", + "docs/public-claims.json": "6f93df0664dbaf0fee6aed003559ac6b0e606753cafd76298d68623a6c5c9f2c", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", - "docs/troubleshooting.md": "321d79a990de3eac0931efbf5446a3fbdb5f8fce06319bf2adf1e7aabf763519", + "docs/troubleshooting.md": "10841d10b51d551cc49d6f9d1c7ee997a8aaf091a10e8f74a570b5cf445d6ada", "docs/validation-and-evidence.md": "e7d91ad49c6adb44784ebe7d94feceb6abd445857f9a0716f0758bf6b55296c5", "docs/why-these-steps.md": "cbe0d769db11ef15bb1dff888009378d6783776ad020e6f5139847a1dd62fa09", "install.ps1": "f48d0f26a26e806b780d10fa916c261ff9f84ab39758cf9e229f647836845e86", @@ -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": "7d5c5cb554ec51e856437b4f359e45cd8b584262a7aca50f2b0d5b855124549f", + "labs/diagram-json/plan.lock.json": "4e3130b0203c93077a3dfdaaf167dcd6adf50155d4477a41046f018c14461ddf", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -399,13 +399,14 @@ "release-notes/2026-07-30-plan-approved-scenarios-escalate-to-require.md": "21c3f2be51b834fc2eb7662236db2549e80b1a1a0665a0721e773b7736e2c079", "release-notes/2026-07-30-visual-attach-retry-prescription.md": "38b610685a7a62c0341ba21b273b42c24d82ca803de6d8f694754156eaa606a9", "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" }, "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "6149e9333caa3d936f69e03015252b712cf453ee", + "commit": "d950cb53d33f4f2571a62ce05e10a85a3da7756b", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/installation_repair.go b/boatstack/installation_repair.go index 1df959b..60119f8 100644 --- a/boatstack/installation_repair.go +++ b/boatstack/installation_repair.go @@ -62,22 +62,57 @@ type InstallationRepairResult struct { } func installedVersion(repo string) (string, error) { - for _, candidate := range []string{ - filepath.Join(repo, ".product-loop", "bin", "install.lock.json"), - filepath.Join(repo, ".product-loop", "generated.lock.json"), + var failures []string + type candidate struct { + label string + value []byte + err error + } + localPath := filepath.Join(repo, ".product-loop", "bin", "install.lock.json") + localValue, localErr := os.ReadFile(localPath) + committedValue, committedErr := exec.Command("git", "-C", repo, "show", "HEAD:.product-loop/generated.lock.json").Output() + for _, candidate := range []candidate{ + {label: "install.lock.json", value: localValue, err: localErr}, + {label: "committed generated.lock.json", value: committedValue, err: committedErr}, } { - value, err := os.ReadFile(candidate) - if err != nil { + if candidate.err != nil { continue } var identity struct { BoatstackVersion string `json:"boatstack_version"` + SourceCommit string `json:"source_commit"` + Runtime struct { + SourceCommit string `json:"source_commit"` + } `json:"runtime"` + } + if err := json.Unmarshal(candidate.value, &identity); err != nil { + failures = append(failures, candidate.label+" is malformed") + continue } - if json.Unmarshal(value, &identity) == nil && strings.TrimSpace(identity.BoatstackVersion) != "" { - return normalizedVersion(identity.BoatstackVersion) + if strings.TrimSpace(identity.BoatstackVersion) == "" { + failures = append(failures, candidate.label+" has no version") + continue + } + sourceCommit := strings.TrimSpace(identity.SourceCommit) + if sourceCommit == "" { + sourceCommit = strings.TrimSpace(identity.Runtime.SourceCommit) } + if sourceCommit == "" || strings.EqualFold(sourceCommit, "unknown") { + failures = append(failures, candidate.label+" has invalid source commit") + continue + } + version, err := normalizedVersion(identity.BoatstackVersion) + if err != nil { + failures = append(failures, candidate.label+" has invalid version") + continue + } + return version, nil + } + detail := strings.Join(failures, "; ") + if detail != "" { + detail = ": " + detail } - return "", fmt.Errorf("installed Boatstack version cannot be established from owned provenance") + return "", fmt.Errorf("installed Boatstack version cannot be established from owned provenance%s", detail) } func updateDirection(installed, target string) (string, error) { diff --git a/boatstack/installation_repair_test.go b/boatstack/installation_repair_test.go index aaf8025..d6281e5 100644 --- a/boatstack/installation_repair_test.go +++ b/boatstack/installation_repair_test.go @@ -297,6 +297,84 @@ func TestRepairPreservesIntegrationFallbackWhenInstallLockIsMissing(t *testing.T } } +func TestRepairFallsBackToCommittedPinWhenLocalProvenanceIsInvalid(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 }) + for name, value := range map[string][]byte{ + "development identity": []byte(`{"boatstack_version":"dev","source_commit":"unknown"}`), + "unknown source": []byte(`{"boatstack_version":"v0.4.0","source_commit":"unknown"}`), + "malformed identity": []byte("{\n"), + } { + t.Run(name, func(t *testing.T) { + repo, _ := updateInstalledRepo(t) + lockPath := filepath.Join(repo, ".product-loop", "bin", "install.lock.json") + if err := os.WriteFile(lockPath, value, 0o644); err != nil { + t.Fatal(err) + } + // Recovery authority comes from the committed pin, not a potentially + // drifted generated file in the worktree. + if err := os.WriteFile(filepath.Join(repo, ".product-loop", "generated.lock.json"), []byte("{\n"), 0o644); err != nil { + t.Fatal(err) + } + installed, err := installedVersion(repo) + if err != nil || installed != "v0.4.0" { + t.Fatalf("committed pin did not recover version identity: version=%q err=%v", installed, err) + } + config, _, err := LoadConfig(filepath.Join(repo, ".boatstack-project.json")) + if err != nil { + t.Fatal(err) + } + result, err := ClassifyInstallationRepair(repo, config.Adapters, false) + if err != nil { + t.Fatal(err) + } + if result.VerificationStatus != "REPAIR_AVAILABLE" || result.InstalledVersion != "v0.4.0" { + t.Fatalf("invalid local provenance was not safely repairable: %#v", result) + } + }) + } +} + +func TestDetachedUpdateRepairsInvalidLocalProvenanceEndToEnd(t *testing.T) { + now := time.Date(2026, 8, 1, 12, 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") + if err := os.WriteFile(filepath.Join(repo, ".product-loop", "bin", "install.lock.json"), []byte(`{"boatstack_version":"dev","source_commit":"unknown"}`), 0o644); err != nil { + t.Fatal(err) + } + Version = "v0.5.0" + SourceCommit = "update-test-0.5.0" + if err := RunUpdate(InitOptions{Repo: repo, Repair: true, Yes: true, Input: strings.NewReader(""), Output: &bytes.Buffer{}}); err != nil { + t.Fatal(err) + } + receipts, err := operationReceipts(repo) + if err != nil { + t.Fatal(err) + } + found := false + for _, receipt := range receipts { + if receipt.Kind == "install-update" && receipt.State == OperationSucceeded { + found = true + } + } + if !found { + t.Fatalf("detached repair did not persist a successful update operation: %#v", receipts) + } + directory, err := WorkspaceFor(repo).OperationDir() + if err != nil { + t.Fatal(err) + } + if strings.HasPrefix(directory, repo+string(filepath.Separator)) { + t.Fatalf("detached update receipt entered the repository: %s", directory) + } +} + 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 }) diff --git a/boatstack/operation.go b/boatstack/operation.go index 9e15f49..1f24cfe 100644 --- a/boatstack/operation.go +++ b/boatstack/operation.go @@ -133,24 +133,22 @@ func pruneLegacyOperationLedger(repo string) { _ = os.RemoveAll(legacy) } -func operationPath(repo, operationID string) (string, error) { +func operationOwnedPath(repo, operationID string) (controllerPath, error) { id, err := safeCacheSegment(operationID, "operation id") if err != nil { - return "", err - } - directory, err := operationDirectory(repo) - if err != nil { - return "", err + return controllerPath{}, err } - path := filepath.Join(directory, id+".json") - gitDir, err := worktreeGitDir(repo) + ctx := WorkspaceFor(repo) + directory, err := ctx.OperationDir() if err != nil { - return "", err + return controllerPath{}, err } - if err := rejectSymlinkComponents(gitDir, path); err != nil { - return "", err - } - return path, nil + return ctx.worktreeOwnedPath(filepath.Join(directory, id+".json")) +} + +func operationPath(repo, operationID string) (string, error) { + owned, err := operationOwnedPath(repo, operationID) + return owned.path, err } func operationID(kind, target, fingerprint string) string { @@ -223,18 +221,15 @@ func saveOperation(repo string, receipt OperationReceipt) error { } func withOperationLock(repo, id string, apply func() error) error { - path, err := operationPath(repo, id) + path, err := operationOwnedPath(repo, id) if err != nil { return err } - lock := strings.TrimSuffix(path, ".json") + ".lock" - gitDir, err := worktreeGitDir(repo) + lockPath, err := path.Sibling(strings.TrimSuffix(filepath.Base(path.path), ".json") + ".lock") if err != nil { return err } - if err := rejectSymlinkComponents(gitDir, lock); err != nil { - return err - } + lock := lockPath.path if err := os.MkdirAll(filepath.Dir(lock), 0o700); err != nil { return err } diff --git a/boatstack/operation_test.go b/boatstack/operation_test.go index b8b8f90..e934202 100644 --- a/boatstack/operation_test.go +++ b/boatstack/operation_test.go @@ -4,6 +4,7 @@ import ( "errors" "os" "path/filepath" + "runtime" "strings" "testing" "time" @@ -174,6 +175,69 @@ func preparedOperation(t *testing.T, repo, fingerprint, retryClass string, attem return receipt } +// control-law: controller-effects-use-the-owning-storage-boundary +func TestDetachedOperationLifecycleUsesExternalOwnedBoundary(t *testing.T) { + repo := detachedTestRepo(t, "https://github.com/acme/detached-operations.git") + if _, err := AttachDetached(AttachOptions{Repo: repo}); err != nil { + t.Fatal(err) + } + receipt := preparedOperation(t, repo, "detached-package", "ATOMIC_LOCAL", 1) + if receipt.State != OperationAuthorized { + t.Fatalf("operation was not authorized: %+v", receipt) + } + path, err := operationPath(repo, receipt.OperationID) + if err != nil { + t.Fatal(err) + } + ctx := WorkspaceFor(repo) + directory, err := ctx.OperationDir() + if err != nil { + t.Fatal(err) + } + if !strings.HasPrefix(path, directory+string(filepath.Separator)) || strings.HasPrefix(path, repo+string(filepath.Separator)) { + t.Fatalf("detached operation escaped its external ledger: %s", path) + } + if _, err := os.Stat(path); err != nil { + t.Fatalf("detached operation receipt was not written: %v", err) + } + gitDir, err := worktreeGitDir(repo) + if err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(gitDir, "boatstack", "operations", "v2", filepath.Base(path))); !os.IsNotExist(err) { + t.Fatalf("detached receipt also entered the Git directory: %v", err) + } +} + +// control-law: detached-owned-boundaries-reject-symlink-escapes +func TestDetachedOperationRejectsSymlinkedControllerPath(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlink creation requires privileges on Windows") + } + repo := detachedTestRepo(t, "https://github.com/acme/detached-operation-symlink.git") + if _, err := AttachDetached(AttachOptions{Repo: repo}); err != nil { + t.Fatal(err) + } + base, err := WorkspaceFor(repo).worktreeControlDir() + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(base, 0o700); err != nil { + t.Fatal(err) + } + if err := os.Symlink(t.TempDir(), filepath.Join(base, "operations")); err != nil { + t.Fatal(err) + } + _, err = PrepareOperation(OperationPrepareOptions{ + Repo: repo, Kind: "test-write", Target: "artifact.json", PackageFingerprint: "escape", + AuthorizationFingerprint: "approved", RetryClass: "ATOMIC_LOCAL", MaxAttempts: 1, + ExpectedPostcondition: "artifact exists", + }) + if err == nil || !strings.Contains(err.Error(), "symlinked path") { + t.Fatalf("symlinked detached operation path was not rejected: %v", err) + } +} + func TestOperationLifecycleAndReplayProtection(t *testing.T) { repo := operationTestRepo(t) receipt := preparedOperation(t, repo, "package-a", "ATOMIC_LOCAL", 2) diff --git a/boatstack/paths.go b/boatstack/paths.go index 4c278f9..6f587a6 100644 --- a/boatstack/paths.go +++ b/boatstack/paths.go @@ -1,6 +1,7 @@ package boatstack import ( + "fmt" "path/filepath" "sync" ) @@ -61,6 +62,59 @@ type WorkspaceContext struct { sharedControlRoot string } +// controllerPath carries a Boatstack-owned path together with the boundary that +// owns it. Effectful callers validate this value instead of independently +// choosing a repository, Git, or detached-state root. +type controllerPath struct { + path string + root string +} + +func newControllerPath(root, target string) (controllerPath, error) { + if root == "" || target == "" { + return controllerPath{}, fmt.Errorf("controller path ownership is incomplete") + } + owned := controllerPath{path: filepath.Clean(target), root: filepath.Clean(root)} + if err := owned.Validate(); err != nil { + return controllerPath{}, err + } + return owned, nil +} + +func (p controllerPath) Validate() error { + return rejectSymlinkComponents(p.root, p.path) +} + +// Sibling derives another target without losing the owning boundary. +func (p controllerPath) Sibling(name string) (controllerPath, error) { + if filepath.Base(name) != name || name == "." || name == ".." { + return controllerPath{}, fmt.Errorf("invalid controller path name: %s", name) + } + return newControllerPath(p.root, filepath.Join(filepath.Dir(p.path), name)) +} + +func (w WorkspaceContext) worktreeOwnedPath(target string) (controllerPath, error) { + if w.Mode == SupervisionDetached { + return newControllerPath(w.sharedControlRoot, target) + } + root, err := worktreeGitDir(w.RepoRoot) + if err != nil { + return controllerPath{}, err + } + return newControllerPath(root, target) +} + +func (w WorkspaceContext) sharedOwnedPath(target string) (controllerPath, error) { + if w.Mode == SupervisionDetached { + return newControllerPath(w.sharedControlRoot, target) + } + root, err := gitCommonDir(w.RepoRoot) + if err != nil { + return controllerPath{}, err + } + return newControllerPath(root, target) +} + // WorkspaceFor returns the resolver for a repository. It consults the external // attachment registry and returns a detached context when the repository is // attached and its binding verifies; otherwise it returns the embedded layout. diff --git a/boatstack/references/failure-moves.md b/boatstack/references/failure-moves.md index b1d2fe3..8895bc8 100644 --- a/boatstack/references/failure-moves.md +++ b/boatstack/references/failure-moves.md @@ -17,6 +17,8 @@ The `root-cause` operation operationalizes this taxonomy for a single bug: it cl | Review miss | Defect found after same-agent review | Independent reviewer; risk checklist; mechanical enforcement | Expensive review everywhere | | Scope drift | Diff no longer maps to approved outcomes | Re-scope; split PR; update spec with approval | Hiding product changes in implementation | | Update self-lockout | An installed helper, stale hook event, or damaged owned receipt blocks its own updater | Let the verified target helper classify state; migrate exact provenance automatically or offer fingerprinted `--repair` | Reinstalling blindly, overwriting user settings, or treating `--repair` as downgrade authority | +| Controller-root split | A detached controller path is resolved under external state, then an effectful caller independently validates it against the repository or Git directory and rejects its own owned path as an escape | Carry the target and its owning boundary as one typed value; derive child paths from it; make every effect validate that value; test embedded/detached and worktree/shared storage classes | Broadening the boundary to bypass validation, or letting another caller reconstruct the root independently | +| Recovery provenance self-dependency | A damaged local install lock is the first candidate used to decide whether that same lock may be repaired, so a development or malformed identity blocks the verified target helper before recovery begins | Treat the local lock as evidence when valid; otherwise derive the prior stable identity from the committed generated pin and let the verified target helper classify the exact owned repair | Trusting an uncommitted generated lock, inferring an arbitrary version, or overwriting mixed/user-owned state | | Ownership projection contradiction | Update admission classifies a path as Boatstack-owned, then final validation rejects the controller's own bounded mutation | Build one semantic ownership projection before execution; reuse it for admission, mutation, final verification, staging, and preview | Path-only allowlists accepting user content or independently maintained validators disagreeing after a side effect | | Security/tenancy | Trust boundary or data scope violated | Specialist review; invariant test; deny-by-default guard | Generic prompt mistaken for enforcement | | Integration/deploy | Local pass but runtime fails | Environment parity; canary; health checks; rollback | Treating staging as identical to production | diff --git a/boatstack/references/workflow.md b/boatstack/references/workflow.md index 0fc81de..a8b9b50 100644 --- a/boatstack/references/workflow.md +++ b/boatstack/references/workflow.md @@ -440,6 +440,8 @@ After successful publication only, the publisher may use the ignored 24-hour rel For an available version, create `chore/update-boatstack-v` and download and checksum-verify the target helper before consulting the installed runtime. The target helper classifies hook fragments, generated locks, helper provenance, and marker-bounded interceptors. Exact installed state migrates automatically. Recoverable owned drift is fingerprinted and, interactively, offered as **Repair Boatstack-owned state and continue the update? [y/N]**; noninteractive updates stop with one `--repair` retry. Repair backs up the exact paths in Git-common state and remains in the same update PR. User-owned, mixed, malformed, symlinked, or product state stays blocked. Downgrades require both `--repair` and `--allow-downgrade`. +If the ignored local install lock is missing, malformed, or carries a development identity, the verified target helper derives the prior stable version only from `HEAD:.product-loop/generated.lock.json`. That committed pin makes the local provenance path repairable without trusting drifted worktree bytes. Every mutable controller target is paired with its owning storage boundary: embedded worktree state uses the worktree Git directory, embedded shared state uses the Git common directory, and detached state uses the external Boatstack control root. Effectful callers validate the paired boundary and never reconstruct it from the repository path. + `update -binary ` installs the passed binary's **own self-reported version**, not the running helper's. Because each helper embeds its own version-bound generated bundle and compile-time constants, an older helper cannot correctly install a newer one in-process; when the passed binary self-reports a different identity, the whole update is re-executed by that binary so it installs itself — its bundle, constants, version-keyed shared-runtime slot, and durable receipt are then authoritative by construction, and the hand-off terminates in a single hop. The write boundary refuses to install a `-binary` whose self-report disagrees with the process running it, and re-hashes the freshly written slot against its manifest, rolling back on mismatch — so a runtime can never be labeled one version while carrying another's bytes. The runtime bytes never travel through Git — only the guard's baked version path and the committed version pin do — so a teammate who pulls a merged version bump, or clones fresh, starts with the new pointers but an **empty**, gitignored, version-keyed shared slot. Rather than fail-close every such teammate until they re-install by hand, the safety guard **auto-hydrates** an absent slot: it runs the tag-pinned, `.sha256`-verified installer in a branch-free, slot-only `hydrate` mode, serialized clone-wide by an atomic `mkdir` lock (peers wait briefly for the slot to appear) and bounded by a timeout, then falls through to the existing missing/symlink/manifest/checksum gates. Hydration is strictly additive: those gates remain the sole authority for execution and stay fail-closed, so a disabled, timed-out, or failed hydration simply denies — now with the exact one-line self-heal command embedded in the message. The `hydrate-runtime` helper subcommand it invokes rewrites no committed generated file and requires no dedicated branch; it refuses to populate a slot whose identity disagrees with the worktree's pin, and since the installer downloads the exact pinned version first, running equals installed by construction (the runtime-cache re-hash-and-rollback is the backstop). This is a deliberate posture change — the guard runs a fetched installer on cold start — bounded by tag pinning, HTTPS, sidecar verification, the guard's own checksum re-verify before `exec`, the clone-wide lock, the timeout, and the `BOATSTACK_AUTO_HYDRATE=0` kill switch (with a `BOATSTACK_HYDRATE_COMMAND` override). It never becomes a new authority for execution. diff --git a/boatstack/runtime_cache.go b/boatstack/runtime_cache.go index fe9a8c2..dd9deaf 100644 --- a/boatstack/runtime_cache.go +++ b/boatstack/runtime_cache.go @@ -94,24 +94,22 @@ func sharedRuntimeDirectory(repo, version, sourceCommit string) (string, error) } func sharedRuntimePaths(repo, version, sourceCommit string) (string, string, error) { - directory, err := sharedRuntimeDirectory(repo, version, sourceCommit) - if err != nil { - return "", "", err - } - return filepath.Join(directory, helperName()), filepath.Join(directory, "runtime.lock.json"), nil + binary, manifest, err := sharedRuntimeOwnedPaths(repo, version, sourceCommit) + return binary.path, manifest.path, err } -// runtimeSymlinkRoot returns the ownership boundary for the runtime selected by -// WorkspaceFor. Embedded runtimes live under the Git common directory; detached -// runtimes live under Boatstack's external control root. Keeping the check on the -// same side of that projection prevents detached hydration and doctor from -// rejecting their own external runtime as a repository escape. -func runtimeSymlinkRoot(repo string) (string, error) { +func sharedRuntimeOwnedPaths(repo, version, sourceCommit string) (controllerPath, controllerPath, error) { ctx := WorkspaceFor(repo) - if ctx.Mode == SupervisionDetached { - return ctx.sharedControlDir() + directory, err := sharedRuntimeDirectory(repo, version, sourceCommit) + if err != nil { + return controllerPath{}, controllerPath{}, err + } + binary, err := ctx.sharedOwnedPath(filepath.Join(directory, helperName())) + if err != nil { + return controllerPath{}, controllerPath{}, err } - return gitCommonDir(repo) + manifest, err := ctx.sharedOwnedPath(filepath.Join(directory, "runtime.lock.json")) + return binary, manifest, err } func atomicWriteMode(path string, content []byte, mode fs.FileMode) error { @@ -149,15 +147,11 @@ func atomicWriteMode(path string, content []byte, mode fs.FileMode) error { } func installSharedRuntime(source, repo string, integrations map[string]IntegrationState) (runtimeManifest, error) { - binaryPath, manifestPath, err := sharedRuntimePaths(repo, Version, SourceCommit) - if err != nil { - return runtimeManifest{}, err - } - root, err := runtimeSymlinkRoot(repo) + binaryPath, manifestPath, err := sharedRuntimeOwnedPaths(repo, Version, SourceCommit) if err != nil { return runtimeManifest{}, err } - return writeRuntimeSlot(source, root, binaryPath, manifestPath, integrations) + return writeRuntimeSlot(source, binaryPath, manifestPath, integrations) } // installDetachedRuntime populates a detached repository's external shared-runtime @@ -171,22 +165,19 @@ func installDetachedRuntime(repo, source string) (runtimeManifest, error) { if ctx.Mode != SupervisionDetached { return runtimeManifest{}, fmt.Errorf("installDetachedRuntime requires an attached detached repository") } - binaryPath, manifestPath, err := sharedRuntimePaths(repo, Version, SourceCommit) - if err != nil { - return runtimeManifest{}, err - } - root, err := ctx.sharedControlDir() + binaryPath, manifestPath, err := sharedRuntimeOwnedPaths(repo, Version, SourceCommit) if err != nil { return runtimeManifest{}, err } - return writeRuntimeSlot(source, root, binaryPath, manifestPath, nil) + return writeRuntimeSlot(source, binaryPath, manifestPath, nil) } // writeRuntimeSlot copies a helper binary and its manifest into a version-labeled -// runtime slot atomically, rejecting symlinked components under symlinkRoot and +// runtime slot atomically, rejecting symlinked components under each path's +// owning controller boundary and // verifying the written bytes against the manifest checksum. It is the shared core // of the embedded and detached runtime installers. -func writeRuntimeSlot(source, symlinkRoot, binaryPath, manifestPath string, integrations map[string]IntegrationState) (runtimeManifest, error) { +func writeRuntimeSlot(source string, binaryPath, manifestPath controllerPath, integrations map[string]IntegrationState) (runtimeManifest, error) { value, err := os.ReadFile(source) if err != nil { return runtimeManifest{}, err @@ -196,54 +187,51 @@ func writeRuntimeSlot(source, symlinkRoot, binaryPath, manifestPath string, inte Platform: platformKey(), BinarySHA256: SHA256Bytes(value), ReleaseChecksumsSHA256: ChecksumsSHA256, Integrations: integrations, } - for _, path := range []string{binaryPath, manifestPath} { - if err := rejectSymlinkComponents(symlinkRoot, path); err != nil { + for _, path := range []controllerPath{binaryPath, manifestPath} { + if err := path.Validate(); err != nil { return runtimeManifest{}, err } } // This exact provenance path is Boatstack-owned. A verified installer is the // repair surface for an interrupted or corrupted cache population, so it may // atomically replace the cached bytes after the symlink checks above. - if err := atomicWriteMode(binaryPath, value, 0o755); err != nil { + if err := atomicWriteMode(binaryPath.path, value, 0o755); err != nil { return runtimeManifest{}, err } encoded, err := MarshalJSON(manifest) if err != nil { return runtimeManifest{}, err } - if err := atomicWriteMode(manifestPath, encoded, 0o644); err != nil { + if err := atomicWriteMode(manifestPath.path, encoded, 0o644); err != nil { return runtimeManifest{}, err } // Post-write integrity: the bytes that landed in the version-labeled slot must // be exactly what the manifest attests. A mismatch means the atomic replace // raced or the slot was tampered mid-install; remove the slot rather than leave // a mislabeled runtime that would pass the checksum gate but drift at hydration. - writtenHash, err := SHA256File(binaryPath) + writtenHash, err := SHA256File(binaryPath.path) if err != nil { return runtimeManifest{}, err } if writtenHash != manifest.BinarySHA256 { - _ = os.Remove(binaryPath) - _ = os.Remove(manifestPath) - return runtimeManifest{}, fmt.Errorf("installed runtime failed post-write verification: %s does not match its manifest checksum", binaryPath) + _ = os.Remove(binaryPath.path) + _ = os.Remove(manifestPath.path) + return runtimeManifest{}, fmt.Errorf("installed runtime failed post-write verification: %s does not match its manifest checksum", binaryPath.path) } return manifest, nil } func loadSharedRuntime(repo string) (runtimeManifest, string, error) { - binaryPath, manifestPath, err := sharedRuntimePaths(repo, Version, SourceCommit) - if err != nil { - return runtimeManifest{}, "", err - } - root, err := runtimeSymlinkRoot(repo) + binaryOwned, manifestOwned, err := sharedRuntimeOwnedPaths(repo, Version, SourceCommit) if err != nil { return runtimeManifest{}, "", err } - for _, path := range []string{binaryPath, manifestPath} { - if err := rejectSymlinkComponents(root, path); err != nil { + for _, path := range []controllerPath{binaryOwned, manifestOwned} { + if err := path.Validate(); err != nil { return runtimeManifest{}, "", err } } + binaryPath, manifestPath := binaryOwned.path, manifestOwned.path value, err := os.ReadFile(manifestPath) if err != nil { return runtimeManifest{}, "", fmt.Errorf("shared Boatstack runtime is missing; run the verified installer once from any checkout in this Git clone: %w", err) diff --git a/boatstack/update.go b/boatstack/update.go index 89fa6da..876c8ee 100644 --- a/boatstack/update.go +++ b/boatstack/update.go @@ -339,7 +339,7 @@ func CheckExistingInstallProvenance(repo string) error { if err := DecodeJSON("check existing install provenance", path, value, &lock); err != nil { return err } - if _, err := parseStableVersion(lock.BoatstackVersion); err != nil || strings.TrimSpace(lock.SourceCommit) == "" { + if _, err := parseStableVersion(lock.BoatstackVersion); err != nil || strings.TrimSpace(lock.SourceCommit) == "" || strings.EqualFold(strings.TrimSpace(lock.SourceCommit), "unknown") { return fmt.Errorf("previous local install lock has invalid release provenance") } binaryPath, err := resolveRepositoryRelativePath(repo, lock.BinaryPath) diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index cf26a8d..a767caf 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -96,7 +96,7 @@ subject to acceptance criteria pass approval is current ``` -That is why context trimming is not automatically an optimization. If removing state increases rework or false acceptance, total cost rises. The canonical runtime references are approximately **22340 estimated tokens**, while host adapters point to one operation at a time. +That is why context trimming is not automatically an optimization. If removing state increases rework or false acceptance, total cost rises. The canonical runtime references are approximately **22771 estimated tokens**, while host adapters point to one operation at a time. ## Control appears at transitions @@ -146,6 +146,6 @@ Delivery and system improvement also remain separate. A failed task may suggest ## What is evidence-backed -The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`6149e9333caa3d936f69e03015252b712cf453ee`](https://github.com/operatorstack/intelligence-flow/tree/6149e9333caa3d936f69e03015252b712cf453ee/labs/12-product-engineering-loop). +The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`d950cb53d33f4f2571a62ce05e10a85a3da7756b`](https://github.com/operatorstack/intelligence-flow/tree/d950cb53d33f4f2571a62ce05e10a85a3da7756b/labs/12-product-engineering-loop). The evidence supports specific failure mechanisms and guardrails. It does not establish that Boatstack is optimal, that control-theory notation proves software quality, or that one workflow dominates every team. Those are evaluation questions, so the distribution preserves measurements, provenance, gaps, and negative results. diff --git a/docs/public-claims.json b/docs/public-claims.json index 1012b6e..c780f6f 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "6149e9333caa3d936f69e03015252b712cf453ee", + "source_commit": "d950cb53d33f4f2571a62ce05e10a85a3da7756b", "statuses": ["verified", "observed", "still_being_evaluated"], "claims": [ { @@ -12,7 +12,7 @@ "readable_evidence": "why-these-steps.md#portable-workflow-and-state", "implementation": ["../boatstack/export.go", "../boatstack/references/artifacts.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go"], - "last_verified_version": "source:6149e9333caa3d936f69e03015252b712cf453ee" + "last_verified_version": "source:d950cb53d33f4f2571a62ce05e10a85a3da7756b" }, { "id": "human-decisions", @@ -23,7 +23,7 @@ "readable_evidence": "why-these-steps.md#human-decisions", "implementation": ["../boatstack/references/workflow.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:6149e9333caa3d936f69e03015252b712cf453ee" + "last_verified_version": "source:d950cb53d33f4f2571a62ce05e10a85a3da7756b" }, { "id": "validation-provenance", @@ -34,7 +34,7 @@ "readable_evidence": "why-these-steps.md#validation-provenance", "implementation": ["validation-and-evidence.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go"], - "last_verified_version": "source:6149e9333caa3d936f69e03015252b712cf453ee" + "last_verified_version": "source:d950cb53d33f4f2571a62ce05e10a85a3da7756b" }, { "id": "irreversible-operations", @@ -46,7 +46,7 @@ "readable_evidence": "why-these-steps.md#irreversible-operations", "implementation": ["safety.md", "../boatstack/safety.go", "../boatstack/hooks.go"], "verification": ["../boatstack/safety_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:6149e9333caa3d936f69e03015252b712cf453ee" + "last_verified_version": "source:d950cb53d33f4f2571a62ce05e10a85a3da7756b" }, { "id": "reviewer-ready-pr", @@ -57,7 +57,7 @@ "readable_evidence": "why-these-steps.md#reviewer-ready-pr", "implementation": ["../boatstack/pr.go", "getting-started.md"], "verification": ["../boatstack/pr_test.go"], - "last_verified_version": "source:6149e9333caa3d936f69e03015252b712cf453ee" + "last_verified_version": "source:d950cb53d33f4f2571a62ce05e10a85a3da7756b" }, { "id": "phase-scoped-delivery", @@ -68,7 +68,7 @@ "readable_evidence": "why-these-steps.md#phase-scoped-delivery", "implementation": ["../boatstack/delivery.go", "../boatstack/safety.go", "../boatstack/hooks.go", "../boatstack/references/workflow.md"], "verification": ["../boatstack/delivery_test.go", "../boatstack/pr_test.go"], - "last_verified_version": "source:6149e9333caa3d936f69e03015252b712cf453ee" + "last_verified_version": "source:d950cb53d33f4f2571a62ce05e10a85a3da7756b" }, { "id": "model-neutral-contract", @@ -79,7 +79,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:6149e9333caa3d936f69e03015252b712cf453ee" + "last_verified_version": "source:d950cb53d33f4f2571a62ce05e10a85a3da7756b" }, { "id": "cross-model-failures", @@ -90,7 +90,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:6149e9333caa3d936f69e03015252b712cf453ee" + "last_verified_version": "source:d950cb53d33f4f2571a62ce05e10a85a3da7756b" }, { "id": "lower-cost-outcomes", @@ -101,7 +101,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:6149e9333caa3d936f69e03015252b712cf453ee" + "last_verified_version": "source:d950cb53d33f4f2571a62ce05e10a85a3da7756b" }, { "id": "git-worktree-activation", @@ -112,7 +112,7 @@ "readable_evidence": "why-these-steps.md#git-worktree-activation", "implementation": ["../boatstack/runtime_cache.go", "../boatstack/hooks.go"], "verification": ["../boatstack/runtime_cache_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:6149e9333caa3d936f69e03015252b712cf453ee" + "last_verified_version": "source:d950cb53d33f4f2571a62ce05e10a85a3da7756b" }, { "id": "visible-updates", @@ -123,7 +123,7 @@ "readable_evidence": "why-these-steps.md#visible-updates", "implementation": ["../boatstack/update.go", "../boatstack/init.go"], "verification": ["../boatstack/update_test.go", "../boatstack/init_test.go", "../boatstack/export_test.go"], - "last_verified_version": "source:6149e9333caa3d936f69e03015252b712cf453ee" + "last_verified_version": "source:d950cb53d33f4f2571a62ce05e10a85a3da7756b" } ] } diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 77d922d..38aea6c 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -135,6 +135,8 @@ Do not use `--repair` for user-owned or mixed changes. Move durable project cont Use the installer for the target release in update mode. It downloads and verifies the target helper before treating the installed helper's `doctor` result as diagnostic, so a missing helper or stale owned hook cannot disable recovery. Run `repair-status --repo . --json` to inspect the secret-free classification. Malformed host JSON, partial interceptor markers, symlinks, and unverifiable user content remain blocking and are never overwritten. +If the ignored local install lock says `dev`, has an unknown source commit, is malformed, or is absent, rerun the same verified target installer with `BOATSTACK_REPAIR=1`. The target helper recovers the prior stable version from the repository's committed generated pin and repairs only verified Boatstack-owned state. Do not delete repository files or move detached state by hand. Detached operation receipts and runtime slots are validated against their external Boatstack ownership root, so a path under `Application Support/boatstack` is not treated as a repository escape. + ## A tool call repeats or publication appears stuck Run `.product-loop/bin/boatstack-helper operation-status --repo . --json`. `EXECUTING` means the exact call already has a live lease, so wait instead of launching it again. `RECONCILE_REQUIRED` means Boatstack did not observe completion; verify the reported Git, GitHub, file, browser, or MCP postcondition before retrying. A successful operation whose response was lost is recovered from that observation. Do not reset the task, repeat a denied push, or open another PR. diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 0ca88b6..ac163b8 100644 --- a/labs/diagram-json/plan.lock.json +++ b/labs/diagram-json/plan.lock.json @@ -6,7 +6,7 @@ "plan_path": "labs/diagram-json/plan.md", "plan_sha256": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "schema_version": 1, - "source_commit": "6149e9333caa3d936f69e03015252b712cf453ee", + "source_commit": "d950cb53d33f4f2571a62ce05e10a85a3da7756b", "source_plan_path": "labs/diagram-json/source-plan.md", "source_plan_sha256": "e10593ddaa7522ab80cc991d0a09399257139799e37f737794cd49d68a39985b", "spec_path": "labs/diagram-json/spec.md", diff --git a/release-notes/2026-08-01-detached-update-recovery.md b/release-notes/2026-08-01-detached-update-recovery.md new file mode 100644 index 0000000..0b79085 --- /dev/null +++ b/release-notes/2026-08-01-detached-update-recovery.md @@ -0,0 +1,3 @@ +### Detached update recovery + +Boatstack updates can now recover from an invalid local development install lock by using the repository's committed stable pin as the prior release identity. Detached operation receipts and shared runtimes carry their external ownership boundary through validation, preventing the updater from rejecting its own controller state as a repository escape. Symlink and mixed-ownership checks remain fail closed.