diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d37fdee..401e599 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/91e33a95add752894fb532a67b980c109ecd28e8/labs/12-product-engineering-loop). +Boatstack is a generated content distribution. Propose changes to workflow semantics, templates, evidence rules, or generated presentation in [Intelligence Flow](https://github.com/operatorstack/intelligence-flow/tree/dc7d340e0028db5df232d75b4c69ba866aa133f9/labs/12-product-engineering-loop). The Boatstack repository receives product/runtime changes through a generated pull request. Review the PR's `UPSTREAM.json`, tests, adapter diff, and context-size change; do not hand-edit generated output on `main`. `.github/workflows` is the exception: it is Boatstack's executable control plane, excluded from scheduled projection and changed only through a separate manually reviewed Boatstack PR. diff --git a/UPSTREAM.json b/UPSTREAM.json index 533f311..26b432c 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -1,7 +1,7 @@ { "canonical_context": { - "characters": 60587, - "estimated_tokens": 15147, + "characters": 60577, + "estimated_tokens": 15145, "estimator": "ceil(total characters / 4); compactness signal, not provider billing", "files": [ "product-engineering-loop/references/workflow.md", @@ -12,12 +12,12 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "f9ed388745c12187dcc3e39fc4821ec5094085182157005cae561df38a3ab2c8", + "CONTRIBUTING.md": "e73050efeefb479d4e2694977de316dd0d6068bcf71ab61e69aaa1514c376cb3", "README.md": "125b47671a68556df382f19756fb61fa18925606cbbaf54d6bc9df8872b36870", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", "assets/boatstack-portability.svg": "66dfdfa85db857b3bd18b32047a6975f1fbbfc4dc091158e8277193f9969a346", - "boatstack/SKILL.md": "1fbdab536732aca6425d657acfe0eb2f37743cb542c07e92f8e06ddafdb910ff", + "boatstack/SKILL.md": "7c7b3568d836cb176c92762a8315fa834cd61a531a629c97a5cd98d6f7689be0", "boatstack/agents/gemini.yaml": "cbf43b387399e456fa6178f86d83e6e35567e6142ff800f8de6ffca306fa963e", "boatstack/agents/openai.yaml": "68a30a60859556c5a26e16d184594ca243a6043d99c8cf7d66b5dd6d50a93cd1", "boatstack/assets/templates/adr.md": "c577a3c1c1319061f61deb053597e6e853657022185fe28b8f733327e2a78565", @@ -34,7 +34,7 @@ "boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f", "boatstack/changelog.go": "c5e1f31440b44d61e6037ad27af0333540af3545d655e35819a0241cbbebd8ec", "boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7", - "boatstack/cmd/boatstack-helper/main.go": "718803eb4e0c1ca1bb0ca2923f64cf055d35dab6e3ad7f6a9f62b0cec724b610", + "boatstack/cmd/boatstack-helper/main.go": "43f070dc71f924ea6466b4190c43811d7884cece7d9d44df433c9beeb6982053", "boatstack/cmd/boatstack-helper/main_test.go": "ff73003b6a5157202fa09ddf1129fb13c3d79702b2e05a8721ce5a11bf5ab779", "boatstack/command.go": "94d2117c6e390d5a644afc5cd90f7e712e3f8b1032f8c9e3b253cc134524c28a", "boatstack/command_test.go": "9f707abba3640add81c3e97ba7e72fedbf98f3394b1c060a9ca4b4a28e919968", @@ -44,13 +44,13 @@ "boatstack/delivery.go": "ea53af0e702ec3668a563a5f786dcac2e095362285ca6093b7ed71ec495a0a48", "boatstack/delivery_test.go": "564ad2029a8412de7953967b1acdf377e6c87b6f6e3465d1f8741a4813f2949f", "boatstack/evidence.go": "497a31e6ff632cb1d7c3adfc9f269af3f6aa84e948dd5d417c162767542a27df", - "boatstack/export.go": "1cb10bd4efc0906c22220a139da1b10ff7729881501b17ac369dd9dfedac97eb", - "boatstack/export_test.go": "50c372ad5713107a8d5d34edb8d65bb56b21b97e3c63ceddf98e099bd9290d71", + "boatstack/export.go": "abccdfc04b7b49a17e8531c0332f80262e693273f7974b646528fe00af1d6ae6", + "boatstack/export_test.go": "67eb890728d20630925d6e4e90d2a97ec025195ba5c54994c1b098ab72721dca", "boatstack/go.mod": "6086ef1b2a83f5696190dca692c653925f27b61f652f659fd3fca43ed54a1641", "boatstack/go.sum": "26c315c867b11b886f3c9402fce7f341f6a9115a5d61f54afbb5e1b1fb5f6017", "boatstack/hooks.go": "b88cedcd045e5217fedfac625ced2e4f691adb42e62155fcbc392a8f6d88366e", "boatstack/hooks_test.go": "c5786bc6463cf6932a6612b26cbe65d035008ecbca5c0063bea253d857c2f622", - "boatstack/init.go": "302957edf2e663f806d3d5de4486fef5eaf04797270a6f797cfe0170a732f66d", + "boatstack/init.go": "b9b73449e889359d8a4e354e3df18ef4db20e1325b960976f543003563913769", "boatstack/init_test.go": "5fdf687205e7a5984a98a87336b7127e4ae9b651d57e21ec2dc8ca7e653ee602", "boatstack/init_transaction.go": "112456c4e1c4db54c4137bcf4f7a9a9e63399a6f5971e9b3dc952d0c4b2aa4b6", "boatstack/installation_repair.go": "6574f7133a9644843c9260b9b9daede641a14438f7357bae42fb8ec188890446", @@ -58,12 +58,12 @@ "boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c", "boatstack/migrate.go": "eaf589e2b266238068e42c6d78e01dc040266d28e342cb24f09e33e8541749b3", "boatstack/migrate_test.go": "9f4bda2fb158c5e54bcc0242dace1da3c1965f9846a213c573956a35b7d1724e", - "boatstack/next.go": "b7852451069764a486fb01b297b83c7882622e62505abf622559a8edcd49490c", - "boatstack/next_test.go": "6d5acd01311a1c357580040d5a14804cc3bb12706f5c997b66f5e1198271a9d6", + "boatstack/next.go": "86ec196ac966c13b92b6302ded846d99a4e0e37d9d584bd80490e4e1047249e5", + "boatstack/next_test.go": "24d8c86da4b320af085117117b8032fa59d0f78896393cf1c8b0c2194a430090", "boatstack/operation.go": "62f97bf2091f33eb2ca91915bf08bee73d53387611b673e849355bfd516ca467", "boatstack/operation_test.go": "2d624eaba342b2c81b45cdf50918a65a9c002b5376a02041b24180658ee6a25a", - "boatstack/plan.go": "8189ee42902bce62dcd39ce7a2e423cecb4c9e0d1dd34a5497e5a732a45c851f", - "boatstack/plan_test.go": "ea96bf0047a43e8224f02e0b8761978ce48636a83a63b6296c9bd3336f51250c", + "boatstack/plan.go": "6854d744a14e60eb285216ddc90c41085d6dde08ca5cae497e6863a703123b2f", + "boatstack/plan_test.go": "be866076d6932668cefab3ceb6d888394c4d989d03edb28b48221e43f860b630", "boatstack/plan_validation.go": "412f06750832fe46f01190ea5e475fc6f6ea59c8ba78131f94ec031053a405d2", "boatstack/plan_validation_test.go": "6cbde4ac719baef6b73aa569515d6a9daadcbf14b33f76fa78159826954e20fa", "boatstack/planning.go": "ef4507a9fecc900f0691372c50883f328c9232c3dfd6986fbde26fb7ef436ae3", @@ -78,7 +78,7 @@ "boatstack/references/host-hook-contracts.md": "d68ae1556e7b1e29e9ac7cb4db767809d510aabf0be52e60e44665ea7abb980e", "boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441", "boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae", - "boatstack/references/workflow.md": "5d9959ea1fdabec568472d08c14d47657ece6cdf108c231f1397baba200861b1", + "boatstack/references/workflow.md": "87692bdf6b6830bcb68ed052e2b88a8f21d755c0b6be5ef03787fb4669d572fd", "boatstack/release.go": "82dcb4ca59e8c79a68d5333d650f90e64abd448d04e0c6f504fdf07f42b5ed76", "boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690", "boatstack/run.go": "74967ad5b3ed3847baffec1231aae69a81a70f9cce3a9412fa05bcfdc4eca6d1", @@ -86,8 +86,8 @@ "boatstack/runtime.go": "007f38f0631200b448f27f79b8cefb9874dd767b500dc389f2c9a663d0d0f9b0", "boatstack/runtime_cache.go": "60c4eb0c7dde91d40d6ef3f05adc1a1282d17ff1ca12470d0a008454f7ca7489", "boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85", - "boatstack/safety.go": "994baf314fe1fda41a70cf7b0696a8ce8c7bfc666c39fe773ec7c21c1c8b8afe", - "boatstack/safety_test.go": "60d3248a6223859cdfbe2bb3ad7ecd7b9e840755236d25c85c31be78ab4bef5c", + "boatstack/safety.go": "e5d91bf219838f5e0c80daaf5b18a2cca9661f211fa60e2c1fab9e5835082b36", + "boatstack/safety_test.go": "01f28bc3bfcb6bdd47b307e309e36bbc1921b6426ad0b777d81fe4131200c37e", "boatstack/skill_frontmatter.go": "73364df463ce828c2d005aab55f72bb92f7a34d99cf3f53d4e0cd5a4da9dbd0e", "boatstack/skill_frontmatter_test.go": "a3ec52e7df357a72265c95dd66db15d9c0effc7e5f90f14ce69c27792ce394eb", "boatstack/testdata/reviewer-pr-body.md": "4c64e3788e5d61a377aeb0f797f7fc8d2316ab6e49572d15636eea7ba9e34ac4", @@ -107,14 +107,14 @@ "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", "docs/configuration.md": "4d8f207b415a5a1e3b9b1698ee7bb1221aa0e5496a061bb8054294df2f347ad1", - "docs/evidence-engineered-coding.md": "2f9d42da150a4552e59af30aaa83b3ff1dd7aadc5e2ba4135296aae2a4fab202", + "docs/evidence-engineered-coding.md": "1104a171aa4dcfcf881bc59adcc0018a90fa94868f08f5a596129aa4f4b8e344", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", - "docs/getting-started.md": "d5f0b170209e61518810a23b851bf9eb50b6755906703ca313e6e9faab20e1cd", - "docs/public-claims.json": "35181ffc37a69bec15347a024a6e49a0eacb03efcfc1c06dd17a477bfb90dca9", + "docs/getting-started.md": "f314270c5ed1a55bbef5f3ddbcb5596693dbee9374e5f0d3df8838cefbd68052", + "docs/public-claims.json": "baa8dbebf79506bc4071bbdeb122148055a634085b56eea138886cba66c38cae", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", - "docs/research-and-design.md": "d65c66e323037bda5d45aacef5d48afa6bf93da55901378891d235aca3a5684f", + "docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", - "docs/troubleshooting.md": "76f8cf2558345bbb873b19977fe27b455ef9f5f3e70e0386951fc462138706fa", + "docs/troubleshooting.md": "321d79a990de3eac0931efbf5446a3fbdb5f8fce06319bf2adf1e7aabf763519", "docs/validation-and-evidence.md": "e7d91ad49c6adb44784ebe7d94feceb6abd445857f9a0716f0758bf6b55296c5", "docs/why-these-steps.md": "cbe0d769db11ef15bb1dff888009378d6783776ad020e6f5139847a1dd62fa09", "install.ps1": "6f5857ec0feb502683c5781b9bfbbe31ed39556cd13384ddc66da622a8423cb7", @@ -124,7 +124,7 @@ "labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d", "labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71", "labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39", - "labs/diagram-json/plan.lock.json": "9828551b5812c00108df19faf9b6faabc2e9efea6c2af10b83f1139e28637f9e", + "labs/diagram-json/plan.lock.json": "f7d44717599db90f46da9556ca4d7195fa43c65db28fa532c9e7f5b6ba738589", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -184,13 +184,14 @@ "release-notes/2026-07-22-value-translation-boundary.md": "9cf168ff7caaf3906b78533935bdfb2c86e753984ed1e5ec8204cb373d083390", "release-notes/2026-07-23-bootstrap-safe-update-repair.md": "d8e66c46ae05e3d228dfea45879f4d1166d5e3a253ac24bababd4c3e396c214b", "release-notes/2026-07-23-canonical-update-ownership.md": "7f34f890b252493797519389b23f1b56ec7ec16db7547aac8ea196f75f3b8c2c", + "release-notes/2026-07-23-explicit-source-plan.md": "ec1f97434f9263f6db4bc3b83ae83213053cd2bc6b7465e4fb2d67cbea5cf731", "release-notes/2026-07-23-ignore-ambiguous-deliveries.md": "9b1b9fd48db340b91fcced1c282297de0ecad8744723f2b1fdd94033927a88c4", "release-notes/2026-07-23-recoverable-repository-sync.md": "3afc4f6220ae76df3bd6dcd15fc180135274c808729e9c512a2c53462ec690c2" }, "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "91e33a95add752894fb532a67b980c109ecd28e8", + "commit": "dc7d340e0028db5df232d75b4c69ba866aa133f9", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/SKILL.md b/boatstack/SKILL.md index 3b6de1f..d41b3ca 100644 --- a/boatstack/SKILL.md +++ b/boatstack/SKILL.md @@ -29,11 +29,11 @@ For the full state machine, read [workflow.md](references/workflow.md). For arti ## Report what is next -Run the project-local helper's read-only `next-status --repo . --json` inspection. Repository artifacts, managed delivery state, gate receipts, and the recorded PR identity are evidence; conversation, terminal, worktree, and process observations are context only. Never run the returned operation automatically. `NOT_STARTED` and `SOURCE_PLAN_READY` point to `auto-plan`; `PUBLISHED` means a PR exists but is not a verified merge; only `FEATURE_COMPLETE` requires no action. If state is ambiguous, stale, or invalid, name the blocker instead of choosing by recency or clearing artifacts. When an `AMBIGUOUS` block names only past deliveries the user no longer cares about, name the ignorable delivery slug(s) and offer to exclude them from ambiguity resolution; only after explicit user confirmation, add each slug with `.product-loop/bin/boatstack-helper ignore-delivery --repo . --feature ` (a bounded, provenance-safe write to `workflow.ignored_deliveries` — never hand-edit config or delivery state). Any new, unlisted ambiguous delivery still pauses the workflow. +Run the project-local helper's read-only `next-status --repo . --json` inspection. Repository artifacts, managed delivery state, gate receipts, and the recorded PR identity are evidence; conversation, terminal, worktree, and process observations are context only. Never run the returned operation automatically. `NOT_STARTED` points to `auto-plan` (run it with the plan path via `--plan`); `PUBLISHED` means a PR exists but is not a verified merge; only `FEATURE_COMPLETE` requires no action. If state is ambiguous, stale, or invalid, name the blocker instead of choosing by recency or clearing artifacts. When an `AMBIGUOUS` block names only past deliveries the user no longer cares about, name the ignorable delivery slug(s) and offer to exclude them from ambiguity resolution; only after explicit user confirmation, add each slug with `.product-loop/bin/boatstack-helper ignore-delivery --repo . --feature ` (a bounded, provenance-safe write to `workflow.ignored_deliveries` — never hand-edit config or delivery state). Any new, unlisted ambiguous delivery still pauses the workflow. ## Run through ship -For `$boatstack run`, `/boatstack-run`, or natural language such as “run Boatstack through ship,” first run the read-only `next-status --repo . --json` and `operation-status --repo . --json`. Wait for an executing operation and reconcile unknown completion before retrying. When one saved source plan is ready, enter `auto-plan`; when no source plan exists, stop and ask the user to save the host Plan-mode file. Return **Feature complete** only for a verified completed feature, and stop on unverified, ambiguous, stale, or invalid state. Before the first delivery-stage operation (`build`, `repair`, `test-gate`, `review-gate`, or `ship-gate`), run `run-preflight --repo . --json`. Planning and approval do not require a remote fetch. The preflight fetches `origin` and verifies the current named branch contains the fetched delivery base and is not behind or diverged from its upstream. A failed fetch, missing remote/base, stale base, upstream drift, or constrained branch mismatch blocks before delivery mutation. Never repair freshness by merging, rebasing, switching or creating a constrained delivery branch, discarding changes, force-pushing, or broadening permissions. +For `$boatstack run`, `/boatstack-run`, or natural language such as “run Boatstack through ship,” first run the read-only `next-status --repo . --json` and `operation-status --repo . --json`. Wait for an executing operation and reconcile unknown completion before retrying. When the host supplies the plan path, enter `auto-plan` with `--plan `; when no plan path is supplied, stop and ask the user for the plan to build. Return **Feature complete** only for a verified completed feature, and stop on unverified, ambiguous, stale, or invalid state. Before the first delivery-stage operation (`build`, `repair`, `test-gate`, `review-gate`, or `ship-gate`), run `run-preflight --repo . --json`. Planning and approval do not require a remote fetch. The preflight fetches `origin` and verifies the current named branch contains the fetched delivery base and is not behind or diverged from its upstream. A failed fetch, missing remote/base, stale base, upstream drift, or constrained branch mismatch blocks before delivery mutation. Never repair freshness by merging, rebasing, switching or creating a constrained delivery branch, discarding changes, force-pushing, or broadening permissions. After preflight, repeatedly run `next-status --repo . --json`, execute only its verified next operation using the canonical semantics below, verify the resulting repository state, and resolve again. Continue across all declared slices. Pause for explicit `a` plan approval, material product questions, and the exact `o` or `u` PR confirmation; a valid answer resumes the foreground run in the current host session. The run invocation itself is never approval or publication authority. Same-intent test/review failures may be recorded and repaired for at most three complete repair-and-gate cycles per active slice; the durable delivery attempt count does not reset across turns or hosts. Stop on amendments, ambiguity, safety failures, stale evidence, unsupported recovery, branch mismatch, or an exhausted budget. Persist execution facts and retry identity, never autonomous workflow intent; conversation is not workflow evidence. Completion means every slice PR is published for review, never merged or deployed. @@ -61,7 +61,7 @@ For ordinary feature work, define one bounded outcome: Because this workflow is also a reusable product, maintain delivery and improvement as separate paths: -- **Delivery path:** intent -> host Plan mode -> saved source plan -> questions -> spec -> approved plan -> code -> gates -> PR. +- **Delivery path:** intent -> host Plan mode -> plan passed to auto-plan via `--plan` -> questions -> spec -> approved plan -> code -> gates -> PR. - **Improvement path:** traces -> failure classification -> proposed move -> paired evaluation -> promote/reject. Never mix benchmark observations or speculative harness changes into the delivery path during an active feature. The improvement path may propose an experiment; only a passed promotion gate changes the canonical loop. @@ -95,7 +95,7 @@ Before starting `/auto-plan` for a new feature, check `next-status --repo . --js ## Run `auto-plan` -0. Require exactly one saved plan file created in the active host's Plan mode. First use the active plan path exposed in host/system conversation context, when available, and validate it with `.product-loop/bin/boatstack-helper check-source-plan --repo . --plan `. Otherwise run `check-source-plan --repo .` to search only `.product-loop/intake/` and bounded repo-local host plan directories. If the result is missing or ambiguous, return `BLOCKED`; never choose by recency alone. An explicit `/auto-plan ` is only the ambiguity fallback. Do not write the missing source plan inside `auto-plan`. +0. Require the plan file produced in the active host's Plan mode, passed explicitly. Validate it with `.product-loop/bin/boatstack-helper check-source-plan --repo . --plan `. Boatstack never scans directories for plans, so `--plan` is required and no unshipped saved plan becomes ambient context. If no plan path is supplied or the file is missing, empty, or unreadable, return `BLOCKED`; do not write or guess the missing source plan inside `auto-plan`. Because its hash is re-checked through `build`, point `--plan` at a durable in-repo path that stays present and unchanged; a path outside the repository is rejected. 1. Treat the supplied plan as an initial proposal, not approved truth. Record its path as `source_plan_path` in the structured plan. 2. Write the bounded outcome definition before proposing architecture. 3. Separate facts, decisions, unknowns, and safely deferrable gaps. diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go index 074de4c..556293b 100644 --- a/boatstack/cmd/boatstack-helper/main.go +++ b/boatstack/cmd/boatstack-helper/main.go @@ -313,8 +313,8 @@ func checkPlanCommand(arguments []string) int { func checkSourcePlanCommand(arguments []string) int { flags := flag.NewFlagSet("check-source-plan", flag.ContinueOnError) - repo := flags.String("repo", ".", "repository whose bounded plan locations should be searched") - plan := flags.String("plan", "", "optional explicit plan file created by the host Plan mode") + repo := flags.String("repo", ".", "repository the source plan is validated against") + plan := flags.String("plan", "", "required in-repo path to the plan produced in the host conversation") if err := flags.Parse(arguments); err != nil { return 2 } diff --git a/boatstack/export.go b/boatstack/export.go index 4e6efcb..00d10a5 100644 --- a/boatstack/export.go +++ b/boatstack/export.go @@ -167,7 +167,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. 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.` + preflight = `Before reading repository context or drafting artifacts, identify the path of the plan produced in the active host/system conversation — the user supplies it as the invocation argument, ` + "`/auto-plan `" + ` — and run the project-local helper with ` + "`check-source-plan --repo . --plan `" + `. Use its ` + "`SOURCE_PLAN`" + ` result. Boatstack does not scan directories for plans: ` + "`--plan`" + ` is required, so no unshipped saved plan becomes ambient context. If no plan path is available, stop and ask the user for the plan to build; do not create or guess a substitute inside auto-plan. The plan file must remain present and unchanged through build, so point ` + "`--plan`" + ` at a durable in-repo path, not an ephemeral scratch file; a path outside the repository is rejected because it cannot stay committed and hash-current through build. 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 @@ -217,7 +217,6 @@ func BuildExportBundle(configPath string, config ProjectConfig, rawConfig []byte } files[".product-loop/project.json"] = projectJSON files[".product-loop/.gitignore"] = []byte("bin/\nworktrees/\n") - files[".product-loop/intake/.gitkeep"] = []byte{} files[".product-loop/hooks/guard.sh"] = guardShellScript() files[".product-loop/hooks/guard.ps1"] = guardPowerShellScript() for _, host := range []string{"cursor", "claude", "codex", "gemini"} { @@ -267,9 +266,9 @@ func BuildExportBundle(configPath string, config ProjectConfig, rawConfig []byte } operations := map[string]string{ - "boatstack-next": "Run the project-local helper next-status --repo . --json. This operation is strictly read-only: do not run the reported operation, edit artifacts, contact GitHub beyond the helper's bounded published-PR inspection, or advance a gate. Translate the structured result into the canonical response contract. Show the verified feature and active slice when present. Distinguish NOT_STARTED and SOURCE_PLAN_READY, whose next operation is auto-plan, from PUBLISHED, which responds PR published and makes reviewing its checks the one action, and FEATURE_COMPLETE, which is reserved for a verified merged PR and responds Feature complete with No action required. If verification_status is BLOCKED, name the ambiguity or invalid evidence and make its safe restoration the one action; never clear artifacts. Conversation, terminal, worktree, or process observations may be included as clearly labeled context only and must never override the repository-backed result. Otherwise make the returned next_operation the one next action.", - "boatstack-run": "First run the read-only next-status --repo . --json and operation-status --repo . --json. If an operation is executing, wait and report it instead of launching it again; if reconciliation is required, verify its exact postcondition before retrying. If SOURCE_PLAN_READY, execute auto-plan without Git preflight and pause at its normal decision or approval boundary. If NOT_STARTED, respond Start a Boatstack feature and ask the user to save exactly one host Plan-mode file, then run /auto-plan; do not fetch or require a feature branch. If PUBLISHED, report that the PR is awaiting or lacks verified completion and make reviewing its checks the one next action; do not claim completion. If FEATURE_COMPLETE, respond Feature complete with No action required. Stop on UNVERIFIED, BLOCKED, ambiguous, stale, or invalid state. Before executing the first delivery-stage next_operation (build, repair, test-gate, review-gate, or ship-gate), run the project-local helper run-preflight --repo . --json; planning and plan-gate do not require it. Stop on a blocked preflight; never merge, rebase, force-push, discard changes, switch branches, or create a constrained delivery branch to repair freshness. Then execute exactly the verified next_operation using the canonical operation semantics, verify the resulting repository state, and resolve again. Continue across every declared delivery slice. Pause for the exact plan approval reply a, any material product decision, and the exact PR publication reply o or u; after a valid reply in the current host session, automatically continue the run. A run request never supplies approval or publication authority. For a same-intent test or review failure, use repair, record the observation, and retry from the returned stage. The delivery state's durable repair_attempt is the budget; stop after three complete automated repair-and-gate cycles even across new turns, host restarts, or async notifications. Stop immediately on an amendment, ambiguity, unsafe or destructive capability, stale evidence, branch mismatch, unsupported recovery, or exhausted repair budget. If Cursor reports MainThreadShellExec not initialized, explain that Cursor failed before the Boatstack hook started and make Developer: Reload Window the one recovery action; do not recommend reinstall unless Boatstack reports a missing, drifted, unsafe, or checksum-invalid runtime. Do not use conversation as workflow evidence. Durable operation receipts store execution facts and retry budgets, never autonomous workflow intent. Report the feature, active slice, stages completed, completion or pause reason, durable repair-cycle count, and exactly one next action. Ship means publishing every declared slice PR for review; never merge or deploy.", - "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. If workflow.boundary_analysis is true, evaluate if the change is a symptom of a missing systemic boundary and perform a rapid codebase scan for other vulnerabilities. Present this as a material product decision with tiered paths: [1a] Symptom Patch or [1b] Programmatic Enforcement (Slice 1 for the boundary, Slice 2 for the feature). When workflow.pr_visual_evidence is suggest or require, record a structural pr_visual_evidence decision: relevant with one to three entry/state/viewport/expected scenarios, or not_relevant with a reason. Discover existing visual tooling but never require a frontend framework or add repository tooling during planning. Record affected_paths and structured side_effects for external writes; use an immutable target identity, transactional or fix-forward recovery, and destructive=false. When workflow.maintain_changelog is true, include CHANGELOG.md in every delivery slice's affected paths. Keep internal phases as tasks in one delivery slice. Only when the accepted outcome explicitly needs multiple PRs, declare ordered delivery_slices and assign every task exactly once; plan approval never authorizes publication. Do not implement, create JSON or locks, or imply acceptance. If ready, respond with Plan ready and make Run /plan-gate the one next action. If decisions remain, respond with I need your input and ask only 1-3 material questions.", + "boatstack-next": "Run the project-local helper next-status --repo . --json. This operation is strictly read-only: do not run the reported operation, edit artifacts, contact GitHub beyond the helper's bounded published-PR inspection, or advance a gate. Translate the structured result into the canonical response contract. Show the verified feature and active slice when present. Distinguish NOT_STARTED, whose next operation is auto-plan run with the plan path via --plan, from PUBLISHED, which responds PR published and makes reviewing its checks the one action, and FEATURE_COMPLETE, which is reserved for a verified merged PR and responds Feature complete with No action required. If verification_status is BLOCKED, name the ambiguity or invalid evidence and make its safe restoration the one action; never clear artifacts. Conversation, terminal, worktree, or process observations may be included as clearly labeled context only and must never override the repository-backed result. Otherwise make the returned next_operation the one next action.", + "boatstack-run": "First run the read-only next-status --repo . --json and operation-status --repo . --json. If an operation is executing, wait and report it instead of launching it again; if reconciliation is required, verify its exact postcondition before retrying. If NOT_STARTED, respond Start a Boatstack feature and ask the user for the plan produced in the host conversation, then execute auto-plan with its path via --plan (Boatstack does not scan directories for plans) without Git preflight, pausing at its normal decision or approval boundary; do not fetch or require a feature branch. If PUBLISHED, report that the PR is awaiting or lacks verified completion and make reviewing its checks the one next action; do not claim completion. If FEATURE_COMPLETE, respond Feature complete with No action required. Stop on UNVERIFIED, BLOCKED, ambiguous, stale, or invalid state. Before executing the first delivery-stage next_operation (build, repair, test-gate, review-gate, or ship-gate), run the project-local helper run-preflight --repo . --json; planning and plan-gate do not require it. Stop on a blocked preflight; never merge, rebase, force-push, discard changes, switch branches, or create a constrained delivery branch to repair freshness. Then execute exactly the verified next_operation using the canonical operation semantics, verify the resulting repository state, and resolve again. Continue across every declared delivery slice. Pause for the exact plan approval reply a, any material product decision, and the exact PR publication reply o or u; after a valid reply in the current host session, automatically continue the run. A run request never supplies approval or publication authority. For a same-intent test or review failure, use repair, record the observation, and retry from the returned stage. The delivery state's durable repair_attempt is the budget; stop after three complete automated repair-and-gate cycles even across new turns, host restarts, or async notifications. Stop immediately on an amendment, ambiguity, unsafe or destructive capability, stale evidence, branch mismatch, unsupported recovery, or exhausted repair budget. If Cursor reports MainThreadShellExec not initialized, explain that Cursor failed before the Boatstack hook started and make Developer: Reload Window the one recovery action; do not recommend reinstall unless Boatstack reports a missing, drifted, unsafe, or checksum-invalid runtime. Do not use conversation as workflow evidence. Durable operation receipts store execution facts and retry budgets, never autonomous workflow intent. Report the feature, active slice, stages completed, completion or pause reason, durable repair-cycle count, and exactly one next action. Ship means publishing every declared slice PR for review; never merge or deploy.", + "auto-plan": "Take the plan produced in the host conversation, supplied explicitly via --plan (Boatstack never scans directories for plans), and refine it into a Markdown-only draft feature package whose canonical structured artifact is plan.md. Run check-plan read-only. If workflow.boundary_analysis is true, evaluate if the change is a symptom of a missing systemic boundary and perform a rapid codebase scan for other vulnerabilities. Present this as a material product decision with tiered paths: [1a] Symptom Patch or [1b] Programmatic Enforcement (Slice 1 for the boundary, Slice 2 for the feature). When workflow.pr_visual_evidence is suggest or require, record a structural pr_visual_evidence decision: relevant with one to three entry/state/viewport/expected scenarios, or not_relevant with a reason. Discover existing visual tooling but never require a frontend framework or add repository tooling during planning. Record affected_paths and structured side_effects for external writes; use an immutable target identity, transactional or fix-forward recovery, and destructive=false. When workflow.maintain_changelog is true, include CHANGELOG.md in every delivery slice's affected paths. Keep internal phases as tasks in one delivery slice. Only when the accepted outcome explicitly needs multiple PRs, declare ordered delivery_slices and assign every task exactly once; plan approval never authorizes publication. Do not implement, create JSON or locks, or imply acceptance. If ready, respond with Plan ready and make Run /plan-gate the one next action. If decisions remain, respond with I need your input and ask only 1-3 material questions.", "plan-gate": "Run check-plan read-only and present its plan fingerprint, baseline product diff fingerprint, changed paths, exact baseline diff when non-empty, and all open decisions. If workflow.human_plan_approval is true, require explicit human approval. While plan approval is pending, the normal user action is the exact standalone reply a. Trim surrounding whitespace and match a case-insensitively; do not treat [a] or an a embedded in other text as approval. Continue accepting the full reply approve for compatibility, but do not advertise it in the user-facing response. Resolve approved_by from an explicit supplied identity, otherwise from the authenticated GitHub login when available; ask one short identity follow-up only when neither exists, and never infer it from a filesystem username, commit history, or agent identity. On approval invoke record-approval with the displayed baseline fingerprint, omitting it only when the baseline is clean, so it writes only approval.md. While pending respond Ready for your approval and render: Reply `a` to approve. After recording respond Approved — ready to build. If human_plan_approval is false, do not request approval or create approval.md; state that Build will create a fingerprinted policy-activation lock. In either mode Remain in Plan mode, do not compile, and make entering execution mode and running /build the next action once ready.", "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 internally without activating the plan, compiling JSON, or writing a lock. Only then locate plan.md and, when workflow.human_plan_approval is true, approval.md; run activate-plan before the first product-code edit and omit --approval for policy activation. Stop if it reports BLOCKED. Read delivery-status and implement only the active delivery slice task_ids. When workflow.maintain_changelog is true, add a concise entry grounded in the active slice's actual changes under the current CHANGELOG.md Unreleased heading before recording test evidence. Use only the one allowed category needed by the entry and do not add empty category headings. If the file is absent, create the documented minimal skeleton with ## [Unreleased] - YYYY-MM-DD and the first categorized entry; if it exists, add to the current file without rewriting its history or layout. Run the internal repository safety check after operational or high-risk edits; a destructive capability blocks execution and gate progression but does not block reviewable source editing. Implementation tactics remain open inside the authorized boundary, but push and PR mutation are never build tactics and are denied while managed delivery is active. On success respond Build complete and make Run /test-gate the one next action. When a new product decision blocks work, respond Build needs a decision and ask only that question.", "repair": "First run recovery-status --repo . with the user's exact free-form requested change, its observed source stage, bounded evidence when available, and --json. This resolver covers both active and current-branch published deliveries. On repair_active, read delivery-status, the current plan lock and acceptance criteria, the actual diff, and current receipts; classify the request and invoke record-change before any product edit. On draft_corrective_child, invoke record-change on the published parent, preserve its lock, receipts, slices, and publication evidence, and automatically prepare the suggested one-slice child plan with parent_delivery, exact correction, inherited intent, observed failure, returned existing_diff_sha256 and existing_changed_paths, verification requirements, and the resolved PR destination. Lead with The PR needs a corrective delivery. I prepared it for your approval. Then pause at the normal fingerprinted plan approval boundary; never reuse the parent's approval. An open PR reuses its verified head branch and is updated after fresh gates and publication confirmation. A merged or closed PR uses a fresh branch and PR; when a fingerprinted correction diff already exists, leave the original worktree untouched and transfer that exact reviewed diff into the fresh child only after approval. PUBLISHED_UNKNOWN may be drafted but its destination remains blocking at publication. Stop on BLOCKED and ask one targeted feature question using the returned blockers. If no managed target exists, continue ordinary conversation. Never discard pre-existing correction edits, edit runtime state directly, or bypass test, review, and ship gates. Never ask the user to repeat a denied push or PR mutation. If Cursor reports MainThreadShellExec not initialized, make Developer: Reload Window the one recovery action because Boatstack's hook did not start; reserve reinstall guidance for Boatstack runtime integrity errors.", @@ -293,7 +292,7 @@ alwaysApply: true 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. +Ordinary product intent starts in the host's Plan mode. Pass the resulting plan to auto-plan explicitly with --plan ; Boatstack never scans directories for plans, and must not invent a substitute. Keep that plan file present and current through build. Do not start build work until the plan gate is ready and build activation has produced a valid plan lock. Require approval.md only when workflow.human_plan_approval is true; otherwise the lock must record policy activation. Before modifying product code, resolve complete Boatstack state. A saved feature plan latches managed authority: draft, approved, policy-ready, ambiguous, stale, or invalid pre-activation state cannot mutate product files, and only exact planning or activation transitions remain available. Once a current plan lock exists, preserve active managed delivery and published-delivery recovery behavior. Async task completion and conversation state never grant authority. When active or published work receives a product behavior, implementation, test, review, delivery-evidence, CI, or publication problem or modification, route through the Boatstack repair operation before editing. Never ask the user to manually repeat a denied push or PR mutation. If no saved plan or managed delivery exists, continue ordinary conversation. %s @@ -326,7 +325,7 @@ description: Use when the user asks what is next in Boatstack, asks Boatstack to Follow the User-facing response contract in .product-loop/workflow.md for every operation. Lead with the mapped plain-language outcome, show only decision-relevant content, end with exactly one Next step, and move machine statuses, helper output, fingerprints, artifact paths, receipts, and locks into collapsed Technical details. Internal helper names must not appear in the primary response. -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 only when human approval is enabled. 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 a human or policy authorization 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 authorization lock, diff, and evidence after build. +Ordinary product intent must first be explored in the host's Plan mode and saved as a file. The host passes that plan to auto-plan explicitly with --plan , which auto-plan validates and records as source_plan_path. Boatstack never scans directories for plans, so --plan is required; if no plan path is supplied, auto-plan is BLOCKED and must not guess or create a substitute. The plan must live inside the repository so it stays committed and hash-current through build; an out-of-repo path is rejected. Auto-plan and plan-gate write Markdown only: plan.md remains canonical, and approval.md records explicit acceptance only when human approval is enabled. 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 a human or policy authorization 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 authorization lock, diff, and evidence after build. Internal phases are ordinary tasks inside one delivery slice. Multiple PRs require explicit ordered delivery_slices with every task assigned exactly once. After activation, read delivery-status and work only on the active slice. Test-gate and review-gate must record slice-scoped receipts bound to the current branches, commit, diff, and evidence. Direct push, PR mutation, and ad-hoc PR routing are denied while managed delivery is active. Successful confirmed publication advances exactly one slice; plan approval never authorizes later slices. diff --git a/boatstack/export_test.go b/boatstack/export_test.go index cac58fb..78bf4af 100644 --- a/boatstack/export_test.go +++ b/boatstack/export_test.go @@ -243,6 +243,20 @@ func TestExportAndDriftCheck(t *testing.T) { 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") } + // Conformance: no ambient plan context. The exported auto-plan adapter must + // require an explicit --plan and must never reference the removed intake + // staging directory, and the bundle must not scaffold it. + if !strings.Contains(autoPlan, "--plan") { + t.Fatal("auto-plan adapter must instruct passing the plan explicitly with --plan") + } + if strings.Contains(autoPlan, ".product-loop/intake") { + t.Fatal("auto-plan adapter must not reference the removed .product-loop/intake staging directory") + } + for path := range bundle.Files { + if strings.HasPrefix(path, ".product-loop/intake") { + t.Fatalf("export bundle must not scaffold intake staging, found %q", path) + } + } for _, expected := range []string{"compact keys such as 1a/1b", "exactly one choice per question with (Recommended)", "offer r to accept all displayed recommendations", "echo the selected mapping"} { if !strings.Contains(autoPlan, expected) { t.Fatalf("auto-plan adapter is missing finite-question shortcut rule %q", expected) @@ -336,11 +350,14 @@ func TestExportAndDriftCheck(t *testing.T) { } } runCommand := string(bundle.Files[".cursor/commands/boatstack-run.md"]) - for _, expected := range []string{"SOURCE_PLAN_READY", "NOT_STARTED", "auto-plan", "planning and plan-gate do not require", "MainThreadShellExec not initialized", "Developer: Reload Window"} { + for _, expected := range []string{"NOT_STARTED", "auto-plan", "planning and plan-gate do not require", "MainThreadShellExec not initialized", "Developer: Reload Window"} { if !strings.Contains(runCommand, expected) { t.Fatalf("run adapter is missing startup recovery rule %q", expected) } } + if strings.Contains(runCommand, "SOURCE_PLAN_READY") { + t.Fatal("run adapter must no longer emit the retired SOURCE_PLAN_READY stage") + } for _, path := range []string{".claude/skills/boatstack/SKILL.md", ".gemini/skills/boatstack/SKILL.md", ".agents/skills/boatstack/SKILL.md"} { router := string(bundle.Files[path]) if !strings.Contains(router, "automatically use repair") || !strings.Contains(router, "current-branch published managed delivery") || !strings.Contains(router, "Never instruct the user to manually repeat") { diff --git a/boatstack/init.go b/boatstack/init.go index 0435ce9..26cb4b6 100644 --- a/boatstack/init.go +++ b/boatstack/init.go @@ -685,7 +685,7 @@ func RunInit(options InitOptions) (returnErr error) { fmt.Fprintln(options.Output, "\nAfter the update PR is merged, reload Cursor, Codex, or Claude.") } else { 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, " 1. Describe the product change, save the host plan as a durable file inside the repository, and pass its path to auto-plan with --plan .") } fmt.Fprintln(options.Output, "Host activation checklist:") fmt.Fprintln(options.Output, " Cursor: reload the window and confirm beforeShellExecution and beforeMCPExecution are paired with their after events, plus synchronous pre/post native-tool hooks; the hooks are defense in depth.") @@ -710,7 +710,7 @@ func RunInit(options InitOptions) (returnErr error) { const ExecutionBoundaryDX = ` **Boatstack Execution Boundary:** When the user approves a plan within your native Plan Mode, **do not immediately transition to Auto-Edit or begin mutating product files.** Because this repository is managed by Boatstack, execution must pass through verifiable gates. Instead of executing the code: -1. Save your proposed plan to ` + "`.product-loop/intake/source-plan.md`" + `. +1. Save your proposed plan as a durable file inside the repository and pass its path to auto-plan with ` + "`--plan `" + ` (Boatstack does not scan directories for plans; an out-of-repo path is rejected so the plan stays hash-current through build). 2. Before auto-plan succeeds, the user may still choose an unmanaged workflow. Once auto-plan creates a saved feature plan, do not offer direct product editing: resolve Boatstack state and continue through plan-gate, approval when configured, and build activation. 3. Async task completion, conversation state, or an execution-mode transition never creates implementation authority. Only a current plan lock does. ` diff --git a/boatstack/next.go b/boatstack/next.go index 52df601..0d26708 100644 --- a/boatstack/next.go +++ b/boatstack/next.go @@ -65,49 +65,6 @@ func featurePlanCandidates(repo string) ([]string, error) { return features, nil } -func unclaimedSourcePlanCandidates(repo string) ([]string, error) { - candidates, err := sourcePlanCandidates(repo) - if err != nil { - return nil, err - } - claimed := map[string]bool{} - root := filepath.Join(repo, ".product-loop", "features") - entries, readErr := os.ReadDir(root) - if readErr != nil && !os.IsNotExist(readErr) { - return nil, readErr - } - for _, entry := range entries { - if !entry.IsDir() || !featureSlugPattern.MatchString(entry.Name()) { - continue - } - planPath := filepath.Join(root, entry.Name(), "plan.md") - sourcePath, sourceErr := SourcePlanForStructuredPlan(planPath) - if sourceErr != nil { - continue - } - absolute, absoluteErr := filepath.Abs(sourcePath) - if absoluteErr != nil { - return nil, absoluteErr - } - claimed[filepath.Clean(absolute)] = true - } - unclaimed := []string{} - for _, candidate := range candidates { - absolute := candidate - if !filepath.IsAbs(absolute) { - absolute = filepath.Join(repo, filepath.FromSlash(candidate)) - } - absolute, err = filepath.Abs(absolute) - if err != nil { - return nil, err - } - if !claimed[filepath.Clean(absolute)] { - unclaimed = append(unclaimed, candidate) - } - } - return unclaimed, nil -} - func orphanedFeatureArtifacts(repo string) ([]string, error) { root := filepath.Join(repo, ".product-loop", "features") entries, err := os.ReadDir(root) @@ -289,26 +246,6 @@ func ResolveNext(repoPath, explicitFeature string) (NextStatus, error) { return blockedNextStatus("INVALID_STATE", "repair-state", "Boatstack found a PR preview without the plan lock required to verify it. Preserve the artifacts and restore the feature evidence before continuing.", orphans...), nil } - sourcePlans, sourceErr := unclaimedSourcePlanCandidates(repo) - if sourceErr != nil { - return NextStatus{}, sourceErr - } - if len(sourcePlans) == 1 { - base.VerificationStatus = "VERIFIED" - base.ObservedStage = "SOURCE_PLAN_READY" - base.NextOperation = "auto-plan" - base.Reason = fmt.Sprintf("Saved Plan-mode file %q is ready to become a Boatstack feature.", sourcePlans[0]) - return base, nil - } - if len(sourcePlans) > 1 { - base.VerificationStatus = "BLOCKED" - base.ObservedStage = "AMBIGUOUS" - base.NextOperation = "resolve-ambiguity" - base.Reason = "Multiple unclaimed Plan-mode files are available; Boatstack will not choose by recency." - base.BlockingAmbiguity = sourcePlans - return base, nil - } - candidates, err := featurePlanCandidates(repo) if err != nil { return NextStatus{}, err @@ -394,7 +331,7 @@ func ResolveNext(repoPath, explicitFeature string) (NextStatus, error) { base.VerificationStatus = "VERIFIED" base.ObservedStage = "NOT_STARTED" base.NextOperation = "auto-plan" - base.Reason = "No Boatstack feature has started and no saved Plan-mode file is available." + base.Reason = "No Boatstack feature has started; run auto-plan with the plan produced in the host conversation (--plan )." return base, nil } diff --git a/boatstack/next_test.go b/boatstack/next_test.go index aeecf41..877de49 100644 --- a/boatstack/next_test.go +++ b/boatstack/next_test.go @@ -62,17 +62,6 @@ func writeSavedFeaturePlan(t *testing.T, repo, feature string) { } } -func writeIntakePlan(t *testing.T, repo, name string) { - t.Helper() - directory := filepath.Join(repo, ".product-loop", "intake") - if err := os.MkdirAll(directory, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(directory, name), []byte("# Source plan\n"), 0o644); err != nil { - t.Fatal(err) - } -} - func TestResolveNextReportsNotStartedWhenNoFeatureExists(t *testing.T) { repo := nextTestRepo(t) status, err := ResolveNext(repo, "") @@ -84,65 +73,54 @@ func TestResolveNextReportsNotStartedWhenNoFeatureExists(t *testing.T) { } } -func TestResolveNextReportsSavedSourcePlan(t *testing.T) { +// TestResolveNextIgnoresAmbientPlanFiles is the conformance guard for the +// "no Boatstack context for things we did not ship" contract at the state +// machine: saved, never-shipped plan files sitting in the historically scanned +// directories must never surface as SOURCE_PLAN_READY or AMBIGUOUS. next-status +// reports NOT_STARTED regardless. +func TestResolveNextIgnoresAmbientPlanFiles(t *testing.T) { repo := nextTestRepo(t) - writeIntakePlan(t, repo, "feature.md") - status, err := ResolveNext(repo, "") - if err != nil { - t.Fatal(err) - } - if status.ObservedStage != "SOURCE_PLAN_READY" || status.NextOperation != "auto-plan" { - t.Fatalf("unexpected status: %+v", status) + for _, dir := range []string{ + ".product-loop/intake", + ".cursor/plans", + ".claude/plans", + ".codex/plans", + } { + absolute := filepath.Join(repo, filepath.FromSlash(dir)) + if err := os.MkdirAll(absolute, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(absolute, "unshipped.md"), []byte("# Unshipped plan\n"), 0o644); err != nil { + t.Fatal(err) + } } -} - -func TestResolveNextPrefersUniqueSourcePlanOverHistoricalPlans(t *testing.T) { - repo := nextTestRepo(t) - writeSavedFeaturePlan(t, repo, "historical-one") - writeSavedFeaturePlan(t, repo, "historical-two") - writeIntakePlan(t, repo, "current.md") status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } - if status.VerificationStatus != "VERIFIED" || status.ObservedStage != "SOURCE_PLAN_READY" || status.NextOperation != "auto-plan" { - t.Fatalf("new source plan did not outrank historical plans: %+v", status) + if status.ObservedStage != "NOT_STARTED" || status.NextOperation != "auto-plan" { + t.Fatalf("ambient plan files leaked into next-status: %+v", status) } -} - -func TestResolveNextBlocksMultipleSourcePlansBeforeHistoricalPlans(t *testing.T) { - repo := nextTestRepo(t) - writeSavedFeaturePlan(t, repo, "historical-one") - writeSavedFeaturePlan(t, repo, "historical-two") - writeIntakePlan(t, repo, "first.md") - writeIntakePlan(t, repo, "second.md") - - status, err := ResolveNext(repo, "") - if err != nil { - t.Fatal(err) - } - want := []string{".product-loop/intake/first.md", ".product-loop/intake/second.md"} - if status.VerificationStatus != "BLOCKED" || status.ObservedStage != "AMBIGUOUS" || !reflect.DeepEqual(status.BlockingAmbiguity, want) { - t.Fatalf("unexpected source-plan ambiguity: %+v", status) + if status.ObservedStage == "SOURCE_PLAN_READY" { + t.Fatal("SOURCE_PLAN_READY must no longer be produced") } } -func TestResolveNextActiveDeliveryOutranksSourcePlan(t *testing.T) { +func TestResolveNextActiveDeliveryIsReported(t *testing.T) { repo := nextTestRepo(t) writeNextDelivery(t, repo, "active-feature", "BUILD", 0) - writeIntakePlan(t, repo, "new-feature.md") status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } if status.Feature != "active-feature" || status.ObservedStage != "BUILD" || status.NextOperation != "build" { - t.Fatalf("source plan displaced active delivery: %+v", status) + t.Fatalf("active delivery not reported: %+v", status) } } -func TestResolveNextOrphanedEvidenceOutranksSourcePlan(t *testing.T) { +func TestResolveNextOrphanedEvidenceBlocks(t *testing.T) { repo := nextTestRepo(t) directory := filepath.Join(repo, ".product-loop", "features", "orphan") if err := os.MkdirAll(directory, 0o755); err != nil { @@ -151,14 +129,13 @@ func TestResolveNextOrphanedEvidenceOutranksSourcePlan(t *testing.T) { if err := os.WriteFile(filepath.Join(directory, "pr.md"), []byte("# Preview\n"), 0o644); err != nil { t.Fatal(err) } - writeIntakePlan(t, repo, "new-feature.md") status, err := ResolveNext(repo, "") if err != nil { t.Fatal(err) } if status.VerificationStatus != "BLOCKED" || status.ObservedStage != "INVALID_STATE" || status.NextOperation != "repair-state" { - t.Fatalf("source plan bypassed orphaned evidence: %+v", status) + t.Fatalf("orphaned evidence did not block: %+v", status) } } @@ -254,16 +231,12 @@ func TestResolveNextDeliveryTransitions(t *testing.T) { func TestResolveNextReportsPublishedUnknownWithoutPRVerification(t *testing.T) { repo := nextTestRepo(t) writeNextDelivery(t, repo, "recovery", "PUBLISHED", 1) - intake := filepath.Join(repo, ".product-loop", "intake") - if err := os.MkdirAll(intake, 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(intake, "source-plan.md"), []byte("# Source plan\n"), 0o644); err != nil { + if err := os.WriteFile(filepath.Join(repo, "source-plan.md"), []byte("# Source plan\n"), 0o644); err != nil { t.Fatal(err) } plan := validPlan() plan["feature_id"] = "recovery" - plan["source_plan_path"] = "../../intake/source-plan.md" + plan["source_plan_path"] = "../../../source-plan.md" writeMarkdownPlan(t, filepath.Join(repo, ".product-loop", "features", "recovery", "plan.md"), plan, true) status, err := ResolveNext(repo, "") if err != nil { diff --git a/boatstack/plan.go b/boatstack/plan.go index e39b88c..0554b25 100644 --- a/boatstack/plan.go +++ b/boatstack/plan.go @@ -185,87 +185,39 @@ func CheckSourcePlan(path string) error { return nil } -func sourcePlanCandidates(repo string) ([]string, error) { - repoAbsolute, err := filepath.Abs(repo) - if err != nil { - return nil, err - } - roots := []string{ - ".product-loop/intake", - ".cursor/plans", - ".claude/plans", - ".codex/plans", - } - allowed := map[string]bool{".md": true, ".txt": true, ".json": true, ".yaml": true, ".yml": true} - candidates := []string{} - for _, root := range roots { - absoluteRoot := filepath.Join(repoAbsolute, filepath.FromSlash(root)) - if _, err := os.Stat(absoluteRoot); err != nil { - if os.IsNotExist(err) { - continue - } - return nil, err - } - err := filepath.WalkDir(absoluteRoot, func(path string, entry os.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - if entry.IsDir() { - return nil - } - if entry.Type()&os.ModeSymlink != 0 || !allowed[strings.ToLower(filepath.Ext(entry.Name()))] { - return nil - } - if strings.EqualFold(entry.Name(), "README.md") || CheckSourcePlan(path) != nil { - return nil - } - relative, relErr := filepath.Rel(repoAbsolute, path) - if relErr != nil { - return relErr - } - candidates = append(candidates, filepath.ToSlash(relative)) - return nil - }) - if err != nil { - return nil, err - } - } - sort.Strings(candidates) - return candidates, nil -} - +// DiscoverSourcePlan resolves the source plan from an explicit path supplied by +// the caller (the host coding agent). Boatstack never scans directories for +// ambient plan files: the plan produced in the host conversation must be passed +// explicitly via --plan so no unshipped saved plan becomes blocking context. +// +// The resolved path is recorded as source_plan_path and hashed into the plan +// fingerprint, then re-validated for drift through build. A plan file outside +// the repository cannot satisfy that invariant: its absolute path does not +// travel with clones or linked worktrees and it is never committed alongside the +// feature, so build activation later fails on a missing file or hash drift. We +// reject it up front and require an in-repo, durable path instead of surfacing +// the failure downstream at build time. func DiscoverSourcePlan(repo, explicit string) (string, error) { repoAbsolute, err := filepath.Abs(repo) if err != nil { return "", err } - if strings.TrimSpace(explicit) != "" { - candidate := explicit - if !filepath.IsAbs(candidate) { - candidate = filepath.Join(repoAbsolute, candidate) - } - candidate = filepath.Clean(candidate) - if err := CheckSourcePlan(candidate); err != nil { - return "", err - } - relative, err := filepath.Rel(repoAbsolute, candidate) - if err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) { - return filepath.ToSlash(relative), nil - } - return candidate, nil + if strings.TrimSpace(explicit) == "" { + return "", fmt.Errorf("no source plan provided; pass --plan to the plan produced in the host conversation") } - - candidates, err := sourcePlanCandidates(repoAbsolute) - if err != nil { - return "", err + candidate := explicit + if !filepath.IsAbs(candidate) { + candidate = filepath.Join(repoAbsolute, candidate) } - if len(candidates) == 0 { - return "", fmt.Errorf("no saved Plan-mode file found; save the current host plan under .product-loop/intake/ and run auto-plan again") + candidate = filepath.Clean(candidate) + if err := CheckSourcePlan(candidate); err != nil { + return "", err } - if len(candidates) > 1 { - return "", fmt.Errorf("multiple saved Plan-mode files found: %s; keep one active intake file or pass the intended path", strings.Join(candidates, ", ")) + relative, err := filepath.Rel(repoAbsolute, candidate) + if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) { + return "", fmt.Errorf("source plan %s is outside the repository; copy the plan into the repo and pass a durable in-repo path to --plan so it stays present and hash-current through build", explicit) } - return candidates[0], nil + return filepath.ToSlash(relative), nil } func SourcePlanForStructuredPlan(planPath string) (string, error) { diff --git a/boatstack/plan_test.go b/boatstack/plan_test.go index 84ccc1c..408a6d5 100644 --- a/boatstack/plan_test.go +++ b/boatstack/plan_test.go @@ -375,36 +375,101 @@ func TestSourcePlanPreflightBlocksMissingAndEmptyFiles(t *testing.T) { } } -func TestSourcePlanDiscoveryUsesOneBoundedCandidateAndBlocksAmbiguity(t *testing.T) { +// TestSourcePlanRequiresExplicitPlanAndNeverScansDirectories is the conformance +// guard for the "no ambient plan context" contract: Boatstack must never scan a +// directory for source plans. An empty --plan blocks even when plan-shaped files +// exist in the historically scanned locations, and only an explicit path +// resolves. +func TestSourcePlanRequiresExplicitPlanAndNeverScansDirectories(t *testing.T) { repo := t.TempDir() - intake := filepath.Join(repo, ".product-loop", "intake") - if err := os.MkdirAll(intake, 0o755); err != nil { + // Seed plan-shaped files in every location discovery used to scan. None of + // these may be picked up. + for _, dir := range []string{ + ".product-loop/intake", + ".cursor/plans", + ".claude/plans", + ".codex/plans", + } { + absolute := filepath.Join(repo, filepath.FromSlash(dir)) + if err := os.MkdirAll(absolute, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(absolute, "stale.md"), []byte("# Stale unshipped plan\n"), 0o644); err != nil { + t.Fatal(err) + } + } + + if _, err := DiscoverSourcePlan(repo, ""); err == nil { + t.Fatal("expected empty --plan to block; discovery must never scan directories") + } else if !strings.Contains(err.Error(), "--plan") { + t.Fatalf("expected error to require --plan, got %v", err) + } + + // An explicit path is the only way to supply a source plan. + explicitFile := filepath.Join(repo, "docs", "chosen-plan.md") + if err := os.MkdirAll(filepath.Dir(explicitFile), 0o755); err != nil { t.Fatal(err) } - first := filepath.Join(intake, "feature-a.md") - if err := os.WriteFile(first, []byte("# Feature A plan\n"), 0o644); err != nil { + if err := os.WriteFile(explicitFile, []byte("# Chosen plan\n"), 0o644); err != nil { t.Fatal(err) } - discovered, err := DiscoverSourcePlan(repo, "") + explicit, err := DiscoverSourcePlan(repo, "docs/chosen-plan.md") if err != nil { t.Fatal(err) } - if discovered != ".product-loop/intake/feature-a.md" { - t.Fatalf("unexpected discovered path: %s", discovered) + if explicit != "docs/chosen-plan.md" { + t.Fatalf("unexpected explicit path: %s", explicit) + } +} + +// TestSourcePlanRejectsOutsideRepoPath is the durability guard: source_plan_path +// is recorded and re-hashed through build, so a plan outside the repository +// (whose absolute path does not travel and is never committed) must be rejected +// up front rather than drifting at build time. +func TestSourcePlanRejectsOutsideRepoPath(t *testing.T) { + parent := t.TempDir() + repo := filepath.Join(parent, "repo") + if err := os.MkdirAll(repo, 0o755); err != nil { + t.Fatal(err) } - second := filepath.Join(intake, "feature-b.md") - if err := os.WriteFile(second, []byte("# Feature B plan\n"), 0o644); err != nil { + // A real, non-empty plan file that lives outside the repository. It passes + // CheckSourcePlan (it exists and is non-empty) but is not durable relative + // to the repo. + outside := filepath.Join(parent, "ephemeral-plan.md") + if err := os.WriteFile(outside, []byte("# Ephemeral scratch plan\n"), 0o644); err != nil { t.Fatal(err) } - if _, err := DiscoverSourcePlan(repo, ""); err == nil || !strings.Contains(err.Error(), "multiple") { - t.Fatalf("expected ambiguous source plans to block, got %v", err) + if _, err := DiscoverSourcePlan(repo, outside); err == nil { + t.Fatal("expected an out-of-repo source plan to be rejected") + } else if !strings.Contains(err.Error(), "outside the repository") { + t.Fatalf("expected an out-of-repo error, got %v", err) + } + // The same holds for a relative path that escapes the repo. + if _, err := DiscoverSourcePlan(repo, filepath.Join("..", "ephemeral-plan.md")); err == nil { + t.Fatal("expected a repo-escaping relative source plan to be rejected") } - explicit, err := DiscoverSourcePlan(repo, ".product-loop/intake/feature-b.md") +} + +// TestNoIntakeStagingReferenceInProductionSource guards against the intake +// staging concept returning: no production Go file may reference the removed +// .product-loop/intake staging directory. +func TestNoIntakeStagingReferenceInProductionSource(t *testing.T) { + entries, err := os.ReadDir(".") if err != nil { t.Fatal(err) } - if explicit != ".product-loop/intake/feature-b.md" { - t.Fatalf("unexpected explicit path: %s", explicit) + for _, entry := range entries { + name := entry.Name() + if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + contents, readErr := os.ReadFile(name) + if readErr != nil { + t.Fatal(readErr) + } + if strings.Contains(string(contents), ".product-loop/intake") { + t.Errorf("%s still references the removed .product-loop/intake staging directory", name) + } } } diff --git a/boatstack/references/workflow.md b/boatstack/references/workflow.md index 1e67d46..e2c65af 100644 --- a/boatstack/references/workflow.md +++ b/boatstack/references/workflow.md @@ -114,7 +114,7 @@ Lead with a plain outcome, never a machine code such as `PASS`, `PLAN_APPROVED`, ### Foreground run coordinator -`run` is an opt-in foreground coordinator over the existing operations, not a second state machine. It first resolves the read-only repository state, enters `auto-plan` when one saved source plan exists, asks for a saved Plan-mode file when none exists, returns **Feature complete** without requiring a remote only for completed work, and stops on unverified or blocked state. Before the first delivery-stage mutation it runs the versioned Git preflight, which fetches `origin`, requires the fetched remote base, verifies that the current named branch contains that base, rejects a behind or diverged upstream, and enforces any active slice branch constraints. Planning and approval remain local and do not require a remote. It never merges, rebases, switches or creates constrained branches, discards changes, force-pushes, merges a PR, or deploys. +`run` is an opt-in foreground coordinator over the existing operations, not a second state machine. It first resolves the read-only repository state, enters `auto-plan` when the host supplies the plan path (`--plan`), asks for that plan when none is supplied, returns **Feature complete** without requiring a remote only for completed work, and stops on unverified or blocked state. Before the first delivery-stage mutation it runs the versioned Git preflight, which fetches `origin`, requires the fetched remote base, verifies that the current named branch contains that base, rejects a behind or diverged upstream, and enforces any active slice branch constraints. Planning and approval remain local and do not require a remote. It never merges, rebases, switches or creates constrained branches, discards changes, force-pushes, merges a PR, or deploys. After preflight, resolve the repository-backed next operation, execute exactly that canonical operation, verify the resulting state, and resolve again through all declared delivery slices. When the resolved block names only past deliveries, the coordinator may offer to ignore a named past delivery (adding its slug to `workflow.ignored_deliveries`) only after explicit user confirmation; any new, unlisted ambiguous delivery still pauses. Pause for `a`, a material product answer, and `o` or `u`; after the valid state-scoped reply, continue in the current host session. The invocation does not replace either human authorization. Automatically record and repair same-intent test or review failures for at most three complete repair-and-gate cycles per active slice per invocation. Stop immediately for requirement amendments, ambiguous or stale state, unsafe capability, unsupported recovery, branch mismatch, or exhausted repairs. Store no durable run/autopilot mode; re-invocation reconstructs progress from canonical repository state. @@ -145,17 +145,15 @@ For plan approval, resolve `approved_by` from (1) an identity supplied with appr ### `INTENT -> SOURCE_PLAN` -Begin in the active coding host's Plan mode. Explore the ordinary product intent without editing implementation files, then save that host-generated plan as a file. Invoke `auto-plan` without a path in the normal case. +Begin in the active coding host's Plan mode. Explore the ordinary product intent without editing implementation files, then save that host-generated plan as a durable file. Invoke `auto-plan` with the plan's path. Before repository inspection, run: ```bash .product-loop/bin/boatstack-helper check-source-plan --repo . --plan -# If the host exposes no active path: -.product-loop/bin/boatstack-helper check-source-plan --repo . ``` -The host/system conversation path is authoritative when present. Fallback discovery checks `.product-loop/intake/` and bounded repo-local host plan directories; it never scans the whole repository or selects a file solely because it is newest. If the file is missing, ambiguous, empty, or unreadable, `auto-plan` is `BLOCKED` and may request an explicit path. It must not manufacture the missing input. This source plan is an initial proposal rather than human approval. +Boatstack never scans directories for plans, so `--plan` is required and no unshipped saved plan becomes ambient context. If no plan path is supplied, or the file is missing, empty, or unreadable, `auto-plan` is `BLOCKED` and must request the plan to build. It must not manufacture the missing input. Because the file's hash is recorded and re-checked through `BUILD`, `--plan` must point at a durable in-repo path that stays present and unchanged; a path outside the repository is rejected. This source plan is an initial proposal rather than human approval. ### `SOURCE_PLAN -> PROJECT` diff --git a/boatstack/safety.go b/boatstack/safety.go index 5bdfe5a..7d2e8b5 100644 --- a/boatstack/safety.go +++ b/boatstack/safety.go @@ -211,9 +211,6 @@ func attemptedRepositoryPath(repo string, input any) string { } func planningMarkdownPath(path string) bool { - if strings.HasPrefix(path, ".product-loop/intake/") && strings.HasSuffix(strings.ToLower(path), ".md") { - return true - } if !strings.HasPrefix(path, ".product-loop/features/") { return false } diff --git a/boatstack/safety_test.go b/boatstack/safety_test.go index 4e522e4..b3c1c51 100644 --- a/boatstack/safety_test.go +++ b/boatstack/safety_test.go @@ -419,6 +419,24 @@ func TestSafetyGuardLatencyIsBounded(t *testing.T) { } } +// TestPlanningMarkdownPathRejectsIntakeStaging is the conformance guard that the +// removed intake staging directory is no longer a permitted planning-write path; +// only feature-scoped planning artifacts remain writable. +func TestPlanningMarkdownPathRejectsIntakeStaging(t *testing.T) { + rejected := []string{ + ".product-loop/intake/source-plan.md", + ".product-loop/intake/anything.md", + } + for _, path := range rejected { + if planningMarkdownPath(path) { + t.Errorf("intake staging path must no longer be a permitted planning-write path: %s", path) + } + } + if !planningMarkdownPath(".product-loop/features/account-recovery/plan.md") { + t.Fatal("feature-scoped planning artifacts must remain writable") + } +} + func TestPreActivationMutationInterlockLatchesAfterAutoPlan(t *testing.T) { repo := nextTestRepo(t) writeValidSavedFeaturePlan(t, repo, "guarded-feature") diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 3d4d1ef..7cf899d 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -48,7 +48,7 @@ slices, but the control state activates only one. Every active slice must produc fresh diff-bound test and review receipts and receive its own ship confirmation; approval of the parent plan carries scope, not publication authority. -The entry state is not an unstructured chat message. Ordinary product intent is first explored in the active host's Plan mode and saved as a source plan. `auto-plan` resolves the active path from host conversation context or bounded fallback discovery, then requires that file before projecting repository context: +The entry state is not an unstructured chat message. Ordinary product intent is first explored in the active host's Plan mode and saved as a source plan. The host passes that file's path to `auto-plan` explicitly (`--plan `); Boatstack never scans directories for plans, and requires that file before projecting repository context: ```text ordinary intent --host Plan mode--> source plan file --auto-plan--> reviewable feature package @@ -96,7 +96,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 **15147 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 **15145 estimated tokens**, while host adapters point to one operation at a time. ## Control appears at transitions @@ -146,6 +146,6 @@ Delivery and system improvement also remain separate. A failed task may suggest ## What is evidence-backed -The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`91e33a95add752894fb532a67b980c109ecd28e8`](https://github.com/operatorstack/intelligence-flow/tree/91e33a95add752894fb532a67b980c109ecd28e8/labs/12-product-engineering-loop). +The current moves were derived from the Intelligence Flow benchmark corpus and product-repository studies. The generated source commit is [`dc7d340e0028db5df232d75b4c69ba866aa133f9`](https://github.com/operatorstack/intelligence-flow/tree/dc7d340e0028db5df232d75b4c69ba866aa133f9/labs/12-product-engineering-loop). The evidence supports specific failure mechanisms and guardrails. It does not establish that Boatstack is optimal, that control-theory notation proves software quality, or that one workflow dominates every team. Those are evaluation questions, so the distribution preserves measurements, provenance, gaps, and negative results. diff --git a/docs/getting-started.md b/docs/getting-started.md index 2908963..03f9d47 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -62,7 +62,7 @@ Create a feature branch from the base containing Boatstack. Enter your host's Pl Add account recovery without removing the existing passwordless sign-in flow. ``` -Let the host inspect the relevant repository slice and save its plan. Boatstack uses a host-exposed path when available; otherwise save exactly one non-empty plan under `.product-loop/intake/`. +Let the host inspect the relevant repository slice and save its plan as a durable file. Pass that path to `/auto-plan` via `--plan `; Boatstack does not scan directories for plans, so the path is always required. Start Boatstack with the entry point for your host: diff --git a/docs/public-claims.json b/docs/public-claims.json index f060a82..09b6101 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "91e33a95add752894fb532a67b980c109ecd28e8", + "source_commit": "dc7d340e0028db5df232d75b4c69ba866aa133f9", "statuses": ["verified", "observed", "still_being_evaluated"], "claims": [ { @@ -12,7 +12,7 @@ "readable_evidence": "why-these-steps.md#portable-workflow-and-state", "implementation": ["../boatstack/export.go", "../boatstack/references/artifacts.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go"], - "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" + "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" }, { "id": "human-decisions", @@ -23,7 +23,7 @@ "readable_evidence": "why-these-steps.md#human-decisions", "implementation": ["../boatstack/references/workflow.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" + "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" }, { "id": "validation-provenance", @@ -34,7 +34,7 @@ "readable_evidence": "why-these-steps.md#validation-provenance", "implementation": ["validation-and-evidence.md", "../boatstack/plan.go"], "verification": ["../boatstack/plan_test.go"], - "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" + "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" }, { "id": "irreversible-operations", @@ -46,7 +46,7 @@ "readable_evidence": "why-these-steps.md#irreversible-operations", "implementation": ["safety.md", "../boatstack/safety.go", "../boatstack/hooks.go"], "verification": ["../boatstack/safety_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" + "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" }, { "id": "reviewer-ready-pr", @@ -57,7 +57,7 @@ "readable_evidence": "why-these-steps.md#reviewer-ready-pr", "implementation": ["../boatstack/pr.go", "getting-started.md"], "verification": ["../boatstack/pr_test.go"], - "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" + "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" }, { "id": "phase-scoped-delivery", @@ -68,7 +68,7 @@ "readable_evidence": "why-these-steps.md#phase-scoped-delivery", "implementation": ["../boatstack/delivery.go", "../boatstack/safety.go", "../boatstack/hooks.go", "../boatstack/references/workflow.md"], "verification": ["../boatstack/delivery_test.go", "../boatstack/pr_test.go"], - "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" + "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" }, { "id": "model-neutral-contract", @@ -79,7 +79,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md", "../boatstack/references/workflow.md"], "verification": ["../boatstack/export_test.go", "../boatstack/planning_test.go"], - "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" + "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" }, { "id": "cross-model-failures", @@ -90,7 +90,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" + "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" }, { "id": "lower-cost-outcomes", @@ -101,7 +101,7 @@ "readable_evidence": "why-these-steps.md#model-choice-and-budget", "implementation": ["research-and-design.md"], "verification": ["benchmark-corpus-audit.md", "benchmark-submission-audit.md"], - "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" + "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" }, { "id": "git-worktree-activation", @@ -112,7 +112,7 @@ "readable_evidence": "why-these-steps.md#git-worktree-activation", "implementation": ["../boatstack/runtime_cache.go", "../boatstack/hooks.go"], "verification": ["../boatstack/runtime_cache_test.go", "../boatstack/hooks_test.go"], - "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" + "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" }, { "id": "visible-updates", @@ -123,7 +123,7 @@ "readable_evidence": "why-these-steps.md#visible-updates", "implementation": ["../boatstack/update.go", "../boatstack/init.go"], "verification": ["../boatstack/update_test.go", "../boatstack/init_test.go", "../boatstack/export_test.go"], - "last_verified_version": "source:91e33a95add752894fb532a67b980c109ecd28e8" + "last_verified_version": "source:dc7d340e0028db5df232d75b4c69ba866aa133f9" } ] } diff --git a/docs/research-and-design.md b/docs/research-and-design.md index aeba50a..d9f3152 100644 --- a/docs/research-and-design.md +++ b/docs/research-and-design.md @@ -45,7 +45,7 @@ Cursor/GitHub intent `/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. +`/auto-plan` is deliberately not the first planning surface. It requires a non-empty file produced by the active host's Plan mode, supplied explicitly via `--plan `, and refuses to invent that input. Boatstack never scans directories for plans, so no unshipped saved plan becomes ambient context; a missing path blocks instead of silently choosing a file. The source file is hash-bound through build, so the supplied path must stay present and unchanged; after build, test/review/ship consume the approved lock, diff, and evidence rather than repeatedly loading the exploratory plan. ## Why the workflow has no model conditions diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a46a6af..77d922d 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -75,7 +75,7 @@ If Boatstack created `.claude/skills/` while Claude Code was already running, re ## `/auto-plan` cannot find a source plan -Finish the host's Plan-mode exploration and save it. If the host does not expose the path, put exactly one non-empty plan under `.product-loop/intake/`, then rerun `/auto-plan`. Supply an explicit path only when Boatstack reports multiple candidates. +Finish the host's Plan-mode exploration and save it as a durable file, then rerun `/auto-plan` with that path via `--plan `. Boatstack does not scan directories for plans, so the path is always required; point it at a file that stays present and unchanged through build. ## Plan mode cannot write an artifact diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index a212c2b..a1700db 100644 --- a/labs/diagram-json/plan.lock.json +++ b/labs/diagram-json/plan.lock.json @@ -6,7 +6,7 @@ "plan_path": "labs/diagram-json/plan.md", "plan_sha256": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "schema_version": 1, - "source_commit": "91e33a95add752894fb532a67b980c109ecd28e8", + "source_commit": "dc7d340e0028db5df232d75b4c69ba866aa133f9", "source_plan_path": "labs/diagram-json/source-plan.md", "source_plan_sha256": "e10593ddaa7522ab80cc991d0a09399257139799e37f737794cd49d68a39985b", "spec_path": "labs/diagram-json/spec.md", diff --git a/release-notes/2026-07-23-explicit-source-plan.md b/release-notes/2026-07-23-explicit-source-plan.md new file mode 100644 index 0000000..4ae821f --- /dev/null +++ b/release-notes/2026-07-23-explicit-source-plan.md @@ -0,0 +1,8 @@ +### Source plans are supplied explicitly, never discovered + +Boatstack no longer scans directories for source plans, so it never retains context for work that was never shipped. + +- `auto-plan` requires the plan produced in the host conversation to be passed explicitly with `--plan `; the `.product-loop/intake/` staging area and the `.cursor/plans`, `.claude/plans`, and `.codex/plans` scan roots are removed. +- A saved plan-mode file left in the repository can no longer become ambient context or block `next-status` with an ambiguity stop; with no started feature the state is simply `NOT_STARTED` pointing to `auto-plan`. +- The `--plan` path must be a durable in-repo file. A path outside the repository is rejected up front, because `source_plan_path` is hashed into the plan fingerprint and re-checked through build, and an out-of-repo file cannot stay committed and hash-current. +- The `source_plan_path` and `source_plan_sha256` invariant is unchanged: the plan must still be a real, present, hash-current file from `auto-plan` through `build`.