diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 265ad25..13949c0 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/146f50c0f3309e592ed45374b8cddb3d8ed64d85/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/e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a/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 a2191ba..79feb9b 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -12,7 +12,7 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "c3334d810923b2cf4a745dae0aecc859d5de7cdafc94c0d193fb7d7b08892c53", + "CONTRIBUTING.md": "c6a8df3d94c8dd4228cbd6e1d85223ebd3f64613b31a534ee34f9d12c4ccee1f", "README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", @@ -42,9 +42,9 @@ "boatstack/capture_test.go": "7072d0c5f9ab0ea9ef493f9e6dcb01cf0de544514c8201e30b8e4e6a4065505a", "boatstack/changelog.go": "6b06be7cd9738de29ba6e87aa2569f3b027a2e618b04524f5abd7abaa17945bf", "boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7", - "boatstack/cmd/boatstack-helper/coverage_conformance_test.go": "f5a931d2b5cdb0d32af85acbbd499fa0f509ed36d62e772e6b6072e348200aca", + "boatstack/cmd/boatstack-helper/coverage_conformance_test.go": "5c10219591d5b9998361ed83eac88debb4d4c338c835c7502b709ed81cf151b6", "boatstack/cmd/boatstack-helper/flow.go": "0d41c7a86b49004e897f59551780220841d5941396202c81de97a4d52f593520", - "boatstack/cmd/boatstack-helper/main.go": "e69f021955d13160e19c53e4fd2c11cde871821b2d25932ebbe299b778ed77b4", + "boatstack/cmd/boatstack-helper/main.go": "903a233fdbe706e9281b9840abda458e78035b8379a99c9c97dd922832430a64", "boatstack/cmd/boatstack-helper/main_test.go": "b36c52d6d5c9dd2428730de10ff18194b7e32a98722e41341c301c6f7a04cad5", "boatstack/cmd/boatstack-helper/retro.go": "68b83e33ade5b5fec126c70ec798fdcbed22fda755dc1cad2758143fead8e187", "boatstack/command.go": "4726ac515dedab4947be7eb48f88c6cb8b53d674124504b69f03e6396b080ee8", @@ -68,7 +68,7 @@ "boatstack/denial_escalation_conformance_test.go": "d50f0e1c803c8f5731a46dbe6f82c7935c0ccbd582f513cbe07211f5e902157e", "boatstack/denial_ledger.go": "a35bf8fd8c1f6b9302109cf0087e1b9158e43e07b92b18df5e589a637d58491d", "boatstack/denial_solutions.go": "ae6cb218c01a89c14367242b21e2b96d85e1fda357076b7bffedb56066ef961b", - "boatstack/denial_solutions_conformance_test.go": "5371654664318b9fbef65a9999cedf04f893517107edd033c77511753830f9e2", + "boatstack/denial_solutions_conformance_test.go": "3adf1c4e5d4ef1a8775c99648a9597e01f5a2e3b4af73d3587b9d6764e18127b", "boatstack/denial_test.go": "9dc9f0f79328c4947073efaa785479b34e70eb214da57cd72348f39fd672e4fd", "boatstack/detached.go": "b0ea2a1f31bf2a2a83f6089a3065a9b47221194b64de48af9120046e6d70dee8", "boatstack/detached_test.go": "6cc70d15baa9a69afacf66ea29ce112efeb166836acb0a52bf9c4bb4c898cee5", @@ -78,11 +78,11 @@ "boatstack/export_test.go": "dce5aa3ab5499c82d05859cf86b46dfcee308482491366d83e10ca3fb8605bb6", "boatstack/flow_coding.go": "9fa53a0204f98a25f97775c3acf37392a591c14ce850b44aa587b5806e770bb9", "boatstack/flow_coding_test.go": "dddcd7a85892d4fa10af42739d4c1ff265721b0313e27b6e7a1bbb019d5c3b51", - "boatstack/flow_control.go": "363a89b24171f023e71aaeac875765d7c98a050f56f4b86afa8a9d96ea31f983", + "boatstack/flow_control.go": "c9845ef3d87f41257a444f033b93eed72a0f4766b8e539825626ef01ff3b7685", "boatstack/flow_control_test.go": "02d788c83be55ebd79ffc73875bfd019de45325151eb1f70f506980eb8e77f29", "boatstack/flow_drive.go": "90f57e178884aff017195a126954ac0aeb85f27707b9d42844323341dc1fefd9", "boatstack/flow_drive_conformance_test.go": "23edea926c271a1f5718fb9dae1da11e4bf03cceb1357290cd61cd8ffb73beda", - "boatstack/flow_frontier.go": "57e860c1613f3d5cca2fde56fb6e7dad527a71ac148ee3617ceb4bccd9cf805a", + "boatstack/flow_frontier.go": "c69a225abd32306ce3da94e7566cc99ef28b098230a9c965294d7bc5f0f7507c", "boatstack/flow_frontier_conformance_test.go": "771838f06d6157547c1277eca1f7c1df609bb9f3ae630b2fa509f641b49aba86", "boatstack/flow_guard.go": "dd18524d95f4a220cfd3d11b11003dacc52120785ee0ccdbeceb2307fab55872", "boatstack/flow_guard_test.go": "8ba75f11ddd080427c15bd7e25f7d03c1b746a2f587c212cea0e710337d1c0e1", @@ -148,8 +148,8 @@ "boatstack/mutation_test.go": "68d5049c7f96c1ac558e4c781151f67e8deee2f8d6b9bf293b90d44e769ef7c6", "boatstack/mutation_undo.go": "697d11b600a276ddbcabe6a9f8040d4f7283e017a0e8fd689ef53a274638946c", "boatstack/mutation_undo_test.go": "39540e717e3f2136bf975594043a3db9072b28ebe61c6cb0b982cea5e8b1e14e", - "boatstack/next.go": "d66e9303c52cac43a1ecc928b64eeedb5a596de271f0f214084b92a270b53dcc", - "boatstack/next_actor_conformance_test.go": "c881e26fc68a84b2a2e6648cc9a3921abcdce76229013bbe13ad69ed6c47a383", + "boatstack/next.go": "6a8936df6ddf5444d5fe61d9af15cf52948b405689f833e659b5d8de92d4fd01", + "boatstack/next_actor_conformance_test.go": "8759285ed6133c99a3282bc597ca3f774fc051f414fb55e6fc59bfa5a30e22c9", "boatstack/next_banner_test.go": "c431a6987ed1e479442fc9f5db4371632880b92aa790fa9dd0f5285293352c41", "boatstack/next_response.go": "11decf2e3b236cbaa183980946ec17ffbbbb1af9c08bd11a466a8487bf229d5f", "boatstack/next_response_conformance_test.go": "be4f3bc7507abfb0ae9f86310eb29e34b166dcc40b6fa103e05babb81f2bd928", @@ -165,7 +165,7 @@ "boatstack/planning_first_write_conformance_test.go": "873097aa9384b75bf01e74a475f3ec2ac7cca4a28f733e82f2c82959032c6a30", "boatstack/planning_test.go": "06ec7022222d926040c3ae28b84ab50c3d2f804ae6473e61b303804dd992d884", "boatstack/post_publish_prescribe_conformance_test.go": "3c20d359ff84648db7dedb227b4d64e6574d9f41d3cdca0adefec1c60bfbf4ae", - "boatstack/pr.go": "c389bd5fddf788290069eb858e1062433d1ff4dff928855a5211ac470c67ccd7", + "boatstack/pr.go": "57a9f63fc622dc76bd2382cf2ccb9099b447d37eafadc457e3743217eba7a714", "boatstack/pr_phase.go": "59f8cbb75b6b538a5345474acd6a725450979579bf8ecf9591956cbbe1cc4737", "boatstack/pr_phase_conformance_test.go": "bc9c834e9c4ed43b35d81abafd7b1bf2a264ea2a8c4a4ec9758ee18d1d438968", "boatstack/pr_test.go": "5c0ff03eb21e383026a4e9fbc5671b2e316040e4e4c55ab6583b930e7e117dda", @@ -207,7 +207,7 @@ "boatstack/skill_frontmatter.go": "73364df463ce828c2d005aab55f72bb92f7a34d99cf3f53d4e0cd5a4da9dbd0e", "boatstack/skill_frontmatter_test.go": "a3ec52e7df357a72265c95dd66db15d9c0effc7e5f90f14ce69c27792ce394eb", "boatstack/solution_closure_conformance_test.go": "f73e6748dac373e2a10bc9269c2f2e060bd220bb4113bd0d5ff66ca4c8e91a54", - "boatstack/statemap.go": "27aabde21c5dbfa9526f1533578c1e512ddd3174606457b67da344c8a6c36733", + "boatstack/statemap.go": "0c36ea26e5858a90bd81330a352c08b917c04e385151f4ae62cd0ec4ee2af8a6", "boatstack/statemap_conformance_test.go": "e418f88bd91ab466bc64d0965ff5c404d7810479de509b01ec345826dfef978a", "boatstack/supervisory_control_test.go": "c7ea4bcd678e8ec211dac772c834981c4e21762914be2770a5e181bc24605e06", "boatstack/testdata/reviewer-pr-body.md": "4c64e3788e5d61a377aeb0f797f7fc8d2316ab6e49572d15636eea7ba9e34ac4", @@ -217,8 +217,9 @@ "boatstack/update_publication.go": "5c1ac8445345c6546d165b9321a736453b313ced96a0059465b8ad637498bac6", "boatstack/update_publication_test.go": "c5f32578db53be65e35452d5e8b4520884354e4e80a370dcec19860ea644d091", "boatstack/update_test.go": "bf5f19f8499db6dd7356917d867b113b790548ab89bfdea59e2adf4999a82a6a", + "boatstack/visual_attach_conformance_test.go": "a376f032cfc59615fe4d403bdb8d7c7d3972dc7de334c3f6763edf1531ff1f03", "boatstack/visual_evidence.go": "f9fbde0e89f0ce8d9503939197d9b92cc8da377053007371d28360e29715a84a", - "boatstack/visual_evidence_test.go": "0fe8f5154ef4398dfeba5e7f7b387b2d279ed75635ea35d93ff392269f2cc6d0", + "boatstack/visual_evidence_test.go": "b2e50093080d4c2381c8f441a4e1cfeca714fa3d034e7536835b7077fad0556a", "boatstack/visual_publisher.go": "ec5e95228b48e4ec20731975606048f5e732c4e09a7fd175770d4b15e447cc88", "boatstack/visual_publisher_test.go": "979f600edae00c77569ae753b80530e8cbf2e3b342995efcd992d351ff382eaf", "boatstack/workspace.go": "79f472d0d10794bf5193518d0c2798d2f5e7e530226e02fb33f99840110f995f", @@ -231,10 +232,10 @@ "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", "docs/configuration.md": "8c5b0a6a8394333165dd0b2b1a70ca74e9d53f784b478e2ef7f1b12d93bc03b0", - "docs/evidence-engineered-coding.md": "27da8a87d58783dc2ac2ad72cb2265cbdbab456f41fb4e09f261dbc44d90cd12", + "docs/evidence-engineered-coding.md": "a0bc4ce88390774ea8ad188173264024505dc84e554820b3b219944e11cfe870", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", "docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8", - "docs/public-claims.json": "3bf81d5a1ca4371fc34a6b25eddd156da6c3e1cda728785cf3a0559a0d367596", + "docs/public-claims.json": "dd049cd3c8e23362bdd0bfcf2090015432f309e6298ab8b45b67741b780d170a", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -248,7 +249,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": "2c861b4174ba2e09d859ff9463f62e0e5ffb4198db121cb426246b206cc697a4", + "labs/diagram-json/plan.lock.json": "eebacf19b2b399d8cb6bbd3aad96e9e1705e369b69561f8841aeeb06f5d68e03", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -392,12 +393,13 @@ "release-notes/2026-07-29-readiness-and-journey-control.md": "2411db76974990ad0128fcd90e795c4c1c5c70d6d6f15fccd452f7e1a242310b", "release-notes/2026-07-30-auto-capture-on-ship.md": "49aa077ac52005d380ba2cae9e406c7d0a9941157c40aa4ed44b9e59195b3837", "release-notes/2026-07-30-plan-approved-scenarios-escalate-to-require.md": "21c3f2be51b834fc2eb7662236db2549e80b1a1a0665a0721e773b7736e2c079", + "release-notes/2026-07-30-visual-attach-retry-prescription.md": "38b610685a7a62c0341ba21b273b42c24d82ca803de6d8f694754156eaa606a9", "release-notes/2026-07-30-visual-evidence-survives-preview-commit.md": "71d19e9fabb40e8cb1e939f26bf288911d227979926f43f337046c6cf6b66551" }, "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "146f50c0f3309e592ed45374b8cddb3d8ed64d85", + "commit": "e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/cmd/boatstack-helper/coverage_conformance_test.go b/boatstack/cmd/boatstack-helper/coverage_conformance_test.go index cfce070..452a70a 100644 --- a/boatstack/cmd/boatstack-helper/coverage_conformance_test.go +++ b/boatstack/cmd/boatstack-helper/coverage_conformance_test.go @@ -63,6 +63,7 @@ var nonDeliveryVerbs = map[string]bool{ "provision-capability": true, "capability-register": true, "record-pr-visual-publication": true, + "attach-evidence": true, // PR construction / verification helpers reached around the ship gate. "check-pr": true, // Detached Supervision lifecycle (control-plane ownership, not delivery moves). diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go index 2658c84..0e25bcd 100644 --- a/boatstack/cmd/boatstack-helper/main.go +++ b/boatstack/cmd/boatstack-helper/main.go @@ -705,6 +705,28 @@ func recordPRVisualPublicationCommand(arguments []string) int { return 0 } +func attachEvidenceCommand(arguments []string) int { + flags := flag.NewFlagSet("attach-evidence", flag.ContinueOnError) + repo := flags.String("repo", ".", "repository whose Git-common state owns the evidence") + feature := flags.String("feature", "", "managed Boatstack feature slug") + if err := flags.Parse(arguments); err != nil { + return 2 + } + if *feature == "" { + return fail(fmt.Errorf("attach-evidence requires --feature")) + } + manifest, err := boatstack.RetryVisualAttachment(*repo, *feature, boatstack.SelectVisualPublisher(*repo)) + if err != nil { + return fail(err) + } + value, err := boatstack.MarshalJSON(manifest) + if err != nil { + return fail(err) + } + fmt.Print(string(value)) + return 0 +} + func deliveryStatusCommand(arguments []string) int { flags := flag.NewFlagSet("delivery-status", flag.ContinueOnError) repo := flags.String("repo", ".", "repository containing the managed delivery") @@ -1487,7 +1509,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] { @@ -1567,6 +1589,8 @@ func run() int { return capabilityRegisterCommand(os.Args[2:]) case "record-pr-visual-publication": return recordPRVisualPublicationCommand(os.Args[2:]) + case "attach-evidence": + return attachEvidenceCommand(os.Args[2:]) case "pr-context": return prContextCommand(os.Args[2:]) case "check-pr": diff --git a/boatstack/denial_solutions_conformance_test.go b/boatstack/denial_solutions_conformance_test.go index a765741..5097c32 100644 --- a/boatstack/denial_solutions_conformance_test.go +++ b/boatstack/denial_solutions_conformance_test.go @@ -123,7 +123,7 @@ func TestTamperDenialNamesDeclaredOwnerVerbs(t *testing.T) { ".git/boatstack/mutations/v1/abc.json": {"activate-plan", "undo"}, ".git/boatstack/quarantine/demo/receipt.json": {"repair-state"}, "state-root/boatstack/registry.json": {"attach", "detach"}, - ".git/boatstack/visual-evidence/x/manifest.json": {"record-pr-visual-evidence", "capture-evidence", "record-pr-visual-publication"}, + ".git/boatstack/visual-evidence/x/manifest.json": {"record-pr-visual-evidence", "capture-evidence", "record-pr-visual-publication", "attach-evidence"}, "boatstack/repositories/sample/binding.json": {"attach", "detach", "activate"}, } for attempted, want := range cases { diff --git a/boatstack/flow_control.go b/boatstack/flow_control.go index 83adb0b..0e90b21 100644 --- a/boatstack/flow_control.go +++ b/boatstack/flow_control.go @@ -105,6 +105,11 @@ const ( // control-law: merged-terminal-prescribes-merge-never-executes-it MarkerPublishedWatch = deliverycontrol.TransitionID("published.watch_checks") MarkerPublishedMerge = deliverycontrol.TransitionID("published.merge") + // MarkerPublishedAttach names the owed-attachment retry of a published + // PR's visual-evidence comment. Unlike the merged-terminal markers above + // it fires under BOTH terminals: attaching evidence completes the + // publication itself, it is not merge pursuit. + MarkerPublishedAttach = deliverycontrol.TransitionID("published.attach_evidence") ) // NextActor names who performs the prescribed next step. The operator owns a @@ -157,6 +162,15 @@ func classifyNextActor(status NextStatus, next FlowNext) NextActor { status.ObservedStage == "PUBLISHED" && status.Lifecycle == "PUBLISHED_MERGED": return NextActorNone case status.ObservedStage == "PUBLISHED": + // An owed visual attachment splits by what it owes: a transient + // publisher failure (visual_pending) is work-derivable — the agent + // retries attach-evidence; manual_required owes operator authority (a + // signed-in browser or an external-host opt-in) and stays theirs. A + // fired goal escape still demotes unconditionally. + // control-law: turn-ends-only-at-the-operator-frontier + if status.Lifecycle == "PUBLISHED_OPEN" && status.GoalEscape == "" && status.VisualPublication == "visual_pending" { + return NextActorAgent + } // Under the default published terminal, reviewing the open pull // request is the operator's act — unchanged. Under the merged // terminal, the frontier extends: the phases whose next step is @@ -438,6 +452,45 @@ func prescribePlanning(repo string, status NextStatus) (*PrescribedCommand, stri } } +// prescribeVisualAttach closes the owed-attachment gap of a published-open +// PR so the flow never goes dark on visual_pending or manual_required. It +// fires under BOTH terminals — the attachment completes publication, it is +// not merge pursuit. visual_pending prescribes the attach-evidence retry +// (work-derivable); manual_required prescribes recording the manually +// attached comment, owing the operator-observed URL. A fired goal escape +// prescribes nothing, exactly like the post-publish layer. +// control-law: prescriptive-closure-every-stage-names-a-runnable-command +func prescribeVisualAttach(repo string, status NextStatus) (*PrescribedCommand, string) { + if status.ObservedStage != "PUBLISHED" || status.Lifecycle != "PUBLISHED_OPEN" || status.Feature == "" || status.GoalEscape != "" { + return nil, "" + } + var repoArgs []string + if repo != "" && repo != "." { + repoArgs = []string{"--repo", repo} + } + switch status.VisualPublication { + case "visual_pending": + cmd := &PrescribedCommand{ + Verb: "attach-evidence", Args: append(repoArgs, "--feature", status.Feature), + AutoDerivable: true, Transition: MarkerPublishedAttach, + } + return cmd, "The PR is open; only its Boatstack visual-evidence comment is owed. If the publisher keeps failing, attach the fingerprinted PNGs manually and record the URL with record-pr-visual-publication." + case "manual_required": + cmd := &PrescribedCommand{ + Verb: "record-pr-visual-publication", Args: append(repoArgs, "--key", status.Feature), + RequiresHumanInput: []string{"--comment-url"}, + Transition: MarkerPublishedAttach, + } + if strings.TrimSpace(status.PRURL) != "" { + cmd.Args = append(cmd.Args, "--pr-url", status.PRURL) + } else { + cmd.RequiresHumanInput = append(cmd.RequiresHumanInput, "--pr-url") + } + return cmd, "No automatic publisher is available here: attach the fingerprinted PNGs to one PR comment yourself, then record the observed comment URL." + } + return nil, "" +} + // prescribePostPublish closes the prescriptive loop past publish, but ONLY // under the merged terminal: with the published default this function returns // nothing and post-publish behavior is exactly what it always was. The @@ -606,10 +659,17 @@ func nextControlFromStatus(repo string, status NextStatus) (FlowNext, error) { } } } - // Past publish the oracle sits at its sink and prescribes nothing; under - // the merged terminal the observation-derived post-publish layer takes - // over. It fills only an empty prescription — it can never override an - // oracle move. + // Past publish the oracle sits at its sink and prescribes nothing. An + // owed visual attachment is consulted first and under BOTH terminals — + // it completes the publication itself — then, under the merged terminal + // only, the observation-derived post-publish layer. Each fills only an + // empty prescription — neither can override an oracle move. + if out.Prescribed == nil { + if cmd, followUp := prescribeVisualAttach(repo, status); cmd != nil { + out.Prescribed = cmd + out.FollowUp = followUp + } + } if out.Prescribed == nil { if cmd, followUp := prescribePostPublish(repo, status, out.Terminal); cmd != nil { out.Prescribed = cmd diff --git a/boatstack/flow_frontier.go b/boatstack/flow_frontier.go index 84f9b87..59e8689 100644 --- a/boatstack/flow_frontier.go +++ b/boatstack/flow_frontier.go @@ -86,7 +86,7 @@ func ResolveFrontier(repoPath string) (FlowFrontier, error) { continue } branch, _, prURL := deliveryBranchAndSlice(state) - status := publishedNextStatus(state, observePRTarget(repo, prURL, branch), resolveDeliveryTerminal(repo, state.Feature)) + status := publishedNextStatus(state, observePRTarget(repo, prURL, branch), resolveDeliveryTerminal(repo, state.Feature), observeVisualPublication(repo, state.Feature)) frontier.Rows = append(frontier.Rows, frontierRowFromStatus(repo, status)) } for _, row := range frontier.Rows { diff --git a/boatstack/next.go b/boatstack/next.go index b9e4065..eb85659 100644 --- a/boatstack/next.go +++ b/boatstack/next.go @@ -35,6 +35,9 @@ type NextStatus struct { PRURL string `json:"pr_url,omitempty"` HeadBranch string `json:"head_branch,omitempty"` ParentDelivery string `json:"parent_delivery,omitempty"` + // VisualPublication surfaces an owed evidence attachment of a published + // PR ("visual_pending" or "manual_required"); empty otherwise. + VisualPublication string `json:"visual_publication,omitempty"` } func blockedNextStatus(stage, operation, reason string, ambiguity ...string) NextStatus { @@ -146,7 +149,7 @@ func nextForPublished(repo string, state DeliveryState) NextStatus { pr := observePublishedPR(repo, state) persistObservedTerminalPRState(repo, state, pr) terminal := resolveDeliveryTerminal(repo, state.Feature) - status := publishedNextStatus(state, pr, terminal) + status := publishedNextStatus(state, pr, terminal, observeVisualPublication(repo, state.Feature)) // A fired escape is cached best-effort so the demotion holds offline in a // fresh session — the same bounded bypass as the terminal PRState cache. // control-law: goal-escape-demotes-to-operator-and-stops @@ -160,7 +163,28 @@ func nextForPublished(repo string, state DeliveryState) NextStatus { // published NextStatus. Split from nextForPublished so the frontier report can // present the same projection without nextForPublished's best-effort terminal // cache write. control-law: frontier-reports-never-mutates -func publishedNextStatus(state DeliveryState, pr publishedPRObservation, terminal DeliveryTerminal) NextStatus { +// observeVisualPublication reads the owed-attachment state of a feature's +// visual evidence, best-effort and read-only: any load failure is today's +// empty answer, never a block, and only the two owed states surface — +// "pending" belongs to first publication (publish-pr) and "published" owes +// nothing. control-law: frontier-reports-never-mutates +func observeVisualPublication(repo, feature string) string { + key, err := visualEvidenceKey("managed", feature, "") + if err != nil { + return "" + } + manifest, err := LoadPRVisualEvidence(repo, key) + if err != nil { + return "" + } + switch manifest.Publication.State { + case "visual_pending", "manual_required": + return manifest.Publication.State + } + return "" +} + +func publishedNextStatus(state DeliveryState, pr publishedPRObservation, terminal DeliveryTerminal, visualPublication string) NextStatus { _, sliceID, _ := deliveryBranchAndSlice(state) status := NextStatus{ SchemaVersion: nextStatusSchemaVersion, VerificationStatus: "VERIFIED", @@ -205,6 +229,15 @@ func publishedNextStatus(state DeliveryState, pr publishedPRObservation, termina default: status.Reason = fmt.Sprintf("Feature %q is published, but its PR state could not be verified.", state.Feature) } + if pr.Lifecycle == "PUBLISHED_OPEN" { + status.VisualPublication = visualPublication + switch visualPublication { + case "visual_pending": + status.Reason += " Its Boatstack visual-evidence comment is still owed; Boatstack can retry the attachment (attach-evidence)." + case "manual_required": + status.Reason += " Its visual-evidence comment needs manual attachment; record the observed URL with record-pr-visual-publication." + } + } if status.GoalEscape != "" { status.Reason = fmt.Sprintf("Feature %q is published; the merged-goal pursuit is paused because %s. Record the correction to start a new cycle, or handle the pull request yourself.", state.Feature, goalEscapeReason(status.GoalEscape)) } diff --git a/boatstack/next_actor_conformance_test.go b/boatstack/next_actor_conformance_test.go index 864bab5..bbdb956 100644 --- a/boatstack/next_actor_conformance_test.go +++ b/boatstack/next_actor_conformance_test.go @@ -101,6 +101,9 @@ func TestNextActorFrontierBoundaries(t *testing.T) { {"owed_evidence_stays_agents", NextStatus{ObservedStage: "BUILD"}, FlowNext{ Prescribed: &PrescribedCommand{Verb: "record-delivery-gate", RequiresHumanInput: []string{"--status", "--evidence"}, Transition: deliverycontrol.TransitionID("delivery.record_gate_test")}, }, NextActorAgent}, + {"owed_visual_attach_retry_is_agents", NextStatus{ObservedStage: "PUBLISHED", Lifecycle: "PUBLISHED_OPEN", VisualPublication: "visual_pending"}, FlowNext{}, NextActorAgent}, + {"manual_visual_attachment_is_operators", NextStatus{ObservedStage: "PUBLISHED", Lifecycle: "PUBLISHED_OPEN", VisualPublication: "manual_required"}, FlowNext{}, NextActorOperator}, + {"escaped_pursuit_demotes_despite_owed_attachment", NextStatus{ObservedStage: "PUBLISHED", Lifecycle: "PUBLISHED_OPEN", VisualPublication: "visual_pending", GoalEscape: "pr_closed"}, FlowNext{Terminal: TerminalMerged}, NextActorOperator}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/boatstack/pr.go b/boatstack/pr.go index c3ef0c2..5473b9c 100644 --- a/boatstack/pr.go +++ b/boatstack/pr.go @@ -277,6 +277,15 @@ func publishPRVisualEvidence(repo, prURL string, context PRContext, publisher PR if manifest.Publication.State == "published" && manifest.Publication.PRURL == prURL && strings.TrimSpace(manifest.Publication.CommentURL) != "" { return nil } + return attachVisualEvidence(repo, prURL, manifest, publisher, context.PRVisualEvidencePolicy) +} + +// attachVisualEvidence performs the one publisher call and records the +// observed outcome: manual_required without a publisher, visual_pending on a +// publisher failure (PR preserved, fix forward), published on an observable +// comment URL. Shared by first publication (publishPRVisualEvidence) and the +// attach-evidence retry, so both paths record identical states. +func attachVisualEvidence(repo, prURL string, manifest PRVisualEvidenceManifest, publisher PRVisualEvidencePublisher, policy string) error { now := time.Now().UTC().Truncate(time.Second).Format(time.RFC3339) if publisher == nil { _, recordErr := recordPRVisualPublication(repo, manifest, PRVisualPublication{ @@ -286,7 +295,7 @@ func publishPRVisualEvidence(repo, prURL string, context PRContext, publisher PR if recordErr != nil { return fmt.Errorf("PR opened but manual visual-evidence fallback could not be recorded: %w", recordErr) } - if context.PRVisualEvidencePolicy == "require" { + if policy == "require" { return fmt.Errorf("PR opened at %s but required visual evidence still needs manual attachment; update the same PR after attachment", prURL) } return nil @@ -302,12 +311,49 @@ func publishPRVisualEvidence(repo, prURL string, context PRContext, publisher PR if strings.TrimSpace(commentURL) == "" { return fmt.Errorf("visual evidence publisher returned no observable comment URL") } - _, err = recordPRVisualPublication(repo, manifest, PRVisualPublication{ + _, err := recordPRVisualPublication(repo, manifest, PRVisualPublication{ State: "published", PRURL: prURL, CommentURL: strings.TrimSpace(commentURL), UpdatedAt: now, }) return err } +// RetryVisualAttachment retries the owed evidence comment of an already +// published feature PR — the exact fingerprinted package the operator +// confirmed at publication; publication authority is never re-asked. It is +// idempotent: an already published attachment is a no-op. +func RetryVisualAttachment(repo, feature string, publisher PRVisualEvidencePublisher) (PRVisualEvidenceManifest, error) { + resolved, err := ResolveRepository(repo) + if err != nil { + return PRVisualEvidenceManifest{}, err + } + key, err := visualEvidenceKey("managed", feature, "") + if err != nil { + return PRVisualEvidenceManifest{}, err + } + manifest, err := LoadPRVisualEvidence(resolved, key) + if err != nil { + return PRVisualEvidenceManifest{}, fmt.Errorf("no recorded visual evidence for feature %q: %w", feature, err) + } + state := manifest.Publication.State + if state == "published" && strings.TrimSpace(manifest.Publication.CommentURL) != "" { + return manifest, nil + } + if state != "visual_pending" && state != "manual_required" { + return PRVisualEvidenceManifest{}, fmt.Errorf("visual evidence for %q owes no attachment retry (publication state %q); first publication is owned by publish-pr", feature, state) + } + prURL := strings.TrimSpace(manifest.Publication.PRURL) + if prURL == "" { + return PRVisualEvidenceManifest{}, fmt.Errorf("visual evidence for %q records no pull request; publish-pr owns first publication", feature) + } + if publisher == nil { + return PRVisualEvidenceManifest{}, fmt.Errorf("no visual publisher is available in this environment; attach the fingerprinted PNGs to one PR comment yourself and record the observed URL with record-pr-visual-publication --key %s --pr-url %s --comment-url ", key, prURL) + } + if err := attachVisualEvidence(resolved, prURL, manifest, publisher, ""); err != nil { + return PRVisualEvidenceManifest{}, err + } + return LoadPRVisualEvidence(resolved, key) +} + func gitCommand(repo string, arguments ...string) (string, error) { return commandOutput(repo, "git", append([]string{"-C", repo}, arguments...)...) } diff --git a/boatstack/statemap.go b/boatstack/statemap.go index bf46d3d..baaa3f8 100644 --- a/boatstack/statemap.go +++ b/boatstack/statemap.go @@ -228,7 +228,7 @@ func StateRegistry() []StateEntry { }, { Name: "visual-evidence", Class: ClassRuntimeShared, Partition: "git-common", Gitignored: true, GuardProtected: true, - OwnerVerbs: []string{"record-pr-visual-evidence", "capture-evidence", "record-pr-visual-publication"}, + OwnerVerbs: []string{"record-pr-visual-evidence", "capture-evidence", "record-pr-visual-publication", "attach-evidence"}, Sample: staticSample(filepath.FromSlash(".git/boatstack/visual-evidence/sample/manifest.json")), }, { diff --git a/boatstack/visual_attach_conformance_test.go b/boatstack/visual_attach_conformance_test.go new file mode 100644 index 0000000..6d89445 --- /dev/null +++ b/boatstack/visual_attach_conformance_test.go @@ -0,0 +1,92 @@ +package boatstack + +import ( + "strings" + "testing" +) + +// control-law: prescriptive-closure-every-stage-names-a-runnable-command +// control-law: turn-ends-only-at-the-operator-frontier +// +// A published-open slice with an owed visual publication never resolves to a +// dark prescription: visual_pending prescribes the agent-owned attach-evidence +// retry, manual_required prescribes recording the operator-attached comment, +// and both fire under BOTH terminals because the attachment completes the +// publication itself — it is not merge pursuit. + +func publishedOpenStatus(visualPublication string) NextStatus { + return NextStatus{ + VerificationStatus: "VERIFIED", + ObservedStage: "PUBLISHED", Lifecycle: "PUBLISHED_OPEN", Feature: "demo", + PRURL: "https://github.com/example/repo/pull/7", VisualPublication: visualPublication, + } +} + +func TestOwedVisualAttachmentNeverResolvesDark(t *testing.T) { + t.Run("visual_pending_prescribes_the_retry", func(t *testing.T) { + cmd, followUp := prescribeVisualAttach(".", publishedOpenStatus("visual_pending")) + if cmd == nil || cmd.Verb != "attach-evidence" || !cmd.AutoDerivable { + t.Fatalf("visual_pending did not prescribe the derivable retry: %+v", cmd) + } + if strings.Join(cmd.Args, " ") != "--feature demo" { + t.Fatalf("retry arguments are not state-derived: %v", cmd.Args) + } + if cmd.Transition != MarkerPublishedAttach { + t.Fatalf("retry must carry the attach marker, got %s", cmd.Transition) + } + if followUp == "" { + t.Fatal("the retry prescription owes its manual-fallback follow-up") + } + }) + + t.Run("manual_required_prescribes_the_recording", func(t *testing.T) { + cmd, _ := prescribeVisualAttach(".", publishedOpenStatus("manual_required")) + if cmd == nil || cmd.Verb != "record-pr-visual-publication" { + t.Fatalf("manual_required did not prescribe the recording: %+v", cmd) + } + if cmd.AutoDerivable || strings.Join(cmd.RequiresHumanInput, " ") != "--comment-url" { + t.Fatalf("the observed comment URL must be owed to the operator: %+v", cmd) + } + if !strings.Contains(strings.Join(cmd.Args, " "), "--pr-url https://github.com/example/repo/pull/7") { + t.Fatalf("the recorded PR URL is state-derived and must be in Args: %v", cmd.Args) + } + }) + + t.Run("attach_fires_under_the_published_default_terminal", func(t *testing.T) { + repo := nextTestRepo(t) + next, err := nextControlFromStatus(repo, publishedOpenStatus("visual_pending")) + if err != nil { + t.Fatal(err) + } + if next.Terminal != TerminalPublished { + t.Fatalf("fixture must exercise the published default, got %s", next.Terminal) + } + if next.Prescribed == nil || next.Prescribed.Verb != "attach-evidence" { + t.Fatalf("owed attachment resolved dark under the published terminal: %+v", next.Prescribed) + } + if next.Actor != NextActorAgent { + t.Fatalf("the derivable retry is the agent's step, got %s", next.Actor) + } + }) + + t.Run("no_owed_attachment_prescribes_nothing", func(t *testing.T) { + if cmd, _ := prescribeVisualAttach(".", publishedOpenStatus("")); cmd != nil { + t.Fatalf("nothing is owed but something was prescribed: %+v", cmd) + } + }) + + t.Run("goal_escape_still_demotes_and_stops", func(t *testing.T) { + status := publishedOpenStatus("visual_pending") + status.GoalEscape = "pr_closed" + if cmd, _ := prescribeVisualAttach(".", status); cmd != nil { + t.Fatalf("a fired escape must prescribe nothing: %+v", cmd) + } + }) + + t.Run("attach_marker_is_never_auto_driven", func(t *testing.T) { + cmd, _ := prescribeVisualAttach(".", publishedOpenStatus("visual_pending")) + if canAutoDrive(cmd, autoDrivableTransitions) { + t.Fatal("the attach retry must be prescribed, never driven") + } + }) +} diff --git a/boatstack/visual_evidence_test.go b/boatstack/visual_evidence_test.go index 8b60db4..2b8011e 100644 --- a/boatstack/visual_evidence_test.go +++ b/boatstack/visual_evidence_test.go @@ -194,3 +194,51 @@ func TestPRVisualPublisherReusesOneCommentAndRecordsPendingFailure(t *testing.T) t.Fatalf("visual-pending state was not retained: %#v %v", failed.Publication, err) } } + +// Invariant: the attach retry completes exactly the owed publication — the +// confirmed fingerprinted package against its recorded PR — and an already +// published attachment is a no-op that never re-consults the publisher. +func TestRetryVisualAttachmentCompletesOwedPublication(t *testing.T) { + repo := visualTestRepo(t) + manifest := savedVisualManifest(t, repo, "feature-warning") + context := PRContext{PRVisualEvidencePolicy: "suggest", PRVisualEvidenceStatus: "PASS", PRVisualEvidence: &manifest} + prURL := "https://github.com/example/repo/pull/3" + if err := publishPRVisualEvidence(repo, prURL, context, &fakeVisualPublisher{err: os.ErrPermission}); err == nil { + t.Fatal("fixture publication was expected to fail into visual_pending") + } + retried, err := RetryVisualAttachment(repo, "feature-warning", &fakeVisualPublisher{commentURL: "https://github.com/example/repo/pull/3#issuecomment-9"}) + if err != nil { + t.Fatal(err) + } + if retried.Publication.State != "published" || retried.Publication.PRURL != prURL || retried.Publication.CommentURL == "" { + t.Fatalf("retry did not complete the owed publication: %#v", retried.Publication) + } + again, err := RetryVisualAttachment(repo, "feature-warning", &fakeVisualPublisher{err: os.ErrPermission}) + if err != nil || again.Publication.State != "published" { + t.Fatalf("published attachment must be an idempotent no-op: %#v %v", again.Publication, err) + } +} + +// Refusals: the retry never usurps first publication (publish-pr owns it) and +// a missing publisher routes to the manual recording verb by name. +func TestRetryVisualAttachmentRefusesWhatItDoesNotOwn(t *testing.T) { + repo := visualTestRepo(t) + if _, err := RetryVisualAttachment(repo, "missing-feature", &fakeVisualPublisher{commentURL: "x"}); err == nil || !strings.Contains(err.Error(), "no recorded visual evidence") { + t.Fatalf("missing manifest was not refused: %v", err) + } + manifest := savedVisualManifest(t, repo, "feature-warning") + if _, err := RetryVisualAttachment(repo, "feature-warning", &fakeVisualPublisher{commentURL: "x"}); err == nil || !strings.Contains(err.Error(), "publish-pr") { + t.Fatalf("pre-publication manifest was not routed to publish-pr: %v", err) + } + context := PRContext{PRVisualEvidencePolicy: "suggest", PRVisualEvidenceStatus: "PASS", PRVisualEvidence: &manifest} + if err := publishPRVisualEvidence(repo, "https://github.com/example/repo/pull/4", context, nil); err != nil { + t.Fatal(err) + } + if _, err := RetryVisualAttachment(repo, "feature-warning", nil); err == nil || !strings.Contains(err.Error(), "record-pr-visual-publication") { + t.Fatalf("missing publisher must name the manual recording verb: %v", err) + } + recovered, err := RetryVisualAttachment(repo, "feature-warning", &fakeVisualPublisher{commentURL: "https://github.com/example/repo/pull/4#issuecomment-1"}) + if err != nil || recovered.Publication.State != "published" { + t.Fatalf("manual_required with a live publisher should still recover: %#v %v", recovered.Publication, err) + } +} diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 2c27596..c72a6f9 100644 --- a/docs/evidence-engineered-coding.md +++ b/docs/evidence-engineered-coding.md @@ -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 [`146f50c0f3309e592ed45374b8cddb3d8ed64d85`](https://github.com/operatorstack/intelligence-flow/tree/146f50c0f3309e592ed45374b8cddb3d8ed64d85/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 [`e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a`](https://github.com/operatorstack/intelligence-flow/tree/e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a/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 099760a..3c22133 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "146f50c0f3309e592ed45374b8cddb3d8ed64d85", + "source_commit": "e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a", "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:146f50c0f3309e592ed45374b8cddb3d8ed64d85" + "last_verified_version": "source:e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a" }, { "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:146f50c0f3309e592ed45374b8cddb3d8ed64d85" + "last_verified_version": "source:e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a" }, { "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:146f50c0f3309e592ed45374b8cddb3d8ed64d85" + "last_verified_version": "source:e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a" }, { "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:146f50c0f3309e592ed45374b8cddb3d8ed64d85" + "last_verified_version": "source:e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a" }, { "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:146f50c0f3309e592ed45374b8cddb3d8ed64d85" + "last_verified_version": "source:e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a" }, { "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:146f50c0f3309e592ed45374b8cddb3d8ed64d85" + "last_verified_version": "source:e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a" }, { "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:146f50c0f3309e592ed45374b8cddb3d8ed64d85" + "last_verified_version": "source:e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a" }, { "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:146f50c0f3309e592ed45374b8cddb3d8ed64d85" + "last_verified_version": "source:e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a" }, { "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:146f50c0f3309e592ed45374b8cddb3d8ed64d85" + "last_verified_version": "source:e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a" }, { "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:146f50c0f3309e592ed45374b8cddb3d8ed64d85" + "last_verified_version": "source:e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a" }, { "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:146f50c0f3309e592ed45374b8cddb3d8ed64d85" + "last_verified_version": "source:e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 6f71efc..01a73d9 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": "146f50c0f3309e592ed45374b8cddb3d8ed64d85", + "source_commit": "e5dbf426e4417f4f2f73c9f855d34808f7fa8f9a", "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-30-visual-attach-retry-prescription.md b/release-notes/2026-07-30-visual-attach-retry-prescription.md new file mode 100644 index 0000000..090d083 --- /dev/null +++ b/release-notes/2026-07-30-visual-attach-retry-prescription.md @@ -0,0 +1,3 @@ +### The flow now prescribes the owed visual-evidence attachment + +A published PR whose Boatstack evidence comment failed to attach (`visual_pending`) or needs manual attachment (`manual_required`) no longer goes dark in `flow next`. The new `attach-evidence --repo --feature` verb retries exactly the operator-confirmed evidence package against the recorded PR — publication authority is never re-asked, and an already attached comment is a no-op. `flow next` prescribes the retry as the agent's step for a transient publisher failure, and prescribes `record-pr-visual-publication` (owing the observed comment URL) as the operator's step when no automatic publisher is available. Both fire under the `published` and `merged` terminals, because attaching evidence completes the publication itself.