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: 0 additions & 1 deletion IMPORT_PROVENANCE.json
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,6 @@
"docs/safety.md": "1a2b84e0a4b9aa6322d35d6677ff52662c306089031295692b569233cdd94d8f",
"docs/troubleshooting.md": "252937c97d40197e9122e20f421f90616aecdd5ddf50cee8bdd348bc6c5caf32",
"docs/validation-and-evidence.md": "e7d91ad49c6adb44784ebe7d94feceb6abd445857f9a0716f0758bf6b55296c5",
"docs/why-these-steps.md": "cbe0d769db11ef15bb1dff888009378d6783776ad020e6f5139847a1dd62fa09",
"install.ps1": "f48d0f26a26e806b780d10fa916c261ff9f84ab39758cf9e229f647836845e86",
"install.sh": "2575f82568b76b14e72fb88de4e8af677da1e77b9cf1a085b4ddfbbd766e67f9",
"labs/diagram-json/README.md": "f56a120877c8a3b10daa49c6d951481c02e98b8b8bb3f28672e9e092d97a37bb",
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ Boatstack is a repository-local delivery harness.
- <!-- boatstack-claim:cross-model-failures -->**Observed:** benchmark runs exposed failures in protocol handling, context, verification, and recovery — not only model capability.
- <!-- boatstack-claim:lower-cost-outcomes -->**Being evaluated:** whether this improves product quality, cost, or delivery time with lower-cost models.

This does not mean every model performs equally. [See the evidence and paired evaluation design](docs/why-these-steps.md#model-choice-and-budget).
This does not mean every model performs equally. [See the evidence and evaluation design](docs/research-and-design.md#evaluation-of-the-finished-node).

## Built from failures observed in real coding work

Expand All @@ -165,9 +165,9 @@ These behaviors come from coding failures observed in benchmark and product work
| <!-- boatstack-claim:irreversible-operations -->A failed write led to an invented reset path | Denies high-confidence destructive recovery | Hook behavior verified; outcome benefit still being evaluated |
| <!-- boatstack-claim:reviewer-ready-pr -->A PR lost decisions and accepted gaps | Builds a review brief from scope, diff, and evidence | Projection and stale-preview tests |
| <!-- boatstack-claim:phase-scoped-delivery -->A phased plan opened PRs during build | Gates and publishes one delivery slice at a time | Slice-state and bypass tests |
| <!-- boatstack-claim:git-worktree-activation -->A worktree inherited no ignored helper | Uses its tracked pinned launcher to verify and activate the exact runtime before command dispatch | Linked-worktree, identity, and tamper tests |
| <!-- boatstack-claim:git-worktree-activation -->A feature worktree lost its helper or stranded its validated plan | Verifies the pinned runtime and moves the exact planning package before approval or autonomy | Linked-worktree, identity, rollback, and plan-fingerprint tests |

[Read what happened, what is tested, and what remains open](docs/why-these-steps.md). The [claim record](docs/public-claims.json) keeps every material statement tied to its sources.
The [claim record](docs/public-claims.json) keeps every material statement tied to its sources and tests.

## A small example

Expand Down Expand Up @@ -204,7 +204,7 @@ The installer previews generated paths, verifies the platform helper, offers opt

**Start:** [Getting started](docs/getting-started.md) · [Files](docs/generated-files.md) · [Troubleshooting](docs/troubleshooting.md)

**Inspect:** [Why these steps](docs/why-these-steps.md) · [Validation and evidence](docs/validation-and-evidence.md) · [Safety](docs/safety.md)
**Inspect:** [Research and design](docs/research-and-design.md) · [Validation and evidence](docs/validation-and-evidence.md) · [Safety](docs/safety.md)

**Go deeper:** [Coding](docs/evidence-engineered-coding.md) · [Design](docs/research-and-design.md) · [Contributing](CONTRIBUTING.md)

Expand Down
11 changes: 6 additions & 5 deletions boatstack/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ To see every feature at once, run the read-only `.product-loop/boatstack flow fr

## Run to an explicit goal

For `$boatstack run --to plan|verified|pr`, `/boatstack-run`, or a natural-language run request, resolve the target from the request. When it is absent, ask once for `plan`, `verified`, or `pr`. First run the read-only `next-status --repo . --json` and `operation-status --repo . --json`. Wait for an executing operation and reconcile unknown completion before retrying. When the host supplies the plan path, enter `auto-plan` with `--plan <path>`; when no plan path is supplied, stop and ask the user for the plan to build. Return **Feature complete** only for a verified completed feature, and stop on unverified, ambiguous, stale, or invalid state. Schema-v3 `check-plan` runs the Git freshness preflight before it displays the plan fingerprint. Record the selected target with `record-autonomy --plan <plan.md> --target <target>` after all material questions are answered or every remaining question has a valid `RESOLVED_BY_POLICY` autonomy decision. Target `plan` stops after the valid reviewable plan. Targets `verified` and `pr` activate with `--autonomy <autonomy.md>` and stop if that receipt becomes stale. A failed fetch, missing remote/base, stale base, upstream drift, wrong worktree, constrained branch mismatch, incomplete journey decision, or ineligible policy decision blocks without creating authority or consuming repair budget. Never repair freshness by merging, rebasing, switching or creating a constrained delivery branch, discarding changes, force-pushing, or broadening permissions.
For `$boatstack run --to plan|verified|pr`, `/boatstack-run`, or a natural-language run request, resolve the target from the request. When it is absent, ask once for `plan`, `verified`, or `pr`. First run the read-only `next-status --repo . --json` and `operation-status --repo . --json`. Wait for an executing operation and reconcile unknown completion before retrying. When the host supplies the plan path, enter `auto-plan` with `--plan <path>`; when no plan path is supplied, stop and ask the user for the plan to build. Return **Feature complete** only for a verified completed feature, and stop on unverified, ambiguous, stale, or invalid state. Schema-v3 `check-plan` runs the Git freshness preflight before it displays the plan fingerprint. When workspace management is enabled, run `workspace-cut` after the plan passes validation and continue every later command from its `destination_repository`. Only then record human approval or the selected autonomy target, so the receipt binds the final feature branch. Target `plan` stops after the valid reviewable plan. Targets `verified` and `pr` activate with `--autonomy <autonomy.md>` and stop if that receipt becomes stale. A failed fetch, missing remote/base, stale base, upstream drift, wrong worktree, constrained branch mismatch, incomplete journey decision, or ineligible policy decision blocks without creating authority or consuming repair budget. Never repair freshness by merging, rebasing, switching or creating a constrained delivery branch, discarding changes, force-pushing, or broadening permissions.

After preflight, repeatedly run `next-status --repo . --json`, execute only its verified next operation, verify the resulting repository state, and resolve again. Continue across all declared slices until the selected target is reached. A policy receipt may resolve only a non-material, within-spec, reversible choice with one recommendation, repository evidence, no protected impact, and a runnable oracle. Record it as `RESOLVED_BY_POLICY`, never `ANSWERED`. Any failed or unknown condition pauses for the human. Target `verified` stops after current test and review evidence passes. Target `pr` supplies scoped authority for one normal open or update action recorded in `autonomy.md`; after the exact preview is revalidated, call `publish-pr --autonomy <autonomy.md>` without asking for `o` or `u`. A changed plan, repository, branch, PR action, preview, evidence, or target invalidates that path. Same-intent test/review failures may be repaired for at most three complete cycles per active slice. Stop on amendments, ambiguity, safety failures, stale evidence, unsupported recovery, branch mismatch, or exhausted repairs. Never force-push, merge, deploy, or execute a foreign program.

Expand Down Expand Up @@ -135,10 +135,11 @@ Treat repository-owned product context as canonical. Do not require it to be mig
```

2. Present the draft spec, plan, open decisions, accepted assumptions, gaps, risks, validation provenance, `PLAN_FINGERPRINT`, and `READINESS_FINGERPRINT` in a reviewable form. A schema-v3 plan must decide `journey_evidence`: `relevant` with complete typed runnable oracles, or `not_relevant` with a reason.
3. When `workflow.human_plan_approval` is true, ask the developer to approve it or request changes and end with: Reply `a` to approve. When false, state that Build will create a policy-activation lock and do not imply human approval.
4. On changes, return to `auto-plan`, preserve the feedback in the question ledger, and issue a new draft.
5. When human approval is enabled, invoke `.product-loop/boatstack record-approval` with the plan, named human, RFC3339 timestamp, and exact fingerprint. When disabled, create no `approval.md`.
6. End in Plan mode and tell the developer the feature is authorized for the host's normal Build transition. Do not compile tasks, create a lock, request Agent mode merely to write a file, or edit product code.
3. When workspace management is enabled, run `workspace-cut` with this feature. Continue from the returned `destination_repository`. The validated plan fingerprint must remain unchanged.
4. When `workflow.human_plan_approval` is true, ask the developer to approve it or request changes and end with: Reply `a` to approve. When false, state that Build will create a policy-activation lock and do not imply human approval.
5. On changes, return to `auto-plan`, preserve the feedback in the question ledger, and issue a new draft.
6. When human approval is enabled, invoke `.product-loop/boatstack record-approval` with the destination plan, named human, RFC3339 timestamp, and exact fingerprint. When disabled, create no `approval.md`.
7. End in Plan mode and tell the developer the feature is authorized for the host's normal Build transition. Do not compile tasks, create a lock, request Agent mode merely to write a file, or edit product code.

All files created or updated by `auto-plan` and `plan-gate` must be Markdown. gstack and Spec Kit may help produce those documents, but their implementation stages and non-Markdown executable state are deferred to `build`.

Expand Down
12 changes: 7 additions & 5 deletions boatstack/autonomy.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const (

type RunTarget string

var autonomyRecommendedPRAction = RecommendedPRAction

const (
RunTargetPlan RunTarget = "plan"
RunTargetVerified RunTarget = "verified"
Expand Down Expand Up @@ -219,23 +221,23 @@ func RecordAutonomy(options AutonomyRecordOptions) (AutonomyReceipt, error) {
if err != nil {
return AutonomyReceipt{}, err
}
feature := stringValue(check.Plan["feature_id"])
if workspaceEnabled(repo) && needsFreshCut(repo, feature) {
return AutonomyReceipt{}, fmt.Errorf("autonomy requires the feature workspace; run workspace-cut --repo %s --feature %s and continue from destination_repository", repo, feature)
}
branch, err := gitCommand(repo, "rev-parse", "--abbrev-ref", "HEAD")
branch = strings.TrimSpace(branch)
if err != nil || branch == "" || branch == "HEAD" {
return AutonomyReceipt{}, fmt.Errorf("autonomy requires an identifiable current branch")
}
issuingBranch := branch
feature := stringValue(check.Plan["feature_id"])
if workspaceEnabled(repo) && needsFreshCut(repo, feature) {
branch = branchForFeature(feature)
}
repository, err := gitCommand(repo, "remote", "get-url", "origin")
if err != nil {
return AutonomyReceipt{}, fmt.Errorf("autonomy requires an origin repository identity")
}
action := ""
if target == RunTargetPR {
action, _, err = RecommendedPRAction(repo)
action, _, err = autonomyRecommendedPRAction(repo)
if err != nil {
return AutonomyReceipt{}, fmt.Errorf("PR target requires a stable open or update action: %w", err)
}
Expand Down
23 changes: 4 additions & 19 deletions boatstack/autonomy_conformance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,13 @@ func TestAutonomyReceiptOverridesHumanPlanGateOnlyForExactPlan(t *testing.T) {
}
}

// control-law: pre-cut-policy-authority-binds-the-future-managed-branch
func TestAutonomyReceiptBindsFreshWorkspaceBranch(t *testing.T) {
// control-law: autonomy-receipt-binds-policy-activation-to-plan-repository-and-branch
func TestAutonomyReceiptRequiresFreshWorkspaceBranch(t *testing.T) {
root := workspaceRepo(t, defaultWorkspace())
runGit(t, root, "remote", "add", "origin", "https://example.invalid/operatorstack/example.git")
_, _, planPath := writePlanInputs(t, root, true)
receipt, err := RecordAutonomy(AutonomyRecordOptions{Repo: root, PlanPath: planPath, Target: RunTargetVerified})
if err != nil {
t.Fatal(err)
}
if receipt.IssuingBranch != "main" || receipt.Branch != "feat/feature-one" {
t.Fatalf("receipt branches = issuing %q target %q", receipt.IssuingBranch, receipt.Branch)
}
check, err := CheckPlan(planPath)
if err != nil {
t.Fatal(err)
}
path := filepath.Join(filepath.Dir(planPath), "autonomy.md")
if _, err := CheckAutonomyReceiptForPlanning(path, check, root, RunTargetPlan); err != nil {
t.Fatalf("pre-cut planning check: %v", err)
}
if _, err := CheckAutonomyReceipt(path, check, root, RunTargetVerified, ""); err == nil || !strings.Contains(err.Error(), "branch identity changed") {
t.Fatalf("activation on issuing branch should fail, got %v", err)
if _, err := RecordAutonomy(AutonomyRecordOptions{Repo: root, PlanPath: planPath, Target: RunTargetVerified}); err == nil || !strings.Contains(err.Error(), "workspace-cut") {
t.Fatalf("pre-cut autonomy should name the workspace transition, got %v", err)
}
}

Expand Down
79 changes: 79 additions & 0 deletions boatstack/detached.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,85 @@ func saveRegistry(stateRoot string, registry detachedRegistry) error {
return os.WriteFile(registryPath(stateRoot), raw, 0o644)
}

// registerDetachedWorkspaceAlias binds another worktree path of the same Git
// repository to the existing detached controller. It never creates new
// authority: origin, initial history, repository id, and Git common directory
// must match the already-verified source binding.
func registerDetachedWorkspaceAlias(sourceRepo, destinationRepo string) (bool, error) {
source, ok, err := detachedContextFor(sourceRepo)
if err != nil || !ok {
if err == nil {
err = fmt.Errorf("source repository is not attached in detached mode")
}
return false, err
}
destination, err := repoIdentity(destinationRepo)
if err != nil {
return false, err
}
if destination.RepoID != source.RepoID {
return false, fmt.Errorf("destination repository identity does not match the detached controller")
}
binding, err := loadBinding(filepath.Dir(filepath.Dir(source.controlRoot)), source.RepoID)
if err != nil {
// controlRoot is <state>/repositories/<repoID>; resolve the state root
// directly when a non-standard layout makes the derivation ambiguous.
stateRoot, rootErr := detachedStateRoot()
if rootErr != nil {
return false, rootErr
}
binding, err = loadBinding(stateRoot, source.RepoID)
}
if err != nil || !bindingMatchesIdentity(binding, destination) || binding.GitCommonIdentity != destination.GitCommonIdentity {
return false, fmt.Errorf("destination worktree does not match the detached binding")
}
stateRoot, err := detachedStateRoot()
if err != nil {
return false, err
}
registry, err := loadRegistry(stateRoot)
if err != nil {
return false, err
}
root := destination.CanonicalRepoPath
if existing, found := registry.Repositories[root]; found {
if existing != source.RepoID {
return false, fmt.Errorf("destination worktree is already bound to another controller")
}
return false, nil
}
registry.Repositories[root] = source.RepoID
if err := saveRegistry(stateRoot, registry); err != nil {
return false, err
}
invalidateWorkspaceCache()
return true, nil
}

func unregisterDetachedWorkspaceAlias(repo string) error {
root, err := ResolveRepository(repo)
if err != nil {
return err
}
stateRoot, err := detachedStateRoot()
if err != nil {
return err
}
registry, err := loadRegistry(stateRoot)
if err != nil {
return err
}
if _, found := registry.Repositories[root]; !found {
return nil
}
delete(registry.Repositories, root)
if err := saveRegistry(stateRoot, registry); err != nil {
return err
}
invalidateWorkspaceCache()
return nil
}

func loadBinding(stateRoot, repoID string) (DetachedBinding, error) {
var binding DetachedBinding
raw, err := os.ReadFile(bindingPath(stateRoot, repoID))
Expand Down
Loading