diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 73a9fb9..3af28b5 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/3cd11c293b3903aeac9ff8d8eacd47840d63a02d/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/29f36332e8e249528c6b088473d65e1190ff00b8/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.
diff --git a/README.md b/README.md
index 46e8ba0..4360576 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,49 @@
-# Boatstack
+
+
+
-**Build freely. Prove it. Ship.**
+
Boatstack
-Boatstack is **evidence-engineered coding**: a model-neutral coding node that turns product intent and repository context into an explicitly approved, tested, reviewable change. It does not prescribe the model, implementation technique, tools, or document structure. It governs what may be claimed, approved, or shipped. Its behavior is generated from [Intelligence Flow at `3cd11c293b3903aeac9ff8d8eacd47840d63a02d`](https://github.com/operatorstack/intelligence-flow/tree/3cd11c293b3903aeac9ff8d8eacd47840d63a02d/examples/12-product-engineering-loop).
+
Build freely. Prove it. Ship.
+
+Boatstack is **evidence-engineered coding**: a model-neutral coding node that turns product intent and repository context into an explicitly approved, tested, reviewable change. It does not prescribe the model, implementation technique, tools, or document structure. It governs what may be claimed, approved, or shipped. Its behavior is generated from [Intelligence Flow at `29f36332e8e249528c6b088473d65e1190ff00b8`](https://github.com/operatorstack/intelligence-flow/tree/29f36332e8e249528c6b088473d65e1190ff00b8/examples/12-product-engineering-loop).
> **You are free in how you build. Only claims of completion require evidence.**
It is not a claim that a longer prompt writes better code. Here is what the node actually makes observable.
+## Install in a repository
+
+Install Boatstack on a clean infrastructure branch and merge that PR before starting product work. This keeps the one-time host adapters and repository policy separate from every feature diff.
+
+macOS or Linux:
+
+```bash
+git switch -c chore/install-boatstack
+/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/operatorstack/boatstack/main/install.sh)"
+```
+
+Windows PowerShell:
+
+```powershell
+git switch -c chore/install-boatstack
+irm https://raw.githubusercontent.com/operatorstack/boatstack/main/install.ps1 | iex
+```
+
+The installer previews the generated paths, verifies the platform helper, asks about optional gstack and Spec Kit integrations, runs a smoke check, and prints the exact infrastructure commit commands. Boatstack core requires no Python, Node, Go, or package manager. The helper is repository-local and ignored; the adapters and policy are committed.
+
+**New here?** [Install and ship your first feature](docs/getting-started.md) · [Understand generated files](docs/generated-files.md) · [Troubleshoot](docs/troubleshooting.md) · [See the real account-recovery walkthrough](docs/account-recovery-walkthrough.md)
+
+**Go deeper:** [Validation and evidence](docs/validation-and-evidence.md) · [gstack and Spec Kit](#use-boatstack-with-gstack-and-github-spec-kit) · [Evidence-engineered coding](docs/evidence-engineered-coding.md)
+
+```text
+idea -> Plan mode -> /auto-plan -> questions -> /plan-gate
+ -> approve -> Build -> /build -> /test-gate
+ -> /review-gate -> /ship-gate -> PR
+```
+
## Plan first, then auto-plan
Start with ordinary product intent **inside Cursor, Codex, or Claude Plan mode**:
@@ -102,6 +136,8 @@ Choose the host's normal Build action.
`/plan-gate` presents a fingerprint over the complete source plan, spec, and `plan.md` and requires a named human. Explicit approval creates only `approval.md`, so the developer remains in Plan mode. The host's normal Build transition then validates and activates the exact approved plan before editing code.
+The source plan remains required and hash-checked through `/build`; later gates rely on the resulting lock, actual diff, and accumulated evidence.
+
Internal deterministic boundary
@@ -163,24 +199,6 @@ existing product docs + code -> questions -> feature spec -> approval -> enginee
Product documents define what and why. ADRs record durable technical decisions. Gaps record known incomplete work. Boatstack references these sources without replacing them. Any generated spec or plan must remain traceable to its sources and reviewable as a lossy task projection. No context map or documentation migration is required in V1; the project config may list useful starting paths when a repository wants stable defaults.
-## Install into a repository
-
-macOS or Linux:
-
-```bash
-/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/operatorstack/boatstack/main/install.sh)"
-```
-
-Windows PowerShell:
-
-```powershell
-irm https://raw.githubusercontent.com/operatorstack/boatstack/main/install.ps1 | iex
-```
-
-Run the command from the product repository. The installer detects the repository and available coding hosts, previews every generated path, verifies the downloaded helper, and asks whether to add gstack, Spec Kit, both, or core only. Boatstack core requires no Python, Node, Go, or package manager.
-
-After installation, open the chosen host's Plan mode, describe the change, save its plan, and run `/auto-plan`. Boatstack uses the host-exposed active path or discovers exactly one file under `.product-loop/intake/`; an explicit path is only needed to resolve ambiguity. Auto-plan and plan-gate write Markdown only. After explicit approval, use the host's normal Build transition; `/build` activates and locks the plan before its first code edit. The source plan remains required and hash-checked through `/build`; later gates use the lock, actual diff, and evidence.
-
## Use Boatstack with gstack and GitHub Spec Kit
Boatstack is primarily a **control and evidence layer** over your coding host and optional planning/review tools. It does not need to reproduce everything those projects already do well:
@@ -283,7 +301,7 @@ Read the [research and design record](docs/research-and-design.md) and [corpus a
## Context has a budget
-The three canonical runtime references currently total approximately **4521 estimated tokens** using `ceil(characters / 4)`. That is a stable compactness signal, not provider billing. Host adapters stay thin and load the operation-specific slice on demand.
+The three canonical runtime references currently total approximately **4625 estimated tokens** using `ceil(characters / 4)`. That is a stable compactness signal, not provider billing. Host adapters stay thin and load the operation-specific slice on demand.
## Status
diff --git a/UPSTREAM.json b/UPSTREAM.json
index 96bdc20..a30996e 100644
--- a/UPSTREAM.json
+++ b/UPSTREAM.json
@@ -1,7 +1,7 @@
{
"canonical_context": {
- "characters": 18081,
- "estimated_tokens": 4521,
+ "characters": 18498,
+ "estimated_tokens": 4625,
"estimator": "ceil(total characters / 4); compactness signal, not provider billing",
"files": [
"product-engineering-loop/references/workflow.md",
@@ -11,9 +11,10 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
- "CONTRIBUTING.md": "baaa5828ac493da05a5cf517524175534c318728830e191deb510e492b7d9c0f",
- "README.md": "de733eb7d8a15a218cd52d37f510acfca1c3bdccc5085dfce8d9e09ea89d3ba2",
- "boatstack/SKILL.md": "56cc1da6dea4fb57bd16d37ec9635eb06676d5e8580511190cddc78cd9e37346",
+ "CONTRIBUTING.md": "a24e1ad4601735f38b7499fc12a01273e7d5ef8871e87bdbf532a624b944abc7",
+ "README.md": "f423e876419caf5d4a84369fb41c07f6ea6b94e19826f8b8a0b3c5c04a31c3e1",
+ "assets/boatstack-mark.svg": "c46e935f06fcfde3b37abfd579c1963b765b2337a0fa993f9538c9b652297e39",
+ "boatstack/SKILL.md": "724dddef577576fb4c26e871422a4304fea3dc1d8b0f9324da200f20cd93078c",
"boatstack/agents/openai.yaml": "68a30a60859556c5a26e16d184594ca243a6043d99c8cf7d66b5dd6d50a93cd1",
"boatstack/assets/templates/adr.md": "c577a3c1c1319061f61deb053597e6e853657022185fe28b8f733327e2a78565",
"boatstack/assets/templates/approval.md": "74b0b816703a6dce3c96c8f95f981af910b020b6908e7f76cf5630778637e9f5",
@@ -25,31 +26,39 @@
"boatstack/assets/templates/plan.md": "358216e38d41f930c23c526741b718f30100c0071c8373a0c8b765d0e59f3fdf",
"boatstack/assets/templates/questions.md": "5875bbfc32d5a1b326c2a48da7449bb90c87f462a4c3a862173247e5f7ea6415",
"boatstack/assets/templates/test-plan.md": "6db8a9f27dd171fb80222a501cae50eb051e7278c04703fa43b5ff86dd4d2df4",
- "boatstack/cmd/boatstack-helper/main.go": "853e60ba22766567afc3f7526260ab70b9056645c5c0500d97a9f3fc0f3a0bf0",
- "boatstack/export.go": "7d70967d28032a76e58e083f07f27ad9609b0ab894dd208b1d2f3b726c8cd751",
- "boatstack/export_test.go": "8f78c9bb50210161a19878d17775e73ffb591502d6ee11e6428ac436c128fe23",
+ "boatstack/atomic_unix.go": "89f2723361591de2bb8bd22ce7e34ec529d3278509f0df78fd5c4a7d4140fbe9",
+ "boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f",
+ "boatstack/cmd/boatstack-helper/main.go": "e21101b8df6170c01a98bff71f8a2e9d7ac9ef92b883ec50020eb3a011a7737d",
+ "boatstack/export.go": "75e21e6602f7410bd332c2651097e77463976adc6977edd5b0415e33a25067e8",
+ "boatstack/export_test.go": "33c4d0754e59cfb6fc92938ffb3547f267a5dfdd6e8d5f8c359bef349b2862d4",
"boatstack/go.mod": "daf262a00abfe961d8ca266d4b26eea09a6aee73e4c53baaa537a809eaef59f6",
- "boatstack/init.go": "6a04d5482748bd3b6a9febdf1723b89f8460144bcd5315d5feda0f82de477f07",
- "boatstack/init_test.go": "7ca705f014f6bb22f7ea61a1eb370b98d1bc722f77a73711749375fb7a08ea14",
+ "boatstack/init.go": "4bcc010420f99a6d55b7ca4a48daa4ac49ced91c16fa6f7bdd7886b064324e22",
+ "boatstack/init_test.go": "1d1c51be4ef248ea1de0fbfd0a0af220f17247ed06ba353c690bfa700c95bf5d",
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
"boatstack/plan.go": "8250ee93f91b4dd60e3788636ab79f32c626ace6daef6677fdc6338858f89a52",
"boatstack/plan_test.go": "f95ce7a38276f957064ee83901566c84ebf3eec0683e81794f823dc446b2ac54",
+ "boatstack/planning.go": "e3d8250d3343d724e0161cafd7f30202bc1b959b415697a99771067394aedc00",
+ "boatstack/planning_test.go": "9d19a64ee3b5361ef4413ee27f6d05b7e12ae5fc89e039f5ffec16095d6bdeea",
"boatstack/references/artifacts.md": "22b6cc596b65c8c2a22b0f3a692ca16a808fa495dad0b4453d23ea734978a2b7",
"boatstack/references/failure-moves.md": "2d7d3988c70718e9cc02104f9899a00208173e2f654d1046edd22079f4d46f41",
"boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae",
- "boatstack/references/workflow.md": "0b22fbe168aa31a3ac2832f2439367224aedd2b59468941ee45092b2c1ada1a9",
+ "boatstack/references/workflow.md": "2829b34cb5d62cca4c8a59e6ac3a62e75b5c6d6b3196005b36c3769e94ba262b",
"boatstack/runtime.go": "66c02aa0b6e9c031c26799b86dfbcb26df74487cc26b32f7fb85e38707eeec31",
+ "docs/account-recovery-walkthrough.md": "912edec85d930750c044bcd6117df9d03491a8c91f139af3ee82ab853452f1b7",
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
- "docs/evidence-engineered-coding.md": "453536fa767b98e4671ba885375a5f9af19cb95adb88444691e6e8905966fecf",
+ "docs/evidence-engineered-coding.md": "7e66d214d0f59bc7ecce42b7b0ffb3f612cdbf00b7ca10db614d5a181cab9184",
+ "docs/generated-files.md": "0ca73d52bb286b86324c6bcf0ef1c5aee5dc831e35b826b8a9e8cce65c311505",
+ "docs/getting-started.md": "4607ed828b6aa372a350046021bc8c2f6a758927554864a20b2e4a445ee7b57b",
"docs/research-and-design.md": "67dc454f0d13e0e2809f49e910f7847457fa78f03c4cef6fb0138f6039a57c4f",
+ "docs/troubleshooting.md": "6a2f8483d6f1fb7e0ae6265f3a07af05583e8c5585b8b887d307702bd9b14705",
"docs/validation-and-evidence.md": "3b5ed588bd44c5568f0c313be0dfaa411e959dc184fe886dfd0a81aee9fd25cc",
"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": "81617b3d642c50734c4b7188d639a244f344da16b5e35c6d0e0af8bd354efe37",
+ "examples/diagram-json/plan.lock.json": "a0df918570abdf146061852563a4684131fa7b5ba4b20108af9b537a7770b025",
"examples/diagram-json/plan.md": "3ad35cc3cbe48306e7ee401bd9e9047d25e46c8a6fe9679aa1b3f5e96ceea292",
"examples/diagram-json/questions.md": "1a0050041cac0a8d53e6ebfe04cbec4a298cdc8c50efeeb6fa15aeb663c5ec76",
"examples/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
@@ -62,7 +71,7 @@
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
- "commit": "3cd11c293b3903aeac9ff8d8eacd47840d63a02d",
+ "commit": "29f36332e8e249528c6b088473d65e1190ff00b8",
"path": "examples/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
diff --git a/assets/boatstack-mark.svg b/assets/boatstack-mark.svg
new file mode 100644
index 0000000..6ccbd04
--- /dev/null
+++ b/assets/boatstack-mark.svg
@@ -0,0 +1,9 @@
+
diff --git a/boatstack/SKILL.md b/boatstack/SKILL.md
index c1ec84a..0a1938f 100644
--- a/boatstack/SKILL.md
+++ b/boatstack/SKILL.md
@@ -59,12 +59,12 @@ Do not scan the entire repository by default. Record discovered paths and comman
3. Separate facts, decisions, unknowns, and safely deferrable gaps.
4. Answer discoverable code questions by inspection.
5. Ask the developer only questions whose answers materially change behavior, contracts, risk, or acceptance. Ask 1-3 concise questions at a time, give 2-3 mutually exclusive choices, recommend one, and explain the impact. If the host has no structured question tool, ask the same questions as plain text, return `WAITING_FOR_INPUT`, and do not select a default.
-6. Record answers and provenance in the question ledger. A repository-derived choice is `PROPOSED`, not `ANSWERED`, until a human accepts it. Copy material open question IDs into `plan.md` as `blocking_questions`.
+6. Record answers and provenance in the question ledger. An authoritative repository fact is `DISCOVERED`, an agent suggestion or inferred choice is `PROPOSED`, and only an explicit human response is `ANSWERED`. Every material proposal remains in `plan.md` as a `blocking_questions` ID until the human answers it. Never use labels such as “answered by plan default.”
7. Create the feature spec: problem, users, outcomes, non-goals, acceptance criteria, invariants, interfaces, failure behavior, observability, rollout, and rollback. Translate every accepted claim into an observable condition with a defensible oracle.
8. Run product, design, engineering, and developer-experience reviews only when applicable. If gstack is installed, its review skills can implement these lenses; do not require it.
9. If Spec Kit is installed, use its constitution/specify/clarify/plan/tasks/analyze/checklist flow as an artifact generator. The canonical artifact contract remains authoritative.
10. For every planned validation, record the exact `criteria` it can support plus `run`, `origin`, `oracle`, and `independence`. Commands, automated tests, external checks, and named human review procedures are all valid forms, but an ambiguous claim without a threshold/rubric and authorized decision remains `BLOCKED`.
-11. Write only Markdown feature artifacts, including the canonical structured `plan.md`. Put its authoritative JSON inside the marked Boatstack block and run `boatstack-helper check-plan --plan /plan.md`; this command is read-only.
+11. Write only Markdown feature artifacts, including the canonical structured `plan.md`. Put its authoritative JSON inside the marked Boatstack block and run `boatstack-helper check-plan --plan /plan.md`; this command is read-only. If the host blocks its ordinary Markdown writer, pass the document to `boatstack-helper planning-write --repo . --feature --artifact ` on stdin. Never use arbitrary shell redirection to evade a host write boundary.
12. End with a **draft**, never an implied approval. Do not generate executable task state, JSON artifacts, locks, or implementation changes from `auto-plan`.
Do not treat an ADR as general project context. ADRs record accepted durable decisions. Use a question ledger for unknowns and a gap ledger for known divergence.
@@ -83,13 +83,14 @@ Treat repository-owned product context as canonical. Do not require it to be mig
2. Present the draft spec, plan, open decisions, accepted assumptions, gaps, risks, validation provenance, and `PLAN_FINGERPRINT` in a reviewable form.
3. Ask the developer to approve it or request changes. Silence, continued conversation, tool permission, and permission to build are not approval.
4. On changes, return to `auto-plan`, preserve the feedback in the question ledger, and issue a new draft.
-5. On explicit approval, create only `approval.md` from its template. Record the named human, an RFC3339 timestamp, and the exact fingerprint returned before approval.
+5. On explicit approval, invoke `boatstack-helper record-approval` with the plan, named human, RFC3339 timestamp, and exact fingerprint returned before approval. It verifies the current plan and creates only `approval.md`.
6. End in Plan mode and tell the developer the feature is approved and ready for the host's normal Build transition. Do not compile tasks, create a lock, request Agent mode merely to write a file, or edit product code.
All files created or updated by `auto-plan` and `plan-gate` must be Markdown. gstack and Spec Kit may help produce those documents, but their implementation stages and non-Markdown executable state are deferred to `build`.
## Build without erasing evidence
+- First confirm the host is in an execution-capable mode. If a requested transition is rejected or product-code writes remain unavailable, return `READY_FOR_BUILD` and stop without activating, compiling, or writing a lock.
- Before the first product-code edit, activate the exact approved Markdown plan:
```bash
@@ -135,6 +136,7 @@ Do not branch the workflow on model brand, price, or a guessed capability tier.
- Require a clean, intentional diff; passing required checks; a filled evidence ledger; explicit known gaps; and rollout/rollback notes.
- Create a PR, but keep merge and deploy as separate authorized actions.
- Never hide failed experiments, skipped checks, or `PASS_WITH_GAPS` behind a green summary.
+- If a required check also fails on the base branch, record that comparison and recommend a separate repair PR. Do not edit unrelated code in the approved feature branch. A bypass is valid only when repository policy permits it and the human explicitly authorizes it; otherwise return to planning for any scope expansion.
Gate statuses are `PASS`, `PASS_WITH_GAPS`, and `BLOCKED`. Critical safety, correctness, or product-acceptance gaps always produce `BLOCKED`.
diff --git a/boatstack/atomic_unix.go b/boatstack/atomic_unix.go
new file mode 100644
index 0000000..5bdefac
--- /dev/null
+++ b/boatstack/atomic_unix.go
@@ -0,0 +1,9 @@
+//go:build !windows
+
+package boatstack
+
+import "os"
+
+func replaceFile(source, destination string) error {
+ return os.Rename(source, destination)
+}
diff --git a/boatstack/atomic_windows.go b/boatstack/atomic_windows.go
new file mode 100644
index 0000000..a3d1e79
--- /dev/null
+++ b/boatstack/atomic_windows.go
@@ -0,0 +1,36 @@
+//go:build windows
+
+package boatstack
+
+import (
+ "fmt"
+ "syscall"
+ "unsafe"
+)
+
+const (
+ moveFileReplaceExisting = 0x1
+ moveFileWriteThrough = 0x8
+)
+
+var moveFileExW = syscall.NewLazyDLL("kernel32.dll").NewProc("MoveFileExW")
+
+func replaceFile(source, destination string) error {
+ sourcePointer, err := syscall.UTF16PtrFromString(source)
+ if err != nil {
+ return err
+ }
+ destinationPointer, err := syscall.UTF16PtrFromString(destination)
+ if err != nil {
+ return err
+ }
+ result, _, callErr := moveFileExW.Call(
+ uintptr(unsafe.Pointer(sourcePointer)),
+ uintptr(unsafe.Pointer(destinationPointer)),
+ uintptr(moveFileReplaceExisting|moveFileWriteThrough),
+ )
+ if result == 0 {
+ return fmt.Errorf("atomic replace failed: %w", callErr)
+ }
+ return nil
+}
diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go
index eb9b821..4299860 100644
--- a/boatstack/cmd/boatstack-helper/main.go
+++ b/boatstack/cmd/boatstack-helper/main.go
@@ -3,6 +3,7 @@ package main
import (
"flag"
"fmt"
+ "io"
"os"
"sort"
"strings"
@@ -137,9 +138,70 @@ func activatePlanCommand(arguments []string) int {
return 0
}
+func planningWriteCommand(arguments []string) int {
+ flags := flag.NewFlagSet("planning-write", flag.ContinueOnError)
+ repo := flags.String("repo", ".", "repository containing the feature package")
+ feature := flags.String("feature", "", "lowercase kebab-case feature slug")
+ artifact := flags.String("artifact", "", "known Markdown planning artifact name")
+ if err := flags.Parse(arguments); err != nil {
+ return 2
+ }
+ if *feature == "" || *artifact == "" {
+ return fail(fmt.Errorf("planning-write requires --feature and --artifact; Markdown content is read from stdin"))
+ }
+ content, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ return fail(err)
+ }
+ path, err := boatstack.WritePlanningArtifact(boatstack.PlanningWriteOptions{
+ Repo: *repo, Feature: *feature, Artifact: *artifact, Content: content,
+ })
+ if err != nil {
+ return fail(err)
+ }
+ fmt.Printf("PASS: wrote bounded planning Markdown: %s\n", path)
+ return 0
+}
+
+func recordApprovalCommand(arguments []string) int {
+ flags := flag.NewFlagSet("record-approval", flag.ContinueOnError)
+ plan := flags.String("plan", "", "approved Markdown plan")
+ output := flags.String("output", "", "approval.md path; defaults beside plan.md")
+ approvedBy := flags.String("approved-by", "", "named human approver")
+ approvedAt := flags.String("approved-at", "", "RFC3339 approval timestamp")
+ fingerprint := flags.String("fingerprint", "", "exact fingerprint displayed before approval")
+ if err := flags.Parse(arguments); err != nil {
+ return 2
+ }
+ if *plan == "" || *approvedBy == "" || *approvedAt == "" || *fingerprint == "" {
+ return fail(fmt.Errorf("record-approval requires --plan, --approved-by, --approved-at, and --fingerprint"))
+ }
+ if err := boatstack.RecordApproval(boatstack.ApprovalRecordOptions{
+ PlanPath: *plan, OutputPath: *output, ApprovedBy: *approvedBy,
+ ApprovedAt: *approvedAt, Fingerprint: *fingerprint,
+ }); err != nil {
+ return fail(err)
+ }
+ fmt.Println("PASS: exact Markdown plan approval recorded")
+ return 0
+}
+
+func doctorCommand(arguments []string) int {
+ flags := flag.NewFlagSet("doctor", flag.ContinueOnError)
+ repo := flags.String("repo", ".", "repository whose Boatstack installation should be checked")
+ if err := flags.Parse(arguments); err != nil {
+ return 2
+ }
+ if err := boatstack.DoctorRepairHint(boatstack.Doctor(*repo)); err != nil {
+ return fail(err)
+ }
+ fmt.Printf("PASS: Boatstack %s installation and generated adapters are healthy\n", boatstack.Version)
+ return 0
+}
+
func run() int {
if len(os.Args) < 2 {
- fmt.Fprintln(os.Stderr, "usage: boatstack-helper ")
+ fmt.Fprintln(os.Stderr, "usage: boatstack-helper ")
return 2
}
switch os.Args[1] {
@@ -151,8 +213,14 @@ func run() int {
return checkSourcePlanCommand(os.Args[2:])
case "check-plan":
return checkPlanCommand(os.Args[2:])
+ case "planning-write":
+ return planningWriteCommand(os.Args[2:])
+ case "record-approval":
+ return recordApprovalCommand(os.Args[2:])
case "activate-plan":
return activatePlanCommand(os.Args[2:])
+ case "doctor":
+ return doctorCommand(os.Args[2:])
case "version":
fmt.Printf("Boatstack %s (%s)\n", boatstack.Version, boatstack.SourceCommit)
return 0
diff --git a/boatstack/export.go b/boatstack/export.go
index 1991251..0975027 100644
--- a/boatstack/export.go
+++ b/boatstack/export.go
@@ -79,7 +79,7 @@ func normalizedAdapters(adapters []string) []string {
func commandBody(operation, extra string) string {
preflight := ""
if operation == "auto-plan" {
- preflight = `Before reading repository context or drafting artifacts, inspect the active host/system conversation for its Plan-mode file path. If present, run the project-local helper with ` + "`check-source-plan --repo . --plan `" + `. Otherwise run ` + "`check-source-plan --repo .`" + `. Use its ` + "`SOURCE_PLAN`" + ` result. Fallback discovery searches only bounded Plan-mode locations and succeeds only for exactly one non-empty file. If discovery blocks, stop and show the candidates or ask the user to save the host plan under ` + "`.product-loop/intake/`" + `. Accept ` + "`/auto-plan `" + ` only as an ambiguity override. Do not create the missing source plan inside auto-plan.`
+ preflight = `Before reading repository context or drafting artifacts, inspect the active host/system conversation for its Plan-mode file path. If present, run the project-local helper with ` + "`check-source-plan --repo . --plan `" + `. Otherwise run ` + "`check-source-plan --repo .`" + `. Use its ` + "`SOURCE_PLAN`" + ` result. Fallback discovery searches only bounded Plan-mode locations and succeeds only for exactly one non-empty file. If discovery blocks, stop and show the candidates or ask the user to save the host plan under ` + "`.product-loop/intake/`" + `. Accept ` + "`/auto-plan `" + ` only as an ambiguity override. Do not create the missing source plan inside auto-plan. If the host blocks its ordinary Markdown write tool, pass each known planning document on stdin to ` + "`boatstack-helper planning-write`" + `; never bypass the host boundary with arbitrary shell redirection.`
}
return fmt.Sprintf(`# %s
@@ -89,7 +89,7 @@ Run the %s operation from @.product-loop/workflow.md.
Read @.product-loop/project.json, @.product-loop/artifacts.md, and only the minimal repository context relevant to the current feature. %s
-Use the gate semantics in the canonical workflow. Do not redefine them in this adapter. Auto-plan and plan-gate may create or update Markdown only. If a structured question tool is unavailable, ask 1-3 plain-text questions, return WAITING_FOR_INPUT, and never silently choose a default. Boatstack leaves implementation tactics open, but completion, approval, and shipping claims require current evidence.
+Use the gate semantics in the canonical workflow. Do not redefine them in this adapter. Auto-plan and plan-gate may create or update Markdown only. Classify authoritative repository facts as DISCOVERED, agent suggestions as PROPOSED, and only explicit human responses as ANSWERED. Every material proposal remains in blocking_questions; never label an agent default as answered. If a structured question tool is unavailable, ask 1-3 plain-text questions, return WAITING_FOR_INPUT, and never silently choose a default. Boatstack leaves implementation tactics open, but completion, approval, and shipping claims require current evidence.
`, operation, operation, preflight, extra)
}
@@ -149,13 +149,13 @@ func BuildExportBundle(configPath string, config ProjectConfig, rawConfig []byte
operations := map[string]string{
"auto-plan": "Discover exactly one saved Plan-mode file and refine it into a Markdown-only draft feature package whose canonical structured artifact is plan.md. Run check-plan read-only. Do not implement, create JSON or locks, or imply acceptance.",
- "plan-gate": "Run check-plan read-only, present its fingerprint and all open decisions, and require explicit human approval. On approval write only approval.md with the named human, RFC3339 timestamp, and exact fingerprint. Remain in Plan mode; do not compile or request an early mode switch.",
- "build": "Before the first product-code edit, locate plan.md and approval.md and run the project-local activate-plan command to compile machine artifacts and create and verify plan.lock.json. Stop if it reports BLOCKED. Implementation tactics remain open inside the approved boundary.",
+ "plan-gate": "Run check-plan read-only, present its fingerprint and all open decisions, and require explicit human approval. On approval invoke record-approval with the named human, RFC3339 timestamp, and exact displayed fingerprint so it writes only approval.md. Remain in Plan mode; do not compile or request an early mode switch.",
+ "build": "First confirm the host is in an execution-capable mode. If the mode transition is rejected or product-code writes remain unavailable, return READY_FOR_BUILD without activating the plan, compiling JSON, or writing a lock. Only then locate plan.md and approval.md and run activate-plan before the first product-code edit. Stop if it reports BLOCKED. Implementation tactics remain open inside the approved boundary.",
"test-gate": "Build a requirement-to-evidence matrix and treat self-authored tests as evidence rather than the sole oracle.",
"review-gate": "Review the actual diff against approved intent, invariants, risks, gaps, and test evidence.",
- "ship-gate": "Prepare a PR only; do not merge or deploy without separate authorization.",
+ "ship-gate": "Prepare a PR only; do not merge or deploy without separate authorization. If a required check fails on the base branch too, record the evidence and recommend a separate repair PR. Never edit unrelated code in this approved feature branch; a policy-approved bypass requires explicit human authorization.",
"review": "Alias of review-gate: review the actual diff against approved intent, invariants, risks, gaps, and test evidence.",
- "ship": "Alias of ship-gate: prepare a PR only; do not merge or deploy without separate authorization.",
+ "ship": "Alias of ship-gate: prepare a PR only; do not merge or deploy without separate authorization. Keep pre-existing unrelated failures out of the approved feature branch.",
"retro": "Classify evidence and propose a move; never promote it or change durable rules without a paired gate.",
}
@@ -191,10 +191,12 @@ description: Run Boatstack's evidence-engineered coding node for question-led pl
Read .product-loop/project.json and .product-loop/workflow.md. The requested operation is supplied by the user; valid operations are auto-plan, plan-gate, build, test-gate, review-gate/review, ship-gate/ship, and retro.
-Ordinary product intent must first be explored in the host's Plan mode and saved as a file, preferably under .product-loop/intake/. Auto-plan runs bounded discovery before inspecting the repository and records the single result as source_plan_path. If no file exists or multiple candidates remain, auto-plan is BLOCKED; it must not guess or create a substitute. An explicit path is only an ambiguity override. Auto-plan and plan-gate write Markdown only: plan.md remains canonical and approval.md records explicit acceptance. Build activation compiles machine artifacts and the lock before the first product-code edit. The source plan remains required and hash-current through build. Test, review, and ship gates operate from the approved lock, diff, and evidence after build.
+Ordinary product intent must first be explored in the host's Plan mode and saved as a file, preferably under .product-loop/intake/. Auto-plan runs bounded discovery before inspecting the repository and records the single result as source_plan_path. If no file exists or multiple candidates remain, auto-plan is BLOCKED; it must not guess or create a substitute. An explicit path is only an ambiguity override. Auto-plan and plan-gate write Markdown only: plan.md remains canonical and approval.md records explicit acceptance. If the host blocks its normal Markdown writer, use the bounded planning-write helper and never arbitrary shell redirection. Repository facts are DISCOVERED, agent suggestions are PROPOSED, and only human responses are ANSWERED; every material proposal remains blocking. At build, confirm the host can edit product code before activating the plan. A rejected mode transition returns READY_FOR_BUILD and creates no machine artifacts or lock. Once execution is available, activation compiles machine artifacts and the lock before the first product-code edit. The source plan remains required and hash-current through build. Test, review, and ship gates operate from the approved lock, diff, and evidence after build.
Use .product-loop/artifacts.md for document boundaries and .product-loop/failure-moves.md for improvement experiments. If a structured question tool is unavailable, ask 1-3 plain-text questions and return WAITING_FOR_INPUT; never select defaults on the user's behalf. Do not implement from an unapproved or stale plan. Implementation tactics are open; completion, approval, and shipping claims require current evidence. Do not branch on model identity; use observable state and gate evidence.
+At ship, prove whether a failing check is pre-existing by checking the base branch. Keep unrelated repairs in a separate PR; do not modify unrelated code under the approved feature lock. A repository-policy bypass requires explicit human authorization and recorded evidence.
+
If gstack is enabled, use only its namespaced /gstack-* specialist lenses inside Boatstack operations. If Spec Kit is enabled, use it to generate or cross-check artifacts; never invoke speckit.implement to bypass Boatstack's plan approval and build gate.
`, adapterName)
if contains(adapters, "claude") {
diff --git a/boatstack/export_test.go b/boatstack/export_test.go
index 9d0abcb..7afc199 100644
--- a/boatstack/export_test.go
+++ b/boatstack/export_test.go
@@ -61,15 +61,19 @@ func TestExportAndDriftCheck(t *testing.T) {
autoPlan := string(bundle.Files[".cursor/commands/auto-plan.md"])
planGate := string(bundle.Files[".cursor/commands/plan-gate.md"])
build := string(bundle.Files[".cursor/commands/build.md"])
- if !strings.Contains(autoPlan, "Markdown-only") || !strings.Contains(autoPlan, "never silently choose a default") {
+ if !strings.Contains(autoPlan, "Markdown-only") || !strings.Contains(autoPlan, "never silently choose a default") || !strings.Contains(autoPlan, "planning-write") || !strings.Contains(autoPlan, "PROPOSED") {
t.Fatal("auto-plan adapter does not enforce the Markdown and question boundaries")
}
- if !strings.Contains(planGate, "approval.md") || !strings.Contains(planGate, "Remain in Plan mode") {
+ if !strings.Contains(planGate, "approval.md") || !strings.Contains(planGate, "Remain in Plan mode") || !strings.Contains(planGate, "record-approval") {
t.Fatal("plan-gate adapter does not keep approval in Plan mode")
}
- if !strings.Contains(build, "activate-plan") || strings.Contains(build, "compile-plan") {
+ if !strings.Contains(build, "activate-plan") || !strings.Contains(build, "READY_FOR_BUILD") || !strings.Contains(build, "without activating") || strings.Contains(build, "compile-plan") {
t.Fatal("build adapter must activate the Markdown plan exactly once")
}
+ ship := string(bundle.Files[".cursor/commands/ship-gate.md"])
+ if !strings.Contains(ship, "separate repair PR") || !strings.Contains(ship, "Never edit unrelated code") {
+ t.Fatal("ship adapter permits unrelated scope expansion")
+ }
lock := string(bundle.Files[".product-loop/generated.lock.json"])
if !strings.Contains(lock, `"source_commit"`) || !strings.Contains(lock, `"integrations"`) {
t.Fatal("generated lock must record runtime provenance and integrations")
diff --git a/boatstack/init.go b/boatstack/init.go
index ef28ecc..9c913fb 100644
--- a/boatstack/init.go
+++ b/boatstack/init.go
@@ -46,6 +46,9 @@ func ResolveRepository(path string) (string, error) {
}
func detectTestCommand(repo string) string {
+ if fileExists(filepath.Join(repo, "scripts", "check.sh")) {
+ return "bash scripts/check.sh"
+ }
packagePath := filepath.Join(repo, "package.json")
if value, err := os.ReadFile(packagePath); err == nil {
var packageJSON struct {
@@ -71,6 +74,22 @@ func detectTestCommand(repo string) string {
return candidate.command
}
}
+ pyprojectUsesPytest := false
+ if value, err := os.ReadFile(filepath.Join(repo, "pyproject.toml")); err == nil {
+ pyprojectUsesPytest = strings.Contains(strings.ToLower(string(value)), "pytest")
+ }
+ pythonProject := fileExists(filepath.Join(repo, "pytest.ini")) ||
+ fileExists(filepath.Join(repo, "conftest.py")) || pyprojectUsesPytest
+ if pythonProject {
+ switch {
+ case fileExists(filepath.Join(repo, "uv.lock")):
+ return "uv run pytest"
+ case fileExists(filepath.Join(repo, "poetry.lock")):
+ return "poetry run pytest"
+ default:
+ return "python -m pytest"
+ }
+ }
return ""
}
@@ -311,13 +330,23 @@ func RunInit(options InitOptions) error {
if err := CheckExport(repo, bundle.Files); err != nil {
return err
}
+ if err := Doctor(repo); err != nil {
+ return fmt.Errorf("post-install smoke check failed: %w", err)
+ }
fmt.Fprintln(options.Output, "\nPASS: Boatstack core installed without a language runtime.")
keys := sortedKeys(states)
for _, name := range keys {
state := states[name]
fmt.Fprintf(options.Output, " %s: %s — %s\n", name, state.Status, state.Detail)
}
- fmt.Fprintln(options.Output, "\nStart in Cursor, Codex, or Claude Plan mode:")
+ fmt.Fprintln(options.Output, "\nBefore product work, commit Boatstack infrastructure in its own PR:")
+ stagePaths := append([]string{".boatstack-project.json"}, paths...)
+ fmt.Fprintln(options.Output, " git status --short")
+ fmt.Fprintln(options.Output, " git add -- "+strings.Join(stagePaths, " "))
+ fmt.Fprintln(options.Output, " git commit -m \"chore: install Boatstack\"")
+ fmt.Fprintln(options.Output, " git push -u origin chore/install-boatstack")
+ fmt.Fprintln(options.Output, "The platform helper and local install lock under .product-loop/bin/ are ignored; rerun the installer on a fresh clone.")
+ fmt.Fprintln(options.Output, "\nAfter that PR is merged, reload Cursor, Codex, or Claude and start in Plan mode:")
fmt.Fprintln(options.Output, " 1. Describe the product change and save the host plan (use .product-loop/intake/ if the host exposes no path).")
fmt.Fprintln(options.Output, " 2. Run /auto-plan")
return nil
diff --git a/boatstack/init_test.go b/boatstack/init_test.go
index fcbf680..33205f4 100644
--- a/boatstack/init_test.go
+++ b/boatstack/init_test.go
@@ -42,6 +42,11 @@ func TestRuntimeFreeInit(t *testing.T) {
if !strings.Contains(output.String(), "PASS: Boatstack core installed without a language runtime") {
t.Fatalf("unexpected init output: %s", output.String())
}
+ for _, expected := range []string{"commit Boatstack infrastructure in its own PR", "git add -- .boatstack-project.json", "git push -u origin chore/install-boatstack", "reload Cursor, Codex, or Claude"} {
+ if !strings.Contains(output.String(), expected) {
+ t.Fatalf("init output is missing %q: %s", expected, output.String())
+ }
+ }
configValue, _ := os.ReadFile(filepath.Join(repo, ".boatstack-project.json"))
if strings.Contains(string(configValue), `"status"`) {
t.Fatal("machine-local integration status leaked into repository configuration")
@@ -52,6 +57,41 @@ func TestRuntimeFreeInit(t *testing.T) {
}
}
+func TestDetectTestCommandCoversCheckScriptAndPythonProjects(t *testing.T) {
+ for name, setup := range map[string]struct {
+ files map[string]string
+ want string
+ }{
+ "check script": {files: map[string]string{"scripts/check.sh": "#!/bin/sh\n"}, want: "bash scripts/check.sh"},
+ "uv pytest": {files: map[string]string{"pyproject.toml": "[tool.pytest.ini_options]\n", "uv.lock": ""}, want: "uv run pytest"},
+ "poetry pytest": {files: map[string]string{"pyproject.toml": "[tool.pytest.ini_options]\n", "poetry.lock": ""}, want: "poetry run pytest"},
+ "plain pytest": {files: map[string]string{"pytest.ini": "[pytest]\n"}, want: "python -m pytest"},
+ } {
+ t.Run(name, func(t *testing.T) {
+ repo := t.TempDir()
+ for relative, value := range setup.files {
+ path := filepath.Join(repo, filepath.FromSlash(relative))
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte(value), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if got := detectTestCommand(repo); got != setup.want {
+ t.Fatalf("detectTestCommand() = %q, want %q", got, setup.want)
+ }
+ })
+ }
+ repo := t.TempDir()
+ if err := os.WriteFile(filepath.Join(repo, "pyproject.toml"), []byte("[project]\nname = \"fixture\"\n"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if got := detectTestCommand(repo); got != "" {
+ t.Fatalf("plain pyproject.toml invented a test command: %q", got)
+ }
+}
+
func TestGStackMissingPrerequisiteIsPartialNotCoreFailure(t *testing.T) {
oldLookPath := lookPath
defer func() { lookPath = oldLookPath }()
diff --git a/boatstack/planning.go b/boatstack/planning.go
new file mode 100644
index 0000000..ff4082e
--- /dev/null
+++ b/boatstack/planning.go
@@ -0,0 +1,229 @@
+package boatstack
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "time"
+ "unicode/utf8"
+)
+
+var featureSlugPattern = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
+
+var planningArtifacts = map[string]bool{
+ "source-plan.md": true,
+ "feature-spec.md": true,
+ "questions.md": true,
+ "gaps.md": true,
+ "test-plan.md": true,
+ "plan.md": true,
+}
+
+type PlanningWriteOptions struct {
+ Repo string
+ Feature string
+ Artifact string
+ Content []byte
+}
+
+type ApprovalRecordOptions struct {
+ PlanPath string
+ OutputPath string
+ ApprovedBy string
+ ApprovedAt string
+ Fingerprint string
+}
+
+func rejectSymlinkComponents(root, target string) error {
+ relative, err := filepath.Rel(root, target)
+ if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
+ return fmt.Errorf("path escapes repository boundary: %s", target)
+ }
+ current := root
+ for _, component := range strings.Split(relative, string(filepath.Separator)) {
+ current = filepath.Join(current, component)
+ info, statErr := os.Lstat(current)
+ if os.IsNotExist(statErr) {
+ continue
+ }
+ if statErr != nil {
+ return statErr
+ }
+ if info.Mode()&os.ModeSymlink != 0 {
+ return fmt.Errorf("refusing symlinked planning path: %s", current)
+ }
+ }
+ return nil
+}
+
+func atomicWrite(path string, content []byte) error {
+ directory := filepath.Dir(path)
+ if err := os.MkdirAll(directory, 0o755); err != nil {
+ return err
+ }
+ temporary, err := os.CreateTemp(directory, ".boatstack-planning-*")
+ if err != nil {
+ return err
+ }
+ temporaryPath := temporary.Name()
+ defer os.Remove(temporaryPath)
+ if err := temporary.Chmod(0o644); err != nil {
+ temporary.Close()
+ return err
+ }
+ if _, err := temporary.Write(content); err != nil {
+ temporary.Close()
+ return err
+ }
+ if err := temporary.Sync(); err != nil {
+ temporary.Close()
+ return err
+ }
+ if err := temporary.Close(); err != nil {
+ return err
+ }
+ return replaceFile(temporaryPath, path)
+}
+
+func WritePlanningArtifact(options PlanningWriteOptions) (string, error) {
+ if !featureSlugPattern.MatchString(options.Feature) {
+ return "", fmt.Errorf("feature must be a lowercase kebab-case slug")
+ }
+ if !planningArtifacts[options.Artifact] {
+ return "", fmt.Errorf("unsupported planning artifact: %s", options.Artifact)
+ }
+ if !utf8.Valid(options.Content) {
+ return "", fmt.Errorf("planning artifact must be valid UTF-8 Markdown")
+ }
+ if strings.TrimSpace(string(options.Content)) == "" {
+ return "", fmt.Errorf("planning artifact must not be empty")
+ }
+ repo, err := ResolveRepository(options.Repo)
+ if err != nil {
+ return "", err
+ }
+ destination := filepath.Join(repo, ".product-loop", "features", options.Feature, options.Artifact)
+ if err := rejectSymlinkComponents(repo, destination); err != nil {
+ return "", err
+ }
+ if err := atomicWrite(destination, options.Content); err != nil {
+ return "", err
+ }
+ relative, err := filepath.Rel(repo, destination)
+ if err != nil {
+ return "", err
+ }
+ return filepath.ToSlash(relative), nil
+}
+
+func RecordApproval(options ApprovalRecordOptions) error {
+ if strings.TrimSpace(options.ApprovedBy) == "" {
+ return fmt.Errorf("approval requires a named human")
+ }
+ approvedAt, err := time.Parse(time.RFC3339, options.ApprovedAt)
+ if err != nil {
+ return fmt.Errorf("approval timestamp must be RFC3339")
+ }
+ check, err := CheckPlan(options.PlanPath)
+ if err != nil {
+ return err
+ }
+ if options.Fingerprint != check.Fingerprint {
+ return fmt.Errorf("approval fingerprint does not match the current plan")
+ }
+ expectedOutput := filepath.Join(filepath.Dir(options.PlanPath), "approval.md")
+ output := options.OutputPath
+ if output == "" {
+ output = expectedOutput
+ }
+ expectedAbsolute, err := filepath.Abs(expectedOutput)
+ if err != nil {
+ return err
+ }
+ outputAbsolute, err := filepath.Abs(output)
+ if err != nil {
+ return err
+ }
+ if filepath.Clean(outputAbsolute) != filepath.Clean(expectedAbsolute) {
+ return fmt.Errorf("approval receipt must be written beside plan.md as approval.md")
+ }
+ planDirectory, err := filepath.Abs(filepath.Dir(options.PlanPath))
+ if err != nil {
+ return err
+ }
+ if err := rejectSymlinkComponents(planDirectory, outputAbsolute); err != nil {
+ return err
+ }
+ payload, err := MarshalJSON(map[string]any{
+ "schema_version": 1,
+ "status": "APPROVED",
+ "approved_by": strings.TrimSpace(options.ApprovedBy),
+ "approved_at": approvedAt.Format(time.RFC3339),
+ "approval_fingerprint": check.Fingerprint,
+ })
+ if err != nil {
+ return err
+ }
+ body := "# Plan approval\n\n" + approvalMarkerStart + "\n```json\n" + strings.TrimSpace(string(payload)) + "\n```\n" + approvalMarkerEnd + "\n"
+ return atomicWrite(outputAbsolute, []byte(body))
+}
+
+type installLock struct {
+ BoatstackVersion string `json:"boatstack_version"`
+ SourceCommit string `json:"source_commit"`
+ BinaryPath string `json:"binary_path"`
+ BinarySHA256 string `json:"binary_sha256"`
+}
+
+func Doctor(repoPath string) error {
+ repo, err := ResolveRepository(repoPath)
+ if err != nil {
+ return err
+ }
+ configPath := filepath.Join(repo, ".boatstack-project.json")
+ config, raw, err := LoadConfig(configPath)
+ if err != nil {
+ return fmt.Errorf("invalid or missing .boatstack-project.json: %w", err)
+ }
+ bundle, err := BuildExportBundle(configPath, config, raw, "boatstack")
+ if err != nil {
+ return err
+ }
+ if err := CheckExport(repo, bundle.Files); err != nil {
+ return err
+ }
+ lockPath := filepath.Join(repo, ".product-loop", "bin", "install.lock.json")
+ value, err := os.ReadFile(lockPath)
+ if err != nil {
+ return fmt.Errorf("missing local install lock: %w", err)
+ }
+ var lock installLock
+ if err := json.Unmarshal(value, &lock); err != nil {
+ return fmt.Errorf("invalid local install lock: %w", err)
+ }
+ if lock.BoatstackVersion != Version || lock.SourceCommit != SourceCommit {
+ return fmt.Errorf("helper version drift: installed %s (%s), expected %s (%s)", lock.BoatstackVersion, lock.SourceCommit, Version, SourceCommit)
+ }
+ binaryPath := filepath.Join(repo, filepath.FromSlash(lock.BinaryPath))
+ if err := checkNonEmptyFile(binaryPath, "Boatstack helper"); err != nil {
+ return err
+ }
+ hash, err := SHA256File(binaryPath)
+ if err != nil {
+ return err
+ }
+ if hash != lock.BinarySHA256 {
+ return fmt.Errorf("Boatstack helper checksum does not match the install lock")
+ }
+ return nil
+}
+
+func DoctorRepairHint(err error) error {
+ if err == nil {
+ return nil
+ }
+ return fmt.Errorf("%w; repair: rerun the Boatstack installer from the repository root, then reload the coding host", err)
+}
diff --git a/boatstack/planning_test.go b/boatstack/planning_test.go
new file mode 100644
index 0000000..af4b137
--- /dev/null
+++ b/boatstack/planning_test.go
@@ -0,0 +1,169 @@
+package boatstack
+
+import (
+ "encoding/json"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+)
+
+func planningRepo(t *testing.T) string {
+ t.Helper()
+ repo := t.TempDir()
+ if output, err := exec.Command("git", "-C", repo, "init").CombinedOutput(); err != nil {
+ t.Fatalf("git init: %v: %s", err, output)
+ }
+ return repo
+}
+
+func TestPlanningWriteIsBoundedMarkdownOnly(t *testing.T) {
+ repo := planningRepo(t)
+ path, err := WritePlanningArtifact(PlanningWriteOptions{
+ Repo: repo, Feature: "account-recovery", Artifact: "questions.md",
+ Content: []byte("# Questions\n\nQ-1 remains open.\n"),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if path != ".product-loop/features/account-recovery/questions.md" {
+ t.Fatalf("unexpected planning path: %s", path)
+ }
+ value, _ := os.ReadFile(filepath.Join(repo, filepath.FromSlash(path)))
+ if !strings.Contains(string(value), "Q-1") {
+ t.Fatal("planning write lost content")
+ }
+
+ cases := []PlanningWriteOptions{
+ {Repo: repo, Feature: "../escape", Artifact: "plan.md", Content: []byte("# bad\n")},
+ {Repo: repo, Feature: "account-recovery", Artifact: "plan.json", Content: []byte("{}")},
+ {Repo: repo, Feature: "account-recovery", Artifact: "../README.md", Content: []byte("# bad\n")},
+ {Repo: repo, Feature: "account-recovery", Artifact: "plan.md", Content: []byte(" \n")},
+ {Repo: repo, Feature: "account-recovery", Artifact: "plan.md", Content: []byte{0xff, 0xfe}},
+ }
+ for _, options := range cases {
+ if _, err := WritePlanningArtifact(options); err == nil {
+ t.Fatalf("expected bounded writer to reject %#v", options)
+ }
+ }
+}
+
+func TestPlanningWriteRejectsSymlinksAndPreservesExistingContentOnFailure(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("symlink creation needs elevated Windows permissions")
+ }
+ repo := planningRepo(t)
+ outside := t.TempDir()
+ productLoop := filepath.Join(repo, ".product-loop")
+ if err := os.Symlink(outside, productLoop); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := WritePlanningArtifact(PlanningWriteOptions{
+ Repo: repo, Feature: "feature", Artifact: "plan.md", Content: []byte("# plan\n"),
+ }); err == nil || !strings.Contains(err.Error(), "symlink") {
+ t.Fatalf("expected symlink rejection, got %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(outside, "features", "feature", "plan.md")); !os.IsNotExist(err) {
+ t.Fatal("bounded writer followed a symlink")
+ }
+
+ if err := os.Remove(productLoop); err != nil {
+ t.Fatal(err)
+ }
+ destination, err := WritePlanningArtifact(PlanningWriteOptions{
+ Repo: repo, Feature: "feature", Artifact: "plan.md", Content: []byte("# known good\n"),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := WritePlanningArtifact(PlanningWriteOptions{
+ Repo: repo, Feature: "feature", Artifact: "plan.md", Content: []byte("\n"),
+ }); err == nil {
+ t.Fatal("expected invalid replacement to fail")
+ }
+ value, _ := os.ReadFile(filepath.Join(repo, filepath.FromSlash(destination)))
+ if string(value) != "# known good\n" {
+ t.Fatal("failed planning write damaged the previous artifact")
+ }
+}
+
+func TestRecordApprovalChecksFingerprintAndWritesOnlyReceipt(t *testing.T) {
+ root := t.TempDir()
+ _, _, planPath := writePlanInputs(t, root, true)
+ check, err := CheckPlan(planPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ approval := filepath.Join(root, "approval.md")
+ if err := RecordApproval(ApprovalRecordOptions{
+ PlanPath: planPath, OutputPath: approval, ApprovedBy: "Test Human",
+ ApprovedAt: "2026-07-16T12:00:00Z", Fingerprint: "wrong",
+ }); err == nil {
+ t.Fatal("expected stale fingerprint to block approval")
+ }
+ if _, err := os.Stat(approval); !os.IsNotExist(err) {
+ t.Fatal("failed approval created a receipt")
+ }
+ if err := RecordApproval(ApprovalRecordOptions{
+ PlanPath: planPath, ApprovedBy: "Test Human",
+ ApprovedAt: "2026-07-16T12:00:00Z", Fingerprint: check.Fingerprint,
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := CheckApprovalReceipt(approval, check); err != nil {
+ t.Fatal(err)
+ }
+ entries, _ := os.ReadDir(root)
+ for _, entry := range entries {
+ if strings.HasSuffix(entry.Name(), ".json") {
+ t.Fatalf("approval wrote machine state before build: %s", entry.Name())
+ }
+ }
+}
+
+func TestDoctorDetectsMissingConfigAdapterAndVersionDrift(t *testing.T) {
+ repo := planningRepo(t)
+ if err := os.WriteFile(filepath.Join(repo, "go.mod"), []byte("module fixture\n"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := RunInit(InitOptions{Repo: repo, IntegrationChoice: "core", Yes: true}); err != nil {
+ t.Fatal(err)
+ }
+ if err := Doctor(repo); err != nil {
+ t.Fatal(err)
+ }
+ command := filepath.Join(repo, ".cursor", "commands", "ship-gate.md")
+ if err := os.Remove(command); err != nil {
+ t.Fatal(err)
+ }
+ if err := Doctor(repo); err == nil || !strings.Contains(err.Error(), "missing .cursor/commands/ship-gate.md") {
+ t.Fatalf("expected missing adapter diagnosis, got %v", err)
+ }
+ config, raw, _ := LoadConfig(filepath.Join(repo, ".boatstack-project.json"))
+ bundle, _ := BuildExportBundle(filepath.Join(repo, ".boatstack-project.json"), config, raw, "boatstack")
+ if err := WriteExport(repo, bundle.Files); err != nil {
+ t.Fatal(err)
+ }
+ lockPath := filepath.Join(repo, ".product-loop", "bin", "install.lock.json")
+ lockValue, _ := os.ReadFile(lockPath)
+ var lock map[string]any
+ if err := json.Unmarshal(lockValue, &lock); err != nil {
+ t.Fatal(err)
+ }
+ lock["boatstack_version"] = "v0.0.0"
+ updated, _ := MarshalJSON(lock)
+ if err := os.WriteFile(lockPath, updated, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := Doctor(repo); err == nil || !strings.Contains(err.Error(), "version drift") {
+ t.Fatalf("expected helper version diagnosis, got %v", err)
+ }
+ if err := os.Remove(filepath.Join(repo, ".boatstack-project.json")); err != nil {
+ t.Fatal(err)
+ }
+ if err := Doctor(repo); err == nil || !strings.Contains(err.Error(), ".boatstack-project.json") {
+ t.Fatalf("expected missing config diagnosis, got %v", err)
+ }
+}
diff --git a/boatstack/references/workflow.md b/boatstack/references/workflow.md
index c4c311e..ec2cad6 100644
--- a/boatstack/references/workflow.md
+++ b/boatstack/references/workflow.md
@@ -121,13 +121,13 @@ Run `boatstack-helper check-plan --plan /plan.md`, present the full dra
### `PLAN_GATE -> PLAN_APPROVED`
-After explicit approval, create `approval.md` containing the named human, RFC3339 timestamp, and exact approval fingerprint. This receipt is the only new gate artifact. Remain in the host's Plan mode; do not compile machine artifacts or edit product code.
+After explicit approval, invoke the deterministic `record-approval` operation with the named human, RFC3339 timestamp, and exact approval fingerprint. It rechecks the plan and creates only `approval.md`. This receipt is the only new gate artifact. Remain in the host's Plan mode; do not compile machine artifacts or edit product code.
-If the host lacks a structured question tool, ask 1-3 plain-text questions and return `WAITING_FOR_INPUT`. Never convert an unavailable question UI into permission to choose a default. A repository-derived product choice remains `PROPOSED` until accepted, and material open IDs remain in `blocking_questions`.
+If the host lacks a structured question tool, ask 1-3 plain-text questions and return `WAITING_FOR_INPUT`. Never convert an unavailable question UI into permission to choose a default. Authoritative repository facts are `DISCOVERED`; agent suggestions and repository-derived product choices are `PROPOSED`; only explicit human responses are `ANSWERED`. Every material proposal remains in `blocking_questions` until answered.
### `PLAN_APPROVED -> BUILD_ACTIVATION -> PLAN_LOCKED`
-At the host's normal Build transition, before the first product-code edit, `activate-plan` deterministically:
+At the host's normal Build transition, first confirm the host is in an execution-capable mode. If the transition is rejected or product-code writes remain unavailable, return `READY_FOR_BUILD` without compiling or writing a lock. Once execution is available and before the first product-code edit, `activate-plan` deterministically:
1. parse and validate the marked structured block in `plan.md`;
2. hash the complete source plan, spec, and `plan.md` and match them to `approval.md`;
diff --git a/docs/account-recovery-walkthrough.md b/docs/account-recovery-walkthrough.md
new file mode 100644
index 0000000..9e5aefb
--- /dev/null
+++ b/docs/account-recovery-walkthrough.md
@@ -0,0 +1,59 @@
+
+
+# Walkthrough: account recovery in a passwordless product
+
+This sanitized real-world sequence shows why Boatstack asks questions before it turns a request into code.
+
+## Intent collides with repository reality
+
+The product request was:
+
+```text
+Add a password reset button on the homepage.
+```
+
+Minimal repository inspection found passwordless email-code authentication, no password reset route, and copy promising that users needed no password. A literal implementation would have produced a button for a capability that did not exist.
+
+`/auto-plan` therefore stopped and asked two product questions in plain text:
+
+```text
+Q-1 Clarify email-code recovery, introduce passwords, or choose another behavior?
+Q-2 If passwords are introduced, do they replace email codes or sit alongside them?
+```
+
+The human chose password authentication alongside the existing passwordless flow. Those responses became `ANSWERED`; the repository facts were `DISCOVERED`. Boatstack did not treat its own recommendation as an answer.
+
+## Approval turns the choice into a bounded change
+
+The refined plan kept passwordless login, added password login and recovery routes, preserved passwordless signup, updated misleading copy, and required route and authentication tests. `/plan-gate` displayed the exact scope, non-goals, operational redirect gap, and fingerprint. An explicit `approve` created only `approval.md`.
+
+After the host entered its execution-capable mode, `/build` activated that exact plan and implemented the feature. The targeted suite initially passed.
+
+## Review falsifies a completion claim
+
+`/review-gate` inspected the actual diff and found that the reset screen accepted any authenticated session as proof of password recovery. A normally signed-in user could reach a form intended only for a recovery event.
+
+The gate returned `BLOCKED`. The implementation was repaired to unlock the form only for the recovery event, a regression test was added, and `/review-gate` then passed with the separate operator redirect gap still explicit.
+
+## Shipping respects repository boundaries
+
+At `/ship-gate`, a pre-push type check failed in code unrelated to the approved feature. The correct response is to prove whether the failure exists on the base branch and then either:
+
+1. repair it in a separate PR; or
+2. use a repository-policy bypass only with explicit human authorization and recorded evidence.
+
+Changing unrelated code in the feature branch would silently widen the approved scope.
+
+## What this demonstrates
+
+```text
+vague intent
+ -> discover conflicting repository fact
+ -> ask the product owner
+ -> approve one observable slice
+ -> build freely inside that boundary
+ -> let evidence force a local repair
+ -> keep unrelated repository failures outside the feature
+```
+
+The value did not come from a larger prompt. It came from preserving the original intent, separating discovered facts from human decisions, and requiring the implementation to survive an evidence boundary before shipping.
diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md
index 2e940c0..212bac9 100644
--- a/docs/evidence-engineered-coding.md
+++ b/docs/evidence-engineered-coding.md
@@ -90,7 +90,7 @@ subject to acceptance criteria pass
approval is current
```
-That is why context trimming is not automatically an optimization. If removing state increases rework or false acceptance, total cost rises. The canonical runtime references are approximately **4521 estimated tokens**, while host adapters point to one operation at a time.
+That is why context trimming is not automatically an optimization. If removing state increases rework or false acceptance, total cost rises. The canonical runtime references are approximately **4625 estimated tokens**, while host adapters point to one operation at a time.
## Control appears at transitions
@@ -140,6 +140,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 [`3cd11c293b3903aeac9ff8d8eacd47840d63a02d`](https://github.com/operatorstack/intelligence-flow/tree/3cd11c293b3903aeac9ff8d8eacd47840d63a02d/examples/12-product-engineering-loop).
+The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`29f36332e8e249528c6b088473d65e1190ff00b8`](https://github.com/operatorstack/intelligence-flow/tree/29f36332e8e249528c6b088473d65e1190ff00b8/examples/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/generated-files.md b/docs/generated-files.md
new file mode 100644
index 0000000..c95bb2d
--- /dev/null
+++ b/docs/generated-files.md
@@ -0,0 +1,46 @@
+
+
+# What Boatstack generates
+
+Boatstack creates two different kinds of repository state. Keeping them separate is what makes feature diffs reviewable.
+
+## Installation state: commit in its own PR
+
+| Path | Ownership | What to do |
+|---|---|---|
+| `.boatstack-project.json` | Project-owned input | Review and edit repository facts, real commands, context starting points, and policy. |
+| `.product-loop/project.json`, references, templates, and `generated.lock.json` | Boatstack-generated | Commit; regenerate through the installer instead of editing directly. |
+| `.cursor/commands/` and `.cursor/rules/` | Boatstack-generated Cursor adapter | Commit so slash commands survive clones, branch changes, and cleanup. |
+| `.agents/skills/boatstack/` | Boatstack-generated Codex/open-agent adapter | Commit. |
+| `.claude/skills/boatstack/` | Boatstack-generated Claude adapter | Commit. |
+| `.github/PULL_REQUEST_TEMPLATE/boatstack.md` | Boatstack-generated PR adapter | Commit. |
+| `.product-loop/bin/` | Machine-local | Do not commit. It contains the verified platform helper and local install lock and is ignored. |
+
+The installation manifest `.product-loop/generated.lock.json` describes generated infrastructure. It is different from a feature's `plan.lock.json`, which proves that a specific approved plan activated without drift.
+
+## Feature state: commit with the feature PR
+
+| Artifact | Meaning |
+|---|---|
+| `source-plan.md` | Preserved host Plan-mode interpretation of the original request. |
+| `feature-spec.md` | Accepted outcome, boundaries, behavior, and criteria. |
+| `questions.md` | Discovered facts, proposed choices, human answers, and open unknowns. |
+| `gaps.md` | Known incomplete or deferred work with impact and revisit trigger. |
+| `test-plan.md` | Criterion-to-oracle and validation design. |
+| `plan.md` | Canonical human-readable and structured approved plan. |
+| `approval.md` | Named human, timestamp, and exact plan fingerprint. |
+| `compiled/` | Build-time task graph, test matrix, and evidence skeleton. |
+| `plan.lock.json` | Content-addressed build activation record. |
+| `evidence.md` | Commands, results, findings, runtime checks, and gate status. |
+
+These files travel with the product diff because they explain what was approved and why completion is defensible. Changes to the source plan, spec, or `plan.md` invalidate approval until the plan gate runs again.
+
+## Fresh clones and updates
+
+Committed adapters remain available after cloning. Restore only the ignored helper by rerunning the installer from the repository root. For an update, create a new `chore/update-boatstack` branch, rerun the installer, inspect the generated diff and version provenance, and merge it as a separate infrastructure PR.
+
+Never delete untracked adapters merely to make a feature diff smaller. If Boatstack was installed without committing its infrastructure, stop and create the installation PR first. Run this read-only check whenever commands disappear or generated state looks suspicious:
+
+```bash
+.product-loop/bin/boatstack-helper doctor --repo .
+```
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 0000000..dfbc9ec
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,104 @@
+
+
+# Install Boatstack and ship a first feature
+
+Boatstack is repository-local. Install it once in an infrastructure PR, then create ordinary feature branches from the merged base.
+
+## 1. Install on a clean branch
+
+macOS or Linux:
+
+```bash
+git switch -c chore/install-boatstack
+/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/operatorstack/boatstack/main/install.sh)"
+```
+
+Windows PowerShell:
+
+```powershell
+git switch -c chore/install-boatstack
+irm https://raw.githubusercontent.com/operatorstack/boatstack/main/install.ps1 | iex
+```
+
+Choose `core` unless you already want gstack review lenses, Spec Kit artifact generation, or both. Confirm the real test command when asked. The installer previews every path before writing it, verifies the helper checksum, and runs `doctor` after installation.
+
+Review the printed paths and repository facts:
+
+```bash
+git status --short
+git diff -- .boatstack-project.json .product-loop .cursor .agents .claude .github/PULL_REQUEST_TEMPLATE
+.product-loop/bin/boatstack-helper doctor --repo .
+```
+
+Use the exact `git add` command printed by the installer, then commit and push:
+
+```bash
+git commit -m "chore: install Boatstack"
+git push -u origin chore/install-boatstack
+```
+
+Open and merge that infrastructure PR before starting a feature. The first PR is intentionally larger because it establishes the shared workflow; later feature PRs contain only the product change and its feature evidence. A fresh clone reruns the installer to restore the ignored platform helper without changing committed adapters.
+
+## 2. Start one feature in Plan mode
+
+Create a feature branch from the base containing the merged Boatstack installation. Open Cursor, Codex, or Claude in its planning surface and describe ordinary product intent:
+
+```text
+Add account recovery without removing the existing passwordless sign-in flow.
+```
+
+Let the host explore the smallest relevant repository slice and save its plan. Boatstack uses a host-exposed plan path when available; otherwise save one plan under `.product-loop/intake/`.
+
+Run:
+
+```text
+/auto-plan
+```
+
+`/auto-plan` may answer discoverable repository facts, but it cannot answer product decisions for you. If it asks questions, answer them in plain text. Material agent suggestions stay `PROPOSED`; only your responses become `ANSWERED`. Re-run `/auto-plan` until `check-plan` passes and the draft is ready.
+
+## 3. Review and approve the exact plan
+
+Run:
+
+```text
+/plan-gate
+```
+
+Boatstack presents the intended outcome, non-goals, decisions, known gaps, validation origins, and an exact fingerprint. Request changes or explicitly reply `approve`. Approval records a named human and the displayed fingerprint in `approval.md`; it does not build code.
+
+## 4. Enter the host's execution surface and build
+
+Use the host's normal transition out of planning, then run:
+
+```text
+/build
+```
+
+Boatstack first activates the approved plan into compiled tasks, a requirement-to-test matrix, evidence skeleton, and content-addressed lock. Only then may the host edit product code. If the host rejects the mode transition, Boatstack returns `READY_FOR_BUILD` and creates no machine state.
+
+Host notes:
+
+| Host | Planning | Build transition | Boatstack adapter |
+|---|---|---|---|
+| Cursor | Plan mode; plain-text questions work when structured questions are unavailable | Accept Cursor's normal switch to Agent/Build mode | `.cursor/commands/*.md` |
+| Codex | Plan mode in the app or supported client | Move to its normal execution-capable mode | `.agents/skills/boatstack/SKILL.md` |
+| Claude Code | Start or switch to plan permission mode | Exit plan mode before `/build` | `.claude/skills/boatstack/SKILL.md` |
+
+## 5. Prove, review, and ship
+
+Run the gates in order:
+
+```text
+/test-gate
+/review-gate
+/ship-gate
+```
+
+- `test-gate` maps every acceptance criterion to current evidence.
+- `review-gate` reviews the actual diff and may send the feature back for a local repair.
+- `ship-gate` prepares and opens the PR; merge and deploy remain separate decisions.
+
+`PASS_WITH_GAPS` is honest success with explicitly owned, non-critical gaps. `BLOCKED` means the claim cannot progress. After fixing a review finding, rerun the affected gates. A check that already fails on the base branch belongs in a separate repair PR or an explicitly authorized repository-policy bypass—not an unrelated edit hidden in the feature branch.
+
+See the [sanitized account-recovery walkthrough](account-recovery-walkthrough.md) for a complete realistic path, or the [diagram JSON example](../examples/diagram-json/README.md) for exact artifacts.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
new file mode 100644
index 0000000..949f862
--- /dev/null
+++ b/docs/troubleshooting.md
@@ -0,0 +1,46 @@
+
+
+# Troubleshooting Boatstack
+
+## Cursor does not recognize a slash command
+
+Cursor discovers project commands from `.cursor/commands/*.md`. Check the installation:
+
+```bash
+.product-loop/bin/boatstack-helper doctor --repo .
+ls .cursor/commands
+```
+
+If commands are missing, rerun the installer and reload the Cursor window. Commit the restored installation state in a dedicated PR; otherwise a cleanup, branch change, or fresh clone can remove untracked commands again.
+
+## `/auto-plan` says no source plan exists
+
+Boatstack will not invent a source plan. Finish the host's Plan-mode exploration and save it. If the host does not expose the active plan path, place exactly one non-empty plan under `.product-loop/intake/`, then rerun `/auto-plan`. Pass an explicit path only when discovery reports multiple candidates.
+
+## Plan mode blocks the normal Write tool
+
+Boatstack planning remains Markdown-only. The adapter may use the bounded `planning-write` helper for known feature documents; it must not use arbitrary redirection to bypass the host or write product code. If the host cannot support even that bounded operation, return `WAITING_FOR_HOST_WRITE_PERMISSION` instead of leaving planning early.
+
+## `/build` says `READY_FOR_BUILD`
+
+The plan is approved, but the host is still read-only. Accept the host's normal transition into its execution-capable surface and rerun `/build`. Boatstack does not compile tasks or create a lock until product-code writes are available.
+
+## Approval is stale
+
+The source plan, feature spec, or complete `plan.md` changed after approval. Return to `/auto-plan`, review the new fingerprint at `/plan-gate`, and approve the revised plan. Never edit the fingerprint in `approval.md` manually.
+
+## A gate reports `PASS_WITH_GAPS`
+
+The proven criteria passed, while named non-critical gaps remain. The evidence must identify their impact, owner, reason, affected criteria, and revisit trigger. Any critical safety, correctness, or product-acceptance gap is `BLOCKED`, not `PASS_WITH_GAPS`.
+
+## A pre-push hook fails on unrelated base-branch code
+
+Reproduce the failure against the target branch. If it is pre-existing, keep the repair in a separate PR. A bypass is allowed only when repository policy permits it and the human explicitly authorizes it; record that evidence. Do not quietly add unrelated repairs to the approved feature branch.
+
+## Non-interactive installation cannot detect tests
+
+Boatstack recognizes common package-manager tests, `scripts/check.sh`, Go, Rust, Make, and Python/pytest projects. If the repository uses a custom command, run the installer interactively or create `.boatstack-project.json` with the real test command. Boatstack will not invent a command merely to complete installation.
+
+## A fresh clone has adapters but no helper
+
+This is expected: `.product-loop/bin/` is machine-local and ignored. Rerun the installer from the repository root; the generated diff should remain clean when the committed configuration and installed Boatstack version match.
diff --git a/examples/diagram-json/plan.lock.json b/examples/diagram-json/plan.lock.json
index 7e40732..98087f5 100644
--- a/examples/diagram-json/plan.lock.json
+++ b/examples/diagram-json/plan.lock.json
@@ -6,7 +6,7 @@
"plan_path": "examples/diagram-json/plan.md",
"plan_sha256": "3ad35cc3cbe48306e7ee401bd9e9047d25e46c8a6fe9679aa1b3f5e96ceea292",
"schema_version": 1,
- "source_commit": "3cd11c293b3903aeac9ff8d8eacd47840d63a02d",
+ "source_commit": "29f36332e8e249528c6b088473d65e1190ff00b8",
"source_plan_path": "examples/diagram-json/source-plan.md",
"source_plan_sha256": "e10593ddaa7522ab80cc991d0a09399257139799e37f737794cd49d68a39985b",
"spec_path": "examples/diagram-json/spec.md",