diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12d4c1e..2ab354b 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/7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933/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/052ce000ad11b193ea73e262592d59535043f73f/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 6f3e374..24dedd5 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -12,7 +12,7 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "1367675d8ded3c180fdf1b113e34bd22669a88b71c848b318d9484fc0ffca0bc", + "CONTRIBUTING.md": "c7edd6bb227d0220f83ee5c00901723ed273ab144f3e1cf4bf77697ab63cbf72", "README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", @@ -77,8 +77,8 @@ "boatstack/flow_trace_test.go": "99f89a831e904f6a8ef710b6977ed3a808ce1c7ddfaba457b292d84f2ddca51b", "boatstack/go.mod": "6086ef1b2a83f5696190dca692c653925f27b61f652f659fd3fca43ed54a1641", "boatstack/go.sum": "26c315c867b11b886f3c9402fce7f341f6a9115a5d61f54afbb5e1b1fb5f6017", - "boatstack/hooks.go": "0639eff2ec5ce50dcbe77ace0f7c25de1e9a68784a6ed70d6acc9984d049ef1a", - "boatstack/hooks_hydrate_test.go": "8b6317cbaa1f46e71505534672fa21ef9281e0920e9ecda18d2e4b47d5a8799e", + "boatstack/hooks.go": "c8606417aec79fdcf84b3420758e7d491c3757e7b3117c7fc8df2bf4b0733e03", + "boatstack/hooks_hydrate_test.go": "7beeb26b2b1398741e8a28963a9686e974047016cc736f233024004add1afc32", "boatstack/hooks_test.go": "fb75e3aabf2204871b3e6d16de98d26fb33b0ec19e41aae761cf1f34397c31f4", "boatstack/hydrate_runtime_test.go": "dbd5eae2ba85701e4af0430ba3a0d70ea98e028b66992bd4fc05f3f582398627", "boatstack/init.go": "1b2721ad64dcfba3e954b97bb46218238873f46ec29fc4dac327aa99980f9cf6", @@ -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": "e40b490f2b03bd27a99b4b7aa94d94d0e8cceab34946ed5893968626349f2067", + "docs/evidence-engineered-coding.md": "61449fce7dd97bdbe69e2cdbd5cdf53ee9bae52dfe6e1ce51e377007ba2f4c72", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", "docs/getting-started.md": "1dd4f4e2e636cc5adfc2f79939629701e171087c3d5e558cf919548b9224adfd", - "docs/public-claims.json": "92e9b30989c9c601d8c4447ef4c4420c906490ee9124bed80607af634e1928aa", + "docs/public-claims.json": "35c12ea50b1ae2a8403adcdd8e6e849e079a60f13822faa86bddff6543842010", "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": "6c86e5f1613dea677fc4e8eb0add4dbedc03549abfbc12635db44d65e7c4bcde", + "labs/diagram-json/plan.lock.json": "b8249b7cea66cfba8b42b64551cdf4ca6d71ac7c4979a55e1fede27479cbd3e1", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -288,6 +288,7 @@ "release-notes/2026-07-25-deliverycontrol-flow-tasks-subaction.md": "f5fb40312e4f3084d352492805a1a9d2f23ee1a0a38057d2c696a995044101b4", "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-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", @@ -298,7 +299,7 @@ "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933", + "commit": "052ce000ad11b193ea73e262592d59535043f73f", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/hooks.go b/boatstack/hooks.go index 4c6981a..68d1078 100644 --- a/boatstack/hooks.go +++ b/boatstack/hooks.go @@ -155,15 +155,18 @@ esac HELPER="$COMMON/boatstack/runtimes/%s/%s/${OS_NAME}-${ARCH}/boatstack-helper${EXTENSION}" MANIFEST="$COMMON/boatstack/runtimes/%s/%s/${OS_NAME}-${ARCH}/runtime.lock.json" -# Auto-hydrate a missing shared-runtime slot. A teammate who pulls a version -# bump or clones fresh inherits the committed pointers (this guard's baked -# version path) but an empty, gitignored slot, so without this the very next -# tool call would hard-deny before any Go runs. On an absent slot we run the +# Auto-hydrate a missing or incomplete shared-runtime slot. A teammate who pulls +# a version bump or clones fresh inherits the committed pointers (this guard's +# baked version path) but an empty, gitignored slot, so without this the very next +# tool call would hard-deny before any Go runs. On an incomplete slot we run the # tag-pinned, checksum-verifying installer in branch-free hydrate mode, serialize # clone-wide with an atomic mkdir lock, and bound the attempt. This is purely # additive: the existing missing/symlink/checksum gates below stay authoritative # and fail-closed, so a disabled, timed-out, or failed hydration simply denies. -if [[ ! -x "$HELPER" && "${BOATSTACK_AUTO_HYDRATE:-1}" != "0" ]]; then +# The entry test mirrors those gates (helper AND manifest present, non-symlink): +# an installer copies the helper before the manifest, so a peer arriving in that +# window must join the lock and wait, not skip the block and deny a half-slot. +if { [[ ! -x "$HELPER" || -L "$HELPER" || ! -f "$MANIFEST" || -L "$MANIFEST" ]]; } && [[ "${BOATSTACK_AUTO_HYDRATE:-1}" != "0" ]]; then mkdir -p "$COMMON/boatstack" 2>/dev/null || true HYDRATE_LOCK="$COMMON/boatstack/hydrate-%s.lock" if mkdir "$HYDRATE_LOCK" 2>/dev/null; then @@ -184,9 +187,15 @@ if [[ ! -x "$HELPER" && "${BOATSTACK_AUTO_HYDRATE:-1}" != "0" ]]; then ) >&2 || true rmdir "$HYDRATE_LOCK" 2>/dev/null || true else - # A peer is hydrating the shared slot; wait briefly for it to appear. - for _ in $(seq 1 8); do - [[ -x "$HELPER" ]] && break + # A peer holds the hydrate lock. Wait for the peer to finish — it removes the + # lock only after its hydrate command returns — before inspecting the slot, so + # a waiter never observes a half-written runtime (for example the helper copied + # but the manifest not yet in place). A released lock means the slot is as + # complete as it will get; the authoritative gates below then accept it or fail + # closed. Bound the wait above the peer's own hydrate timeout so a slow but + # succeeding peer still wins. + for _ in $(seq 1 12); do + [[ -d "$HYDRATE_LOCK" ]] || break sleep 1 done fi @@ -249,7 +258,7 @@ $manifestPath = Join-Path $common "boatstack/runtimes/%s/%s/windows-$arch/runtim # checksum-verifying installer in branch-free hydrate mode, serialized clone-wide # with an atomic directory lock. Purely additive: the gates below stay # authoritative and fail-closed if hydration is disabled, fails, or is skipped. -if ((-not (Test-Path -LiteralPath $helper -PathType Leaf)) -and $env:BOATSTACK_AUTO_HYDRATE -ne "0") { +if (((-not (Test-Path -LiteralPath $helper -PathType Leaf)) -or (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf))) -and $env:BOATSTACK_AUTO_HYDRATE -ne "0") { $bsCommon = Join-Path $common "boatstack" New-Item -ItemType Directory -Path $bsCommon -Force -ErrorAction SilentlyContinue | Out-Null $hydrateLock = Join-Path $bsCommon "hydrate-%s.lock" @@ -270,8 +279,12 @@ if ((-not (Test-Path -LiteralPath $helper -PathType Leaf)) -and $env:BOATSTACK_A Remove-Item -LiteralPath $hydrateLock -Recurse -Force -ErrorAction SilentlyContinue } } else { - for ($i = 0; $i -lt 8; $i++) { - if (Test-Path -LiteralPath $helper -PathType Leaf) { break } + # Wait for the peer to release the lock (it does so only after its hydrate + # command returns) before inspecting the slot, so a waiter never observes a + # half-written runtime. The authoritative gates below then accept it or fail + # closed. Bound the wait above the peer's own hydrate timeout. + for ($i = 0; $i -lt 12; $i++) { + if (-not (Test-Path -LiteralPath $hydrateLock)) { break } Start-Sleep -Seconds 1 } } diff --git a/boatstack/hooks_hydrate_test.go b/boatstack/hooks_hydrate_test.go index 2433e2a..36f16ca 100644 --- a/boatstack/hooks_hydrate_test.go +++ b/boatstack/hooks_hydrate_test.go @@ -8,6 +8,7 @@ import ( "strings" "sync" "testing" + "time" ) func requireBash(t *testing.T) { @@ -171,24 +172,34 @@ func TestGuardAutoHydrationInvokesPinnedHydrator(t *testing.T) { // TestGuardAutoHydrationSerializesConcurrentFirstUse proves the clone-wide lock: // two guards racing an absent slot invoke the hydrator at most once, and both // still proceed. +// TestGuardAutoHydrationSerializesConcurrentFirstUse is a bounded conformance +// test for the clone-wide serialization invariant: when many guards hit an empty +// slot at once, exactly one hydrates and every guard proceeds. A start barrier +// releases all guards together to force real contention on the mkdir lock. The +// invariant holds for any interleaving because each losing guard waits for the +// winner to release the lock before it inspects the slot, so no guard observes a +// half-written runtime. Bounded: a fixed fan-out, a single round. func TestGuardAutoHydrationSerializesConcurrentFirstUse(t *testing.T) { requireBash(t) repo := runtimeTestRepo(t) - binaryPath, manifestPath, restore := stageVerifiedHelper(t, repo) + binaryPath, _, restore := stageVerifiedHelper(t, repo) counter := filepath.Join(t.TempDir(), "count") stub := fmt.Sprintf("echo x >> %q && %s", counter, restore) - _ = manifestPath + const guards = 8 + start := make(chan struct{}) var wg sync.WaitGroup - outputs := make([]string, 2) - errs := make([]error, 2) - for i := 0; i < 2; i++ { + outputs := make([]string, guards) + errs := make([]error, guards) + for i := 0; i < guards; i++ { wg.Add(1) go func(idx int) { defer wg.Done() + <-start // release every guard together for genuine contention outputs[idx], errs[idx] = runGuard(t, repo, "claude", "BOATSTACK_HYDRATE_COMMAND="+stub) }(i) } + close(start) wg.Wait() for i := range errs { @@ -207,3 +218,116 @@ func TestGuardAutoHydrationSerializesConcurrentFirstUse(t *testing.T) { t.Fatalf("hydrator ran %d times under contention, want exactly 1", got) } } + +// hydrateLockPath returns the clone-wide hydrate lock the guard uses, derived +// from the shared binary path: /boatstack/hydrate-.lock. +func hydrateLockPath(t *testing.T, binaryPath string) string { + t.Helper() + // binaryPath = /boatstack/runtimes////boatstack-helper + bsCommon := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(binaryPath))))) + return filepath.Join(bsCommon, "hydrate-"+Version+".lock") +} + +// TestGuardAutoHydrationWaiterAwaitsPeerCompletion is a bounded, deterministic +// regression test for the exact failure mode that flaked in CI: a waiting guard +// used to break as soon as the helper file appeared and then fail the manifest +// gate ("unsafe or incomplete") while the peer was still mid-copy. Here a peer +// holds the lock and hydrates non-atomically — it writes the helper, pauses, then +// writes the manifest, then releases the lock, exactly the installer's copy order. +// The waiting guard must not judge the slot until the peer releases the lock, so +// it proceeds cleanly. Before the fix this test fails; after it, it passes on any +// timing. +func TestGuardAutoHydrationWaiterAwaitsPeerCompletion(t *testing.T) { + requireBash(t) + repo := runtimeTestRepo(t) + binaryPath, manifestPath := emptySharedSlot(t, repo) + lockDir := hydrateLockPath(t, binaryPath) + + // A peer already holds the clone-wide hydrate lock. + if err := os.MkdirAll(lockDir, 0o755); err != nil { + t.Fatal(err) + } + + fakeHelper := []byte("#!/usr/bin/env bash\necho boatstack-guard-hydration-sentinel >&2\nexit 0\n") + manifestBytes := []byte(fmt.Sprintf(`{"binary_sha256":"%s"}`, SHA256Bytes(fakeHelper))) + + peerDone := make(chan struct{}) + go func() { + defer close(peerDone) + // Let the guard reach its waiter loop while the slot is still empty. + time.Sleep(300 * time.Millisecond) + if err := os.MkdirAll(filepath.Dir(binaryPath), 0o755); err != nil { + return + } + // The helper appears first — the non-atomic window that broke the old waiter. + if err := os.WriteFile(binaryPath, fakeHelper, 0o755); err != nil { + return + } + time.Sleep(1 * time.Second) + // The manifest lands only now; the slot becomes complete. + if err := os.WriteFile(manifestPath, manifestBytes, 0o644); err != nil { + return + } + // Release the lock last, signaling completion. + _ = os.Remove(lockDir) + }() + + // The guard finds the lock held and the helper absent, so it enters the waiter + // branch. It must wait for the peer to release the lock, then clear every gate. + output, err := runGuard(t, repo, "claude") + <-peerDone + if err != nil { + t.Fatalf("waiter judged a slot mid-hydration instead of awaiting the peer: err=%v output=%s", err, output) + } + if _, statErr := os.Stat(binaryPath); statErr != nil { + t.Fatalf("shared slot was not populated after the peer finished: %v", statErr) + } +} + +// TestGuardAutoHydrationWaitsWhenSlotHalfWritten is a bounded, deterministic +// regression test for the skip-path variant of the same failure mode. A guard +// that judged readiness by the helper alone would, on a half-written slot (helper +// present, manifest not yet), skip the hydrate/wait block entirely and deny at the +// manifest gate — even while a peer held the lock and was about to finish. The +// entry test now mirrors the gates (helper AND manifest), so such a guard joins +// the lock and waits instead. Before the fix this test fails with the exact CI +// error; after it, it passes. +func TestGuardAutoHydrationWaitsWhenSlotHalfWritten(t *testing.T) { + requireBash(t) + repo := runtimeTestRepo(t) + binaryPath, manifestPath := emptySharedSlot(t, repo) + lockDir := hydrateLockPath(t, binaryPath) + + fakeHelper := []byte("#!/usr/bin/env bash\necho boatstack-guard-hydration-sentinel >&2\nexit 0\n") + manifestBytes := []byte(fmt.Sprintf(`{"binary_sha256":"%s"}`, SHA256Bytes(fakeHelper))) + + // The slot is half-written — the helper is present but the manifest is not — + // and a peer holds the hydrate lock because it is still mid-copy. + if err := os.MkdirAll(filepath.Dir(binaryPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(binaryPath, fakeHelper, 0o755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(lockDir, 0o755); err != nil { + t.Fatal(err) + } + + peerDone := make(chan struct{}) + go func() { + defer close(peerDone) + time.Sleep(1 * time.Second) + if err := os.WriteFile(manifestPath, manifestBytes, 0o644); err != nil { + return + } + _ = os.Remove(lockDir) + }() + + // A guard seeing only the helper must not treat the slot as ready; it must join + // the lock, wait for the peer to finish, then clear every gate. + output, err := runGuard(t, repo, "claude") + <-peerDone + if err != nil { + t.Fatalf("guard skipped hydration on a half-written slot instead of waiting: err=%v output=%s", err, output) + } +} diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 7d31de7..b4188f0 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 [`7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933`](https://github.com/operatorstack/intelligence-flow/tree/7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933/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 [`052ce000ad11b193ea73e262592d59535043f73f`](https://github.com/operatorstack/intelligence-flow/tree/052ce000ad11b193ea73e262592d59535043f73f/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 392d815..204e532 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933", + "source_commit": "052ce000ad11b193ea73e262592d59535043f73f", "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:7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933" + "last_verified_version": "source:052ce000ad11b193ea73e262592d59535043f73f" }, { "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:7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933" + "last_verified_version": "source:052ce000ad11b193ea73e262592d59535043f73f" }, { "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:7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933" + "last_verified_version": "source:052ce000ad11b193ea73e262592d59535043f73f" }, { "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:7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933" + "last_verified_version": "source:052ce000ad11b193ea73e262592d59535043f73f" }, { "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:7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933" + "last_verified_version": "source:052ce000ad11b193ea73e262592d59535043f73f" }, { "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:7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933" + "last_verified_version": "source:052ce000ad11b193ea73e262592d59535043f73f" }, { "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:7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933" + "last_verified_version": "source:052ce000ad11b193ea73e262592d59535043f73f" }, { "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:7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933" + "last_verified_version": "source:052ce000ad11b193ea73e262592d59535043f73f" }, { "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:7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933" + "last_verified_version": "source:052ce000ad11b193ea73e262592d59535043f73f" }, { "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:7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933" + "last_verified_version": "source:052ce000ad11b193ea73e262592d59535043f73f" }, { "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:7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933" + "last_verified_version": "source:052ce000ad11b193ea73e262592d59535043f73f" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 31aa53d..d470518 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": "7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933", + "source_commit": "052ce000ad11b193ea73e262592d59535043f73f", "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-guard-hydration-serialization.md b/release-notes/2026-07-25-guard-hydration-serialization.md new file mode 100644 index 0000000..2ede672 --- /dev/null +++ b/release-notes/2026-07-25-guard-hydration-serialization.md @@ -0,0 +1,20 @@ +### Concurrent first use no longer trips a false "unsafe runtime" denial + +When several tool calls hit an empty shared-runtime slot at the same time, one guard +hydrates the slot and the others must wait. The installer copies the helper before the +manifest, so for a short moment the slot holds the helper but not the manifest. A guard that +looked during that moment judged the runtime "unsafe or incomplete" and denied the tool call +by mistake. This showed up as a flaky CI failure on Linux under contention. + +The guard now closes both sides of the race. First, it treats the slot as ready only when +both the helper and the manifest are present. So a guard that arrives during the gap joins +the hydrate lock instead of denying a half-written slot. Second, a waiting guard now waits +for the hydrating peer to release the clone-wide lock. The peer releases the lock only after +its installer finishes, so a released lock means the slot is complete. The guard then runs +the same checksum and safety gates, which still fail closed if hydration was disabled, timed +out, or failed. The fix applies to both the bash and PowerShell guards. + +Three bounded conformance tests cover the fix. One runs many guards at once and confirms +that exactly one hydrates and every guard proceeds. Two deterministic tests drive a slow, +non-atomic peer — one from an empty slot and one from a half-written slot — and confirm the +guard waits for the peer instead of judging an incomplete slot.