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 `## <Plain-language outcome>` 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.