From 4983ee8f44209c479db709a801c82a180d8d9626 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 16 Jul 2026 18:51:43 +0000
Subject: [PATCH] Sync Boatstack from Intelligence Flow 3cd11c293b39
---
CONTRIBUTING.md | 2 +-
README.md | 68 +++--
UPSTREAM.json | 44 +--
boatstack/SKILL.md | 56 ++--
boatstack/assets/templates/approval.md | 15 +
boatstack/assets/templates/plan-lock.json | 5 +
.../assets/templates/{plan.json => plan.md} | 21 +-
boatstack/assets/templates/questions.md | 2 +
boatstack/cmd/boatstack-helper/main.go | 63 ++--
boatstack/export.go | 14 +-
boatstack/export_test.go | 17 ++
boatstack/plan.go | 289 +++++++++++++++++-
boatstack/plan_test.go | 217 +++++++++++--
boatstack/references/artifacts.md | 8 +-
boatstack/references/workflow.md | 26 +-
docs/evidence-engineered-coding.md | 13 +-
docs/research-and-design.md | 8 +-
examples/diagram-json/README.md | 55 ++--
examples/diagram-json/approval.md | 15 +
examples/diagram-json/plan.lock.json | 10 +-
examples/diagram-json/{plan.json => plan.md} | 83 ++---
21 files changed, 770 insertions(+), 261 deletions(-)
create mode 100644 boatstack/assets/templates/approval.md
rename boatstack/assets/templates/{plan.json => plan.md} (60%)
create mode 100644 examples/diagram-json/approval.md
rename examples/diagram-json/{plan.json => plan.md} (77%)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 35bfb61..73a9fb9 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/40ebae5dfb3d090812301a438aaac079426edcfc/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/3cd11c293b3903aeac9ff8d8eacd47840d63a02d/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 4ff5575..46e8ba0 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
**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 `40ebae5dfb3d090812301a438aaac079426edcfc`](https://github.com/operatorstack/intelligence-flow/tree/40ebae5dfb3d090812301a438aaac079426edcfc/examples/12-product-engineering-loop).
+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).
> **You are free in how you build. Only claims of completion require evidence.**
@@ -32,11 +32,13 @@ Q2 Stability? versioned schema | internal object dump
Q3 Run data? compact overlay | entire execution trace
```
-The accepted answers become observable criteria and tasks—not hidden assumptions:
+The accepted answers become observable criteria and tasks—not hidden assumptions. They live in the single marked structured block inside human-readable `plan.md`:
```json
{
"source_plan_path": "source-plan.md",
+ "spec_path": "feature-spec.md",
+ "blocking_questions": [],
"acceptance_criteria": [
{"id": "AC-1", "text": "Return parseable schema-versioned graph JSON."},
{"id": "AC-4", "text": "Keep existing ASCII output byte-compatible."}
@@ -90,34 +92,46 @@ Ambiguous claims cannot pass unchanged:
See [Validation and evidence](docs/validation-and-evidence.md) for validation forms, ambiguity handling, independence levels, gate outcomes, and the benchmark observations behind this contract.
-The compiler refuses a criterion with no task or verification. Then `/plan-gate` requires a named human and binds approval to content hashes:
+The developer-facing transition stays inside the coding host:
+
+```text
+/plan-gate
+Approve the displayed plan.
+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.
+
+
+Internal deterministic boundary
+
+The generated adapter invokes the repository-local helper; users do not need to learn these commands:
```bash
-.product-loop/bin/boatstack-helper compile-plan \
- --plan .product-loop/features/diagram-json/plan.json \
- --out-dir .product-loop/features/diagram-json/compiled
-
-.product-loop/bin/boatstack-helper approve-plan \
- --source-plan .product-loop/features/diagram-json/source-plan.md \
- --spec .product-loop/features/diagram-json/spec.md \
- --plan .product-loop/features/diagram-json/plan.json \
- --tasks .product-loop/features/diagram-json/compiled/tasks.json \
- --approved-by "Boateng Opoku-Yeboah" \
+.product-loop/bin/boatstack-helper check-plan \
+ --plan .product-loop/features/diagram-json/plan.md
+
+.product-loop/bin/boatstack-helper activate-plan \
+ --plan .product-loop/features/diagram-json/plan.md \
+ --approval .product-loop/features/diagram-json/approval.md \
+ --out-dir .product-loop/features/diagram-json/compiled \
--output .product-loop/features/diagram-json/plan.lock.json
```
-Build work checks that lock first:
+Activation compiles the machine task graph and writes the lock last. A changed planning input cannot reuse the receipt:
```console
-$ .product-loop/bin/boatstack-helper approve-plan ... --check
-PASS: approved plan lock matches the current artifacts
+$ .product-loop/bin/boatstack-helper activate-plan ...
+PASS: approved Markdown plan activated and locked
-# after plan.json changes
-$ .product-loop/bin/boatstack-helper approve-plan ... --check
-BLOCKED: stale or invalid plan lock: plan
+# after plan.md prose, its structured block, the spec, or source plan changes
+$ .product-loop/bin/boatstack-helper activate-plan ...
+BLOCKED: stale approval receipt
```
-That is the approval boundary in code: conversation cannot silently turn a draft into permission to build.
+
+
+That is the approval boundary in code: conversation cannot silently turn a draft into permission to build, and Plan mode never needs to write JSON or executable state.
See the complete, linked [worked example](examples/diagram-json/README.md).
@@ -165,7 +179,7 @@ irm https://raw.githubusercontent.com/operatorstack/boatstack/main/install.ps1 |
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. The source plan remains required and hash-checked through `/build`; after build, test, review, and ship gates use the approved lock, actual diff, and evidence instead.
+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
@@ -180,7 +194,7 @@ product intent + repository context
\ /
normalized spec + plan + decisions
|
- approval lock -> open build -> evidence gates -> PR
+ Markdown approval -> build activation -> evidence gates -> PR
```
| Layer | What it contributes | What remains Boatstack-owned |
@@ -188,7 +202,7 @@ product intent + repository context
| Coding host: Cursor, Codex, or Claude | Plan mode, repository exploration, implementation, tool execution | Cross-host artifact meanings and transition rules |
| [gstack](https://github.com/garrytan/gstack) | Product/CEO, design, engineering, and developer-experience review lenses; adversarial plan critique | Which findings change the approved plan, validation provenance, approval hashing, and gate outcomes |
| [GitHub Spec Kit](https://github.com/github/spec-kit) | Constitution, specify, clarify, plan, tasks, analyze, checklist, and related spec-driven artifacts | Normalization into Boatstack's criterion/validation contract and explicit human plan gate |
-| Boatstack core | Source-plan discovery, provenance, question/gap boundaries, deterministic compilation, approval/drift locks, evidence mapping, review/ship gates | The completion and shipping claim itself |
+| Boatstack core | Source-plan discovery, provenance, question/gap boundaries, Markdown approval, deterministic build activation, drift locks, evidence mapping, review/ship gates | The completion and shipping claim itself |
### With gstack
@@ -207,11 +221,11 @@ speckit.specify / clarify / plan / tasks / analyze / checklist
explicit /plan-gate
```
-`speckit.implement` does not bypass Boatstack's plan lock or `/build` boundary. If Spec Kit changes accepted semantics, Boatstack invalidates the old lock and returns to approval. This preserves Spec Kit's artifact-generation value without allowing a generator to approve or validate its own output.
+`speckit.implement` does not bypass Boatstack's approval receipt, build activation, or `/build` boundary. If Spec Kit changes accepted semantics, Boatstack invalidates the receipt and returns to approval. This preserves Spec Kit's artifact-generation value without allowing a generator to approve or validate its own output.
### Core only
-Both integrations are optional. Boatstack core still performs Plan-mode source discovery, question-led specification, structured planning, deterministic approval locking, validation/evidence mapping, test/review gates, and PR preparation. Integration failure is recorded as partial installation and does not roll back the working core.
+Both integrations are optional. Boatstack core still performs Plan-mode source discovery, question-led specification, structured Markdown planning, explicit approval receipts, deterministic build activation, validation/evidence mapping, test/review gates, and PR preparation. Integration failure is recorded as partial installation and does not roll back the working core.
The installer creates one canonical `.product-loop/` runtime and thin adapters for:
@@ -269,8 +283,8 @@ 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 **4044 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 **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.
## Status
-Boatstack is an alpha research distribution. It can generate host adapters, compile traceable task/test artifacts, hash-lock explicit approval, detect stale plans, and preserve provenance. The next proof boundary is a paired feature-development evaluation against a plain host harness.
+Boatstack is an alpha research distribution. It can generate host adapters, keep planning and approval Markdown-native, compile traceable task/test artifacts at Build, detect stale approvals, and preserve provenance. The next proof boundary is a paired feature-development evaluation against a plain host harness.
diff --git a/UPSTREAM.json b/UPSTREAM.json
index e150ba6..96bdc20 100644
--- a/UPSTREAM.json
+++ b/UPSTREAM.json
@@ -1,7 +1,7 @@
{
"canonical_context": {
- "characters": 16173,
- "estimated_tokens": 4044,
+ "characters": 18081,
+ "estimated_tokens": 4521,
"estimator": "ceil(total characters / 4); compactness signal, not provider billing",
"files": [
"product-engineering-loop/references/workflow.md",
@@ -11,44 +11,46 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
- "CONTRIBUTING.md": "cf876c19c52a1758c7347c0a27f63c12ef3e49d67066988df638cd09889b88ca",
- "README.md": "0aada7f928bd0f1d2dcfd703fe822759a178e0e6a49779370f0c85f3a9d098f7",
- "boatstack/SKILL.md": "1c71535962a56c9f9c7841e55e0e44c2988af427929f95e12cd7fcf08ef26686",
+ "CONTRIBUTING.md": "baaa5828ac493da05a5cf517524175534c318728830e191deb510e492b7d9c0f",
+ "README.md": "de733eb7d8a15a218cd52d37f510acfca1c3bdccc5085dfce8d9e09ea89d3ba2",
+ "boatstack/SKILL.md": "56cc1da6dea4fb57bd16d37ec9635eb06676d5e8580511190cddc78cd9e37346",
"boatstack/agents/openai.yaml": "68a30a60859556c5a26e16d184594ca243a6043d99c8cf7d66b5dd6d50a93cd1",
"boatstack/assets/templates/adr.md": "c577a3c1c1319061f61deb053597e6e853657022185fe28b8f733327e2a78565",
+ "boatstack/assets/templates/approval.md": "74b0b816703a6dce3c96c8f95f981af910b020b6908e7f76cf5630778637e9f5",
"boatstack/assets/templates/evidence.md": "12dac552bc5373ab443367d5797f41988f14284bcf46d16dfd72015cfddf9ad1",
"boatstack/assets/templates/feature-spec.md": "c7e007cc4295ed4c599642c0587021ef978e729cf0946f6bf3a6c4f01d366ad4",
"boatstack/assets/templates/gaps.md": "911cc2f086104d35071b952950c2ec44258641419f10b2355c594f33eb492cbe",
"boatstack/assets/templates/move.md": "91bfd9a9b9426ac023eb88fd19f4f638190481c1855f1239acc73830528e50f0",
- "boatstack/assets/templates/plan-lock.json": "3e44dea05419cf198ee8112e9b9fdff92287edc2480a03fca026560fe929d468",
- "boatstack/assets/templates/plan.json": "ff530e27959495aa800a494ebb745cd3703c8bd1c2b6c800c373f5210e89c9af",
- "boatstack/assets/templates/questions.md": "86c9bcf51172fe222b7b28bffccaf3da3b1ea0633c7a2348272fdbbd8eea6740",
+ "boatstack/assets/templates/plan-lock.json": "a51e17bb74aa7cd95daaa70fab646a20374ff4bc1d63468d61c5119da61e930f",
+ "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": "c9238e078fd85540073a284772f7360597e68aa0af86b7d3fc0dbc2c73a198ac",
- "boatstack/export.go": "e1dc2d79126e98a8202d93a5fc13049b7186e105724daa93814c7133336e810a",
- "boatstack/export_test.go": "f880bc99e78bbb3d8533153abaa63eee39c3d067df0eef2451e622083cce5676",
+ "boatstack/cmd/boatstack-helper/main.go": "853e60ba22766567afc3f7526260ab70b9056645c5c0500d97a9f3fc0f3a0bf0",
+ "boatstack/export.go": "7d70967d28032a76e58e083f07f27ad9609b0ab894dd208b1d2f3b726c8cd751",
+ "boatstack/export_test.go": "8f78c9bb50210161a19878d17775e73ffb591502d6ee11e6428ac436c128fe23",
"boatstack/go.mod": "daf262a00abfe961d8ca266d4b26eea09a6aee73e4c53baaa537a809eaef59f6",
"boatstack/init.go": "6a04d5482748bd3b6a9febdf1723b89f8460144bcd5315d5feda0f82de477f07",
"boatstack/init_test.go": "7ca705f014f6bb22f7ea61a1eb370b98d1bc722f77a73711749375fb7a08ea14",
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
- "boatstack/plan.go": "8787fb1204fc7a81a2c81e39e4469faab637fd180297dce220eab1e48bf7787d",
- "boatstack/plan_test.go": "5afab86a9b7f749652cfd31675f5a9bd1ec3e03086300923dd7d47765cd80963",
- "boatstack/references/artifacts.md": "3aa4b2abc4195656b011cbfdd61b8e05759a0b67b37590be3a028c21b8a453e6",
+ "boatstack/plan.go": "8250ee93f91b4dd60e3788636ab79f32c626ace6daef6677fdc6338858f89a52",
+ "boatstack/plan_test.go": "f95ce7a38276f957064ee83901566c84ebf3eec0683e81794f823dc446b2ac54",
+ "boatstack/references/artifacts.md": "22b6cc596b65c8c2a22b0f3a692ca16a808fa495dad0b4453d23ea734978a2b7",
"boatstack/references/failure-moves.md": "2d7d3988c70718e9cc02104f9899a00208173e2f654d1046edd22079f4d46f41",
"boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae",
- "boatstack/references/workflow.md": "b8262570751ea73cf40cf9de76fc2e6acf1303657d1956b6957ca9af7034fbb0",
+ "boatstack/references/workflow.md": "0b22fbe168aa31a3ac2832f2439367224aedd2b59468941ee45092b2c1ada1a9",
"boatstack/runtime.go": "66c02aa0b6e9c031c26799b86dfbcb26df74487cc26b32f7fb85e38707eeec31",
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
- "docs/evidence-engineered-coding.md": "2c2b9cb428d3e75b8463d17099ec254afd204367cdc6f8a2214c9def97d8c2a9",
- "docs/research-and-design.md": "fc9c517f2783489cbbc4de5ad020a4e4e5bb542c7092b13b317a1893a3415cb4",
+ "docs/evidence-engineered-coding.md": "453536fa767b98e4671ba885375a5f9af19cb95adb88444691e6e8905966fecf",
+ "docs/research-and-design.md": "67dc454f0d13e0e2809f49e910f7847457fa78f03c4cef6fb0138f6039a57c4f",
"docs/validation-and-evidence.md": "3b5ed588bd44c5568f0c313be0dfaa411e959dc184fe886dfd0a81aee9fd25cc",
- "examples/diagram-json/README.md": "fbb4721434e6110bbef84813c244132bbe7ae0e9e0cd535b6871e3359bcd4559",
+ "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.json": "df1b205517cf7dbdf5c5db65a342622922bd959a5ba885326888f6dd2b9c50d3",
- "examples/diagram-json/plan.lock.json": "c904d4873bf678198dc2d7aba4e90e278385d0266e8bf8bc54334d3f764f1a1a",
+ "examples/diagram-json/plan.lock.json": "81617b3d642c50734c4b7188d639a244f344da16b5e35c6d0e0af8bd354efe37",
+ "examples/diagram-json/plan.md": "3ad35cc3cbe48306e7ee401bd9e9047d25e46c8a6fe9679aa1b3f5e96ceea292",
"examples/diagram-json/questions.md": "1a0050041cac0a8d53e6ebfe04cbec4a298cdc8c50efeeb6fa15aeb663c5ec76",
"examples/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
"examples/diagram-json/source-plan.md": "e10593ddaa7522ab80cc991d0a09399257139799e37f737794cd49d68a39985b",
@@ -60,7 +62,7 @@
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
- "commit": "40ebae5dfb3d090812301a438aaac079426edcfc",
+ "commit": "3cd11c293b3903aeac9ff8d8eacd47840d63a02d",
"path": "examples/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
diff --git a/boatstack/SKILL.md b/boatstack/SKILL.md
index 4eb4fc8..c1ec84a 100644
--- a/boatstack/SKILL.md
+++ b/boatstack/SKILL.md
@@ -13,8 +13,8 @@ Map the request to one operation:
- `init`: inspect a repository and create or update `.product-loop/project.json`.
- `auto-plan`: refine a saved host Plan-mode file into a reviewable draft feature package; refuse when that file is absent.
-- `plan-gate`: present the draft for explicit human acceptance, then freeze its approved contents and generate the executable package.
-- `build`: implement approved tasks in bounded, reversible slices.
+- `plan-gate`: validate the Markdown draft, present it for explicit human acceptance, and record that acceptance in Markdown.
+- `build`: activate the approved Markdown plan, then implement its tasks in bounded, reversible slices.
- `test-gate`: test requirements and relevant regressions using independent evidence.
- `review-gate`: review the diff against the spec, project invariants, risks, and known gaps.
- `ship-gate`: prepare a reviewable PR with evidence, rollback notes, and explicit gaps.
@@ -58,13 +58,14 @@ Do not scan the entire repository by default. Record discovered paths and comman
2. Write the bounded outcome definition before proposing architecture.
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.
-6. Record answers and provenance in the question ledger.
+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`.
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. End with a **draft**, never an implied approval. Do not generate executable task state or start implementation from `auto-plan` alone.
+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.
+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.
@@ -72,36 +73,35 @@ Treat repository-owned product context as canonical. Do not require it to be mig
## Run `plan-gate`
-1. Present the draft spec, plan, open decisions, accepted assumptions, gaps, risks, and proposed verification—including the origin, oracle, and independence of every validation—in a reviewable form.
-2. Ask the developer to approve it or request changes. Silence and continued conversation are not approval.
-3. On changes, return to `auto-plan`, preserve the feedback in the question/decision ledger, and issue a new draft.
-4. On explicit approval, deterministically compile the already-approved structured plan into the task graph, requirement-test traceability rows, evidence skeleton, and expected gate commands. Do not add semantics during compilation.
-5. Calculate content hashes and write `plan.lock.json` with the approver, timestamp, source commit, source-plan hash, spec hash, structured-plan hash, and task-graph hash.
-6. If the source plan, spec, or structured plan changes later, invalidate the lock and return to this gate.
-
-`build` must refuse to run when the source Plan-mode file is absent or when the plan lock is absent, stale, or does not match the source plan and approved artifacts.
-
-The reference implementation performs the post-approval materialization and lock in this order:
+1. Run the read-only Markdown preflight and retain its exact fingerprint:
```bash
-.product-loop/bin/boatstack-helper compile-plan \
- --plan .product-loop/features//plan.json \
- --out-dir .product-loop/features//compiled
-
-.product-loop/bin/boatstack-helper approve-plan \
- --source-plan \
- --spec .product-loop/features//spec.md \
- --plan .product-loop/features//plan.json \
- --tasks .product-loop/features//compiled/tasks.json \
- --approved-by "" \
- --output .product-loop/features//plan.lock.json
+.product-loop/bin/boatstack-helper check-plan \
+ --plan .product-loop/features//plan.md
```
-The first command validates and compiles already-approved semantics; it must not invent new tasks or acceptance criteria.
+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.
+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
-- Before the first edit, pass the source plan along with the approved artifacts to `approve-plan --check`. It remains a required, hash-checked input through completion of `build`.
+- Before the first product-code edit, activate the exact approved Markdown plan:
+
+```bash
+.product-loop/bin/boatstack-helper activate-plan \
+ --plan .product-loop/features//plan.md \
+ --approval .product-loop/features//approval.md \
+ --out-dir .product-loop/features//compiled \
+ --output .product-loop/features//plan.lock.json
+```
+
+- Activation verifies the approval fingerprint, compiles `tasks.json`, `test-matrix.json`, and the evidence skeleton, writes the content-addressed lock last, and rechecks it. It adds no semantics. Missing approval, open blocking questions, or any change to the source plan, spec, or complete `plan.md` returns `BLOCKED`.
+- Keep the source plan present and hash-current through completion of `build`.
- Choose any suitable model, tool, or implementation tactic inside the approved boundary. Boatstack controls transitions and claims, not local creativity.
- Work from approved tasks and acceptance criteria.
- Preserve the last known-good state; repair locally instead of restarting a near-correct implementation.
diff --git a/boatstack/assets/templates/approval.md b/boatstack/assets/templates/approval.md
new file mode 100644
index 0000000..6ee7708
--- /dev/null
+++ b/boatstack/assets/templates/approval.md
@@ -0,0 +1,15 @@
+# Plan approval:
+
+This receipt may be created only after the named human explicitly approves the exact fingerprint shown by `boatstack-helper check-plan`.
+
+
+```json
+{
+ "schema_version": 1,
+ "status": "APPROVED",
+ "approved_by": "",
+ "approved_at": "",
+ "approval_fingerprint": ""
+}
+```
+
diff --git a/boatstack/assets/templates/plan-lock.json b/boatstack/assets/templates/plan-lock.json
index 7b99ae5..8901971 100644
--- a/boatstack/assets/templates/plan-lock.json
+++ b/boatstack/assets/templates/plan-lock.json
@@ -4,8 +4,13 @@
"approved_by": "",
"approved_at": "",
"source_commit": "",
+ "source_plan_path": "source-plan.md",
+ "source_plan_sha256": "",
+ "spec_path": "feature-spec.md",
"spec_sha256": "",
+ "plan_path": "plan.md",
"plan_sha256": "",
+ "task_graph_path": "compiled/tasks.json",
"task_graph_sha256": "",
"invalidated_at": null,
"invalidation_reason": null
diff --git a/boatstack/assets/templates/plan.json b/boatstack/assets/templates/plan.md
similarity index 60%
rename from boatstack/assets/templates/plan.json
rename to boatstack/assets/templates/plan.md
index 3396b3b..2ae2feb 100644
--- a/boatstack/assets/templates/plan.json
+++ b/boatstack/assets/templates/plan.md
@@ -1,8 +1,23 @@
+# Structured plan:
+
+- This Markdown file is the canonical plan.
+- Prose and structured data are both covered by the approval fingerprint.
+- Approval state is recorded separately in `approval.md`; never edit this file merely to mark it approved.
+
+## Human-readable summary
+
+
+
+## Structured plan
+
+
+```json
{
"schema_version": 1,
"feature_id": "",
- "source_plan_path": "",
- "spec_path": "",
+ "source_plan_path": "source-plan.md",
+ "spec_path": "feature-spec.md",
+ "blocking_questions": [],
"acceptance_criteria": [
{
"id": "AC-1",
@@ -28,3 +43,5 @@
}
]
}
+```
+
diff --git a/boatstack/assets/templates/questions.md b/boatstack/assets/templates/questions.md
index 03355d4..2a2402d 100644
--- a/boatstack/assets/templates/questions.md
+++ b/boatstack/assets/templates/questions.md
@@ -2,3 +2,5 @@
| ID | Question | Why it matters | Options | Recommendation | Answer | Source | Status/expiry |
|---|---|---|---|---|---|---|---|
+
+Use `ANSWERED` only for an explicit human answer or an authoritative existing contract. Repository inference is `PROPOSED` until the human accepts it. Material unanswered questions remain `OPEN`, appear in `plan.md` as `blocking_questions`, and block approval.
diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go
index 1c89599..eb9b821 100644
--- a/boatstack/cmd/boatstack-helper/main.go
+++ b/boatstack/cmd/boatstack-helper/main.go
@@ -84,20 +84,20 @@ func exportCommand(arguments []string) int {
return 0
}
-func compileCommand(arguments []string) int {
- flags := flag.NewFlagSet("compile-plan", flag.ContinueOnError)
- plan := flags.String("plan", "", "approved structured plan")
- outDir := flags.String("out-dir", "", "compiled artifact directory")
+func checkPlanCommand(arguments []string) int {
+ flags := flag.NewFlagSet("check-plan", flag.ContinueOnError)
+ plan := flags.String("plan", "", "Markdown structured plan")
if err := flags.Parse(arguments); err != nil {
return 2
}
- if *plan == "" || *outDir == "" {
- return fail(fmt.Errorf("compile-plan requires --plan and --out-dir"))
+ if *plan == "" {
+ return fail(fmt.Errorf("check-plan requires --plan"))
}
- if err := boatstack.CompilePlanFiles(*plan, *outDir); err != nil {
- return fail(fmt.Errorf("invalid approved plan: %w", err))
+ check, err := boatstack.CheckPlan(*plan)
+ if err != nil {
+ return fail(fmt.Errorf("invalid Markdown plan: %w", err))
}
- fmt.Printf("PASS: compiled approved plan into %s\n", *outDir)
+ fmt.Printf("PASS: Markdown plan is structurally valid\nPLAN_FINGERPRINT=%s\nSOURCE_PLAN=%s\nSPEC=%s\n", check.Fingerprint, check.SourcePlanPath, check.SpecPath)
return 0
}
@@ -116,41 +116,30 @@ func checkSourcePlanCommand(arguments []string) int {
return 0
}
-func approveCommand(arguments []string) int {
- flags := flag.NewFlagSet("approve-plan", flag.ContinueOnError)
- options := boatstack.ApprovalOptions{}
- flags.StringVar(&options.SourcePlanPath, "source-plan", "", "plan file created by the host Plan mode")
- flags.StringVar(&options.SpecPath, "spec", "", "approved spec")
- flags.StringVar(&options.PlanPath, "plan", "", "approved structured plan")
- flags.StringVar(&options.TasksPath, "tasks", "", "compiled task graph")
- flags.StringVar(&options.ApprovedBy, "approved-by", "", "human approver")
- flags.StringVar(&options.ApprovedAt, "approved-at", "", "approval timestamp")
- flags.StringVar(&options.SourceCommit, "source-commit", "", "source Git commit")
+func activatePlanCommand(arguments []string) int {
+ flags := flag.NewFlagSet("activate-plan", flag.ContinueOnError)
+ options := boatstack.ActivationOptions{}
+ flags.StringVar(&options.PlanPath, "plan", "", "approved Markdown plan")
+ flags.StringVar(&options.ApprovalPath, "approval", "", "Markdown approval receipt")
+ flags.StringVar(&options.OutDir, "out-dir", "", "compiled artifact directory")
flags.StringVar(&options.OutputPath, "output", "", "plan lock path")
- check := flags.Bool("check", false, "verify an existing plan lock")
+ flags.StringVar(&options.SourceCommit, "source-commit", "", "source Git commit")
if err := flags.Parse(arguments); err != nil {
return 2
}
- if options.SourcePlanPath == "" || options.SpecPath == "" || options.PlanPath == "" || options.TasksPath == "" || options.OutputPath == "" {
- return fail(fmt.Errorf("approve-plan requires --source-plan, --spec, --plan, --tasks, and --output"))
+ if options.PlanPath == "" || options.ApprovalPath == "" || options.OutDir == "" || options.OutputPath == "" {
+ return fail(fmt.Errorf("activate-plan requires --plan, --approval, --out-dir, and --output"))
}
- if *check {
- if err := boatstack.CheckApprovalLock(options); err != nil {
- return fail(err)
- }
- fmt.Println("PASS: approved plan lock matches the current artifacts")
- return 0
- }
- if err := boatstack.CreateApprovalLock(options); err != nil {
- return fail(err)
+ if err := boatstack.ActivatePlan(options); err != nil {
+ return fail(fmt.Errorf("plan activation failed: %w", err))
}
- fmt.Printf("PASS: wrote approved plan lock: %s\n", options.OutputPath)
+ fmt.Printf("PASS: approved Markdown plan activated and locked: %s\n", options.OutputPath)
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] {
@@ -160,10 +149,10 @@ func run() int {
return exportCommand(os.Args[2:])
case "check-source-plan":
return checkSourcePlanCommand(os.Args[2:])
- case "compile-plan":
- return compileCommand(os.Args[2:])
- case "approve-plan":
- return approveCommand(os.Args[2:])
+ case "check-plan":
+ return checkPlanCommand(os.Args[2:])
+ case "activate-plan":
+ return activatePlanCommand(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 c4f9c79..1991251 100644
--- a/boatstack/export.go
+++ b/boatstack/export.go
@@ -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. 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. 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)
}
@@ -148,9 +148,9 @@ func BuildExportBundle(configPath string, config ProjectConfig, rawConfig []byte
}
operations := map[string]string{
- "auto-plan": "Discover exactly one saved Plan-mode file, refine it into a draft feature package, and record its path as source_plan_path. Do not implement and do not imply the user accepted it.",
- "plan-gate": "Require the source Plan-mode file and explicit human approval. Only then run the project-local Boatstack helper to compile the executable task/evidence package and approval lock.",
- "build": "Before editing, locate the source Plan-mode file, feature spec, structured plan, compiled tasks, and plan lock; pass all of them to the project-local Boatstack helper when checking the lock. Stop if it reports BLOCKED. Implementation tactics remain open inside the approved boundary.",
+ "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.",
"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.",
@@ -169,7 +169,7 @@ alwaysApply: false
The source of truth is @.product-loop/workflow.md and @.product-loop/project.json.
Use @.product-loop/artifacts.md for document meanings and @.product-loop/failure-moves.md for improvement experiments.
Ordinary product intent starts in the host's Plan mode. Save the completed plan under .product-loop/intake/. Auto-plan discovers exactly one saved plan from bounded host locations, validates it, and must not invent a substitute. Keep the source plan present and current through build.
-Do not start build work until the explicit plan gate has produced a valid plan lock.
+Do not start build work until the explicit plan gate has produced approval.md and build activation has produced a valid plan lock.
Implementation methods are open. Claims of completion, approval, review, and shipping require evidence.
Do not branch behavior on model name, provider, or price; branch on observed work state and evidence.
`
@@ -191,9 +191,9 @@ 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. The source plan remains required and hash-current through plan-gate and 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. 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.
-Use .product-loop/artifacts.md for document boundaries and .product-loop/failure-moves.md for improvement experiments. 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.
+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.
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)
diff --git a/boatstack/export_test.go b/boatstack/export_test.go
index 36cbba4..9d0abcb 100644
--- a/boatstack/export_test.go
+++ b/boatstack/export_test.go
@@ -45,6 +45,8 @@ func TestExportAndDriftCheck(t *testing.T) {
".claude/skills/boatstack/SKILL.md",
".agents/skills/boatstack/SKILL.md",
".product-loop/.gitignore",
+ ".product-loop/templates/plan.md",
+ ".product-loop/templates/approval.md",
} {
if !fileExists(filepath.Join(repo, filepath.FromSlash(path))) {
t.Fatalf("expected generated file %s", path)
@@ -53,6 +55,21 @@ func TestExportAndDriftCheck(t *testing.T) {
if _, exists := bundle.Files[".product-loop/tools/approve_plan.py"]; exists {
t.Fatal("public export must not contain Python runtime tools")
}
+ if _, exists := bundle.Files[".product-loop/templates/plan.json"]; exists {
+ t.Fatal("Markdown-native planning must not export plan.json")
+ }
+ 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") {
+ 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") {
+ t.Fatal("plan-gate adapter does not keep approval in Plan mode")
+ }
+ if !strings.Contains(build, "activate-plan") || strings.Contains(build, "compile-plan") {
+ t.Fatal("build adapter must activate the Markdown plan exactly once")
+ }
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/plan.go b/boatstack/plan.go
index 5c366f8..c68817c 100644
--- a/boatstack/plan.go
+++ b/boatstack/plan.go
@@ -11,6 +11,13 @@ import (
"time"
)
+const (
+ planMarkerStart = ""
+ planMarkerEnd = ""
+ approvalMarkerStart = ""
+ approvalMarkerEnd = ""
+)
+
func stringValue(value any) string {
result, _ := value.(string)
return result
@@ -73,18 +80,94 @@ func validationSlice(value any) ([]map[string]any, bool) {
return result, true
}
-func LoadPlan(path string) (map[string]any, error) {
+func fencedJSONBlocks(value string) ([]string, error) {
+ lines := strings.Split(value, "\n")
+ blocks := []string{}
+ inJSON := false
+ current := []string{}
+ for _, line := range lines {
+ trimmed := strings.TrimSpace(line)
+ if !inJSON {
+ if trimmed == "```json" {
+ inJSON = true
+ current = nil
+ }
+ continue
+ }
+ if trimmed == "```" {
+ blocks = append(blocks, strings.Join(current, "\n"))
+ inJSON = false
+ current = nil
+ continue
+ }
+ current = append(current, line)
+ }
+ if inJSON {
+ return nil, fmt.Errorf("unterminated json fence")
+ }
+ return blocks, nil
+}
+
+func markedJSON(value, label, startMarker, endMarker string, allowLegacy bool) ([]byte, error) {
+ startCount := strings.Count(value, startMarker)
+ endCount := strings.Count(value, endMarker)
+ if startCount == 0 && endCount == 0 {
+ if !allowLegacy {
+ return nil, fmt.Errorf("%s is missing %s markers", label, label)
+ }
+ blocks, err := fencedJSONBlocks(value)
+ if err != nil {
+ return nil, fmt.Errorf("invalid %s: %w", label, err)
+ }
+ if len(blocks) != 1 {
+ return nil, fmt.Errorf("%s requires exactly one json fence; found %d", label, len(blocks))
+ }
+ return []byte(blocks[0]), nil
+ }
+ if startCount != 1 || endCount != 1 {
+ return nil, fmt.Errorf("%s requires exactly one marker pair", label)
+ }
+ start := strings.Index(value, startMarker) + len(startMarker)
+ end := strings.Index(value, endMarker)
+ if end <= start {
+ return nil, fmt.Errorf("%s markers are out of order", label)
+ }
+ blocks, err := fencedJSONBlocks(value[start:end])
+ if err != nil {
+ return nil, fmt.Errorf("invalid %s: %w", label, err)
+ }
+ if len(blocks) != 1 {
+ return nil, fmt.Errorf("marked %s requires exactly one json fence; found %d", label, len(blocks))
+ }
+ return []byte(blocks[0]), nil
+}
+
+func loadJSONObject(path, label, startMarker, endMarker string, allowLegacyMarkdown bool) (map[string]any, error) {
value, err := os.ReadFile(path)
if err != nil {
return nil, err
}
+ payload := value
+ if strings.EqualFold(filepath.Ext(path), ".md") {
+ payload, err = markedJSON(string(value), label, startMarker, endMarker, allowLegacyMarkdown)
+ if err != nil {
+ return nil, err
+ }
+ }
var plan map[string]any
- if err := json.Unmarshal(value, &plan); err != nil {
- return nil, err
+ if err := json.Unmarshal(payload, &plan); err != nil {
+ return nil, fmt.Errorf("invalid %s json: %w", label, err)
}
return plan, nil
}
+func LoadPlan(path string) (map[string]any, error) {
+ if !strings.EqualFold(filepath.Ext(path), ".md") {
+ return nil, fmt.Errorf("structured plan must be a Markdown file: %s", path)
+ }
+ return loadJSONObject(path, "structured plan", planMarkerStart, planMarkerEnd, true)
+}
+
func CheckSourcePlan(path string) error {
if strings.TrimSpace(path) == "" {
return fmt.Errorf("source plan path is required; start in the host Plan mode and save its plan before running auto-plan")
@@ -189,6 +272,100 @@ func SourcePlanForStructuredPlan(planPath string) (string, error) {
return filepath.Clean(sourcePlan), nil
}
+func SpecForStructuredPlan(planPath string) (string, error) {
+ plan, err := LoadPlan(planPath)
+ if err != nil {
+ return "", err
+ }
+ spec := stringValue(plan["spec_path"])
+ if strings.TrimSpace(spec) == "" {
+ return "", fmt.Errorf("spec_path is required")
+ }
+ if !filepath.IsAbs(spec) {
+ spec = filepath.Join(filepath.Dir(planPath), spec)
+ }
+ return filepath.Clean(spec), nil
+}
+
+func checkNonEmptyFile(path, label string) error {
+ info, err := os.Stat(path)
+ if err != nil || !info.Mode().IsRegular() {
+ return fmt.Errorf("%s does not exist as a regular file: %s", label, path)
+ }
+ value, err := os.ReadFile(path)
+ if err != nil {
+ return fmt.Errorf("%s is unreadable: %w", label, err)
+ }
+ if strings.TrimSpace(string(value)) == "" {
+ return fmt.Errorf("%s is empty: %s", label, path)
+ }
+ return nil
+}
+
+type PlanCheck struct {
+ Plan map[string]any
+ PlanPath string
+ SourcePlanPath string
+ SpecPath string
+ PlanHash string
+ SourcePlanHash string
+ SpecHash string
+ Fingerprint string
+}
+
+func CheckPlan(planPath string) (PlanCheck, error) {
+ plan, err := LoadPlan(planPath)
+ if err != nil {
+ return PlanCheck{}, err
+ }
+ if err := ValidatePlan(plan); err != nil {
+ return PlanCheck{}, err
+ }
+ sourcePlan, err := SourcePlanForStructuredPlan(planPath)
+ if err != nil {
+ return PlanCheck{}, err
+ }
+ if err := CheckSourcePlan(sourcePlan); err != nil {
+ return PlanCheck{}, err
+ }
+ spec, err := SpecForStructuredPlan(planPath)
+ if err != nil {
+ return PlanCheck{}, err
+ }
+ if err := checkNonEmptyFile(spec, "feature spec"); err != nil {
+ return PlanCheck{}, err
+ }
+ planHash, err := SHA256File(planPath)
+ if err != nil {
+ return PlanCheck{}, err
+ }
+ sourcePlanHash, err := SHA256File(sourcePlan)
+ if err != nil {
+ return PlanCheck{}, err
+ }
+ specHash, err := SHA256File(spec)
+ if err != nil {
+ return PlanCheck{}, err
+ }
+ fingerprintInput, err := MarshalJSON(map[string]any{
+ "schema_version": 1,
+ "plan_path": filepath.Base(planPath),
+ "plan_sha256": planHash,
+ "source_plan_path": filepath.ToSlash(filepath.Clean(stringValue(plan["source_plan_path"]))),
+ "source_plan_sha256": sourcePlanHash,
+ "spec_path": filepath.ToSlash(filepath.Clean(stringValue(plan["spec_path"]))),
+ "spec_sha256": specHash,
+ })
+ if err != nil {
+ return PlanCheck{}, err
+ }
+ return PlanCheck{
+ Plan: plan, PlanPath: filepath.Clean(planPath), SourcePlanPath: sourcePlan, SpecPath: spec,
+ PlanHash: planHash, SourcePlanHash: sourcePlanHash, SpecHash: specHash,
+ Fingerprint: SHA256Bytes(fingerprintInput),
+ }, nil
+}
+
func checkApprovalSourcePlan(options ApprovalOptions) error {
expected, err := SourcePlanForStructuredPlan(options.PlanPath)
if err != nil {
@@ -218,6 +395,15 @@ func ValidatePlan(plan map[string]any) error {
if stringValue(plan["source_plan_path"]) == "" {
return fmt.Errorf("source_plan_path is required")
}
+ if questions, present := plan["blocking_questions"]; present {
+ values, ok := stringSlice(questions)
+ if !ok {
+ return fmt.Errorf("blocking_questions must be a list of question ids")
+ }
+ if len(values) > 0 {
+ return fmt.Errorf("unresolved blocking questions: %s", strings.Join(values, ", "))
+ }
+ }
criteria, ok := objectSlice(plan["acceptance_criteria"])
if !ok || len(criteria) == 0 {
return fmt.Errorf("at least one acceptance criterion is required")
@@ -448,6 +634,103 @@ type ApprovalOptions struct {
OutputPath string
}
+type ApprovalReceipt struct {
+ SchemaVersion int
+ Status string
+ ApprovedBy string
+ ApprovedAt string
+ Fingerprint string
+}
+
+func LoadApprovalReceipt(path string) (ApprovalReceipt, error) {
+ if !strings.EqualFold(filepath.Ext(path), ".md") {
+ return ApprovalReceipt{}, fmt.Errorf("approval receipt must be a Markdown file: %s", path)
+ }
+ value, err := loadJSONObject(path, "approval receipt", approvalMarkerStart, approvalMarkerEnd, false)
+ if err != nil {
+ return ApprovalReceipt{}, err
+ }
+ receipt := ApprovalReceipt{
+ SchemaVersion: intValue(value["schema_version"]),
+ Status: stringValue(value["status"]),
+ ApprovedBy: stringValue(value["approved_by"]),
+ ApprovedAt: stringValue(value["approved_at"]),
+ Fingerprint: stringValue(value["approval_fingerprint"]),
+ }
+ if receipt.SchemaVersion != 1 {
+ return ApprovalReceipt{}, fmt.Errorf("approval receipt schema_version must be 1")
+ }
+ if receipt.Status != "APPROVED" {
+ return ApprovalReceipt{}, fmt.Errorf("approval receipt status must be APPROVED")
+ }
+ if strings.TrimSpace(receipt.ApprovedBy) == "" {
+ return ApprovalReceipt{}, fmt.Errorf("approval receipt must name the human approver")
+ }
+ if _, err := time.Parse(time.RFC3339, receipt.ApprovedAt); err != nil {
+ return ApprovalReceipt{}, fmt.Errorf("approval receipt approved_at must be RFC3339: %w", err)
+ }
+ if strings.TrimSpace(receipt.Fingerprint) == "" {
+ return ApprovalReceipt{}, fmt.Errorf("approval receipt fingerprint is required")
+ }
+ return receipt, nil
+}
+
+func intValue(value any) int {
+ number, ok := value.(float64)
+ if !ok {
+ return 0
+ }
+ return int(number)
+}
+
+func CheckApprovalReceipt(path string, planCheck PlanCheck) (ApprovalReceipt, error) {
+ receipt, err := LoadApprovalReceipt(path)
+ if err != nil {
+ return ApprovalReceipt{}, err
+ }
+ if receipt.Fingerprint != planCheck.Fingerprint {
+ return ApprovalReceipt{}, fmt.Errorf("stale approval receipt: fingerprint does not match the current source plan, spec, and plan")
+ }
+ return receipt, nil
+}
+
+type ActivationOptions struct {
+ PlanPath string
+ ApprovalPath string
+ OutDir string
+ OutputPath string
+ SourceCommit string
+}
+
+func ActivatePlan(options ActivationOptions) error {
+ check, err := CheckPlan(options.PlanPath)
+ if err != nil {
+ return err
+ }
+ receipt, err := CheckApprovalReceipt(options.ApprovalPath, check)
+ if err != nil {
+ return err
+ }
+ if err := CompilePlanFiles(options.PlanPath, options.OutDir); err != nil {
+ return err
+ }
+ tasksPath := filepath.Join(options.OutDir, "tasks.json")
+ approval := ApprovalOptions{
+ SourcePlanPath: check.SourcePlanPath,
+ SpecPath: check.SpecPath,
+ PlanPath: options.PlanPath,
+ TasksPath: tasksPath,
+ ApprovedBy: receipt.ApprovedBy,
+ ApprovedAt: receipt.ApprovedAt,
+ SourceCommit: options.SourceCommit,
+ OutputPath: options.OutputPath,
+ }
+ if err := CreateApprovalLock(approval); err != nil {
+ return err
+ }
+ return CheckApprovalLock(approval)
+}
+
func gitCommit(directory string) string {
command := exec.Command("git", "-C", directory, "rev-parse", "HEAD")
value, err := command.Output()
diff --git a/boatstack/plan_test.go b/boatstack/plan_test.go
index c9cd046..c415b24 100644
--- a/boatstack/plan_test.go
+++ b/boatstack/plan_test.go
@@ -10,9 +10,11 @@ import (
func validPlan() map[string]any {
return map[string]any{
- "schema_version": float64(1),
- "feature_id": "feature-one",
- "source_plan_path": "source-plan.md",
+ "schema_version": float64(1),
+ "feature_id": "feature-one",
+ "source_plan_path": "source-plan.md",
+ "spec_path": "spec.md",
+ "blocking_questions": []any{},
"acceptance_criteria": []any{
map[string]any{"id": "AC-1", "text": "observable result"},
},
@@ -30,54 +32,215 @@ func validPlan() map[string]any {
}
}
-func TestPlanCompilationApprovalAndStaleness(t *testing.T) {
- root := t.TempDir()
+func writeMarkdownPlan(t *testing.T, path string, plan map[string]any, marked bool) {
+ t.Helper()
+ value, err := MarshalJSON(plan)
+ if err != nil {
+ t.Fatal(err)
+ }
+ body := "# Structured plan\n\nHuman-readable summary covered by approval.\n\n"
+ if marked {
+ body += planMarkerStart + "\n"
+ }
+ body += "```json\n" + strings.TrimSpace(string(value)) + "\n```\n"
+ if marked {
+ body += planMarkerEnd + "\n"
+ }
+ if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func writeApprovalReceipt(t *testing.T, path, fingerprint string) {
+ t.Helper()
+ body := `# Plan approval
+
+
+` + "```json\n" + `{
+ "schema_version": 1,
+ "status": "APPROVED",
+ "approved_by": "Test Human",
+ "approved_at": "2026-07-16T12:00:00Z",
+ "approval_fingerprint": "` + fingerprint + `"
+}
+` + "```\n" + `
+`
+ if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func writePlanInputs(t *testing.T, root string, marked bool) (string, string, string) {
+ t.Helper()
sourcePlan := filepath.Join(root, "source-plan.md")
spec := filepath.Join(root, "spec.md")
- planPath := filepath.Join(root, "plan.json")
- compiled := filepath.Join(root, "compiled")
- lock := filepath.Join(root, "plan.lock.json")
+ planPath := filepath.Join(root, "plan.md")
if err := os.WriteFile(sourcePlan, []byte("# Host Plan-mode proposal\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(spec, []byte("# Accepted spec\n"), 0o644); err != nil {
t.Fatal(err)
}
- planJSON, _ := MarshalJSON(validPlan())
- if err := os.WriteFile(planPath, planJSON, 0o644); err != nil {
+ writeMarkdownPlan(t, planPath, validPlan(), marked)
+ return sourcePlan, spec, planPath
+}
+
+func TestMarkdownPlanActivationAndStaleness(t *testing.T) {
+ root := t.TempDir()
+ sourcePlan, _, planPath := writePlanInputs(t, root, true)
+ approval := filepath.Join(root, "approval.md")
+ compiled := filepath.Join(root, "compiled")
+ lock := filepath.Join(root, "plan.lock.json")
+ check, err := CheckPlan(planPath)
+ if err != nil {
t.Fatal(err)
}
- if err := CompilePlanFiles(planPath, compiled); err != nil {
+ writeApprovalReceipt(t, approval, check.Fingerprint)
+ options := ActivationOptions{PlanPath: planPath, ApprovalPath: approval, OutDir: compiled, OutputPath: lock, SourceCommit: "test"}
+ if err := ActivatePlan(options); err != nil {
t.Fatal(err)
}
- tasks := filepath.Join(compiled, "tasks.json")
- options := ApprovalOptions{
- SourcePlanPath: sourcePlan,
- SpecPath: spec, PlanPath: planPath, TasksPath: tasks,
- ApprovedBy: "Test Human", ApprovedAt: "2026-07-16T12:00:00Z",
- SourceCommit: "test", OutputPath: lock,
+ for _, path := range []string{filepath.Join(compiled, "tasks.json"), filepath.Join(compiled, "test-matrix.json"), filepath.Join(compiled, "evidence.md"), lock} {
+ if info, err := os.Stat(path); err != nil || !info.Mode().IsRegular() {
+ t.Fatalf("expected activated artifact %s", path)
+ }
}
- if err := CreateApprovalLock(options); err != nil {
+ if err := os.WriteFile(sourcePlan, []byte("# Changed host plan\n"), 0o644); err != nil {
t.Fatal(err)
}
- if err := CheckApprovalLock(options); err != nil {
+ if err := ActivatePlan(options); err == nil || !strings.Contains(err.Error(), "stale approval") {
+ t.Fatalf("expected stale approval after source-plan change, got %v", err)
+ }
+ if err := os.WriteFile(sourcePlan, []byte("# Host Plan-mode proposal\n"), 0o644); err != nil {
t.Fatal(err)
}
- if err := os.WriteFile(sourcePlan, []byte("# Changed host plan\n"), 0o644); err != nil {
+ value, _ := os.ReadFile(planPath)
+ if err := os.WriteFile(planPath, append([]byte("Changed human summary.\n"), value...), 0o644); err != nil {
t.Fatal(err)
}
- if err := CheckApprovalLock(options); err == nil || !strings.Contains(err.Error(), "source_plan") {
- t.Fatalf("expected stale source plan lock, got %v", err)
+ if err := ActivatePlan(options); err == nil || !strings.Contains(err.Error(), "stale approval") {
+ t.Fatalf("expected stale approval after plan prose change, got %v", err)
}
- if err := os.WriteFile(sourcePlan, []byte("# Host Plan-mode proposal\n"), 0o644); err != nil {
+}
+
+func TestCurrentCursorSingleJSONFencePlanIsAccepted(t *testing.T) {
+ root := t.TempDir()
+ _, _, planPath := writePlanInputs(t, root, false)
+ if _, err := CheckPlan(planPath); err != nil {
+ t.Fatalf("current Cursor plan.md shape should be accepted: %v", err)
+ }
+}
+
+func TestPlanJSONIsRejected(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "plan.json")
+ value, _ := MarshalJSON(validPlan())
+ if err := os.WriteFile(path, value, 0o644); err != nil {
t.Fatal(err)
}
- value, _ := os.ReadFile(planPath)
- if err := os.WriteFile(planPath, append(value, '\n'), 0o644); err != nil {
+ if _, err := LoadPlan(path); err == nil || !strings.Contains(err.Error(), "Markdown") {
+ t.Fatalf("expected clean-cut Markdown-only plan contract, got %v", err)
+ }
+}
+
+func TestMarkdownPlanRejectsMissingMultipleMalformedAndOpenQuestions(t *testing.T) {
+ root := t.TempDir()
+ _, _, planPath := writePlanInputs(t, root, true)
+
+ if err := os.WriteFile(planPath, []byte("# no structured block\n"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := CheckPlan(planPath); err == nil {
+ t.Fatal("expected missing block to fail")
+ }
+
+ value, _ := MarshalJSON(validPlan())
+ multiple := "# ambiguous\n\n```json\n" + string(value) + "```\n\n```json\n" + string(value) + "```\n"
+ if err := os.WriteFile(planPath, []byte(multiple), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := CheckPlan(planPath); err == nil || !strings.Contains(err.Error(), "exactly one") {
+ t.Fatalf("expected multiple blocks to fail, got %v", err)
+ }
+
+ malformed := planMarkerStart + "\n```json\n{bad}\n```\n" + planMarkerEnd + "\n"
+ if err := os.WriteFile(planPath, []byte(malformed), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := CheckPlan(planPath); err == nil || !strings.Contains(err.Error(), "invalid structured plan json") {
+ t.Fatalf("expected malformed json to fail, got %v", err)
+ }
+
+ plan := validPlan()
+ plan["blocking_questions"] = []any{"Q-4"}
+ writeMarkdownPlan(t, planPath, plan, true)
+ if _, err := CheckPlan(planPath); err == nil || !strings.Contains(err.Error(), "Q-4") {
+ t.Fatalf("expected open material question to block, got %v", err)
+ }
+}
+
+func TestReadOnlyCheckAndFailedActivationWriteNothing(t *testing.T) {
+ root := t.TempDir()
+ _, _, planPath := writePlanInputs(t, root, true)
+ before, _ := os.ReadDir(root)
+ check, err := CheckPlan(planPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ after, _ := os.ReadDir(root)
+ if len(after) != len(before) {
+ t.Fatalf("check-plan wrote files: before=%d after=%d", len(before), len(after))
+ }
+ approval := filepath.Join(root, "approval.md")
+ compiled := filepath.Join(root, "compiled")
+ lock := filepath.Join(root, "plan.lock.json")
+ activation := ActivationOptions{PlanPath: planPath, ApprovalPath: approval, OutDir: compiled, OutputPath: lock}
+ err = ActivatePlan(activation)
+ if err == nil {
+ t.Fatal("expected missing approval receipt to block")
+ }
+ writeApprovalReceipt(t, approval, "wrong-"+check.Fingerprint)
+ err = ActivatePlan(activation)
+ if err == nil || !strings.Contains(err.Error(), "stale approval") {
+ t.Fatalf("expected invalid receipt to block, got %v", err)
+ }
+ if _, err := os.Stat(compiled); !os.IsNotExist(err) {
+ t.Fatal("failed activation created compiled output")
+ }
+ if _, err := os.Stat(lock); !os.IsNotExist(err) {
+ t.Fatal("failed activation created a plan lock")
+ }
+}
+
+func TestApprovalReceiptRequiresMarkersHumanTimestampAndFingerprint(t *testing.T) {
+ root := t.TempDir()
+ _, _, planPath := writePlanInputs(t, root, true)
+ check, err := CheckPlan(planPath)
+ if err != nil {
t.Fatal(err)
}
- if err := CheckApprovalLock(options); err == nil || !strings.Contains(err.Error(), "stale") {
- t.Fatalf("expected stale plan lock, got %v", err)
+ approval := filepath.Join(root, "approval.md")
+ cases := []struct {
+ name string
+ body string
+ want string
+ }{
+ {name: "unmarked", body: "```json\n{}\n```\n", want: "markers"},
+ {name: "missing human", body: `{"schema_version":1,"status":"APPROVED","approved_by":"","approved_at":"2026-07-16T12:00:00Z","approval_fingerprint":"` + check.Fingerprint + `"}`, want: "human approver"},
+ {name: "bad timestamp", body: `{"schema_version":1,"status":"APPROVED","approved_by":"Test Human","approved_at":"today","approval_fingerprint":"` + check.Fingerprint + `"}`, want: "RFC3339"},
+ }
+ for _, test := range cases {
+ t.Run(test.name, func(t *testing.T) {
+ body := test.body
+ if test.name != "unmarked" {
+ body = approvalMarkerStart + "\n```json\n" + body + "\n```\n" + approvalMarkerEnd + "\n"
+ }
+ if err := os.WriteFile(approval, []byte(body), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := CheckApprovalReceipt(approval, check); err == nil || !strings.Contains(err.Error(), test.want) {
+ t.Fatalf("expected %q error, got %v", test.want, err)
+ }
+ })
}
}
diff --git a/boatstack/references/artifacts.md b/boatstack/references/artifacts.md
index 12e9a3e..9259806 100644
--- a/boatstack/references/artifacts.md
+++ b/boatstack/references/artifacts.md
@@ -10,7 +10,9 @@ Artifacts separate facts, decisions, unknowns, incompleteness, and evidence. Com
| Feature brief/spec | Product intent, outcomes, scenarios, acceptance criteria, non-goals | A product slice is proposed or its intent changes |
| Question ledger | Unknowns, choices, human answers, provenance, expiry | The repo cannot answer a material question |
| ADR | Accepted durable architecture decision and rationale | A meaningful architecture choice is accepted |
-| Plan/tasks | Dependency-ordered implementation operations and checks | A spec is resolved enough to build |
+| Markdown plan | Human-readable plan plus its one marked structured block; canonical before and during build | A spec is resolved enough to propose tasks and checks |
+| Approval receipt | Named human, timestamp, and fingerprint in Markdown; not executable state | The exact draft is explicitly approved in Plan mode |
+| Compiled tasks | Deterministic dependency graph generated from the approved Markdown plan | Build activation succeeds |
| Test plan | Requirement-to-evidence mapping with each validation's origin, falsifiable oracle, procedure, and independence | Planning and after discovered failure modes |
| Gap ledger | Known divergence between desired and current state | Work is deferred, partial, incompatible, or intentionally absent |
| Risk/threat note | Assets, actors, trust boundaries, abuse/failure paths | Security, data, tenancy, billing, auth, or destructive paths change |
@@ -59,6 +61,10 @@ Every material statement should indicate whether it came from:
Generated artifacts include the canonical loop version and config hash. Human edits to generated adapters are drift and should be moved into project-owned context or canonical source.
+## Planning boundary
+
+`auto-plan` and `plan-gate` create or update Markdown only. `plan.md` is the canonical structured input and `approval.md` is the human-approval receipt. Compiled JSON and `plan.lock.json` begin only at `build` activation, after the receipt is verified. This keeps planning compatible with hosts that intentionally restrict Plan mode to documents.
+
## Templates
Copy only the templates required for the current slice from `assets/templates/`. Do not create empty ceremony. The feature spec, question ledger, test plan, gap ledger, and evidence ledger are the usual minimum for material product work.
diff --git a/boatstack/references/workflow.md b/boatstack/references/workflow.md
index 775e2a5..c4c311e 100644
--- a/boatstack/references/workflow.md
+++ b/boatstack/references/workflow.md
@@ -10,6 +10,8 @@ INTENT
-> SPEC
-> PLAN
-> PLAN_GATE
+ -> PLAN_APPROVED
+ -> BUILD_ACTIVATION
-> PLAN_LOCKED
-> BUILD
-> TEST_GATE
@@ -101,6 +103,8 @@ Run only relevant review lenses:
If gstack is installed, its review skills can execute these lenses. If Spec Kit is installed, it can generate and cross-check the spec, plan, tasks, and checklists. Their output is normalized into this artifact contract.
+`plan.md` is the canonical structured plan. Its human-readable prose and one marked JSON block are a single approval surface. Until `BUILD_ACTIVATION`, feature artifacts are Markdown only; no compiled task graph, machine lock, or executable state exists.
+
Validation must be derived before implementation. Each check records:
- `run`: an executable command or a specific human/external procedure;
@@ -113,19 +117,25 @@ Subjective work is not exempt from validation. Convert ambiguity into an approve
### `PLAN -> PLAN_GATE`
-Present the full draft and require an explicit human `approve` or a change request. Do not interpret silence, a new implementation question, or a tool permission as plan approval.
+Run `boatstack-helper check-plan --plan /plan.md`, present the full draft and returned fingerprint, then require an explicit human `approve` or a change request. The check is read-only. Do not interpret silence, a new implementation question, a tool permission, or permission to build as plan approval.
+
+### `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.
+
+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`.
-### `PLAN_GATE -> PLAN_LOCKED`
+### `PLAN_APPROVED -> BUILD_ACTIVATION -> PLAN_LOCKED`
-After approval, deterministically:
+At the host's normal Build transition, before the first product-code edit, `activate-plan` deterministically:
-1. hash the approved spec and plan;
-2. hash the saved source Plan-mode file;
-3. compile the approved structured plan into the task graph, requirement-test traceability rows, evidence skeleton, and expected gate commands without adding semantics;
+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`;
+3. compile the task graph, requirement-test traceability rows, and evidence skeleton without adding semantics;
4. record approver, timestamp, source commit, and all artifact hashes in `plan.lock.json`;
-5. verify every task maps to at least one acceptance criterion or declared enabling dependency.
+5. write the lock last and recheck it before permitting implementation.
-Any later change to the source plan, approved spec, or structured plan before build completes invalidates the lock and returns the feature to `PLAN_GATE`.
+Missing approval, unresolved `blocking_questions`, or any change to the source plan, approved spec, or complete `plan.md` blocks activation and returns the feature to `PLAN_GATE`. A failed or partial compilation never creates a valid lock.
### `PLAN_LOCKED -> BUILD`
diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md
index e9d080d..2e940c0 100644
--- a/docs/evidence-engineered-coding.md
+++ b/docs/evidence-engineered-coding.md
@@ -90,18 +90,19 @@ 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 **4044 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 **4521 estimated tokens**, while host adapters point to one operation at a time.
## Control appears at transitions
-The plan gate is a concrete controller boundary:
+The Plan-mode approval gate is a concrete controller boundary:
```text
-if sha256(current_plan) != approval.plan_sha256:
- BLOCKED: plan changed after approval
+fingerprint = hash(source_plan + spec + complete_plan_md)
+if fingerprint != approval_md.approval_fingerprint:
+ BLOCKED: planning input changed after approval
```
-The plan compiler is another:
+Build activation adds the machine coverage boundary:
```text
uncovered = acceptance_ids - task_acceptance_ids
@@ -139,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 [`40ebae5dfb3d090812301a438aaac079426edcfc`](https://github.com/operatorstack/intelligence-flow/tree/40ebae5dfb3d090812301a438aaac079426edcfc/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 [`3cd11c293b3903aeac9ff8d8eacd47840d63a02d`](https://github.com/operatorstack/intelligence-flow/tree/3cd11c293b3903aeac9ff8d8eacd47840d63a02d/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/research-and-design.md b/docs/research-and-design.md
index 5cf807b..fd22b3c 100644
--- a/docs/research-and-design.md
+++ b/docs/research-and-design.md
@@ -33,17 +33,17 @@ Value emerges twice. The delivery path reduces assumption-driven code and produc
```text
Cursor/GitHub intent
-> host Plan mode saved source plan; no implementation
- -> /auto-plan validate source plan, then draft spec + structured plan; no code
+ -> /auto-plan validate source plan, then Markdown-only draft package; no code
-> /plan-gate explicit human approve/change request
- after approval: compile tasks/test matrix/evidence + hash lock
- -> /build refuses absent or stale lock
+ after approval: write Markdown approval receipt only
+ -> /build verify receipt, compile tasks/evidence + lock, then code
-> /test-gate requirement-derived independent evidence
-> /review diff + intent + invariant + risk + gap review
-> /ship PR preparation, not merge/deploy
-> /retro propose a harness move; never silently promote it
```
-`/auto-plan` cannot infer acceptance from silence. `/plan-gate` records the approver and hashes the source Plan-mode file, spec, structured plan, and compiled task graph. Any semantic edit before build completes invalidates the lock and returns to approval. This turns the developer's agreement into a machine-checkable state transition instead of conversational memory.
+`/auto-plan` cannot infer acceptance from silence. The canonical `plan.md` contains human-readable reasoning and one marked structured block. `/plan-gate` records explicit acceptance in `approval.md` using a fingerprint over the source Plan-mode file, spec, and complete plan. At the normal Build transition, Boatstack verifies that receipt, compiles the machine task graph and evidence, then writes and checks the lock before code changes. Any planning edit invalidates the receipt and returns to approval. This turns agreement into a machine-checkable state transition without asking Plan mode to write executable state.
`/auto-plan` is deliberately not the first planning surface. It requires exactly one non-empty file produced by the active host's Plan mode and refuses to invent that input. It resolves the active plan from host/system conversation context first, then checks only bounded plan locations; zero or multiple candidates block instead of silently choosing the newest file. The source file is hash-bound through build; after build, test/review/ship consume the approved lock, diff, and evidence rather than repeatedly loading the exploratory plan.
diff --git a/examples/diagram-json/README.md b/examples/diagram-json/README.md
index 5b3d971..20f2787 100644
--- a/examples/diagram-json/README.md
+++ b/examples/diagram-json/README.md
@@ -43,12 +43,18 @@ The result is a draft, not code:
- [source Plan-mode file](source-plan.md)
- [question and decision ledger](questions.md)
- [feature specification](spec.md)
-- [structured plan](plan.json)
+- [canonical Markdown plan](plan.md)
## The missing human step
-The agent presents the draft with the three contract decisions in
-`questions.md`. A representative exchange is:
+The agent first validates `plan.md` without creating files:
+
+```bash
+.product-loop/bin/boatstack-helper check-plan --plan plan.md
+```
+
+It then presents the draft, its fingerprint, and the three contract decisions
+in `questions.md`. A representative exchange is:
```text
Agent: The draft is ready. The recommended contract is an additive serializer,
@@ -58,19 +64,18 @@ Approve this plan or tell me what to revise.
Example Maintainer: Approve this demonstration plan.
```
-Only after that explicit answer does `/plan-gate` compile and lock the plan:
+Only after that explicit answer does `/plan-gate` write the Markdown
+[approval receipt](approval.md). No JSON, lock, executable state, or product
+code is created in Plan mode.
+
+When the developer uses the host's normal Build transition, `/build` activates
+the exact approved plan before its first product-code edit:
```bash
-.product-loop/bin/boatstack-helper compile-plan \
- --plan plan.json \
- --out-dir compiled
-
-.product-loop/bin/boatstack-helper approve-plan \
- --source-plan source-plan.md \
- --spec spec.md \
- --plan plan.json \
- --tasks compiled/tasks.json \
- --approved-by "Example Maintainer (simulated walkthrough)" \
+.product-loop/bin/boatstack-helper activate-plan \
+ --plan plan.md \
+ --approval approval.md \
+ --out-dir compiled \
--output plan.lock.json
```
@@ -81,18 +86,18 @@ That produces:
- [evidence ledger](compiled/evidence.md)
- [content-addressed plan lock](plan.lock.json)
-The lock is the deterministic boundary between agreement and implementation.
-Editing `source-plan.md`, `spec.md`, `plan.json`, or the compiled task graph
-makes its check fail before or during `/build`.
+The approval receipt is the persisted human boundary; activation turns that
+approved state into deterministic machine artifacts. Editing `source-plan.md`,
+`spec.md`, or any part of `plan.md` changes the fingerprint and blocks before
+implementation.
```bash
-.product-loop/bin/boatstack-helper approve-plan \
- --source-plan source-plan.md \
- --spec spec.md \
- --plan plan.json \
- --tasks compiled/tasks.json \
- --output plan.lock.json \
- --check
+.product-loop/bin/boatstack-helper activate-plan \
+ --plan plan.md \
+ --approval approval.md \
+ --out-dir compiled \
+ --output plan.lock.json
+# BLOCKED: stale approval receipt
```
## What happens next in a real feature
@@ -100,7 +105,7 @@ makes its check fail before or during `/build`.
The remaining commands consume the same canonical artifacts regardless of the
coding model or host:
-1. `/build` verifies the lock, implements one task at a time, and stops for any
+1. `/build` activates and verifies the lock, implements one task at a time, and stops for any
newly discovered product decision.
2. `/test-gate` runs the matrix and attaches command output or fixture evidence
to each acceptance criterion.
diff --git a/examples/diagram-json/approval.md b/examples/diagram-json/approval.md
new file mode 100644
index 0000000..def70de
--- /dev/null
+++ b/examples/diagram-json/approval.md
@@ -0,0 +1,15 @@
+# Plan approval: diagram-json-v1
+
+This is a simulated walkthrough receipt, not authorization to implement or ship a real feature.
+
+
+```json
+{
+ "schema_version": 1,
+ "status": "APPROVED",
+ "approved_by": "Example Maintainer (simulated walkthrough)",
+ "approved_at": "2026-07-16T12:00:00Z",
+ "approval_fingerprint": "6f140ee4aa648d76d82ba3af8d9ac6436002cd2ee39d9bcc22337b25005b1015"
+}
+```
+
diff --git a/examples/diagram-json/plan.lock.json b/examples/diagram-json/plan.lock.json
index 9c9c52d..7e40732 100644
--- a/examples/diagram-json/plan.lock.json
+++ b/examples/diagram-json/plan.lock.json
@@ -1,13 +1,13 @@
{
- "approved_at": "2026-07-16T12:00:00+00:00",
+ "approved_at": "2026-07-16T12:00:00Z",
"approved_by": "Example Maintainer (simulated walkthrough)",
"invalidated_at": null,
"invalidation_reason": null,
- "plan_path": "examples/diagram-json/plan.json",
- "plan_sha256": "df1b205517cf7dbdf5c5db65a342622922bd959a5ba885326888f6dd2b9c50d3",
+ "plan_path": "examples/diagram-json/plan.md",
+ "plan_sha256": "3ad35cc3cbe48306e7ee401bd9e9047d25e46c8a6fe9679aa1b3f5e96ceea292",
"schema_version": 1,
- "source_commit": "40ebae5dfb3d090812301a438aaac079426edcfc",
- "source_plan_path": "source-plan.md",
+ "source_commit": "3cd11c293b3903aeac9ff8d8eacd47840d63a02d",
+ "source_plan_path": "examples/diagram-json/source-plan.md",
"source_plan_sha256": "e10593ddaa7522ab80cc991d0a09399257139799e37f737794cd49d68a39985b",
"spec_path": "examples/diagram-json/spec.md",
"spec_sha256": "a943c81cf2a88d23d5b300e6b9dc1dafc80923a9b6b9ab5297a67b4e2054b9d5",
diff --git a/examples/diagram-json/plan.json b/examples/diagram-json/plan.md
similarity index 77%
rename from examples/diagram-json/plan.json
rename to examples/diagram-json/plan.md
index 352ccbd..6216986 100644
--- a/examples/diagram-json/plan.json
+++ b/examples/diagram-json/plan.md
@@ -1,8 +1,15 @@
+# Structured plan: diagram-json-v1
+
+This is the canonical human-readable and machine-checkable plan used by the worked example.
+
+
+```json
{
"schema_version": 1,
"feature_id": "diagram-json-v1",
"source_plan_path": "source-plan.md",
- "spec_path": "examples/diagram-json/spec.md",
+ "spec_path": "spec.md",
+ "blocking_questions": [],
"acceptance_criteria": [
{
"id": "AC-1",
@@ -30,29 +37,17 @@
"id": "T-1",
"title": "Define the v1 schema and pure serializer at the diagram boundary",
"depends_on": [],
- "acceptance_criteria": [
- "AC-1",
- "AC-2",
- "AC-3"
- ],
+ "acceptance_criteria": ["AC-1", "AC-2", "AC-3"],
"validation": [
{
- "criteria": [
- "AC-1",
- "AC-2",
- "AC-3"
- ],
+ "criteria": ["AC-1", "AC-2", "AC-3"],
"run": "pnpm typecheck",
"origin": "Public schema types required by AC-1, AC-2, and AC-3",
"oracle": "The repository's existing TypeScript compiler configuration",
"independence": "pre-existing"
},
{
- "criteria": [
- "AC-1",
- "AC-2",
- "AC-3"
- ],
+ "criteria": ["AC-1", "AC-2", "AC-3"],
"run": "pnpm exec tsx examples/05-diagram-printer/json-check.ts",
"origin": "The approved JSON contract in AC-1, AC-2, and AC-3",
"oracle": "Parser, schema-version, ordering, and compact-overlay assertions derived from the approved contract",
@@ -64,26 +59,18 @@
{
"id": "T-2",
"title": "Expose and document the additive public contract",
- "depends_on": [
- "T-1"
- ],
- "acceptance_criteria": [
- "AC-5"
- ],
+ "depends_on": ["T-1"],
+ "acceptance_criteria": ["AC-5"],
"validation": [
{
- "criteria": [
- "AC-5"
- ],
+ "criteria": ["AC-5"],
"run": "pnpm typecheck",
"origin": "The public type-export requirement in AC-5",
"oracle": "The repository's existing TypeScript compiler configuration",
"independence": "pre-existing"
},
{
- "criteria": [
- "AC-5"
- ],
+ "criteria": ["AC-5"],
"run": "pnpm build",
"origin": "The package export and documentation contract in AC-5",
"oracle": "The repository's existing production build",
@@ -95,63 +82,39 @@
{
"id": "T-3",
"title": "Add contract fixtures and prove text-renderer compatibility",
- "depends_on": [
- "T-1",
- "T-2"
- ],
- "acceptance_criteria": [
- "AC-1",
- "AC-2",
- "AC-3",
- "AC-4",
- "AC-5"
- ],
+ "depends_on": ["T-1", "T-2"],
+ "acceptance_criteria": ["AC-1", "AC-2", "AC-3", "AC-4", "AC-5"],
"validation": [
{
- "criteria": [
- "AC-1",
- "AC-2",
- "AC-3"
- ],
+ "criteria": ["AC-1", "AC-2", "AC-3"],
"run": "pnpm exec tsx examples/05-diagram-printer/json-check.ts",
"origin": "The approved JSON behaviors in AC-1, AC-2, and AC-3",
"oracle": "Contract-derived parser and fixture assertions",
"independence": "contract-derived"
},
{
- "criteria": [
- "AC-4"
- ],
+ "criteria": ["AC-4"],
"run": "pnpm example:diagram",
"origin": "The existing diagram behavior protected by AC-4",
"oracle": "The repository's pre-feature executable example",
"independence": "pre-existing"
},
{
- "criteria": [
- "AC-4"
- ],
+ "criteria": ["AC-4"],
"run": "diff -u examples/05-diagram-printer/expected-output.txt <(pnpm --silent example:diagram)",
"origin": "The byte-compatibility decision in AC-4",
"oracle": "The pre-feature expected ASCII fixture",
"independence": "pre-existing"
},
{
- "criteria": [
- "AC-1",
- "AC-2",
- "AC-3",
- "AC-5"
- ],
+ "criteria": ["AC-1", "AC-2", "AC-3", "AC-5"],
"run": "pnpm typecheck",
"origin": "The public type contracts in AC-1 through AC-5",
"oracle": "The repository's existing TypeScript compiler configuration",
"independence": "pre-existing"
},
{
- "criteria": [
- "AC-5"
- ],
+ "criteria": ["AC-5"],
"run": "pnpm build",
"origin": "The distributable package contract in AC-5",
"oracle": "The repository's existing production build",
@@ -162,3 +125,5 @@
}
]
}
+```
+