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/c288814c89139fa36e7b914ce14d9917f12d32d0/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/c9e110ae06ef139d8ae336262558bc8bf21a1e2a/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
16 changes: 9 additions & 7 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "47f98fbd40e12646c9314b53ce5a1b297cfd154bae919b5751bf93db28557bd0",
"CONTRIBUTING.md": "66d71c53812017e686d180a310b71f058664a081be8f05c38299e429581b5c64",
"README.md": "125b47671a68556df382f19756fb61fa18925606cbbaf54d6bc9df8872b36870",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
"assets/boatstack-portability.svg": "66dfdfa85db857b3bd18b32047a6975f1fbbfc4dc091158e8277193f9969a346",
"boatstack/AGENTS.md": "8694b4f13fb8b0553ea065a5bb0fa216aefc69389dfc0e7ad7703d8b81baff4d",
"boatstack/BUG-worktree-delivery-state.md": "02469cf51c3849dad5743783e248e5c04583e4240507fbef0e3f890cd6a95724",
"boatstack/SKILL.md": "7c7b3568d836cb176c92762a8315fa834cd61a531a629c97a5cd98d6f7689be0",
"boatstack/agents/gemini.yaml": "cbf43b387399e456fa6178f86d83e6e35567e6142ff800f8de6ffca306fa963e",
Expand Down Expand Up @@ -69,8 +70,8 @@
"boatstack/plan_validation_test.go": "6cbde4ac719baef6b73aa569515d6a9daadcbf14b33f76fa78159826954e20fa",
"boatstack/planning.go": "ef4507a9fecc900f0691372c50883f328c9232c3dfd6986fbde26fb7ef436ae3",
"boatstack/planning_test.go": "c105a9c78c342be06614bf54d0bc1b661b0f7af64d63b79e43bd1fcc2769edd5",
"boatstack/pr.go": "d277d03008f0af2443d754998784a6e8d9540298294658b578565ff7d8fd354a",
"boatstack/pr_test.go": "838e6c244aed84317d00e6f49de6b65bb08f6fe1e1a11fb37aad8428ae2f6ea1",
"boatstack/pr.go": "bdb066acb329b6b772cb880db2e590b381cae909f270085cfacb8ef2fbfda651",
"boatstack/pr_test.go": "7f82954d94c1ceae848a581dda25e58af92251d78a5a94ed2d672bedf5a0349e",
"boatstack/recovery.go": "dd816b18b54a0085b8d8276a93ee98d2b1e90099059a0d85cf6e24edf6f37d5b",
"boatstack/recovery_test.go": "29490e7477ba602491330036a491289dd9117b99ff862f66dae421ba17e04c9f",
"boatstack/references/artifacts.md": "5fa888ac519085d65cee1d04df5902761651bcf2d7af81711fa0f8ecd1fc0f59",
Expand Down Expand Up @@ -108,10 +109,10 @@
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "4d8f207b415a5a1e3b9b1698ee7bb1221aa0e5496a061bb8054294df2f347ad1",
"docs/evidence-engineered-coding.md": "295e908297c7a1c63d9e4784e7f5a5299ab6f515f9e6364f825b2a2b8fa3688d",
"docs/evidence-engineered-coding.md": "4b7b413d909b936853b0db3fe13af84a53d5ade5a62ad4bdbbfac5189a53f700",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "f314270c5ed1a55bbef5f3ddbcb5596693dbee9374e5f0d3df8838cefbd68052",
"docs/public-claims.json": "dff1654cb0901c026770f0358918b29e11a5836a6fbc8668f7449b10ee208e53",
"docs/public-claims.json": "e57651659291d0d78ab9e62cfdfcc91d9e1d8692d044a626b7a55e1b5f77b258",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -125,7 +126,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": "100ee5d8381c04b3f11b4ffe3462a214883b85ddb1df73beb88a61251952cd41",
"labs/diagram-json/plan.lock.json": "49b9accb7a855c021b7f0bc62d283f242e0a566923d0a2f430e5dafaf76d13f4",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand Down Expand Up @@ -187,13 +188,14 @@
"release-notes/2026-07-23-canonical-update-ownership.md": "7f34f890b252493797519389b23f1b56ec7ec16db7547aac8ea196f75f3b8c2c",
"release-notes/2026-07-23-explicit-source-plan.md": "ec1f97434f9263f6db4bc3b83ae83213053cd2bc6b7465e4fb2d67cbea5cf731",
"release-notes/2026-07-23-ignore-ambiguous-deliveries.md": "9b1b9fd48db340b91fcced1c282297de0ecad8744723f2b1fdd94033927a88c4",
"release-notes/2026-07-23-managed-pr-task-graph-layout.md": "e2d67f15cc6a1eb200d6f13f81d891b30eae1bd51182d768dda561fcfdc69435",
"release-notes/2026-07-23-recoverable-repository-sync.md": "3afc4f6220ae76df3bd6dcd15fc180135274c808729e9c512a2c53462ec690c2",
"release-notes/2026-07-23-shipped-feature-candidate-resolution.md": "bd8ee8e7f3f216b356b121a83ef10cb0c8131a90b9ab803edebf23b882d9cf89"
},
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "c288814c89139fa36e7b914ce14d9917f12d32d0",
"commit": "c9e110ae06ef139d8ae336262558bc8bf21a1e2a",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
62 changes: 62 additions & 0 deletions boatstack/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Agent guide — Boatstack product-engineering-loop

Read this before opening a PR that touches anything under
`labs/12-product-engineering-loop/`.

## Every PR that changes this lab REQUIRES a new release note

CI runs `scripts/release_notes.py check-policy` (the **Generated distribution**
check). It fails the PR if the diff touches **any** file under
`labs/12-product-engineering-loop/` — source, tests, docs, scripts, anything —
without **adding** a new release-note fragment. This check is **not** part of
`go test ./...`, so a green local test run does **not** mean you are done. Skipping
the note costs a full CI round trip (fail → add note → push → re-run).

### Add the note in the same commit as your change

Create one new file per PR:

```
labs/12-product-engineering-loop/boatstack-distribution/release-notes/YYYY-MM-DD-<slug>.md
```

Contract (enforced by `validate_release_note`):

- **Name:** `YYYY-MM-DD-<slug>.md`, slug lowercase `[a-z0-9]` words joined by `-`.
- **First line:** a level-three Markdown heading — `### <Title>` (no leading blank line).
- **Body:** at least one non-empty line after the heading describing **user impact**
(what changed for someone using Boatstack, not the code mechanics).
- **Encoding/EOL:** UTF-8, and the file must end with a trailing newline.
- **Append-only:** never edit or delete an existing note. To correct a shipped
note, add a new correction fragment. Only added (`A`) files under
`release-notes/` are allowed in the diff.

### Verify locally before you push — avoid the CI round trip

Commit your change **and** the note, then run the same policy CI runs:

```
# format check on the notes directory
python3 labs/12-product-engineering-loop/scripts/release_notes.py \
validate --root labs/12-product-engineering-loop/boatstack-distribution/release-notes

# append-only + "note present for lab changes" against origin/main (needs a clean,
# committed tree — it inspects the committed PR diff, not the working tree)
python3 labs/12-product-engineering-loop/scripts/release_notes.py \
preflight --repo . --base-branch main
```

`preflight` fetches `origin/main` and checks the committed diff. `PASS` means the
**Generated distribution** check will pass; `BLOCKED` prints exactly what to fix.

## Other checks that are not in `go test`

- **Repository conformance** and **Runtime** (windows/macos/ubuntu) run in CI.
Locally, always run `go build ./...`, `go vet ./...`, and `go test ./...` from
`product-engineering-loop`, plus `python3 -m unittest tests.test_product_loop`
from `labs/12-product-engineering-loop` for the Python surface.

## PR body honesty

Do not write "no release note required" for a change under this lab — a note is
always required. State which note you added.
26 changes: 21 additions & 5 deletions boatstack/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,25 @@ func relativeSource(repo, path, kind string) (PRSource, error) {
return PRSource{Kind: kind, Path: relative, SHA256: hash}, nil
}

// featureArtifactPath resolves a feature artifact that may live in either the
// newer compiled/ subdirectory or the older feature-root layout. Candidates are
// tried in priority order (each artifact's canonical location first); the first
// that exists wins. When none exist the last candidate is returned so the
// downstream check reports a clear, canonical error path rather than a guessed
// one. This keeps the task graph and evidence resolution on one shared rule so
// the two layouts can never silently diverge.
func featureArtifactPath(directory string, candidates ...string) string {
var last string
for _, name := range candidates {
path := filepath.Join(directory, name)
last = path
if fileExists(path) {
return path
}
}
return last
}

func managedPRSources(repo, feature string) ([]PRSource, map[string]string, error) {
directory := filepath.Join(repo, ".product-loop", "features", feature)
planPath := filepath.Join(directory, "plan.md")
Expand All @@ -411,7 +430,7 @@ func managedPRSources(repo, feature string) ([]PRSource, map[string]string, erro
return nil, nil, fmt.Errorf("managed PR requires current approval: %w", err)
}
}
tasksPath := filepath.Join(directory, "compiled", "tasks.json")
tasksPath := featureArtifactPath(directory, filepath.Join("compiled", "tasks.json"), "tasks.json")
if err := CheckApprovalLock(ApprovalOptions{
SourcePlanPath: check.SourcePlanPath,
SpecPath: check.SpecPath,
Expand All @@ -422,10 +441,7 @@ func managedPRSources(repo, feature string) ([]PRSource, map[string]string, erro
}); err != nil {
return nil, nil, fmt.Errorf("managed PR requires a current build lock: %w", err)
}
evidencePath := filepath.Join(directory, "evidence.md")
if !fileExists(evidencePath) {
evidencePath = filepath.Join(directory, "compiled", "evidence.md")
}
evidencePath := featureArtifactPath(directory, "evidence.md", filepath.Join("compiled", "evidence.md"))
if err := checkNonEmptyFile(evidencePath, "feature evidence"); err != nil {
return nil, nil, err
}
Expand Down
96 changes: 95 additions & 1 deletion boatstack/pr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ func TestAdHocPRContextAndPreviewAreEvidenceLimited(t *testing.T) {
}

func activateManagedFeature(t *testing.T, repo, feature string) string {
t.Helper()
return activateManagedFeatureLayout(t, repo, feature, true)
}

// activateManagedFeatureLayout activates a feature in either the newer compiled/
// layout (compiled=true → OutDir=<feature>/compiled) or the older feature-root
// layout (compiled=false → OutDir=<feature>, tasks.json at the feature root, no
// compiled/ dir), so managed-PR resolution can be exercised against both.
func activateManagedFeatureLayout(t *testing.T, repo, feature string, compiled bool) string {
t.Helper()
directory := filepath.Join(repo, ".product-loop", "features", feature)
if err := os.MkdirAll(directory, 0o755); err != nil {
Expand Down Expand Up @@ -193,9 +202,13 @@ func activateManagedFeature(t *testing.T, repo, feature string) string {
approvalPath = filepath.Join(directory, "approval.md")
writeApprovalReceipt(t, approvalPath, check.Fingerprint)
}
outDir := directory
if compiled {
outDir = filepath.Join(directory, "compiled")
}
if err := ActivatePlan(ActivationOptions{
PlanPath: filepath.Join(directory, "plan.md"), ApprovalPath: approvalPath,
OutDir: filepath.Join(directory, "compiled"), OutputPath: filepath.Join(directory, "plan.lock.json"),
OutDir: outDir, OutputPath: filepath.Join(directory, "plan.lock.json"),
SourceCommit: runGit(t, repo, "rev-parse", "HEAD"),
}); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -257,6 +270,87 @@ No migration; revert the feature commit.
return directory
}

// TestFeatureArtifactPathResolvesBothLayouts pins the layout-resolution rule
// from the root up, independent of PR wiring: the canonical location is tried
// first, the alternate is the fallback, and when neither exists the last
// candidate is returned for a clear downstream error path.
func TestFeatureArtifactPathResolvesBothLayouts(t *testing.T) {
for _, test := range []struct {
name string
present []string
want string
}{
{name: "only compiled", present: []string{filepath.Join("compiled", "tasks.json")}, want: filepath.Join("compiled", "tasks.json")},
{name: "only root", present: []string{"tasks.json"}, want: "tasks.json"},
{name: "both prefer canonical", present: []string{filepath.Join("compiled", "tasks.json"), "tasks.json"}, want: filepath.Join("compiled", "tasks.json")},
{name: "neither returns last", present: nil, want: "tasks.json"},
} {
t.Run(test.name, func(t *testing.T) {
directory := t.TempDir()
for _, rel := range test.present {
path := filepath.Join(directory, rel)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
}
got := featureArtifactPath(directory, filepath.Join("compiled", "tasks.json"), "tasks.json")
if got != filepath.Join(directory, test.want) {
t.Fatalf("resolved %q, want %q", got, filepath.Join(directory, test.want))
}
})
}
}

// TestManagedPRSourcesAcceptsBothTaskGraphLayouts is the conformance guard for
// the build-lock layout bug: a feature activated in the older feature-root
// layout (tasks.json at the feature root, no compiled/ dir) must resolve its
// build lock exactly like the newer compiled layout, so the ship-gate is not
// blocked after build/test/review all passed.
func TestManagedPRSourcesAcceptsBothTaskGraphLayouts(t *testing.T) {
for _, test := range []struct {
name string
compiled bool
}{
{name: "compiled layout", compiled: true},
{name: "feature-root layout", compiled: false},
} {
t.Run(test.name, func(t *testing.T) {
repo := prTestRepo(t)
feature := "cta-transport-feedback"
directory := activateManagedFeatureLayout(t, repo, feature, test.compiled)

if !test.compiled {
if !fileExists(filepath.Join(directory, "tasks.json")) {
t.Fatal("feature-root fixture must place tasks.json at the feature root")
}
if fileExists(filepath.Join(directory, "compiled", "tasks.json")) {
t.Fatal("feature-root fixture must not have a compiled/tasks.json")
}
}

sources, statuses, err := managedPRSources(repo, feature)
if err != nil {
t.Fatalf("managed PR sources must resolve for the %s: %v", test.name, err)
}
if statuses["test"] != "PASS" || statuses["review"] != "PASS_WITH_GAPS" {
t.Fatalf("unexpected gate statuses: %+v", statuses)
}
found := false
for _, source := range sources {
if source.Kind == "plan_lock" {
found = true
}
}
if !found {
t.Fatalf("managed PR sources missing plan_lock: %+v", sources)
}
})
}
}

func TestManagedPRRechecksCurrentAuthorizationAndGapPolicy(t *testing.T) {
t.Run("policy activation omits approval source", func(t *testing.T) {
repo := prTestRepoConfigured(t, func(config *ProjectConfig) {
Expand Down
2 changes: 1 addition & 1 deletion docs/evidence-engineered-coding.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 [`c288814c89139fa36e7b914ce14d9917f12d32d0`](https://github.com/operatorstack/intelligence-flow/tree/c288814c89139fa36e7b914ce14d9917f12d32d0/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 [`c9e110ae06ef139d8ae336262558bc8bf21a1e2a`](https://github.com/operatorstack/intelligence-flow/tree/c9e110ae06ef139d8ae336262558bc8bf21a1e2a/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.
Loading
Loading