From a1c0de4ed4cd1c20c7686f5ecf855aa4a5c5af3d Mon Sep 17 00:00:00 2001 From: "operator-stack-publisher[bot]" Date: Mon, 27 Jul 2026 12:58:02 +0000 Subject: [PATCH] Sync Boatstack from Intelligence Flow Labs @ 2126fde051cc --- CONTRIBUTING.md | 2 +- UPSTREAM.json | 30 +- boatstack/SKILL.md | 2 +- boatstack/denial.go | 11 + boatstack/denial_test.go | 26 ++ boatstack/export.go | 6 +- boatstack/flow_control.go | 176 +++++++++- boatstack/flow_control_test.go | 28 +- ...low_planning_prescribe_conformance_test.go | 317 ++++++++++++++++++ boatstack/references/workflow.md | 4 +- docs/evidence-engineered-coding.md | 4 +- docs/public-claims.json | 24 +- labs/diagram-json/plan.lock.json | 2 +- ...026-07-27-prescriptive-planning-closure.md | 7 + 14 files changed, 588 insertions(+), 51 deletions(-) create mode 100644 boatstack/flow_planning_prescribe_conformance_test.go create mode 100644 release-notes/2026-07-27-prescriptive-planning-closure.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d4816f1..82b2929 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/2364eaedaefbe73d8996108dfac261c38b0ce2b3/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/2126fde051cc11a8c3de9fff4fc17b5397240383/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 befd862..506bdcc 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -1,7 +1,7 @@ { "canonical_context": { - "characters": 80569, - "estimated_tokens": 20143, + "characters": 80947, + "estimated_tokens": 20237, "estimator": "ceil(total characters / 4); compactness signal, not provider billing", "files": [ "product-engineering-loop/references/workflow.md", @@ -12,14 +12,14 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "ea4d692766d22427d515ab944c9d692e862d63584c255fdecdb67e063cb32e32", + "CONTRIBUTING.md": "7338b0102e28024ac659eebe01be35bbcdf31aecb92e8c8dea15e5aa3abcf317", "README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", "assets/boatstack-portability.svg": "66dfdfa85db857b3bd18b32047a6975f1fbbfc4dc091158e8277193f9969a346", "boatstack/AGENTS.md": "bc76221e1fe90a91afbacd7c6bc9b41a70e6c10fc128c275a6a0b9bc094d9506", "boatstack/BUG-worktree-delivery-state.md": "02469cf51c3849dad5743783e248e5c04583e4240507fbef0e3f890cd6a95724", - "boatstack/SKILL.md": "b393fe00f23f701e1310d7c1006f339935d3082c7adb9b35c038a3ad1bcc459e", + "boatstack/SKILL.md": "1eb959a55c5462143c54f0016963542e7b28ceb5ed6ce1cfba942fde9595348a", "boatstack/activation.go": "deb7712d20aed37371254596613bfaccfc05fcb59634408b03378d1a0bf693ea", "boatstack/agents/gemini.yaml": "cbf43b387399e456fa6178f86d83e6e35567e6142ff800f8de6ffca306fa963e", "boatstack/agents/openai.yaml": "68a30a60859556c5a26e16d184594ca243a6043d99c8cf7d66b5dd6d50a93cd1", @@ -60,22 +60,23 @@ "boatstack/delivery_reactivation_test.go": "573a2dba0034bc4290478414e3bdd8670b06a326128eb0295d77e748ecc8689e", "boatstack/delivery_test.go": "45c48ff7581c911bcaf821c3e4241d4ae2a9bb4aa682485cc58b6ad8fe1c85bf", "boatstack/deliverycontrol_parity_test.go": "f8662cfc35043395a0e1eef8a87051c2120752f38b09c56b78f82896008f1b65", - "boatstack/denial.go": "656dc5e71a11daba25e7a23599f2e1aafa7a7439589af943cdccc72154a45816", - "boatstack/denial_test.go": "c480c0b2a489838b22b4d5ec20cc30ef1859cc0820a75974bc56a46c31f90041", + "boatstack/denial.go": "4132674988f35b776dc3207e5179dbb2e4da6b8e815a5913acfc3c8771f6ce51", + "boatstack/denial_test.go": "b865bf041b42ad4b904de0c7cf8d26ab7b5ba5bcc29407afe6caafa318f54791", "boatstack/detached.go": "b0ea2a1f31bf2a2a83f6089a3065a9b47221194b64de48af9120046e6d70dee8", "boatstack/detached_test.go": "6cc70d15baa9a69afacf66ea29ce112efeb166836acb0a52bf9c4bb4c898cee5", "boatstack/docs/control-law-scoping.md": "0ae984821248eabda8c0eeaf201b367991e6742984e7c718df20ecc24caee475", "boatstack/evidence.go": "497a31e6ff632cb1d7c3adfc9f269af3f6aa84e948dd5d417c162767542a27df", - "boatstack/export.go": "1a01d19ac6e8418febf93f9ebf1a466a46da4b09eea7bafe6ccfb6cfb0f73a6d", + "boatstack/export.go": "56dd395382e033cc919ae8894722d5c6d135ffdee9153edd900dd064c3d08962", "boatstack/export_test.go": "dce5aa3ab5499c82d05859cf86b46dfcee308482491366d83e10ca3fb8605bb6", "boatstack/flow_coding.go": "9fa53a0204f98a25f97775c3acf37392a591c14ce850b44aa587b5806e770bb9", "boatstack/flow_coding_test.go": "dddcd7a85892d4fa10af42739d4c1ff265721b0313e27b6e7a1bbb019d5c3b51", - "boatstack/flow_control.go": "23b539c0d2dfcabb4606b94abeee1b25754cd9627c3c39f08ddc3c49fed81615", - "boatstack/flow_control_test.go": "d52e095f2e0f18067abe03e3b5f7c98bc30f8b1c8f5230103797c599aec95013", + "boatstack/flow_control.go": "76b78c69305475f827ab512cf5e78ae22e6a1b40c4e8b2387b72e00fee09bf07", + "boatstack/flow_control_test.go": "02d788c83be55ebd79ffc73875bfd019de45325151eb1f70f506980eb8e77f29", "boatstack/flow_drive.go": "a501ceda390dfd3605e22cf7ecfa15f9d50240b3fac6ebb2bb2d80c615d0a9fc", "boatstack/flow_drive_conformance_test.go": "23edea926c271a1f5718fb9dae1da11e4bf03cceb1357290cd61cd8ffb73beda", "boatstack/flow_guard.go": "dd18524d95f4a220cfd3d11b11003dacc52120785ee0ccdbeceb2307fab55872", "boatstack/flow_guard_test.go": "8ba75f11ddd080427c15bd7e25f7d03c1b746a2f587c212cea0e710337d1c0e1", + "boatstack/flow_planning_prescribe_conformance_test.go": "c2fa2566b0676f34a777764ade87a0670d41413d052a9a339e01bdb6a9a8699b", "boatstack/flow_prescribe_conformance_test.go": "e2287aa079b7aafaf822e1f752a1bb5017acda811363e576eeff793347d0d5c8", "boatstack/flow_report.go": "9e58cec51c6c903847f3ebc79cd6bf25e2f91d14b81811e82e5f4e026a7b71a3", "boatstack/flow_report_test.go": "eae00f2b8ead4f1ec20e1f1bc47db4c53a36048bb84eca9bea4f1a2105bdcdde", @@ -152,7 +153,7 @@ "boatstack/references/host-hook-contracts.md": "2a89d44d0e418a53f2e3b6300fed957cdf878f45ea97ce24b55b66065f0eaa1d", "boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441", "boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae", - "boatstack/references/workflow.md": "81da3ea831ef244eb03b09e1989052b6f52db816e3d65be98da3ab9d5cc31969", + "boatstack/references/workflow.md": "d6174f8d546e75829a35418741e94a5606d18314e4a2d8e0bb04394745f335bd", "boatstack/release.go": "82dcb4ca59e8c79a68d5333d650f90e64abd448d04e0c6f504fdf07f42b5ed76", "boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690", "boatstack/repair_state_test.go": "f3779ac47c3db3927175a545728d3b2e020dbc85f41394d8235753b52afc3739", @@ -190,10 +191,10 @@ "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", "docs/configuration.md": "060775c73431f28bd16066bdf9e0f89034d2855c7ca0f5544f660d24b91211d0", - "docs/evidence-engineered-coding.md": "0083581913336f9612f321997a3e9afb8b7f2549e41d6fbae122920174ae4779", + "docs/evidence-engineered-coding.md": "e5c48eb66d9ac2967aa41b8ba972f0d85d7fb95cdaa2fe1019f45bd544d73eb6", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", "docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8", - "docs/public-claims.json": "aa2ee5ecd6a3f29d5dd3c4e538d29931191606a52fad6433d43184af814334c3", + "docs/public-claims.json": "b723b764dcb7ca02f339a36e837a6fcf7d9b59ab881878e0ea1aab9c3a101070", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -207,7 +208,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": "9c2377941197d6bd160f338e0bc55dd74f9118844291b5ee73a17867757a5edb", + "labs/diagram-json/plan.lock.json": "4c31e6695c8cc726d795e8dd39d8f7ba84d208a615786d674ef445e91e60b52b", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -326,13 +327,14 @@ "release-notes/2026-07-27-discoverable-planning-errors.md": "d8099d1a6cd1805c3fcd446d9fa95739dec93e57ff421ecf4f99562b143c9966", "release-notes/2026-07-27-guard-dual-reward-corpus.md": "6bec0385c6c553f00517259821e502796ca1b1907aeab718a287560e3e0fa0d6", "release-notes/2026-07-27-invalid-delivery-block-actionable.md": "8fac8e3921e2285291703efa46e624b72cb5bac1b8492beca4c4b633abb5ba16", + "release-notes/2026-07-27-prescriptive-planning-closure.md": "e544408e1c3cceb0cb1979833ea120853c38020933439b39e7f009f454b9661e", "release-notes/2026-07-27-read-only-inspection-pipelines.md": "0963286371e9a12592915c23a958dd013bf2a35e9fca6921691bc8bb3c3d8dc8", "release-notes/2026-07-27-sandboxed-migration-grading.md": "03cebc372bbdfed37cc70d18f3b6374d1aa5e585bafefa073dbcced58bd0336a" }, "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "2364eaedaefbe73d8996108dfac261c38b0ce2b3", + "commit": "2126fde051cc11a8c3de9fff4fc17b5397240383", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/SKILL.md b/boatstack/SKILL.md index 4af0b1e..c28f73e 100644 --- a/boatstack/SKILL.md +++ b/boatstack/SKILL.md @@ -113,7 +113,7 @@ Before starting `/auto-plan` for a new feature, check `next-status --repo . --js 13. 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. 14. 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`. 14. For every external write, record `affected_paths` plus side-effect kind, immutable target identity, reversibility, failure policy, and `destructive: false`. Reject ambiguous reset rollback or target names. -15. Write only Markdown feature artifacts, including the canonical structured `plan.md`. Put its authoritative JSON inside the marked Boatstack block and run `boatstack-helper check-plan --plan /plan.md`; this command is read-only. If the host blocks its ordinary Markdown writer, pass the document to `boatstack-helper planning-write --repo . --feature --artifact ` on stdin. Never use arbitrary shell redirection to evade a host write boundary. +15. Write only Markdown feature artifacts, including the canonical structured `plan.md`. Author every feature artifact through the owned channel: pass the document to `boatstack-helper planning-write --repo . --feature --artifact ` on stdin — it is the primary writer for `.product-loop/features/`, not a fallback, and it remains available after the planning latch denies raw writes. Put the authoritative JSON inside the marked Boatstack block and run `boatstack-helper check-plan --plan /plan.md`; this command is read-only. The host's ordinary Markdown writer may be used only where the host explicitly permits it. Never use arbitrary shell redirection to evade a host write boundary. 16. Keep implementation tasks separate from publication authority. Internal phases remain tasks inside one delivery slice. When the accepted outcome explicitly requires multiple PRs, declare ordered `delivery_slices`; assign every task exactly once and give each slice its own optional base/head branch contract. Plan approval approves this structure but never authorizes a push or PR. 17. End with a **draft**, never an implied approval. Do not generate executable task state, JSON artifacts, locks, or implementation changes from `auto-plan`. diff --git a/boatstack/denial.go b/boatstack/denial.go index d3bac40..1412fff 100644 --- a/boatstack/denial.go +++ b/boatstack/denial.go @@ -357,6 +357,17 @@ func denialFor(host string, finding SafetyFinding) Denial { } d.Qualifier = "plan gate" d.Detail = fmt.Sprintf("Product mutation is denied because %s is at %s.%s Continue with `%s`; unrelated task completions do not authorize implementation.", target, finding.WorkflowStage, attempted, next) + // A planning-state denial must name the owned authoring channel, not just + // the cleanup verb — otherwise the corrective move (planning-write) is + // discoverable only by failing again. + // control-law: prescriptive-closure-every-stage-names-a-runnable-command + if finding.Source == "planning-state" { + slug := "" + if finding.BlockingFeature != "" { + slug = finding.BlockingFeature + } + d.Detail += fmt.Sprintf(" Planning Markdown is authored through the owned channel: `boatstack-helper planning-write --repo . --feature %s --artifact ` with the document on stdin — never a raw host write into `.product-loop/features/`.", slug) + } d.Reassurance = reassureUntouched return d diff --git a/boatstack/denial_test.go b/boatstack/denial_test.go index 68a1153..4f4aaaf 100644 --- a/boatstack/denial_test.go +++ b/boatstack/denial_test.go @@ -35,6 +35,32 @@ func TestDenialRenderModesCarryTheSameInformation(t *testing.T) { } } +// control-law: prescriptive-closure-every-stage-names-a-runnable-command — a +// planning-state plan-gate denial names the owned authoring channel +// (planning-write), not just the cleanup verb, in every render mode. +func TestPlanningPhaseBypassDenialNamesOwnedChannel(t *testing.T) { + finding := SafetyFinding{ + Category: "workflow-phase-bypass", Source: "planning-state", + WorkflowStage: "INVALID_STATE", NextOperation: "repair-state", + BlockingFeature: "sample-feature", + } + d := denialFor("claude", finding) + for mode, name := range map[RenderMode]string{RenderPlain: "plain", RenderMarkdown: "markdown", RenderANSI: "ansi"} { + out := d.Render(mode) + if !strings.Contains(out, "repair-state") { + t.Fatalf("%s denial dropped the recovery verb: %q", name, out) + } + if !strings.Contains(out, "planning-write --repo . --feature sample-feature --artifact ") { + t.Fatalf("%s denial must name the owned planning-write channel: %q", name, out) + } + } + // A non-planning finding must not gain the planning guidance. + other := denialFor("claude", SafetyFinding{Category: "workflow-phase-bypass", Source: "delivery-state", WorkflowStage: "BUILD", NextOperation: "plan-gate"}).Render(RenderPlain) + if strings.Contains(other, "planning-write") { + t.Fatalf("non-planning denial must not mention planning-write: %q", other) + } +} + func TestDenialReassuranceIsCategoryAware(t *testing.T) { // A blocked-before-effect denial reassures that nothing was written. tamper := denialFor("claude", SafetyFinding{Category: "workflow-state-tamper"}).Render(RenderPlain) diff --git a/boatstack/export.go b/boatstack/export.go index a7547a7..ad42e36 100644 --- a/boatstack/export.go +++ b/boatstack/export.go @@ -199,7 +199,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, 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.` + 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. Author each known planning document through ` + "`boatstack-helper planning-write`" + ` on stdin — the owned channel for ` + "`.product-loop/features/`" + `, and the writer that remains valid once the planning latch denies raw writes; use the host's own Markdown writer only where it is explicitly permitted, and never bypass the host boundary with arbitrary shell redirection.` } return fmt.Sprintf(`# %s @@ -301,7 +301,7 @@ func BuildExportBundle(configPath string, config ProjectConfig, rawConfig []byte "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.", "root-cause": "Perform failure-mode elimination on a bug, not a patch. This operation is strictly read-only: do not edit product code, create or update artifacts, advance a gate, or contact GitHub; the user supplies the symptom, stack trace, error log, or failing signal as the argument. Locate the failure below its surface symptom and classify it against the failure classes in @.product-loop/failure-moves.md; name the failure CLASS, not the one instance, and if no class fits, name the new class in that vocabulary. Investigate with read-only tools and produce a numbered root-cause chain in which every step is cited to file:line and which distinguishes the crashing frame (the victim) from the true origin (the cause); label authoritative repository facts DISCOVERED and any inference PROPOSED. State the blast radius: every other call site or path exposed to the same class. Propose the minimal STRUCTURAL elimination that makes the whole class unreachable and covers every exposed site, reusing an existing repository pattern or utility where one exists, rather than a local guard on the single line in the trace. Present this as a material product decision with the same tiered paths auto-plan uses under boundary_analysis: [1a] Symptom Patch or [1b] Programmatic Enforcement (a boundary that eliminates the class), and recommend one. Require a regression that reproduces the failure mode before the fix plus the project's own gates as the proof the class is gone, and name related latent hazards left out of scope as non-goals. Then format the result as a host Plan-mode source plan (symptom, root-cause chain, failure mode, blast radius, elimination, non-goals, verification, delivery base branch) and respond Root cause found, making the one next action: save this plan to a durable in-repo path and run auto-plan with it via --plan. Do not implement the fix; hand off to the plan gate.", - "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. When a scenario is relevant but no capability command resolves, surface a material provisioning decision with tiered paths: [1a] provision the capture capability now as its own ordered delivery slice, [1b] bundle the capture harness into the feature slice, or [1c] record the gap and defer; this is a surfaced choice, never an imposed framework. 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. If an earlier hand-authored draft was never registered and its plan cannot be verified, the guard denies every product mutation at INVALID_STATE with next operation repair-state; run repair-state to quarantine that unregistered malformed draft and return to auto-plan. It is reversible, refuses any feature carrying a plan lock, pr.md, delivery state, tracked files, or an active or published delivery, and never edits product code.", + "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. When a scenario is relevant but no capability command resolves, surface a material provisioning decision with tiered paths: [1a] provision the capture capability now as its own ordered delivery slice, [1b] bundle the capture harness into the feature slice, or [1c] record the gap and defer; this is a surfaced choice, never an imposed framework. 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. If an earlier hand-authored draft was never registered and its plan cannot be verified, the guard denies every product mutation at INVALID_STATE with next operation repair-state; run repair-state to quarantine that unregistered malformed draft and return to auto-plan, then re-author the planning Markdown through the owned planning-write channel (stdin), never a raw file write. It is reversible, refuses any feature carrying a plan lock, pr.md, delivery state, tracked files, or an active or published delivery, and never edits product code.", "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. activate-plan promotes the compiled task graph, test matrix, evidence ledger, and the plan lock together through the transactional mutation boundary as one mutation, so all four land all-or-nothing with a reversible receipt and a failed or interrupted promote leaves the prior state unchanged rather than half-written. The boundary is closed under inversion: mutation-status lists the receipts and undo --mutation reverses a managed-artifact promotion (redo is undo of the undo receipt), with undo refusing to reverse an activation once a delivery gate would be stranded; this governs Boatstack-generated artifacts only, never source code. 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.", @@ -359,7 +359,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. Write every response in Simplified Technical English: short sentences, the active voice, the present tense, one idea per sentence, the condition first, and the simple common word. -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. +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. Planning Markdown is authored through the bounded planning-write helper — the owned channel for feature artifacts, not a fallback; use the host's normal Markdown writer only where it is explicitly permitted, and arbitrary shell redirection never. 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/flow_control.go b/boatstack/flow_control.go index 08f18b2..4ac3cfe 100644 --- a/boatstack/flow_control.go +++ b/boatstack/flow_control.go @@ -45,9 +45,10 @@ func FormatFlowCheck(result deliverycontrol.CheckResult) string { // flowStateFromStage maps a read-only NextStatus.ObservedStage to a // delivery-flow StateID. It resolves ONLY the concrete slice-lifecycle stages, // where the position is unambiguous; every planning, ambiguous, or invalid stage -// returns false so callers fall back to existing behavior rather than act on a -// guessed position. This conservative mapping is what keeps flow control from -// ever interfering with pre-activation or ambiguous flows. +// returns false so the oracle never scores a guessed position. The oracle stays +// delivery-only; pre-activation stages are covered instead by prescribePlanning, +// which names the exact runnable command without ever claiming a flow state. +// control-law: prescriptive-closure-every-stage-names-a-runnable-command func flowStateFromStage(stage string) (deliverycontrol.StateID, bool) { switch stage { case "BUILD": @@ -79,6 +80,23 @@ func CurrentFlowState(repo, feature string) (deliverycontrol.StateID, bool) { return flowStateFromStage(status.ObservedStage) } +// Non-registry prescription markers. These name pre-activation and recovery +// moves the delivery oracle deliberately does not model (deliverycontrol shadows +// the DELIVERY machine only). They are never legal registry transitions, never +// allowlisted for auto-drive, and exist so a prescription's provenance is +// self-describing in JSON and telemetry. +// control-law: prescriptive-closure-every-stage-names-a-runnable-command +const ( + MarkerPlanningInit = deliverycontrol.TransitionID("planning.init") + MarkerPlanningCheckSource = deliverycontrol.TransitionID("planning.check_source_plan") + MarkerPlanningCheckPlan = deliverycontrol.TransitionID("planning.check_plan") + MarkerPlanningActivate = deliverycontrol.TransitionID("planning.activate") + MarkerPlanningWorkspace = deliverycontrol.TransitionID("planning.workspace_cut") + MarkerRecoveryDoctor = deliverycontrol.TransitionID("recovery.doctor") + MarkerRecoveryDiscard = deliverycontrol.TransitionID("recovery.discard_delivery") + MarkerRecoveryRepair = deliverycontrol.TransitionID("recovery.repair_state") +) + // FlowNext is the advisory answer for `flow next`: the current delivery-flow // state, the real recommended operation (from ResolveNext — the authoritative // next-move table), and the oracle's lowest-cost next control plus the remaining @@ -93,11 +111,18 @@ type FlowNext struct { OracleNext deliverycontrol.TransitionID `json:"oracle_next_transition,omitempty"` RemainingCost int `json:"remaining_flow_cost"` Reason string `json:"reason"` - // Prescribed is the exact runnable command for the oracle's lowest-cost next - // move. It is non-nil ONLY when the flow position resolves and the transition - // can be assembled faithfully; an unresolved position prescribes nothing rather - // than a guessed command. + // Prescribed is the exact runnable command for the next move. When the flow + // position resolves it is the oracle's lowest-cost transition; when it does + // not, it is the pre-activation prescription for the observed stage (marked by + // a planning./recovery. Transition). It is non-nil ONLY when the command can + // be assembled faithfully; otherwise nothing is prescribed rather than a + // guessed command. Prescribed *PrescribedCommand `json:"prescribed,omitempty"` + // FollowUp names the step after the prescribed pre-activation command, set + // only by the planning prescription layer (e.g. record approval after + // check-plan; re-author via planning-write after repair-state). Empty for + // oracle moves. + FollowUp string `json:"follow_up,omitempty"` // SubAction is the read-only next coding sub-action from the plan's task DAG, // surfaced only while the active slice is in BUILD (where "build" is otherwise // opaque). It is a pointer into the slice's dependency-ordered tasks; it is nil @@ -113,7 +138,10 @@ type FlowNext struct { // human/CI and must NEVER be fabricated (evidence, gate status, the human-confirmed // preview fingerprint, reviewer identity); those flags are deliberately absent from // Args. AutoDerivable is true exactly when RequiresHumanInput is empty — the only -// commands the opt-in execute driver may run. +// commands the opt-in execute driver may run. Transition is the registry +// TransitionID of an oracle move, or a planning./recovery.-prefixed marker for a +// pre-activation prescription outside the delivery model; markers never pass the +// auto-drive allowlist, so a marked prescription is always prescribe-and-stop. type PrescribedCommand struct { Verb string `json:"verb"` Args []string `json:"args,omitempty"` @@ -169,6 +197,105 @@ func prescribeCommand(repo, feature string, status NextStatus, transition delive return cmd, true } +// prescribePlanning assembles the exact runnable command for a stage the +// delivery oracle deliberately does not model: the pre-activation planning +// stages and the blocked recovery stages. It closes the prescriptive loop — +// every reachable pre-activation stage names at least one concrete command — +// without adding planning states to deliverycontrol, whose declared scope is +// the DELIVERY machine only. It returns (nil, "") exactly for the documented +// exceptions: AMBIGUOUS (choosing a feature is a human act, and the candidates +// already surface via Reason/BlockingAmbiguity) and unknown stages (never +// guess). AutoDerivable here is a rendering fact ("all arguments follow from +// state"), not an execution grant: markers are off the auto-drive allowlist +// and have no executor, so the driver always prescribes-and-stops on them. +// control-law: prescriptive-closure-every-stage-names-a-runnable-command +func prescribePlanning(repo string, status NextStatus) (*PrescribedCommand, string) { + var repoArgs []string + if repo != "" && repo != "." { + repoArgs = []string{"--repo", repo} + } + featureDir := filepath.Join(repo, ".product-loop", "features", status.Feature) + finish := func(cmd *PrescribedCommand, followUp string) (*PrescribedCommand, string) { + cmd.AutoDerivable = len(cmd.RequiresHumanInput) == 0 + return cmd, followUp + } + switch status.ObservedStage { + case "NOT_INITIALIZED": + return finish(&PrescribedCommand{ + Verb: "init", Args: repoArgs, Transition: MarkerPlanningInit, + }, "") + case "NOT_STARTED": + // The host plan path is knowable only to the host conversation; owe it. + return finish(&PrescribedCommand{ + Verb: "check-source-plan", Args: repoArgs, + RequiresHumanInput: []string{"--plan"}, + Transition: MarkerPlanningCheckSource, + }, "Then run auto-plan with the validated SOURCE_PLAN path; author every feature artifact through `boatstack-helper planning-write` (document on stdin).") + case "DRAFT_PLAN": + return finish(&PrescribedCommand{ + Verb: "check-plan", + Args: []string{"--plan", filepath.Join(featureDir, "plan.md")}, + Transition: MarkerPlanningCheckPlan, + }, "After the check passes, present the plan for approval and record it with `record-approval` using the exact PLAN_FINGERPRINT it printed.") + case "APPROVED", "POLICY_READY": + // ResolveNext already ordered the move: a fresh workspace cut when one is + // needed, otherwise activation. "build" is an operation name, not a verb; + // activate-plan is the build operation's first concrete command. + if status.NextOperation == "workspace-cut" { + return finish(&PrescribedCommand{ + Verb: "workspace-cut", + Args: append(repoArgs, "--feature", status.Feature), + Transition: MarkerPlanningWorkspace, + }, "Then activate the plan from the fresh workspace with `activate-plan`.") + } + args := []string{ + "--plan", filepath.Join(featureDir, "plan.md"), + "--out-dir", filepath.Join(featureDir, "compiled"), + "--output", filepath.Join(featureDir, "plan.lock.json"), + } + if status.ObservedStage == "APPROVED" { + args = append(args, "--approval", filepath.Join(featureDir, "approval.md")) + } + return finish(&PrescribedCommand{ + Verb: "activate-plan", Args: args, Transition: MarkerPlanningActivate, + }, "") + case "INVALID_STATE": + switch status.NextOperation { + case "doctor": + return finish(&PrescribedCommand{ + Verb: "doctor", Args: repoArgs, Transition: MarkerRecoveryDoctor, + }, "") + case "discard-delivery": + cmd := &PrescribedCommand{Verb: "discard-delivery", Args: repoArgs, Transition: MarkerRecoveryDiscard} + if len(status.BlockingAmbiguity) == 1 { + cmd.Args = append(cmd.Args, "--feature", status.BlockingAmbiguity[0]) + } else { + cmd.RequiresHumanInput = []string{"--feature"} + } + return finish(cmd, "") + case "repair-state": + // ResolveNext never routes here today; the safety finding does. Keep the + // case so any carrier of the repair-state operation gets the full loop: + // quarantine, then re-author through the owned channel. + cmd := &PrescribedCommand{Verb: "repair-state", Args: repoArgs, Transition: MarkerRecoveryRepair} + slug := status.Feature + if slug == "" && len(status.BlockingAmbiguity) == 1 { + slug = status.BlockingAmbiguity[0] + } + if slug != "" { + cmd.Args = append(cmd.Args, "--feature", slug) + } else { + slug = "" + } + return finish(cmd, fmt.Sprintf("After repair, re-author the planning Markdown through the owned channel: `boatstack-helper planning-write --repo . --feature %s --artifact ` with the document on stdin.", slug)) + } + return nil, "" + default: + // AMBIGUOUS and anything unrecognized: no prescription, never a guess. + return nil, "" + } +} + // NextControl composes the authoritative read-only recommendation (ResolveNext) // with the deterministic oracle to advise the lowest-cost next move toward a // published delivery. It performs no mutation and is safe to call at any time. @@ -189,6 +316,13 @@ func NextControl(repo, feature string) (FlowNext, error) { state, resolved = flowStateFromStage(status.ObservedStage) } if !resolved { + // Pre-activation and blocked stages sit outside the delivery oracle, but + // they still name their exact runnable command. Resolved stays false: the + // flow-state conservativeness contract is untouched. + if cmd, followUp := prescribePlanning(repo, status); cmd != nil { + out.Prescribed = cmd + out.FollowUp = followUp + } return out, nil } out.State = state @@ -230,12 +364,7 @@ func FormatFlowNext(next FlowNext) string { fmt.Fprintf(&b, "Flow state: %s -> goal %s\n", next.State, next.Goal) fmt.Fprintf(&b, "Advisory (flow oracle): next %s, remaining cost %d\n", next.OracleNext, next.RemainingCost) if next.Prescribed != nil { - fmt.Fprintf(&b, "Run: %s\n", next.Prescribed.CommandLine()) - if next.Prescribed.AutoDerivable { - fmt.Fprintf(&b, " (auto-derivable — all arguments follow from state)\n") - } else { - fmt.Fprintf(&b, " You must supply: %s (never auto-filled)\n", strings.Join(next.Prescribed.RequiresHumanInput, " ")) - } + writePrescribed(&b, next.Prescribed) } if next.SubAction != nil { title := next.SubAction.Title @@ -244,8 +373,25 @@ func FormatFlowNext(next FlowNext) string { } fmt.Fprintf(&b, "Next sub-action: %s%s (from the plan task DAG; see `flow tasks`)\n", next.SubAction.ID, title) } + } else if next.Prescribed != nil { + fmt.Fprintf(&b, "Flow state: pre-activation (delivery oracle not engaged)\n") + writePrescribed(&b, next.Prescribed) + if next.FollowUp != "" { + fmt.Fprintf(&b, "Then: %s\n", next.FollowUp) + } } else { - fmt.Fprintf(&b, "Flow state: unresolved (no oracle advisory)\n") + fmt.Fprintf(&b, "Flow state: unresolved (no oracle advisory; follow the recommended operation above)\n") } return b.String() } + +// writePrescribed renders the Run line and its owed-input annotation for a +// prescribed command, shared by the oracle and pre-activation branches. +func writePrescribed(b *strings.Builder, p *PrescribedCommand) { + fmt.Fprintf(b, "Run: %s\n", p.CommandLine()) + if p.AutoDerivable { + fmt.Fprintf(b, " (auto-derivable — all arguments follow from state)\n") + } else { + fmt.Fprintf(b, " You must supply: %s (never auto-filled)\n", strings.Join(p.RequiresHumanInput, " ")) + } +} diff --git a/boatstack/flow_control_test.go b/boatstack/flow_control_test.go index d8e2cbc..ac2b9ba 100644 --- a/boatstack/flow_control_test.go +++ b/boatstack/flow_control_test.go @@ -1,6 +1,7 @@ package boatstack import ( + "strings" "testing" "github.com/operatorstack/boatstack/boatstack/internal/deliverycontrol" @@ -69,7 +70,8 @@ func TestNextControlAdvisesFromReviewPassed(t *testing.T) { } // A stage that is not a concrete slice-lifecycle position must resolve to unknown -// so callers fall back rather than act on a guess. +// so callers fall back rather than act on a guess. Pre-activation stages are +// covered by prescribePlanning instead — a prescription, never a flow state. func TestFlowStateFromStageIsConservative(t *testing.T) { for _, stage := range []string{"NOT_STARTED", "AMBIGUOUS", "INVALID_STATE", "POLICY_READY", "DRAFT_PLAN", ""} { if _, ok := flowStateFromStage(stage); ok { @@ -77,3 +79,27 @@ func TestFlowStateFromStageIsConservative(t *testing.T) { } } } + +// A pre-activation stage keeps Resolved=false (flow-state conservativeness) yet +// still names its exact runnable command, and the rendering shows the honest +// pre-activation label rather than the bare unresolved line. +// control-law: prescriptive-closure-every-stage-names-a-runnable-command +func TestNextControlPrescribesWithoutResolvingBeforeActivation(t *testing.T) { + repo := nextTestRepo(t) + writeSavedFeaturePlan(t, repo, "demo") + + next, err := NextControl(repo, "") + if err != nil { + t.Fatalf("NextControl: %v", err) + } + if next.Resolved { + t.Fatal("pre-activation must never resolve a flow state") + } + if next.Prescribed == nil || next.Prescribed.Verb != "check-plan" { + t.Fatalf("DRAFT_PLAN must prescribe check-plan: %+v", next.Prescribed) + } + out := FormatFlowNext(next) + if !strings.Contains(out, "pre-activation (delivery oracle not engaged)") || !strings.Contains(out, "Run: boatstack-helper check-plan") { + t.Fatalf("pre-activation rendering must label the state and carry the Run line: %q", out) + } +} diff --git a/boatstack/flow_planning_prescribe_conformance_test.go b/boatstack/flow_planning_prescribe_conformance_test.go new file mode 100644 index 0000000..984dc33 --- /dev/null +++ b/boatstack/flow_planning_prescribe_conformance_test.go @@ -0,0 +1,317 @@ +package boatstack + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/operatorstack/boatstack/boatstack/internal/deliverycontrol" +) + +// control-law: prescriptive-closure-every-stage-names-a-runnable-command +// +// `flow next` used to go quiet for every pre-activation stage: the delivery +// oracle deliberately does not model planning, so the advisory printed only a +// soft operation name and no runnable command, and a denied raw write named +// only the cleanup verb. The contract these tests hold: every ObservedStage +// ResolveNext can emit either resolves through the delivery oracle, or is +// prescribed a concrete runnable command by prescribePlanning, or sits on an +// explicit documented exception list — and the prescriptions never fabricate +// human-owed input, never become legal delivery moves, and never auto-execute. + +// planningExceptions are the stages that deliberately prescribe nothing. +// Adding a stage to ResolveNext without a prescription rule or an entry here +// fails the totality sweep below. +var planningExceptions = map[string]string{ + "AMBIGUOUS": "choosing between candidate features/deliveries is a human act; candidates surface via Reason/BlockingAmbiguity", +} + +// planningStages is one representative synthetic NextStatus per pre-activation +// stage (INVALID_STATE once per NextOperation route ResolveNext or the safety +// finding can carry). +var planningStages = []NextStatus{ + {ObservedStage: "NOT_INITIALIZED", NextOperation: "init"}, + {ObservedStage: "NOT_STARTED", NextOperation: "auto-plan"}, + {ObservedStage: "DRAFT_PLAN", NextOperation: "plan-gate", Feature: "demo"}, + {ObservedStage: "APPROVED", NextOperation: "build", Feature: "demo"}, + {ObservedStage: "APPROVED", NextOperation: "workspace-cut", Feature: "demo"}, + {ObservedStage: "POLICY_READY", NextOperation: "build", Feature: "demo"}, + {ObservedStage: "POLICY_READY", NextOperation: "workspace-cut", Feature: "demo"}, + {ObservedStage: "INVALID_STATE", NextOperation: "doctor"}, + {ObservedStage: "INVALID_STATE", NextOperation: "discard-delivery", BlockingAmbiguity: []string{"stale"}}, + {ObservedStage: "INVALID_STATE", NextOperation: "repair-state", Feature: "demo"}, +} + +// Positive: each pre-activation stage reached through the real read-only +// projection (NextControl end to end) yields the exact runnable command. +func TestNextControlPrescribesPreActivationStages(t *testing.T) { + t.Run("not_initialized", func(t *testing.T) { + repo := t.TempDir() + if output, err := exec.Command("git", "-C", repo, "init").CombinedOutput(); err != nil { + t.Fatalf("git init: %v: %s", err, output) + } + next, err := NextControl(repo, "") + if err != nil { + t.Fatal(err) + } + if next.Resolved { + t.Fatalf("pre-activation must not resolve a flow state: %+v", next) + } + if next.Prescribed == nil || next.Prescribed.Verb != "init" { + t.Fatalf("NOT_INITIALIZED must prescribe init: %+v", next.Prescribed) + } + }) + + t.Run("not_started_owes_plan_path", func(t *testing.T) { + repo := nextTestRepo(t) + next, err := NextControl(repo, "") + if err != nil { + t.Fatal(err) + } + p := next.Prescribed + if p == nil || p.Verb != "check-source-plan" { + t.Fatalf("NOT_STARTED must prescribe check-source-plan: %+v", p) + } + if p.AutoDerivable || len(p.RequiresHumanInput) != 1 || p.RequiresHumanInput[0] != "--plan" { + t.Fatalf("the host plan path is unknowable and must be owed, never fabricated: %+v", p) + } + if !strings.Contains(next.FollowUp, "planning-write") { + t.Fatalf("the planning follow-up must name the owned authoring channel: %q", next.FollowUp) + } + }) + + t.Run("draft_plan_prescribes_check_plan_on_real_path", func(t *testing.T) { + repo := nextTestRepo(t) + writeSavedFeaturePlan(t, repo, "demo") + next, err := NextControl(repo, "") + if err != nil { + t.Fatal(err) + } + p := next.Prescribed + if p == nil || p.Verb != "check-plan" || !p.AutoDerivable { + t.Fatalf("DRAFT_PLAN must prescribe an auto-derivable check-plan: %+v", p) + } + planPath := "" + for i, arg := range p.Args { + if arg == "--plan" && i+1 < len(p.Args) { + planPath = p.Args[i+1] + } + } + if planPath == "" { + t.Fatalf("check-plan prescription carries no --plan: %+v", p.Args) + } + if _, err := os.Stat(planPath); err != nil { + t.Fatalf("prescribed --plan does not point at the saved plan: %v", err) + } + if !strings.Contains(next.FollowUp, "record-approval") { + t.Fatalf("DRAFT_PLAN follow-up must route to record-approval: %q", next.FollowUp) + } + }) + + t.Run("approved_prescribes_activate_plan_with_approval", func(t *testing.T) { + repo := nextTestRepo(t) + writeSavedFeaturePlan(t, repo, "demo") + approval := filepath.Join(repo, ".product-loop", "features", "demo", "approval.md") + if err := os.WriteFile(approval, []byte("approved\n"), 0o644); err != nil { + t.Fatal(err) + } + next, err := NextControl(repo, "") + if err != nil { + t.Fatal(err) + } + p := next.Prescribed + if p == nil || p.Verb != "activate-plan" || !p.AutoDerivable { + t.Fatalf("APPROVED must prescribe an auto-derivable activate-plan: %+v", p) + } + joined := strings.Join(p.Args, " ") + for _, want := range []string{"--plan", "--out-dir", "--output", "--approval"} { + if !strings.Contains(joined, want) { + t.Fatalf("activate-plan prescription missing %s: %q", want, joined) + } + } + }) + + t.Run("policy_ready_prescribes_activate_plan_without_approval", func(t *testing.T) { + repo := nextTestRepo(t) + config := testConfig() + config.Workflow.HumanPlanApproval = false + value, err := MarshalJSON(config) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(repo, ".product-loop", "project.json"), value, 0o644); err != nil { + t.Fatal(err) + } + writeSavedFeaturePlan(t, repo, "demo") + next, err := NextControl(repo, "") + if err != nil { + t.Fatal(err) + } + p := next.Prescribed + if p == nil || p.Verb != "activate-plan" { + t.Fatalf("POLICY_READY must prescribe activate-plan: %+v", p) + } + if strings.Contains(strings.Join(p.Args, " "), "--approval") { + t.Fatalf("policy activation must not reference an approval receipt: %+v", p.Args) + } + }) + + t.Run("invalid_state_prescribes_discard_delivery", func(t *testing.T) { + repo := nextTestRepo(t) + directory := filepath.Join(repo, ".product-loop", "features", "orphan") + if err := os.MkdirAll(directory, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(directory, "pr.md"), []byte("# Preview\n"), 0o644); err != nil { + t.Fatal(err) + } + next, err := NextControl(repo, "") + if err != nil { + t.Fatal(err) + } + p := next.Prescribed + if p == nil || p.Verb != "discard-delivery" { + t.Fatalf("INVALID_STATE orphan must prescribe discard-delivery: %+v", p) + } + if !strings.Contains(strings.Join(p.Args, " "), "--feature orphan") { + t.Fatalf("a unique blocker derives --feature: %+v", p.Args) + } + }) +} + +// Relation/Totality: every ObservedStage ResolveNext can emit is oracle-owned, +// prescribed, or a documented exception. A new stage fails here until it gets a +// rule or an exception entry. +func TestEveryObservedStageIsPrescribedOrExcepted(t *testing.T) { + prescribed := map[string]bool{} + for _, status := range planningStages { + if cmd, _ := prescribePlanning(".", status); cmd != nil { + prescribed[status.ObservedStage] = true + } + } + allStages := []string{ + "NOT_INITIALIZED", "NOT_STARTED", "DRAFT_PLAN", "APPROVED", "POLICY_READY", + "AMBIGUOUS", "INVALID_STATE", + "BUILD", "TEST_PASSED", "REVIEW_PASSED", "PR_PREVIEW", "PUBLISHED", "FEATURE_COMPLETE", + } + for _, stage := range allStages { + if _, oracle := flowStateFromStage(stage); oracle { + continue + } + if prescribed[stage] { + continue + } + if reason, ok := planningExceptions[stage]; ok { + if reason == "" { + t.Fatalf("exception for %s must carry a reason", stage) + } + continue + } + t.Errorf("stage %s is neither oracle-owned, prescribed, nor a documented exception", stage) + } +} + +// Negative: prescriptions never fabricate human-owed input, and AutoDerivable +// is exactly the absence of owed input. +func TestPlanningPrescriptionsNeverFabricateHumanInput(t *testing.T) { + for _, status := range planningStages { + cmd, _ := prescribePlanning(".", status) + if cmd == nil { + continue + } + if cmd.AutoDerivable != (len(cmd.RequiresHumanInput) == 0) { + t.Errorf("%s/%s: AutoDerivable must equal owed-input emptiness: %+v", status.ObservedStage, status.NextOperation, cmd) + } + for _, owed := range cmd.RequiresHumanInput { + for _, arg := range cmd.Args { + if arg == owed { + t.Errorf("%s/%s: owed flag %s must never appear in Args: %+v", status.ObservedStage, status.NextOperation, owed, cmd.Args) + } + } + } + } + // A non-unique blocker cannot derive --feature; it must be owed. + cmd, _ := prescribePlanning(".", NextStatus{ + ObservedStage: "INVALID_STATE", NextOperation: "discard-delivery", + BlockingAmbiguity: []string{"one", "two"}, + }) + if cmd == nil || cmd.AutoDerivable || len(cmd.RequiresHumanInput) != 1 || cmd.RequiresHumanInput[0] != "--feature" { + t.Fatalf("ambiguous discard-delivery must owe --feature: %+v", cmd) + } +} + +// Bypass: planning/recovery markers can never reach the delivery machine or +// the execute driver — a marked prescription is always prescribe-and-stop. +func TestPlanningMarkersCannotReachDeliveryMachineOrDriver(t *testing.T) { + graph := deliverycontrol.RegistryGraph(deliverycontrol.DefaultFlowCostWeights()) + states := []deliverycontrol.StateID{ + deliverycontrol.StateUninitialized, deliverycontrol.StatePending, + deliverycontrol.StateBuild, deliverycontrol.StateTestPassed, + deliverycontrol.StateReviewPassed, deliverycontrol.StatePublished, + } + for _, status := range planningStages { + cmd, _ := prescribePlanning(".", status) + if cmd == nil { + continue + } + if _, ok := deliverycontrol.Transition(cmd.Transition); ok { + t.Errorf("marker %s must not be a registry transition", cmd.Transition) + } + for _, state := range states { + if graph.IsLegalMove(state, cmd.Transition) { + t.Errorf("marker %s must not be a legal move from %s", cmd.Transition, state) + } + } + decision := decideDrive(FlowNext{Prescribed: cmd}, true, false, autoDrivableTransitions) + if decision.Action != DrivePrescribe { + t.Errorf("marker %s must prescribe-and-stop under --execute, got %s", cmd.Transition, decision.Action) + } + } +} + +// Failure-state: the repair loop names the owned authoring channel; ambiguous +// and unknown stages prescribe nothing rather than a guess, and the rendering +// still routes to the recommended operation. +func TestPlanningPrescriptionFailureStates(t *testing.T) { + t.Run("repair_follow_up_names_planning_write", func(t *testing.T) { + cmd, followUp := prescribePlanning(".", NextStatus{ + ObservedStage: "INVALID_STATE", NextOperation: "repair-state", Feature: "demo", + }) + if cmd == nil || cmd.Verb != "repair-state" { + t.Fatalf("repair-state route must be prescribed: %+v", cmd) + } + if !strings.Contains(followUp, "planning-write --repo . --feature demo") { + t.Fatalf("repair follow-up must name planning-write for the feature: %q", followUp) + } + }) + + t.Run("unknown_and_ambiguous_prescribe_nothing", func(t *testing.T) { + for _, stage := range []string{"AMBIGUOUS", "SOMETHING_NEW", ""} { + if cmd, followUp := prescribePlanning(".", NextStatus{ObservedStage: stage}); cmd != nil || followUp != "" { + t.Fatalf("stage %q must prescribe nothing: %+v %q", stage, cmd, followUp) + } + } + }) + + t.Run("ambiguous_render_keeps_operation_fallback", func(t *testing.T) { + repo := nextTestRepo(t) + writeSavedFeaturePlan(t, repo, "plan-one") + writeSavedFeaturePlan(t, repo, "plan-two") + next, err := NextControl(repo, "") + if err != nil { + t.Fatal(err) + } + if next.Resolved || next.Prescribed != nil { + t.Fatalf("AMBIGUOUS must stay unresolved and unprescribed: %+v", next) + } + out := FormatFlowNext(next) + if !strings.Contains(out, "follow the recommended operation above") { + t.Fatalf("ambiguous rendering must route to the recommended operation: %q", out) + } + if strings.Contains(out, "Run: ") { + t.Fatalf("ambiguous rendering must not fabricate a command: %q", out) + } + }) +} diff --git a/boatstack/references/workflow.md b/boatstack/references/workflow.md index 13241ba..6ad3cb2 100644 --- a/boatstack/references/workflow.md +++ b/boatstack/references/workflow.md @@ -255,6 +255,8 @@ If gstack is installed, its review skills can execute these lenses. If Spec Kit `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. +Feature artifacts are authored through the owned channel `boatstack-helper planning-write --repo . --feature --artifact ` (Markdown on stdin). The host's own Markdown writer is permitted only where the host allows it; arbitrary shell redirection never is. + Validation must be derived before implementation. Each check records: - `run`: an executable command or a specific human/external procedure; @@ -305,7 +307,7 @@ is read from the pointer and slice status, never from `pr_state`. Missing required human approval, unresolved `blocking_questions`, or any change to the source plan, spec, complete `plan.md`, or displayed product baseline blocks activation and returns the feature to `PLAN_GATE`. Existing schema-v1 approval receipts remain valid only with a clean product baseline. A failed or partial compilation never creates a valid lock. Existing schema-v1 human locks remain valid; policy activation always writes schema v2. -After `auto-plan` successfully saves a feature plan, managed authority is latched before activation. Reads and bounded Markdown planning transitions remain available, but native edits, mutation-capable MCP tools, and shell commands not proven read-only are denied until activation creates a current lock. Approval itself does not authorize product edits. Ambiguous, stale, malformed, or unverifiable phase state fails closed with one recovery operation; repositories with no saved managed plan retain ordinary unmanaged behavior. +After `auto-plan` successfully saves a feature plan, managed authority is latched before activation. Reads and bounded Markdown planning transitions remain available — `planning-write` is the channel that stays open for authoring planning Markdown while the latch holds — but native edits, mutation-capable MCP tools, and shell commands not proven read-only are denied until activation creates a current lock. Approval itself does not authorize product edits. Ambiguous, stale, malformed, or unverifiable phase state fails closed with one recovery operation; repositories with no saved managed plan retain ordinary unmanaged behavior. ### `PLAN_LOCKED -> BUILD` diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 0a657f7..e699801 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -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 **20143 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 **20237 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 [`2364eaedaefbe73d8996108dfac261c38b0ce2b3`](https://github.com/operatorstack/intelligence-flow/tree/2364eaedaefbe73d8996108dfac261c38b0ce2b3/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 [`2126fde051cc11a8c3de9fff4fc17b5397240383`](https://github.com/operatorstack/intelligence-flow/tree/2126fde051cc11a8c3de9fff4fc17b5397240383/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/public-claims.json b/docs/public-claims.json index bb59722..4d8a79f 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "2364eaedaefbe73d8996108dfac261c38b0ce2b3", + "source_commit": "2126fde051cc11a8c3de9fff4fc17b5397240383", "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:2364eaedaefbe73d8996108dfac261c38b0ce2b3" + "last_verified_version": "source:2126fde051cc11a8c3de9fff4fc17b5397240383" }, { "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:2364eaedaefbe73d8996108dfac261c38b0ce2b3" + "last_verified_version": "source:2126fde051cc11a8c3de9fff4fc17b5397240383" }, { "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:2364eaedaefbe73d8996108dfac261c38b0ce2b3" + "last_verified_version": "source:2126fde051cc11a8c3de9fff4fc17b5397240383" }, { "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:2364eaedaefbe73d8996108dfac261c38b0ce2b3" + "last_verified_version": "source:2126fde051cc11a8c3de9fff4fc17b5397240383" }, { "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:2364eaedaefbe73d8996108dfac261c38b0ce2b3" + "last_verified_version": "source:2126fde051cc11a8c3de9fff4fc17b5397240383" }, { "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:2364eaedaefbe73d8996108dfac261c38b0ce2b3" + "last_verified_version": "source:2126fde051cc11a8c3de9fff4fc17b5397240383" }, { "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:2364eaedaefbe73d8996108dfac261c38b0ce2b3" + "last_verified_version": "source:2126fde051cc11a8c3de9fff4fc17b5397240383" }, { "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:2364eaedaefbe73d8996108dfac261c38b0ce2b3" + "last_verified_version": "source:2126fde051cc11a8c3de9fff4fc17b5397240383" }, { "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:2364eaedaefbe73d8996108dfac261c38b0ce2b3" + "last_verified_version": "source:2126fde051cc11a8c3de9fff4fc17b5397240383" }, { "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:2364eaedaefbe73d8996108dfac261c38b0ce2b3" + "last_verified_version": "source:2126fde051cc11a8c3de9fff4fc17b5397240383" }, { "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:2364eaedaefbe73d8996108dfac261c38b0ce2b3" + "last_verified_version": "source:2126fde051cc11a8c3de9fff4fc17b5397240383" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 5123418..eb11f1b 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": "2364eaedaefbe73d8996108dfac261c38b0ce2b3", + "source_commit": "2126fde051cc11a8c3de9fff4fc17b5397240383", "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-27-prescriptive-planning-closure.md b/release-notes/2026-07-27-prescriptive-planning-closure.md new file mode 100644 index 0000000..8e1ef57 --- /dev/null +++ b/release-notes/2026-07-27-prescriptive-planning-closure.md @@ -0,0 +1,7 @@ +### Planning stages now name their next command + +Before activation, `flow next` used to go quiet: the delivery oracle deliberately does not model planning, so the advisory printed only a soft operation name and no runnable command. An agent authoring a feature plan could drift to raw file writes, land in INVALID_STATE, and loop between rewriting and `repair-state` — because the denial named only the cleanup verb, never the correct authoring channel. + +Every pre-activation stage now prescribes its exact runnable command: `check-source-plan` when nothing is started, `check-plan` on a saved draft, `activate-plan` (or `workspace-cut`) once approved, and the matching recovery verb when state is invalid. The flow oracle itself is unchanged and still scores delivery stages only; prescriptions carry explicit `planning.`/`recovery.` markers that can never become delivery moves or auto-execute. + +Plan-gate denials now also name the owned authoring channel: after `repair-state`, re-author planning Markdown through `boatstack-helper planning-write` on stdin. The skill and workflow guidance were inverted to match — `planning-write` is the primary writer for feature artifacts, not a fallback for blocked hosts — so the correct move is prescribed up front instead of being discoverable only by failing.