Skip to content

Commit 400818b

Browse files
authored
Make delivery control programmable (#189)
* feat: make delivery control programmable * fix: harden control program boundaries
1 parent f7a5c9d commit 400818b

94 files changed

Lines changed: 19246 additions & 1393 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
docs/architecture/boatstack-v2-*.md text eol=lf
22
docs/architecture/boatstack-v2-*.mmd text eol=lf
33
docs/architecture/boatstack-v2-*.json text eol=lf
4+
docs/architecture/boatstack-standard-flow.mmd text eol=lf

.github/tests/test_repository_contract.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,19 +309,23 @@ def test_catalog_and_generated_artifacts_match_the_executable_registry(self) ->
309309
"docs/architecture/boatstack-v2-*.md text eol=lf",
310310
"docs/architecture/boatstack-v2-*.mmd text eol=lf",
311311
"docs/architecture/boatstack-v2-*.json text eol=lf",
312+
"docs/architecture/boatstack-standard-flow.mmd text eol=lf",
312313
):
313314
self.assertIn(pattern, attributes)
314315

315316
response = json.loads(self.run_helper("catalog").stdout)
316317
transitions = response["catalog"]
317-
self.assertEqual(len(transitions), 61)
318-
self.assertEqual(len({item["id"] for item in transitions}), 61)
318+
self.assertEqual(len(transitions), 62)
319+
self.assertEqual(len({item["id"] for item in transitions}), 62)
319320
self.assertEqual(
320321
{item["class"] for item in transitions},
321322
{"authority", "owned-local", "owned-external", "recovery", "observed-external"},
322323
)
323324
markdown = self.run_helper("catalog", "--format", "markdown").stdout
324325
mermaid = self.run_helper("catalog", "--format", "mermaid").stdout
326+
standard_flow = self.run_helper(
327+
"catalog", "--format", "standard-flow-mermaid"
328+
).stdout
325329
locus_safety = self.run_helper(
326330
"catalog", "--format", "locus-safety"
327331
).stdout
@@ -336,14 +340,19 @@ def test_catalog_and_generated_artifacts_match_the_executable_registry(self) ->
336340
mermaid,
337341
(REPO / "docs" / "architecture" / "boatstack-v2-transition-catalog.mmd").read_text(),
338342
)
343+
self.assertEqual(
344+
standard_flow,
345+
(REPO / "docs" / "architecture" / "boatstack-standard-flow.mmd").read_text(),
346+
)
347+
self.assertEqual(standard_flow.count("<br/>"), 30)
339348
for name, rendered in (
340349
("boatstack-v2-locus-safety.json", locus_safety),
341350
("boatstack-v2-locus-liveness.json", locus_liveness),
342351
):
343352
checked = (REPO / "docs" / "architecture" / name).read_text()
344353
self.assertEqual(rendered, checked)
345354
model = json.loads(checked)
346-
self.assertEqual(len(model["events"]), 61)
355+
self.assertEqual(len(model["events"]), 62)
347356
self.assertEqual(
348357
{event["id"] for event in model["events"]},
349358
{item["id"] for item in transitions},
@@ -413,7 +422,7 @@ def test_offline_installer_initializes_updates_and_guards_through_kernel(self) -
413422
self.run_command(launcher, "doctor", "--repo", repository, env=env).stdout
414423
)
415424
self.assertTrue(doctor["doctor"]["healthy"])
416-
self.assertEqual(doctor["doctor"]["transition_count"], 61)
425+
self.assertEqual(doctor["doctor"]["transition_count"], 62)
417426
self.assertEqual(doctor["snapshot"]["runtime"]["value"], "verified")
418427

419428
goal = (

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,35 @@ concurrency:
1414
cancel-in-progress: true
1515

1616
jobs:
17+
component:
18+
name: component-${{ matrix.name }}
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
include:
23+
- name: kernel-mechanism
24+
packages: ./internal/kernel/model ./internal/kernel/catalog ./internal/kernel/supervisor ./internal/kernel/engine
25+
- name: control-program-compiler
26+
packages: ./control ./core
27+
- name: standard-flow
28+
packages: ./flow/standard ./internal/kernel/protocol
29+
- name: extension-conformance
30+
packages: ./extension/... ./distribution
31+
- name: surface-parity
32+
packages: ./internal/surfaces ./sdk ./cmd/boatstack-helper
33+
- name: plant-integration
34+
packages: ./internal/plant ./internal/effects
35+
runs-on: ubuntu-latest
36+
steps:
37+
- uses: actions/checkout@v7
38+
- uses: actions/setup-go@v7
39+
with:
40+
go-version-file: boatstack/go.mod
41+
cache-dependency-path: boatstack/go.sum
42+
- name: Test ${{ matrix.name }}
43+
working-directory: boatstack
44+
run: go test ${{ matrix.packages }}
45+
1746
# Unix runs the full suite serially (~1-2 min) and is the unsharded correctness
1847
# reference. Windows is sharded in `test-windows` (see below).
1948
test:

README.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Boatstack
22

3-
Boatstack V2 is one repository-delivery controller for humans and coding agents.
4-
It observes a repository, resolves one legal transition, binds exact authority,
5-
executes owned effects transactionally, verifies the result, and records a
6-
receipt.
3+
Boatstack is a programmable supervisory control runtime for software delivery,
4+
with a first-party standard delivery flow. It compiles one CoreSystem, one
5+
explicit primary flow, and optional conservative extensions into an immutable
6+
ControlProgram before it observes a repository, resolves one legal transition,
7+
binds exact authority, executes owned effects, verifies the result, and records
8+
a receipt.
79

810
Cursor, Codex, Claude Code, Gemini CLI, MCP, the CLI, and the Go SDK use the same
911
versioned protocol. They do not keep separate workflow state machines.
@@ -70,7 +72,7 @@ boatstack apply --repo . --transition <stable-id> --format json
7072
```
7173

7274
- `status`, `next`, `doctor`, `catalog`, and `events` are read-only.
73-
- `apply` and `recover` request stable transition IDs from the 61-event
75+
- `apply` and `recover` request stable transition IDs from the 62-event
7476
executable catalog.
7577
- Friendly aliases such as `plan-create`, `plan-approve`,
7678
`workspace-cut`, `record-test`, and `publish-pr` map to those IDs.
@@ -85,10 +87,17 @@ boatstack apply --repo . --transition <stable-id> --format json
8587

8688
The generated [transition catalog](docs/architecture/boatstack-v2-transition-catalog.md)
8789
and [Mermaid inventory](docs/architecture/boatstack-v2-transition-catalog.mmd)
88-
come directly from the runtime registry.
90+
come directly from the runtime registry. The generated
91+
[StandardFlow graph](docs/architecture/boatstack-standard-flow.mmd) filters the
92+
same compiled registry by primary-flow origin; it is not a second graph.
8993
The [replacement closure report](docs/architecture/boatstack-v2-closure-report.md)
9094
records the deleted V1 authority and its V2 evidence.
9195

96+
The Go SDK keeps the standard distribution ergonomic with `sdk.New(...)`.
97+
Custom applications use `sdk.NewKernel(..., sdk.WithFlow(flow),
98+
sdk.WithExtension(extension))`; the lower-level constructor requires an
99+
explicit trusted in-process primary flow and never inserts StandardFlow.
100+
92101
## Coding-agent skills
93102

94103
Boatstack exposes exactly three operation skills on every supported interactive

boatstack/cmd/boatstack-helper/main.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818

1919
boatstack "github.com/operatorstack/boatstack/boatstack"
2020
"github.com/operatorstack/boatstack/boatstack/analysis"
21+
"github.com/operatorstack/boatstack/boatstack/distribution"
2122
"github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
2223
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
2324
"github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
@@ -84,7 +85,7 @@ func run(arguments []string) error {
8485
if err != nil {
8586
return err
8687
}
87-
kernel, err := boatstack.NewV2Kernel("")
88+
kernel, err := standardKernel(context.Background(), request)
8889
if err != nil {
8990
return err
9091
}
@@ -116,7 +117,7 @@ func runRPC() error {
116117
if err := decoder.Decode(&trailing); err != io.EOF {
117118
return fmt.Errorf("V2 RPC request contains trailing JSON")
118119
}
119-
kernel, err := boatstack.NewV2Kernel("")
120+
kernel, err := standardKernel(context.Background(), request)
120121
if err != nil {
121122
return err
122123
}
@@ -220,7 +221,7 @@ func parseOptions(command string, arguments []string, transition catalog.Transit
220221
flags.Var(&options.parameters, "param", "transition parameter name=value (repeatable)")
221222
flags.Var(&options.authorityReceipts, "authority-receipt", "authority receipt JSON path (repeatable)")
222223
flags.BoolVar(&options.follow, "follow", false, "follow passive process events (events with jsonl only)")
223-
flags.StringVar(&options.host, "host", options.host, "cli, cursor, codex, claude, gemini, or mcp")
224+
flags.StringVar(&options.host, "host", options.host, "cli, sdk, cursor, codex, claude, gemini, or mcp")
224225
flags.StringVar(&options.command, "command", "", "raw command to classify at the guard boundary")
225226
if err := flags.Parse(arguments); err != nil {
226227
return commandOptions{}, err
@@ -245,7 +246,22 @@ func parseOptions(command string, arguments []string, transition catalog.Transit
245246
return options, nil
246247
}
247248

248-
func followEvents(kernel boatstack.V2Kernel, request surfaces.Request) error {
249+
func standardKernel(ctx context.Context, request surfaces.Request) (boatstack.Kernel, error) {
250+
programRequest := distribution.RepositoryProgramRequest{
251+
Repository: request.Repository, Host: request.Host, CorrelationID: request.CorrelationID,
252+
}
253+
if request.TransitionID == "installation.initialize" || request.TransitionID == "configuration.initialize" {
254+
programRequest.ConfigurationPath, _ = request.Parameters.Get("config_path")
255+
programRequest.ConfigurationFingerprint, _ = request.Parameters.Get("config_sha256")
256+
}
257+
program, err := distribution.StandardProgramForRepository(ctx, programRequest)
258+
if err != nil {
259+
return boatstack.Kernel{}, err
260+
}
261+
return boatstack.NewKernel("", program)
262+
}
263+
264+
func followEvents(kernel boatstack.Kernel, request surfaces.Request) error {
249265
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
250266
defer stop()
251267
encoder := json.NewEncoder(os.Stdout)
@@ -480,6 +496,9 @@ func renderResponse(response surfaces.Response, format string) error {
480496
case "mermaid":
481497
fmt.Print(surfaces.RenderCatalogMermaid(response.Catalog))
482498
return nil
499+
case "standard-flow-mermaid":
500+
fmt.Print(surfaces.RenderStandardFlowMermaid(response.Catalog))
501+
return nil
483502
case "locus-safety":
484503
value, err := surfaces.RenderCatalogLocusSafety(response.Catalog)
485504
if err != nil {
@@ -518,7 +537,11 @@ func renderResponse(response surfaces.Response, format string) error {
518537
return nil
519538
}
520539
if response.Doctor != nil {
521-
fmt.Printf("healthy=%t transitions=%d snapshot=%s\n%s\n", response.Doctor.Healthy, response.Doctor.TransitionCount, response.Doctor.Snapshot, response.Doctor.Detail)
540+
fmt.Printf("healthy=%t kernel=%s core=%s@%s flow=%s@%s core_transitions=%d flow_transitions=%d extension_transitions=%d transitions=%d program=%s drift=%t snapshot=%s\n%s\n",
541+
response.Doctor.Healthy, response.Doctor.KernelVersion, response.Doctor.CoreSystemID, response.Doctor.CoreSystemVersion,
542+
response.Doctor.PrimaryFlowID, response.Doctor.PrimaryFlowVersion, response.Doctor.CoreTransitionCount,
543+
response.Doctor.FlowTransitionCount, response.Doctor.ExtensionTransitionCount, response.Doctor.TransitionCount,
544+
response.Doctor.ProgramFingerprint, response.Doctor.UnresolvedProgramDrift, response.Doctor.Snapshot, response.Doctor.Detail)
522545
return nil
523546
}
524547
if response.Decision != nil {

boatstack/cmd/boatstack-helper/main_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import (
66
"testing"
77

88
boatstack "github.com/operatorstack/boatstack/boatstack"
9-
"github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
109
"github.com/operatorstack/boatstack/boatstack/internal/surfaces"
10+
"github.com/operatorstack/boatstack/boatstack/internal/testprogram"
1111
)
1212

1313
func TestEveryFriendlyMutationAliasMapsToOneRegistryTransition(t *testing.T) {
1414
// control-law: cli-verbs-are-adapters-not-transition-authority
15-
registry := catalog.Default()
15+
registry := testprogram.StandardRegistry()
1616
commands := []string{"init", "update", "attach", "detach", "hydrate-runtime", "configure", "goal-configure", "plan-create", "plan-validate", "plan-approve", "plan-activate", "plan-amend", "workspace-cut", "workspace-sync", "workspace-cleanup", "workspace-reap", "record-build", "record-test", "record-review", "record-change", "record-journey", "publication-preview", "publish-pr", "observe-pr", "correct-pr", "abandon"}
1717
for _, command := range commands {
1818
operation, transitionID, _, err := classifyCommand(command)

0 commit comments

Comments
 (0)