Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/9bfb96de291e2552d307fa3c1b23e67771bcb86d/examples/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/09388a6c92ece15283a8d0dc2b7edef3d2cc3aba/examples/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.

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ They come from real coding failures we observed—not guesses. For every safegua
| <!-- boatstack-claim:validation-provenance -->“Tests passed” was used to support claims the tests did not cover | It links each promised outcome to the check that can disprove it | Plan compiler and coverage tests |
| <!-- boatstack-claim:irreversible-operations -->A failed external write led to an invented reset path | It denies high-confidence destructive recovery before execution | Host-hook fixtures; overall benefit still being evaluated |
| <!-- boatstack-claim:reviewer-ready-pr -->A PR lost the decisions and gaps behind the change | It builds a review brief from the approved scope, actual diff, and recorded evidence | PR projection and stale-preview tests |
| <!-- boatstack-claim:git-worktree-activation -->A new worktree had the safety hook but not its ignored helper | It restores the verified local runtime from the clone before judging the first command | Real linked-worktree and tamper tests |

[Read what happened, what is tested, and what remains open](docs/why-these-steps.md). The machine-readable [claim record](docs/public-claims.json) keeps the public wording tied to its sources.

Expand All @@ -37,6 +38,8 @@ Install Boatstack in this repository from https://github.com/operatorstack/boats

Install Boatstack in its own infrastructure PR and merge that PR before starting a feature. This keeps one-time repository setup out of later product diffs.

Install once per Git clone. Linked Git worktrees reuse the verified runtime and restore their ignored local helper automatically on first use.

## Start with two moves

1. Create and save a plan in your coding tool's Plan mode.
Expand Down
38 changes: 20 additions & 18 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"canonical_context": {
"characters": 32685,
"estimated_tokens": 8172,
"characters": 33244,
"estimated_tokens": 8311,
"estimator": "ceil(total characters / 4); compactness signal, not provider billing",
"files": [
"product-engineering-loop/references/workflow.md",
Expand All @@ -12,8 +12,8 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "03ac814fc7ad16af762ddfd019b9d07227a1afda535fbcea8ba1653818123002",
"README.md": "9dcc7ba6fd7032b2a11d66891a353508f2f23ffa8fb8969a32a94a5e6db1a3aa",
"CONTRIBUTING.md": "fbc9acf97597aaad553fc0a1b78af6ba70b92eb4174c61d46718595f765765bd",
"README.md": "9e82e07dd95021ea9cb307e0e13a14747852be051d8a9b1261db15b24b919879",
"assets/boatstack-journey.svg": "c1f7fe2741f5e9ca66bb3fe9b103e6364ba5acbca8b7a8054768ffd85cf325ea",
"assets/boatstack-mark.svg": "c46e935f06fcfde3b37abfd579c1963b765b2337a0fa993f9538c9b652297e39",
"assets/boatstack-model-choice.svg": "979952c2fc6220d41426f9072186fca000fb3f388a4e775cc09cc1e830ebdde4",
Expand All @@ -31,27 +31,29 @@
"boatstack/assets/templates/test-plan.md": "6db8a9f27dd171fb80222a501cae50eb051e7278c04703fa43b5ff86dd4d2df4",
"boatstack/atomic_unix.go": "89f2723361591de2bb8bd22ce7e34ec529d3278509f0df78fd5c4a7d4140fbe9",
"boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f",
"boatstack/cmd/boatstack-helper/main.go": "93a3eda3c6216018d83e0e419e201da7a046aab8f36f9dc6cb2f8b0842335242",
"boatstack/cmd/boatstack-helper/main.go": "32d73f6ac8e87cbe7ebd8ee74e63dfe6f0b8b35da1d3200e23dcf011be9c19ea",
"boatstack/export.go": "c53c5ec83dcea392d2e360c6819202b5f7cf9b3ae64510087bd627c4aaee82e3",
"boatstack/export_test.go": "f95be9c458645e9b150f25921a0452c9e10e6a94809567ee5ddbc8e833e4f040",
"boatstack/go.mod": "57c377eccea51372d6664de4169e2ca45806b046f7e8a98a1e35a9eb454b4b8d",
"boatstack/hooks.go": "3030ca262a39b5bbef8509bb1395b3b9635719dd9ce0b196f72c596922509e92",
"boatstack/hooks_test.go": "5b8852e6176d96315c983f261d8503b57298063eb251283088eb103e42d7ec0d",
"boatstack/init.go": "74e8f1dda5761c36d62c6679ee8e935138115b930f654c9b9d04a153af2642a7",
"boatstack/hooks.go": "aed9cee6d3e3fb42e5e98288eade420793a07a4b071e78a613fc888c74521406",
"boatstack/hooks_test.go": "a5298b7f46709bce617913085fe3b597a4bb4f730a5b5b51f459be85adbefcbb",
"boatstack/init.go": "ddee0cfd8ab9f3416fb895afd99d1682ca09550d2d9b2ad3cd11f5e06438c585",
"boatstack/init_test.go": "b761ada1f5a04c0a27225a6f1eb99baf5477a424c5a9748a3267f07ba5a84605",
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
"boatstack/plan.go": "519fe7a782c0384d62fda228c58145d36e01a3941691b1839a2d1476528c27b7",
"boatstack/plan_test.go": "006cdc6681f77e579c5a0f709e30ede759c337132d4f2f5193b7b79b29bd7149",
"boatstack/planning.go": "9485eedde503f54141388ea91d6d7a17f5d59663a38db2af9fddcd933f057571",
"boatstack/planning.go": "3a26417a295e5dfc2b6dcac702287c04b6053e7b74215858a4ea11cf9f9dadfe",
"boatstack/planning_test.go": "4662908c1ec063aa8ef6f91db52247864303d9b91ef2363a8f68b41082fe383f",
"boatstack/pr.go": "6fbfd1e673c55e8358090693b20edb9bc6e8efb8913de9ffcaa5143ca24f0947",
"boatstack/pr_test.go": "f200a3a860e3da22798ec17a8eaf335724885b09d48b36bf0c350acdd3cc3ab7",
"boatstack/references/artifacts.md": "0a72961aa7a942056f3185417f545d205a3ce21856e602e367141660298c9410",
"boatstack/references/failure-moves.md": "5ac4bbc279a1f7c2b420c15b0f9bc73fb15c37c8f2ec08c45e1acd3aae46b75a",
"boatstack/references/failure-moves.md": "1d35126348d0b681976e8819665e16fd745fd65eca271492603cb80aab75bf49",
"boatstack/references/irreversible-operation-boundary.md": "2a695f2d7de95cfc8750f107bef9c86581712aa1f02e7233b69b850d8c2af42e",
"boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae",
"boatstack/references/workflow.md": "0a32f00c12ea1d92db2e3b29ce5cfdcdd5a013c50ae67f2fc56f0ebde6951988",
"boatstack/runtime.go": "b988d57ec14e15fc6a57949a995879fc0e0d6bfa9a7b62935e7754df0b85d87a",
"boatstack/runtime_cache.go": "ab0fbb7f8a2eb8d8428e928fbc3d8866c84104e7cb330a901c121dfec82cddb3",
"boatstack/runtime_cache_test.go": "4cbca9dec7800d7df6e3ec0d74c7ecbe1508e5c5a288d863f35fc8d22986c308",
"boatstack/safety.go": "fbf30c34642db6ac18e0e15abbf78cbcd9177cc7aa678b44b4eaabc0202f5bd5",
"boatstack/safety_test.go": "62375fd640d543ab8875c7b31fd935ac7f5385830f625123f44508e629b4ff08",
"boatstack/testdata/reviewer-pr-body.md": "4c64e3788e5d61a377aeb0f797f7fc8d2316ab6e49572d15636eea7ba9e34ac4",
Expand All @@ -62,22 +64,22 @@
"docs/account-recovery-walkthrough.md": "acd3558a95f48004f18a0590670de496e1cc9f0cd1d187f924615497f57e1d6f",
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/evidence-engineered-coding.md": "3fb731119f5d11bc9340e61a8d5a6535709a777379d1d1269764e6be2e9e08ef",
"docs/generated-files.md": "040149341dceb192ba240edc250f8d3e4124b9b9444d0628df42dab667192db6",
"docs/getting-started.md": "432b64d3de11ffe56204f6eb12712c714eca31620d51afead482be70c76a2f35",
"docs/public-claims.json": "60f57b0c796c0f662ef72929288f537ea9414da69437bff9f7fd106af047063c",
"docs/evidence-engineered-coding.md": "7240e06a5f1ee2fc6cecf2777f6438a5b7ebdaae96fe8f7ef626886dcab6f666",
"docs/generated-files.md": "33ea0799eb01af6e035fb1322c6a58c39ecc5dcc95169ce0adbd7933a0bfbee1",
"docs/getting-started.md": "9741947c4b072c0838d0ee3a578215d5fac1d72cf5e73075136d0a9c95db2bbb",
"docs/public-claims.json": "a011f7fc31dc656428f3544b61e34ebef2806f376635151cb44571b31e5422f7",
"docs/public-surface.md": "53d741f04b2928a6ee8c006d647a6d675a215e863412e5862cd67d48433bff76",
"docs/research-and-design.md": "d65c66e323037bda5d45aacef5d48afa6bf93da55901378891d235aca3a5684f",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
"docs/troubleshooting.md": "a9f28e156702a970792421766d38cab3fb99a6a9ce497bd058ee4026608798ff",
"docs/troubleshooting.md": "27e73986a30df5d011b33c3d4701ce30610359e32c496c50d1b783ef5adf5c69",
"docs/validation-and-evidence.md": "a9fe9274f3dc22b152094a307feda5d8c3ab099755100aef77bda13024cc3166",
"docs/why-these-steps.md": "dc633f3edcc0c9d94ee7ea3feb57220987ad63ee8fb08b3df6e438dec866cacd",
"docs/why-these-steps.md": "e8f8918ad42b74314cf84974f6f4a7b4e79af4fc4571b6df7554e3c95f9e0da7",
"examples/diagram-json/README.md": "061b583180e43bbd26618bbd9d3d79af4b75d7c8f37c66475640745a97328fbc",
"examples/diagram-json/approval.md": "bc421a825349923512d5cb0ce489310d3a4d7cbac35e661a693b4a32eec263d1",
"examples/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d",
"examples/diagram-json/compiled/tasks.json": "f040696f1f8bcedc4a8ed9816a61a49edbda970ec0cc3b28175ba37b73bbc896",
"examples/diagram-json/compiled/test-matrix.json": "6c6895c509271e4337f3c91d9f62ee3a2b34e768e78513784cb012506a328ecf",
"examples/diagram-json/plan.lock.json": "4103a084bb610e1fc2fdf3257fd2ca2bb4f82f181da6f71faea73e1275d333a7",
"examples/diagram-json/plan.lock.json": "fe627c500ffccf639c113ec7eda6e1d20b39aaf643672ca7721e518b5d8f6ebe",
"examples/diagram-json/plan.md": "3ad35cc3cbe48306e7ee401bd9e9047d25e46c8a6fe9679aa1b3f5e96ceea292",
"examples/diagram-json/questions.md": "1a0050041cac0a8d53e6ebfe04cbec4a298cdc8c50efeeb6fa15aeb663c5ec76",
"examples/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand All @@ -90,7 +92,7 @@
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "9bfb96de291e2552d307fa3c1b23e67771bcb86d",
"commit": "09388a6c92ece15283a8d0dc2b7edef3d2cc3aba",
"path": "examples/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
23 changes: 23 additions & 0 deletions boatstack/cmd/boatstack-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,27 @@ func safetyHookCommand(arguments []string) int {
return 0
}

func bootstrapSafetyHookCommand(arguments []string) int {
flags := flag.NewFlagSet("bootstrap-safety-hook", flag.ContinueOnError)
host := flags.String("host", "", "cursor, claude, or codex")
repo := flags.String("repo", ".", "worktree protected by the hook")
if err := flags.Parse(arguments); err != nil {
return 2
}
input, err := io.ReadAll(os.Stdin)
if err != nil {
input = nil
}
if err := boatstack.HydrateWorktree(*repo); err != nil {
return fail(fmt.Errorf("worktree runtime activation failed: %w", err))
}
value, _ := boatstack.HookDecision(boatstack.SafetyHookOptions{Host: *host, Repo: *repo, Input: input})
if len(value) > 0 {
fmt.Print(string(value))
}
return 0
}

func checkSafetyCommand(arguments []string) int {
flags := flag.NewFlagSet("check-safety", flag.ContinueOnError)
repo := flags.String("repo", ".", "repository whose operational diff should be checked")
Expand Down Expand Up @@ -398,6 +419,8 @@ func run() int {
return doctorCommand(os.Args[2:])
case "safety-hook":
return safetyHookCommand(os.Args[2:])
case "bootstrap-safety-hook":
return bootstrapSafetyHookCommand(os.Args[2:])
case "check-safety":
return checkSafetyCommand(os.Args[2:])
case "version":
Expand Down
92 changes: 82 additions & 10 deletions boatstack/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
const hookCommandMarker = ".product-loop/hooks/guard"

func guardShellScript() []byte {
return []byte(`#!/usr/bin/env bash
return []byte(fmt.Sprintf(`#!/usr/bin/env bash
# Generated by Boatstack. Do not edit; change canonical source or .boatstack-project.json.
set -u

Expand All @@ -22,33 +22,105 @@ if [[ -z "$ROOT" ]]; then
exit 2
fi

HELPER="$ROOT/.product-loop/bin/boatstack-helper"
COMMON="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)"
if [[ -z "$COMMON" ]]; then
echo "Boatstack safety guard could not resolve the Git common directory; denying tool execution." >&2
exit 2
fi

case "$(uname -s)" in
Darwin) OS_NAME="darwin"; EXTENSION="" ;;
Linux) OS_NAME="linux"; EXTENSION="" ;;
MINGW*|MSYS*|CYGWIN*) OS_NAME="windows"; EXTENSION=".exe" ;;
*) echo "Boatstack safety guard found an unsupported operating system; denying tool execution." >&2; exit 2 ;;
esac
case "$(uname -m)" in
x86_64|amd64) ARCH="amd64" ;;
arm64|aarch64) ARCH="arm64" ;;
*) echo "Boatstack safety guard found an unsupported architecture; denying tool execution." >&2; exit 2 ;;
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"
if [[ ! -x "$HELPER" ]]; then
echo "Boatstack safety helper is missing; rerun the installer from the repository root." >&2
echo "Boatstack shared runtime is missing; run the verified installer once from any checkout in this Git clone." >&2
exit 2
fi
if [[ -L "$HELPER" || ! -f "$MANIFEST" || -L "$MANIFEST" ]]; then
echo "Boatstack shared runtime is unsafe or incomplete; rerun the verified tagged installer." >&2
exit 2
fi
EXPECTED="$(sed -n 's/.*"binary_sha256"[[:space:]]*:[[:space:]]*"\([0-9a-f]\{64\}\)".*/\1/p' "$MANIFEST" | head -n 1)"
if command -v sha256sum >/dev/null 2>&1; then
ACTUAL="$(sha256sum "$HELPER" | awk '{print $1}')"
elif command -v shasum >/dev/null 2>&1; then
ACTUAL="$(shasum -a 256 "$HELPER" | awk '{print $1}')"
else
echo "Boatstack cannot verify the shared runtime checksum; denying tool execution." >&2
exit 2
fi
if [[ -z "$EXPECTED" || "$ACTUAL" != "$EXPECTED" ]]; then
echo "Boatstack shared runtime checksum is invalid; rerun the verified tagged installer." >&2
exit 2
fi

exec "$HELPER" safety-hook --host "$HOST" --repo "$ROOT"
`)
exec "$HELPER" bootstrap-safety-hook --host "$HOST" --repo "$ROOT"
`, Version, SourceCommit, Version, SourceCommit))
}

func guardPowerShellScript() []byte {
return []byte(`# Generated by Boatstack. Do not edit; change canonical source or .boatstack-project.json.
return []byte(fmt.Sprintf(`# Generated by Boatstack. Do not edit; change canonical source or .boatstack-project.json.
param([Parameter(Mandatory=$true)][string]$HostName)
$ErrorActionPreference = "Stop"
$root = (& git rev-parse --show-toplevel 2>$null)
if (-not $root) {
[Console]::Error.WriteLine("Boatstack safety guard could not resolve the repository; denying tool execution.")
exit 2
}
$helper = Join-Path $root ".product-loop/bin/boatstack-helper.exe"
$common = (& git rev-parse --path-format=absolute --git-common-dir 2>$null)
if (-not $common) {
[Console]::Error.WriteLine("Boatstack safety guard could not resolve the Git common directory; denying tool execution.")
exit 2
}
$architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
$arch = switch ($architecture) {
"x64" { "amd64" }
"arm64" { "arm64" }
default {
[Console]::Error.WriteLine("Boatstack safety guard found an unsupported architecture; denying tool execution.")
exit 2
}
}
$helper = Join-Path $common "boatstack/runtimes/%s/%s/windows-$arch/boatstack-helper.exe"
$manifestPath = Join-Path $common "boatstack/runtimes/%s/%s/windows-$arch/runtime.lock.json"
if (-not (Test-Path -LiteralPath $helper -PathType Leaf)) {
[Console]::Error.WriteLine("Boatstack safety helper is missing; rerun the installer from the repository root.")
[Console]::Error.WriteLine("Boatstack shared runtime is missing; run the verified installer once from any checkout in this Git clone.")
exit 2
}
$helperInfo = Get-Item -LiteralPath $helper
if (($helperInfo.Attributes -band [IO.FileAttributes]::ReparsePoint) -or -not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
[Console]::Error.WriteLine("Boatstack shared runtime is unsafe or incomplete; rerun the verified tagged installer.")
exit 2
}
$manifestInfo = Get-Item -LiteralPath $manifestPath
if ($manifestInfo.Attributes -band [IO.FileAttributes]::ReparsePoint) {
[Console]::Error.WriteLine("Boatstack shared runtime manifest is unsafe; rerun the verified tagged installer.")
exit 2
}
try {
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
$actual = (Get-FileHash -LiteralPath $helper -Algorithm SHA256).Hash.ToLowerInvariant()
} catch {
[Console]::Error.WriteLine("Boatstack could not verify the shared runtime; denying tool execution.")
exit 2
}
if (-not $manifest.binary_sha256 -or $actual -ne $manifest.binary_sha256.ToLowerInvariant()) {
[Console]::Error.WriteLine("Boatstack shared runtime checksum is invalid; rerun the verified tagged installer.")
exit 2
}
& $helper safety-hook --host $HostName --repo $root
& $helper bootstrap-safety-hook --host $HostName --repo $root
exit $LASTEXITCODE
`)
`, Version, SourceCommit, Version, SourceCommit))
}

func hookCommand(host string) string {
Expand Down
24 changes: 23 additions & 1 deletion boatstack/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,33 @@ func TestMissingHelperLauncherFailsClosed(t *testing.T) {
command := exec.Command("bash", path, "cursor")
command.Dir = repo
output, err := command.CombinedOutput()
if err == nil || !strings.Contains(string(output), "helper is missing") {
if err == nil || !strings.Contains(string(output), "shared runtime is missing") {
t.Fatalf("missing helper did not fail closed: err=%v output=%s", err, output)
}
}

func TestGuardRejectsTamperedSharedRuntimeBeforeExecution(t *testing.T) {
if _, err := exec.LookPath("bash"); err != nil {
t.Skip("bash unavailable")
}
repo := runtimeTestRepo(t)
binaryPath, _, err := sharedRuntimePaths(repo, Version, SourceCommit)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(binaryPath, []byte("tampered"), 0o755); err != nil {
t.Fatal(err)
}
path := filepath.Join(repo, ".product-loop", "hooks", "guard.sh")
command := exec.Command("bash", path, "claude")
command.Dir = repo
command.Stdin = strings.NewReader(`{"tool_name":"Bash","tool_input":{"command":"git status --short"}}`)
output, runErr := command.CombinedOutput()
if runErr == nil || !strings.Contains(string(output), "checksum is invalid") {
t.Fatalf("tampered shared helper was not denied before execution: err=%v output=%s", runErr, output)
}
}

func TestHookFragmentsAreValidJSON(t *testing.T) {
for _, host := range []string{"cursor", "claude", "codex"} {
value, err := hookFragmentJSON(host)
Expand Down
Loading
Loading