From 850e8da2f8c17feccdc0b74d7014fd554ed93738 Mon Sep 17 00:00:00 2001 From: "operator-stack-publisher[bot]" Date: Tue, 28 Jul 2026 16:31:33 +0000 Subject: [PATCH] Sync Boatstack from Intelligence Flow Labs @ d3c22e162d0b --- CONTRIBUTING.md | 2 +- UPSTREAM.json | 19 +- boatstack/cmd/boatstack-helper/flow.go | 32 ++- boatstack/flow_frontier.go | 223 +++++++++++++++++ boatstack/flow_frontier_conformance_test.go | 224 ++++++++++++++++++ boatstack/next.go | 8 + boatstack/recovery.go | 9 + docs/evidence-engineered-coding.md | 2 +- docs/public-claims.json | 24 +- labs/diagram-json/plan.lock.json | 2 +- .../2026-07-28-flow-frontier-dashboard.md | 5 + 11 files changed, 526 insertions(+), 24 deletions(-) create mode 100644 boatstack/flow_frontier.go create mode 100644 boatstack/flow_frontier_conformance_test.go create mode 100644 release-notes/2026-07-28-flow-frontier-dashboard.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e90429b..45dd833 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/c03b391323146e8d05b6049e8ba6506c8fdfdf97/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/d3c22e162d0b3ff1c81f117d7c4643dedc79a627/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 af1de35..8ef8b4b 100644 --- a/UPSTREAM.json +++ b/UPSTREAM.json @@ -12,7 +12,7 @@ }, "files": { ".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957", - "CONTRIBUTING.md": "fdfd3c320151827975d09023ad8746cdeb8efdad8fe791679a97b33e75fc26e8", + "CONTRIBUTING.md": "cefcf0e9a1d895daa2f41e2bc14a6e47efc4517e354511edb4b016f66b07722b", "README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad", "assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63", "assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5", @@ -43,7 +43,7 @@ "boatstack/changelog.go": "6b06be7cd9738de29ba6e87aa2569f3b027a2e618b04524f5abd7abaa17945bf", "boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7", "boatstack/cmd/boatstack-helper/coverage_conformance_test.go": "347810fec8cc65300ad58cf84570040a001f6dffd9d464f21038034bec6f00e9", - "boatstack/cmd/boatstack-helper/flow.go": "5ab24541d3f85c2f442730d3122d18eb6676600bc11fde4805e803a25a8300c7", + "boatstack/cmd/boatstack-helper/flow.go": "448b8c3065db347bf8b23e17c056e84ee95dd51af7028947e863b06d2bafe4b6", "boatstack/cmd/boatstack-helper/main.go": "9e0712b0a3936a3066a33b9c97f92d8ebc07f6e200664b21e6a3af03a00d9f3b", "boatstack/cmd/boatstack-helper/main_test.go": "b36c52d6d5c9dd2428730de10ff18194b7e32a98722e41341c301c6f7a04cad5", "boatstack/command.go": "4726ac515dedab4947be7eb48f88c6cb8b53d674124504b69f03e6396b080ee8", @@ -79,6 +79,8 @@ "boatstack/flow_control_test.go": "02d788c83be55ebd79ffc73875bfd019de45325151eb1f70f506980eb8e77f29", "boatstack/flow_drive.go": "a501ceda390dfd3605e22cf7ecfa15f9d50240b3fac6ebb2bb2d80c615d0a9fc", "boatstack/flow_drive_conformance_test.go": "23edea926c271a1f5718fb9dae1da11e4bf03cceb1357290cd61cd8ffb73beda", + "boatstack/flow_frontier.go": "64304ff8aa7f662d166ac3ae9d54cfd32caf35458101c3019cedd8b8326ca5b4", + "boatstack/flow_frontier_conformance_test.go": "771838f06d6157547c1277eca1f7c1df609bb9f3ae630b2fa509f641b49aba86", "boatstack/flow_guard.go": "dd18524d95f4a220cfd3d11b11003dacc52120785ee0ccdbeceb2307fab55872", "boatstack/flow_guard_test.go": "8ba75f11ddd080427c15bd7e25f7d03c1b746a2f587c212cea0e710337d1c0e1", "boatstack/flow_planning_prescribe_conformance_test.go": "c2fa2566b0676f34a777764ade87a0670d41413d052a9a339e01bdb6a9a8699b", @@ -129,7 +131,7 @@ "boatstack/mutation_test.go": "68d5049c7f96c1ac558e4c781151f67e8deee2f8d6b9bf293b90d44e769ef7c6", "boatstack/mutation_undo.go": "697d11b600a276ddbcabe6a9f8040d4f7283e017a0e8fd689ef53a274638946c", "boatstack/mutation_undo_test.go": "39540e717e3f2136bf975594043a3db9072b28ebe61c6cb0b982cea5e8b1e14e", - "boatstack/next.go": "7bd3d143f74452399b875da61a5353e91cbf6218bea5516a39fc13126a5fa042", + "boatstack/next.go": "e45b5e573616e6ac7616503d5b9b50ad6f9f9ee51b57f6bc90436b3375c5f338", "boatstack/next_actor_conformance_test.go": "23c055bcc99d889344c3f86c7940eb9ef2ef6f4ddab34e91cf215c9d078f5d42", "boatstack/next_banner_test.go": "c431a6987ed1e479442fc9f5db4371632880b92aa790fa9dd0f5285293352c41", "boatstack/next_response.go": "11decf2e3b236cbaa183980946ec17ffbbbb1af9c08bd11a466a8487bf229d5f", @@ -154,7 +156,7 @@ "boatstack/provision_test.go": "214e9edb991a66d5bbb696a7c1b63876d2f799f2cab4e3f40785f4e8f1eac57b", "boatstack/publication_ignored_repro_test.go": "b6f3aeb8ba22949ff9af7ac5afe8fb828385d9708d5d5893ef41f33a3de873e1", "boatstack/published_slice_routing_test.go": "ea7e7351018bc13dcd31c4b96f50f8bc230e8a1dbf7806fba32a12ae58923e7e", - "boatstack/recovery.go": "8c963dbaa30adcac929c52944171f76843f240194e53a2e2d4c1ff65463a93e4", + "boatstack/recovery.go": "14c71acefef8a806b84266e4dfc6c11bd9f04be3f9fc565866d7800c94e44a5c", "boatstack/recovery_test.go": "29490e7477ba602491330036a491289dd9117b99ff862f66dae421ba17e04c9f", "boatstack/reexec.go": "fed55416479d7bd3e0c3637057ffe8eb58a032f93fc358f76df906ab7acc677b", "boatstack/reexec_unix.go": "ff86157a9aa20c82a56fcd859b70669b7eacf4e0a9f61a4546ef33808437939e", @@ -206,10 +208,10 @@ "docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6", "docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79", "docs/configuration.md": "060775c73431f28bd16066bdf9e0f89034d2855c7ca0f5544f660d24b91211d0", - "docs/evidence-engineered-coding.md": "359548672c7d786d81838f2bee6c74c0861e050a30b7e2a089775b1011aaf1c4", + "docs/evidence-engineered-coding.md": "0a78679e87b821cc47d78af524d89fb1d3cd57f132f54843cf33ff46e6783808", "docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3", "docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8", - "docs/public-claims.json": "df83f3ca329170435ede3c3d75d04f99b5266296a15bb44cc564abdf57ebb3a1", + "docs/public-claims.json": "ce20eadaff1e1ec07dd5c082eb1862aa7545e7829439727f98729addbdd12da4", "docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907", "docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6", "docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6", @@ -223,7 +225,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": "fbc797694160cab69ae7287cf99d15d4391a5e2a0838664eb45f24832ce7f1c8", + "labs/diagram-json/plan.lock.json": "9ba74ea99fff5c8dffe7422251359c2e8e5941538d81b9b92987c4fb9e1e2af7", "labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51", "labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d", "labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed", @@ -352,6 +354,7 @@ "release-notes/2026-07-27-repeated-denials-escalate.md": "ee54b597e593ce74d8d9acbc67c74d2d8cb972ff206f618e08a047d4d962d7af", "release-notes/2026-07-27-sandboxed-migration-grading.md": "03cebc372bbdfed37cc70d18f3b6374d1aa5e585bafefa073dbcced58bd0336a", "release-notes/2026-07-27-state-ownership-map.md": "d032547aafc1a4acbeb520f6cbb59d7757de4f33fe824701d7b5ea8cd8c8b9e7", + "release-notes/2026-07-28-flow-frontier-dashboard.md": "9a768e0fd67f122bc0aef99899314f0a8530189164fb560b536ee03f20e29081", "release-notes/2026-07-28-minimum-app-permissions.md": "9ef97e32e5591966ef34ba23f4e0a7aa14d061a4ac5f72f5f4dcecc9bfb62a89", "release-notes/2026-07-28-native-auto-merge-conformance.md": "67d0fab76fd4911b5836d19d06b319537cb1807650d35dbd534627a2c3622757", "release-notes/2026-07-28-operator-frontier-next-actor.md": "7e769625a2beb8a204d2d79158c18bdac350656de5c55998988a8a5aba2c319a", @@ -361,7 +364,7 @@ "generator": "operatorstack/intelligence-flow:boatstack-distribution", "schema_version": 1, "source": { - "commit": "c03b391323146e8d05b6049e8ba6506c8fdfdf97", + "commit": "d3c22e162d0b3ff1c81f117d7c4643dedc79a627", "path": "labs/12-product-engineering-loop", "repository": "operatorstack/intelligence-flow" } diff --git a/boatstack/cmd/boatstack-helper/flow.go b/boatstack/cmd/boatstack-helper/flow.go index a846448..7b5874c 100644 --- a/boatstack/cmd/boatstack-helper/flow.go +++ b/boatstack/cmd/boatstack-helper/flow.go @@ -14,7 +14,7 @@ import ( // gate, authority, or exit code. func flowCommand(arguments []string) int { if len(arguments) == 0 { - fmt.Fprintln(os.Stderr, "usage: boatstack-helper flow ") + fmt.Fprintln(os.Stderr, "usage: boatstack-helper flow ") return 2 } switch arguments[0] { @@ -24,6 +24,8 @@ func flowCommand(arguments []string) int { return flowNextCommand(arguments[1:]) case "tasks": return flowTasksCommand(arguments[1:]) + case "frontier": + return flowFrontierCommand(arguments[1:]) case "report": return flowReportCommand(arguments[1:]) default: @@ -147,6 +149,34 @@ func executePrescribed(cmd *boatstack.PrescribedCommand) error { } } +// flowFrontierCommand renders the cross-delivery frontier dashboard: one row +// per managed delivery slice with its observed position and the actor who owes +// the next step. Strictly read-only — it performs zero writes, including the +// terminal PR-state cache that next/recovery maintain. +// control-law: frontier-reports-never-mutates +func flowFrontierCommand(arguments []string) int { + flags := flag.NewFlagSet("flow frontier", flag.ContinueOnError) + repo := flags.String("repo", ".", "repository whose delivery frontier should be reported") + jsonOutput := flags.Bool("json", false, "print the structured frontier report") + if err := flags.Parse(arguments); err != nil { + return 2 + } + frontier, err := boatstack.ResolveFrontier(*repo) + if err != nil { + return fail(err) + } + if *jsonOutput { + value, marshalErr := boatstack.MarshalJSON(frontier) + if marshalErr != nil { + return fail(marshalErr) + } + fmt.Print(string(value)) + } else { + fmt.Print(boatstack.FormatFlowFrontier(frontier)) + } + return 0 +} + // flowTasksCommand renders the active delivery slice's sub-actions from the // compiled plan task DAG, in dependency order, with the one to start pointed at. // It is read-only and never fails on flow position — an unresolved slice or an diff --git a/boatstack/flow_frontier.go b/boatstack/flow_frontier.go new file mode 100644 index 0000000..6acbe7c --- /dev/null +++ b/boatstack/flow_frontier.go @@ -0,0 +1,223 @@ +package boatstack + +import ( + "fmt" + "strings" +) + +// The frontier report answers "where is every ball, and whose is it" in one +// read-only view: every managed delivery — active, published, or invalid — +// becomes a row carrying its observed position and the actor who owes the +// next step. It is pure presentation over the same resolution the flow oracle +// uses, so a frontier row can never disagree with `flow next` for the same +// delivery; and unlike next/recovery it performs ZERO writes — not even the +// best-effort terminal PR-state cache — because a report that mutates is a +// report that can lie about what it found. +// control-law: frontier-reports-never-mutates +const flowFrontierSchemaVersion = 1 + +// FrontierRow is one delivery slice's position on the operator frontier. +type FrontierRow struct { + Feature string `json:"feature"` + Slice string `json:"slice,omitempty"` + SliceIndex int `json:"slice_index,omitempty"` + TotalSlices int `json:"total_slices,omitempty"` + Stage string `json:"stage"` + Lifecycle string `json:"lifecycle,omitempty"` + PRPhase string `json:"pr_phase,omitempty"` + PRFailingChecks []string `json:"pr_failing_checks,omitempty"` + PRURL string `json:"pr_url,omitempty"` + Actor string `json:"next_actor"` + NextOperation string `json:"next_operation"` + Prescribed string `json:"prescribed,omitempty"` + Reason string `json:"reason"` + Blocked bool `json:"blocked,omitempty"` +} + +// FlowFrontier is the full cross-delivery dashboard. +type FlowFrontier struct { + SchemaVersion int `json:"schema_version"` + Initialized bool `json:"initialized"` + Rows []FrontierRow `json:"rows"` + AgentSteps int `json:"agent_steps"` + OperatorSteps int `json:"operator_steps"` + TerminalRows int `json:"terminal_rows"` + BlockedRows int `json:"blocked_rows"` +} + +// ResolveFrontier builds the frontier report. Faults are partitioned, never +// propagated: one invalid delivery becomes one blocked row instead of +// poisoning the view of every healthy delivery (the same partition law the +// read-only recovery boundary uses). +// control-law: frontier-reports-never-mutates +// control-law: stale-delivery-cannot-block-unrelated-feature +func ResolveFrontier(repoPath string) (FlowFrontier, error) { + frontier := FlowFrontier{SchemaVersion: flowFrontierSchemaVersion} + repo, err := ResolveRepository(repoPath) + if err != nil { + return frontier, err + } + if !fileExists(WorkspaceFor(repo).ProjectConfigPath()) { + return frontier, nil + } + frontier.Initialized = true + config, _, configErr := LoadConfig(WorkspaceFor(repo).ProjectConfigPath()) + if configErr != nil { + return frontier, fmt.Errorf("boatstack project configuration is invalid; fix the config file (doctor diagnoses): %w", configErr) + } + states, invalid, err := allManagedDeliveryStates(repo) + if err != nil { + return frontier, err + } + states = withoutIgnoredDeliveryStates(states, config.Workflow.IgnoredDeliveries) + invalid = withoutIgnoredDeliveries(invalid, config.Workflow.IgnoredDeliveries) + + for _, slug := range invalid { + frontier.Rows = append(frontier.Rows, FrontierRow{ + Feature: slug, Stage: "INVALID_STATE", Actor: string(NextActorOperator), + NextOperation: "discard-delivery", Blocked: true, + Reason: "This managed delivery state cannot be verified; restore its evidence, ignore it, or discard it.", + }) + } + for _, state := range states { + if state.ActiveIndex < len(state.Slices) { + frontier.Rows = append(frontier.Rows, activeDeliveryRows(repo, state)...) + continue + } + branch, _, prURL := deliveryBranchAndSlice(state) + status := publishedNextStatus(state, observePRTarget(repo, prURL, branch)) + frontier.Rows = append(frontier.Rows, frontierRowFromStatus(repo, status)) + } + for _, row := range frontier.Rows { + switch { + case row.Blocked: + frontier.BlockedRows++ + case row.Actor == string(NextActorAgent): + frontier.AgentSteps++ + case row.Actor == string(NextActorNone): + frontier.TerminalRows++ + default: + frontier.OperatorSteps++ + } + } + return frontier, nil +} + +// activeDeliveryRows renders an active delivery: one row for the active slice +// via the authoritative resolution, plus one row for every earlier slice that +// is published with a still-open PR — those are live balls too (their checks +// can be failing while the active slice builds), and they are exactly the +// addressable set the actuators can still re-gate in place. +func activeDeliveryRows(repo string, state DeliveryState) []FrontierRow { + rows := []FrontierRow{} + status, err := nextForDelivery(repo, state.Feature) + if err != nil { + rows = append(rows, FrontierRow{ + Feature: state.Feature, Stage: "INVALID_STATE", Actor: string(NextActorOperator), + NextOperation: "discard-delivery", Blocked: true, + Reason: "The active managed delivery cannot be verified: " + err.Error(), + }) + } else { + rows = append(rows, frontierRowFromStatus(repo, status)) + } + limit := state.ActiveIndex + if limit > len(state.Slices) { + limit = len(state.Slices) + } + for i := 0; i < limit; i++ { + slice := state.Slices[i] + if slice.Status != "PUBLISHED" || strings.TrimSpace(slice.PRState) == "" || isTerminalPRState(slice.PRState) { + continue + } + observation := observePRTarget(repo, slice.PRURL, slice.HeadBranch) + sliceStatus := NextStatus{ + SchemaVersion: nextStatusSchemaVersion, VerificationStatus: "VERIFIED", + Feature: state.Feature, ActiveSlice: slice.ID, SliceIndex: i + 1, + TotalSlices: len(state.Slices), ObservedStage: "PUBLISHED", NextOperation: "none", + Lifecycle: observation.Lifecycle, PRURL: observation.URL, HeadBranch: observation.Branch, + PRPhase: string(observation.Phase), PRReviewDecision: observation.ReviewDecision, + PRMergeState: observation.MergeState, PRFailingChecks: observation.FailingChecks, + Reason: fmt.Sprintf("Slice %q is published with an open pull request while a later slice is active.", slice.ID), + } + rows = append(rows, frontierRowFromStatus(repo, sliceStatus)) + } + return rows +} + +// frontierRowFromStatus projects one resolved status through the SAME actor +// classification and prescription layer `flow next` uses — one resolution +// path, so the dashboard and the advisor can never name different owners for +// the same step. +func frontierRowFromStatus(repo string, status NextStatus) FrontierRow { + row := FrontierRow{ + Feature: status.Feature, Slice: status.ActiveSlice, + SliceIndex: status.SliceIndex, TotalSlices: status.TotalSlices, + Stage: status.ObservedStage, Lifecycle: status.Lifecycle, + PRPhase: status.PRPhase, PRFailingChecks: status.PRFailingChecks, + PRURL: status.PRURL, NextOperation: status.NextOperation, + Reason: status.Reason, + Blocked: status.VerificationStatus == "BLOCKED", + } + next, err := nextControlFromStatus(repo, status) + if err != nil { + row.Actor = string(NextActorOperator) + row.Blocked = true + return row + } + row.Actor = string(next.Actor) + if next.Prescribed != nil { + row.Prescribed = next.Prescribed.CommandLine() + } + return row +} + +// frontierPosition is the one-word position column: the observed PR phase when +// it is positively known, the stage otherwise. +func frontierPosition(row FrontierRow) string { + if row.PRPhase != "" && row.PRPhase != string(PRPhaseUnknown) { + return row.PRPhase + } + return row.Stage +} + +// FormatFlowFrontier renders the dashboard as fixed-width human-facing lines. +func FormatFlowFrontier(frontier FlowFrontier) string { + var b strings.Builder + if !frontier.Initialized { + b.WriteString("Boatstack is not tracking anything here yet.\n") + return b.String() + } + if len(frontier.Rows) == 0 { + b.WriteString("Frontier: no managed deliveries.\n") + return b.String() + } + fmt.Fprintf(&b, "Frontier: %d for you, %d for the agent, %d complete, %d blocked\n", + frontier.OperatorSteps, frontier.AgentSteps, frontier.TerminalRows, frontier.BlockedRows) + nameWidth, positionWidth := len("FEATURE"), len("POSITION") + for _, row := range frontier.Rows { + if len(frontierLabel(row)) > nameWidth { + nameWidth = len(frontierLabel(row)) + } + if len(frontierPosition(row)) > positionWidth { + positionWidth = len(frontierPosition(row)) + } + } + fmt.Fprintf(&b, "%-*s %-*s %-8s %s\n", nameWidth, "FEATURE", positionWidth, "POSITION", "ACTOR", "NEXT") + for _, row := range frontier.Rows { + next := row.NextOperation + if len(row.PRFailingChecks) > 0 { + next += " (failing: " + strings.Join(row.PRFailingChecks, ", ") + ")" + } + fmt.Fprintf(&b, "%-*s %-*s %-8s %s\n", nameWidth, frontierLabel(row), positionWidth, frontierPosition(row), row.Actor, next) + } + return b.String() +} + +// frontierLabel names a row: the feature, with the slice id appended when the +// delivery has more than one slice so two rows of one delivery stay distinct. +func frontierLabel(row FrontierRow) string { + if row.TotalSlices > 1 && row.Slice != "" { + return row.Feature + "/" + row.Slice + } + return row.Feature +} diff --git a/boatstack/flow_frontier_conformance_test.go b/boatstack/flow_frontier_conformance_test.go new file mode 100644 index 0000000..851a216 --- /dev/null +++ b/boatstack/flow_frontier_conformance_test.go @@ -0,0 +1,224 @@ +package boatstack + +// control-law: frontier-reports-never-mutates +// +// The frontier dashboard is a pure projection: it reports every managed +// delivery's position and owing actor while performing ZERO writes — not even +// the best-effort terminal PR-state cache the next/recovery resolvers +// maintain. A report that mutates is a report that can lie about what it +// found. Companion laws exercised here: +// stale-delivery-cannot-block-unrelated-feature (one corrupt delivery is one +// blocked row, never a poisoned view) and +// turn-ends-only-at-the-operator-frontier (a frontier row's actor equals the +// flow advisor's actor for the same delivery — one classification path). +// +// Test classes: positive (a multi-delivery store renders every slice with a +// typed actor and live PR position), negative (a corrupt delivery yields one +// blocked row while healthy rows survive), bypass (state files are +// byte-identical after a frontier run, even under a terminal MERGED +// observation), relation (frontier actor == flow next actor per delivery). + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func frontierStateBytes(t *testing.T, repo string, features ...string) map[string]string { + t.Helper() + snapshot := map[string]string{} + for _, feature := range features { + path, err := deliveryStatePath(repo, feature) + if err != nil { + t.Fatal(err) + } + value, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + snapshot[feature] = string(value) + } + return snapshot +} + +// Positive + relation: every delivery renders with a typed actor, the +// published delivery carries its live PR phase, and each row's actor matches +// the flow advisor's actor for the same feature. +func TestFrontierRendersEveryDeliveryWithTypedActor(t *testing.T) { + repo := nextTestRepo(t) + writeNextDelivery(t, repo, "building", "BUILD", 0) + writeNextDelivery(t, repo, "shipped", "PUBLISHED", 1) + updateRecoveryDelivery(t, repo, "shipped", "feat/phase", "https://example.invalid/pr/9", "") + withRecoveryGh(t, phaseObservationPayload("OPEN", "", "CLEAN", rollupCheckRunFail)) + + frontier, err := ResolveFrontier(repo) + if err != nil { + t.Fatal(err) + } + rows := map[string]FrontierRow{} + for _, row := range frontier.Rows { + rows[row.Feature] = row + } + building, ok := rows["building"] + if !ok || building.Stage != "BUILD" || building.Actor != string(NextActorAgent) { + t.Fatalf("unexpected building row: %#v", building) + } + if building.Prescribed == "" { + t.Fatal("an agent-owned row must carry its prescribed command") + } + shipped, ok := rows["shipped"] + if !ok || shipped.Stage != "PUBLISHED" || shipped.PRPhase != string(PRPhaseChecksFailing) { + t.Fatalf("unexpected shipped row: %#v", shipped) + } + if shipped.Actor != string(NextActorOperator) { + t.Fatalf("published-open step belongs to the operator today: %#v", shipped) + } + if frontier.AgentSteps != 1 || frontier.OperatorSteps != 1 || frontier.BlockedRows != 0 { + t.Fatalf("unexpected summary: %#v", frontier) + } + + // Relation: the frontier's actor for each feature equals the advisor's. + for _, feature := range []string{"building", "shipped"} { + next, nextErr := NextControl(repo, feature) + if nextErr != nil { + t.Fatal(nextErr) + } + if string(next.Actor) != rows[feature].Actor { + t.Fatalf("frontier actor %q disagrees with flow next actor %q for %s", rows[feature].Actor, next.Actor, feature) + } + } + + rendered := FormatFlowFrontier(frontier) + if !strings.Contains(rendered, "PR_CHECKS_FAILING") || !strings.Contains(rendered, "failing: unit") { + t.Fatalf("rendered frontier hides the live PR position:\n%s", rendered) + } +} + +// Positive: an active delivery with an earlier published-but-open slice shows +// both balls — the building active slice and the open PR of the earlier slice. +func TestFrontierShowsEarlierPublishedOpenSlices(t *testing.T) { + repo := nextTestRepo(t) + directory := filepath.Join(repo, ".product-loop", "features", "layered") + if err := os.MkdirAll(directory, 0o755); err != nil { + t.Fatal(err) + } + lockPath := filepath.Join(directory, "plan.lock.json") + if err := os.WriteFile(lockPath, []byte("lock\n"), 0o644); err != nil { + t.Fatal(err) + } + hash, err := SHA256File(lockPath) + if err != nil { + t.Fatal(err) + } + if err := saveDeliveryState(repo, DeliveryState{ + SchemaVersion: deliveryStateSchemaVersion, Feature: "layered", PlanLockHash: hash, + ActiveIndex: 1, Slices: []DeliverySlice{ + {ID: "first", Title: "First", Status: "PUBLISHED", PRURL: "https://example.invalid/pr/9", HeadBranch: "feat/phase", PRState: "OPEN"}, + {ID: "second", Title: "Second", Status: "BUILD"}, + }, + }); err != nil { + t.Fatal(err) + } + withRecoveryGh(t, phaseObservationPayload("OPEN", "", "CLEAN", rollupCheckRunFail)) + + frontier, frontierErr := ResolveFrontier(repo) + if frontierErr != nil { + t.Fatal(frontierErr) + } + if len(frontier.Rows) != 2 { + t.Fatalf("want active + earlier published rows, got %#v", frontier.Rows) + } + var earlier *FrontierRow + for i := range frontier.Rows { + if frontier.Rows[i].Slice == "first" { + earlier = &frontier.Rows[i] + } + } + if earlier == nil || earlier.PRPhase != string(PRPhaseChecksFailing) || earlier.Actor != string(NextActorOperator) { + t.Fatalf("earlier published-open slice not surfaced: %#v", frontier.Rows) + } +} + +// Negative: one corrupt delivery becomes one blocked row; the healthy +// delivery's row survives untouched. +func TestFrontierPartitionsCorruptDeliveries(t *testing.T) { + repo := nextTestRepo(t) + writeNextDelivery(t, repo, "healthy", "BUILD", 0) + writeNextDelivery(t, repo, "corrupt", "BUILD", 0) + statePath, err := deliveryStatePath(repo, "corrupt") + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(statePath, []byte("not json"), 0o644); err != nil { + t.Fatal(err) + } + + frontier, err := ResolveFrontier(repo) + if err != nil { + t.Fatal(err) + } + if len(frontier.Rows) != 2 || frontier.BlockedRows != 1 || frontier.AgentSteps != 1 { + t.Fatalf("unexpected partition: %#v", frontier) + } + for _, row := range frontier.Rows { + if row.Feature == "corrupt" && (!row.Blocked || row.NextOperation != "discard-delivery") { + t.Fatalf("corrupt delivery not routed to its remedy: %#v", row) + } + if row.Feature == "healthy" && (row.Blocked || row.Actor != string(NextActorAgent)) { + t.Fatalf("healthy delivery poisoned by corrupt neighbor: %#v", row) + } + } +} + +// Bypass: the frontier performs zero writes — the delivery ledger is +// byte-identical after a run, even when the live observation is terminal +// (MERGED), which next/recovery WOULD cache. The report never mutates. +func TestFrontierWritesNothingEvenOnTerminalObservation(t *testing.T) { + repo := nextTestRepo(t) + writeNextDelivery(t, repo, "building", "BUILD", 0) + writeNextDelivery(t, repo, "shipped", "PUBLISHED", 1) + updateRecoveryDelivery(t, repo, "shipped", "feat/phase", "https://example.invalid/pr/9", "") + withRecoveryGh(t, phaseObservationPayload("MERGED", "", "", "")) + + before := frontierStateBytes(t, repo, "building", "shipped") + frontier, err := ResolveFrontier(repo) + if err != nil { + t.Fatal(err) + } + after := frontierStateBytes(t, repo, "building", "shipped") + for feature, value := range before { + if after[feature] != value { + t.Fatalf("frontier mutated delivery state for %q", feature) + } + } + var shipped FrontierRow + for _, row := range frontier.Rows { + if row.Feature == "shipped" { + shipped = row + } + } + if shipped.Actor != string(NextActorNone) || shipped.Stage != "FEATURE_COMPLETE" { + t.Fatalf("terminal observation misclassified: %#v", shipped) + } +} + +// Failure-state: an uninitialized repository reports an empty, unblocked +// frontier rather than an error. +func TestFrontierOnUninitializedRepository(t *testing.T) { + repo := t.TempDir() + if output, err := exec.Command("git", "-C", repo, "init").CombinedOutput(); err != nil { + t.Fatalf("git init: %v: %s", err, output) + } + frontier, err := ResolveFrontier(repo) + if err != nil { + t.Fatal(err) + } + if frontier.Initialized || len(frontier.Rows) != 0 { + t.Fatalf("unexpected frontier: %#v", frontier) + } + if rendered := FormatFlowFrontier(frontier); !strings.Contains(rendered, "not tracking") { + t.Fatalf("unexpected rendering: %s", rendered) + } +} diff --git a/boatstack/next.go b/boatstack/next.go index 7c54300..53a16c7 100644 --- a/boatstack/next.go +++ b/boatstack/next.go @@ -144,6 +144,14 @@ func nextForDelivery(repo, feature string) (NextStatus, error) { func nextForPublished(repo string, state DeliveryState) NextStatus { pr := observePublishedPR(repo, state) persistObservedTerminalPRState(repo, state, pr) + return publishedNextStatus(state, pr) +} + +// publishedNextStatus is the pure mapping from one live PR observation to the +// 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) NextStatus { _, sliceID, _ := deliveryBranchAndSlice(state) status := NextStatus{ SchemaVersion: nextStatusSchemaVersion, VerificationStatus: "VERIFIED", diff --git a/boatstack/recovery.go b/boatstack/recovery.go index 5651979..f2bed74 100644 --- a/boatstack/recovery.go +++ b/boatstack/recovery.go @@ -197,6 +197,15 @@ func selectRecoveryDelivery(states []DeliveryState, explicitFeature, currentBran func observePublishedPR(repo string, state DeliveryState) publishedPRObservation { branch, _, prURL := deliveryBranchAndSlice(state) + return observePRTarget(repo, prURL, branch) +} + +// observePRTarget performs the single live, read-only PR observation for one +// explicit PR URL or head branch. Split from observePublishedPR so callers +// that must not write anything (the frontier report) and callers that need a +// non-active slice's PR (an earlier published-but-open slice) share the exact +// same observation. +func observePRTarget(repo, prURL, branch string) publishedPRObservation { observation := publishedPRObservation{Lifecycle: "PUBLISHED_UNKNOWN", URL: prURL, Branch: branch, Phase: PRPhaseUnknown} target := prURL if target == "" { diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md index 9ab4834..cd97c23 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 [`c03b391323146e8d05b6049e8ba6506c8fdfdf97`](https://github.com/operatorstack/intelligence-flow/tree/c03b391323146e8d05b6049e8ba6506c8fdfdf97/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 [`d3c22e162d0b3ff1c81f117d7c4643dedc79a627`](https://github.com/operatorstack/intelligence-flow/tree/d3c22e162d0b3ff1c81f117d7c4643dedc79a627/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 f1428e8..9f5a585 100644 --- a/docs/public-claims.json +++ b/docs/public-claims.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "source_commit": "c03b391323146e8d05b6049e8ba6506c8fdfdf97", + "source_commit": "d3c22e162d0b3ff1c81f117d7c4643dedc79a627", "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:c03b391323146e8d05b6049e8ba6506c8fdfdf97" + "last_verified_version": "source:d3c22e162d0b3ff1c81f117d7c4643dedc79a627" }, { "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:c03b391323146e8d05b6049e8ba6506c8fdfdf97" + "last_verified_version": "source:d3c22e162d0b3ff1c81f117d7c4643dedc79a627" }, { "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:c03b391323146e8d05b6049e8ba6506c8fdfdf97" + "last_verified_version": "source:d3c22e162d0b3ff1c81f117d7c4643dedc79a627" }, { "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:c03b391323146e8d05b6049e8ba6506c8fdfdf97" + "last_verified_version": "source:d3c22e162d0b3ff1c81f117d7c4643dedc79a627" }, { "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:c03b391323146e8d05b6049e8ba6506c8fdfdf97" + "last_verified_version": "source:d3c22e162d0b3ff1c81f117d7c4643dedc79a627" }, { "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:c03b391323146e8d05b6049e8ba6506c8fdfdf97" + "last_verified_version": "source:d3c22e162d0b3ff1c81f117d7c4643dedc79a627" }, { "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:c03b391323146e8d05b6049e8ba6506c8fdfdf97" + "last_verified_version": "source:d3c22e162d0b3ff1c81f117d7c4643dedc79a627" }, { "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:c03b391323146e8d05b6049e8ba6506c8fdfdf97" + "last_verified_version": "source:d3c22e162d0b3ff1c81f117d7c4643dedc79a627" }, { "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:c03b391323146e8d05b6049e8ba6506c8fdfdf97" + "last_verified_version": "source:d3c22e162d0b3ff1c81f117d7c4643dedc79a627" }, { "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:c03b391323146e8d05b6049e8ba6506c8fdfdf97" + "last_verified_version": "source:d3c22e162d0b3ff1c81f117d7c4643dedc79a627" }, { "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:c03b391323146e8d05b6049e8ba6506c8fdfdf97" + "last_verified_version": "source:d3c22e162d0b3ff1c81f117d7c4643dedc79a627" } ] } diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json index 22a9105..c2768f0 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": "c03b391323146e8d05b6049e8ba6506c8fdfdf97", + "source_commit": "d3c22e162d0b3ff1c81f117d7c4643dedc79a627", "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-28-flow-frontier-dashboard.md b/release-notes/2026-07-28-flow-frontier-dashboard.md new file mode 100644 index 0000000..9315f37 --- /dev/null +++ b/release-notes/2026-07-28-flow-frontier-dashboard.md @@ -0,0 +1,5 @@ +### One command now shows every feature, its position, and whose move it is + +`flow frontier` renders a read-only dashboard across all of your managed deliveries: each row names the feature, its observed position (building, awaiting review, PR checks failing, eligible to merge, complete), and the actor who owes the next step — you or the agent — with the exact prescribed command when one exists. Earlier slices that are published with a still-open pull request appear as their own rows, so a red check on an already-published slice is visible while a later slice builds. + +The dashboard is a pure report: it performs no writes at all, one unverifiable delivery becomes one blocked row instead of hiding your healthy work, and a row's owner always matches what `flow next` would say for the same feature. Before this, reconstructing "where is everything and what is waiting on me" required running status per feature and reading each answer.