Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
26 changes: 14 additions & 12 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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"
}
Expand Down
2 changes: 1 addition & 1 deletion boatstack/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions boatstack/cmd/boatstack-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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))
}
Expand Down
175 changes: 175 additions & 0 deletions boatstack/next.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"path/filepath"
"sort"
"strings"
"unicode/utf8"
)

const nextStatusSchemaVersion = 2
Expand Down Expand Up @@ -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 "━━ <title> ━━━…" 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"
}
}
Loading
Loading