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/af3a40320dd2525fccc99e67c7ca26a43ec7ded8/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/3c8217d87c7117173b02ce29cae5316ccb6beca4/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
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,31 @@ Receipts remain as history; published corrections become linked deliveries.

## Features

- **A guided path from idea to PR.** Start with `/auto-plan`; Boatstack presents one next action at a time through planning, approval, build, validation, review, and PR preparation.
- **A guided path from idea to PR.** `/auto-plan` starts a one-action-at-a-time delivery flow.
- **Instant orientation after a break.** `boatstack next` reconstructs the verified stage without treating chat or a running process as workflow evidence.
- **Human decisions stay human.** Material product questions remain open until a person answers them, and implementation waits for explicit approval.
- **Evidence tied to the promise.** Tests and checks map to the outcomes the change claims to deliver instead of treating one green command as proof of everything.
- **Context that survives the feature.** Plans, decisions, accepted gaps, evidence, review findings, and code state can inform the next feature rather than disappearing with the chat.
- **Context that survives the feature.** Plans, decisions, gaps, evidence, and code state remain useful beyond the chat.
- **Conversational repair after Build.** Describe what changed; Boatstack preserves valid work and reruns only affected boundaries.
- **Safer agent execution.** High-confidence destructive recovery is stopped before execution; phased work is gated and published one approved delivery slice at a time.
- **Reviewer-ready pull requests.** Boatstack carries the reason, actual changes, validation, risks, gaps, rollout, and rollback into a focused PR brief.
- **Portable across your AI stack.** Cursor, Codex, Claude Code, different model tiers, and specialist skills use the same repository-owned delivery contract.
- **Repository-friendly maintenance.** Linked worktrees restore their verified runtime automatically, while Boatstack updates stay isolated in reviewable infrastructure PRs.
- **Reviewer-ready pull requests.** Actual changes, evidence, risks, rollout, and rollback become a focused PR brief.
- **Optional repository changelog.** Require readable `CHANGELOG.md` entries grounded in actual changes.
- **Portable across your AI stack.** Hosts, models, and skills share one repository-owned delivery contract.
- **Repository-friendly maintenance.** Worktrees restore runtime; updates stay in separate infrastructure PRs.

You remain free to build however the work requires. Boatstack governs claims of approval, completion, review, and shipping—not the implementation technique.
### Optional changelog

It is disabled by default. Enable it in `.boatstack-project.json`:

```json
{
"workflow": {
"maintain_changelog": true
}
}
```

Enabled repositories require a categorized `CHANGELOG.md` → `Unreleased` entry for every managed slice and Boatstack-prepared ad-hoc PR. The file stays user-owned; install and update never overwrite it. [See the format and first-entry example](docs/getting-started.md#keep-a-repository-changelog).

## How Boatstack fits into your AI stack

Expand Down
45 changes: 24 additions & 21 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"canonical_context": {
"characters": 42600,
"estimated_tokens": 10650,
"characters": 43583,
"estimated_tokens": 10896,
"estimator": "ceil(total characters / 4); compactness signal, not provider billing",
"files": [
"product-engineering-loop/references/workflow.md",
Expand All @@ -12,8 +12,8 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "acc6358f197620db553f49454b13ca97230d3cf39f394a775524feeafde1c79c",
"README.md": "73147594dce1b867d0258c87301032ee5e20ad83ee74843fd076a31838880f0e",
"CONTRIBUTING.md": "115637ba74936e2d3452359dc3bbe3b3520e7cebcabe696896ab7beb61cf2177",
"README.md": "a840004db57a129ecd361bb0421adfe02065d86751736dd91953c8a8b178e5ef",
"assets/boatstack-journey.svg": "c1f7fe2741f5e9ca66bb3fe9b103e6364ba5acbca8b7a8054768ffd85cf325ea",
"assets/boatstack-mark.svg": "ec96165583b15cfd446c27049d49217973f3e9b1defa5771cc08eec0c9542ce4",
"assets/boatstack-portability.svg": "ce648f5581d16586d25824d3a8132ef1b3d88b73329179173120129d4f74fd24",
Expand All @@ -26,21 +26,23 @@
"boatstack/assets/templates/gaps.md": "911cc2f086104d35071b952950c2ec44258641419f10b2355c594f33eb492cbe",
"boatstack/assets/templates/move.md": "91bfd9a9b9426ac023eb88fd19f4f638190481c1855f1239acc73830528e50f0",
"boatstack/assets/templates/plan-lock.json": "a51e17bb74aa7cd95daaa70fab646a20374ff4bc1d63468d61c5119da61e930f",
"boatstack/assets/templates/plan.md": "1c7d5802b67d674c13bee51a028c5ba933d8b9bfbb32e894af66338f3f4040f5",
"boatstack/assets/templates/plan.md": "ce0d43ea36a40486b0e3fc5edd11649d9591abb1ccf6b3c3affdba0048d6cd05",
"boatstack/assets/templates/questions.md": "1133b557a832d4988545f3694b365ebffa808640ed696b6c04b5a390266eed80",
"boatstack/assets/templates/test-plan.md": "6db8a9f27dd171fb80222a501cae50eb051e7278c04703fa43b5ff86dd4d2df4",
"boatstack/atomic_unix.go": "89f2723361591de2bb8bd22ce7e34ec529d3278509f0df78fd5c4a7d4140fbe9",
"boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f",
"boatstack/changelog.go": "5f0b1e7a66586c58aad03bfea8126178316f82ff6583bd67d35d6fcf71a6f9bc",
"boatstack/changelog_test.go": "2cee0d580dc2fa0752ee58564d030961b20139008de132ee8abff9e6b6713e28",
"boatstack/cmd/boatstack-helper/main.go": "b4b8b43d80dbf60f15e18885ff8ee01637df6e47c0249938714d885163350539",
"boatstack/delivery.go": "907a0ff8dc3e6120387eef3c7d97cdadb0b9dd8f788cb39e149aa7a6c4a6260a",
"boatstack/delivery_test.go": "744d166757deafe5b9fc4f66b79b324de43dcc36a26a317572eaec73a8b21044",
"boatstack/export.go": "f097e3b143011c13c17fcba7cd147c19848b2f9b472862fe8cad4bbff6310e57",
"boatstack/export_test.go": "67c3b8ce09aa63bd8c5168f3d3fe56156c5b47cef4d2f83bbcaa7eb7302ad712",
"boatstack/delivery.go": "565bc8465252a798124a29874e1aeffff16e9027d1a86715e11b3e4bc0743bd2",
"boatstack/delivery_test.go": "2545c26389d330a92c11e7b09fb2342101db5d09a825a2a9e81b4ea9eea9407b",
"boatstack/export.go": "48032b870f226b1a2758f332aa5b684fad65c2dd5370ae6447d694f406a6a467",
"boatstack/export_test.go": "9474985bf2aab4f2f14d37dcc3cedc6d7a74db39739fbbfd7615297e35b079b5",
"boatstack/go.mod": "57c377eccea51372d6664de4169e2ca45806b046f7e8a98a1e35a9eb454b4b8d",
"boatstack/hooks.go": "1d5d8c4bf7e6e867c8bf07e391d86158347269f856647a5d256f345bbb8d3c96",
"boatstack/hooks_test.go": "c3f359416ea53f258d8747d0247381e8946efd4d4a5bcf072c4147f885475ad3",
"boatstack/init.go": "fb863a68a6cced5bd2dcf9cd8ca53f0dab7ef0309830269efd985ad8b8a92059",
"boatstack/init_test.go": "46ef98d955224d7744a5d67798bbc001f2d4c2c6aa2ac8a802c56e91a3ad8fb8",
"boatstack/init_test.go": "eb91d280077115c0dbe3d8472e1e4fa9794d5ae6e5a58afbb9c3f4f39aee80d8",
"boatstack/init_transaction.go": "112456c4e1c4db54c4137bcf4f7a9a9e63399a6f5971e9b3dc952d0c4b2aa4b6",
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
"boatstack/next.go": "9a9b3b9dde0a528991d2b6ac358638b249a791a4eb3e7a2656bd43b652a2c628",
Expand All @@ -49,18 +51,18 @@
"boatstack/plan_test.go": "006cdc6681f77e579c5a0f709e30ede759c337132d4f2f5193b7b79b29bd7149",
"boatstack/planning.go": "3a26417a295e5dfc2b6dcac702287c04b6053e7b74215858a4ea11cf9f9dadfe",
"boatstack/planning_test.go": "6b156a64182ed76d4c3d392b4c5a26abe5d8b81cea27ee12ac7c4627c827e186",
"boatstack/pr.go": "2e0ea90a991fe4504e54972506cb306f48124c8b34be8577d96f6c97079bdf0c",
"boatstack/pr_test.go": "74afb1a9be3c4a95a426515be415149ae24b077454b486d8e69b7118c42f2916",
"boatstack/pr.go": "ba537fc35cc8d2aaf584c5ba6de1194fdd1c4255dfb227a09478457752e2b9ad",
"boatstack/pr_test.go": "ae23130d9d96cf214cf272227aa572dd09e2fe3adac22921949f541ba99ecb23",
"boatstack/references/artifacts.md": "8f2e79b8af4bd3ad2e32aa3e8c07f10812555d0a3247e4991db75cc1cda395c3",
"boatstack/references/failure-moves.md": "1d35126348d0b681976e8819665e16fd745fd65eca271492603cb80aab75bf49",
"boatstack/references/irreversible-operation-boundary.md": "2a695f2d7de95cfc8750f107bef9c86581712aa1f02e7233b69b850d8c2af42e",
"boatstack/references/portability.md": "fb683095991bb0cb06ec56fb8884c49038b283172a7d2f8b203483b7cacb4bae",
"boatstack/references/workflow.md": "7e889d106aaab0a4acbb2c0af6a79b914a7599f84cb03eb26d1f28c33c4e3bea",
"boatstack/references/workflow.md": "b460b647bdd5d0ce4f1bd1015434a9e6c3b8ef525920ed018fb26a22bbc3aa9a",
"boatstack/release.go": "fa2ac926df89c90c5844e938a2e02d4b8dbbaefbf85bb7a1a89fc51690bea520",
"boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690",
"boatstack/run.go": "a9afb239c5e2cb80f96dab594f0dc94852f2f16625b1de05c5110ee85f61cfaa",
"boatstack/run_test.go": "fdc416f15e787b5c8401fc0f0e3aeb58b4891c828a8a869c8dce4e8f1541d809",
"boatstack/runtime.go": "f393745950e8ba2da7e25d5539ad536a6239a10c17109224bad20cf48445c380",
"boatstack/runtime.go": "504b7b68e550ed178c2c12bb815aa21e67300cad65cf2311bb5e8b2860747f81",
"boatstack/runtime_cache.go": "60c4eb0c7dde91d40d6ef3f05adc1a1282d17ff1ca12470d0a008454f7ca7489",
"boatstack/runtime_cache_test.go": "4cbca9dec7800d7df6e3ec0d74c7ecbe1508e5c5a288d863f35fc8d22986c308",
"boatstack/safety.go": "8bcce4c11094b4018093ac5fb5a5256cc5c63c825ddf7ec1a46b9d0098557d33",
Expand All @@ -73,10 +75,10 @@
"docs/account-recovery-walkthrough.md": "676034974594a7d1a559b24dbed31d7ccc429eb81404b203ca07bbdaa19ec3d3",
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/evidence-engineered-coding.md": "bbf3f5d0cddccae591aee3b92083ba78df20451a7f920d3b94129cf3ab33c563",
"docs/evidence-engineered-coding.md": "800027421c4aa40d31b337ecb4aa96edc672911c7e475501f40fd01543e1387f",
"docs/generated-files.md": "136422baf0c7fc2bd5100cfe0ebdb3d9d0705dfd7e7d54bf745dd1037e63492c",
"docs/getting-started.md": "61efc6bd618bd6674687d8efac9e1bc425fe5c10e72f87e267461dd2a830736e",
"docs/public-claims.json": "13a35ca66fd3ff382da2e181b6f14b8d28fdbc2621a6c322fd5a67bc79d9cdd8",
"docs/getting-started.md": "c298c0d78054266099ae98232a4c29976fa4e176d4ace49b6a20a8c13c35bc51",
"docs/public-claims.json": "85a6de5fb913d6e039323d0b7d0dc166f3abae1f231354af034288214ef680e5",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "d65c66e323037bda5d45aacef5d48afa6bf93da55901378891d235aca3a5684f",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -90,13 +92,13 @@
"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": "e48d49d74d6b3f337f3d9ef14243a6f9bea44bf55641f3dea459d04e61d02626",
"labs/diagram-json/plan.lock.json": "d9320eed4adfd09430d9bb6bc4fd6878cbb32a25e1662e130105f18c40839958",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
"labs/diagram-json/source-plan.md": "e10593ddaa7522ab80cc991d0a09399257139799e37f737794cd49d68a39985b",
"labs/diagram-json/spec.md": "506b12b57bef99183d8b9a87b9f1aa5e68c39b910de88d7637b2207b7b1d6bb4",
"project.example.json": "d1f7aa3cff0b55ede79500bd2ca710bb99cb2ae579f0a058dc00934accf03d33",
"project.example.json": "f90fa25280c75e37a7073d384f61330b0889248fd2ca263babcc57a0bb8afd09",
"release-notes/2026-07-17-delivery-harness-framing.md": "6fccbe7efdb288f5eca7e824ee5e44ad6b30a34f312eb997dca6aa44c55b26cb",
"release-notes/2026-07-17-phase-scoped-delivery.md": "bfc8edd30a67daf5940ad4ceceebf81d01d62914ebeabb7073985c53f13df2ff",
"release-notes/2026-07-17-visible-release-messages.md": "c93e8c812528a983502263e35d66c86a265d6ccba8203f393788c069b3fa6606",
Expand All @@ -113,12 +115,13 @@
"release-notes/2026-07-18-run-through-ship.md": "e69d314fe65265eb1f38c933340772d15bbb53b74c33d9d489b8a55849f545c7",
"release-notes/2026-07-18-safety-sql-boundaries.md": "32011ca3d02a371e8f3f2899ffb34df3af0843d18d25e7db95fbca32c2dcf18c",
"release-notes/2026-07-18-stacked-bar-mark.md": "c4d5bd5fb89c280d7fba015384fd795fcb8c31ffe501078aa55a90cbcf66ba7b",
"release-notes/2026-07-18-startup-recovery-routing.md": "c305a1c2b8347e1bc6d7fcbd4a8e629438cdeb4bcf6f181a7d6fc154a88f4318"
"release-notes/2026-07-18-startup-recovery-routing.md": "c305a1c2b8347e1bc6d7fcbd4a8e629438cdeb4bcf6f181a7d6fc154a88f4318",
"release-notes/2026-07-19-optional-repository-changelog.md": "7ae6bed436c269335ac5055e04438489451bef00836fecb3e5fcb06a18501853"
},
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "af3a40320dd2525fccc99e67c7ca26a43ec7ded8",
"commit": "3c8217d87c7117173b02ce29cae5316ccb6beca4",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
3 changes: 2 additions & 1 deletion boatstack/assets/templates/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,5 @@ Boatstack rejects ambiguous targets, automated resets, and destructive rollback.
Every task belongs to exactly one delivery slice. Each slice receives its own
build, test, review, ship confirmation, and PR; plan approval never authorizes
publication by itself. Internal implementation phases should remain ordinary tasks
inside one delivery slice.
inside one delivery slice. When `workflow.maintain_changelog` is enabled, include
`CHANGELOG.md` in the affected paths of tasks assigned to every delivery slice.
147 changes: 147 additions & 0 deletions boatstack/changelog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package boatstack

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)

const changelogPath = "CHANGELOG.md"

var changelogCategories = map[string]bool{
"Added": true,
"Changed": true,
"Fixed": true,
"Removed": true,
"Security": true,
"Documentation": true,
"Maintenance": true,
}

// changelogEntries returns the categorized bullets in the Unreleased section.
// Historical release sections are intentionally ignored: the policy requires a
// new reader-facing entry for the change being prepared, not rewritten history.
func changelogEntries(value []byte) (map[string]int, error) {
entries := map[string]int{}
inUnreleased := false
category := ""
foundUnreleased := false
for _, rawLine := range strings.Split(strings.ReplaceAll(string(value), "\r\n", "\n"), "\n") {
line := strings.TrimSpace(rawLine)
if strings.HasPrefix(line, "## ") {
if line == "## Unreleased" {
if foundUnreleased {
return nil, fmt.Errorf("%s must contain exactly one ## Unreleased section", changelogPath)
}
foundUnreleased = true
inUnreleased = true
category = ""
continue
}
if inUnreleased {
inUnreleased = false
}
continue
}
if !inUnreleased || line == "" {
continue
}
if strings.HasPrefix(line, "### ") {
category = strings.TrimSpace(strings.TrimPrefix(line, "### "))
if !changelogCategories[category] {
return nil, fmt.Errorf("%s uses unsupported Unreleased category %q", changelogPath, category)
}
continue
}
if strings.HasPrefix(line, "-") {
entry := strings.TrimSpace(strings.TrimPrefix(line, "-"))
if category == "" {
return nil, fmt.Errorf("%s Unreleased entries must appear under an allowed category", changelogPath)
}
if entry == "" {
return nil, fmt.Errorf("%s contains an empty Unreleased entry", changelogPath)
}
entries[category+"\x00"+entry]++
}
}
if !foundUnreleased {
return nil, fmt.Errorf("%s must contain a ## Unreleased section", changelogPath)
}
return entries, nil
}

func readFileAtCommit(repo, commit, path string) ([]byte, bool, error) {
command := exec.Command("git", "-C", repo, "show", commit+":"+path)
value, err := command.Output()
if err == nil {
return value, true, nil
}
if exit, ok := err.(*exec.ExitError); ok && exit.ExitCode() != 0 {
return nil, false, nil
}
return nil, false, fmt.Errorf("cannot read %s at base commit: %w", path, err)
}

func validateChangelogChange(repo, baseCommit string, config ProjectConfig) error {
if !config.Workflow.MaintainChangelog {
return nil
}
current, err := os.ReadFile(filepath.Join(repo, changelogPath))
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("changelog policy requires %s with a new entry under ## Unreleased", changelogPath)
}
return err
}
currentEntries, err := changelogEntries(current)
if err != nil {
return err
}
baseEntries := map[string]int{}
base, exists, err := readFileAtCommit(repo, baseCommit, changelogPath)
if err != nil {
return err
}
if exists {
parsed, parseErr := changelogEntries(base)
if parseErr == nil {
baseEntries = parsed
}
}
for entry, count := range currentEntries {
if count > baseEntries[entry] {
return nil
}
}
return fmt.Errorf("changelog policy requires a new categorized entry under ## Unreleased in %s", changelogPath)
}

// changelogComparisonBase makes each managed slice prove its own entry. Later
// slices compare with the previous slice's reviewed head, even when both slices
// use the same Git base and earlier Unreleased entries are still present.
func changelogComparisonBase(repo, feature, mergeBase string) (string, error) {
if strings.TrimSpace(feature) == "" {
return mergeBase, nil
}
state, err := LoadDeliveryState(repo, feature)
if err != nil {
return "", err
}
if state.ActiveIndex == 0 {
return mergeBase, nil
}
if state.ActiveIndex >= len(state.Slices) {
return "", fmt.Errorf("delivery %s has no active slice for changelog comparison", feature)
}
previous := state.Slices[state.ActiveIndex-1]
receipt, err := readDeliveryReceipt(repo, feature, previous.ID, "review")
if err != nil {
return "", fmt.Errorf("cannot establish changelog baseline for delivery slice %s: %w", state.Slices[state.ActiveIndex].ID, err)
}
if strings.TrimSpace(receipt.HeadCommit) == "" {
return "", fmt.Errorf("previous delivery slice %s has no reviewed head commit", previous.ID)
}
return receipt.HeadCommit, nil
}
Loading
Loading