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/e896983570aa5117a6518003a69a4bb7b1f2ad11/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/c5f3e330d6167247446e1915deaf1bc593cf2e0d/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
30 changes: 17 additions & 13 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "353be923ee17b17d4dcdd1924fd074775399bb98a98fbef3b0f747877ef09fb3",
"CONTRIBUTING.md": "87a58a3120f70e719a3471f4ad2be5673242ef29150a65a5fd6d2b1079ee69f4",
"README.md": "125b47671a68556df382f19756fb61fa18925606cbbaf54d6bc9df8872b36870",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
Expand Down Expand Up @@ -40,17 +40,17 @@
"boatstack/capture_test.go": "63fa1177738081f1e862364d7a4257f5e259f8e9c36276ba1775b8085b277105",
"boatstack/changelog.go": "c5e1f31440b44d61e6037ad27af0333540af3545d655e35819a0241cbbebd8ec",
"boatstack/changelog_test.go": "ce792f23a7fe1e09fb3096cd1314130a6ab69321d4877b12a8e994027541baf7",
"boatstack/cmd/boatstack-helper/main.go": "5aff755d60b2f5c06dab3ed5d30df026d28c6fb90d891b3be38e955aa200621d",
"boatstack/cmd/boatstack-helper/main.go": "545c2926f0d9ae59ca884c99880ae30188a7ee8456a0253a10c83651f385f47c",
"boatstack/cmd/boatstack-helper/main_test.go": "ff73003b6a5157202fa09ddf1129fb13c3d79702b2e05a8721ce5a11bf5ab779",
"boatstack/command.go": "94d2117c6e390d5a644afc5cd90f7e712e3f8b1032f8c9e3b253cc134524c28a",
"boatstack/command.go": "4726ac515dedab4947be7eb48f88c6cb8b53d674124504b69f03e6396b080ee8",
"boatstack/command_test.go": "9f707abba3640add81c3e97ba7e72fedbf98f3394b1c060a9ca4b4a28e919968",
"boatstack/config_documentation_test.go": "1fa56768409824afaff61bbe4ae2733ca854a4cbe87254a7954c6aaa2a6d75ac",
"boatstack/config_documentation_test.go": "0632366edc5e88145bb080083ea03c6515da07b0162ce404d63e51bb5bc0774e",
"boatstack/decision.go": "257ca328da6ae19ab252f10ee5d06bd7daf49dd8141d083ab1b32f106ea7a94c",
"boatstack/decision_test.go": "1a92ff832610f9559bd47ccac7fc1755a8b4f8261c35bc72a092830dff05f7c0",
"boatstack/delivery.go": "ea53af0e702ec3668a563a5f786dcac2e095362285ca6093b7ed71ec495a0a48",
"boatstack/delivery_test.go": "564ad2029a8412de7953967b1acdf377e6c87b6f6e3465d1f8741a4813f2949f",
"boatstack/evidence.go": "497a31e6ff632cb1d7c3adfc9f269af3f6aa84e948dd5d417c162767542a27df",
"boatstack/export.go": "388c676e4fb25442a15e51e31bfc5931b6264d7a95a3422ecec0c1f78ab613d9",
"boatstack/export.go": "f4393643206eaa503dadb1a3f58069e2a3acc9525466334ce674bcffcabc7cc7",
"boatstack/export_test.go": "67eb890728d20630925d6e4e90d2a97ec025195ba5c54994c1b098ab72721dca",
"boatstack/go.mod": "6086ef1b2a83f5696190dca692c653925f27b61f652f659fd3fca43ed54a1641",
"boatstack/go.sum": "26c315c867b11b886f3c9402fce7f341f6a9115a5d61f54afbb5e1b1fb5f6017",
Expand Down Expand Up @@ -81,7 +81,7 @@
"boatstack/recovery.go": "dd816b18b54a0085b8d8276a93ee98d2b1e90099059a0d85cf6e24edf6f37d5b",
"boatstack/recovery_test.go": "29490e7477ba602491330036a491289dd9117b99ff862f66dae421ba17e04c9f",
"boatstack/references/artifacts.md": "5fa888ac519085d65cee1d04df5902761651bcf2d7af81711fa0f8ecd1fc0f59",
"boatstack/references/config-schema.md": "4df91e9769125bf21654595a749483b8db7d2a27399db346e9e194d525a328f7",
"boatstack/references/config-schema.md": "0170b90f1d0a592f58e255ffeff642fa037676042443f74a0f1b6e39be5dbbb8",
"boatstack/references/failure-moves.md": "34a39aefb282b1b5d9ea387f8536bdc5e0145a240f102ae3d01c7ada6d4abebc",
"boatstack/references/host-hook-contracts.md": "d68ae1556e7b1e29e9ac7cb4db767809d510aabf0be52e60e44665ea7abb980e",
"boatstack/references/irreversible-operation-boundary.md": "e0076f0fea3bf729b2e9bdf353eaeaaf7cdafabfaf26b8d9b27287e5414c2441",
Expand All @@ -91,7 +91,7 @@
"boatstack/release_test.go": "5cf2d76fe9b836a91ca68eba53d5585e2c4be5b9421aaf939ea0723063a24690",
"boatstack/run.go": "74967ad5b3ed3847baffec1231aae69a81a70f9cce3a9412fa05bcfdc4eca6d1",
"boatstack/run_test.go": "5b291510fa90cefdc26eb89e18a3443385456a6ebc73408325ac1945b7c084d6",
"boatstack/runtime.go": "007f38f0631200b448f27f79b8cefb9874dd767b500dc389f2c9a663d0d0f9b0",
"boatstack/runtime.go": "d1e95895002ea2b27199b6e05b33c4c6e20f63455a44f63ca3cfeedecfc23420",
"boatstack/runtime_cache.go": "60c4eb0c7dde91d40d6ef3f05adc1a1282d17ff1ca12470d0a008454f7ca7489",
"boatstack/runtime_cache_test.go": "b981467ddc9f0f562da6bff5de7a80a9fe5a433a0317541d1e48df268546ac85",
"boatstack/safety.go": "e5d91bf219838f5e0c80daaf5b18a2cca9661f211fa60e2c1fab9e5835082b36",
Expand All @@ -107,18 +107,20 @@
"boatstack/update_test.go": "bf5f19f8499db6dd7356917d867b113b790548ab89bfdea59e2adf4999a82a6a",
"boatstack/visual_evidence.go": "4d69f98adb6087d4830e6703273ae6e03b28ec8b3c496a6e432fd5e0e54d8a2f",
"boatstack/visual_evidence_test.go": "0fe8f5154ef4398dfeba5e7f7b387b2d279ed75635ea35d93ff392269f2cc6d0",
"boatstack/visual_publisher.go": "330511d000e712fa37c52af17d59c8ec9cb41f49c773a517f856651e66a60d25",
"boatstack/visual_publisher_test.go": "979f600edae00c77569ae753b80530e8cbf2e3b342995efcd992d351ff382eaf",
"boatstack/workspace.go": "91b343400b3506a6f516c28fabc3f1575f22024a5b19f934a020be660a20482e",
"boatstack/workspace_sync.go": "0cc2f03fd1aa57c66b3d603d5ef30aa820f14ab60771a795cf10c46f3c9a71b8",
"boatstack/workspace_sync_test.go": "a5fd532d23a6675c96fc5eb29a149c812717f21237a4050ae5f41afb34601273",
"boatstack/workspace_test.go": "ea022ab13cf593c84cf053eca8dad345aae656d3e089b0ae68a4ba7e7cdc87c6",
"docs/account-recovery-walkthrough.md": "676034974594a7d1a559b24dbed31d7ccc429eb81404b203ca07bbdaa19ec3d3",
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "4d8f207b415a5a1e3b9b1698ee7bb1221aa0e5496a061bb8054294df2f347ad1",
"docs/evidence-engineered-coding.md": "0b362fa9462b207203a985930545d4312d282d80becfa2bb6642afbd4b378cb5",
"docs/configuration.md": "df054f49d532c8b1b7d94184810d1b3b5bf18cdc30eb985b4b6d0639162e341a",
"docs/evidence-engineered-coding.md": "091930ef2e129debd56a13f9465364fb1b30d2bdc09d28cafb99f8bdf372a899",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "f314270c5ed1a55bbef5f3ddbcb5596693dbee9374e5f0d3df8838cefbd68052",
"docs/public-claims.json": "2777b425b9ee7cef373d178fa97a9aa9558aae95eef40e27ef9bf5e88e091184",
"docs/public-claims.json": "1cd0ea8ce15058bf69e18bef9a8eabcc57d77bf7fb6c05d0ff46b5ba78ca0ff2",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -132,7 +134,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": "e80e6a008dcba346d40af08f6d4be2bf5fe15241173bac6170f958e0690c7f12",
"labs/diagram-json/plan.lock.json": "1dfe1ea29c712d61940cc77f259f8bb0ecdcd073d6426eb29eda6612cbb645b5",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand Down Expand Up @@ -200,14 +202,16 @@
"release-notes/2026-07-23-labkit-publishing-pipeline.md": "b3bfc5f28baf3961fb187380ff66e5383186c05a933bc6ca162416ce61149218",
"release-notes/2026-07-23-labkit-standalone-package.md": "50ae9ba89fdada6e04a9200ea26d53a2a04484a1759cdea663687cc9b42fb93c",
"release-notes/2026-07-23-managed-pr-task-graph-layout.md": "e2d67f15cc6a1eb200d6f13f81d891b30eae1bd51182d768dda561fcfdc69435",
"release-notes/2026-07-23-programmatic-visual-publisher.md": "d84e94e6c62fb45aff94fd467582a7c1d940c4542cce0021564074c23383fe91",
"release-notes/2026-07-23-recoverable-repository-sync.md": "3afc4f6220ae76df3bd6dcd15fc180135274c808729e9c512a2c53462ec690c2",
"release-notes/2026-07-23-shipped-feature-candidate-resolution.md": "bd8ee8e7f3f216b356b121a83ef10cb0c8131a90b9ab803edebf23b882d9cf89",
"release-notes/2026-07-23-sync-title-contract.md": "2869d6d084ea60402e57ffe985d0fc4cd83ef9bb09958cc53e349157d3383202"
"release-notes/2026-07-23-sync-title-contract.md": "2869d6d084ea60402e57ffe985d0fc4cd83ef9bb09958cc53e349157d3383202",
"release-notes/2026-07-23-visual-evidence-external-host.md": "09edbe5e6e1bfc866cf5ee744a5001d678f7a67f0f330cf43bd5157eedf04276"
},
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "e896983570aa5117a6518003a69a4bb7b1f2ad11",
"commit": "c5f3e330d6167247446e1915deaf1bc593cf2e0d",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
1 change: 1 addition & 0 deletions boatstack/cmd/boatstack-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ func publishPRCommand(arguments []string) int {
}
url, err := boatstack.PublishPR(boatstack.PRPublishOptions{
Repo: *repo, PreviewPath: *previewPath, ExpectedFingerprint: *fingerprint, Action: *action,
VisualPublisher: boatstack.SelectVisualPublisher(*repo),
})
if err != nil {
return fail(err)
Expand Down
15 changes: 15 additions & 0 deletions boatstack/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package boatstack
import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"
)
Expand Down Expand Up @@ -47,3 +48,17 @@ func commandOutput(repo string, name string, arguments ...string) (string, error
}
return strings.TrimSpace(string(channels.Stdout)), nil
}

// commandOutputEnv is commandOutput with additional environment variables appended
// to the inherited environment. It keeps the same stdout-is-the-only-authority
// contract; extra entries are ordinary NAME=VALUE strings.
func commandOutputEnv(repo string, extraEnv []string, name string, arguments ...string) (string, error) {
command := exec.Command(name, arguments...)
command.Dir = repo
command.Env = append(os.Environ(), extraEnv...)
channels, err := runCommandChannels(command)
if err != nil {
return "", commandFailure(channels, err)
}
return strings.TrimSpace(string(channels.Stdout)), nil
}
3 changes: 3 additions & 0 deletions boatstack/config_documentation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ func TestPublicConfigurationGuideContainsOnlySupportedUserControls(t *testing.T)
"workflow.independent_review_for_high_risk",
"workflow.maintain_changelog",
"workflow.pr_visual_evidence",
"workflow.visual_evidence_publish.expiry",
"workflow.visual_evidence_publish.host",
"workflow.visual_evidence_publish.mode",
"workspace.cleanup",
"workspace.cleanup_after",
"workspace.enabled",
Expand Down
24 changes: 24 additions & 0 deletions boatstack/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,30 @@ func ValidateConfig(config ProjectConfig) error {
if policy := strings.TrimSpace(config.Workflow.PRVisualEvidence); policy != "" && policy != "off" && policy != "suggest" && policy != "require" {
return fmt.Errorf("workflow.pr_visual_evidence must be \"off\", \"suggest\", or \"require\"")
}
if err := validateVisualEvidencePublish(config.Workflow.VisualEvidencePublish); err != nil {
return err
}
return nil
}

// validateVisualEvidencePublish rejects only explicit invalid enum values. A nil
// block or empty fields are legal and resolve to defaults at use, so configs written
// before this block existed remain valid.
func validateVisualEvidencePublish(publish *VisualEvidencePublish) error {
if publish == nil {
return nil
}
if mode := publish.Mode; mode != "" && mode != "external-host" {
return fmt.Errorf("workflow.visual_evidence_publish.mode must be \"external-host\" when set")
}
if host := publish.Host; host != "" && host != "litterbox" && host != "catbox" {
return fmt.Errorf("workflow.visual_evidence_publish.host must be \"litterbox\" or \"catbox\"")
}
switch publish.Expiry {
case "", "1h", "12h", "24h", "72h":
default:
return fmt.Errorf("workflow.visual_evidence_publish.expiry must be one of \"1h\", \"12h\", \"24h\", \"72h\"")
}
return nil
}

Expand Down
8 changes: 8 additions & 0 deletions boatstack/references/config-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ boatstack-config-field:workflow.allow_pass_with_gaps
boatstack-config-field:workflow.maintain_changelog
boatstack-config-field:workflow.boundary_analysis
boatstack-config-field:workflow.pr_visual_evidence
boatstack-config-field:workflow.visual_evidence_publish
boatstack-config-field:workflow.visual_evidence_publish.mode
boatstack-config-field:workflow.visual_evidence_publish.host
boatstack-config-field:workflow.visual_evidence_publish.expiry
boatstack-config-field:workflow.ignored_deliveries
boatstack-config-field:workspace
boatstack-config-field:workspace.enabled
Expand Down Expand Up @@ -66,6 +70,10 @@ This is the exhaustive serialization contract, not a list of recommended user ed
- `maintain_changelog` (boolean, optional): Whether a reader-visible `CHANGELOG.md` entry is required for each delivery slice.
- `boundary_analysis` (boolean, optional): Agent-mediated planning guidance that presents local repair versus programmatic enforcement as a material product decision.
- `pr_visual_evidence` (string, optional): `off`, `suggest`, or `require`. Omission is `off`. Relevant PRs use machine-local PNG evidence without committing media to Git; `suggest` records missing evidence as a visible gap and `require` blocks completed publication.
- `visual_evidence_publish` (object, optional): Agent-mediated publish control for how captured PNG bytes reach the pull-request comment. Omission keeps the default: commit the bytes to a public Boatstack-owned evidence branch and render them inline, but only for a **public** GitHub origin (a private origin falls back to manual attachment). Fields:
- `mode` (string, optional): `external-host` opts the repository — including a **private** one — into uploading the exact PNG bytes to an anonymous expiring host so the comment renders inline anywhere. It is **never auto-selected** because it publishes screenshot bytes to a third party; only this explicit value turns it on. Empty keeps the default public-branch behavior.
- `host` (string, optional): `litterbox` (default) or `catbox`. Only meaningful when `mode` is `external-host`. `litterbox` auto-expires uploads; `catbox` is permanent.
- `expiry` (string, optional): `1h`, `12h`, `24h`, or `72h` (default `72h`). Only meaningful for an expiring host; the PR comment reminds reviewers of the host and this window.
- `ignored_deliveries` (array of strings, optional): Deterministic ambiguity control. Feature slugs of past deliveries to exclude from delivery-ambiguity resolution so historical work no longer blocks new work. New, unlisted ambiguous deliveries still pause the workflow.

### workspace Fields
Expand Down
18 changes: 18 additions & 0 deletions boatstack/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,30 @@ type Workflow struct {
MaintainChangelog bool `json:"maintain_changelog"`
BoundaryAnalysis bool `json:"boundary_analysis,omitempty"`
PRVisualEvidence string `json:"pr_visual_evidence,omitempty"`
// VisualEvidencePublish selects how programmatic visual evidence reaches a PR.
// The nil zero value keeps Boatstack's default: commit the exact PNG bytes to a
// public Boatstack-owned evidence branch and render them inline, but only for a
// PUBLIC GitHub origin (a private origin falls back to manual attachment).
// Setting mode to "external-host" opts a repository — including a private one —
// into uploading the bytes to an anonymous expiring host so the comment renders
// inline anywhere; it is never auto-selected because it publishes screenshot
// bytes to a third party.
VisualEvidencePublish *VisualEvidencePublish `json:"visual_evidence_publish,omitempty"`
// IgnoredDeliveries lists feature slugs of past deliveries to exclude from
// delivery-ambiguity resolution. New, unlisted ambiguous deliveries still
// pause the workflow. Persisted via the LoadConfig -> GeneratedJSON round-trip.
IgnoredDeliveries []string `json:"ignored_deliveries,omitempty"`
}

// VisualEvidencePublish configures the opt-in external-host publish mode. The empty
// zero value of each field resolves to a default at use, so a partially-specified
// block (mode only) still works.
type VisualEvidencePublish struct {
Mode string `json:"mode,omitempty"` // "" (default public-branch) | "external-host"
Host string `json:"host,omitempty"` // external-host only: "litterbox" (default) | "catbox"
Expiry string `json:"expiry,omitempty"` // expiring host only: "1h" | "12h" | "24h" | "72h" (default "72h")
}

type IntegrationState struct {
Requested bool `json:"requested"`
Status string `json:"status,omitempty"`
Expand Down
Loading
Loading