Skip to content

Commit a0ed36d

Browse files
Sync Boatstack from Intelligence Flow Labs @ 052ce000ad11
1 parent 6b3cdf1 commit a0ed36d

8 files changed

Lines changed: 196 additions & 38 deletions

File tree

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/7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933/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/052ce000ad11b193ea73e262592d59535043f73f/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: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"files": {
1414
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
15-
"CONTRIBUTING.md": "1367675d8ded3c180fdf1b113e34bd22669a88b71c848b318d9484fc0ffca0bc",
15+
"CONTRIBUTING.md": "c7edd6bb227d0220f83ee5c00901723ed273ab144f3e1cf4bf77697ab63cbf72",
1616
"README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad",
1717
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
1818
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
@@ -77,8 +77,8 @@
7777
"boatstack/flow_trace_test.go": "99f89a831e904f6a8ef710b6977ed3a808ce1c7ddfaba457b292d84f2ddca51b",
7878
"boatstack/go.mod": "6086ef1b2a83f5696190dca692c653925f27b61f652f659fd3fca43ed54a1641",
7979
"boatstack/go.sum": "26c315c867b11b886f3c9402fce7f341f6a9115a5d61f54afbb5e1b1fb5f6017",
80-
"boatstack/hooks.go": "0639eff2ec5ce50dcbe77ace0f7c25de1e9a68784a6ed70d6acc9984d049ef1a",
81-
"boatstack/hooks_hydrate_test.go": "8b6317cbaa1f46e71505534672fa21ef9281e0920e9ecda18d2e4b47d5a8799e",
80+
"boatstack/hooks.go": "c8606417aec79fdcf84b3420758e7d491c3757e7b3117c7fc8df2bf4b0733e03",
81+
"boatstack/hooks_hydrate_test.go": "7beeb26b2b1398741e8a28963a9686e974047016cc736f233024004add1afc32",
8282
"boatstack/hooks_test.go": "fb75e3aabf2204871b3e6d16de98d26fb33b0ec19e41aae761cf1f34397c31f4",
8383
"boatstack/hydrate_runtime_test.go": "dbd5eae2ba85701e4af0430ba3a0d70ea98e028b66992bd4fc05f3f582398627",
8484
"boatstack/init.go": "1b2721ad64dcfba3e954b97bb46218238873f46ec29fc4dac327aa99980f9cf6",
@@ -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": "e40b490f2b03bd27a99b4b7aa94d94d0e8cceab34946ed5893968626349f2067",
179+
"docs/evidence-engineered-coding.md": "61449fce7dd97bdbe69e2cdbd5cdf53ee9bae52dfe6e1ce51e377007ba2f4c72",
180180
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
181181
"docs/getting-started.md": "1dd4f4e2e636cc5adfc2f79939629701e171087c3d5e558cf919548b9224adfd",
182-
"docs/public-claims.json": "92e9b30989c9c601d8c4447ef4c4420c906490ee9124bed80607af634e1928aa",
182+
"docs/public-claims.json": "35c12ea50b1ae2a8403adcdd8e6e849e079a60f13822faa86bddff6543842010",
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": "6c86e5f1613dea677fc4e8eb0add4dbedc03549abfbc12635db44d65e7c4bcde",
196+
"labs/diagram-json/plan.lock.json": "b8249b7cea66cfba8b42b64551cdf4ca6d71ac7c4979a55e1fede27479cbd3e1",
197197
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
198198
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
199199
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
@@ -288,6 +288,7 @@
288288
"release-notes/2026-07-25-deliverycontrol-flow-tasks-subaction.md": "f5fb40312e4f3084d352492805a1a9d2f23ee1a0a38057d2c696a995044101b4",
289289
"release-notes/2026-07-25-deliverycontrol-shadow-registry.md": "e7f8ca4e4f188eda3088e46cba77369d8ff0d903f29e43846103d986e79a2273",
290290
"release-notes/2026-07-25-evidence-path-resolution.md": "b32cb8a6e69f397f751c3a7fb62be254a7407a28bed25ae9108d6d773c863d11",
291+
"release-notes/2026-07-25-guard-hydration-serialization.md": "0bf43284b8063011d837dafb10dc77fda496133c14a31ee196dc0dd0e4ffeec4",
291292
"release-notes/2026-07-25-published-slice-correction-routing.md": "129cdd62c80c8b93060726027d68ba3abdb0bca1a1ce9e64d6053271af3fd082",
292293
"release-notes/2026-07-25-readme-simplified-technical-english.md": "c362f46702c38dda6b0301d05b95a067da617d170ddfcca22fd7eb9f6e2c1881",
293294
"release-notes/2026-07-25-release-notes-simplified-technical-english.md": "70b273cbeb5ee46c49c10541540f31e8ca67a71102acfd47ae15451856c651db",
@@ -298,7 +299,7 @@
298299
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
299300
"schema_version": 1,
300301
"source": {
301-
"commit": "7cbafd41ee72ad5b0e682a052ad3ebc6f21f5933",
302+
"commit": "052ce000ad11b193ea73e262592d59535043f73f",
302303
"path": "labs/12-product-engineering-loop",
303304
"repository": "operatorstack/intelligence-flow"
304305
}

boatstack/hooks.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -155,15 +155,18 @@ esac
155155
156156
HELPER="$COMMON/boatstack/runtimes/%s/%s/${OS_NAME}-${ARCH}/boatstack-helper${EXTENSION}"
157157
MANIFEST="$COMMON/boatstack/runtimes/%s/%s/${OS_NAME}-${ARCH}/runtime.lock.json"
158-
# Auto-hydrate a missing shared-runtime slot. A teammate who pulls a version
159-
# bump or clones fresh inherits the committed pointers (this guard's baked
160-
# version path) but an empty, gitignored slot, so without this the very next
161-
# tool call would hard-deny before any Go runs. On an absent slot we run the
158+
# Auto-hydrate a missing or incomplete shared-runtime slot. A teammate who pulls
159+
# a version bump or clones fresh inherits the committed pointers (this guard's
160+
# baked version path) but an empty, gitignored slot, so without this the very next
161+
# tool call would hard-deny before any Go runs. On an incomplete slot we run the
162162
# tag-pinned, checksum-verifying installer in branch-free hydrate mode, serialize
163163
# clone-wide with an atomic mkdir lock, and bound the attempt. This is purely
164164
# additive: the existing missing/symlink/checksum gates below stay authoritative
165165
# and fail-closed, so a disabled, timed-out, or failed hydration simply denies.
166-
if [[ ! -x "$HELPER" && "${BOATSTACK_AUTO_HYDRATE:-1}" != "0" ]]; then
166+
# The entry test mirrors those gates (helper AND manifest present, non-symlink):
167+
# an installer copies the helper before the manifest, so a peer arriving in that
168+
# window must join the lock and wait, not skip the block and deny a half-slot.
169+
if { [[ ! -x "$HELPER" || -L "$HELPER" || ! -f "$MANIFEST" || -L "$MANIFEST" ]]; } && [[ "${BOATSTACK_AUTO_HYDRATE:-1}" != "0" ]]; then
167170
mkdir -p "$COMMON/boatstack" 2>/dev/null || true
168171
HYDRATE_LOCK="$COMMON/boatstack/hydrate-%s.lock"
169172
if mkdir "$HYDRATE_LOCK" 2>/dev/null; then
@@ -184,9 +187,15 @@ if [[ ! -x "$HELPER" && "${BOATSTACK_AUTO_HYDRATE:-1}" != "0" ]]; then
184187
) >&2 || true
185188
rmdir "$HYDRATE_LOCK" 2>/dev/null || true
186189
else
187-
# A peer is hydrating the shared slot; wait briefly for it to appear.
188-
for _ in $(seq 1 8); do
189-
[[ -x "$HELPER" ]] && break
190+
# A peer holds the hydrate lock. Wait for the peer to finish — it removes the
191+
# lock only after its hydrate command returns — before inspecting the slot, so
192+
# a waiter never observes a half-written runtime (for example the helper copied
193+
# but the manifest not yet in place). A released lock means the slot is as
194+
# complete as it will get; the authoritative gates below then accept it or fail
195+
# closed. Bound the wait above the peer's own hydrate timeout so a slow but
196+
# succeeding peer still wins.
197+
for _ in $(seq 1 12); do
198+
[[ -d "$HYDRATE_LOCK" ]] || break
190199
sleep 1
191200
done
192201
fi
@@ -249,7 +258,7 @@ $manifestPath = Join-Path $common "boatstack/runtimes/%s/%s/windows-$arch/runtim
249258
# checksum-verifying installer in branch-free hydrate mode, serialized clone-wide
250259
# with an atomic directory lock. Purely additive: the gates below stay
251260
# authoritative and fail-closed if hydration is disabled, fails, or is skipped.
252-
if ((-not (Test-Path -LiteralPath $helper -PathType Leaf)) -and $env:BOATSTACK_AUTO_HYDRATE -ne "0") {
261+
if (((-not (Test-Path -LiteralPath $helper -PathType Leaf)) -or (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf))) -and $env:BOATSTACK_AUTO_HYDRATE -ne "0") {
253262
$bsCommon = Join-Path $common "boatstack"
254263
New-Item -ItemType Directory -Path $bsCommon -Force -ErrorAction SilentlyContinue | Out-Null
255264
$hydrateLock = Join-Path $bsCommon "hydrate-%s.lock"
@@ -270,8 +279,12 @@ if ((-not (Test-Path -LiteralPath $helper -PathType Leaf)) -and $env:BOATSTACK_A
270279
Remove-Item -LiteralPath $hydrateLock -Recurse -Force -ErrorAction SilentlyContinue
271280
}
272281
} else {
273-
for ($i = 0; $i -lt 8; $i++) {
274-
if (Test-Path -LiteralPath $helper -PathType Leaf) { break }
282+
# Wait for the peer to release the lock (it does so only after its hydrate
283+
# command returns) before inspecting the slot, so a waiter never observes a
284+
# half-written runtime. The authoritative gates below then accept it or fail
285+
# closed. Bound the wait above the peer's own hydrate timeout.
286+
for ($i = 0; $i -lt 12; $i++) {
287+
if (-not (Test-Path -LiteralPath $hydrateLock)) { break }
275288
Start-Sleep -Seconds 1
276289
}
277290
}

boatstack/hooks_hydrate_test.go

Lines changed: 129 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99
"sync"
1010
"testing"
11+
"time"
1112
)
1213

1314
func requireBash(t *testing.T) {
@@ -171,24 +172,34 @@ func TestGuardAutoHydrationInvokesPinnedHydrator(t *testing.T) {
171172
// TestGuardAutoHydrationSerializesConcurrentFirstUse proves the clone-wide lock:
172173
// two guards racing an absent slot invoke the hydrator at most once, and both
173174
// still proceed.
175+
// TestGuardAutoHydrationSerializesConcurrentFirstUse is a bounded conformance
176+
// test for the clone-wide serialization invariant: when many guards hit an empty
177+
// slot at once, exactly one hydrates and every guard proceeds. A start barrier
178+
// releases all guards together to force real contention on the mkdir lock. The
179+
// invariant holds for any interleaving because each losing guard waits for the
180+
// winner to release the lock before it inspects the slot, so no guard observes a
181+
// half-written runtime. Bounded: a fixed fan-out, a single round.
174182
func TestGuardAutoHydrationSerializesConcurrentFirstUse(t *testing.T) {
175183
requireBash(t)
176184
repo := runtimeTestRepo(t)
177-
binaryPath, manifestPath, restore := stageVerifiedHelper(t, repo)
185+
binaryPath, _, restore := stageVerifiedHelper(t, repo)
178186
counter := filepath.Join(t.TempDir(), "count")
179187
stub := fmt.Sprintf("echo x >> %q && %s", counter, restore)
180-
_ = manifestPath
181188

189+
const guards = 8
190+
start := make(chan struct{})
182191
var wg sync.WaitGroup
183-
outputs := make([]string, 2)
184-
errs := make([]error, 2)
185-
for i := 0; i < 2; i++ {
192+
outputs := make([]string, guards)
193+
errs := make([]error, guards)
194+
for i := 0; i < guards; i++ {
186195
wg.Add(1)
187196
go func(idx int) {
188197
defer wg.Done()
198+
<-start // release every guard together for genuine contention
189199
outputs[idx], errs[idx] = runGuard(t, repo, "claude", "BOATSTACK_HYDRATE_COMMAND="+stub)
190200
}(i)
191201
}
202+
close(start)
192203
wg.Wait()
193204

194205
for i := range errs {
@@ -207,3 +218,116 @@ func TestGuardAutoHydrationSerializesConcurrentFirstUse(t *testing.T) {
207218
t.Fatalf("hydrator ran %d times under contention, want exactly 1", got)
208219
}
209220
}
221+
222+
// hydrateLockPath returns the clone-wide hydrate lock the guard uses, derived
223+
// from the shared binary path: <common>/boatstack/hydrate-<Version>.lock.
224+
func hydrateLockPath(t *testing.T, binaryPath string) string {
225+
t.Helper()
226+
// binaryPath = <common>/boatstack/runtimes/<Version>/<SourceCommit>/<os-arch>/boatstack-helper
227+
bsCommon := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(binaryPath)))))
228+
return filepath.Join(bsCommon, "hydrate-"+Version+".lock")
229+
}
230+
231+
// TestGuardAutoHydrationWaiterAwaitsPeerCompletion is a bounded, deterministic
232+
// regression test for the exact failure mode that flaked in CI: a waiting guard
233+
// used to break as soon as the helper file appeared and then fail the manifest
234+
// gate ("unsafe or incomplete") while the peer was still mid-copy. Here a peer
235+
// holds the lock and hydrates non-atomically — it writes the helper, pauses, then
236+
// writes the manifest, then releases the lock, exactly the installer's copy order.
237+
// The waiting guard must not judge the slot until the peer releases the lock, so
238+
// it proceeds cleanly. Before the fix this test fails; after it, it passes on any
239+
// timing.
240+
func TestGuardAutoHydrationWaiterAwaitsPeerCompletion(t *testing.T) {
241+
requireBash(t)
242+
repo := runtimeTestRepo(t)
243+
binaryPath, manifestPath := emptySharedSlot(t, repo)
244+
lockDir := hydrateLockPath(t, binaryPath)
245+
246+
// A peer already holds the clone-wide hydrate lock.
247+
if err := os.MkdirAll(lockDir, 0o755); err != nil {
248+
t.Fatal(err)
249+
}
250+
251+
fakeHelper := []byte("#!/usr/bin/env bash\necho boatstack-guard-hydration-sentinel >&2\nexit 0\n")
252+
manifestBytes := []byte(fmt.Sprintf(`{"binary_sha256":"%s"}`, SHA256Bytes(fakeHelper)))
253+
254+
peerDone := make(chan struct{})
255+
go func() {
256+
defer close(peerDone)
257+
// Let the guard reach its waiter loop while the slot is still empty.
258+
time.Sleep(300 * time.Millisecond)
259+
if err := os.MkdirAll(filepath.Dir(binaryPath), 0o755); err != nil {
260+
return
261+
}
262+
// The helper appears first — the non-atomic window that broke the old waiter.
263+
if err := os.WriteFile(binaryPath, fakeHelper, 0o755); err != nil {
264+
return
265+
}
266+
time.Sleep(1 * time.Second)
267+
// The manifest lands only now; the slot becomes complete.
268+
if err := os.WriteFile(manifestPath, manifestBytes, 0o644); err != nil {
269+
return
270+
}
271+
// Release the lock last, signaling completion.
272+
_ = os.Remove(lockDir)
273+
}()
274+
275+
// The guard finds the lock held and the helper absent, so it enters the waiter
276+
// branch. It must wait for the peer to release the lock, then clear every gate.
277+
output, err := runGuard(t, repo, "claude")
278+
<-peerDone
279+
if err != nil {
280+
t.Fatalf("waiter judged a slot mid-hydration instead of awaiting the peer: err=%v output=%s", err, output)
281+
}
282+
if _, statErr := os.Stat(binaryPath); statErr != nil {
283+
t.Fatalf("shared slot was not populated after the peer finished: %v", statErr)
284+
}
285+
}
286+
287+
// TestGuardAutoHydrationWaitsWhenSlotHalfWritten is a bounded, deterministic
288+
// regression test for the skip-path variant of the same failure mode. A guard
289+
// that judged readiness by the helper alone would, on a half-written slot (helper
290+
// present, manifest not yet), skip the hydrate/wait block entirely and deny at the
291+
// manifest gate — even while a peer held the lock and was about to finish. The
292+
// entry test now mirrors the gates (helper AND manifest), so such a guard joins
293+
// the lock and waits instead. Before the fix this test fails with the exact CI
294+
// error; after it, it passes.
295+
func TestGuardAutoHydrationWaitsWhenSlotHalfWritten(t *testing.T) {
296+
requireBash(t)
297+
repo := runtimeTestRepo(t)
298+
binaryPath, manifestPath := emptySharedSlot(t, repo)
299+
lockDir := hydrateLockPath(t, binaryPath)
300+
301+
fakeHelper := []byte("#!/usr/bin/env bash\necho boatstack-guard-hydration-sentinel >&2\nexit 0\n")
302+
manifestBytes := []byte(fmt.Sprintf(`{"binary_sha256":"%s"}`, SHA256Bytes(fakeHelper)))
303+
304+
// The slot is half-written — the helper is present but the manifest is not —
305+
// and a peer holds the hydrate lock because it is still mid-copy.
306+
if err := os.MkdirAll(filepath.Dir(binaryPath), 0o755); err != nil {
307+
t.Fatal(err)
308+
}
309+
if err := os.WriteFile(binaryPath, fakeHelper, 0o755); err != nil {
310+
t.Fatal(err)
311+
}
312+
if err := os.MkdirAll(lockDir, 0o755); err != nil {
313+
t.Fatal(err)
314+
}
315+
316+
peerDone := make(chan struct{})
317+
go func() {
318+
defer close(peerDone)
319+
time.Sleep(1 * time.Second)
320+
if err := os.WriteFile(manifestPath, manifestBytes, 0o644); err != nil {
321+
return
322+
}
323+
_ = os.Remove(lockDir)
324+
}()
325+
326+
// A guard seeing only the helper must not treat the slot as ready; it must join
327+
// the lock, wait for the peer to finish, then clear every gate.
328+
output, err := runGuard(t, repo, "claude")
329+
<-peerDone
330+
if err != nil {
331+
t.Fatalf("guard skipped hydration on a half-written slot instead of waiting: err=%v output=%s", err, output)
332+
}
333+
}

docs/evidence-engineered-coding.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,6 @@ Delivery and system improvement also remain separate. A failed task may suggest
146146

147147
## What is evidence-backed
148148

149-
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).
149+
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).
150150

151151
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.

0 commit comments

Comments
 (0)