Skip to content

Commit 0b71c21

Browse files
Sync Boatstack from Intelligence Flow Labs @ 039454bde99f (#133)
Co-authored-by: operator-stack-publisher[bot] <operator-stack-publisher[bot]@users.noreply.github.com>
1 parent 8a5d0b7 commit 0b71c21

9 files changed

Lines changed: 261 additions & 24 deletions

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Contributing
44

5-
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).
5+
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).
66

77
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.
88

UPSTREAM.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"files": {
1414
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
15-
"CONTRIBUTING.md": "7470a53aa869b8e3e5db7da216873955c32084176403337fc3f62256b0fbb44a",
15+
"CONTRIBUTING.md": "583ad367e1375a741b32879d96dbf084a8c6b3ed8edc6d701342f7b384fc27f1",
1616
"README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad",
1717
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
1818
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
@@ -50,6 +50,7 @@
5050
"boatstack/command_test.go": "9f707abba3640add81c3e97ba7e72fedbf98f3394b1c060a9ca4b4a28e919968",
5151
"boatstack/compiled_artifact_resolution_test.go": "0748d67643263e698211eb04d46464e1dd3db15d94537f5fd5092b5aa689745b",
5252
"boatstack/config_documentation_test.go": "aaecea04ee178ecf7872fff23a64014a3968dce1e7624e61a9453567e99969a9",
53+
"boatstack/content_effect_conformance_test.go": "ebf4f6d50af0a76722177c717c79d33921948b9c06bec2e9d062769dce32b8aa",
5354
"boatstack/context.go": "02510af176d2d040c0080086f06d1235e76a1d47fef5176f96f740ad18d27660",
5455
"boatstack/decision.go": "257ca328da6ae19ab252f10ee5d06bd7daf49dd8141d083ab1b32f106ea7a94c",
5556
"boatstack/decision_test.go": "1a92ff832610f9559bd47ccac7fc1755a8b4f8261c35bc72a092830dff05f7c0",
@@ -164,8 +165,8 @@
164165
"boatstack/runtime_cache.go": "e026ffc1906f7e1e98b768bae63e6658164d2826c07169c9121ce0f23c73faf8",
165166
"boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85",
166167
"boatstack/runtime_provenance_test.go": "1d52f1e6b0691cf4667729cc9b9f3c55c128f0aa3321f3a2843a9aa6fd0e73dc",
167-
"boatstack/safety.go": "ae69a5ab0609767d69c7ca27e6093c77c6576a0e4860bfe5090f50daec1485f8",
168-
"boatstack/safety_corpus_test.go": "bf8d8a4993c9598cecf371e53c245f88cad0ed29841a6b312262cf2db4caf43b",
168+
"boatstack/safety.go": "405782eba91a1718a061faa65ff52c10d345cbf3bccf5ecd2cb8ed45d5df9c00",
169+
"boatstack/safety_corpus_test.go": "e7d8c493d8cee957e4590f2c9034d4aa4af08bdcbe02c9889d0d98a0168bbcf9",
169170
"boatstack/safety_test.go": "500ad53cd5e3a700553eb781d9eaf4028ae27478796759a76bfd57321fff5a7c",
170171
"boatstack/safety_update_publisher_test.go": "ed3f8187036623694dfe7c395cdae00fdae14609bab6124d1fdfc6fe73fa2196",
171172
"boatstack/skill_frontmatter.go": "73364df463ce828c2d005aab55f72bb92f7a34d99cf3f53d4e0cd5a4da9dbd0e",
@@ -192,10 +193,10 @@
192193
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
193194
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
194195
"docs/configuration.md": "060775c73431f28bd16066bdf9e0f89034d2855c7ca0f5544f660d24b91211d0",
195-
"docs/evidence-engineered-coding.md": "4a34055e046930643283281a6364c8e3a976ad56036909bb372b341daef9329d",
196+
"docs/evidence-engineered-coding.md": "c81d462de78afc834b04acc99e6a816f97ac6e05c98f065a35167eb9feb4fce7",
196197
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
197198
"docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8",
198-
"docs/public-claims.json": "9c0c75c2ecb4828ef8ad9be21b46e114adb8ce9e251c8f6ec544af3aec71ad13",
199+
"docs/public-claims.json": "79ddc45aebfbbaba1054a731413183a47da0d1c6c299e444869d6986a27f7498",
199200
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
200201
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
201202
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
@@ -209,7 +210,7 @@
209210
"labs/diagram-json/compiled/evidence.md": "1ba1c989ade070a8ef9a508fbd788d100d7292f2dbacbb2bce895468019f619d",
210211
"labs/diagram-json/compiled/tasks.json": "88f60851abf79d851e9fccc754ff3040034ae595306bc87d64784c19eb403e71",
211212
"labs/diagram-json/compiled/test-matrix.json": "424657ff505768e50fa113801fd8363364a18269d5297480907a993d44063a39",
212-
"labs/diagram-json/plan.lock.json": "95b94e46b1dd097b11f6aac765ecf5d1ca2d525e375a662971c7a16182cd3f4d",
213+
"labs/diagram-json/plan.lock.json": "5807b7d5140a41e9db5a20256e4bd4adebb2820778db2f378803059dd111ea45",
213214
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
214215
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
215216
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
@@ -326,6 +327,7 @@
326327
"release-notes/2026-07-27-constitutional-boundary-floor.md": "41514cbea53867c264e354f149638444ab0d80f64896b9cf720f3979ae78b3de",
327328
"release-notes/2026-07-27-coreachable-recovery.md": "6ffc6b0e9a7d46c0f99a64112813c33d19571c73d02e98ac5573924f1663fd54",
328329
"release-notes/2026-07-27-discoverable-planning-errors.md": "d8099d1a6cd1805c3fcd446d9fa95739dec93e57ff421ecf4f99562b143c9966",
330+
"release-notes/2026-07-27-document-content-is-data.md": "c6a35c222bf53ba465fadf21e7e95e1764b52e12e41852e51584ce9cb6513f4a",
329331
"release-notes/2026-07-27-first-planning-write-owned-channel.md": "7a37e7abf7fd5f8612aca4323d55af1748c9e668bb294518619a1c39e195309f",
330332
"release-notes/2026-07-27-guard-dual-reward-corpus.md": "6bec0385c6c553f00517259821e502796ca1b1907aeab718a287560e3e0fa0d6",
331333
"release-notes/2026-07-27-invalid-delivery-block-actionable.md": "8fac8e3921e2285291703efa46e624b72cb5bac1b8492beca4c4b633abb5ba16",
@@ -336,7 +338,7 @@
336338
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
337339
"schema_version": 1,
338340
"source": {
339-
"commit": "8129a03686d1f9a997ae31326ede835899a74382",
341+
"commit": "039454bde99f8059e1a8ee0356ef433f7837cd74",
340342
"path": "labs/12-product-engineering-loop",
341343
"repository": "operatorstack/intelligence-flow"
342344
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package boatstack
2+
3+
import (
4+
"path/filepath"
5+
"strings"
6+
"testing"
7+
)
8+
9+
// control-law: written-content-is-data-not-effect
10+
//
11+
// A file-writer tool's document body is inert until something executes it, so
12+
// the guard grades a Write/Edit by its NAME and STRUCTURAL fields (paths,
13+
// destinations) — never by prose inside the body. Previously the text
14+
// classifiers ran over the full marshaled input, so a runbook that mentioned
15+
// `terraform destroy` or docs that named `.git/boatstack/` were denied as if
16+
// the words were the act. The floor is unchanged: the same strings as a Bash
17+
// command still block (the text IS the command), a live SQL executor's
18+
// arguments still block (they are executed), and a protected path in any
19+
// structural field still blocks (redaction never launders the target).
20+
21+
// contentDocCases name every non-sqlEffect destruction rule once, as prose.
22+
var contentDocCases = []struct {
23+
name string
24+
content string
25+
}{
26+
{"database-reset", "Ask the operator before `supabase db reset` on staging."},
27+
{"filesystem-destruction", "Cleanup appendix: `rm -rf ~` is the classic footgun; never script it."},
28+
{"git-history-destruction", "Policy: `git push --force origin main` is forbidden; use revert PRs."},
29+
{"infrastructure-destruction", "Teardown runbook: `terraform destroy -auto-approve` after sign-off only."},
30+
{"recovery-destruction", "Quarterly drill: delete backup snapshots older than the retention window."},
31+
{"guard-state-doc", "Delivery state lives under `.git/boatstack/deliveries/` — helper-owned."},
32+
}
33+
34+
// Positive: documents whose bodies name every destruction rule (and the
35+
// managed state tree) pass as file-writer tool calls.
36+
func TestWrittenContentIsNotClassifiedAsEffect(t *testing.T) {
37+
repo := safetyTestRepo(t)
38+
for _, c := range contentDocCases {
39+
c := c
40+
t.Run("write/"+c.name, func(t *testing.T) {
41+
findings := ClassifyTool(repo, "Write", map[string]any{
42+
"file_path": filepath.Join(repo, "docs", c.name+".md"),
43+
"content": c.content,
44+
})
45+
if len(findings) > 0 {
46+
t.Fatalf("document content classified as effect: %#v", findings)
47+
}
48+
})
49+
t.Run("edit/"+c.name, func(t *testing.T) {
50+
findings := ClassifyTool(repo, "Edit", map[string]any{
51+
"file_path": filepath.Join(repo, "docs", c.name+".md"),
52+
"old_string": "TODO",
53+
"new_string": c.content,
54+
})
55+
if len(findings) > 0 {
56+
t.Fatalf("edit content classified as effect: %#v", findings)
57+
}
58+
})
59+
}
60+
}
61+
62+
// Negative: the executor contexts keep the boundary — the same text blocks
63+
// when it IS the command, and executed SQL arguments stay live.
64+
func TestExecutorContextsStillBlockAfterRedaction(t *testing.T) {
65+
repo := safetyTestRepo(t)
66+
for _, command := range []string{
67+
`terraform destroy -auto-approve`,
68+
`git push --force origin main`,
69+
`supabase db reset`,
70+
} {
71+
if findings := ClassifyCommand(repo, command); len(findings) == 0 {
72+
t.Fatalf("live command must still block: %q", command)
73+
}
74+
}
75+
if findings := ClassifyTool(repo, "mcp__db__execute_sql", map[string]any{"query": "DROP TABLE users"}); len(findings) == 0 {
76+
t.Fatal("SQL executor arguments are executed, not stored — must still block")
77+
}
78+
}
79+
80+
// Relation: one table drives both outcomes for the identical hook-shaped tool
81+
// call — content alone allows, a protected structural path denies.
82+
func TestContentAllowsWhilePathDenies(t *testing.T) {
83+
repo := safetyTestRepo(t)
84+
content := "Ops note: state is under .git/boatstack/deliveries/ and terraform destroy is operator-only."
85+
86+
if findings := ClassifyTool(repo, "Write", map[string]any{
87+
"file_path": filepath.Join(repo, "docs", "ops.md"),
88+
"content": content,
89+
}); len(findings) > 0 {
90+
t.Fatalf("content-only mention must pass: %#v", findings)
91+
}
92+
93+
findings := ClassifyTool(repo, "Write", map[string]any{
94+
"file_path": ".git/boatstack/deliveries/checkout/state.json",
95+
"content": content,
96+
})
97+
if len(findings) == 0 {
98+
t.Fatal("write INTO managed state must deny regardless of content")
99+
}
100+
if findings[0].Category != "workflow-state-tamper" {
101+
t.Fatalf("wrong category for state tamper: %#v", findings)
102+
}
103+
}
104+
105+
// Bypass: redaction drops only content fields — a protected path smuggled in
106+
// any structural field (file_path, destination, nested) still blocks, and a
107+
// non-writer tool keeps full-input grading.
108+
func TestRedactionCannotLaunderProtectedTargets(t *testing.T) {
109+
repo := safetyTestRepo(t)
110+
111+
for name, input := range map[string]map[string]any{
112+
"file_path": {"file_path": ".git/boatstack/flow/trajectory.jsonl", "content": "x"},
113+
"destination": {"file_path": "notes.md", "destination": ".git/boatstack/deliveries/x", "content": "x"},
114+
"nested": {"file_path": "notes.md", "meta": map[string]any{"target_path": ".git/boatstack/runtimes/v1"}, "content": "x"},
115+
} {
116+
if findings := ClassifyTool(repo, "Write", input); len(findings) == 0 {
117+
t.Fatalf("structural field %s must survive redaction and deny: %#v", name, input)
118+
}
119+
}
120+
121+
// A tool with no extracted path is not a file-writer: full-input grading holds.
122+
if findings := ClassifyTool(repo, "mcp__infra__delete_resource", map[string]any{
123+
"kind": "database", "note": "drop the staging cluster",
124+
}); len(findings) == 0 {
125+
t.Fatal("non-writer destructive tool must keep full-input grading")
126+
}
127+
}
128+
129+
// Failure-state: redaction is pure — the caller's input map is never mutated,
130+
// and classification performs no I/O on the named document.
131+
func TestRedactionIsPureAndReadOnly(t *testing.T) {
132+
repo := safetyTestRepo(t)
133+
input := map[string]any{
134+
"file_path": filepath.Join(repo, "docs", "ops.md"),
135+
"content": "terraform destroy notes",
136+
"meta": map[string]any{"body": "git reset --hard"},
137+
}
138+
_ = ClassifyTool(repo, "Write", input)
139+
140+
if input["content"] != "terraform destroy notes" {
141+
t.Fatalf("caller input mutated: %#v", input)
142+
}
143+
if meta := input["meta"].(map[string]any); meta["body"] != "git reset --hard" {
144+
t.Fatalf("nested caller input mutated: %#v", meta)
145+
}
146+
if strings.Contains(strings.ToLower("docs/ops.md"), "boatstack") {
147+
t.Fatal("fixture invariant")
148+
}
149+
}

boatstack/safety.go

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,50 @@ func attemptedRepositoryPath(repo string, input any) string {
317317
return visit(input)
318318
}
319319

320+
// contentInputKeys are the tool-input fields that carry a document body rather
321+
// than structure. They are dropped before text classification of a file-writer
322+
// tool call: the body is data (inert until executed), while structural fields
323+
// — file_path, destination, url — survive redaction, so a protected path in
324+
// any of them is still graded.
325+
var contentInputKeys = map[string]bool{
326+
"content": true, "contents": true, "new_string": true, "old_string": true,
327+
"new_str": true, "old_str": true, "patch": true, "diff": true,
328+
"text": true, "body": true, "data": true,
329+
}
330+
331+
// redactContentFields deep-copies a decoded tool input with content-bearing
332+
// fields removed, at every nesting depth. The original input is never mutated.
333+
func redactContentFields(input any) any {
334+
switch value := input.(type) {
335+
case map[string]any:
336+
out := make(map[string]any, len(value))
337+
for key, item := range value {
338+
if contentInputKeys[strings.ToLower(key)] {
339+
continue
340+
}
341+
out[key] = redactContentFields(item)
342+
}
343+
return out
344+
case []any:
345+
out := make([]any, 0, len(value))
346+
for _, item := range value {
347+
out = append(out, redactContentFields(item))
348+
}
349+
return out
350+
default:
351+
return input
352+
}
353+
}
354+
355+
// fileWriterTool reports whether a tool call is a file write: it names a target
356+
// path and its verb shape is a writer (write/edit/patch/create). Live SQL
357+
// executors are excluded — their arguments are executed, not stored. Only
358+
// file-writer calls get content redaction; everything else keeps full-input
359+
// text grading. control-law: written-content-is-data-not-effect
360+
func fileWriterTool(nameLower, attemptedPath string) bool {
361+
return attemptedPath != "" && planningMutationToolPattern.MatchString(nameLower) && !toolExecutesLiveSQL(nameLower)
362+
}
363+
320364
// featureScopedPath reports whether a repo-relative path lands anywhere under
321365
// the managed planning tree. Broader than planningMarkdownPath on purpose: the
322366
// first-write latch covers every depth and name, while planningMarkdownPath
@@ -808,13 +852,25 @@ func ClassifyTool(repo, name string, input any) []SafetyFinding {
808852
if err != nil {
809853
return []SafetyFinding{{Category: "malformed-tool-input", Reason: "invalid-tool-input", Source: "tool-input"}}
810854
}
855+
nameLower := strings.ToLower(name)
856+
attemptedPath := attemptedRepositoryPath(repo, input)
857+
// Written content is DATA, not effect: a file-writer tool's document body is
858+
// inert until something executes it, so the text classifiers grade only the
859+
// tool name and its structural fields (paths, destinations) — a runbook that
860+
// MENTIONS `terraform destroy` or `.git/boatstack/` is not the act of running
861+
// or tampering with either. Bash command strings stay fully text-scanned (the
862+
// text IS the command), and live SQL-executor tools keep full-input grading.
863+
// control-law: written-content-is-data-not-effect
864+
if fileWriterTool(nameLower, attemptedPath) {
865+
if redacted, redactErr := json.Marshal(redactContentFields(input)); redactErr == nil {
866+
value = redacted
867+
}
868+
}
811869
combined := name + " " + string(value)
812870
// Bare SQL grammar in a tool's arguments is a live capability only when the tool
813871
// itself executes SQL (an MCP execute_sql / db query tool). A Write/Edit/Read
814872
// whose content merely contains DDL is a document, not an execution.
815873
findings := classifySafetyText(combined, "tool-input", toolExecutesLiveSQL(name))
816-
nameLower := strings.ToLower(name)
817-
attemptedPath := attemptedRepositoryPath(repo, input)
818874
mutationCapable := mutationToolPattern.MatchString(nameLower) || (strings.HasPrefix(nameLower, "mcp__") && !externalReadOnlyToolPattern.MatchString(nameLower))
819875
if mutationCapable {
820876
if finding, blocked := preActivationFinding(repo, attemptedPath); blocked {

boatstack/safety_corpus_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ func TestGuardCorpusDualReward(t *testing.T) {
6767
}, "constitutional", true},
6868
{"cp-first-plan", "", `cp draft.md .product-loop/features/checkout/plan.md`, "constitutional", true},
6969
{"redirect-first-source-plan", "", `printf x > .product-loop/features/checkout/source-plan.md`, "constitutional", true},
70+
// Content redaction must never launder a protected TARGET: writing INTO
71+
// managed runtime state is tampering regardless of what the body says.
72+
// control-law: written-content-is-data-not-effect
73+
{"write-into-delivery-state", "Write", map[string]any{
74+
"file_path": ".git/boatstack/deliveries/checkout/state.json",
75+
"content": "{}",
76+
}, "constitutional", true},
7077

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

95118
var constTotal, constBlocked, routineTotal, routinePassed int

docs/evidence-engineered-coding.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,6 @@ Delivery and system improvement also remain separate. A failed task may suggest
146146

147147
## What is evidence-backed
148148

149-
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).
149+
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).
150150

151151
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.

0 commit comments

Comments
 (0)