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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
docs/architecture/boatstack-v2-*.md text eol=lf
docs/architecture/boatstack-v2-*.mmd text eol=lf
docs/architecture/boatstack-v2-*.json text eol=lf
docs/architecture/boatstack-standard-flow.mmd text eol=lf
17 changes: 13 additions & 4 deletions .github/tests/test_repository_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,19 +309,23 @@ def test_catalog_and_generated_artifacts_match_the_executable_registry(self) ->
"docs/architecture/boatstack-v2-*.md text eol=lf",
"docs/architecture/boatstack-v2-*.mmd text eol=lf",
"docs/architecture/boatstack-v2-*.json text eol=lf",
"docs/architecture/boatstack-standard-flow.mmd text eol=lf",
):
self.assertIn(pattern, attributes)

response = json.loads(self.run_helper("catalog").stdout)
transitions = response["catalog"]
self.assertEqual(len(transitions), 61)
self.assertEqual(len({item["id"] for item in transitions}), 61)
self.assertEqual(len(transitions), 62)
self.assertEqual(len({item["id"] for item in transitions}), 62)
self.assertEqual(
{item["class"] for item in transitions},
{"authority", "owned-local", "owned-external", "recovery", "observed-external"},
)
markdown = self.run_helper("catalog", "--format", "markdown").stdout
mermaid = self.run_helper("catalog", "--format", "mermaid").stdout
standard_flow = self.run_helper(
"catalog", "--format", "standard-flow-mermaid"
).stdout
locus_safety = self.run_helper(
"catalog", "--format", "locus-safety"
).stdout
Expand All @@ -336,14 +340,19 @@ def test_catalog_and_generated_artifacts_match_the_executable_registry(self) ->
mermaid,
(REPO / "docs" / "architecture" / "boatstack-v2-transition-catalog.mmd").read_text(),
)
self.assertEqual(
standard_flow,
(REPO / "docs" / "architecture" / "boatstack-standard-flow.mmd").read_text(),
)
self.assertEqual(standard_flow.count("<br/>"), 30)
for name, rendered in (
("boatstack-v2-locus-safety.json", locus_safety),
("boatstack-v2-locus-liveness.json", locus_liveness),
):
checked = (REPO / "docs" / "architecture" / name).read_text()
self.assertEqual(rendered, checked)
model = json.loads(checked)
self.assertEqual(len(model["events"]), 61)
self.assertEqual(len(model["events"]), 62)
self.assertEqual(
{event["id"] for event in model["events"]},
{item["id"] for item in transitions},
Expand Down Expand Up @@ -413,7 +422,7 @@ def test_offline_installer_initializes_updates_and_guards_through_kernel(self) -
self.run_command(launcher, "doctor", "--repo", repository, env=env).stdout
)
self.assertTrue(doctor["doctor"]["healthy"])
self.assertEqual(doctor["doctor"]["transition_count"], 61)
self.assertEqual(doctor["doctor"]["transition_count"], 62)
self.assertEqual(doctor["snapshot"]["runtime"]["value"], "verified")

goal = (
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,35 @@ concurrency:
cancel-in-progress: true

jobs:
component:
name: component-${{ matrix.name }}
strategy:
fail-fast: false
matrix:
include:
- name: kernel-mechanism
packages: ./internal/kernel/model ./internal/kernel/catalog ./internal/kernel/supervisor ./internal/kernel/engine
- name: control-program-compiler
packages: ./control ./core
- name: standard-flow
packages: ./flow/standard ./internal/kernel/protocol
- name: extension-conformance
packages: ./extension/... ./distribution
- name: surface-parity
packages: ./internal/surfaces ./sdk ./cmd/boatstack-helper
- name: plant-integration
packages: ./internal/plant ./internal/effects
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: boatstack/go.mod
cache-dependency-path: boatstack/go.sum
- name: Test ${{ matrix.name }}
working-directory: boatstack
run: go test ${{ matrix.packages }}

# Unix runs the full suite serially (~1-2 min) and is the unsharded correctness
# reference. Windows is sharded in `test-windows` (see below).
test:
Expand Down
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Boatstack

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

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

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

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

The Go SDK keeps the standard distribution ergonomic with `sdk.New(...)`.
Custom applications use `sdk.NewKernel(..., sdk.WithFlow(flow),
sdk.WithExtension(extension))`; the lower-level constructor requires an
explicit trusted in-process primary flow and never inserts StandardFlow.

## Coding-agent skills

Boatstack exposes exactly three operation skills on every supported interactive
Expand Down
33 changes: 28 additions & 5 deletions boatstack/cmd/boatstack-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

boatstack "github.com/operatorstack/boatstack/boatstack"
"github.com/operatorstack/boatstack/boatstack/analysis"
"github.com/operatorstack/boatstack/boatstack/distribution"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/model"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/protocol"
Expand Down Expand Up @@ -84,7 +85,7 @@ func run(arguments []string) error {
if err != nil {
return err
}
kernel, err := boatstack.NewV2Kernel("")
kernel, err := standardKernel(context.Background(), request)
if err != nil {
return err
}
Expand Down Expand Up @@ -116,7 +117,7 @@ func runRPC() error {
if err := decoder.Decode(&trailing); err != io.EOF {
return fmt.Errorf("V2 RPC request contains trailing JSON")
}
kernel, err := boatstack.NewV2Kernel("")
kernel, err := standardKernel(context.Background(), request)
if err != nil {
return err
}
Expand Down Expand Up @@ -220,7 +221,7 @@ func parseOptions(command string, arguments []string, transition catalog.Transit
flags.Var(&options.parameters, "param", "transition parameter name=value (repeatable)")
flags.Var(&options.authorityReceipts, "authority-receipt", "authority receipt JSON path (repeatable)")
flags.BoolVar(&options.follow, "follow", false, "follow passive process events (events with jsonl only)")
flags.StringVar(&options.host, "host", options.host, "cli, cursor, codex, claude, gemini, or mcp")
flags.StringVar(&options.host, "host", options.host, "cli, sdk, cursor, codex, claude, gemini, or mcp")
flags.StringVar(&options.command, "command", "", "raw command to classify at the guard boundary")
if err := flags.Parse(arguments); err != nil {
return commandOptions{}, err
Expand All @@ -245,7 +246,22 @@ func parseOptions(command string, arguments []string, transition catalog.Transit
return options, nil
}

func followEvents(kernel boatstack.V2Kernel, request surfaces.Request) error {
func standardKernel(ctx context.Context, request surfaces.Request) (boatstack.Kernel, error) {
programRequest := distribution.RepositoryProgramRequest{
Repository: request.Repository, Host: request.Host, CorrelationID: request.CorrelationID,
}
if request.TransitionID == "installation.initialize" || request.TransitionID == "configuration.initialize" {
programRequest.ConfigurationPath, _ = request.Parameters.Get("config_path")
programRequest.ConfigurationFingerprint, _ = request.Parameters.Get("config_sha256")
}
program, err := distribution.StandardProgramForRepository(ctx, programRequest)
if err != nil {
return boatstack.Kernel{}, err
}
return boatstack.NewKernel("", program)
}

func followEvents(kernel boatstack.Kernel, request surfaces.Request) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
encoder := json.NewEncoder(os.Stdout)
Expand Down Expand Up @@ -480,6 +496,9 @@ func renderResponse(response surfaces.Response, format string) error {
case "mermaid":
fmt.Print(surfaces.RenderCatalogMermaid(response.Catalog))
return nil
case "standard-flow-mermaid":
fmt.Print(surfaces.RenderStandardFlowMermaid(response.Catalog))
return nil
case "locus-safety":
value, err := surfaces.RenderCatalogLocusSafety(response.Catalog)
if err != nil {
Expand Down Expand Up @@ -518,7 +537,11 @@ func renderResponse(response surfaces.Response, format string) error {
return nil
}
if response.Doctor != nil {
fmt.Printf("healthy=%t transitions=%d snapshot=%s\n%s\n", response.Doctor.Healthy, response.Doctor.TransitionCount, response.Doctor.Snapshot, response.Doctor.Detail)
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",
response.Doctor.Healthy, response.Doctor.KernelVersion, response.Doctor.CoreSystemID, response.Doctor.CoreSystemVersion,
response.Doctor.PrimaryFlowID, response.Doctor.PrimaryFlowVersion, response.Doctor.CoreTransitionCount,
response.Doctor.FlowTransitionCount, response.Doctor.ExtensionTransitionCount, response.Doctor.TransitionCount,
response.Doctor.ProgramFingerprint, response.Doctor.UnresolvedProgramDrift, response.Doctor.Snapshot, response.Doctor.Detail)
return nil
}
if response.Decision != nil {
Expand Down
4 changes: 2 additions & 2 deletions boatstack/cmd/boatstack-helper/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import (
"testing"

boatstack "github.com/operatorstack/boatstack/boatstack"
"github.com/operatorstack/boatstack/boatstack/internal/kernel/catalog"
"github.com/operatorstack/boatstack/boatstack/internal/surfaces"
"github.com/operatorstack/boatstack/boatstack/internal/testprogram"
)

func TestEveryFriendlyMutationAliasMapsToOneRegistryTransition(t *testing.T) {
// control-law: cli-verbs-are-adapters-not-transition-authority
registry := catalog.Default()
registry := testprogram.StandardRegistry()
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"}
for _, command := range commands {
operation, transitionID, _, err := classifyCommand(command)
Expand Down
Loading