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/9668feb5b9ba3b816d470dde2cc87c70811a7f9a/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/697ce2f7d98eb3bfb249b33d3556efbd7e9365b1/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
36 changes: 19 additions & 17 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "d79729327266363a2fbc47cca778245c619d874a74c7c4ae7ff6ecee1f9ba6b2",
"CONTRIBUTING.md": "ca94e9856aab321b125ddeb9f9845c9a26f957585ba46e5547525875b34aa64b",
"README.md": "d52c9be1a91165e3bb22318e3014278eb5326d10a45da6311f677d704e5c086d",
"assets/boatstack-journey.svg": "c1f7fe2741f5e9ca66bb3fe9b103e6364ba5acbca8b7a8054768ffd85cf325ea",
"assets/boatstack-mark.svg": "ec96165583b15cfd446c27049d49217973f3e9b1defa5771cc08eec0c9542ce4",
Expand All @@ -32,21 +32,22 @@
"boatstack/atomic_unix.go": "89f2723361591de2bb8bd22ce7e34ec529d3278509f0df78fd5c4a7d4140fbe9",
"boatstack/atomic_windows.go": "cefd775cbe7e7c3bd8a3f5673b11cdd784c6d3ebd6de7dcb8f39406b0bee511f",
"boatstack/cmd/boatstack-helper/main.go": "dbb8cb4bb403aa36a47bd9317e9a34845cabfa5b8e4152a7fb57ded1591d8957",
"boatstack/delivery.go": "bfdce7dd3bc1357a614bd458f2f6b4b8570015c117d1f7638c7a1bf3110e1a48",
"boatstack/delivery.go": "d092ac67116d651938ed1e2912bb845c3b273d650cf88049304888be9683c59c",
"boatstack/delivery_test.go": "a8a5a7e6e8dcfee1538367d49c76c531e04211876c1685265884cff26ae04497",
"boatstack/export.go": "5a2002ba99b1b0c22f503f378072902393edb9d70dba06a090fe25e5ac734612",
"boatstack/export_test.go": "1cbed87e24f1b81d20fbdd64eaef0b5b713abda06518b144d5aed5726247d183",
"boatstack/export.go": "3f086d8615769f857ba704678fea78ec63bdf28c15acd4a7bf0c1414f132e285",
"boatstack/export_test.go": "87fc1aef65f3e437d3ca9660e31b9fa77bbb6d80f9b131adff1dbd422a71fdfe",
"boatstack/go.mod": "57c377eccea51372d6664de4169e2ca45806b046f7e8a98a1e35a9eb454b4b8d",
"boatstack/hooks.go": "718161de45165e450369577e5ef1e9fdc76a484d61fa56a36eb6a117b2e63249",
"boatstack/hooks.go": "1d5d8c4bf7e6e867c8bf07e391d86158347269f856647a5d256f345bbb8d3c96",
"boatstack/hooks_test.go": "c3f359416ea53f258d8747d0247381e8946efd4d4a5bcf072c4147f885475ad3",
"boatstack/init.go": "a9740a401bf4b0b5a700b02fe8f15d80757ed2c697fa6ebce38d3c03f6c7ba75",
"boatstack/init_test.go": "ea5898df01d4fa5e039749df83f9190fd8fe95b2fe9b7e2526e0d0ecba8a7f03",
"boatstack/init.go": "3bc5b94f662c855f32ccf7db021d4783bad75e3c8c2ed7b384e3a9c60ccd4096",
"boatstack/init_test.go": "090cdde8541459330774e6b7ba17f6a0fa2c44e3a4dc7151dccb5d53b535a482",
"boatstack/init_transaction.go": "112456c4e1c4db54c4137bcf4f7a9a9e63399a6f5971e9b3dc952d0c4b2aa4b6",
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
"boatstack/plan.go": "d83037a9f06f927427a59ea936bbee4041ea9276dd84992599c16ef6259b2d4f",
"boatstack/plan.go": "6a8a6bf3352749516b65748c413c5d14d3499897570902aff707ba3de05bfc42",
"boatstack/plan_test.go": "006cdc6681f77e579c5a0f709e30ede759c337132d4f2f5193b7b79b29bd7149",
"boatstack/planning.go": "3a26417a295e5dfc2b6dcac702287c04b6053e7b74215858a4ea11cf9f9dadfe",
"boatstack/planning_test.go": "6b156a64182ed76d4c3d392b4c5a26abe5d8b81cea27ee12ac7c4627c827e186",
"boatstack/pr.go": "2af70c81108288cd9cd319f7100d1abd957e7e84f8db1474afa51a8b74e9d442",
"boatstack/pr.go": "2e0ea90a991fe4504e54972506cb306f48124c8b34be8577d96f6c97079bdf0c",
"boatstack/pr_test.go": "74afb1a9be3c4a95a426515be415149ae24b077454b486d8e69b7118c42f2916",
"boatstack/references/artifacts.md": "3a87b8f8d835329191bedb20c94e6056fe2251b3e9d510656909eb9d276069dc",
"boatstack/references/failure-moves.md": "1d35126348d0b681976e8819665e16fd745fd65eca271492603cb80aab75bf49",
Expand All @@ -55,23 +56,23 @@
"boatstack/references/workflow.md": "6a7d146c2ad072c513324ad058d4b6c38ea967f2b9c199e055cfc528f4b826ad",
"boatstack/release.go": "fa2ac926df89c90c5844e938a2e02d4b8dbbaefbf85bb7a1a89fc51690bea520",
"boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690",
"boatstack/runtime.go": "b988d57ec14e15fc6a57949a995879fc0e0d6bfa9a7b62935e7754df0b85d87a",
"boatstack/runtime_cache.go": "ab0fbb7f8a2eb8d8428e928fbc3d8866c84104e7cb330a901c121dfec82cddb3",
"boatstack/runtime.go": "f393745950e8ba2da7e25d5539ad536a6239a10c17109224bad20cf48445c380",
"boatstack/runtime_cache.go": "60c4eb0c7dde91d40d6ef3f05adc1a1282d17ff1ca12470d0a008454f7ca7489",
"boatstack/runtime_cache_test.go": "4cbca9dec7800d7df6e3ec0d74c7ecbe1508e5c5a288d863f35fc8d22986c308",
"boatstack/safety.go": "4b4ac600f0ef3c3800904e06834bd39f3887b822244b4a1fd5af7330ae91887c",
"boatstack/safety.go": "8bcce4c11094b4018093ac5fb5a5256cc5c63c825ddf7ec1a46b9d0098557d33",
"boatstack/safety_test.go": "03885d5a93f42b6adba6eef6bbef0680d81c0e1442fb72626a105da4862aecdf",
"boatstack/testdata/reviewer-pr-body.md": "4c64e3788e5d61a377aeb0f797f7fc8d2316ab6e49572d15636eea7ba9e34ac4",
"boatstack/testdata/safety/safe_apply.py.txt": "c9ec7fb932cf21b6aa8df597c4d4c54d6ec65e796240e49118d699f583383975",
"boatstack/testdata/safety/unsafe_apply.py.txt": "42db1751865cc15c4dd69a03146b5deca8f21f916d258e433b27bbef5f884ab1",
"boatstack/update.go": "4538ae8138cd8d9e25261a849e7b4a4a543c22ef18c43e21d502597ebfc250b5",
"boatstack/update.go": "d538c3f44aa869a2c7c20b9552f51fadbe4ec82ebf40f7130bc90ed3b04690f3",
"boatstack/update_test.go": "aa0c2ca97038aad661c46321600034e206e88639f89e0216b3c4cf597312cbae",
"docs/account-recovery-walkthrough.md": "676034974594a7d1a559b24dbed31d7ccc429eb81404b203ca07bbdaa19ec3d3",
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/evidence-engineered-coding.md": "027e20dc9faad84dfa533f365514ad70195cad945bf46ca17a67e3d2dcbaedbb",
"docs/evidence-engineered-coding.md": "8c79f43e67c6fa9dbd4f5eb84595ef7846b14a35d21c6be1403182f20551ab4c",
"docs/generated-files.md": "136422baf0c7fc2bd5100cfe0ebdb3d9d0705dfd7e7d54bf745dd1037e63492c",
"docs/getting-started.md": "4ef77719a01a2dbe98c5154d58fc03f51797ae0ef39a09d8804fe0708b8cc920",
"docs/public-claims.json": "915db0f39b14208ea083f9567dcedc5d19b973577c5c448136d6ca7baee94623",
"docs/public-claims.json": "0bcb69c56b2e6edd97d21b5b6d0f3ac3eb8554c46e9ea2324b46166c1814f180",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "d65c66e323037bda5d45aacef5d48afa6bf93da55901378891d235aca3a5684f",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -85,7 +86,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": "d3b39300d7dba9b5b863826b7dc14b582963daa123fa8dda605a353043d2ad7b",
"labs/diagram-json/plan.lock.json": "de95c999eb36de930d3a0fcca0d5751c1087ae49bb26b8347ec90432291457d6",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand All @@ -95,6 +96,7 @@
"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",
"release-notes/2026-07-18-atomic-initialization.md": "86cbf490778f0e0b8e7b5088b47e8168de0782624e282ed05bd54d12d2eb19f0",
"release-notes/2026-07-18-automated-releases.md": "6571eec442a27bd1a55667567f0659993c516171ef10ae71046cce80f7fd29fa",
"release-notes/2026-07-18-base-aware-release-preflight.md": "cdface46ccd959a5299de4c363c9e4057e7257820dab77dcf0e587daef5d3d99",
"release-notes/2026-07-18-claude-command-discovery.md": "bb42c23041916dee21e502449143620a8dcbd0dec4aad01162293499547f1114",
Expand All @@ -108,7 +110,7 @@
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "9668feb5b9ba3b816d470dde2cc87c70811a7f9a",
"commit": "697ce2f7d98eb3bfb249b33d3556efbd7e9365b1",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
10 changes: 6 additions & 4 deletions boatstack/delivery.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package boatstack

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -238,8 +237,8 @@ func LoadDeliveryState(repo, feature string) (DeliveryState, error) {
return DeliveryState{}, fmt.Errorf("managed delivery state is missing: %w", err)
}
var state DeliveryState
if err := json.Unmarshal(value, &state); err != nil {
return DeliveryState{}, fmt.Errorf("managed delivery state is invalid: %w", err)
if err := DecodeJSON("load managed delivery state", path, value, &state); err != nil {
return DeliveryState{}, err
}
if state.SchemaVersion != deliveryStateSchemaVersion || state.Feature != feature || len(state.Slices) == 0 || state.ActiveIndex < 0 || state.ActiveIndex > len(state.Slices) {
return DeliveryState{}, fmt.Errorf("managed delivery state is invalid")
Expand Down Expand Up @@ -385,7 +384,10 @@ func readDeliveryReceipt(repo, feature, sliceID, gate string) (DeliveryGateRecei
return DeliveryGateReceipt{}, fmt.Errorf("%s gate receipt is missing for delivery slice %s", gate, sliceID)
}
var receipt DeliveryGateReceipt
if err := json.Unmarshal(value, &receipt); err != nil || receipt.SchemaVersion != deliveryStateSchemaVersion || receipt.Feature != feature || receipt.SliceID != sliceID || receipt.Gate != gate {
if err := DecodeJSON("load delivery gate receipt", path, value, &receipt); err != nil {
return DeliveryGateReceipt{}, err
}
if receipt.SchemaVersion != deliveryStateSchemaVersion || receipt.Feature != feature || receipt.SliceID != sliceID || receipt.Gate != gate {
return DeliveryGateReceipt{}, fmt.Errorf("%s gate receipt is invalid for delivery slice %s", gate, sliceID)
}
return receipt, nil
Expand Down
25 changes: 19 additions & 6 deletions boatstack/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ var allowedAdapters = map[string]bool{
"github": true,
}

var (
readCanonical = ReadCanonical
readCanonicalDir = ReadCanonicalDir
)

type ExportBundle struct {
Files map[string][]byte
Config ProjectConfig
Expand Down Expand Up @@ -68,7 +73,7 @@ func LoadConfig(path string) (ProjectConfig, []byte, error) {
return ProjectConfig{}, nil, err
}
var config ProjectConfig
if err := json.Unmarshal(raw, &config); err != nil {
if err := DecodeJSON("load project configuration", path, raw, &config); err != nil {
return ProjectConfig{}, nil, err
}
if err := ValidateConfig(config); err != nil {
Expand Down Expand Up @@ -181,34 +186,35 @@ func BuildExportBundle(configPath string, config ProjectConfig, rawConfig []byte
}

for _, name := range []string{"workflow.md", "artifacts.md", "failure-moves.md", "irreversible-operation-boundary.md"} {
value, err := ReadCanonical("references/" + name)
value, err := readCanonical("references/" + name)
if err != nil {
return ExportBundle{}, err
}
files[".product-loop/"+name] = GeneratedMarkdown(string(value))
}

entries, err := ReadCanonicalDir("assets/templates")
entries, err := readCanonicalDir("assets/templates")
if err != nil {
return ExportBundle{}, err
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
value, err := ReadCanonical("assets/templates/" + entry.Name())
value, err := readCanonical("assets/templates/" + entry.Name())
if err != nil {
return ExportBundle{}, err
}
path := ".product-loop/templates/" + entry.Name()
if strings.HasSuffix(entry.Name(), ".json") {
var decoded any
if err := json.Unmarshal(value, &decoded); err != nil {
templateName := "assets/templates/" + entry.Name()
if err := DecodeJSON("build export bundle from JSON template", templateName, value, &decoded); err != nil {
return ExportBundle{}, err
}
files[path], err = GeneratedJSON(decoded)
if err != nil {
return ExportBundle{}, err
return ExportBundle{}, fmt.Errorf("generate JSON output %s from %s: %w", path, templateName, err)
}
} else {
files[path] = GeneratedMarkdown(string(value))
Expand Down Expand Up @@ -389,6 +395,13 @@ List explicit gaps with impact and revisit trigger, or state that no material ga
if err != nil {
return ExportBundle{}, err
}
for _, path := range sortedKeys(files) {
if strings.HasSuffix(path, ".json") {
if err := ValidateJSON("validate generated export bundle", path, files[path]); err != nil {
return ExportBundle{}, err
}
}
}
return ExportBundle{Files: files, Config: config}, nil
}

Expand Down
52 changes: 52 additions & 0 deletions boatstack/export_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,64 @@
package boatstack

import (
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
)

type fixtureDirEntry struct{ name string }

func (entry fixtureDirEntry) Name() string { return entry.name }
func (entry fixtureDirEntry) IsDir() bool { return false }
func (entry fixtureDirEntry) Type() fs.FileMode { return 0 }
func (entry fixtureDirEntry) Info() (fs.FileInfo, error) { return nil, nil }

func TestBuildExportBundleNamesMalformedEmbeddedJSONTemplate(t *testing.T) {
oldRead := readCanonical
oldReadDir := readCanonicalDir
defer func() { readCanonical, readCanonicalDir = oldRead, oldReadDir }()
readCanonicalDir = func(path string) ([]fs.DirEntry, error) {
entries, err := oldReadDir(path)
return append(entries, fixtureDirEntry{name: "nul-fixture.json"}), err
}
readCanonical = func(path string) ([]byte, error) {
if path == "assets/templates/nul-fixture.json" {
return []byte{'{', 0, '}'}, nil
}
return oldRead(path)
}
config := testConfig()
raw, err := MarshalJSON(config)
if err != nil {
t.Fatal(err)
}
_, err = BuildExportBundle(".boatstack-project.json", config, raw, "boatstack")
if err == nil || !strings.Contains(err.Error(), "build export bundle from JSON template") || !strings.Contains(err.Error(), "assets/templates/nul-fixture.json") {
t.Fatalf("malformed template error lacks operation or exact asset: %v", err)
}
}

func TestDecodeJSONAlwaysNamesOperationAndSource(t *testing.T) {
for _, test := range []struct {
operation string
source string
}{
{"load project configuration", "/repo/.boatstack-project.json"},
{"validate generated export bundle", ".product-loop/generated.lock.json"},
{"look up latest release", "GitHub releases/latest response"},
} {
t.Run(test.operation, func(t *testing.T) {
var decoded any
err := DecodeJSON(test.operation, test.source, []byte{'{', 0, '}'}, &decoded)
if err == nil || !strings.Contains(err.Error(), "operation "+test.operation) || !strings.Contains(err.Error(), "parse JSON "+test.source) {
t.Fatalf("JSON diagnostic lacks operation or source: %v", err)
}
})
}
}

func testConfig() ProjectConfig {
return ProjectConfig{
SchemaVersion: 1,
Expand Down
37 changes: 27 additions & 10 deletions boatstack/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@ func loadHookConfig(path string) (map[string]any, error) {
return nil, err
}
config := map[string]any{}
if err := json.Unmarshal(value, &config); err != nil {
return nil, fmt.Errorf("invalid host hook config %s: %w", path, err)
if err := DecodeJSON("load host hook configuration", path, value, &config); err != nil {
return nil, err
}
return config, nil
}
Expand Down Expand Up @@ -275,27 +275,44 @@ func mergeHostHook(config map[string]any, host string) error {
}

func InstallHostHooks(repo string, adapters []string) error {
prepared, err := PrepareHostHooks(repo, adapters)
if err != nil {
return err
}
for _, path := range sortedKeys(prepared) {
if err := atomicWrite(path, prepared[path]); err != nil {
return err
}
}
return nil
}

// PrepareHostHooks renders and validates every selected host document without
// writing, allowing initialization to fail before entering its commit phase.
func PrepareHostHooks(repo string, adapters []string) (map[string][]byte, error) {
prepared := map[string][]byte{}
for _, host := range []string{"cursor", "claude", "codex"} {
if !contains(adapters, host) {
continue
}
path := hostHookConfigPath(repo, host)
config, err := loadHookConfig(path)
if err != nil {
return err
return nil, err
}
if err := mergeHostHook(config, host); err != nil {
return err
return nil, fmt.Errorf("prepare %s host hooks in %s: %w", host, path, err)
}
value, err := MarshalJSON(config)
if err != nil {
return err
return nil, fmt.Errorf("serialize merged host hook configuration %s: %w", path, err)
}
if err := atomicWrite(path, value); err != nil {
return err
if err := ValidateJSON("validate merged host hook configuration", path, value); err != nil {
return nil, err
}
prepared[path] = value
}
return nil
return prepared, nil
}

func CheckHostHooks(repo string, adapters []string) error {
Expand All @@ -318,8 +335,8 @@ func CheckInstalledHostHooks(repo string, adapters []string) error {
if err != nil {
return nil, fmt.Errorf("cannot read installed %s hook fragment: %w", host, err)
}
if err := json.Unmarshal(value, &fragment); err != nil {
return nil, fmt.Errorf("invalid installed %s hook fragment: %w", host, err)
if err := DecodeJSON("load installed host hook fragment", path, value, &fragment); err != nil {
return nil, err
}
if fragment["schema_version"] != float64(1) || fragment["host"] != host {
return nil, fmt.Errorf("invalid installed %s hook fragment identity", host)
Expand Down
Loading
Loading