diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ab354b..f12722c 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/052ce000ad11b193ea73e262592d59535043f73f/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/804141bc65d66fdb0a422a9c7c545a71180bffb1/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 24dedd5..4ce968a 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -12,7 +12,7 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "c7edd6bb227d0220f83ee5c00901723ed273ab144f3e1cf4bf77697ab63cbf72", + "CONTRIBUTING.md": "c7f3497bbe061860f2ecec201bbb9526f045585371c5f62db2b1b5ce17362bc5", "README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", @@ -50,7 +50,7 @@ "boatstack/config_documentation_test.go": "0632366edc5e88145bb080083ea03c6515da07b0162ce404d63e51bb5bc0774e", "boatstack/decision.go": "257ca328da6ae19ab252f10ee5d06bd7daf49dd8141d083ab1b32f106ea7a94c", "boatstack/decision_test.go": "1a92ff832610f9559bd47ccac7fc1755a8b4f8261c35bc72a092830dff05f7c0", - "boatstack/delivery.go": "772793e493b97348c198692a282035a40257b7ca8e21bdea66a881a5c805c22a", + "boatstack/delivery.go": "9bdcfecae7564c34a0d374ddbba4f237b241085db5c9d5bda6c4afb801055b35", "boatstack/delivery_boundary_conformance_test.go": "800cd722d8d2a696a0529e8343d3523e453bb052f0917c8a2cad2990296ac1b3", "boatstack/delivery_migrate.go": "7566e49f9c1838d4d563866e941c7aacd61ac918c9e886222282398d287ca780", "boatstack/delivery_migrate_conformance_test.go": "b8ba53681e1d0361ac62b06586c62b7763d55a65b5427976b5289e1fb1503bdc", @@ -84,7 +84,7 @@ "boatstack/init.go": "1b2721ad64dcfba3e954b97bb46218238873f46ec29fc4dac327aa99980f9cf6", "boatstack/init_test.go": "5fdf687205e7a5984a98a87336b7127e4ae9b651d57e21ec2dc8ca7e653ee602", "boatstack/init_transaction.go": "112456c4e1c4db54c4137bcf4f7a9a9e63399a6f5971e9b3dc952d0c4b2aa4b6", - "boatstack/installation_repair.go": "6574f7133a9644843c9260b9b9daede641a14438f7357bae42fb8ec188890446", + "boatstack/installation_repair.go": "b887f27023aff46636b0e38d16e18e3ab9a7f5c94079b2c152aecee4edc0eae5", "boatstack/installation_repair_test.go": "ae5a5ea1110836bd78cf20ade863a4d32cfd63d282559f92786f57b31869bd14", "boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c", "boatstack/internal/deliverycontrol/advise.go": "4f3a53a785a34f6c34858236a57d4114091141a463b51e5aaefae3336557ae5a", @@ -115,13 +115,13 @@ "boatstack/next.go": "39d813c96a6a5119efcabda0f59ce7c6faf91e5e76c77c7be999bf85f43de284", "boatstack/next_banner_test.go": "c431a6987ed1e479442fc9f5db4371632880b92aa790fa9dd0f5285293352c41", "boatstack/next_test.go": "d442d22023831ba39fcfbbf73f1a2da4170a83fc2aab5ce1b44f10cf9d88e173", - "boatstack/operation.go": "62f97bf2091f33eb2ca91915bf08bee73d53387611b673e849355bfd516ca467", - "boatstack/operation_test.go": "2d624eaba342b2c81b45cdf50918a65a9c002b5376a02041b24180658ee6a25a", + "boatstack/operation.go": "35142d24e166bc400229d233757b6ab185f94b01a3c11be49da33b7f123189a2", + "boatstack/operation_test.go": "59d3dc37319aa4d334c0cacbe886e2f757842e6a28dee8781448e528fecbde11", "boatstack/plan.go": "7209de3a97b134cd6e5224cf6e798b5af47b0a4163ae2f81a8ec0c1ff84031d6", "boatstack/plan_test.go": "53477515165a910910b9175bfa33574548cf0d0f3be48e175ec3a775a12d30bb", "boatstack/plan_validation.go": "412f06750832fe46f01190ea5e475fc6f6ea59c8ba78131f94ec031053a405d2", "boatstack/plan_validation_test.go": "6cbde4ac719baef6b73aa569515d6a9daadcbf14b33f76fa78159826954e20fa", - "boatstack/planning.go": "ef4507a9fecc900f0691372c50883f328c9232c3dfd6986fbde26fb7ef436ae3", + "boatstack/planning.go": "6dfeb802a7cb615f159d87c86f338eab0897bda836417b07f8307695373c78ff", "boatstack/planning_test.go": "c105a9c78c342be06614bf54d0bc1b661b0f7af64d63b79e43bd1fcc2769edd5", "boatstack/pr.go": "eae93f7a6e423f67336545d37181ec2ea350991a2d75985f1cf2cf4b7bc8382d", "boatstack/pr_test.go": "7f82954d94c1ceae848a581dda25e58af92251d78a5a94ed2d672bedf5a0349e", @@ -148,7 +148,7 @@ "boatstack/run.go": "74967ad5b3ed3847baffec1231aae69a81a70f9cce3a9412fa05bcfdc4eca6d1", "boatstack/run_test.go": "5b291510fa90cefdc26eb89e18a3443385456a6ebc73408325ac1945b7c084d6", "boatstack/runtime.go": "d1e95895002ea2b27199b6e05b33c4c6e20f63455a44f63ca3cfeedecfc23420", - "boatstack/runtime_cache.go": "e40c8c43f410d781a7a4e7ebf68a1caa597ea9005190fd6cea02884f863e091e", + "boatstack/runtime_cache.go": "89834409b426dce292fd810a091de1433fefbfba9249d8c1e31ccc40f0d5dbd6", "boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85", "boatstack/runtime_provenance_test.go": "1d52f1e6b0691cf4667729cc9b9f3c55c128f0aa3321f3a2843a9aa6fd0e73dc", "boatstack/safety.go": "24df06e3c0f5dd54ee361ebf7bfd2deca7b18193c2d58d1eb9331c012d342eda", @@ -161,7 +161,7 @@ "boatstack/testdata/safety/safe_apply.py.txt": "c9ec7fb932cf21b6aa8df597c4d4c54d6ec65e796240e49118d699f583383975", "boatstack/testdata/safety/unsafe_apply.py.txt": "42db1751865cc15c4dd69a03146b5deca8f21f916d258e433b27bbef5f884ab1", "boatstack/update.go": "042c7e8141423e2cdb71c92f93cf73cc81d916116d76ab9928c56cf18bc6827a", - "boatstack/update_publication.go": "c8b7bd38019b1cbf8c523652e9e20e8c971c7e48b2f3972a0ac638648326fe63", + "boatstack/update_publication.go": "7cba2e993700190bcc7c3c1cb1a5fe4df84b2053f35bb4fa5e4b0673f397bca2", "boatstack/update_publication_test.go": "c5f32578db53be65e35452d5e8b4520884354e4e80a370dcec19860ea644d091", "boatstack/update_test.go": "bf5f19f8499db6dd7356917d867b113b790548ab89bfdea59e2adf4999a82a6a", "boatstack/visual_evidence.go": "4d69f98adb6087d4830e6703273ae6e03b28ec8b3c496a6e432fd5e0e54d8a2f", @@ -176,10 +176,10 @@ "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", "docs/configuration.md": "df054f49d532c8b1b7d94184810d1b3b5bf18cdc30eb985b4b6d0639162e341a", - "docs/evidence-engineered-coding.md": "61449fce7dd97bdbe69e2cdbd5cdf53ee9bae52dfe6e1ce51e377007ba2f4c72", + "docs/evidence-engineered-coding.md": "8c9ac13f925f67db325db9163a1384aa01591d17af82e47e7447922585f62e16", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", "docs/getting-started.md": "1dd4f4e2e636cc5adfc2f79939629701e171087c3d5e558cf919548b9224adfd", - "docs/public-claims.json": "35c12ea50b1ae2a8403adcdd8e6e849e079a60f13822faa86bddff6543842010", + "docs/public-claims.json": "16d2327409e3bdaefab872aa46abeb88328a8ec263302ed0a4d5d48df431a392", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -193,7 +193,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": "b8249b7cea66cfba8b42b64551cdf4ca6d71ac7c4979a55e1fede27479cbd3e1", + "labs/diagram-json/plan.lock.json": "59951495e1695536035673cac5c799a174d43ac1091e455d4cf4a0e29c9f9763", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -289,6 +289,7 @@ "release-notes/2026-07-25-deliverycontrol-shadow-registry.md": "e7f8ca4e4f188eda3088e46cba77369d8ff0d903f29e43846103d986e79a2273", "release-notes/2026-07-25-evidence-path-resolution.md": "b32cb8a6e69f397f751c3a7fb62be254a7407a28bed25ae9108d6d773c863d11", "release-notes/2026-07-25-guard-hydration-serialization.md": "0bf43284b8063011d837dafb10dc77fda496133c14a31ee196dc0dd0e4ffeec4", + "release-notes/2026-07-25-per-worktree-operation-ledger.md": "d0d4495fe4406cf67e7475032e3fc9eb74ed02a3e68f4928c086b5518422b46c", "release-notes/2026-07-25-published-slice-correction-routing.md": "129cdd62c80c8b93060726027d68ba3abdb0bca1a1ce9e64d6053271af3fd082", "release-notes/2026-07-25-readme-simplified-technical-english.md": "c362f46702c38dda6b0301d05b95a067da617d170ddfcca22fd7eb9f6e2c1881", "release-notes/2026-07-25-release-notes-simplified-technical-english.md": "70b273cbeb5ee46c49c10541540f31e8ca67a71102acfd47ae15451856c651db", @@ -299,7 +300,7 @@ "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "052ce000ad11b193ea73e262592d59535043f73f", + "commit": "804141bc65d66fdb0a422a9c7c545a71180bffb1", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/delivery.go b/boatstack/delivery.go index d25c0d6..39f6777 100644 --- a/boatstack/delivery.go +++ b/boatstack/delivery.go @@ -280,17 +280,11 @@ func deliveryDefinitions(plan map[string]any) ([]DeliverySlice, error) { } func deliveryStateDirectory(repo string) (string, error) { - gitDirectory := gitOutput(repo, "rev-parse", "--path-format=absolute", "--git-dir") - if gitDirectory == "" { - gitDirectory = gitOutput(repo, "rev-parse", "--git-dir") - } - if gitDirectory == "" { - return "", fmt.Errorf("cannot resolve the Git worktree directory") - } - if !filepath.IsAbs(gitDirectory) { - gitDirectory = filepath.Join(repo, gitDirectory) + gitDir, err := worktreeGitDir(repo) + if err != nil { + return "", err } - return filepath.Join(filepath.Clean(gitDirectory), "boatstack", "deliveries"), nil + return filepath.Join(gitDir, "boatstack", "deliveries"), nil } func deliveryStatePath(repo, feature string) (string, error) { diff --git a/boatstack/installation_repair.go b/boatstack/installation_repair.go index 91d1138..3527d26 100644 --- a/boatstack/installation_repair.go +++ b/boatstack/installation_repair.go @@ -404,12 +404,17 @@ func repairOwnedPaths(result InstallationRepairResult) map[string]bool { } func writeInstallationRepairBackup(repo string, result InstallationRepairResult) (string, error) { - common, err := gitCommonDir(repo) + // Repair backups capture this worktree's pre-repair working-tree bytes. Two + // worktrees repairing to the same version share a package fingerprint but hold + // different pre-repair content, so a clone-shared directory would let one + // clobber the other's restore point. Key the backup and its receipt by the + // per-worktree Git directory instead, and keep them co-located. + gitDir, err := worktreeGitDir(repo) if err != nil { return "", err } - directory := filepath.Join(common, "boatstack", "repair-backups", result.PackageFingerprint) - if err := rejectSymlinkComponents(common, directory); err != nil { + directory := filepath.Join(gitDir, "boatstack", "repair-backups", result.PackageFingerprint) + if err := rejectSymlinkComponents(gitDir, directory); err != nil { return "", err } if err := os.MkdirAll(directory, 0o700); err != nil { @@ -449,7 +454,7 @@ func writeInstallationRepairBackup(repo string, result InstallationRepairResult) if err != nil { return "", err } - receiptPath := filepath.Join(common, "boatstack", "updates", version, "repair.json") + receiptPath := filepath.Join(gitDir, "boatstack", "updates", version, "repair.json") if err := atomicWriteMode(receiptPath, receiptValue, 0o600); err != nil { return "", err } @@ -457,7 +462,7 @@ func writeInstallationRepairBackup(repo string, result InstallationRepairResult) } func loadInstallationRepairReceipt(repo, version string) (*InstallationRepairResult, error) { - common, err := gitCommonDir(repo) + gitDir, err := worktreeGitDir(repo) if err != nil { return nil, err } @@ -465,7 +470,7 @@ func loadInstallationRepairReceipt(repo, version string) (*InstallationRepairRes if err != nil { return nil, err } - path := filepath.Join(common, "boatstack", "updates", segment, "repair.json") + path := filepath.Join(gitDir, "boatstack", "updates", segment, "repair.json") value, err := os.ReadFile(path) if os.IsNotExist(err) { return nil, nil diff --git a/boatstack/operation.go b/boatstack/operation.go index f363760..f2432ec 100644 --- a/boatstack/operation.go +++ b/boatstack/operation.go @@ -108,12 +108,33 @@ func operationTimestamp() string { return operationNow().UTC().Truncate(time.Second).Format(time.RFC3339) } +// operationDirectory holds the operation ledger. It is per-worktree: an operation +// records a mutation performed by one worktree at one time, so worktree A's ledger +// must never sit on worktree B's read/write path (a shared ledger produced false +// "operation identity does not match" blocks across worktrees). The "v2" segment +// is intentional: the main worktree's Git directory aliases the common directory, +// so bumping the version cleanly orphans the legacy clone-shared "v1" ledger for +// every worktree — including main — instead of silently inheriting its receipts. func operationDirectory(repo string) (string, error) { - common, err := gitCommonDir(repo) + gitDir, err := worktreeGitDir(repo) if err != nil { return "", err } - return filepath.Join(common, "boatstack", "operations", "v1"), nil + return filepath.Join(gitDir, "boatstack", "operations", "v2"), nil +} + +// pruneLegacyOperationLedger removes the pre-isolation clone-shared "v1" ledger +// under the Git common directory. Once operations moved to the per-worktree "v2" +// ledger those receipts are orphaned for every worktree (including main, whose +// Git directory aliases the common directory). This is best-effort hygiene: the +// v2 path already guarantees correctness, so any failure here is ignored. +func pruneLegacyOperationLedger(repo string) { + common, err := gitCommonDir(repo) + if err != nil { + return + } + legacy := filepath.Join(common, "boatstack", "operations", "v1") + _ = os.RemoveAll(legacy) } func operationPath(repo, operationID string) (string, error) { @@ -126,11 +147,11 @@ func operationPath(repo, operationID string) (string, error) { return "", err } path := filepath.Join(directory, id+".json") - common, err := gitCommonDir(repo) + gitDir, err := worktreeGitDir(repo) if err != nil { return "", err } - if err := rejectSymlinkComponents(common, path); err != nil { + if err := rejectSymlinkComponents(gitDir, path); err != nil { return "", err } return path, nil @@ -211,11 +232,11 @@ func withOperationLock(repo, id string, apply func() error) error { return err } lock := strings.TrimSuffix(path, ".json") + ".lock" - common, err := gitCommonDir(repo) + gitDir, err := worktreeGitDir(repo) if err != nil { return err } - if err := rejectSymlinkComponents(common, lock); err != nil { + if err := rejectSymlinkComponents(gitDir, lock); err != nil { return err } if err := os.MkdirAll(filepath.Dir(lock), 0o700); err != nil { diff --git a/boatstack/operation_test.go b/boatstack/operation_test.go index 0ed3401..b8b8f90 100644 --- a/boatstack/operation_test.go +++ b/boatstack/operation_test.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" "strings" - "sync" "testing" "time" ) @@ -264,41 +263,70 @@ func TestExpiredLeaseBecomesUnknownAndBudgetPersists(t *testing.T) { } } -func TestOperationReceiptsAreSharedAcrossLinkedWorktreesAndSerialized(t *testing.T) { +// TestOperationLedgerIsolatedPerWorktree encodes the deliberate contract reversal +// from the pre-isolation design: the operation ledger is now per-worktree, so one +// worktree neither observes nor blocks another worktree's operations. Two worktrees +// running the same operation identity each hold their own receipt and their own lock. +func TestOperationLedgerIsolatedPerWorktree(t *testing.T) { repo := operationTestRepo(t) linked := filepath.Join(t.TempDir(), "linked") runGit(t, repo, "worktree", "add", "-b", "linked-test", linked) + + // An operation prepared in the main worktree is invisible in a linked worktree: + // the linked worktree cannot resolve it from its own separate ledger. receipt := preparedOperation(t, repo, "package-d", "ATOMIC_LOCAL", 2) - status, err := ResolveOperationStatus(linked, receipt.OperationID) - if err != nil || status.Operation == nil || status.Operation.OperationID != receipt.OperationID { - t.Fatalf("linked worktree did not observe shared operation: %+v %v", status, err) + if _, err := ResolveOperationStatus(linked, receipt.OperationID); err == nil { + t.Fatal("linked worktree observed another worktree's operation ledger") + } + + // The same identity begins independently in each worktree — no cross-worktree + // in-flight serialization and no identity collision. + if _, err := BeginOperation(repo, receipt.OperationID, "main-attempt", "Write"); err != nil { + t.Fatalf("main worktree could not begin its own operation: %v", err) + } + linkedReceipt := preparedOperation(t, linked, "package-d", "ATOMIC_LOCAL", 2) + if linkedReceipt.OperationID != receipt.OperationID { + t.Fatalf("same identity produced different operation ids: %s vs %s", linkedReceipt.OperationID, receipt.OperationID) } + if _, err := BeginOperation(linked, linkedReceipt.OperationID, "linked-attempt", "Write"); err != nil { + t.Fatalf("linked worktree blocked by another worktree's in-flight operation: %v", err) + } +} + +// TestSameVersionUpdateFromTwoWorktreesDoesNotCollide reproduces the reported +// incident: a same-version install-update prepared from a second worktree used to +// fail with "existing operation identity does not match the prepared package" +// because the clone-shared ledger compared a differing scope. Per-worktree ledgers +// make each prepare independent. +func TestSameVersionUpdateFromTwoWorktreesDoesNotCollide(t *testing.T) { + repo := operationTestRepo(t) + linked := filepath.Join(t.TempDir(), "linked") + runGit(t, repo, "worktree", "add", "-b", "update-b", linked) - var wait sync.WaitGroup - errorsSeen := make(chan error, 2) - for _, root := range []string{repo, linked} { - wait.Add(1) - go func(path string) { - defer wait.Done() - _, beginErr := BeginOperation(path, receipt.OperationID, "same-attempt", "Write") - errorsSeen <- beginErr - }(root) - } - wait.Wait() - close(errorsSeen) - successes, inFlight := 0, 0 - for beginErr := range errorsSeen { - switch { - case beginErr == nil: - successes++ - case errors.Is(beginErr, ErrOperationInFlight): - inFlight++ - default: - t.Fatalf("unexpected concurrent begin error: %v", beginErr) + options := func(root, scopeWorktree, branch string) OperationPrepareOptions { + return OperationPrepareOptions{ + Repo: root, + Kind: "install-update", + Target: "boatstack-install:v9.9.9", + PackageFingerprint: "fp-999", + AuthorizationFingerprint: "auth-999", + RetryClass: "ATOMIC_LOCAL", + MaxAttempts: 2, + ExpectedPostcondition: "generated runtime matches the pinned release", + Scope: OperationScope{Worktree: scopeWorktree, HeadBranch: branch}, } } - if successes != 1 || inFlight != 1 { - t.Fatalf("operation lock admitted %d executions and %d in-flight reports", successes, inFlight) + + a, err := PrepareOperation(options(repo, "update-a", "chore/update-boatstack-v9.9.9")) + if err != nil { + t.Fatalf("worktree A prepare failed: %v", err) + } + b, err := PrepareOperation(options(linked, "update-b", "chore/update-boatstack-v9.9.9")) + if err != nil { + t.Fatalf("worktree B blocked by worktree A's operation: %v", err) + } + if a.OperationID != b.OperationID { + t.Fatalf("expected identical operation ids across worktrees, got %s vs %s", a.OperationID, b.OperationID) } } diff --git a/boatstack/planning.go b/boatstack/planning.go index e08b25c..ab4b89c 100644 --- a/boatstack/planning.go +++ b/boatstack/planning.go @@ -332,6 +332,9 @@ func Doctor(repoPath string) error { if err := CheckExport(repo, bundle.Files); err != nil { return err } + // Best-effort hygiene: drop the orphaned clone-shared operation ledger left by + // pre-isolation versions. Never fails doctor. + pruneLegacyOperationLedger(repo) if err := CheckHostHooks(repo, config.Adapters); err != nil { return err } diff --git a/boatstack/runtime_cache.go b/boatstack/runtime_cache.go index 76b8c30..2d5cec3 100644 --- a/boatstack/runtime_cache.go +++ b/boatstack/runtime_cache.go @@ -64,6 +64,31 @@ func gitCommonDir(repo string) (string, error) { return filepath.Clean(absolute), nil } +// worktreeGitDir resolves the per-worktree Git directory (".git" for the main +// worktree, ".git/worktrees/" for a linked worktree). Per-worktree mutable +// state (operations, repair backups, deliveries) lives here so one worktree never +// reads or writes another worktree's record-of-work, and a removed worktree takes +// its state with it. Contrast gitCommonDir, which is shared clone-wide and is the +// right home only for immutable, version-namespaced content (runtime binaries, +// staged update packages). +func worktreeGitDir(repo string) (string, error) { + value := gitOutput(repo, "rev-parse", "--path-format=absolute", "--git-dir") + if value == "" { + value = gitOutput(repo, "rev-parse", "--git-dir") + } + if value == "" { + return "", fmt.Errorf("cannot resolve the Git worktree directory") + } + if !filepath.IsAbs(value) { + value = filepath.Join(repo, value) + } + absolute, err := filepath.Abs(value) + if err != nil { + return "", err + } + return filepath.Clean(absolute), nil +} + func sharedRuntimeDirectory(repo, version, sourceCommit string) (string, error) { version, err := safeCacheSegment(version, "Boatstack version") if err != nil { diff --git a/boatstack/update_publication.go b/boatstack/update_publication.go index 6a5db26..83618df 100644 --- a/boatstack/update_publication.go +++ b/boatstack/update_publication.go @@ -208,7 +208,7 @@ func PrepareUpdatePublication(repoPath, requestedVersion string) (UpdatePublicat } body := "## Why this change\n\nUpdate the repository-owned Boatstack infrastructure to " + version + ".\n\n## What changed\n\nOnly the fingerprinted Boatstack-generated files, host hooks, runtime provenance, and preserved integration state in this update package.\n" if repairReceipt != nil { - body += "\nThe update also repairs fingerprinted Boatstack-owned control state. Repair package: `" + repairReceipt.PackageFingerprint + "`. The pre-repair files are retained in ignored Git-common state at `" + repairReceipt.BackupPath + "`.\n" + body += "\nThe update also repairs fingerprinted Boatstack-owned control state. Repair package: `" + repairReceipt.PackageFingerprint + "`. The pre-repair files are retained in ignored per-worktree Git state at `" + repairReceipt.BackupPath + "`.\n" } body += "\n## Verification\n\n- Boatstack doctor passed after installation.\n- Generated-file and hook projections are validated by the update transaction.\n\n## Rollback\n\nRevert this infrastructure-only commit and rerun the previously pinned installer.\n" preview := UpdatePublicationPreview{ diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index b4188f0..3dfa759 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -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 [`052ce000ad11b193ea73e262592d59535043f73f`](https://github.com/operatorstack/intelligence-flow/tree/052ce000ad11b193ea73e262592d59535043f73f/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 [`804141bc65d66fdb0a422a9c7c545a71180bffb1`](https://github.com/operatorstack/intelligence-flow/tree/804141bc65d66fdb0a422a9c7c545a71180bffb1/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 204e532..be35666 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "052ce000ad11b193ea73e262592d59535043f73f", + "source_commit": "804141bc65d66fdb0a422a9c7c545a71180bffb1", "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:052ce000ad11b193ea73e262592d59535043f73f" + "last_verified_version": "source:804141bc65d66fdb0a422a9c7c545a71180bffb1" }, { "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:052ce000ad11b193ea73e262592d59535043f73f" + "last_verified_version": "source:804141bc65d66fdb0a422a9c7c545a71180bffb1" }, { "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:052ce000ad11b193ea73e262592d59535043f73f" + "last_verified_version": "source:804141bc65d66fdb0a422a9c7c545a71180bffb1" }, { "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:052ce000ad11b193ea73e262592d59535043f73f" + "last_verified_version": "source:804141bc65d66fdb0a422a9c7c545a71180bffb1" }, { "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:052ce000ad11b193ea73e262592d59535043f73f" + "last_verified_version": "source:804141bc65d66fdb0a422a9c7c545a71180bffb1" }, { "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:052ce000ad11b193ea73e262592d59535043f73f" + "last_verified_version": "source:804141bc65d66fdb0a422a9c7c545a71180bffb1" }, { "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:052ce000ad11b193ea73e262592d59535043f73f" + "last_verified_version": "source:804141bc65d66fdb0a422a9c7c545a71180bffb1" }, { "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:052ce000ad11b193ea73e262592d59535043f73f" + "last_verified_version": "source:804141bc65d66fdb0a422a9c7c545a71180bffb1" }, { "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:052ce000ad11b193ea73e262592d59535043f73f" + "last_verified_version": "source:804141bc65d66fdb0a422a9c7c545a71180bffb1" }, { "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:052ce000ad11b193ea73e262592d59535043f73f" + "last_verified_version": "source:804141bc65d66fdb0a422a9c7c545a71180bffb1" }, { "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:052ce000ad11b193ea73e262592d59535043f73f" + "last_verified_version": "source:804141bc65d66fdb0a422a9c7c545a71180bffb1" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index d470518..ac85cbc 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": "052ce000ad11b193ea73e262592d59535043f73f", + "source_commit": "804141bc65d66fdb0a422a9c7c545a71180bffb1", "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-07-25-per-worktree-operation-ledger.md b/release-notes/2026-07-25-per-worktree-operation-ledger.md new file mode 100644 index 0000000..9612020 --- /dev/null +++ b/release-notes/2026-07-25-per-worktree-operation-ledger.md @@ -0,0 +1,29 @@ +### One worktree no longer blocks Boatstack work in another worktree + +Boatstack keeps an operation ledger. The ledger records each mutation, such as a version +update or a PR publish. The ledger used to live in the shared Git common directory. Every +worktree in the clone wrote to the same ledger. + +This caused false blocks across worktrees. An update that succeeded in one worktree left a +receipt. Running the same-version update from another worktree found that receipt, compared a +different worktree scope, and denied the command with "existing operation identity does not +match the prepared package". The shared ledger also grew without bound, because it collected +every worktree's receipts. + +The operation ledger is now per-worktree. It lives under each worktree's own Git directory, +next to the delivery state that is already per-worktree. One worktree no longer sees or +blocks another worktree's operations. Two worktrees can run the same-version update at the +same time; each keeps its own receipt. Repair backups and their receipt move per-worktree +for the same reason, so two worktrees repairing the same version cannot overwrite each +other's restore point. + +The shared runtime binary and the staged update package stay shared and version-namespaced. +They are immutable content, so sharing them across worktrees is safe and saves disk. Git +already forbids the same branch in two worktrees, which keeps shared per-version state and +remote PRs free of contention. + +This is a deliberate break from the previous shared-and-serialized ledger. The ledger path +moves to a new version, so the old shared ledger is orphaned cleanly for every worktree, +including the main worktree. Doctor removes the orphaned ledger as best-effort hygiene. Old +Boatstack versions keep using their own installed binary and are not affected. A removed +worktree now takes its operation state with it.