From 249ff0f8d855c350fa1591bd656029c7b7c6d7e7 Mon Sep 17 00:00:00 2001 From: "operator-stack-publisher[bot]" Date: Sat, 18 Jul 2026 18:38:30 +0000 Subject: [PATCH] Sync Boatstack from Intelligence Flow Labs @ bbed9588e740 --- CONTRIBUTING.md | 2 +- README.md | 12 + UPSTREAM.json | 33 +-- boatstack/SKILL.md | 9 + boatstack/cmd/boatstack-helper/main.go | 29 ++- boatstack/delivery.go | 218 ++++++++++++++++-- boatstack/delivery_test.go | 115 +++++++++ boatstack/export.go | 14 +- boatstack/export_test.go | 19 ++ boatstack/references/artifacts.md | 2 + boatstack/references/workflow.md | 6 + docs/evidence-engineered-coding.md | 4 +- docs/public-claims.json | 24 +- labs/diagram-json/plan.lock.json | 2 +- ...26-07-18-conversational-delivery-repair.md | 11 + 15 files changed, 442 insertions(+), 58 deletions(-) create mode 100644 release-notes/2026-07-18-conversational-delivery-repair.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4af802..58b1324 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/697ce2f7d98eb3bfb249b33d3556efbd7e9365b1/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/bbed9588e74016250e3cf8968e70bdd375afb863/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/README.md b/README.md index 01b7598..802bf3b 100644 --- a/README.md +++ b/README.md @@ -58,12 +58,24 @@ In Claude Code and Cursor, that guidance moves through `/plan-gate` → `/build` One feature moves from idea through planning, approval, building, tests, review, and pull request; its retained plans, decisions, gaps, evidence, and code state combine with the next idea to create the next plan

+## Change course without losing the delivery + +After Build, describe changes normally. Boatstack records them, preserves valid work, and resumes at the earliest boundary. + +```text +“This is wrong” → record → repair → test → review + ↘ changed intent → approve delta +``` + +Receipts remain as history; published corrections become linked deliveries. + ## Features - **A guided path from idea to PR.** Start with `/auto-plan`; Boatstack presents one next action at a time through planning, approval, build, validation, review, and PR preparation. - **Human decisions stay human.** Material product questions remain open until a person answers them, and implementation waits for explicit approval. - **Evidence tied to the promise.** Tests and checks map to the outcomes the change claims to deliver instead of treating one green command as proof of everything. - **Context that survives the feature.** Plans, decisions, accepted gaps, evidence, review findings, and code state can inform the next feature rather than disappearing with the chat. +- **Conversational repair after Build.** Describe what changed; Boatstack preserves valid work and reruns only affected boundaries. - **Safer agent execution.** High-confidence destructive recovery is stopped before execution; phased work is gated and published one approved delivery slice at a time. - **Reviewer-ready pull requests.** Boatstack carries the reason, actual changes, validation, risks, gaps, rollout, and rollback into a focused PR brief. - **Portable across your AI stack.** Cursor, Codex, Claude Code, different model tiers, and specialist skills use the same repository-owned delivery contract. diff --git a/UPSTREAM.json b/UPSTREAM.json index b5e2806..e87b449 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -1,7 +1,7 @@ { "canonical_context": { - "characters": 38485, - "estimated_tokens": 9622, + "characters": 39803, + "estimated_tokens": 9951, "estimator": "ceil(total characters / 4); compactness signal, not provider billing", "files": [ "product-engineering-loop/references/workflow.md", @@ -12,12 +12,12 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "ca94e9856aab321b125ddeb9f9845c9a26f957585ba46e5547525875b34aa64b", - "README.md": "d52c9be1a91165e3bb22318e3014278eb5326d10a45da6311f677d704e5c086d", + "CONTRIBUTING.md": "6c29c112507af7f6661d7d3485da1f7974462b85f54b1075a014fdb5b9a4e546", + "README.md": "7d1473b75566901899b49a9f98cd5446ac40d2fceee3f168c5e6869c742e0b4a", "assets/boatstack-journey.svg": "c1f7fe2741f5e9ca66bb3fe9b103e6364ba5acbca8b7a8054768ffd85cf325ea", "assets/boatstack-mark.svg": "ec96165583b15cfd446c27049d49217973f3e9b1defa5771cc08eec0c9542ce4", "assets/boatstack-portability.svg": "ce648f5581d16586d25824d3a8132ef1b3d88b73329179173120129d4f74fd24", - "boatstack/SKILL.md": "3fde100e855b968cfca3bfebf324955a6440a1b7a14b1bd2ec21ad8e4b9613d8", + "boatstack/SKILL.md": "9443685e255a59de2f5e58928051b4f7385f7401831300b78376d48e41024ba1", "boatstack/agents/openai.yaml": "68a30a60859556c5a26e16d184594ca243a6043d99c8cf7d66b5dd6d50a93cd1", "boatstack/assets/templates/adr.md": "c577a3c1c1319061f61deb053597e6e853657022185fe28b8f733327e2a78565", "boatstack/assets/templates/approval.md": "74b0b816703a6dce3c96c8f95f981af910b020b6908e7f76cf5630778637e9f5", @@ -31,11 +31,11 @@ "boatstack/assets/templates/test-plan.md": "6db8a9f27dd171fb80222a501cae50eb051e7278c04703fa43b5ff86dd4d2df4", "boatstack/atomic_unix.go": "89f2723361591de2bb8bd22ce7e34ec529d3278509f0df78fd5c4a7d4140fbe9", "boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f", - "boatstack/cmd/boatstack-helper/main.go": "dbb8cb4bb403aa36a47bd9317e9a34845cabfa5b8e4152a7fb57ded1591d8957", - "boatstack/delivery.go": "d092ac67116d651938ed1e2912bb845c3b273d650cf88049304888be9683c59c", - "boatstack/delivery_test.go": "a8a5a7e6e8dcfee1538367d49c76c531e04211876c1685265884cff26ae04497", - "boatstack/export.go": "3f086d8615769f857ba704678fea78ec63bdf28c15acd4a7bf0c1414f132e285", - "boatstack/export_test.go": "87fc1aef65f3e437d3ca9660e31b9fa77bbb6d80f9b131adff1dbd422a71fdfe", + "boatstack/cmd/boatstack-helper/main.go": "fc979a21007107f6a2f52a2c1eb82c35be5dbe97a775a908e0d1ebc25864f040", + "boatstack/delivery.go": "907a0ff8dc3e6120387eef3c7d97cdadb0b9dd8f788cb39e149aa7a6c4a6260a", + "boatstack/delivery_test.go": "744d166757deafe5b9fc4f66b79b324de43dcc36a26a317572eaec73a8b21044", + "boatstack/export.go": "72ed801fcf52fc93e8446dd3b7d205d7ef8a0530b33f4e02891f7290bc243343", + "boatstack/export_test.go": "8f04947a5e1723df21f068b14ba295b0281a04b5037d251be0315100bdce65e8", "boatstack/go.mod": "57c377eccea51372d6664de4169e2ca45806b046f7e8a98a1e35a9eb454b4b8d", "boatstack/hooks.go": "1d5d8c4bf7e6e867c8bf07e391d86158347269f856647a5d256f345bbb8d3c96", "boatstack/hooks_test.go": "c3f359416ea53f258d8747d0247381e8946efd4d4a5bcf072c4147f885475ad3", @@ -49,11 +49,11 @@ "boatstack/planning_test.go": "6b156a64182ed76d4c3d392b4c5a26abe5d8b81cea27ee12ac7c4627c827e186", "boatstack/pr.go": "2e0ea90a991fe4504e54972506cb306f48124c8b34be8577d96f6c97079bdf0c", "boatstack/pr_test.go": "74afb1a9be3c4a95a426515be415149ae24b077454b486d8e69b7118c42f2916", - "boatstack/references/artifacts.md": "3a87b8f8d835329191bedb20c94e6056fe2251b3e9d510656909eb9d276069dc", + "boatstack/references/artifacts.md": "8f2e79b8af4bd3ad2e32aa3e8c07f10812555d0a3247e4991db75cc1cda395c3", "boatstack/references/failure-moves.md": "1d35126348d0b681976e8819665e16fd745fd65eca271492603cb80aab75bf49", "boatstack/references/irreversible-operation-boundary.md": "2a695f2d7de95cfc8750f107bef9c86581712aa1f02e7233b69b850d8c2af42e", "boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae", - "boatstack/references/workflow.md": "6a7d146c2ad072c513324ad058d4b6c38ea967f2b9c199e055cfc528f4b826ad", + "boatstack/references/workflow.md": "6794ef2899e3cde7f8cdf182b65def8e371ae3ec3d38cc3cbe2009adb4321512", "boatstack/release.go": "fa2ac926df89c90c5844e938a2e02d4b8dbbaefbf85bb7a1a89fc51690bea520", "boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690", "boatstack/runtime.go": "f393745950e8ba2da7e25d5539ad536a6239a10c17109224bad20cf48445c380", @@ -69,10 +69,10 @@ "docs/account-recovery-walkthrough.md": "676034974594a7d1a559b24dbed31d7ccc429eb81404b203ca07bbdaa19ec3d3", "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", - "docs/evidence-engineered-coding.md": "8c79f43e67c6fa9dbd4f5eb84595ef7846b14a35d21c6be1403182f20551ab4c", + "docs/evidence-engineered-coding.md": "d9397b0ddb7ede2080a1a4d703b3272dcf31080cfc0f7b1cc732c847145386ee", "docs/generated-files.md": "136422baf0c7fc2bd5100cfe0ebdb3d9d0705dfd7e7d54bf745dd1037e63492c", "docs/getting-started.md": "4ef77719a01a2dbe98c5154d58fc03f51797ae0ef39a09d8804fe0708b8cc920", - "docs/public-claims.json": "0bcb69c56b2e6edd97d21b5b6d0f3ac3eb8554c46e9ea2324b46166c1814f180", + "docs/public-claims.json": "67f2b7c4a288ba11338b11e5db2cb7efa7e498c42dcc3264da641ecbadd59f2a", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "d65c66e323037bda5d45aacef5d48afa6bf93da55901378891d235aca3a5684f", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -86,7 +86,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": "de95c999eb36de930d3a0fcca0d5751c1087ae49bb26b8347ec90432291457d6", + "labs/diagram-json/plan.lock.json": "4fc2cba6a3a4bbe37d6e3591b583c023dccc8fff778ec00fd2c8ef789d6a6b0e", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -100,6 +100,7 @@ "release-notes/2026-07-18-automated-releases.md": "6571eec442a27bd1a55667567f0659993c516171ef10ae71046cce80f7fd29fa", "release-notes/2026-07-18-base-aware-release-preflight.md": "cdface46ccd959a5299de4c363c9e4057e7257820dab77dcf0e587daef5d3d99", "release-notes/2026-07-18-claude-command-discovery.md": "bb42c23041916dee21e502449143620a8dcbd0dec4aad01162293499547f1114", + "release-notes/2026-07-18-conversational-delivery-repair.md": "3d1a13b4e2e423f923df6aec29947070b5fef39af15d07437523580fad82622a", "release-notes/2026-07-18-global-reply-shortcuts.md": "329d6fd104079bc5f66e7c3d477f4ff2a6bb264d429485c6f42f9c203d17fa29", "release-notes/2026-07-18-harbor-lab-namespace.md": "6419c049e5a3024c5a8604e4d9fb241c27ceb80bf1d4cc62f66d1a0eaf09ea21", "release-notes/2026-07-18-host-hook-migrations.md": "1c9f81d9318854214f72802045e8e39c9ca45435af0f9d2c28fcf4ff4c1e0071", @@ -110,7 +111,7 @@ "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "697ce2f7d98eb3bfb249b33d3556efbd7e9365b1", + "commit": "bbed9588e74016250e3cf8968e70bdd375afb863", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/SKILL.md b/boatstack/SKILL.md index 57e7d8d..0cec3da 100644 --- a/boatstack/SKILL.md +++ b/boatstack/SKILL.md @@ -15,6 +15,7 @@ Map the request to one operation: - `auto-plan`: refine a saved host Plan-mode file into a reviewable draft feature package; refuse when that file is absent. - `plan-gate`: validate the Markdown draft, present it for explicit human acceptance, and record that acceptance in Markdown. - `build`: activate the approved Markdown plan, then implement only the active delivery slice's tasks. +- `repair`: classify a free-form post-build change, record it durably, and resume from the earliest affected stage without silently changing approved intent. - `test-gate`: test requirements and relevant regressions using independent evidence. - `review-gate`: review the diff against the spec, project invariants, risks, and known gaps. - `ship-gate`: preview, then explicitly open or update, a reviewer-ready PR grounded in the approved diff and evidence. @@ -132,6 +133,14 @@ All files created or updated by `auto-plan` and `plan-gate` must be Markdown. gs Do not branch the workflow on model brand, price, or a guessed capability tier. Branch only on observable work state: unresolved ambiguity, risk, convergence, repeated tactics, tool results, test fidelity, and gate evidence. A repository may choose any implementation model; the contract and gates stay the same. +## Repair from ordinary conversation + +Before any product edit, read managed delivery status. If a delivery is active and the user reports a problem or requests a modification, use `repair` even when they do not name Boatstack or a slash command. Compare the exact request with the current lock, acceptance criteria, diff, evidence, and receipts. Classify it as `implementation_repair`, `verification_repair`, `review_repair`, `requirement_amendment`, or `needs_clarification`, then invoke `record-change` before editing. + +Same-intent repair resumes at the helper-reported stage and reuses the existing gates. A requirement amendment or ambiguous expected behavior blocks product edits and returns to a concise Plan Gate delta. Never edit `changes.md`, ignored delivery state, or receipts directly; those are emitted by controlled transitions. Conversation history is never workflow authority. + +A published delivery is immutable. Record the observation against it, then plan the correction under a new feature id whose structured plan sets `parent_delivery` to the published feature. Activation refuses to reset published slices; the corrective child receives its own lock and full gates. + ## Enforce the gates ### Test gate diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go index 291bd33..ce3914b 100644 --- a/boatstack/cmd/boatstack-helper/main.go +++ b/boatstack/cmd/boatstack-helper/main.go @@ -302,6 +302,31 @@ func deliveryStatusCommand(arguments []string) int { return 0 } +func recordChangeCommand(arguments []string) int { + flags := flag.NewFlagSet("record-change", flag.ContinueOnError) + options := boatstack.ChangeObservationOptions{} + flags.StringVar(&options.Repo, "repo", ".", "repository containing the managed delivery") + flags.StringVar(&options.Feature, "feature", "", "managed Boatstack feature slug") + flags.StringVar(&options.Message, "message", "", "exact user change request") + flags.StringVar(&options.SourceStage, "source-stage", "", "stage where the change was observed") + flags.StringVar(&options.Expected, "expected", "", "approved or requested expected behavior") + flags.StringVar(&options.Actual, "actual", "", "observed behavior") + flags.StringVar(&options.Evidence, "evidence", "", "bounded evidence or reproduction reference") + flags.StringVar(&options.Classification, "classification", "", "implementation_repair, verification_repair, review_repair, requirement_amendment, or needs_clarification") + if err := flags.Parse(arguments); err != nil { + return 2 + } + if options.Feature == "" || options.Message == "" || options.SourceStage == "" || options.Classification == "" { + return fail(fmt.Errorf("record-change requires --feature, --message, --source-stage, and --classification")) + } + observation, state, err := boatstack.RecordChangeObservation(options) + if err != nil { + return fail(err) + } + fmt.Printf("PASS: change observation recorded\nOBSERVATION_ID=%s\nCLASSIFICATION=%s\nMODE=%s\nRESUME_STAGE=%s\n", observation.ID, observation.Classification, state.Mode, state.ResumeStage) + return 0 +} + func doctorCommand(arguments []string) int { flags := flag.NewFlagSet("doctor", flag.ContinueOnError) repo := flags.String("repo", ".", "repository whose Boatstack installation should be checked") @@ -464,7 +489,7 @@ func publishPRCommand(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] { @@ -492,6 +517,8 @@ func run() int { return activatePlanCommand(os.Args[2:]) case "delivery-status": return deliveryStatusCommand(os.Args[2:]) + case "record-change": + return recordChangeCommand(os.Args[2:]) case "record-delivery-gate": return recordDeliveryGateCommand(os.Args[2:]) case "pr-context": diff --git a/boatstack/delivery.go b/boatstack/delivery.go index c344291..986710f 100644 --- a/boatstack/delivery.go +++ b/boatstack/delivery.go @@ -25,26 +25,36 @@ type DeliverySlice struct { } type DeliveryState struct { - SchemaVersion int `json:"schema_version"` - Feature string `json:"feature"` - PlanLockHash string `json:"plan_lock_sha256"` - ActiveIndex int `json:"active_index"` - Slices []DeliverySlice `json:"slices"` + SchemaVersion int `json:"schema_version"` + Feature string `json:"feature"` + PlanLockHash string `json:"plan_lock_sha256"` + PreviousPlanLocks []string `json:"previous_plan_lock_sha256,omitempty"` + ActiveIndex int `json:"active_index"` + Slices []DeliverySlice `json:"slices"` + Mode string `json:"mode,omitempty"` + ResumeStage string `json:"resume_stage,omitempty"` + ActiveObservationID string `json:"active_observation_id,omitempty"` + RepairAttempt int `json:"repair_attempt,omitempty"` + SupersededReceipts []string `json:"superseded_receipts,omitempty"` + ParentDelivery string `json:"parent_delivery,omitempty"` } type DeliveryGateReceipt struct { - SchemaVersion int `json:"schema_version"` - Feature string `json:"feature"` - SliceID string `json:"slice_id"` - Gate string `json:"gate"` - Status string `json:"status"` - BaseBranch string `json:"base_branch"` - HeadBranch string `json:"head_branch"` - HeadCommit string `json:"head_commit"` - DiffSHA256 string `json:"diff_sha256"` - EvidencePath string `json:"evidence_path"` - EvidenceHash string `json:"evidence_sha256"` - RecordedAt string `json:"recorded_at"` + SchemaVersion int `json:"schema_version"` + Feature string `json:"feature"` + SliceID string `json:"slice_id"` + Gate string `json:"gate"` + Status string `json:"status"` + BaseBranch string `json:"base_branch"` + HeadBranch string `json:"head_branch"` + HeadCommit string `json:"head_commit"` + DiffSHA256 string `json:"diff_sha256"` + EvidencePath string `json:"evidence_path"` + EvidenceHash string `json:"evidence_sha256"` + RecordedAt string `json:"recorded_at"` + Attempt int `json:"attempt,omitempty"` + TriggerObservationID string `json:"trigger_observation_id,omitempty"` + Supersedes string `json:"supersedes,omitempty"` } type DeliveryGateOptions struct { @@ -57,6 +67,31 @@ type DeliveryGateOptions struct { EvidencePath string } +type ChangeObservationOptions struct { + Repo string + Feature string + Message string + SourceStage string + Expected string + Actual string + Evidence string + Classification string +} + +type ChangeObservation struct { + ID string `json:"id"` + Feature string `json:"feature"` + SliceID string `json:"slice_id,omitempty"` + SourceStage string `json:"source_stage"` + Expected string `json:"expected,omitempty"` + Actual string `json:"actual,omitempty"` + Evidence string `json:"evidence,omitempty"` + Message string `json:"message"` + Classification string `json:"classification"` + ResumeStage string `json:"resume_stage,omitempty"` + RecordedAt string `json:"recorded_at"` +} + func deliveryEvidenceGateStatus(value, gate, sliceID string, explicit bool) string { if !explicit { return evidenceGateStatus(value, gate) @@ -259,15 +294,140 @@ func initializeDeliveryState(repo, feature, planPath, lockPath string) error { if err != nil { return err } - if existing, loadErr := LoadDeliveryState(repo, feature); loadErr == nil && existing.PlanLockHash == lockHash { - return nil + previousLocks := []string{} + repairAttempt := 0 + if existing, loadErr := LoadDeliveryState(repo, feature); loadErr == nil { + if existing.PlanLockHash == lockHash { + return nil + } + if existing.ActiveIndex >= len(existing.Slices) { + return fmt.Errorf("published delivery %s is immutable; activate the correction under a new feature id with parent_delivery=%s", feature, feature) + } + previousLocks = append(previousLocks, existing.PreviousPlanLocks...) + if existing.PlanLockHash != "" { + previousLocks = append(previousLocks, existing.PlanLockHash) + } + repairAttempt = existing.RepairAttempt } return saveDeliveryState(repo, DeliveryState{ SchemaVersion: deliveryStateSchemaVersion, Feature: feature, PlanLockHash: lockHash, - ActiveIndex: 0, Slices: slices, + PreviousPlanLocks: previousLocks, ActiveIndex: 0, Slices: slices, Mode: "NORMAL", RepairAttempt: repairAttempt, + ParentDelivery: strings.TrimSpace(stringValue(plan["parent_delivery"])), }) } +func archiveDeliveryReceipt(repo, feature, sliceID, gate, observationID string) (string, error) { + path, err := deliveryReceiptPath(repo, feature, sliceID, gate) + if err != nil { + return "", err + } + value, err := os.ReadFile(path) + if os.IsNotExist(err) { + return "", nil + } + if err != nil { + return "", err + } + archive := filepath.Join(filepath.Dir(filepath.Dir(path)), "superseded", observationID, gate+".json") + if err := atomicWriteMode(archive, value, 0o644); err != nil { + return "", err + } + if err := os.Remove(path); err != nil { + return "", err + } + return ".git/boatstack/deliveries/" + feature + "/receipts/superseded/" + observationID + "/" + gate + ".json", nil +} + +func appendChangeObservation(repo string, observation ChangeObservation) error { + path := filepath.Join(repo, ".product-loop", "features", observation.Feature, "changes.md") + existing, err := os.ReadFile(path) + if err != nil && !os.IsNotExist(err) { + return err + } + if len(existing) == 0 { + existing = []byte("# Change observations\n\nAppend-only observations recorded after build activation.\n") + } + block := fmt.Sprintf("\n## %s\n\n- Recorded: `%s`\n- Source stage: `%s`\n- Classification: `%s`\n- Resume stage: `%s`\n- User message: %s\n- Expected: %s\n- Actual: %s\n- Evidence: %s\n- Resolution: pending\n", + observation.ID, observation.RecordedAt, observation.SourceStage, observation.Classification, + observation.ResumeStage, observation.Message, observation.Expected, observation.Actual, observation.Evidence) + return atomicWriteMode(path, append(existing, []byte(block)...), 0o644) +} + +func RecordChangeObservation(options ChangeObservationOptions) (ChangeObservation, DeliveryState, error) { + repo, err := ResolveRepository(options.Repo) + if err != nil { + return ChangeObservation{}, DeliveryState{}, err + } + state, err := LoadDeliveryState(repo, options.Feature) + if err != nil { + return ChangeObservation{}, DeliveryState{}, err + } + if err := checkDeliveryPlanLock(repo, options.Feature, state); err != nil { + return ChangeObservation{}, DeliveryState{}, err + } + classification := strings.ToLower(strings.TrimSpace(options.Classification)) + resume := map[string]string{ + "implementation_repair": "BUILD", "verification_repair": "TEST_GATE", + "review_repair": "REVIEW_GATE", "requirement_amendment": "PLAN_GATE", + "needs_clarification": "", + }[classification] + if _, ok := map[string]bool{"implementation_repair": true, "verification_repair": true, "review_repair": true, "requirement_amendment": true, "needs_clarification": true}[classification]; !ok { + return ChangeObservation{}, DeliveryState{}, fmt.Errorf("unsupported change classification %q", classification) + } + if strings.TrimSpace(options.Message) == "" || strings.TrimSpace(options.SourceStage) == "" { + return ChangeObservation{}, DeliveryState{}, fmt.Errorf("change observation requires the user message and source stage") + } + state.RepairAttempt++ + id := fmt.Sprintf("CHG-%03d", state.RepairAttempt) + observation := ChangeObservation{ + ID: id, Feature: options.Feature, SourceStage: strings.ToUpper(strings.TrimSpace(options.SourceStage)), + Expected: strings.TrimSpace(options.Expected), Actual: strings.TrimSpace(options.Actual), Evidence: strings.TrimSpace(options.Evidence), + Message: strings.TrimSpace(options.Message), Classification: classification, ResumeStage: resume, + RecordedAt: time.Now().UTC().Truncate(time.Second).Format(time.RFC3339), + } + if state.ActiveIndex < len(state.Slices) { + observation.SliceID = state.Slices[state.ActiveIndex].ID + } else if classification != "requirement_amendment" { + return ChangeObservation{}, DeliveryState{}, fmt.Errorf("published delivery changes require requirement_amendment and a corrective child delivery") + } + if err := appendChangeObservation(repo, observation); err != nil { + return ChangeObservation{}, DeliveryState{}, err + } + state.ActiveObservationID = id + state.ResumeStage = resume + if classification == "needs_clarification" || classification == "requirement_amendment" { + state.Mode = "AMENDMENT_REQUIRED" + if classification == "needs_clarification" { + state.ResumeStage = "" + } + } else { + state.Mode = "REWORK" + slice := &state.Slices[state.ActiveIndex] + gates := []string{"review"} + if resume == "BUILD" || resume == "TEST_GATE" { + gates = []string{"test", "review"} + } + for _, gate := range gates { + archived, archiveErr := archiveDeliveryReceipt(repo, options.Feature, slice.ID, gate, id) + if archiveErr != nil { + return ChangeObservation{}, DeliveryState{}, archiveErr + } + if archived != "" { + state.SupersededReceipts = append(state.SupersededReceipts, archived) + } + } + if resume == "REVIEW_GATE" { + slice.Status = "TEST_PASSED" + } else { + slice.Status = "BUILD" + } + } + if err := saveDeliveryState(repo, state); err != nil { + return ChangeObservation{}, DeliveryState{}, err + } + return observation, state, nil +} + func activeDeliverySlice(state DeliveryState) (DeliverySlice, error) { if state.ActiveIndex >= len(state.Slices) { return DeliverySlice{}, fmt.Errorf("all delivery slices are already published") @@ -413,6 +573,9 @@ func RecordDeliveryGate(options DeliveryGateOptions) (DeliveryGateReceipt, error if err := checkDeliveryPlanLock(repo, options.Feature, state); err != nil { return DeliveryGateReceipt{}, err } + if state.Mode == "AMENDMENT_REQUIRED" { + return DeliveryGateReceipt{}, fmt.Errorf("delivery requires an approved plan amendment before gates may continue") + } slice, err := activeDeliverySlice(state) if err != nil { return DeliveryGateReceipt{}, err @@ -476,11 +639,18 @@ func RecordDeliveryGate(options DeliveryGateOptions) (DeliveryGateReceipt, error if err != nil { return DeliveryGateReceipt{}, err } + previous, _ := readDeliveryReceipt(repo, options.Feature, slice.ID, gate) receipt := DeliveryGateReceipt{ SchemaVersion: deliveryStateSchemaVersion, Feature: options.Feature, SliceID: slice.ID, Gate: gate, Status: status, BaseBranch: base, HeadBranch: head, HeadCommit: headCommit, DiffSHA256: diffHash, EvidencePath: relEvidence, EvidenceHash: evidenceHash, RecordedAt: time.Now().UTC().Truncate(time.Second).Format(time.RFC3339), + Attempt: state.RepairAttempt + 1, TriggerObservationID: state.ActiveObservationID, + } + if previous.RecordedAt != "" { + receipt.Supersedes = previous.RecordedAt + } else if state.ActiveObservationID != "" && len(state.SupersededReceipts) > 0 { + receipt.Supersedes = state.SupersededReceipts[len(state.SupersededReceipts)-1] } path, _ := deliveryReceiptPath(repo, options.Feature, slice.ID, gate) value, _ := MarshalJSON(receipt) @@ -496,6 +666,9 @@ func RecordDeliveryGate(options DeliveryGateOptions) (DeliveryGateReceipt, error } } else { state.Slices[state.ActiveIndex].Status = "REVIEW_PASSED" + state.Mode = "NORMAL" + state.ResumeStage = "" + state.ActiveObservationID = "" } if err := saveDeliveryState(repo, state); err != nil { return DeliveryGateReceipt{}, err @@ -511,6 +684,9 @@ func CheckDeliveryReadyForShip(repo, feature, base, head, diffHash string, chang if err := checkDeliveryPlanLock(repo, feature, state); err != nil { return DeliveryState{}, DeliverySlice{}, nil, err } + if state.Mode != "" && state.Mode != "NORMAL" { + return DeliveryState{}, DeliverySlice{}, nil, fmt.Errorf("delivery has unresolved repair state %s", state.Mode) + } slice, err := activeDeliverySlice(state) if err != nil { return DeliveryState{}, DeliverySlice{}, nil, err @@ -582,7 +758,7 @@ func ActiveManagedDeliveries(repo string) ([]string, error) { if loadErr != nil { return nil, fmt.Errorf("invalid managed delivery state for %s: %w", entry.Name(), loadErr) } - if state.ActiveIndex < len(state.Slices) { + if state.ActiveIndex < len(state.Slices) || (state.Mode != "" && state.Mode != "NORMAL") { active = append(active, entry.Name()) } } diff --git a/boatstack/delivery_test.go b/boatstack/delivery_test.go index 7653c9c..b20ebd2 100644 --- a/boatstack/delivery_test.go +++ b/boatstack/delivery_test.go @@ -136,6 +136,121 @@ func TestDeliveryGateReceiptsBindTheActiveSliceAndAdvanceOnce(t *testing.T) { } } +func TestRepairObservationPersistsAndSupersedesAffectedGates(t *testing.T) { + repo, feature := activateTwoSliceDelivery(t) + for _, gate := range []string{"test", "review"} { + if _, err := RecordDeliveryGate(DeliveryGateOptions{Repo: repo, Feature: feature, SliceID: "phase-one", Gate: gate, Status: "PASS"}); err != nil { + t.Fatalf("record %s gate: %v", gate, err) + } + } + observation, state, err := RecordChangeObservation(ChangeObservationOptions{ + Repo: repo, Feature: feature, Message: "the modal remains stuck on Gathering", + SourceStage: "review_gate", Expected: "close after success", Actual: "stays pending", + Evidence: "manual reproduction", Classification: "implementation_repair", + }) + if err != nil { + t.Fatal(err) + } + if observation.ID != "CHG-001" || state.Mode != "REWORK" || state.ResumeStage != "BUILD" || state.Slices[0].Status != "BUILD" { + t.Fatalf("unexpected repair state: observation=%#v state=%#v", observation, state) + } + if len(state.SupersededReceipts) != 2 { + t.Fatalf("expected both receipts to be superseded: %#v", state.SupersededReceipts) + } + changes, err := os.ReadFile(filepath.Join(repo, ".product-loop", "features", feature, "changes.md")) + if err != nil || !strings.Contains(string(changes), "CHG-001") || !strings.Contains(string(changes), "the modal remains stuck") { + t.Fatalf("change observation was not durably recorded: %v %s", err, changes) + } + if _, err := readDeliveryReceipt(repo, feature, "phase-one", "test"); err == nil { + t.Fatal("superseded test receipt remained current") + } +} + +func TestRequirementAmendmentBlocksGates(t *testing.T) { + repo, feature := activateTwoSliceDelivery(t) + _, state, err := RecordChangeObservation(ChangeObservationOptions{ + Repo: repo, Feature: feature, Message: "keep the modal open and show a summary", + SourceStage: "build", Expected: "new summary state", Classification: "requirement_amendment", + }) + if err != nil { + t.Fatal(err) + } + if state.Mode != "AMENDMENT_REQUIRED" || state.ResumeStage != "PLAN_GATE" { + t.Fatalf("amendment did not block: %#v", state) + } + if _, err := RecordDeliveryGate(DeliveryGateOptions{Repo: repo, Feature: feature, SliceID: "phase-one", Gate: "test", Status: "PASS"}); err == nil || !strings.Contains(err.Error(), "approved plan amendment") { + t.Fatalf("gate accepted stale intent: %v", err) + } +} + +func TestPublishedChangeRemainsDiscoverableAndCannotResetOriginalDelivery(t *testing.T) { + repo, feature := activateTwoSliceDelivery(t) + for _, gate := range []string{"test", "review"} { + if _, err := RecordDeliveryGate(DeliveryGateOptions{Repo: repo, Feature: feature, SliceID: "phase-one", Gate: gate, Status: "PASS"}); err != nil { + t.Fatal(err) + } + } + if err := MarkDeliveryPublished(repo, feature, "phase-one", "https://example.invalid/pr/1"); err != nil { + t.Fatal(err) + } + // Collapse the unused second fixture slice; publication behavior itself is + // already covered above and the correction boundary only needs a completed parent. + completed, err := LoadDeliveryState(repo, feature) + if err != nil { + t.Fatal(err) + } + completed.Slices[1].Status = "PUBLISHED" + completed.ActiveIndex = len(completed.Slices) + if err := saveDeliveryState(repo, completed); err != nil { + t.Fatal(err) + } + if _, _, err := RecordChangeObservation(ChangeObservationOptions{ + Repo: repo, Feature: feature, Message: "production needs a different success state", + SourceStage: "published", Classification: "requirement_amendment", + }); err != nil { + t.Fatal(err) + } + active, err := ActiveManagedDeliveries(repo) + if err != nil || len(active) != 1 || active[0] != feature { + t.Fatalf("published correction was not discoverable: %#v %v", active, err) + } + lockPath := filepath.Join(repo, ".product-loop", "features", feature, "plan.lock.json") + if err := os.WriteFile(lockPath, []byte("replacement-lock"), 0o644); err != nil { + t.Fatal(err) + } + if err := initializeDeliveryState(repo, feature, filepath.Join(repo, ".product-loop", "features", feature, "plan.md"), lockPath); err == nil || !strings.Contains(err.Error(), "published delivery") { + t.Fatalf("published parent was reset instead of requiring a child: %v", err) + } +} + +func TestVerificationRepairPreservesImplementationAndRerunsGates(t *testing.T) { + repo, feature := activateTwoSliceDelivery(t) + for _, gate := range []string{"test", "review"} { + if _, err := RecordDeliveryGate(DeliveryGateOptions{Repo: repo, Feature: feature, SliceID: "phase-one", Gate: gate, Status: "PASS"}); err != nil { + t.Fatal(err) + } + } + _, state, err := RecordChangeObservation(ChangeObservationOptions{ + Repo: repo, Feature: feature, Message: "the test checks the wrong success state", + SourceStage: "review_gate", Classification: "verification_repair", + }) + if err != nil { + t.Fatal(err) + } + if state.ResumeStage != "TEST_GATE" || state.Slices[0].Status != "BUILD" { + t.Fatalf("verification repair resumed incorrectly: %#v", state) + } + runGit(t, repo, "add", ".product-loop/features/"+feature+"/changes.md") + runGit(t, repo, "commit", "-m", "record verification repair") + if _, err := RecordDeliveryGate(DeliveryGateOptions{Repo: repo, Feature: feature, SliceID: "phase-one", Gate: "test", Status: "PASS"}); err != nil { + t.Fatalf("rerun test gate failed: %v", err) + } + receipt, err := readDeliveryReceipt(repo, feature, "phase-one", "test") + if err != nil || receipt.TriggerObservationID != "CHG-001" || receipt.Attempt != 2 { + t.Fatalf("repair lineage missing from receipt: %#v %v", receipt, err) + } +} + func TestDeliveryGateRejectsChangesOwnedByALaterSlice(t *testing.T) { repo, feature := activateTwoSliceDelivery(t) if err := os.WriteFile(filepath.Join(repo, "second.go"), []byte("package fixture\n"), 0o644); err != nil { diff --git a/boatstack/export.go b/boatstack/export.go index de909fb..0ad3334 100644 --- a/boatstack/export.go +++ b/boatstack/export.go @@ -49,6 +49,10 @@ var claudeVisibleSkills = []claudeSkillSpec{ Name: "build", Description: "Implement the currently approved Boatstack delivery slice.", }, + { + Name: "repair", + Description: "Classify and route a free-form change to an active Boatstack delivery without losing evidence.", + }, { Name: "test-gate", Description: "Validate the active Boatstack delivery slice and record current test evidence.", @@ -225,6 +229,7 @@ func BuildExportBundle(configPath string, config ProjectConfig, rawConfig []byte "auto-plan": "Discover exactly one saved Plan-mode file and refine it into a Markdown-only draft feature package whose canonical structured artifact is plan.md. Run check-plan read-only. Record affected_paths and structured side_effects for external writes; use an immutable target identity, transactional or fix-forward recovery, and destructive=false. Keep internal phases as tasks in one delivery slice. Only when the accepted outcome explicitly needs multiple PRs, declare ordered delivery_slices and assign every task exactly once; plan approval never authorizes publication. Do not implement, create JSON or locks, or imply acceptance. If ready, respond with Plan ready and make Run /plan-gate the one next action. If decisions remain, respond with I need your input and ask only 1-3 material questions.", "plan-gate": "Run check-plan read-only, present its fingerprint and all open decisions, and 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 resolved human, RFC3339 timestamp, and exact displayed fingerprint so it writes only approval.md. While pending, respond Ready for your approval and render the one next action as: Reply `a` to approve. After recording, respond Approved — ready to build and make entering the host execution mode and running /build the one next action. Remain in Plan mode; do not compile or request an early mode switch.", "build": "First confirm the host is in an execution-capable mode. If the mode transition is rejected or product-code writes remain unavailable, return READY_FOR_BUILD internally without activating the plan, compiling JSON, or writing a lock. Only then locate plan.md and approval.md and run activate-plan before the first product-code edit. Stop if it reports BLOCKED. Read delivery-status and implement only the active delivery slice task_ids. 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 approved 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": "Read delivery-status, the current plan lock and acceptance criteria, the actual diff, and current receipts. Compare the user's exact free-form request with approved intent. Classify it as implementation_repair, verification_repair, review_repair, requirement_amendment, or needs_clarification, then invoke record-change before any product edit. Same-intent repairs may proceed at the returned RESUME_STAGE; requirement amendments and ambiguous intent must stop for a concise plan amendment or one clarifying question. A published delivery is immutable: plan its correction under a new feature id with parent_delivery set to the published feature. Never edit changes.md or managed delivery state directly. After a repair, reuse the existing /test-gate and /review-gate; do not invent repair-specific gates.", "test-gate": "Read delivery-status and test only the active delivery slice. Run the internal repository safety check, build a requirement-to-evidence matrix, and treat self-authored tests as evidence rather than the sole oracle. External writes require immutable target identity, transactional or fix-forward failure behavior, and an independent safety oracle. Commit the intentional slice product and evidence diff, then record-delivery-gate for the active feature and slice with --gate test and PASS or PASS_WITH_GAPS. Editing evidence Markdown alone never passes the gate. On pass respond Tests passed and make Run /review-gate the one next action. On failure respond Testing found a problem and make the required non-destructive repair the one next action.", "review-gate": "Read delivery-status and review the active slice's actual diff against approved intent, invariants, risks, gaps, and test evidence. Run the internal repository safety check. Executable destructive capability is blocking even when ordinary tests pass. On pass invoke record-delivery-gate for the same feature and slice with --gate review; it must reject a changed or untested diff. Then respond Review passed and make Run /ship-gate the one next action. When blocked respond Changes required and make the highest-priority blocking repair the one next action.", "ship-gate": "Prepare a reviewer-ready PR only; do not merge or deploy without separate authorization. Require the current managed feature approval, lock, test evidence, review evidence, and a passing repository safety scan, and commit the intentional product/artifact diff before projection. Internally run pr-context --repo . --feature in json and template formats, project the approved intent, actual committed diff, decisions, evidence, gaps, rollout, rollback, safety outcome, and operator-only recovery boundary into its required pr.md path, then run check-pr --repo . --preview . Always include why, what changed, review order, evidence, gaps/risks, rollout/rollback, and collapsed provenance; add UI evidence, security/privacy, migration, or operations sections only when the diff makes them relevant. Show the exact title and rendered body before any GitHub mutation. If PR_ACTION is open, respond PR ready and render the one next action as: Reply `o` to open PR. If update, render: Reply `u` to update PR. If manual, preserve the preview and give one manual publication action. Continue accepting the full replies open PR and update PR for compatibility without advertising them. Only after the matching state-scoped shortcut or compatible full reply: commit only the reviewed pr.md, rerun check-pr and require the same preview fingerprint (PREVIEW_FINGERPRINT), then run publish-pr with --action open or update and that fingerprint. The publisher performs a non-force push and rechecks context before GitHub mutation. If the diff or evidence changes, regenerate instead. If a required check fails on the base branch too, record the evidence and recommend a separate repair PR. Never edit unrelated code in this approved feature branch; a policy-approved bypass requires explicit human authorization. After publication respond PR opened with the link and make Review the PR the one next action; never imply merge authorization. If publish-pr returns UPDATE_AVAILABLE, keep Review the PR as the only next action and append a collapsed update notice saying no files changed and /boatstack-update may be run from the clean default branch after this feature PR merges. Do not check for releases before successful publication.", @@ -236,15 +241,16 @@ func BuildExportBundle(configPath string, config ProjectConfig, rawConfig []byte if contains(adapters, "cursor") { rule := `--- -description: Use Boatstack for evidence-engineered planning, explicit approval, open implementation, evidence gates, and PR preparation. +description: Use Boatstack for evidence-engineered planning, delivery repair, explicit approval, open implementation, evidence gates, and PR preparation. globs: -alwaysApply: false +alwaysApply: true --- The source of truth is @.product-loop/workflow.md and @.product-loop/project.json. Use @.product-loop/artifacts.md for document meanings and @.product-loop/failure-moves.md for improvement experiments. Ordinary product intent starts in the host's Plan mode. Save the completed plan under .product-loop/intake/. Auto-plan discovers exactly one saved plan from bounded host locations, validates it, and must not invent a substitute. Keep the source plan present and current through build. Do not start build work until the explicit plan gate has produced approval.md and build activation has produced a valid plan lock. +Before modifying product code, check for an active managed delivery. When one exists and the user reports a problem or requests a modification in ordinary language, route through the Boatstack repair operation before editing. The repair operation records the exact request, compares it with approved intent, and either resumes the earliest affected stage or blocks for a plan amendment. If no managed delivery exists, continue ordinary conversation. Implementation methods are open. Claims of completion, approval, review, and shipping require evidence. Plans may contain internal task phases without changing the one-PR flow. Multiple PRs require explicit ordered delivery_slices. Work only on the active slice; every slice must independently pass test-gate, review-gate, and confirmed ship-gate. Direct push and PR mutation are denied while managed delivery is active, and plan approval is never publication authority. When the user naturally asks Boatstack to prepare, improve, summarize, or update an existing PR without a managed feature package, generate an evidence-limited ad-hoc PR brief. Use the committed branch diff and observed checks, label missing evidence NOT_VERIFIED, and never imply Boatstack approval or passed gates. This is natural-language behavior, not a /pr-brief command. Preview the exact title and body before asking for one open/update confirmation. @@ -263,12 +269,12 @@ Boatstack's repository hooks deny high-confidence irreversible operations across adapterSkill := fmt.Sprintf(`--- name: %s -description: Use when the user asks Boatstack to auto-plan, approve a plan, build, test, review, ship, update Boatstack, or run a retrospective. +description: Use when the user asks Boatstack to auto-plan, approve a plan, build, repair or modify an active delivery, test, review, ship, update Boatstack, or run a retrospective. Also use automatically when ordinary free-form change language targets an active managed delivery. --- # Boatstack adapter -Read .product-loop/project.json and .product-loop/workflow.md. The requested operation is supplied by the user; valid operations are auto-plan, plan-gate, build, test-gate, review-gate/review, ship-gate/ship, boatstack-update, and retro. +Read .product-loop/project.json and .product-loop/workflow.md. Valid operations are auto-plan, plan-gate, build, repair, test-gate, review-gate/review, ship-gate/ship, boatstack-update, and retro. Before any product edit, check for an active managed delivery. If one exists and ordinary user language reports a problem or asks for a modification, automatically use repair even when the user did not name the operation. 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. diff --git a/boatstack/export_test.go b/boatstack/export_test.go index 73764eb..7bef03f 100644 --- a/boatstack/export_test.go +++ b/boatstack/export_test.go @@ -99,6 +99,7 @@ func TestExportAndDriftCheck(t *testing.T) { } for _, path := range []string{ ".cursor/commands/boatstack-update.md", + ".cursor/commands/repair.md", ".cursor/commands/plan-gate.md", ".cursor/commands/review.md", ".claude/skills/boatstack/SKILL.md", @@ -170,6 +171,7 @@ func TestExportAndDriftCheck(t *testing.T) { "auto-plan": {"Plan ready", "I need your input"}, "plan-gate": {"Ready for your approval", "Approved — ready to build"}, "build": {"Build complete", "Build needs a decision"}, + "repair": {}, "test-gate": {"Tests passed", "Testing found a problem"}, "review-gate": {"Review passed", "Changes required"}, "review": {"Review passed", "Changes required"}, @@ -254,6 +256,11 @@ func TestExportAndDriftCheck(t *testing.T) { } } cursorRule := string(bundle.Files[".cursor/rules/boatstack.mdc"]) + for _, expected := range []string{"alwaysApply: true", "Before modifying product code", "active managed delivery", "repair operation", "ordinary language"} { + if !strings.Contains(cursorRule, expected) { + t.Fatalf("Cursor rule is missing conversational repair routing %q", expected) + } + } for _, expected := range []string{"delivery_slices", "active slice", "Direct push and PR mutation", "plan approval is never publication authority"} { if !strings.Contains(cursorRule, expected) { t.Fatalf("Cursor rule is missing phase-scoped delivery rule %q", expected) @@ -264,6 +271,18 @@ func TestExportAndDriftCheck(t *testing.T) { t.Fatalf("Cursor rule is missing ad-hoc PR behavior %q", expected) } } + repair := string(bundle.Files[".cursor/commands/repair.md"]) + for _, expected := range []string{"record-change", "implementation_repair", "verification_repair", "requirement_amendment", "needs_clarification", "/test-gate", "/review-gate"} { + if !strings.Contains(repair, expected) { + t.Fatalf("repair adapter is missing %q", expected) + } + } + for _, path := range []string{".claude/skills/boatstack/SKILL.md", ".agents/skills/boatstack/SKILL.md"} { + router := string(bundle.Files[path]) + if !strings.Contains(router, "automatically use repair") || !strings.Contains(router, "active managed delivery") { + t.Fatalf("%s does not auto-route free-form delivery changes", path) + } + } prTemplate := string(bundle.Files[".github/PULL_REQUEST_TEMPLATE/boatstack.md"]) for _, expected := range []string{"## Why this change", "## What changed", "## Review order", "## Evidence", "## Operational safety", "## Known gaps and risks", "## Rollout and rollback", "Boatstack provenance"} { if !strings.Contains(prTemplate, expected) { diff --git a/boatstack/references/artifacts.md b/boatstack/references/artifacts.md index f349e23..a5fb532 100644 --- a/boatstack/references/artifacts.md +++ b/boatstack/references/artifacts.md @@ -14,6 +14,8 @@ Artifacts separate facts, decisions, unknowns, incompleteness, and evidence. Com | Approval receipt | Named human, timestamp, and fingerprint in Markdown; not executable state | The exact draft is explicitly approved in Plan mode | | Compiled tasks | Deterministic dependency graph generated from the approved Markdown plan | Build activation succeeds | | Delivery state | Ignored worktree-local Git active-slice state bound to the approved plan lock; never an approval artifact | Build activation and successful slice publication | +| `changes.md` | Append-only, reviewable post-build observations with exact user message, expected/actual behavior, classification, evidence, and resolution | Controlled `record-change` transition | +| Repair state | Ignored delivery mode, resume stage, active observation, attempt count, and superseded receipt references | Controlled repair and gate transitions | | Gate receipt | Machine-local test or review transition bound to one delivery slice, base/head branches, commit, product diff, and evidence hash | A slice passes test or review | | Test plan | Requirement-to-evidence mapping with each validation's origin, falsifiable oracle, procedure, and independence | Planning and after discovered failure modes | | Gap ledger | Known divergence between desired and current state | Work is deferred, partial, incompatible, or intentionally absent | diff --git a/boatstack/references/workflow.md b/boatstack/references/workflow.md index 46fa86b..754fd51 100644 --- a/boatstack/references/workflow.md +++ b/boatstack/references/workflow.md @@ -14,6 +14,7 @@ INTENT -> BUILD_ACTIVATION -> PLAN_LOCKED -> BUILD + -> REPAIR (when ordinary conversation reveals a change) -> TEST_GATE -> REVIEW_GATE -> SHIP_GATE @@ -23,6 +24,10 @@ INTENT Each transition emits an artifact and evidence. A host adapter may change how a command is invoked, but it must not skip a transition or redefine a gate. +After build activation, persistent host adapters route ordinary change language through `REPAIR` before product edits. Same-intent implementation, verification, and review repairs resume at the earliest affected stage and supersede only downstream receipts. Changed or ambiguous intent enters `AMENDMENT_REQUIRED` and cannot pass a gate until a newly approved plan revision is activated. Existing `/test-gate` and `/review-gate` operations remain rerunnable; there are no repair-specific gates. + +A published delivery cannot be reset. Its correction uses a new feature id and declares `parent_delivery` as the published feature, producing a separate plan lock, delivery state, receipts, and PR while preserving the original evidence. + 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 @@ -63,6 +68,7 @@ Lead with a plain outcome, never a machine code such as `PASS`, `PLAN_APPROVED`, | `auto-plan` ready / needs answers | **Plan ready** -> run `/plan-gate`; **I need your input** -> answer with the displayed choice keys or `r` for all recommendations | | `plan-gate` pending / approved | **Ready for your approval** -> reply `a` to approve; **Approved — ready to build** -> enter execution mode and run `/build` | | `build` success / paused | **Build complete** -> run `/test-gate`; **Build needs a decision** -> answer the blocking question | +| `repair` same intent / amendment | **Repair recorded** -> perform the reported resume stage; **Plan amendment required** -> review the proposed intent delta | | `test-gate` pass / blocked | **Tests passed** -> run `/review-gate`; **Testing found a problem** -> perform or authorize the repair | | `review-gate` pass / blocked | **Review passed** -> run `/ship-gate`; **Changes required** -> address the blocking finding | | `ship-gate` preview / published | **PR ready** -> reply `o` to open or `u` to update the previewed PR; **PR opened** -> review the PR; never imply merge authorization | diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index a7a64f7..604d315 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 **9622 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 **9951 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 [`697ce2f7d98eb3bfb249b33d3556efbd7e9365b1`](https://github.com/operatorstack/intelligence-flow/tree/697ce2f7d98eb3bfb249b33d3556efbd7e9365b1/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 [`bbed9588e74016250e3cf8968e70bdd375afb863`](https://github.com/operatorstack/intelligence-flow/tree/bbed9588e74016250e3cf8968e70bdd375afb863/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 60b1177..c675e2f 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "697ce2f7d98eb3bfb249b33d3556efbd7e9365b1", + "source_commit": "bbed9588e74016250e3cf8968e70bdd375afb863", "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:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" + "last_verified_version": "source:bbed9588e74016250e3cf8968e70bdd375afb863" }, { "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:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" + "last_verified_version": "source:bbed9588e74016250e3cf8968e70bdd375afb863" }, { "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:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" + "last_verified_version": "source:bbed9588e74016250e3cf8968e70bdd375afb863" }, { "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:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" + "last_verified_version": "source:bbed9588e74016250e3cf8968e70bdd375afb863" }, { "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:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" + "last_verified_version": "source:bbed9588e74016250e3cf8968e70bdd375afb863" }, { "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:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" + "last_verified_version": "source:bbed9588e74016250e3cf8968e70bdd375afb863" }, { "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:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" + "last_verified_version": "source:bbed9588e74016250e3cf8968e70bdd375afb863" }, { "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:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" + "last_verified_version": "source:bbed9588e74016250e3cf8968e70bdd375afb863" }, { "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:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" + "last_verified_version": "source:bbed9588e74016250e3cf8968e70bdd375afb863" }, { "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:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" + "last_verified_version": "source:bbed9588e74016250e3cf8968e70bdd375afb863" }, { "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:697ce2f7d98eb3bfb249b33d3556efbd7e9365b1" + "last_verified_version": "source:bbed9588e74016250e3cf8968e70bdd375afb863" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 316c9b9..c5cb08e 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": "697ce2f7d98eb3bfb249b33d3556efbd7e9365b1", + "source_commit": "bbed9588e74016250e3cf8968e70bdd375afb863", "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-18-conversational-delivery-repair.md b/release-notes/2026-07-18-conversational-delivery-repair.md new file mode 100644 index 0000000..5db866b --- /dev/null +++ b/release-notes/2026-07-18-conversational-delivery-repair.md @@ -0,0 +1,11 @@ +### Conversational delivery repair + +Boatstack now keeps the delivery connected when a developer spots a defect, missing check, visual issue, or changed requirement after Build has started. + +- Ordinary chat can route changes into the active managed delivery without requiring the developer to reconstruct context or learn a separate repair process. +- Each observation is recorded outside the conversation with its classification, evidence, repair attempt, and earliest safe resume stage. +- Existing test and review gates are rerunnable; stale receipts are superseded rather than erased. +- Material requirement changes return to human approval and receive a new plan lock. +- Published deliveries remain immutable; later corrections use a linked child delivery with independent gates. + +The public README now explains this repair loop with a compact decision table and lifecycle diagram.