Skip to content

Commit bb746e8

Browse files
Sync Boatstack from Intelligence Flow Labs @ 804141bc65d6
1 parent 5a326d0 commit bb746e8

13 files changed

Lines changed: 184 additions & 78 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/052ce000ad11b193ea73e262592d59535043f73f/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/804141bc65d66fdb0a422a9c7c545a71180bffb1/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: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"files": {
1414
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
15-
"CONTRIBUTING.md": "c7edd6bb227d0220f83ee5c00901723ed273ab144f3e1cf4bf77697ab63cbf72",
15+
"CONTRIBUTING.md": "c7f3497bbe061860f2ecec201bbb9526f045585371c5f62db2b1b5ce17362bc5",
1616
"README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad",
1717
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
1818
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
@@ -50,7 +50,7 @@
5050
"boatstack/config_documentation_test.go": "0632366edc5e88145bb080083ea03c6515da07b0162ce404d63e51bb5bc0774e",
5151
"boatstack/decision.go": "257ca328da6ae19ab252f10ee5d06bd7daf49dd8141d083ab1b32f106ea7a94c",
5252
"boatstack/decision_test.go": "1a92ff832610f9559bd47ccac7fc1755a8b4f8261c35bc72a092830dff05f7c0",
53-
"boatstack/delivery.go": "772793e493b97348c198692a282035a40257b7ca8e21bdea66a881a5c805c22a",
53+
"boatstack/delivery.go": "9bdcfecae7564c34a0d374ddbba4f237b241085db5c9d5bda6c4afb801055b35",
5454
"boatstack/delivery_boundary_conformance_test.go": "800cd722d8d2a696a0529e8343d3523e453bb052f0917c8a2cad2990296ac1b3",
5555
"boatstack/delivery_migrate.go": "7566e49f9c1838d4d563866e941c7aacd61ac918c9e886222282398d287ca780",
5656
"boatstack/delivery_migrate_conformance_test.go": "b8ba53681e1d0361ac62b06586c62b7763d55a65b5427976b5289e1fb1503bdc",
@@ -84,7 +84,7 @@
8484
"boatstack/init.go": "1b2721ad64dcfba3e954b97bb46218238873f46ec29fc4dac327aa99980f9cf6",
8585
"boatstack/init_test.go": "5fdf687205e7a5984a98a87336b7127e4ae9b651d57e21ec2dc8ca7e653ee602",
8686
"boatstack/init_transaction.go": "112456c4e1c4db54c4137bcf4f7a9a9e63399a6f5971e9b3dc952d0c4b2aa4b6",
87-
"boatstack/installation_repair.go": "6574f7133a9644843c9260b9b9daede641a14438f7357bae42fb8ec188890446",
87+
"boatstack/installation_repair.go": "b887f27023aff46636b0e38d16e18e3ab9a7f5c94079b2c152aecee4edc0eae5",
8888
"boatstack/installation_repair_test.go": "ae5a5ea1110836bd78cf20ade863a4d32cfd63d282559f92786f57b31869bd14",
8989
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
9090
"boatstack/internal/deliverycontrol/advise.go": "4f3a53a785a34f6c34858236a57d4114091141a463b51e5aaefae3336557ae5a",
@@ -115,13 +115,13 @@
115115
"boatstack/next.go": "39d813c96a6a5119efcabda0f59ce7c6faf91e5e76c77c7be999bf85f43de284",
116116
"boatstack/next_banner_test.go": "c431a6987ed1e479442fc9f5db4371632880b92aa790fa9dd0f5285293352c41",
117117
"boatstack/next_test.go": "d442d22023831ba39fcfbbf73f1a2da4170a83fc2aab5ce1b44f10cf9d88e173",
118-
"boatstack/operation.go": "62f97bf2091f33eb2ca91915bf08bee73d53387611b673e849355bfd516ca467",
119-
"boatstack/operation_test.go": "2d624eaba342b2c81b45cdf50918a65a9c002b5376a02041b24180658ee6a25a",
118+
"boatstack/operation.go": "35142d24e166bc400229d233757b6ab185f94b01a3c11be49da33b7f123189a2",
119+
"boatstack/operation_test.go": "59d3dc37319aa4d334c0cacbe886e2f757842e6a28dee8781448e528fecbde11",
120120
"boatstack/plan.go": "7209de3a97b134cd6e5224cf6e798b5af47b0a4163ae2f81a8ec0c1ff84031d6",
121121
"boatstack/plan_test.go": "53477515165a910910b9175bfa33574548cf0d0f3be48e175ec3a775a12d30bb",
122122
"boatstack/plan_validation.go": "412f06750832fe46f01190ea5e475fc6f6ea59c8ba78131f94ec031053a405d2",
123123
"boatstack/plan_validation_test.go": "6cbde4ac719baef6b73aa569515d6a9daadcbf14b33f76fa78159826954e20fa",
124-
"boatstack/planning.go": "ef4507a9fecc900f0691372c50883f328c9232c3dfd6986fbde26fb7ef436ae3",
124+
"boatstack/planning.go": "6dfeb802a7cb615f159d87c86f338eab0897bda836417b07f8307695373c78ff",
125125
"boatstack/planning_test.go": "c105a9c78c342be06614bf54d0bc1b661b0f7af64d63b79e43bd1fcc2769edd5",
126126
"boatstack/pr.go": "eae93f7a6e423f67336545d37181ec2ea350991a2d75985f1cf2cf4b7bc8382d",
127127
"boatstack/pr_test.go": "7f82954d94c1ceae848a581dda25e58af92251d78a5a94ed2d672bedf5a0349e",
@@ -148,7 +148,7 @@
148148
"boatstack/run.go": "74967ad5b3ed3847baffec1231aae69a81a70f9cce3a9412fa05bcfdc4eca6d1",
149149
"boatstack/run_test.go": "5b291510fa90cefdc26eb89e18a3443385456a6ebc73408325ac1945b7c084d6",
150150
"boatstack/runtime.go": "d1e95895002ea2b27199b6e05b33c4c6e20f63455a44f63ca3cfeedecfc23420",
151-
"boatstack/runtime_cache.go": "e40c8c43f410d781a7a4e7ebf68a1caa597ea9005190fd6cea02884f863e091e",
151+
"boatstack/runtime_cache.go": "89834409b426dce292fd810a091de1433fefbfba9249d8c1e31ccc40f0d5dbd6",
152152
"boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85",
153153
"boatstack/runtime_provenance_test.go": "1d52f1e6b0691cf4667729cc9b9f3c55c128f0aa3321f3a2843a9aa6fd0e73dc",
154154
"boatstack/safety.go": "24df06e3c0f5dd54ee361ebf7bfd2deca7b18193c2d58d1eb9331c012d342eda",
@@ -161,7 +161,7 @@
161161
"boatstack/testdata/safety/safe_apply.py.txt": "c9ec7fb932cf21b6aa8df597c4d4c54d6ec65e796240e49118d699f583383975",
162162
"boatstack/testdata/safety/unsafe_apply.py.txt": "42db1751865cc15c4dd69a03146b5deca8f21f916d258e433b27bbef5f884ab1",
163163
"boatstack/update.go": "042c7e8141423e2cdb71c92f93cf73cc81d916116d76ab9928c56cf18bc6827a",
164-
"boatstack/update_publication.go": "c8b7bd38019b1cbf8c523652e9e20e8c971c7e48b2f3972a0ac638648326fe63",
164+
"boatstack/update_publication.go": "7cba2e993700190bcc7c3c1cb1a5fe4df84b2053f35bb4fa5e4b0673f397bca2",
165165
"boatstack/update_publication_test.go": "c5f32578db53be65e35452d5e8b4520884354e4e80a370dcec19860ea644d091",
166166
"boatstack/update_test.go": "bf5f19f8499db6dd7356917d867b113b790548ab89bfdea59e2adf4999a82a6a",
167167
"boatstack/visual_evidence.go": "4d69f98adb6087d4830e6703273ae6e03b28ec8b3c496a6e432fd5e0e54d8a2f",
@@ -176,10 +176,10 @@
176176
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
177177
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
178178
"docs/configuration.md": "df054f49d532c8b1b7d94184810d1b3b5bf18cdc30eb985b4b6d0639162e341a",
179-
"docs/evidence-engineered-coding.md": "61449fce7dd97bdbe69e2cdbd5cdf53ee9bae52dfe6e1ce51e377007ba2f4c72",
179+
"docs/evidence-engineered-coding.md": "8c9ac13f925f67db325db9163a1384aa01591d17af82e47e7447922585f62e16",
180180
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
181181
"docs/getting-started.md": "1dd4f4e2e636cc5adfc2f79939629701e171087c3d5e558cf919548b9224adfd",
182-
"docs/public-claims.json": "35c12ea50b1ae2a8403adcdd8e6e849e079a60f13822faa86bddff6543842010",
182+
"docs/public-claims.json": "16d2327409e3bdaefab872aa46abeb88328a8ec263302ed0a4d5d48df431a392",
183183
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
184184
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
185185
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
@@ -193,7 +193,7 @@
193193
"labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d",
194194
"labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71",
195195
"labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39",
196-
"labs/diagram-json/plan.lock.json": "b8249b7cea66cfba8b42b64551cdf4ca6d71ac7c4979a55e1fede27479cbd3e1",
196+
"labs/diagram-json/plan.lock.json": "59951495e1695536035673cac5c799a174d43ac1091e455d4cf4a0e29c9f9763",
197197
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
198198
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
199199
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
@@ -289,6 +289,7 @@
289289
"release-notes/2026-07-25-deliverycontrol-shadow-registry.md": "e7f8ca4e4f188eda3088e46cba77369d8ff0d903f29e43846103d986e79a2273",
290290
"release-notes/2026-07-25-evidence-path-resolution.md": "b32cb8a6e69f397f751c3a7fb62be254a7407a28bed25ae9108d6d773c863d11",
291291
"release-notes/2026-07-25-guard-hydration-serialization.md": "0bf43284b8063011d837dafb10dc77fda496133c14a31ee196dc0dd0e4ffeec4",
292+
"release-notes/2026-07-25-per-worktree-operation-ledger.md": "d0d4495fe4406cf67e7475032e3fc9eb74ed02a3e68f4928c086b5518422b46c",
292293
"release-notes/2026-07-25-published-slice-correction-routing.md": "129cdd62c80c8b93060726027d68ba3abdb0bca1a1ce9e64d6053271af3fd082",
293294
"release-notes/2026-07-25-readme-simplified-technical-english.md": "c362f46702c38dda6b0301d05b95a067da617d170ddfcca22fd7eb9f6e2c1881",
294295
"release-notes/2026-07-25-release-notes-simplified-technical-english.md": "70b273cbeb5ee46c49c10541540f31e8ca67a71102acfd47ae15451856c651db",
@@ -299,7 +300,7 @@
299300
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
300301
"schema_version": 1,
301302
"source": {
302-
"commit": "052ce000ad11b193ea73e262592d59535043f73f",
303+
"commit": "804141bc65d66fdb0a422a9c7c545a71180bffb1",
303304
"path": "labs/12-product-engineering-loop",
304305
"repository": "operatorstack/intelligence-flow"
305306
}

boatstack/delivery.go

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -280,17 +280,11 @@ func deliveryDefinitions(plan map[string]any) ([]DeliverySlice, error) {
280280
}
281281

282282
func deliveryStateDirectory(repo string) (string, error) {
283-
gitDirectory := gitOutput(repo, "rev-parse", "--path-format=absolute", "--git-dir")
284-
if gitDirectory == "" {
285-
gitDirectory = gitOutput(repo, "rev-parse", "--git-dir")
286-
}
287-
if gitDirectory == "" {
288-
return "", fmt.Errorf("cannot resolve the Git worktree directory")
289-
}
290-
if !filepath.IsAbs(gitDirectory) {
291-
gitDirectory = filepath.Join(repo, gitDirectory)
283+
gitDir, err := worktreeGitDir(repo)
284+
if err != nil {
285+
return "", err
292286
}
293-
return filepath.Join(filepath.Clean(gitDirectory), "boatstack", "deliveries"), nil
287+
return filepath.Join(gitDir, "boatstack", "deliveries"), nil
294288
}
295289

296290
func deliveryStatePath(repo, feature string) (string, error) {

boatstack/installation_repair.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -404,12 +404,17 @@ func repairOwnedPaths(result InstallationRepairResult) map[string]bool {
404404
}
405405

406406
func writeInstallationRepairBackup(repo string, result InstallationRepairResult) (string, error) {
407-
common, err := gitCommonDir(repo)
407+
// Repair backups capture this worktree's pre-repair working-tree bytes. Two
408+
// worktrees repairing to the same version share a package fingerprint but hold
409+
// different pre-repair content, so a clone-shared directory would let one
410+
// clobber the other's restore point. Key the backup and its receipt by the
411+
// per-worktree Git directory instead, and keep them co-located.
412+
gitDir, err := worktreeGitDir(repo)
408413
if err != nil {
409414
return "", err
410415
}
411-
directory := filepath.Join(common, "boatstack", "repair-backups", result.PackageFingerprint)
412-
if err := rejectSymlinkComponents(common, directory); err != nil {
416+
directory := filepath.Join(gitDir, "boatstack", "repair-backups", result.PackageFingerprint)
417+
if err := rejectSymlinkComponents(gitDir, directory); err != nil {
413418
return "", err
414419
}
415420
if err := os.MkdirAll(directory, 0o700); err != nil {
@@ -449,23 +454,23 @@ func writeInstallationRepairBackup(repo string, result InstallationRepairResult)
449454
if err != nil {
450455
return "", err
451456
}
452-
receiptPath := filepath.Join(common, "boatstack", "updates", version, "repair.json")
457+
receiptPath := filepath.Join(gitDir, "boatstack", "updates", version, "repair.json")
453458
if err := atomicWriteMode(receiptPath, receiptValue, 0o600); err != nil {
454459
return "", err
455460
}
456461
return directory, nil
457462
}
458463

459464
func loadInstallationRepairReceipt(repo, version string) (*InstallationRepairResult, error) {
460-
common, err := gitCommonDir(repo)
465+
gitDir, err := worktreeGitDir(repo)
461466
if err != nil {
462467
return nil, err
463468
}
464469
segment, err := safeCacheSegment(version, "repair target version")
465470
if err != nil {
466471
return nil, err
467472
}
468-
path := filepath.Join(common, "boatstack", "updates", segment, "repair.json")
473+
path := filepath.Join(gitDir, "boatstack", "updates", segment, "repair.json")
469474
value, err := os.ReadFile(path)
470475
if os.IsNotExist(err) {
471476
return nil, nil

boatstack/operation.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,33 @@ func operationTimestamp() string {
108108
return operationNow().UTC().Truncate(time.Second).Format(time.RFC3339)
109109
}
110110

111+
// operationDirectory holds the operation ledger. It is per-worktree: an operation
112+
// records a mutation performed by one worktree at one time, so worktree A's ledger
113+
// must never sit on worktree B's read/write path (a shared ledger produced false
114+
// "operation identity does not match" blocks across worktrees). The "v2" segment
115+
// is intentional: the main worktree's Git directory aliases the common directory,
116+
// so bumping the version cleanly orphans the legacy clone-shared "v1" ledger for
117+
// every worktree — including main — instead of silently inheriting its receipts.
111118
func operationDirectory(repo string) (string, error) {
112-
common, err := gitCommonDir(repo)
119+
gitDir, err := worktreeGitDir(repo)
113120
if err != nil {
114121
return "", err
115122
}
116-
return filepath.Join(common, "boatstack", "operations", "v1"), nil
123+
return filepath.Join(gitDir, "boatstack", "operations", "v2"), nil
124+
}
125+
126+
// pruneLegacyOperationLedger removes the pre-isolation clone-shared "v1" ledger
127+
// under the Git common directory. Once operations moved to the per-worktree "v2"
128+
// ledger those receipts are orphaned for every worktree (including main, whose
129+
// Git directory aliases the common directory). This is best-effort hygiene: the
130+
// v2 path already guarantees correctness, so any failure here is ignored.
131+
func pruneLegacyOperationLedger(repo string) {
132+
common, err := gitCommonDir(repo)
133+
if err != nil {
134+
return
135+
}
136+
legacy := filepath.Join(common, "boatstack", "operations", "v1")
137+
_ = os.RemoveAll(legacy)
117138
}
118139

119140
func operationPath(repo, operationID string) (string, error) {
@@ -126,11 +147,11 @@ func operationPath(repo, operationID string) (string, error) {
126147
return "", err
127148
}
128149
path := filepath.Join(directory, id+".json")
129-
common, err := gitCommonDir(repo)
150+
gitDir, err := worktreeGitDir(repo)
130151
if err != nil {
131152
return "", err
132153
}
133-
if err := rejectSymlinkComponents(common, path); err != nil {
154+
if err := rejectSymlinkComponents(gitDir, path); err != nil {
134155
return "", err
135156
}
136157
return path, nil
@@ -211,11 +232,11 @@ func withOperationLock(repo, id string, apply func() error) error {
211232
return err
212233
}
213234
lock := strings.TrimSuffix(path, ".json") + ".lock"
214-
common, err := gitCommonDir(repo)
235+
gitDir, err := worktreeGitDir(repo)
215236
if err != nil {
216237
return err
217238
}
218-
if err := rejectSymlinkComponents(common, lock); err != nil {
239+
if err := rejectSymlinkComponents(gitDir, lock); err != nil {
219240
return err
220241
}
221242
if err := os.MkdirAll(filepath.Dir(lock), 0o700); err != nil {

boatstack/operation_test.go

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"os"
66
"path/filepath"
77
"strings"
8-
"sync"
98
"testing"
109
"time"
1110
)
@@ -264,41 +263,70 @@ func TestExpiredLeaseBecomesUnknownAndBudgetPersists(t *testing.T) {
264263
}
265264
}
266265

267-
func TestOperationReceiptsAreSharedAcrossLinkedWorktreesAndSerialized(t *testing.T) {
266+
// TestOperationLedgerIsolatedPerWorktree encodes the deliberate contract reversal
267+
// from the pre-isolation design: the operation ledger is now per-worktree, so one
268+
// worktree neither observes nor blocks another worktree's operations. Two worktrees
269+
// running the same operation identity each hold their own receipt and their own lock.
270+
func TestOperationLedgerIsolatedPerWorktree(t *testing.T) {
268271
repo := operationTestRepo(t)
269272
linked := filepath.Join(t.TempDir(), "linked")
270273
runGit(t, repo, "worktree", "add", "-b", "linked-test", linked)
274+
275+
// An operation prepared in the main worktree is invisible in a linked worktree:
276+
// the linked worktree cannot resolve it from its own separate ledger.
271277
receipt := preparedOperation(t, repo, "package-d", "ATOMIC_LOCAL", 2)
272-
status, err := ResolveOperationStatus(linked, receipt.OperationID)
273-
if err != nil || status.Operation == nil || status.Operation.OperationID != receipt.OperationID {
274-
t.Fatalf("linked worktree did not observe shared operation: %+v %v", status, err)
278+
if _, err := ResolveOperationStatus(linked, receipt.OperationID); err == nil {
279+
t.Fatal("linked worktree observed another worktree's operation ledger")
280+
}
281+
282+
// The same identity begins independently in each worktree — no cross-worktree
283+
// in-flight serialization and no identity collision.
284+
if _, err := BeginOperation(repo, receipt.OperationID, "main-attempt", "Write"); err != nil {
285+
t.Fatalf("main worktree could not begin its own operation: %v", err)
286+
}
287+
linkedReceipt := preparedOperation(t, linked, "package-d", "ATOMIC_LOCAL", 2)
288+
if linkedReceipt.OperationID != receipt.OperationID {
289+
t.Fatalf("same identity produced different operation ids: %s vs %s", linkedReceipt.OperationID, receipt.OperationID)
275290
}
291+
if _, err := BeginOperation(linked, linkedReceipt.OperationID, "linked-attempt", "Write"); err != nil {
292+
t.Fatalf("linked worktree blocked by another worktree's in-flight operation: %v", err)
293+
}
294+
}
295+
296+
// TestSameVersionUpdateFromTwoWorktreesDoesNotCollide reproduces the reported
297+
// incident: a same-version install-update prepared from a second worktree used to
298+
// fail with "existing operation identity does not match the prepared package"
299+
// because the clone-shared ledger compared a differing scope. Per-worktree ledgers
300+
// make each prepare independent.
301+
func TestSameVersionUpdateFromTwoWorktreesDoesNotCollide(t *testing.T) {
302+
repo := operationTestRepo(t)
303+
linked := filepath.Join(t.TempDir(), "linked")
304+
runGit(t, repo, "worktree", "add", "-b", "update-b", linked)
276305

277-
var wait sync.WaitGroup
278-
errorsSeen := make(chan error, 2)
279-
for _, root := range []string{repo, linked} {
280-
wait.Add(1)
281-
go func(path string) {
282-
defer wait.Done()
283-
_, beginErr := BeginOperation(path, receipt.OperationID, "same-attempt", "Write")
284-
errorsSeen <- beginErr
285-
}(root)
286-
}
287-
wait.Wait()
288-
close(errorsSeen)
289-
successes, inFlight := 0, 0
290-
for beginErr := range errorsSeen {
291-
switch {
292-
case beginErr == nil:
293-
successes++
294-
case errors.Is(beginErr, ErrOperationInFlight):
295-
inFlight++
296-
default:
297-
t.Fatalf("unexpected concurrent begin error: %v", beginErr)
306+
options := func(root, scopeWorktree, branch string) OperationPrepareOptions {
307+
return OperationPrepareOptions{
308+
Repo: root,
309+
Kind: "install-update",
310+
Target: "boatstack-install:v9.9.9",
311+
PackageFingerprint: "fp-999",
312+
AuthorizationFingerprint: "auth-999",
313+
RetryClass: "ATOMIC_LOCAL",
314+
MaxAttempts: 2,
315+
ExpectedPostcondition: "generated runtime matches the pinned release",
316+
Scope: OperationScope{Worktree: scopeWorktree, HeadBranch: branch},
298317
}
299318
}
300-
if successes != 1 || inFlight != 1 {
301-
t.Fatalf("operation lock admitted %d executions and %d in-flight reports", successes, inFlight)
319+
320+
a, err := PrepareOperation(options(repo, "update-a", "chore/update-boatstack-v9.9.9"))
321+
if err != nil {
322+
t.Fatalf("worktree A prepare failed: %v", err)
323+
}
324+
b, err := PrepareOperation(options(linked, "update-b", "chore/update-boatstack-v9.9.9"))
325+
if err != nil {
326+
t.Fatalf("worktree B blocked by worktree A's operation: %v", err)
327+
}
328+
if a.OperationID != b.OperationID {
329+
t.Fatalf("expected identical operation ids across worktrees, got %s vs %s", a.OperationID, b.OperationID)
302330
}
303331
}
304332

0 commit comments

Comments
 (0)