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/8129a03686d1f9a997ae31326ede835899a74382/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/039454bde99f8059e1a8ee0356ef433f7837cd74/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,7 +12,7 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "7470a53aa869b8e3e5db7da216873955c32084176403337fc3f62256b0fbb44a",
"CONTRIBUTING.md": "583ad367e1375a741b32879d96dbf084a8c6b3ed8edc6d701342f7b384fc27f1",
"README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
Expand Down Expand Up @@ -50,6 +50,7 @@
"boatstack/command_test.go": "9f707abba3640add81c3e97ba7e72fedbf98f3394b1c060a9ca4b4a28e919968",
"boatstack/compiled_artifact_resolution_test.go": "0748d67643263e698211eb04d46464e1dd3db15d94537f5fd5092b5aa689745b",
"boatstack/config_documentation_test.go": "aaecea04ee178ecf7872fff23a64014a3968dce1e7624e61a9453567e99969a9",
"boatstack/content_effect_conformance_test.go": "ebf4f6d50af0a76722177c717c79d33921948b9c06bec2e9d062769dce32b8aa",
"boatstack/context.go": "02510af176d2d040c0080086f06d1235e76a1d47fef5176f96f740ad18d27660",
"boatstack/decision.go": "257ca328da6ae19ab252f10ee5d06bd7daf49dd8141d083ab1b32f106ea7a94c",
"boatstack/decision_test.go": "1a92ff832610f9559bd47ccac7fc1755a8b4f8261c35bc72a092830dff05f7c0",
Expand Down Expand Up @@ -164,8 +165,8 @@
"boatstack/runtime_cache.go": "e026ffc1906f7e1e98b768bae63e6658164d2826c07169c9121ce0f23c73faf8",
"boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85",
"boatstack/runtime_provenance_test.go": "1d52f1e6b0691cf4667729cc9b9f3c55c128f0aa3321f3a2843a9aa6fd0e73dc",
"boatstack/safety.go": "ae69a5ab0609767d69c7ca27e6093c77c6576a0e4860bfe5090f50daec1485f8",
"boatstack/safety_corpus_test.go": "bf8d8a4993c9598cecf371e53c245f88cad0ed29841a6b312262cf2db4caf43b",
"boatstack/safety.go": "405782eba91a1718a061faa65ff52c10d345cbf3bccf5ecd2cb8ed45d5df9c00",
"boatstack/safety_corpus_test.go": "e7d8c493d8cee957e4590f2c9034d4aa4af08bdcbe02c9889d0d98a0168bbcf9",
"boatstack/safety_test.go": "500ad53cd5e3a700553eb781d9eaf4028ae27478796759a76bfd57321fff5a7c",
"boatstack/safety_update_publisher_test.go": "ed3f8187036623694dfe7c395cdae00fdae14609bab6124d1fdfc6fe73fa2196",
"boatstack/skill_frontmatter.go": "73364df463ce828c2d005aab55f72bb92f7a34d99cf3f53d4e0cd5a4da9dbd0e",
Expand All @@ -192,10 +193,10 @@
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "060775c73431f28bd16066bdf9e0f89034d2855c7ca0f5544f660d24b91211d0",
"docs/evidence-engineered-coding.md": "4a34055e046930643283281a6364c8e3a976ad56036909bb372b341daef9329d",
"docs/evidence-engineered-coding.md": "c81d462de78afc834b04acc99e6a816f97ac6e05c98f065a35167eb9feb4fce7",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8",
"docs/public-claims.json": "9c0c75c2ecb4828ef8ad9be21b46e114adb8ce9e251c8f6ec544af3aec71ad13",
"docs/public-claims.json": "79ddc45aebfbbaba1054a731413183a47da0d1c6c299e444869d6986a27f7498",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -209,7 +210,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": "95b94e46b1dd097b11f6aac765ecf5d1ca2d525e375a662971c7a16182cd3f4d",
"labs/diagram-json/plan.lock.json": "5807b7d5140a41e9db5a20256e4bd4adebb2820778db2f378803059dd111ea45",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand Down Expand Up @@ -326,6 +327,7 @@
"release-notes/2026-07-27-constitutional-boundary-floor.md": "41514cbea53867c264e354f149638444ab0d80f64896b9cf720f3979ae78b3de",
"release-notes/2026-07-27-coreachable-recovery.md": "6ffc6b0e9a7d46c0f99a64112813c33d19571c73d02e98ac5573924f1663fd54",
"release-notes/2026-07-27-discoverable-planning-errors.md": "d8099d1a6cd1805c3fcd446d9fa95739dec93e57ff421ecf4f99562b143c9966",
"release-notes/2026-07-27-document-content-is-data.md": "c6a35c222bf53ba465fadf21e7e95e1764b52e12e41852e51584ce9cb6513f4a",
"release-notes/2026-07-27-first-planning-write-owned-channel.md": "7a37e7abf7fd5f8612aca4323d55af1748c9e668bb294518619a1c39e195309f",
"release-notes/2026-07-27-guard-dual-reward-corpus.md": "6bec0385c6c553f00517259821e502796ca1b1907aeab718a287560e3e0fa0d6",
"release-notes/2026-07-27-invalid-delivery-block-actionable.md": "8fac8e3921e2285291703efa46e624b72cb5bac1b8492beca4c4b633abb5ba16",
Expand All @@ -336,7 +338,7 @@
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "8129a03686d1f9a997ae31326ede835899a74382",
"commit": "039454bde99f8059e1a8ee0356ef433f7837cd74",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
149 changes: 149 additions & 0 deletions boatstack/content_effect_conformance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package boatstack

import (
"path/filepath"
"strings"
"testing"
)

// control-law: written-content-is-data-not-effect
//
// A file-writer tool's document body is inert until something executes it, so
// the guard grades a Write/Edit by its NAME and STRUCTURAL fields (paths,
// destinations) — never by prose inside the body. Previously the text
// classifiers ran over the full marshaled input, so a runbook that mentioned
// `terraform destroy` or docs that named `.git/boatstack/` were denied as if
// the words were the act. The floor is unchanged: the same strings as a Bash
// command still block (the text IS the command), a live SQL executor's
// arguments still block (they are executed), and a protected path in any
// structural field still blocks (redaction never launders the target).

// contentDocCases name every non-sqlEffect destruction rule once, as prose.
var contentDocCases = []struct {
name string
content string
}{
{"database-reset", "Ask the operator before `supabase db reset` on staging."},
{"filesystem-destruction", "Cleanup appendix: `rm -rf ~` is the classic footgun; never script it."},
{"git-history-destruction", "Policy: `git push --force origin main` is forbidden; use revert PRs."},
{"infrastructure-destruction", "Teardown runbook: `terraform destroy -auto-approve` after sign-off only."},
{"recovery-destruction", "Quarterly drill: delete backup snapshots older than the retention window."},
{"guard-state-doc", "Delivery state lives under `.git/boatstack/deliveries/` — helper-owned."},
}

// Positive: documents whose bodies name every destruction rule (and the
// managed state tree) pass as file-writer tool calls.
func TestWrittenContentIsNotClassifiedAsEffect(t *testing.T) {
repo := safetyTestRepo(t)
for _, c := range contentDocCases {
c := c
t.Run("write/"+c.name, func(t *testing.T) {
findings := ClassifyTool(repo, "Write", map[string]any{
"file_path": filepath.Join(repo, "docs", c.name+".md"),
"content": c.content,
})
if len(findings) > 0 {
t.Fatalf("document content classified as effect: %#v", findings)
}
})
t.Run("edit/"+c.name, func(t *testing.T) {
findings := ClassifyTool(repo, "Edit", map[string]any{
"file_path": filepath.Join(repo, "docs", c.name+".md"),
"old_string": "TODO",
"new_string": c.content,
})
if len(findings) > 0 {
t.Fatalf("edit content classified as effect: %#v", findings)
}
})
}
}

// Negative: the executor contexts keep the boundary — the same text blocks
// when it IS the command, and executed SQL arguments stay live.
func TestExecutorContextsStillBlockAfterRedaction(t *testing.T) {
repo := safetyTestRepo(t)
for _, command := range []string{
`terraform destroy -auto-approve`,
`git push --force origin main`,
`supabase db reset`,
} {
if findings := ClassifyCommand(repo, command); len(findings) == 0 {
t.Fatalf("live command must still block: %q", command)
}
}
if findings := ClassifyTool(repo, "mcp__db__execute_sql", map[string]any{"query": "DROP TABLE users"}); len(findings) == 0 {
t.Fatal("SQL executor arguments are executed, not stored — must still block")
}
}

// Relation: one table drives both outcomes for the identical hook-shaped tool
// call — content alone allows, a protected structural path denies.
func TestContentAllowsWhilePathDenies(t *testing.T) {
repo := safetyTestRepo(t)
content := "Ops note: state is under .git/boatstack/deliveries/ and terraform destroy is operator-only."

if findings := ClassifyTool(repo, "Write", map[string]any{
"file_path": filepath.Join(repo, "docs", "ops.md"),
"content": content,
}); len(findings) > 0 {
t.Fatalf("content-only mention must pass: %#v", findings)
}

findings := ClassifyTool(repo, "Write", map[string]any{
"file_path": ".git/boatstack/deliveries/checkout/state.json",
"content": content,
})
if len(findings) == 0 {
t.Fatal("write INTO managed state must deny regardless of content")
}
if findings[0].Category != "workflow-state-tamper" {
t.Fatalf("wrong category for state tamper: %#v", findings)
}
}

// Bypass: redaction drops only content fields — a protected path smuggled in
// any structural field (file_path, destination, nested) still blocks, and a
// non-writer tool keeps full-input grading.
func TestRedactionCannotLaunderProtectedTargets(t *testing.T) {
repo := safetyTestRepo(t)

for name, input := range map[string]map[string]any{
"file_path": {"file_path": ".git/boatstack/flow/trajectory.jsonl", "content": "x"},
"destination": {"file_path": "notes.md", "destination": ".git/boatstack/deliveries/x", "content": "x"},
"nested": {"file_path": "notes.md", "meta": map[string]any{"target_path": ".git/boatstack/runtimes/v1"}, "content": "x"},
} {
if findings := ClassifyTool(repo, "Write", input); len(findings) == 0 {
t.Fatalf("structural field %s must survive redaction and deny: %#v", name, input)
}
}

// A tool with no extracted path is not a file-writer: full-input grading holds.
if findings := ClassifyTool(repo, "mcp__infra__delete_resource", map[string]any{
"kind": "database", "note": "drop the staging cluster",
}); len(findings) == 0 {
t.Fatal("non-writer destructive tool must keep full-input grading")
}
}

// Failure-state: redaction is pure — the caller's input map is never mutated,
// and classification performs no I/O on the named document.
func TestRedactionIsPureAndReadOnly(t *testing.T) {
repo := safetyTestRepo(t)
input := map[string]any{
"file_path": filepath.Join(repo, "docs", "ops.md"),
"content": "terraform destroy notes",
"meta": map[string]any{"body": "git reset --hard"},
}
_ = ClassifyTool(repo, "Write", input)

if input["content"] != "terraform destroy notes" {
t.Fatalf("caller input mutated: %#v", input)
}
if meta := input["meta"].(map[string]any); meta["body"] != "git reset --hard" {
t.Fatalf("nested caller input mutated: %#v", meta)
}
if strings.Contains(strings.ToLower("docs/ops.md"), "boatstack") {
t.Fatal("fixture invariant")
}
}
60 changes: 58 additions & 2 deletions boatstack/safety.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,50 @@ func attemptedRepositoryPath(repo string, input any) string {
return visit(input)
}

// contentInputKeys are the tool-input fields that carry a document body rather
// than structure. They are dropped before text classification of a file-writer
// tool call: the body is data (inert until executed), while structural fields
// — file_path, destination, url — survive redaction, so a protected path in
// any of them is still graded.
var contentInputKeys = map[string]bool{
"content": true, "contents": true, "new_string": true, "old_string": true,
"new_str": true, "old_str": true, "patch": true, "diff": true,
"text": true, "body": true, "data": true,
}

// redactContentFields deep-copies a decoded tool input with content-bearing
// fields removed, at every nesting depth. The original input is never mutated.
func redactContentFields(input any) any {
switch value := input.(type) {
case map[string]any:
out := make(map[string]any, len(value))
for key, item := range value {
if contentInputKeys[strings.ToLower(key)] {
continue
}
out[key] = redactContentFields(item)
}
return out
case []any:
out := make([]any, 0, len(value))
for _, item := range value {
out = append(out, redactContentFields(item))
}
return out
default:
return input
}
}

// fileWriterTool reports whether a tool call is a file write: it names a target
// path and its verb shape is a writer (write/edit/patch/create). Live SQL
// executors are excluded — their arguments are executed, not stored. Only
// file-writer calls get content redaction; everything else keeps full-input
// text grading. control-law: written-content-is-data-not-effect
func fileWriterTool(nameLower, attemptedPath string) bool {
return attemptedPath != "" && planningMutationToolPattern.MatchString(nameLower) && !toolExecutesLiveSQL(nameLower)
}

// featureScopedPath reports whether a repo-relative path lands anywhere under
// the managed planning tree. Broader than planningMarkdownPath on purpose: the
// first-write latch covers every depth and name, while planningMarkdownPath
Expand Down Expand Up @@ -808,13 +852,25 @@ func ClassifyTool(repo, name string, input any) []SafetyFinding {
if err != nil {
return []SafetyFinding{{Category: "malformed-tool-input", Reason: "invalid-tool-input", Source: "tool-input"}}
}
nameLower := strings.ToLower(name)
attemptedPath := attemptedRepositoryPath(repo, input)
// Written content is DATA, not effect: a file-writer tool's document body is
// inert until something executes it, so the text classifiers grade only the
// tool name and its structural fields (paths, destinations) — a runbook that
// MENTIONS `terraform destroy` or `.git/boatstack/` is not the act of running
// or tampering with either. Bash command strings stay fully text-scanned (the
// text IS the command), and live SQL-executor tools keep full-input grading.
// control-law: written-content-is-data-not-effect
if fileWriterTool(nameLower, attemptedPath) {
if redacted, redactErr := json.Marshal(redactContentFields(input)); redactErr == nil {
value = redacted
}
}
combined := name + " " + string(value)
// Bare SQL grammar in a tool's arguments is a live capability only when the tool
// itself executes SQL (an MCP execute_sql / db query tool). A Write/Edit/Read
// whose content merely contains DDL is a document, not an execution.
findings := classifySafetyText(combined, "tool-input", toolExecutesLiveSQL(name))
nameLower := strings.ToLower(name)
attemptedPath := attemptedRepositoryPath(repo, input)
mutationCapable := mutationToolPattern.MatchString(nameLower) || (strings.HasPrefix(nameLower, "mcp__") && !externalReadOnlyToolPattern.MatchString(nameLower))
if mutationCapable {
if finding, blocked := preActivationFinding(repo, attemptedPath); blocked {
Expand Down
23 changes: 23 additions & 0 deletions boatstack/safety_corpus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ func TestGuardCorpusDualReward(t *testing.T) {
}, "constitutional", true},
{"cp-first-plan", "", `cp draft.md .product-loop/features/checkout/plan.md`, "constitutional", true},
{"redirect-first-source-plan", "", `printf x > .product-loop/features/checkout/source-plan.md`, "constitutional", true},
// Content redaction must never launder a protected TARGET: writing INTO
// managed runtime state is tampering regardless of what the body says.
// control-law: written-content-is-data-not-effect
{"write-into-delivery-state", "Write", map[string]any{
"file_path": ".git/boatstack/deliveries/checkout/state.json",
"content": "{}",
}, "constitutional", true},

// --- routine: ordinary product work, MUST pass (false-positive axis) ---
{"git-add-migration", "", `git add migrate.sql`, "routine", false},
Expand All @@ -90,6 +97,22 @@ func TestGuardCorpusDualReward(t *testing.T) {
"file_path": filepath.Join(repo, "src", "app.ts"),
"content": "export const x = 1\n",
}, "routine", false},
// Written content is data: documenting destructive commands or managed
// paths in a file body is not the act of running or tampering with them.
// control-law: written-content-is-data-not-effect
{"write-runbook-mentions-destroy", "Write", map[string]any{
"file_path": filepath.Join(repo, "runbook.md"),
"content": "Recovery drill: run `terraform destroy -target=module.stale` then restore from the snapshot.",
}, "routine", false},
{"write-doc-mentions-guard-state", "Write", map[string]any{
"file_path": filepath.Join(repo, "docs", "ops.md"),
"content": "Delivery state lives under `.git/boatstack/deliveries/` and is owned by the helper.",
}, "routine", false},
{"edit-note-mentions-git-reset", "Edit", map[string]any{
"file_path": filepath.Join(repo, "notes.md"),
"old_string": "TODO",
"new_string": "Never run `git reset --hard` on shared branches; use revert PRs.",
}, "routine", false},
}

var constTotal, constBlocked, routineTotal, routinePassed int
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 [`8129a03686d1f9a997ae31326ede835899a74382`](https://github.com/operatorstack/intelligence-flow/tree/8129a03686d1f9a997ae31326ede835899a74382/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 [`039454bde99f8059e1a8ee0356ef433f7837cd74`](https://github.com/operatorstack/intelligence-flow/tree/039454bde99f8059e1a8ee0356ef433f7837cd74/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