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/49a684f6752d1f017281d7bbb8b17b856904df15/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/fac1f242bb58f88e94da3f7cbb50ba6159d02790/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
20 changes: 15 additions & 5 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"files": {
".gitignore": "a7079e923a776f14f1bb3a6aa0a11a133a8e1dfb35af020f327623357b7e3957",
"CONTRIBUTING.md": "3fea013e59482851b40d6c70734457600b421a021ef679c630bbee0cda19c17d",
"CONTRIBUTING.md": "762db520d51872c6b6b6ec7c312f54c7e1ef5c14fdf61ed17673d65a525a7e01",
"README.md": "6b7402c5cef5b3b9b739281d3d4d576cdc995796ff127fc6aefb97c5743e0bac",
"assets/boatstack-journey.svg": "e465befc50c8ce30f3e07e8fd97012931beeb053392c8fbf38ad645023b3cc63",
"assets/boatstack-mark.svg": "be1f984da1bfa69fa5d1f986d8343d21f7e20921b71db888c928b4d2e54b09b5",
Expand Down Expand Up @@ -56,6 +56,8 @@
"boatstack/evidence.go": "497a31e6ff632cb1d7c3adfc9f269af3f6aa84e948dd5d417c162767542a27df",
"boatstack/export.go": "9cb23234e6cd79441ff6f39f88ed66d6d47ef7c27901404439a3572b03fdf881",
"boatstack/export_test.go": "dce5aa3ab5499c82d05859cf86b46dfcee308482491366d83e10ca3fb8605bb6",
"boatstack/flow_trace.go": "95b5a99f5f557a27a3eca7152de9ec18459f2a88d9749924432463031536a96c",
"boatstack/flow_trace_test.go": "99f89a831e904f6a8ef710b6977ed3a808ce1c7ddfaba457b292d84f2ddca51b",
"boatstack/go.mod": "6086ef1b2a83f5696190dca692c653925f27b61f652f659fd3fca43ed54a1641",
"boatstack/go.sum": "26c315c867b11b886f3c9402fce7f341f6a9115a5d61f54afbb5e1b1fb5f6017",
"boatstack/hooks.go": "0639eff2ec5ce50dcbe77ace0f7c25de1e9a68784a6ed70d6acc9984d049ef1a",
Expand All @@ -69,9 +71,16 @@
"boatstack/installation_repair_test.go": "ae5a5ea1110836bd78cf20ade863a4d32cfd63d282559f92786f57b31869bd14",
"boatstack/integrations.go": "75b39ce2e662fccd66bf4b9bff0e097a4db558f23b3aa1d9bc83a5fc6373444c",
"boatstack/internal/deliverycontrol/cost.go": "a0a22292b8ed55cbfce9808599449d5128ae5b67ef6adc4881e604db0897f3f0",
"boatstack/internal/deliverycontrol/graph.go": "13367b068d0004e0f2e857e7b6e9d19e758ef345070b3fbc008644be27438902",
"boatstack/internal/deliverycontrol/oracle.go": "80765b1946d6c863f0e635a99b68d3ccafa7ff235360fba774811b5b0de791da",
"boatstack/internal/deliverycontrol/oracle_test.go": "ce320a71f0c9440c5a7bc1b742d0f74c6a46759845e919ab36bde5ff8fabb311",
"boatstack/internal/deliverycontrol/registry.go": "aa89cef9eec8d715c06d2f61a472df20bb9334c950a6d751a1e15d67b567c433",
"boatstack/internal/deliverycontrol/registry_test.go": "473ab5e5d33f84d34c29a219db867abfc6eb3ad4489f3d5d0c7dc09b06d193f3",
"boatstack/internal/deliverycontrol/state.go": "2551624bbcbd8f9dd897a1e2240cef2cc1895d117a4030525d88f1d62f6e395e",
"boatstack/internal/deliverycontrol/trajectory.go": "e25eeb092fb2255bd82477141d5b094627c1d779bef3616b89752046ae08e7c3",
"boatstack/internal/deliverycontrol/trajectory_test.go": "df5be9a8f55b09a94b0f6b94d2847180d39d357619eb4d1d11181015935c2f96",
"boatstack/internal/deliverycontrol/trajectorylog.go": "a1da7e7252b33f63f232c101de683b4a515e80243801caf69fda53d24233e42f",
"boatstack/internal/deliverycontrol/trajectorylog_test.go": "227dd6ed9ce181d517a37b67ef4d64dd93779a533eae804798ab54de35c7f13e",
"boatstack/internal/deliverycontrol/transition.go": "b43abb0e99d29697b27b0bb8ee2e2f5f31f3471a2983f25d18ae3564ee246775",
"boatstack/migrate.go": "eaf589e2b266238068e42c6d78e01dc040266d28e342cb24f09e33e8541749b3",
"boatstack/migrate_test.go": "9f4bda2fb158c5e54bcc0242dace1da3c1965f9846a213c573956a35b7d1724e",
Expand Down Expand Up @@ -142,10 +151,10 @@
"docs/benchmark-corpus-audit.md": "f2d206fe8579a514f9da82b2c96c19b343ac004be67617e1bd34f0f8e0e5e6c6",
"docs/benchmark-submission-audit.md": "9518abdd17690729c6423f87cab20418ed47b0915b5faa44b9ef975e9e9c3b79",
"docs/configuration.md": "df054f49d532c8b1b7d94184810d1b3b5bf18cdc30eb985b4b6d0639162e341a",
"docs/evidence-engineered-coding.md": "b8d35dc38d3e9385972fdd81595eb5245367b4e7e55a89363b5f6b53cce73977",
"docs/evidence-engineered-coding.md": "bc8fcd561d1bc57f0cdbe79c537143e005b00995cbc8fe690982d00efdaf64d5",
"docs/generated-files.md": "437791765b0a4015032ae21d1a6618563cad92b7402819e4f963bf5ae16284a3",
"docs/getting-started.md": "1dd4f4e2e636cc5adfc2f79939629701e171087c3d5e558cf919548b9224adfd",
"docs/public-claims.json": "57e7d04dd7141b62e89ff111ced83674b6e9ad9094fa71a96da3dcb723ffc8f5",
"docs/public-claims.json": "476511c6c2b2c4159f507b4e9c8f2c726b8a08a004db51a0967e73e92439050a",
"docs/public-surface.md": "713f7a050b5f339cf948299103ef3800417dccfecf2cc1a4166397ea6f978907",
"docs/research-and-design.md": "8d78678108f0a6c924e1ff9b32c0f81aae9d1f779e0082843b6f99ad993ae2b6",
"docs/safety.md": "7b9b5c515d36e683767ec8d3d9d6d119ac93650b2f629d351deadd4c600ed6a6",
Expand All @@ -159,7 +168,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": "1ddebf9603a1058b81ea2f02a7314bbba1a46a82a32d59bf3f3721cf74a85228",
"labs/diagram-json/plan.lock.json": "2a120ef6977a3e028e66c534244d9cfcf5a347cde3738ad176038b6c57a1c370",
"labs/diagram-json/plan.md": "3cc4f533b8d69386deff16b3a594a3ba09d4c0c3db636cccd8c4380084ce6a51",
"labs/diagram-json/questions.md": "74733b015002c8a6777c558e7e997fa48c94850b9bd39054fe9366c97ecf728d",
"labs/diagram-json/request.md": "0808fc41c36779c404f4a3a121167da6e76cac56df526e70f9ed6d3e0d4c02ed",
Expand Down Expand Up @@ -243,6 +252,7 @@
"release-notes/2026-07-25-boatstack-banner.md": "28e83f294de606211cfdc91b2586aa834e004dee76d5c4bee08859986ae86b5b",
"release-notes/2026-07-25-delivery-control-inventory.md": "1f359bcf4071dd47bd1011c877db573bd26309d28683abea8389c353f1c6c88d",
"release-notes/2026-07-25-delivery-flow-navigation-model.md": "b2d805fae30100a7de4e76760341247237cc2476fdcc57d99074825bf47d6450",
"release-notes/2026-07-25-deliverycontrol-flow-oracle.md": "04e64e27638b32a90a132f615f896bf20ee805f3c75bf5e3ab088f3c55e641a7",
"release-notes/2026-07-25-deliverycontrol-shadow-registry.md": "e7f8ca4e4f188eda3088e46cba77369d8ff0d903f29e43846103d986e79a2273",
"release-notes/2026-07-25-evidence-path-resolution.md": "b32cb8a6e69f397f751c3a7fb62be254a7407a28bed25ae9108d6d773c863d11",
"release-notes/2026-07-25-published-slice-correction-routing.md": "129cdd62c80c8b93060726027d68ba3abdb0bca1a1ce9e64d6053271af3fd082",
Expand All @@ -251,7 +261,7 @@
"generator": "operatorstack/intelligence-flow:boatstack-distribution",
"schema_version": 1,
"source": {
"commit": "49a684f6752d1f017281d7bbb8b17b856904df15",
"commit": "fac1f242bb58f88e94da3f7cbb50ba6159d02790",
"path": "labs/12-product-engineering-loop",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
64 changes: 64 additions & 0 deletions boatstack/flow_trace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package boatstack

import (
"os"
"path/filepath"

"github.com/operatorstack/boatstack/boatstack/internal/deliverycontrol"
)

// flowTraceKillSwitch disables shadow trajectory recording when set to "0".
// Recording is best-effort and off the critical path either way; the switch
// exists so an operator can silence it entirely without a rebuild.
const flowTraceKillSwitch = "BOATSTACK_FLOW_TRACE"

// flowLogDirectory is the append-only trajectory log location,
// <gitdir>/boatstack/flow, a sibling of the delivery-state directory. It reuses
// deliveryStateDirectory's Git-dir resolution so the two stay in lockstep.
func flowLogDirectory(repo string) (string, error) {
deliveries, err := deliveryStateDirectory(repo)
if err != nil {
return "", err
}
return filepath.Join(filepath.Dir(deliveries), "flow"), nil
}

// RecordFlowTransition appends a best-effort shadow record of one delivery-flow
// control attempt. It is deliberately inert with respect to command behavior:
// it never returns an error, never panics into a caller, and writes nothing when
// disabled or when anything goes wrong. Nothing consumes the log at runtime yet
// — this is the measurement substrate for the flow-navigation meter, not a
// control point.
//
// transition is the registry TransitionID being attempted; from is the
// delivery-flow state the attempt started in ("" if unknown); ok reports whether
// the underlying handler succeeded (a false ok on a mutation is billed as
// friction, per the cmg model).
func RecordFlowTransition(repo string, transition deliverycontrol.TransitionID, from deliverycontrol.StateID, ok bool) {
// A trace must never take down a command. Swallow any panic from the
// best-effort path.
defer func() { _ = recover() }()

if os.Getenv(flowTraceKillSwitch) == "0" {
return
}
descriptor, found := deliverycontrol.Transition(transition)
if !found {
return
}

outcome := deliverycontrol.OutcomeAllowed
if !ok {
outcome = deliverycontrol.OutcomeDenied
}
directory, err := flowLogDirectory(repo)
if err != nil {
return
}
_ = deliverycontrol.AppendAttempt(directory, deliverycontrol.TransitionAttempt{
From: from,
Transition: transition,
Outcome: outcome,
CostClass: deliverycontrol.ChargedCostClass(descriptor.Kind, descriptor.CostClass, outcome),
})
}
67 changes: 67 additions & 0 deletions boatstack/flow_trace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package boatstack

import (
"testing"

"github.com/operatorstack/boatstack/boatstack/internal/deliverycontrol"
)

func readFlowTrajectory(t *testing.T, repo string) deliverycontrol.Trajectory {
t.Helper()
dir, err := flowLogDirectory(repo)
if err != nil {
t.Fatalf("flow log directory: %v", err)
}
traj, err := deliverycontrol.ReadTrajectory(dir)
if err != nil {
t.Fatalf("read trajectory: %v", err)
}
return traj
}

// An allowed mutation and a denied one are both recorded, and the denial is
// billed as friction — the recorder is a faithful, cheap witness to real
// command outcomes.
func TestRecordFlowTransitionCapturesOutcomes(t *testing.T) {
repo := prTestRepo(t)

RecordFlowTransition(repo, "delivery.record_gate_test", deliverycontrol.StateBuild, true)
RecordFlowTransition(repo, "delivery.undo", deliverycontrol.StateBuild, false)

traj := readFlowTrajectory(t, repo)
if len(traj) != 2 {
t.Fatalf("recorded %d attempts, want 2", len(traj))
}
if traj[0].Outcome != deliverycontrol.OutcomeAllowed || traj[0].CostClass != deliverycontrol.CostMutation {
t.Errorf("allowed gate: got %+v", traj[0])
}
if traj[1].Outcome != deliverycontrol.OutcomeDenied || traj[1].CostClass != deliverycontrol.CostFriction {
t.Errorf("denied undo should be friction: got %+v", traj[1])
}
if cost := traj.WalkCost(deliverycontrol.DefaultFlowCostWeights()); cost != 4 {
t.Errorf("walk cost = %d, want 4 (1 move + 3 friction)", cost)
}
}

// The recorder is best-effort: an unresolvable repo, an unknown transition, and
// the kill switch each leave command behavior untouched and write nothing —
// without erroring or panicking.
func TestRecordFlowTransitionIsBestEffort(t *testing.T) {
// A non-git directory cannot resolve a flow-log location; must be a silent no-op.
RecordFlowTransition(t.TempDir(), "delivery.record_gate_test", deliverycontrol.StateBuild, true)

repo := prTestRepo(t)

// An unknown transition is ignored.
RecordFlowTransition(repo, "delivery.does_not_exist", deliverycontrol.StateBuild, true)
if traj := readFlowTrajectory(t, repo); len(traj) != 0 {
t.Errorf("unknown transition should record nothing; got %d", len(traj))
}

// The kill switch disables recording entirely.
t.Setenv(flowTraceKillSwitch, "0")
RecordFlowTransition(repo, "delivery.record_gate_test", deliverycontrol.StateBuild, true)
if traj := readFlowTrajectory(t, repo); len(traj) != 0 {
t.Errorf("kill switch should suppress recording; got %d", len(traj))
}
}
96 changes: 96 additions & 0 deletions boatstack/internal/deliverycontrol/graph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package deliverycontrol

import "sort"

// Edge is a directed, costed control between two delivery-flow states: a single
// move an agent can make, priced by its cost class.
type Edge struct {
Transition TransitionID
From StateID
To StateID
CostClass TransitionCostClass
Cost int
}

// Graph is a weighted directed graph of delivery-flow controls. It is the
// optimization projection of the single registry declaration: the same
// transitions the conformance projection audits, arranged as a graph a
// deterministic oracle can score. Only state-changing transitions (To != "")
// become edges; read-only observes do not advance state and so are not moves on
// this graph.
type Graph struct {
weights FlowCostWeights
out map[StateID][]Edge
nodes map[StateID]bool
}

// NewGraph returns an empty graph priced by the given weights.
func NewGraph(weights FlowCostWeights) *Graph {
return &Graph{
weights: weights,
out: map[StateID][]Edge{},
nodes: map[StateID]bool{},
}
}

// AddEdge adds a directed edge priced by the transition's cost class. An edge
// whose cost class has no defined weight is skipped (the well-formedness
// conformance test forbids that in the registry, so this only guards ad-hoc
// graphs). Endpoints are registered as nodes even when the class is unknown, so
// a state that only appears on a skipped edge is still a known node.
func (g *Graph) AddEdge(from, to StateID, transition TransitionID, class TransitionCostClass) {
g.nodes[from] = true
g.nodes[to] = true
cost, ok := g.weights.Cost(class)
if !ok {
return
}
g.out[from] = append(g.out[from], Edge{
Transition: transition,
From: from,
To: to,
CostClass: class,
Cost: cost,
})
}

// Out returns the out-edges of a state in insertion order (deterministic).
func (g *Graph) Out(state StateID) []Edge {
return g.out[state]
}

// Has reports whether a state is a known node (appears as an edge endpoint).
// The oracle uses this to return Unresolved for a state it has never seen rather
// than fabricate a path from nowhere.
func (g *Graph) Has(state StateID) bool {
return g.nodes[state]
}

// Nodes returns every known state, sorted for deterministic iteration.
func (g *Graph) Nodes() []StateID {
out := make([]StateID, 0, len(g.nodes))
for n := range g.nodes {
out = append(out, n)
}
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out
}

// RegistryGraph projects the registry into a costed graph: one edge per
// (From, To) pair of every transition that changes delivery state. A transition
// with an empty To (a pure observation) advances nothing and contributes no
// edge; a transition with an empty From (delivery.next, resolved from any state)
// likewise contributes no edge because it changes no state. Iteration order
// follows the registry declaration, so the projection is deterministic.
func RegistryGraph(weights FlowCostWeights) *Graph {
g := NewGraph(weights)
for _, tr := range Transitions() {
if tr.To == "" {
continue
}
for _, from := range tr.From {
g.AddEdge(from, tr.To, tr.ID, tr.CostClass)
}
}
return g
}
Loading
Loading