From 3ccda0d8fb9a31db207821eff4dc34a7e2f509ac Mon Sep 17 00:00:00 2001 From: "operator-stack-publisher[bot]" Date: Fri, 24 Jul 2026 12:48:54 +0000 Subject: [PATCH] Sync Boatstack from Intelligence Flow Labs @ 4dcec85043e6 --- CONTRIBUTING.md | 2 +- UPSTREAM.json | 30 +-- boatstack/cmd/boatstack-helper/main.go | 32 ++- boatstack/export.go | 2 +- boatstack/recovery.go | 191 ++++++++++++++++ boatstack/references/failure-moves.md | 1 + boatstack/references/workflow.md | 2 + boatstack/repair_state_test.go | 208 ++++++++++++++++++ boatstack/safety.go | 8 + docs/evidence-engineered-coding.md | 4 +- docs/public-claims.json | 24 +- labs/diagram-json/plan.lock.json | 2 +- .../2026-07-24-repair-state-recovery.md | 7 + 13 files changed, 481 insertions(+), 32 deletions(-) create mode 100644 boatstack/repair_state_test.go create mode 100644 release-notes/2026-07-24-repair-state-recovery.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d1759a..bfeeacb 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/5227e7828d9bcac8c6b11c19289c2b4ef5be92a2/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/4dcec85043e6fec7558e7bdff97cb2babe3af7c2/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 9b9ba0f..cc211b9 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -1,7 +1,7 @@ { "canonical_context": { - "characters": 60577, - "estimated_tokens": 15145, + "characters": 62113, + "estimated_tokens": 15529, "estimator": "ceil(total characters / 4); compactness signal, not provider billing", "files": [ "product-engineering-loop/references/workflow.md", @@ -12,7 +12,7 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "77a60a6dbf5dd2f29aefc942283201f8c48f45ed72a3caf105065ad3322ffcd6", + "CONTRIBUTING.md": "89b36205a93d738d4052c1269e8994076c08526a29eb645a90fe869e85514981", "README.md": "125b47671a68556df382f19756fb61fa18925606cbbaf54d6bc9df8872b36870", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", @@ -40,7 +40,7 @@ "boatstack/capture_test.go": "63fa1177738081f1e862364d7a4257f5e259f8e9c36276ba1775b8085b277105", "boatstack/changelog.go": "c5e1f31440b44d61e6037ad27af0333540af3545d655e35819a0241cbbebd8ec", "boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7", - "boatstack/cmd/boatstack-helper/main.go": "545c2926f0d9ae59ca884c99880ae30188a7ee8456a0253a10c83651f385f47c", + "boatstack/cmd/boatstack-helper/main.go": "a094a0be3b64ad4626edf9e73e31cf89e707e0bfaa0f7af89ee49f3381a7e6ad", "boatstack/cmd/boatstack-helper/main_test.go": "ff73003b6a5157202fa09ddf1129fb13c3d79702b2e05a8721ce5a11bf5ab779", "boatstack/command.go": "4726ac515dedab4947be7eb48f88c6cb8b53d674124504b69f03e6396b080ee8", "boatstack/command_test.go": "9f707abba3640add81c3e97ba7e72fedbf98f3394b1c060a9ca4b4a28e919968", @@ -50,7 +50,7 @@ "boatstack/delivery.go": "ea53af0e702ec3668a563a5f786dcac2e095362285ca6093b7ed71ec495a0a48", "boatstack/delivery_test.go": "564ad2029a8412de7953967b1acdf377e6c87b6f6e3465d1f8741a4813f2949f", "boatstack/evidence.go": "497a31e6ff632cb1d7c3adfc9f269af3f6aa84e948dd5d417c162767542a27df", - "boatstack/export.go": "f4393643206eaa503dadb1a3f58069e2a3acc9525466334ce674bcffcabc7cc7", + "boatstack/export.go": "228d193a1dd6c506600c29f29387761ffdac8a32acc3921c8825013212395f3d", "boatstack/export_test.go": "67eb890728d20630925d6e4e90d2a97ec025195ba5c54994c1b098ab72721dca", "boatstack/go.mod": "6086ef1b2a83f5696190dca692c653925f27b61f652f659fd3fca43ed54a1641", "boatstack/go.sum": "26c315c867b11b886f3c9402fce7f341f6a9115a5d61f54afbb5e1b1fb5f6017", @@ -79,23 +79,24 @@ "boatstack/provision.go": "4882d49681f99b11ba9d182ca13772131b7f9a11a6c2b560800654ca14f5111e", "boatstack/provision_test.go": "214e9edb991a66d5bbb696a7c1b63876d2f799f2cab4e3f40785f4e8f1eac57b", "boatstack/publication_ignored_repro_test.go": "b6f3aeb8ba22949ff9af7ac5afe8fb828385d9708d5d5893ef41f33a3de873e1", - "boatstack/recovery.go": "dd816b18b54a0085b8d8276a93ee98d2b1e90099059a0d85cf6e24edf6f37d5b", + "boatstack/recovery.go": "c8b2064791eab066311dad3f12a9143d312a8daefd8cc3670ff4095899a8a195", "boatstack/recovery_test.go": "29490e7477ba602491330036a491289dd9117b99ff862f66dae421ba17e04c9f", "boatstack/references/artifacts.md": "5fa888ac519085d65cee1d04df5902761651bcf2d7af81711fa0f8ecd1fc0f59", "boatstack/references/config-schema.md": "0170b90f1d0a592f58e255ffeff642fa037676042443f74a0f1b6e39be5dbbb8", - "boatstack/references/failure-moves.md": "34a39aefb282b1b5d9ea387f8536bdc5e0145a240f102ae3d01c7ada6d4abebc", + "boatstack/references/failure-moves.md": "03eb5d2c11fd1b58bcee43d96626d45ffc466e785926c5ef0b73e351ffdc87ec", "boatstack/references/host-hook-contracts.md": "d68ae1556e7b1e29e9ac7cb4db767809d510aabf0be52e60e44665ea7abb980e", "boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441", "boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae", - "boatstack/references/workflow.md": "87692bdf6b6830bcb68ed052e2b88a8f21d755c0b6be5ef03787fb4669d572fd", + "boatstack/references/workflow.md": "b38abdf69533f75471278c9fa69fd8f0e6b744ee891cf5b3223e92e75b4e4bbb", "boatstack/release.go": "82dcb4ca59e8c79a68d5333d650f90e64abd448d04e0c6f504fdf07f42b5ed76", "boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690", + "boatstack/repair_state_test.go": "f3779ac47c3db3927175a545728d3b2e020dbc85f41394d8235753b52afc3739", "boatstack/run.go": "74967ad5b3ed3847baffec1231aae69a81a70f9cce3a9412fa05bcfdc4eca6d1", "boatstack/run_test.go": "5b291510fa90cefdc26eb89e18a3443385456a6ebc73408325ac1945b7c084d6", "boatstack/runtime.go": "d1e95895002ea2b27199b6e05b33c4c6e20f63455a44f63ca3cfeedecfc23420", "boatstack/runtime_cache.go": "60c4eb0c7dde91d40d6ef3f05adc1a1282d17ff1ca12470d0a008454f7ca7489", "boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85", - "boatstack/safety.go": "b0fb94bc802baf39bc39d09276e1f0eb76a39027a421b0dfd49188173571bfe8", + "boatstack/safety.go": "7f914279b3900d7357e965322a943263713b1fdfb24c1e0a3b9942f69c126ae3", "boatstack/safety_test.go": "01f28bc3bfcb6bdd47b307e309e36bbc1921b6426ad0b777d81fe4131200c37e", "boatstack/skill_frontmatter.go": "73364df463ce828c2d005aab55f72bb92f7a34d99cf3f53d4e0cd5a4da9dbd0e", "boatstack/skill_frontmatter_test.go": "a3ec52e7df357a72265c95dd66db15d9c0effc7e5f90f14ce69c27792ce394eb", @@ -118,10 +119,10 @@ "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", "docs/configuration.md": "df054f49d532c8b1b7d94184810d1b3b5bf18cdc30eb985b4b6d0639162e341a", - "docs/evidence-engineered-coding.md": "ae3c106e9d1b04ea97cda781048a7dd2641104662aa2f8b7c05356c7fcfcaa31", + "docs/evidence-engineered-coding.md": "5babc3a8484d8b17a11e9c8014ac13837c6f12489211cb822048d9e1017892e5", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", "docs/getting-started.md": "f314270c5ed1a55bbef5f3ddbcb5596693dbee9374e5f0d3df8838cefbd68052", - "docs/public-claims.json": "9ba4aa7c99afee1b30ee1ac75e32c4d3e8724fbfbbb6142957407ae2165de27f", + "docs/public-claims.json": "7e57c6835d1b9244be1afb60f0ec5498b68b6ae147863391a1793d41e1f2056a", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -135,7 +136,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": "5a5e68c8129157e18219f5ce21ba319234d4ec11572ee9196cb31f6ec9843b96", + "labs/diagram-json/plan.lock.json": "4364d0ec4235fbff6448066adc1e12f3403d9018b146d0438e633ea9e2732386", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -208,12 +209,13 @@ "release-notes/2026-07-23-shipped-feature-candidate-resolution.md": "bd8ee8e7f3f216b356b121a83ef10cb0c8131a90b9ab803edebf23b882d9cf89", "release-notes/2026-07-23-sync-title-contract.md": "2869d6d084ea60402e57ffe985d0fc4cd83ef9bb09958cc53e349157d3383202", "release-notes/2026-07-23-visual-evidence-external-host.md": "09edbe5e6e1bfc866cf5ee744a5001d678f7a67f0f330cf43bd5157eedf04276", - "release-notes/2026-07-24-ignored-deliveries-publication-authority.md": "a25f8469316276101490c79a57c1a236c18072dfbb23682bb1778871d247067d" + "release-notes/2026-07-24-ignored-deliveries-publication-authority.md": "a25f8469316276101490c79a57c1a236c18072dfbb23682bb1778871d247067d", + "release-notes/2026-07-24-repair-state-recovery.md": "daaa12deb51a5f647178d6164ea5b4bcd29bf5482b77d90002429f69e0da5dd0" }, "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "5227e7828d9bcac8c6b11c19289c2b4ef5be92a2", + "commit": "4dcec85043e6fec7558e7bdff97cb2babe3af7c2", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go index 146e01c..aba7e00 100644 --- a/boatstack/cmd/boatstack-helper/main.go +++ b/boatstack/cmd/boatstack-helper/main.go @@ -614,6 +614,34 @@ func recoveryStatusCommand(arguments []string) int { return 0 } +func repairStateCommand(arguments []string) int { + flags := flag.NewFlagSet("repair-state", flag.ContinueOnError) + repo := flags.String("repo", ".", "repository whose stuck feature draft should be repaired") + feature := flags.String("feature", "", "optional specific feature draft to repair") + jsonOutput := flags.Bool("json", false, "print the versioned structured repair decision") + if err := flags.Parse(arguments); err != nil { + return 2 + } + result, err := boatstack.RepairState(*repo, *feature) + if err != nil { + return fail(err) + } + if *jsonOutput { + value, marshalErr := boatstack.MarshalJSON(result) + if marshalErr != nil { + return fail(marshalErr) + } + fmt.Print(string(value)) + } else { + fmt.Printf("Repair: %s\nFeature: %s\nAction: %s\nQuarantine: %s\nNext operation: %s\nReason: %s\n", + result.VerificationStatus, result.Feature, result.Action, result.QuarantinePath, result.NextOperation, result.Reason) + } + if result.VerificationStatus == "BLOCKED" { + return 1 + } + return 0 +} + func runPreflightCommand(arguments []string) int { flags := flag.NewFlagSet("run-preflight", flag.ContinueOnError) repo := flags.String("repo", ".", "repository whose Git state should be verified before boatstack run") @@ -1046,7 +1074,7 @@ func workspaceSyncCommand(arguments []string) int { func run() int { if len(os.Args) < 2 { - fmt.Fprintln(os.Stderr, "usage: boatstack-helper ") + fmt.Fprintln(os.Stderr, "usage: boatstack-helper ") return 2 } switch os.Args[1] { @@ -1086,6 +1114,8 @@ func run() int { return nextStatusCommand(os.Args[2:]) case "recovery-status": return recoveryStatusCommand(os.Args[2:]) + case "repair-state": + return repairStateCommand(os.Args[2:]) case "run-preflight": return runPreflightCommand(os.Args[2:]) case "record-change": diff --git a/boatstack/export.go b/boatstack/export.go index 8bf4671..e3bb24e 100644 --- a/boatstack/export.go +++ b/boatstack/export.go @@ -292,7 +292,7 @@ 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, 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. 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.", + "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.", "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.", diff --git a/boatstack/recovery.go b/boatstack/recovery.go index a8dc2e8..f8554e3 100644 --- a/boatstack/recovery.go +++ b/boatstack/recovery.go @@ -9,6 +9,7 @@ import ( "sort" "strconv" "strings" + "time" ) const recoveryStatusSchemaVersion = 1 @@ -361,3 +362,193 @@ func ResolveRecovery(options RecoveryStatusOptions) (RecoveryStatus, error) { } return status, nil } + +const repairStateSchemaVersion = 1 + +// RepairStateResult is the host-neutral outcome of the bounded recovery that +// clears a workflow stuck at INVALID_STATE because of a hand-authored, +// unregistered, malformed feature draft. It carries no authority to edit, +// approve, or publish product code; its only mutation is to quarantine one such +// draft directory so planning can restart cleanly. +type RepairStateResult struct { + SchemaVersion int `json:"schema_version"` + VerificationStatus string `json:"verification_status"` // VERIFIED | BLOCKED | UNVERIFIED + Feature string `json:"feature,omitempty"` + Action string `json:"action"` // quarantined | none | refused + QuarantinePath string `json:"quarantine_path,omitempty"` + NextOperation string `json:"next_operation"` + Reason string `json:"reason"` + Blockers []string `json:"blockers,omitempty"` +} + +func refusedRepairState(feature, reason string, blockers ...string) RepairStateResult { + return RepairStateResult{ + SchemaVersion: repairStateSchemaVersion, VerificationStatus: "BLOCKED", + Feature: feature, Action: "refused", NextOperation: "none", Reason: reason, Blockers: blockers, + } +} + +// RepairState inspects one unregistered, malformed feature draft directory and +// quarantines it so the workflow can return to auto-plan. It NEVER touches a +// directory that carries durable authority: any plan.lock.json, pr.md, managed +// delivery state, git-tracked file, or active/published delivery causes a +// refusal. When feature is empty it resolves the sole malformed unregistered +// candidate and blocks if more than one is present. +func RepairState(repoPath, feature string) (RepairStateResult, error) { + repo, err := ResolveRepository(repoPath) + if err != nil { + return RepairStateResult{}, err + } + feature = strings.TrimSpace(feature) + if feature == "" { + candidates, candErr := featurePlanCandidates(repo) + if candErr != nil { + return RepairStateResult{}, candErr + } + eligible := []string{} + for _, candidate := range candidates { + if _, checkErr := CheckPlan(filepath.Join(repo, ".product-loop", "features", candidate, "plan.md")); checkErr != nil { + eligible = append(eligible, candidate) + } + } + switch { + case len(eligible) == 0: + return RepairStateResult{ + SchemaVersion: repairStateSchemaVersion, VerificationStatus: "UNVERIFIED", + Action: "none", NextOperation: "none", + Reason: "no unregistered malformed feature draft was found to repair", + }, nil + case len(eligible) > 1: + return RepairStateResult{ + SchemaVersion: repairStateSchemaVersion, VerificationStatus: "BLOCKED", + Action: "refused", NextOperation: "resolve_ambiguity", + Reason: "more than one unregistered malformed draft matches; rerun with --feature", + Blockers: eligible, + }, nil + default: + feature = eligible[0] + } + } + if !featureSlugPattern.MatchString(feature) { + return RepairStateResult{}, fmt.Errorf("invalid feature slug: %q", feature) + } + + directory := filepath.Join(repo, ".product-loop", "features", feature) + planPath := filepath.Join(directory, "plan.md") + if !fileExists(planPath) { + return refusedRepairState(feature, "no plan.md exists for this feature; nothing to repair"), nil + } + if _, checkErr := CheckPlan(planPath); checkErr == nil { + return refusedRepairState(feature, "the saved plan is valid; repair-state only quarantines a malformed unregistered draft"), nil + } + + // The draft must carry no durable authority. Any of these markers means a + // registered, published, or tracked feature that must never be quarantined. + blockers := []string{} + if fileExists(filepath.Join(directory, "plan.lock.json")) { + blockers = append(blockers, "plan.lock.json present (registered or activated plan)") + } + if fileExists(filepath.Join(directory, "pr.md")) { + blockers = append(blockers, "pr.md present (published or orphaned delivery)") + } + if statePath, stateErr := deliveryStatePath(repo, feature); stateErr == nil && fileExists(statePath) { + blockers = append(blockers, "managed delivery state present") + } + relDir := filepath.ToSlash(filepath.Join(".product-loop", "features", feature)) + tracked, gitErr := gitCommand(repo, "ls-files", "--", relDir) + if gitErr != nil { + return refusedRepairState(feature, "cannot verify git tracking state; refusing to quarantine", gitErr.Error()), nil + } + if strings.TrimSpace(tracked) != "" { + blockers = append(blockers, "directory contains git-tracked files") + } + active, activeErr := ActiveManagedDeliveries(repo) + if activeErr != nil { + return refusedRepairState(feature, "cannot verify active managed deliveries; refusing to quarantine", activeErr.Error()), nil + } + for _, candidate := range active { + if candidate == feature { + blockers = append(blockers, "feature has an active managed delivery") + } + } + if completed, completedErr := completedManagedStates(repo); completedErr == nil { + for _, state := range completed { + if state.Feature == feature { + blockers = append(blockers, "feature has a published managed delivery") + } + } + } + if len(blockers) > 0 { + return refusedRepairState(feature, "refusing to quarantine a registered, published, or tracked feature directory", blockers...), nil + } + + common, err := gitCommonDir(repo) + if err != nil { + return RepairStateResult{}, err + } + stamp := time.Now().UTC().Format("20060102T150405Z") + destParent := filepath.Join(common, "boatstack", "quarantine", feature) + dest := filepath.Join(destParent, stamp) + for index := 2; fileExists(dest); index++ { + dest = filepath.Join(destParent, fmt.Sprintf("%s-%d", stamp, index)) + } + if err := rejectSymlinkComponents(common, dest); err != nil { + return RepairStateResult{}, err + } + if err := os.MkdirAll(destParent, 0o700); err != nil { + return RepairStateResult{}, err + } + if err := os.Rename(directory, dest); err != nil { + if copyErr := copyTree(directory, dest); copyErr != nil { + return RepairStateResult{}, copyErr + } + if rmErr := os.RemoveAll(directory); rmErr != nil { + return RepairStateResult{}, rmErr + } + } + + quarantine := dest + if rel, relErr := filepath.Rel(repo, dest); relErr == nil { + quarantine = filepath.ToSlash(rel) + } + return RepairStateResult{ + SchemaVersion: repairStateSchemaVersion, VerificationStatus: "VERIFIED", + Feature: feature, Action: "quarantined", QuarantinePath: quarantine, + NextOperation: "auto-plan", + Reason: "quarantined an unregistered malformed feature draft; restart planning with auto-plan", + }, nil +} + +// copyTree copies a directory tree file-by-file for the cross-device fallback of +// os.Rename. It refuses symlinks so quarantine cannot follow a link out of the +// repository. +func copyTree(source, destination string) error { + return filepath.WalkDir(source, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + relative, relErr := filepath.Rel(source, path) + if relErr != nil { + return relErr + } + target := filepath.Join(destination, relative) + if entry.IsDir() { + return os.MkdirAll(target, 0o755) + } + info, infoErr := entry.Info() + if infoErr != nil { + return infoErr + } + if info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("refusing to copy symlinked path: %s", path) + } + data, readErr := os.ReadFile(path) + if readErr != nil { + return readErr + } + if mkErr := os.MkdirAll(filepath.Dir(target), 0o755); mkErr != nil { + return mkErr + } + return os.WriteFile(target, data, info.Mode().Perm()) + }) +} diff --git a/boatstack/references/failure-moves.md b/boatstack/references/failure-moves.md index 7586093..7724254 100644 --- a/boatstack/references/failure-moves.md +++ b/boatstack/references/failure-moves.md @@ -23,6 +23,7 @@ Select a move only after locating the failure below its surface symptom. “Time | Worktree bootstrap deadlock | A linked worktree inherits fail-closed hooks but not the ignored runtime required to evaluate them | Versioned Git-common runtime; atomic first-use hydration; provenance check | Cross-version execution or weakened failure behavior | | Post-publication correction routing | CI, review, or a denied push targets work already marked published | Resolve branch and recorded PR identity; append the observation; draft an independently approved corrective child | Treating PR creation as completion or asking the user to bypass the guard | | Unobserved side-effect completion | The same visible state could mean not started, executing, succeeded with a lost response, or failed | Durable operation receipt; exact lease; observe completion; reconcile the expected postcondition before retry | Conversation-scoped retry loops, duplicate PRs, or phantom success | +| Unregistered malformed draft lockout | A hand-authored feature `plan.md` never passed through the helper, so a `CheckPlan` failure escalates to `INVALID_STATE` and the guard denies every product mutation, including the prescribed recovery | `repair-state` quarantines the draft out of `features/` and returns the workflow to `auto-plan`, refusing any directory with a lock, `pr.md`, delivery state, or tracked files | Loosening candidate selection so a genuinely invalid plan silently unblocks product edits | ## Lessons encoded from the benchmark campaign diff --git a/boatstack/references/workflow.md b/boatstack/references/workflow.md index e2c65af..43341f3 100644 --- a/boatstack/references/workflow.md +++ b/boatstack/references/workflow.md @@ -34,6 +34,8 @@ A published delivery cannot be reset. Its correction uses a deterministic new fe `recovery-status` is the read-only resolver for CI failures, review findings, denied publication, and ordinary corrections. It selects by explicit feature, current active branch, current published branch, recorded PR identity, or one unambiguous candidate. It never chooses by recency. A stale reported head SHA, branch mismatch, or multiple match returns a blocker instead of drafting against the wrong delivery. +`repair-state` is the bounded recovery for the one state `recovery-status` cannot resolve: an unregistered feature draft whose `plan.md` never passed through the helper, so it has no plan lock and no delivery state. A malformed such draft makes the guard fail closed at `INVALID_STATE` and deny every product mutation. `repair-state` quarantines that directory out of `.product-loop/features/` into `/boatstack/quarantine//` — reversible, never a hard delete — and returns the workflow to `auto-plan`. It resolves the sole malformed candidate when `--feature` is omitted and refuses ambiguity. It acts only on a directory carrying no durable authority: it refuses any feature with a valid saved plan, a plan lock, a `pr.md`, a managed delivery state, git-tracked files, or an active or published delivery. The guard allowlists it independent of stage but still rejects shell metacharacters and non-helper executables; gating for registered, active, or published deliveries is unchanged. + The `SOURCE_PLAN` file is required from entry through completion of `BUILD`. After build, its path and hash remain recorded for provenance, but `TEST_GATE`, `REVIEW_GATE`, and `SHIP_GATE` do not require the original file to be present. ## Irreversible-operation boundary diff --git a/boatstack/repair_state_test.go b/boatstack/repair_state_test.go new file mode 100644 index 0000000..1f707cc --- /dev/null +++ b/boatstack/repair_state_test.go @@ -0,0 +1,208 @@ +package boatstack + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// writeMalformedDraft models the taxweave-roles failure: an agent hand-authored a +// feature directory with a prose plan.md but never let the helper register it, so +// there is no plan.lock.json and no delivery state. CheckPlan fails on it, which +// the guard escalates to INVALID_STATE and denies every product mutation. +func writeMalformedDraft(t *testing.T, repo, feature string) string { + t.Helper() + directory := filepath.Join(repo, ".product-loop", "features", feature) + if err := os.MkdirAll(directory, 0o755); err != nil { + t.Fatal(err) + } + planPath := filepath.Join(directory, "plan.md") + body := "# " + feature + "\n\nHand-written prose with no structured, marked plan block.\n" + if err := os.WriteFile(planPath, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + if _, err := CheckPlan(planPath); err == nil { + t.Fatalf("fixture is not malformed: CheckPlan unexpectedly passed for %s", feature) + } + return directory +} + +func TestControlledPhaseTransitionAllowsRepairStateAcrossStages(t *testing.T) { + command := ".product-loop/bin/boatstack-helper repair-state --repo . --feature stuck" + for _, stage := range []string{"", "INVALID_STATE", "DRAFT_PLAN", "APPROVED"} { + if !controlledPhaseTransition(command, stage) { + t.Fatalf("repair-state was denied at stage %q; recovery must be reachable", stage) + } + } + // The escape hatch must not widen the surface for real transitions or metachars. + if controlledPhaseTransition(".product-loop/bin/boatstack-helper activate-plan", "INVALID_STATE") { + t.Fatal("activate-plan escaped the INVALID_STATE interlock") + } + if controlledPhaseTransition(".product-loop/bin/boatstack-helper repair-state; rm -rf .", "INVALID_STATE") { + t.Fatal("chained destruction was allowed to ride on repair-state") + } + if controlledPhaseTransition("python scripts/migrate.py", "INVALID_STATE") { + t.Fatal("an ordinary mutation was allowed at INVALID_STATE") + } +} + +// TestRepairStateClosesTheInvalidStateLoop is the end-to-end contract: with a +// malformed unregistered draft on disk, a product mutation is denied and names +// repair-state, and repair-state itself is then allowed — the recovery the guard +// prescribes is genuinely reachable. +func TestRepairStateClosesTheInvalidStateLoop(t *testing.T) { + repo := nextTestRepo(t) + writeMalformedDraft(t, repo, "stuck-feature") + + findings := ClassifyCommand(repo, "python scripts/migrate.py") + if len(findings) == 0 || findings[0].WorkflowStage != "INVALID_STATE" || findings[0].NextOperation != "repair-state" { + t.Fatalf("product mutation was not denied with a repair-state prescription: %#v", findings) + } + if denied := ClassifyCommand(repo, ".product-loop/bin/boatstack-helper repair-state --repo . --feature stuck-feature"); len(denied) != 0 { + t.Fatalf("the prescribed recovery was itself denied: %#v", denied) + } +} + +func TestRepairStateQuarantinesUnregisteredMalformedDraft(t *testing.T) { + repo := nextTestRepo(t) + directory := writeMalformedDraft(t, repo, "stuck-feature") + + result, err := RepairState(repo, "stuck-feature") + if err != nil { + t.Fatal(err) + } + if result.VerificationStatus != "VERIFIED" || result.Action != "quarantined" || result.NextOperation != "auto-plan" { + t.Fatalf("unexpected repair outcome: %#v", result) + } + if dirExists(directory) { + t.Fatal("the malformed draft directory was not removed from features/") + } + if result.QuarantinePath == "" || !dirExists(filepath.Join(repo, filepath.FromSlash(result.QuarantinePath))) { + t.Fatalf("quarantine copy is missing at %q", result.QuarantinePath) + } + if !fileExists(filepath.Join(repo, filepath.FromSlash(result.QuarantinePath), "plan.md")) { + t.Fatal("quarantine did not preserve the draft plan.md") + } + // After recovery the workflow must be able to plan again from a clean slate. + status, err := ResolveNext(repo, "") + if err != nil { + t.Fatal(err) + } + if status.ObservedStage != "NOT_STARTED" { + t.Fatalf("workflow did not return to a plannable state: %q", status.ObservedStage) + } +} + +func TestRepairStateResolvesSoleCandidateWithoutFeature(t *testing.T) { + repo := nextTestRepo(t) + writeMalformedDraft(t, repo, "only-stuck") + result, err := RepairState(repo, "") + if err != nil { + t.Fatal(err) + } + if result.VerificationStatus != "VERIFIED" || result.Feature != "only-stuck" { + t.Fatalf("sole malformed candidate was not resolved: %#v", result) + } +} + +func TestRepairStateBlocksAmbiguousDrafts(t *testing.T) { + repo := nextTestRepo(t) + writeMalformedDraft(t, repo, "stuck-one") + writeMalformedDraft(t, repo, "stuck-two") + result, err := RepairState(repo, "") + if err != nil { + t.Fatal(err) + } + if result.VerificationStatus != "BLOCKED" || len(result.Blockers) != 2 { + t.Fatalf("ambiguous drafts were not refused with both candidates: %#v", result) + } +} + +func TestRepairStateRefusesRegisteredAndPublishedFeatures(t *testing.T) { + t.Run("valid saved plan", func(t *testing.T) { + repo := nextTestRepo(t) + writeValidSavedFeaturePlan(t, repo, "good-feature") + assertRefused(t, repo, "good-feature") + }) + t.Run("locked plan", func(t *testing.T) { + repo := nextTestRepo(t) + writeMalformedDraft(t, repo, "locked-feature") + writeDraftFile(t, repo, "locked-feature", "plan.lock.json", "lock\n") + assertRefused(t, repo, "locked-feature") + }) + t.Run("published pr", func(t *testing.T) { + repo := nextTestRepo(t) + writeMalformedDraft(t, repo, "shipped-feature") + writeDraftFile(t, repo, "shipped-feature", "pr.md", "# PR\n") + assertRefused(t, repo, "shipped-feature") + }) + t.Run("active delivery state", func(t *testing.T) { + repo := nextTestRepo(t) + writeNextDelivery(t, repo, "active-feature", "BUILD", 0) + // Overwrite the registered plan with a malformed one to prove the refusal + // comes from the delivery state, not from CheckPlan. + planPath := filepath.Join(repo, ".product-loop", "features", "active-feature", "plan.md") + if err := os.WriteFile(planPath, []byte("# prose only\n"), 0o644); err != nil { + t.Fatal(err) + } + assertRefused(t, repo, "active-feature") + }) + t.Run("tracked directory", func(t *testing.T) { + repo := nextTestRepo(t) + writeMalformedDraft(t, repo, "tracked-feature") + runGit(t, repo, "config", "user.name", "Boatstack Test") + runGit(t, repo, "config", "user.email", "boatstack@example.invalid") + runGit(t, repo, "add", ".product-loop/features/tracked-feature/plan.md") + assertRefused(t, repo, "tracked-feature") + }) +} + +func assertRefused(t *testing.T, repo, feature string) { + t.Helper() + result, err := RepairState(repo, feature) + if err != nil { + t.Fatal(err) + } + if result.VerificationStatus != "BLOCKED" || result.Action != "refused" { + t.Fatalf("repair-state should have refused %s but returned %#v", feature, result) + } + directory := filepath.Join(repo, ".product-loop", "features", feature) + if !dirExists(directory) { + t.Fatalf("refused feature %s was mutated on disk", feature) + } +} + +func writeDraftFile(t *testing.T, repo, feature, name, body string) { + t.Helper() + path := filepath.Join(repo, ".product-loop", "features", feature, name) + if err := os.WriteFile(path, []byte(body), 0o644); err != nil { + t.Fatal(err) + } +} + +// TestRepairStateDoesNotBypassRegisteredMalformedPlan guards against a silent +// escape: a feature with a plan.lock.json is registered even if its plan.md is +// malformed, so it is excluded from candidate resolution and never eligible for +// quarantine by an empty --feature. +func TestRepairStateDoesNotBypassRegisteredMalformedPlan(t *testing.T) { + repo := nextTestRepo(t) + writeMalformedDraft(t, repo, "registered-broken") + writeDraftFile(t, repo, "registered-broken", "plan.lock.json", "lock\n") + candidates, err := featurePlanCandidates(repo) + if err != nil { + t.Fatal(err) + } + for _, candidate := range candidates { + if candidate == "registered-broken" { + t.Fatal("a locked feature was offered as a repair candidate") + } + } + result, err := RepairState(repo, "") + if err != nil { + t.Fatal(err) + } + if result.Action == "quarantined" || strings.Contains(result.QuarantinePath, "registered-broken") { + t.Fatalf("repair-state quarantined a registered feature: %#v", result) + } +} diff --git a/boatstack/safety.go b/boatstack/safety.go index 7e2d3a2..7b49864 100644 --- a/boatstack/safety.go +++ b/boatstack/safety.go @@ -100,6 +100,14 @@ func controlledPhaseTransition(command, stage string) bool { if readOnlyHelpers[fields[1]] { return true } + // repair-state is the guard-prescribed recovery for a workflow stuck at + // INVALID_STATE because of an unregistered malformed draft. Those findings + // carry an empty stage, so allow it independent of stage. It mutates (it + // quarantines the draft), so it is not a read-only helper; RepairState + // self-guards, refusing any registered, published, or tracked directory. + if fields[1] == "repair-state" { + return true + } switch stage { case "DRAFT_PLAN": return fields[1] == "planning-write" || fields[1] == "record-approval" diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 86e2bde..9194a73 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 **15145 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 **15529 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 [`5227e7828d9bcac8c6b11c19289c2b4ef5be92a2`](https://github.com/operatorstack/intelligence-flow/tree/5227e7828d9bcac8c6b11c19289c2b4ef5be92a2/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 [`4dcec85043e6fec7558e7bdff97cb2babe3af7c2`](https://github.com/operatorstack/intelligence-flow/tree/4dcec85043e6fec7558e7bdff97cb2babe3af7c2/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 038b2c2..ccf3e63 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "5227e7828d9bcac8c6b11c19289c2b4ef5be92a2", + "source_commit": "4dcec85043e6fec7558e7bdff97cb2babe3af7c2", "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:5227e7828d9bcac8c6b11c19289c2b4ef5be92a2" + "last_verified_version": "source:4dcec85043e6fec7558e7bdff97cb2babe3af7c2" }, { "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:5227e7828d9bcac8c6b11c19289c2b4ef5be92a2" + "last_verified_version": "source:4dcec85043e6fec7558e7bdff97cb2babe3af7c2" }, { "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:5227e7828d9bcac8c6b11c19289c2b4ef5be92a2" + "last_verified_version": "source:4dcec85043e6fec7558e7bdff97cb2babe3af7c2" }, { "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:5227e7828d9bcac8c6b11c19289c2b4ef5be92a2" + "last_verified_version": "source:4dcec85043e6fec7558e7bdff97cb2babe3af7c2" }, { "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:5227e7828d9bcac8c6b11c19289c2b4ef5be92a2" + "last_verified_version": "source:4dcec85043e6fec7558e7bdff97cb2babe3af7c2" }, { "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:5227e7828d9bcac8c6b11c19289c2b4ef5be92a2" + "last_verified_version": "source:4dcec85043e6fec7558e7bdff97cb2babe3af7c2" }, { "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:5227e7828d9bcac8c6b11c19289c2b4ef5be92a2" + "last_verified_version": "source:4dcec85043e6fec7558e7bdff97cb2babe3af7c2" }, { "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:5227e7828d9bcac8c6b11c19289c2b4ef5be92a2" + "last_verified_version": "source:4dcec85043e6fec7558e7bdff97cb2babe3af7c2" }, { "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:5227e7828d9bcac8c6b11c19289c2b4ef5be92a2" + "last_verified_version": "source:4dcec85043e6fec7558e7bdff97cb2babe3af7c2" }, { "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:5227e7828d9bcac8c6b11c19289c2b4ef5be92a2" + "last_verified_version": "source:4dcec85043e6fec7558e7bdff97cb2babe3af7c2" }, { "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:5227e7828d9bcac8c6b11c19289c2b4ef5be92a2" + "last_verified_version": "source:4dcec85043e6fec7558e7bdff97cb2babe3af7c2" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 2d53eae..2b19297 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": "5227e7828d9bcac8c6b11c19289c2b4ef5be92a2", + "source_commit": "4dcec85043e6fec7558e7bdff97cb2babe3af7c2", "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-24-repair-state-recovery.md b/release-notes/2026-07-24-repair-state-recovery.md new file mode 100644 index 0000000..9f99fda --- /dev/null +++ b/release-notes/2026-07-24-repair-state-recovery.md @@ -0,0 +1,7 @@ +### repair-state recovery for stuck malformed drafts + +The `repair-state` recovery the guard prescribes is now real and reachable. When an unregistered feature draft is authored by hand — a `plan.md` that never passed through the helper, so there is no `plan.lock.json` and no delivery state — a malformed plan makes the guard fail closed at `INVALID_STATE` and deny every product mutation. Previously `repair-state` existed only as a `next_operation` label: it was neither implemented as a helper subcommand nor allowlisted in the guard, so the prescribed recovery was itself blocked and even `rm` of the untracked directory was denied. The workflow was stuck behind the lock the recovery was meant to open. + +`boatstack-helper repair-state [--feature ]` now quarantines the offending draft: it moves the directory out of `.product-loop/features/` into `/boatstack/quarantine//` (reversible, never a hard delete) and returns the workflow to `auto-plan`. When `--feature` is omitted it resolves the sole malformed unregistered candidate and refuses ambiguously when more than one is present. + +Recovery is bounded and self-guarding. It acts only on a directory that carries no durable authority: it refuses any feature with a valid saved plan, a `plan.lock.json`, a `pr.md`, a managed delivery state, git-tracked files, or an active/published delivery. The guard allowlists `repair-state` independent of stage but still rejects shell metacharacters and non-helper executables, and gating for registered, active, or published deliveries is unchanged — an unregistered malformed draft still blocks product mutation, but the escape hatch is now genuine and one command away.