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/6f60e1b420c8236479d1ed76d755372de52620fc/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/5e9cd972c1a75c067b73625cf411ecd290086715/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
23 changes: 13 additions & 10 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "581c585e4246b2ef56f1d875a306446d09dc5dedc507b3aee3ee7f22697e52e2",
"CONTRIBUTING.md": "6833ef30ef884c47136f2bc2e77eec791b723c6cdf561c223cb114f1208dc9a7",
"README.md": "3ce3e95e511089b44e946a44b8d5f4f81d019ece5336db65b2cab1f9dc4d4dad",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
Expand Down Expand Up @@ -61,8 +61,10 @@
"boatstack/delivery_reactivation_test.go": "573a2dba0034bc4290478414e3bdd8670b06a326128eb0295d77e748ecc8689e",
"boatstack/delivery_test.go": "45c48ff7581c911bcaf821c3e4241d4ae2a9bb4aa682485cc58b6ad8fe1c85bf",
"boatstack/deliverycontrol_parity_test.go": "f8662cfc35043395a0e1eef8a87051c2120752f38b09c56b78f82896008f1b65",
"boatstack/denial.go": "4132674988f35b776dc3207e5179dbb2e4da6b8e815a5913acfc3c8771f6ce51",
"boatstack/denial_test.go": "b865bf041b42ad4b904de0c7cf8d26ab7b5ba5bcc29407afe6caafa318f54791",
"boatstack/denial.go": "fabc517434048c2c7327f0b123baa5e9594507f6d4be460d742f9e8f59d2d0b8",
"boatstack/denial_solutions.go": "5e5f110ee62bb08f65e8240b85d34a16cea5cb19c1993e4b39f2e83addfaf95d",
"boatstack/denial_solutions_conformance_test.go": "1055048d1d7935dded699abdb7015b30a966e8e1ae54b1253c106dde54aff10e",
"boatstack/denial_test.go": "9dc9f0f79328c4947073efaa785479b34e70eb214da57cd72348f39fd672e4fd",
"boatstack/detached.go": "b0ea2a1f31bf2a2a83f6089a3065a9b47221194b64de48af9120046e6d70dee8",
"boatstack/detached_test.go": "6cc70d15baa9a69afacf66ea29ce112efeb166836acb0a52bf9c4bb4c898cee5",
"boatstack/docs/control-law-scoping.md": "0ae984821248eabda8c0eeaf201b367991e6742984e7c718df20ecc24caee475",
Expand Down Expand Up @@ -168,9 +170,9 @@
"boatstack/runtime_cache.go": "e026ffc1906f7e1e98b768bae63e6658164d2826c07169c9121ce0f23c73faf8",
"boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85",
"boatstack/runtime_provenance_test.go": "1d52f1e6b0691cf4667729cc9b9f3c55c128f0aa3321f3a2843a9aa6fd0e73dc",
"boatstack/safety.go": "0d04805e834c9cf10ba59b823fa1dd194c226fdfcd0d67889953cb38b6305653",
"boatstack/safety_corpus_test.go": "e7d8c493d8cee957e4590f2c9034d4aa4af08bdcbe02c9889d0d98a0168bbcf9",
"boatstack/safety_test.go": "500ad53cd5e3a700553eb781d9eaf4028ae27478796759a76bfd57321fff5a7c",
"boatstack/safety.go": "432e20956789c74dfefc4b174238f320876c9e68ad6c6392286c6acdbdb5ef6d",
"boatstack/safety_corpus_test.go": "a43fdc4324abc9db0d93e407336a2b0ca5b755121013ae62ce7d844795a6d8b9",
"boatstack/safety_test.go": "ddd7a72d4ff50c046aa46fd97b108d629cef27cf15816150b6d54baf5f1c4c36",
"boatstack/safety_update_publisher_test.go": "ed3f8187036623694dfe7c395cdae00fdae14609bab6124d1fdfc6fe73fa2196",
"boatstack/skill_frontmatter.go": "73364df463ce828c2d005aab55f72bb92f7a34d99cf3f53d4e0cd5a4da9dbd0e",
"boatstack/skill_frontmatter_test.go": "a3ec52e7df357a72265c95dd66db15d9c0effc7e5f90f14ce69c27792ce394eb",
Expand Down Expand Up @@ -199,10 +201,10 @@
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "060775c73431f28bd16066bdf9e0f89034d2855c7ca0f5544f660d24b91211d0",
"docs/evidence-engineered-coding.md": "d8fce9c9a21e9ddb382811bfe397edc526360c1ebe4e8ec4f5e603436e1082b8",
"docs/evidence-engineered-coding.md": "f2c0402bd0608ffb0ee40d797d7ca464683fdc20ff3b89cdf62de37fc52ce855",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "51c2823f21e35140d31e6d5083dc4b89fddd24721ac6acc474154a4da53ee9f8",
"docs/public-claims.json": "4932a0b1c1a59b3d75fa3facc206b8ba94720079b7de8f1c01ab10c0458d3df4",
"docs/public-claims.json": "9387bb9b0de3aeba92f1558bb27aefd3faf17a89284cde234857b1aaafdcc6ed",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -216,7 +218,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": "76555eec4fcf58509beb849db669a1a924d0c9f123a784b51876ed1f9a92fd34",
"labs/diagram-json/plan.lock.json": "96dc2d5722262e62271f1488b158f726f277b0e6d63fffed95f474c2962587ab",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand Down Expand Up @@ -332,6 +334,7 @@
"release-notes/2026-07-26-workspace-reap.md": "e691d6a1c232cf218157880655413005fcb2c4f3113ededffdb80899a5054bb8",
"release-notes/2026-07-27-constitutional-boundary-floor.md": "41514cbea53867c264e354f149638444ab0d80f64896b9cf720f3979ae78b3de",
"release-notes/2026-07-27-coreachable-recovery.md": "6ffc6b0e9a7d46c0f99a64112813c33d19571c73d02e98ac5573924f1663fd54",
"release-notes/2026-07-27-denials-name-their-solutions.md": "ce05bbbb49f6d5c8d92515ecd0e89e4637ecf36f1e7f5005506d79927118be8a",
"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",
Expand All @@ -347,7 +350,7 @@
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "6f60e1b420c8236479d1ed76d755372de52620fc",
"commit": "5e9cd972c1a75c067b73625cf411ecd290086715",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
105 changes: 104 additions & 1 deletion boatstack/denial.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ type Denial struct {
Detail string // guidance; may contain `code` spans
Reassurance string // "Nothing was written; your files are untouched." (empty if an effect occurred)
Hint string // recovery command, e.g. "boatstack-helper diagnose-hook"
// Options is the denial's computed solution set: the admissible commands
// from exactly the position the finding describes, so a weaker model picks
// a legal move instead of retrying the blocked one. Derived from the same
// declarations the guard enforces; renders as a short "You can:" list and
// rides in full on the structured payload.
// control-law: solution-set-derives-from-guard-declarations
Options []PrescribedCommand
OptionsTruncated bool
// OwnerVerbs names the verbs that own a protected path (state-tamper
// denials), derived from the state-ownership map. Named, never compiled
// into runnable commands — their full arguments are not derivable here.
OwnerVerbs []string
}

// --- ANSI palette (truecolor; matches the approved mockup) -------------------
Expand Down Expand Up @@ -107,6 +119,29 @@ func (d Denial) Render(mode RenderMode) string {
}
}

// optionLines renders the solution set as at most `limit` numbered command
// lines, plus an overflow note. Shared by the three text renderers so every
// surface shows the same picks.
// control-law: solution-set-derives-from-guard-declarations
func (d Denial) optionLines(limit int) []string {
if len(d.Options) == 0 {
return nil
}
shown := d.Options
if len(shown) > limit {
shown = shown[:limit]
}
lines := make([]string, 0, len(shown)+1)
for i, option := range shown {
lines = append(lines, fmt.Sprintf(" %d) %s", i+1, option.CommandLine()))
}
hidden := len(d.Options) - len(shown)
if d.OptionsTruncated || hidden > 0 {
lines = append(lines, " (more legal moves: run boatstack-helper next-status)")
}
return lines
}

func (d Denial) renderPlain(badge string) string {
var b strings.Builder
head := badge
Expand All @@ -122,6 +157,13 @@ func (d Denial) renderPlain(badge string) string {
b.WriteString("\n\n↳ ")
b.WriteString(d.Reassurance)
}
if len(d.OwnerVerbs) > 0 {
b.WriteString("\n\nThis path is owned by: " + strings.Join(d.OwnerVerbs, ", ") + ".")
}
if lines := d.optionLines(solutionSetTextCap); len(lines) > 0 {
b.WriteString("\n\nYou can:\n")
b.WriteString(strings.Join(lines, "\n"))
}
if d.Hint != "" {
b.WriteString("\n\nFalse positive? run: ")
b.WriteString(d.Hint)
Expand All @@ -141,6 +183,15 @@ func (d Denial) renderMarkdown(badge string) string {
if d.Reassurance != "" {
b.WriteString("\n\n↳ _" + d.Reassurance + "_")
}
if len(d.OwnerVerbs) > 0 {
b.WriteString("\n\nThis path is owned by: `" + strings.Join(d.OwnerVerbs, "`, `") + "`.")
}
if lines := d.optionLines(solutionSetTextCap); len(lines) > 0 {
b.WriteString("\n\nYou can:\n")
for _, line := range lines {
b.WriteString("\n" + line)
}
}
if d.Hint != "" {
b.WriteString("\n\nFalse positive? run `" + d.Hint + "`")
}
Expand All @@ -160,6 +211,15 @@ func (d Denial) renderANSI(badge string) string {
if d.Reassurance != "" {
b.WriteString("\n" + fgGray + "↳ " + d.Reassurance + ansiReset)
}
if len(d.OwnerVerbs) > 0 {
b.WriteString("\n" + fgGray + "this path is owned by: " + ansiReset + fgCode + strings.Join(d.OwnerVerbs, ", ") + ansiReset)
}
if lines := d.optionLines(solutionSetTextCap); len(lines) > 0 {
b.WriteString("\n" + fgGray + "you can:" + ansiReset)
for _, line := range lines {
b.WriteString("\n" + fgCode + line + ansiReset)
}
}
if d.Hint != "" {
b.WriteString("\n" + fgGray + ansiDim + "false positive? run " + ansiReset + fgCode + d.Hint + ansiReset)
}
Expand Down Expand Up @@ -206,6 +266,33 @@ func (d Denial) Structured() map[string]any {
if d.Hint != "" {
out["hint"] = d.Hint
}
// Additive keys only — schema_version stays 1; a consumer that ignores them
// loses nothing (the flat reason string already carries the capped picks).
// control-law: solution-set-derives-from-guard-declarations
if len(d.Options) > 0 {
options := make([]map[string]any, 0, len(d.Options))
for _, option := range d.Options {
row := map[string]any{
"verb": option.Verb,
"command_line": option.CommandLine(),
"transition": string(option.Transition),
}
if len(option.Args) > 0 {
row["args"] = option.Args
}
if len(option.RequiresHumanInput) > 0 {
row["requires_human_input"] = option.RequiresHumanInput
}
options = append(options, row)
}
out["options"] = options
if d.OptionsTruncated {
out["options_truncated"] = true
}
}
if len(d.OwnerVerbs) > 0 {
out["owner_verbs"] = d.OwnerVerbs
}
return out
}

Expand Down Expand Up @@ -296,11 +383,27 @@ func DenialDemo(host string, mode RenderMode) string {
if i > 0 {
b.WriteString("\n\n")
}
b.WriteString(denialFor(host, finding).Render(mode))
b.WriteString(denialWithOptions(".", host, finding).Render(mode))
}
return b.String()
}

// denialWithOptions composes the pure finding→Denial mapping with the
// enumerated solution set for the finding's position. denialFor stays pure
// (DenialDemo and tests use it directly); the hook deny paths call this so
// every real denial carries its picks.
// control-law: solution-set-derives-from-guard-declarations
func denialWithOptions(repo, host string, finding SafetyFinding) Denial {
d := denialFor(host, finding)
set := enumerateDenialSolutions(repo, host, finding)
d.Options = set.Options
d.OptionsTruncated = set.Truncated
if finding.Category == "workflow-state-tamper" {
d.OwnerVerbs = tamperOwnerVerbs(repo, finding.AttemptedPath)
}
return d
}

const reassureUntouched = "Nothing was written; your files are untouched."

// denialFor maps a SafetyFinding to a structured Denial. It preserves every
Expand Down
Loading
Loading