diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 31a6102..fe93a3b 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/c6d00bbcf9defd9df85ead397ec1612efb650aee/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/e37b56904acfc2a70bcc91139521ced8dd3e6051/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 74f8533..577e892 100644
--- a/UPSTREAM.json
+++ b/UPSTREAM.json
@@ -1,7 +1,7 @@
{
"canonical_context": {
- "characters": 76152,
- "estimated_tokens": 19038,
+ "characters": 76916,
+ "estimated_tokens": 19229,
"estimator": "ceil(total characters / 4); compactness signal, not provider billing",
"files": [
"product-engineering-loop/references/workflow.md",
@@ -12,14 +12,14 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
- "CONTRIBUTING.md": "b55a6677dc3d5180d97b25564268ae62e660633158ae6319210d78a7b7079959",
+ "CONTRIBUTING.md": "3ff3ff4de9be3c70f7b447e971b6a5b1aaf6451dd14810f8b42a3ad9e7f79b6a",
"README.md": "125b47671a68556df382f19756fb61fa18925606cbbaf54d6bc9df8872b36870",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
"assets/boatstack-portability.svg": "66dfdfa85db857b3bd18b32047a6975f1fbbfc4dc091158e8277193f9969a346",
"boatstack/AGENTS.md": "39574398c3c3f82c22077299b45fd46a587926e1c9a35b66998c65ab8a756554",
"boatstack/BUG-worktree-delivery-state.md": "02469cf51c3849dad5743783e248e5c04583e4240507fbef0e3f890cd6a95724",
- "boatstack/SKILL.md": "7c7b3568d836cb176c92762a8315fa834cd61a531a629c97a5cd98d6f7689be0",
+ "boatstack/SKILL.md": "639264b06a3315e2d100783d9a55c8e6b489578d6d5fdedd8d7f36eaa7f499e7",
"boatstack/agents/gemini.yaml": "cbf43b387399e456fa6178f86d83e6e35567e6142ff800f8de6ffca306fa963e",
"boatstack/agents/openai.yaml": "68a30a60859556c5a26e16d184594ca243a6043d99c8cf7d66b5dd6d50a93cd1",
"boatstack/assets/templates/adr.md": "c577a3c1c1319061f61deb053597e6e853657022185fe28b8f733327e2a78565",
@@ -40,7 +40,7 @@
"boatstack/capture_test.go": "63fa1177738081f1e862364d7a4257f5e259f8e9c36276ba1775b8085b277105",
"boatstack/changelog.go": "6b06be7cd9738de29ba6e87aa2569f3b027a2e618b04524f5abd7abaa17945bf",
"boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7",
- "boatstack/cmd/boatstack-helper/main.go": "a388d8b607475350fe8989c096a54caad944273969c64f83e0ae15d8fbd2d6f0",
+ "boatstack/cmd/boatstack-helper/main.go": "6e9cb69ef962974f9893b1b4211df5e5fbf15f50207e958d6458b64dc042a683",
"boatstack/cmd/boatstack-helper/main_test.go": "ff73003b6a5157202fa09ddf1129fb13c3d79702b2e05a8721ce5a11bf5ab779",
"boatstack/command.go": "4726ac515dedab4947be7eb48f88c6cb8b53d674124504b69f03e6396b080ee8",
"boatstack/command_test.go": "9f707abba3640add81c3e97ba7e72fedbf98f3394b1c060a9ca4b4a28e919968",
@@ -72,7 +72,8 @@
"boatstack/mutation_test.go": "68d5049c7f96c1ac558e4c781151f67e8deee2f8d6b9bf293b90d44e769ef7c6",
"boatstack/mutation_undo.go": "697d11b600a276ddbcabe6a9f8040d4f7283e017a0e8fd689ef53a274638946c",
"boatstack/mutation_undo_test.go": "39540e717e3f2136bf975594043a3db9072b28ebe61c6cb0b982cea5e8b1e14e",
- "boatstack/next.go": "07ec5fc7bf26975605fb18477c70bd66a3a1c9ac769d788f3e329c49a3128b25",
+ "boatstack/next.go": "d45f4e5b3ab7072e700725cab5b6eac83b1a460cd08f07b55370de5ee4d8ee59",
+ "boatstack/next_banner_test.go": "c431a6987ed1e479442fc9f5db4371632880b92aa790fa9dd0f5285293352c41",
"boatstack/next_test.go": "d442d22023831ba39fcfbbf73f1a2da4170a83fc2aab5ce1b44f10cf9d88e173",
"boatstack/operation.go": "62f97bf2091f33eb2ca91915bf08bee73d53387611b673e849355bfd516ca467",
"boatstack/operation_test.go": "2d624eaba342b2c81b45cdf50918a65a9c002b5376a02041b24180658ee6a25a",
@@ -99,7 +100,7 @@
"boatstack/references/host-hook-contracts.md": "d68ae1556e7b1e29e9ac7cb4db767809d510aabf0be52e60e44665ea7abb980e",
"boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441",
"boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae",
- "boatstack/references/workflow.md": "fad616acd737818d0125655cd5d81d52dc39aff6242a6925c1a43dc38658b36e",
+ "boatstack/references/workflow.md": "667ccba32f6c2710ad4fb41e34253dfe38477499d40f0dd51f8a3714b47a53c9",
"boatstack/release.go": "82dcb4ca59e8c79a68d5333d650f90e64abd448d04e0c6f504fdf07f42b5ed76",
"boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690",
"boatstack/repair_state_test.go": "f3779ac47c3db3927175a545728d3b2e020dbc85f41394d8235753b52afc3739",
@@ -133,10 +134,10 @@
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "df054f49d532c8b1b7d94184810d1b3b5bf18cdc30eb985b4b6d0639162e341a",
- "docs/evidence-engineered-coding.md": "dad43a315165883e40d73c428f610480b20d3e9c36830c99bb16762f7ed5afc2",
+ "docs/evidence-engineered-coding.md": "2a01fc2590c21b01548ac1376f6adaa8387451159f60e1c1a6500bc741f3be3d",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "f314270c5ed1a55bbef5f3ddbcb5596693dbee9374e5f0d3df8838cefbd68052",
- "docs/public-claims.json": "657bf4734ca4e169e3c1b17054df383f65ab3500dc4af5428d5b36b54a76acd0",
+ "docs/public-claims.json": "a33f473598df850c8ed1b0e2b7d2b766c3295b6f2aa699d5035fdf8753e7903d",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
@@ -150,7 +151,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": "b28d8bb0d854dc65fcd7201920e1dbea95fc7a14afb551fc8c3bdb017216bd45",
+ "labs/diagram-json/plan.lock.json": "aa8119d00ee13aa06f50c1b3f9779ea6b1f67e88243096812b83d74ce4b6baf8",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
@@ -230,12 +231,13 @@
"release-notes/2026-07-24-publication-nonblocking-control.md": "2b9d8ea817896783273a843ec183fbf00bb2f7ac420b4ce3409aeda7f59b7fb5",
"release-notes/2026-07-24-reactivation-preserves-published-progress.md": "77233df3d6955e8f7a076c301ce7cbff84ba138ab812f18ddd97785600fd3d22",
"release-notes/2026-07-24-repair-state-recovery.md": "daaa12deb51a5f647178d6164ea5b4bcd29bf5482b77d90002429f69e0da5dd0",
- "release-notes/2026-07-24-transactional-mutation-boundary.md": "38819a4811edbc99a9d8a77983aedbd0589bdbbf849da4991d3e21a1b319a65c"
+ "release-notes/2026-07-24-transactional-mutation-boundary.md": "38819a4811edbc99a9d8a77983aedbd0589bdbbf849da4991d3e21a1b319a65c",
+ "release-notes/2026-07-25-boatstack-banner.md": "28e83f294de606211cfdc91b2586aa834e004dee76d5c4bee08859986ae86b5b"
},
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
- "commit": "c6d00bbcf9defd9df85ead397ec1612efb650aee",
+ "commit": "e37b56904acfc2a70bcc91139521ced8dd3e6051",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
diff --git a/boatstack/SKILL.md b/boatstack/SKILL.md
index d41b3ca..07d4f34 100644
--- a/boatstack/SKILL.md
+++ b/boatstack/SKILL.md
@@ -79,7 +79,7 @@ Do not scan the entire repository by default. Record discovered paths and comman
## Respond to the developer
-Follow the **User-facing response contract** in `references/workflow.md` for every operation. Lead with the mapped plain-language outcome, show only decision-relevant content, end with one `### Next step`, and put machine status, helper output, fingerprints, artifact paths, receipts, and locks inside collapsed **Technical details**. Internal operations such as `check-plan`, `record-approval`, and `activate-plan` must not appear in the primary response.
+Follow the **User-facing response contract** in `references/workflow.md` for every operation. Begin every Boatstack response with the status banner (`boatstack-helper next-status --repo . --render`), then lead with the mapped plain-language outcome, show only decision-relevant content, end with one `### Next step`, and put machine status, helper output, fingerprints, artifact paths, receipts, and locks inside collapsed **Technical details**. Internal operations such as `check-plan`, `record-approval`, and `activate-plan` must not appear in the primary response.
Use the global, state-scoped reply shortcuts for finite input: `a` approves the pending plan, `o` opens the currently previewed feature/ad-hoc/update PR, `u` updates the currently previewed existing PR, and `r` accepts every recommendation displayed in the current finite-question response. Trim surrounding whitespace and match the complete reply case-insensitively. Bracketed forms such as `[o]`, embedded letters, and shortcuts from another state are ordinary text. Continue accepting `approve`, `open PR`, `update PR`, and `open update PR` for compatibility, but do not advertise them in user-facing responses.
diff --git a/boatstack/cmd/boatstack-helper/main.go b/boatstack/cmd/boatstack-helper/main.go
index a969be2..899dcc8 100644
--- a/boatstack/cmd/boatstack-helper/main.go
+++ b/boatstack/cmd/boatstack-helper/main.go
@@ -563,6 +563,7 @@ func nextStatusCommand(arguments []string) int {
repo := flags.String("repo", ".", "repository whose Boatstack stage should be inspected")
feature := flags.String("feature", "", "optional specific managed feature to inspect")
jsonOutput := flags.Bool("json", false, "print the versioned structured status")
+ render := flags.Bool("render", false, "print the branded, human-facing status banner")
if err := flags.Parse(arguments); err != nil {
return 2
}
@@ -576,6 +577,8 @@ func nextStatusCommand(arguments []string) int {
return fail(marshalErr)
}
fmt.Print(string(value))
+ } else if *render {
+ fmt.Print(boatstack.RenderNextStatusBanner(status))
} else {
fmt.Print(boatstack.FormatNextStatus(status))
}
diff --git a/boatstack/next.go b/boatstack/next.go
index 5ca9be8..51ab8a0 100644
--- a/boatstack/next.go
+++ b/boatstack/next.go
@@ -6,6 +6,7 @@ import (
"path/filepath"
"sort"
"strings"
+ "unicode/utf8"
)
const nextStatusSchemaVersion = 2
@@ -395,3 +396,177 @@ func FormatNextStatus(status NextStatus) string {
}
return strings.Join(parts, "\n") + "\n"
}
+
+// Banner glyphs (Kit C "Flightpath"). Deliberately no glyph for Boatstack itself.
+const (
+ bannerGlyphDone = "✓" // a journey node that is finished
+ bannerGlyphNow = "▸" // the node in progress right now
+ bannerGlyphTodo = "·" // a node not yet reached
+ bannerGlyphBlocked = "▲" // the current node needs a human
+ bannerGlyphComplete = "✱" // the whole feature is done
+ bannerWordmark = "Boatstack"
+ bannerRuleWidth = 44
+)
+
+// RenderNextStatusBanner produces the branded, human-facing header shown at the
+// top of every Boatstack message. It is pure presentation of the read-only
+// NextStatus: a wordmark rule, a plain-language subtitle, and an unlabeled
+// four-node progress rail with one friendly active phrase.
+//
+// Control law "banner-hides-internal-machinery": the banner MUST NEVER surface
+// internal stage names or machine codes (BUILD, TEST_PASSED, REVIEW_PASSED,
+// POLICY_READY, PUBLISHED, DRAFT_PLAN, APPROVED, NOT_INITIALIZED, INVALID_STATE,
+// AMBIGUOUS, discard-delivery, repair-state, ship-gate, …) and MUST NOT carry a
+// logo/badge for Boatstack. Every ObservedStage/NextOperation/VerificationStatus/
+// Lifecycle value maps to friendly words or degrades to a safe generic phrase.
+// The renderer emits plain Unicode (no ANSI): the banner lands inside a Markdown
+// response, so colour is a host concern applied later.
+func RenderNextStatusBanner(status NextStatus) string {
+ var b strings.Builder
+ b.WriteString(bannerRule(bannerWordmark, bannerRuleWidth) + "\n")
+
+ if subtitle := bannerSubtitle(status); subtitle != "" {
+ b.WriteString(" " + subtitle + "\n")
+ }
+
+ phrase := friendlyPhrase(status)
+ if status.VerificationStatus == "UNVERIFIED" {
+ // No feature is being tracked yet: show the wordmark and a plain phrase,
+ // no rail (an all-todo rail would imply work is queued when it is not).
+ b.WriteString(" " + phrase + "\n")
+ } else {
+ rail := strings.Join(journeyNodes(status), "──")
+ b.WriteString(" " + rail + " " + phrase + "\n")
+ }
+
+ b.WriteString(strings.Repeat("━", bannerRuleWidth) + "\n")
+ return b.String()
+}
+
+// bannerRule renders the top rule "━━
━━━…" padded to width runes.
+func bannerRule(title string, width int) string {
+ prefix := "━━ " + title + " "
+ fill := width - utf8.RuneCountInString(prefix)
+ if fill < 1 {
+ fill = 1
+ }
+ return prefix + strings.Repeat("━", fill)
+}
+
+// bannerSubtitle is the feature line, using the non-coder word "part" for slices.
+func bannerSubtitle(status NextStatus) string {
+ if status.Feature == "" {
+ return ""
+ }
+ if status.TotalSlices > 1 {
+ return fmt.Sprintf("%s · part %d of %d", status.Feature, status.SliceIndex, status.TotalSlices)
+ }
+ return status.Feature
+}
+
+// journeyNodes returns the four rail glyphs. The four nodes are a deliberate
+// user-facing abstraction of the internal machine; they are never labelled.
+func journeyNodes(status NextStatus) []string {
+ if status.ObservedStage == "FEATURE_COMPLETE" || status.Lifecycle == "PUBLISHED_MERGED" {
+ return []string{bannerGlyphDone, bannerGlyphDone, bannerGlyphDone, bannerGlyphComplete}
+ }
+ if status.ObservedStage == "PUBLISHED" {
+ return []string{bannerGlyphDone, bannerGlyphDone, bannerGlyphDone, bannerGlyphDone}
+ }
+
+ pos := stagePosition(status.ObservedStage)
+ blocked := bannerBlocked(status)
+ nodes := make([]string, 4)
+ for i := range nodes {
+ switch {
+ case i < pos:
+ nodes[i] = bannerGlyphDone
+ case i == pos:
+ if blocked {
+ nodes[i] = bannerGlyphBlocked
+ } else {
+ nodes[i] = bannerGlyphNow
+ }
+ default:
+ nodes[i] = bannerGlyphTodo
+ }
+ }
+ return nodes
+}
+
+// stagePosition collapses the internal stages into a 0..3 position on the rail.
+func stagePosition(stage string) int {
+ switch stage {
+ case "NOT_STARTED", "NOT_INITIALIZED", "DRAFT_PLAN", "AMBIGUOUS":
+ return 0
+ case "POLICY_READY", "APPROVED", "BUILD", "INVALID_STATE":
+ return 1
+ case "TEST_PASSED":
+ return 2
+ case "PR_PREVIEW", "REVIEW_PASSED":
+ return 3
+ default:
+ return 0
+ }
+}
+
+func bannerBlocked(status NextStatus) bool {
+ return status.VerificationStatus == "BLOCKED" ||
+ status.ObservedStage == "AMBIGUOUS" ||
+ status.ObservedStage == "INVALID_STATE"
+}
+
+// friendlyPhrase maps the internal status to one plain-language sentence. It must
+// never echo a raw stage name or machine code.
+func friendlyPhrase(status NextStatus) string {
+ if status.VerificationStatus == "UNVERIFIED" {
+ return "not tracking a feature here yet"
+ }
+ if bannerBlocked(status) {
+ return "needs you: " + friendlyBlockReason(status)
+ }
+ switch status.ObservedStage {
+ case "NOT_STARTED", "NOT_INITIALIZED", "DRAFT_PLAN":
+ return "getting your plan ready"
+ case "POLICY_READY", "APPROVED":
+ return "ready to build"
+ case "BUILD":
+ return "building your changes"
+ case "TEST_PASSED":
+ return "checking your changes"
+ case "PR_PREVIEW", "REVIEW_PASSED":
+ return "ready to ship"
+ case "PUBLISHED":
+ if status.Lifecycle == "PUBLISHED_MERGED" {
+ return "complete"
+ }
+ return "shipped — in review"
+ case "FEATURE_COMPLETE":
+ return "complete"
+ default:
+ return "working on your changes"
+ }
+}
+
+// friendlyBlockReason translates NextOperation into a plain "what you need to do"
+// sentence. The raw operation name is never printed.
+func friendlyBlockReason(status NextStatus) string {
+ switch status.NextOperation {
+ case "resolve-ambiguity":
+ return "pick which feature to continue"
+ case "discard-delivery":
+ return "an old draft needs clearing before we continue"
+ case "repair-state":
+ return "the workspace needs a quick reset"
+ case "init":
+ return "Boatstack isn't set up here yet"
+ case "plan-gate":
+ return "your plan needs approval"
+ case "review-gate":
+ return "a review check needs attention"
+ case "ship-gate":
+ return "a ship check needs attention"
+ default:
+ return "a check needs your attention"
+ }
+}
diff --git a/boatstack/next_banner_test.go b/boatstack/next_banner_test.go
new file mode 100644
index 0000000..3816cec
--- /dev/null
+++ b/boatstack/next_banner_test.go
@@ -0,0 +1,203 @@
+package boatstack
+
+import (
+ "strings"
+ "testing"
+)
+
+// Boundary: the human-facing status banner (RenderNextStatusBanner).
+// Control law: banner-hides-internal-machinery — the banner is pure presentation
+// of the read-only NextStatus. It must never surface an internal stage name or
+// machine code, must never carry a logo/badge for Boatstack, must render exactly
+// one "current" marker on a fixed four-node rail, and a blocked status must show
+// the needs-you marker rather than a clean in-progress marker.
+
+// The exhaustive enum value sets the banner must tolerate as inputs.
+var (
+ allObservedStages = []string{
+ "BUILD", "TEST_PASSED", "REVIEW_PASSED", "PR_PREVIEW", "PUBLISHED",
+ "FEATURE_COMPLETE", "NOT_INITIALIZED", "INVALID_STATE", "AMBIGUOUS",
+ "POLICY_READY", "APPROVED", "DRAFT_PLAN", "NOT_STARTED",
+ }
+ allVerificationStatuses = []string{"BLOCKED", "VERIFIED", "UNVERIFIED"}
+ allNextOperations = []string{
+ "build", "review-gate", "ship-gate", "none", "repair-state",
+ "discard-delivery", "init", "resolve-ambiguity", "workspace-cut",
+ "plan-gate", "workspace-cleanup", "auto-plan",
+ }
+ allLifecycles = []string{
+ "", "PUBLISHED_UNKNOWN", "PUBLISHED_OPEN", "PUBLISHED_MERGED", "PUBLISHED_CLOSED",
+ }
+
+ // Tokens that would leak the internal machine into a user-facing surface.
+ forbiddenBannerTokens = []string{
+ "BUILD", "TEST_PASSED", "REVIEW_PASSED", "PR_PREVIEW", "PUBLISHED",
+ "FEATURE_COMPLETE", "NOT_INITIALIZED", "NOT_STARTED", "INVALID_STATE",
+ "AMBIGUOUS", "POLICY_READY", "APPROVED", "DRAFT_PLAN",
+ "PUBLISHED_UNKNOWN", "PUBLISHED_OPEN", "PUBLISHED_MERGED", "PUBLISHED_CLOSED",
+ "discard-delivery", "repair-state", "ship-gate", "review-gate", "plan-gate",
+ "resolve-ambiguity", "workspace-cut", "workspace-cleanup", "auto-plan",
+ "⚓", // no logo/badge for Boatstack itself
+ }
+)
+
+// negative / bypass: no combination of inputs ever leaks internal machinery.
+// control-law: banner-hides-internal-machinery
+func TestBannerNeverLeaksInternalMachinery(t *testing.T) {
+ for _, stage := range allObservedStages {
+ for _, verification := range allVerificationStatuses {
+ for _, op := range allNextOperations {
+ for _, lifecycle := range allLifecycles {
+ status := NextStatus{
+ VerificationStatus: verification,
+ ObservedStage: stage,
+ NextOperation: op,
+ Lifecycle: lifecycle,
+ Feature: "roles-access",
+ ActiveSlice: "slice-2",
+ SliceIndex: 2,
+ TotalSlices: 4,
+ }
+ banner := RenderNextStatusBanner(status)
+ for _, token := range forbiddenBannerTokens {
+ if strings.Contains(banner, token) {
+ t.Fatalf("banner leaked internal token %q for stage=%s verification=%s op=%s lifecycle=%s:\n%s",
+ token, stage, verification, op, lifecycle, banner)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+// positive: representative states render the expected rail, phrase, and wordmark.
+// control-law: banner-hides-internal-machinery
+func TestBannerRendersExpectedFriendlyStates(t *testing.T) {
+ cases := []struct {
+ name string
+ status NextStatus
+ wantRail string
+ wantPhrase string
+ }{
+ {
+ name: "building",
+ status: NextStatus{VerificationStatus: "VERIFIED", ObservedStage: "BUILD", NextOperation: "build", Feature: "roles-access", SliceIndex: 2, TotalSlices: 4},
+ wantRail: "✓──▸──·──·",
+ wantPhrase: "building your changes",
+ },
+ {
+ name: "checking",
+ status: NextStatus{VerificationStatus: "VERIFIED", ObservedStage: "TEST_PASSED", NextOperation: "review-gate", Feature: "roles-access", SliceIndex: 2, TotalSlices: 4},
+ wantRail: "✓──✓──▸──·",
+ wantPhrase: "checking your changes",
+ },
+ {
+ name: "ready to ship",
+ status: NextStatus{VerificationStatus: "VERIFIED", ObservedStage: "REVIEW_PASSED", NextOperation: "ship-gate", Feature: "roles-access", SliceIndex: 2, TotalSlices: 4},
+ wantRail: "✓──✓──✓──▸",
+ wantPhrase: "ready to ship",
+ },
+ {
+ name: "complete",
+ status: NextStatus{VerificationStatus: "VERIFIED", ObservedStage: "FEATURE_COMPLETE", NextOperation: "none", Feature: "roles-access"},
+ wantRail: "✓──✓──✓──✱",
+ wantPhrase: "complete",
+ },
+ {
+ name: "blocked needs you",
+ status: NextStatus{VerificationStatus: "BLOCKED", ObservedStage: "INVALID_STATE", NextOperation: "discard-delivery", Feature: "roles-access", SliceIndex: 2, TotalSlices: 4},
+ wantRail: "✓──▲──·──·",
+ wantPhrase: "needs you: an old draft needs clearing before we continue",
+ },
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ banner := RenderNextStatusBanner(tc.status)
+ if !strings.Contains(banner, bannerWordmark) {
+ t.Errorf("banner missing wordmark:\n%s", banner)
+ }
+ if !strings.Contains(banner, tc.wantRail) {
+ t.Errorf("banner missing rail %q:\n%s", tc.wantRail, banner)
+ }
+ if !strings.Contains(banner, tc.wantPhrase) {
+ t.Errorf("banner missing phrase %q:\n%s", tc.wantPhrase, banner)
+ }
+ })
+ }
+}
+
+// relation: the rail is always exactly four nodes.
+// control-law: banner-hides-internal-machinery
+func TestBannerRailIsAlwaysFourNodes(t *testing.T) {
+ for _, stage := range allObservedStages {
+ nodes := journeyNodes(NextStatus{ObservedStage: stage, VerificationStatus: "VERIFIED"})
+ if len(nodes) != 4 {
+ t.Fatalf("stage %s produced %d nodes, want 4: %v", stage, len(nodes), nodes)
+ }
+ }
+}
+
+// relation: a non-terminal, non-blocked state has exactly one in-progress marker
+// and no needs-you marker.
+// control-law: banner-hides-internal-machinery
+func TestBannerSingleCurrentMarkerWhenMidFlight(t *testing.T) {
+ midFlight := []string{"NOT_STARTED", "DRAFT_PLAN", "POLICY_READY", "APPROVED", "BUILD", "TEST_PASSED", "REVIEW_PASSED", "PR_PREVIEW"}
+ for _, stage := range midFlight {
+ banner := RenderNextStatusBanner(NextStatus{ObservedStage: stage, VerificationStatus: "VERIFIED", Feature: "roles-access"})
+ if got := strings.Count(banner, bannerGlyphNow); got != 1 {
+ t.Errorf("stage %s: want exactly one %q, got %d:\n%s", stage, bannerGlyphNow, got, banner)
+ }
+ if strings.Contains(banner, bannerGlyphBlocked) {
+ t.Errorf("stage %s: mid-flight banner must not show the needs-you marker %q:\n%s", stage, bannerGlyphBlocked, banner)
+ }
+ }
+}
+
+// bypass / failure-state: a BLOCKED status always shows the needs-you marker and
+// never a clean in-progress marker — a stall can never masquerade as progress.
+// control-law: banner-hides-internal-machinery
+func TestBannerBlockedShowsNeedsYouNotProgress(t *testing.T) {
+ for _, stage := range allObservedStages {
+ if stage == "FEATURE_COMPLETE" || stage == "PUBLISHED" {
+ continue // terminal states are not blockable on the rail
+ }
+ banner := RenderNextStatusBanner(NextStatus{ObservedStage: stage, VerificationStatus: "BLOCKED", NextOperation: "repair-state", Feature: "roles-access"})
+ if !strings.Contains(banner, bannerGlyphBlocked) {
+ t.Errorf("blocked stage %s must show the needs-you marker %q:\n%s", stage, bannerGlyphBlocked, banner)
+ }
+ if strings.Contains(banner, bannerGlyphNow) {
+ t.Errorf("blocked stage %s must not show a clean in-progress marker %q:\n%s", stage, bannerGlyphNow, banner)
+ }
+ if !strings.Contains(banner, "needs you:") {
+ t.Errorf("blocked stage %s must lead with a needs-you phrase:\n%s", stage, banner)
+ }
+ }
+}
+
+// relation: the no-project (UNVERIFIED) banner shows no rail at all.
+// control-law: banner-hides-internal-machinery
+func TestBannerUnverifiedShowsNoRail(t *testing.T) {
+ banner := RenderNextStatusBanner(NextStatus{VerificationStatus: "UNVERIFIED", ObservedStage: "NOT_INITIALIZED", NextOperation: "init"})
+ for _, glyph := range []string{bannerGlyphNow, bannerGlyphDone, bannerGlyphTodo, bannerGlyphBlocked} {
+ if strings.Contains(banner, glyph) {
+ t.Errorf("UNVERIFIED banner must not render a rail glyph %q:\n%s", glyph, banner)
+ }
+ }
+ if !strings.Contains(banner, "not tracking a feature here yet") {
+ t.Errorf("UNVERIFIED banner should explain nothing is tracked:\n%s", banner)
+ }
+}
+
+// relation: subtitle uses the non-coder word "part" and is omitted for single-slice.
+// control-law: banner-hides-internal-machinery
+func TestBannerSubtitleUsesPartAndOmitsSingleSlice(t *testing.T) {
+ multi := RenderNextStatusBanner(NextStatus{VerificationStatus: "VERIFIED", ObservedStage: "BUILD", Feature: "roles-access", SliceIndex: 2, TotalSlices: 4})
+ if !strings.Contains(multi, "roles-access · part 2 of 4") {
+ t.Errorf("multi-slice subtitle wrong:\n%s", multi)
+ }
+ single := RenderNextStatusBanner(NextStatus{VerificationStatus: "VERIFIED", ObservedStage: "BUILD", Feature: "roles-access", SliceIndex: 1, TotalSlices: 1})
+ if strings.Contains(single, "part") {
+ t.Errorf("single-slice subtitle should not mention parts:\n%s", single)
+ }
+}
diff --git a/boatstack/references/workflow.md b/boatstack/references/workflow.md
index 99b7fb6..395c9cf 100644
--- a/boatstack/references/workflow.md
+++ b/boatstack/references/workflow.md
@@ -57,6 +57,17 @@ Hooks are defense in depth rather than a complete sandbox. Protected systems sti
Helper commands and state labels are internal control machinery. Every normal response uses
the structure below, with a host-compatible rendering for **Technical details**.
+### Boatstack banner
+
+Begin every Boatstack response with the status banner, so the reader can tell Boatstack's
+output apart from ordinary prose and see where their work stands at a glance. Emit the exact
+output of `boatstack-helper next-status --repo . --render` verbatim (a fenced code block or as
+plain lines), above the `## ` heading. The banner is presentation only:
+it does not replace the single `### Next step`, does not add a second action, and never
+introduces machine codes or internal stage names (the renderer already hides them). The `--json`
+projection remains the source of truth for decisions and belongs in **Technical details**, not
+the banner. Skip the banner only for replies that are not about a Boatstack operation.
+
Cursor and Claude Code use a collapsed disclosure:
```markdown
diff --git a/docs/evidence-engineered-coding.md b/docs/evidence-engineered-coding.md
index 83d683e..94aa73d 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 **19038 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 **19229 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 [`c6d00bbcf9defd9df85ead397ec1612efb650aee`](https://github.com/operatorstack/intelligence-flow/tree/c6d00bbcf9defd9df85ead397ec1612efb650aee/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 [`e37b56904acfc2a70bcc91139521ced8dd3e6051`](https://github.com/operatorstack/intelligence-flow/tree/e37b56904acfc2a70bcc91139521ced8dd3e6051/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 24eb7d3..3035b3e 100644
--- a/docs/public-claims.json
+++ b/docs/public-claims.json
@@ -1,6 +1,6 @@
{
"schema_version": 1,
- "source_commit": "c6d00bbcf9defd9df85ead397ec1612efb650aee",
+ "source_commit": "e37b56904acfc2a70bcc91139521ced8dd3e6051",
"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:c6d00bbcf9defd9df85ead397ec1612efb650aee"
+ "last_verified_version": "source:e37b56904acfc2a70bcc91139521ced8dd3e6051"
},
{
"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:c6d00bbcf9defd9df85ead397ec1612efb650aee"
+ "last_verified_version": "source:e37b56904acfc2a70bcc91139521ced8dd3e6051"
},
{
"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:c6d00bbcf9defd9df85ead397ec1612efb650aee"
+ "last_verified_version": "source:e37b56904acfc2a70bcc91139521ced8dd3e6051"
},
{
"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:c6d00bbcf9defd9df85ead397ec1612efb650aee"
+ "last_verified_version": "source:e37b56904acfc2a70bcc91139521ced8dd3e6051"
},
{
"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:c6d00bbcf9defd9df85ead397ec1612efb650aee"
+ "last_verified_version": "source:e37b56904acfc2a70bcc91139521ced8dd3e6051"
},
{
"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:c6d00bbcf9defd9df85ead397ec1612efb650aee"
+ "last_verified_version": "source:e37b56904acfc2a70bcc91139521ced8dd3e6051"
},
{
"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:c6d00bbcf9defd9df85ead397ec1612efb650aee"
+ "last_verified_version": "source:e37b56904acfc2a70bcc91139521ced8dd3e6051"
},
{
"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:c6d00bbcf9defd9df85ead397ec1612efb650aee"
+ "last_verified_version": "source:e37b56904acfc2a70bcc91139521ced8dd3e6051"
},
{
"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:c6d00bbcf9defd9df85ead397ec1612efb650aee"
+ "last_verified_version": "source:e37b56904acfc2a70bcc91139521ced8dd3e6051"
},
{
"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:c6d00bbcf9defd9df85ead397ec1612efb650aee"
+ "last_verified_version": "source:e37b56904acfc2a70bcc91139521ced8dd3e6051"
},
{
"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:c6d00bbcf9defd9df85ead397ec1612efb650aee"
+ "last_verified_version": "source:e37b56904acfc2a70bcc91139521ced8dd3e6051"
}
]
}
diff --git a/labs/diagram-json/plan.lock.json b/labs/diagram-json/plan.lock.json
index c1bf2f8..ec0326e 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": "c6d00bbcf9defd9df85ead397ec1612efb650aee",
+ "source_commit": "e37b56904acfc2a70bcc91139521ced8dd3e6051",
"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-25-boatstack-banner.md b/release-notes/2026-07-25-boatstack-banner.md
new file mode 100644
index 0000000..a458a4b
--- /dev/null
+++ b/release-notes/2026-07-25-boatstack-banner.md
@@ -0,0 +1,3 @@
+### See where your work stands with a Boatstack banner
+
+Every Boatstack message now opens with a small branded banner: the feature name, which part you're on, and a simple progress rail with one plain-language line such as "building your changes" or "ready to ship". It makes Boatstack's output easy to spot and tells you at a glance where things stand — without exposing internal workflow steps.