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 boatstack/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ Do not branch the workflow on model brand, price, or a guessed capability tier.

## Repair from ordinary conversation

Before any product edit or explicit `repair`, run `recovery-status` with the exact requested change and observed source stage. It resolves active work and published work associated with the current branch or recorded PR. Automatically use repair for ordinary CI failures, review findings, denied publication, problems, and modifications even when the user does not name Boatstack or a slash command. Active work resumes through `record-change`; a published parent returns `CORRECTIVE_CHILD_REQUIRED` and a deterministic child id. Never ask the user to manually repeat a denied push or PR mutation.
Before any product edit or explicit `repair`, run `recovery-status` with the exact requested change and observed source stage. It resolves active work and published work associated with the current branch or recorded PR. Automatically use repair for ordinary CI failures, review findings, denied publication, problems, and modifications even when the user does not name Boatstack or a slash command. Active work resumes through `record-change`; a published parent returns `CORRECTIVE_CHILD_REQUIRED` and a deterministic child id. If active work enters `AMENDMENT_REQUIRED` or `PLAN_INVALID`, treat that composite state as authoritative over the slice status. Save the accepted amendment as a durable source plan, obtain a fresh lifecycle-bound `flow bootstrap` prescription for the same feature, execute only its returned envelope, then require normal approval and activation before delivery resumes. Never ask the user to manually repeat a denied push or PR mutation.

If Cursor reports `MainThreadShellExec not initialized`, the host failed before Boatstack's hook process started. Keep the hook fail-closed and make **Developer: Reload Window** the primary recovery, then retry the operation. Recommend the verified installer only when Boatstack itself reports a missing, drifted, unsafe, or checksum-invalid helper/runtime.

Expand Down
73 changes: 56 additions & 17 deletions boatstack/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import (

const bootstrapPrescriptionSchemaVersion = 1

// Tests may replace this seam. Nil selects the production installation health
// check immediately before a bootstrap prescription is rendered.
var bootstrapInstallationHealth func(string) error

type BootstrapShell string

const (
Expand Down Expand Up @@ -45,6 +49,10 @@ type BootstrapPrescription struct {
Artifact string `json:"artifact"`
ArtifactPath string `json:"artifact_path"`
DocumentSHA256 string `json:"document_sha256"`
LifecycleState string `json:"lifecycle_state,omitempty"`
LifecycleSHA256 string `json:"lifecycle_sha256,omitempty"`
ObservationID string `json:"observation_id,omitempty"`
PreviousPlanLock string `json:"previous_plan_lock_sha256,omitempty"`
Shell BootstrapShell `json:"shell"`
Argv []string `json:"argv"`
PlanningEnvelope string `json:"planning_envelope"`
Expand All @@ -62,22 +70,23 @@ func normalizedPlanningDocument(document []byte) ([]byte, error) {
return value, nil
}

func bootstrapFeatureDisposition(repo string, workspace WorkspaceContext, feature string) (string, error) {
func bootstrapFeatureDisposition(repo string, workspace WorkspaceContext, feature string) (string, *LifecycleSnapshot, error) {
directory := workspace.FeatureDir(feature)
info, err := os.Lstat(directory)
if os.IsNotExist(err) {
return "CREATE_CANDIDATE", nil
return "CREATE_CANDIDATE", nil, nil
}
if err != nil {
return "", err
return "", nil, err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return "", fmt.Errorf("feature %s has conflicting planning state; run recovery-status before bootstrapping", feature)
return "", nil, fmt.Errorf("feature %s has conflicting planning state; run recovery-status before bootstrapping", feature)
}
statePath, stateErr := deliveryStatePath(repo, feature)
if stateErr != nil {
return "", stateErr
return "", nil, stateErr
}
managed := false
for _, path := range []string{
statePath,
filepath.Join(directory, "plan.lock.json"),
Expand All @@ -86,24 +95,35 @@ func bootstrapFeatureDisposition(repo string, workspace WorkspaceContext, featur
filepath.Join(directory, "autonomy.md"),
} {
if fileExists(path) {
return "", fmt.Errorf("feature %s already carries managed authority; use flow next --feature %s", feature, feature)
managed = true
break
}
}
if managed {
snapshot, snapshotErr := ResolveLifecycleSnapshot(repo, feature)
if snapshotErr != nil {
return "", nil, fmt.Errorf("feature %s carries managed authority that cannot be verified: %w", feature, snapshotErr)
}
if !amendmentLifecycleState(snapshot.State) {
return "", nil, fmt.Errorf("feature %s already carries managed authority; use flow next --feature %s", feature, feature)
}
return "AMEND_ACTIVE", &snapshot, nil
}
entries, err := os.ReadDir(directory)
if err != nil {
return "", err
return "", nil, err
}
for _, entry := range entries {
if entry.IsDir() || !planningArtifacts[entry.Name()] {
return "", fmt.Errorf("feature %s has conflicting planning state; run recovery-status before bootstrapping", feature)
return "", nil, fmt.Errorf("feature %s has conflicting planning state; run recovery-status before bootstrapping", feature)
}
}
if fileExists(filepath.Join(directory, "plan.md")) {
if _, err := CheckPlan(filepath.Join(directory, "plan.md")); err != nil {
return "", fmt.Errorf("feature %s has an invalid saved plan; run recovery-status before bootstrapping: %w", feature, err)
return "", nil, fmt.Errorf("feature %s has an invalid saved plan; run recovery-status before bootstrapping: %w", feature, err)
}
}
return "RESUME_CANDIDATE", nil
return "RESUME_CANDIDATE", nil, nil
}

func bootstrapProgram(workspace WorkspaceContext, shell BootstrapShell) string {
Expand All @@ -113,15 +133,23 @@ func bootstrapProgram(workspace WorkspaceContext, shell BootstrapShell) string {
return workspace.LauncherPath(shell == BootstrapShellPowerShell)
}

func planningArgv(program, repo, feature, artifact, sourcePlan, sourceSHA string) []string {
return []string{
func planningArgv(program, repo, feature, artifact, sourcePlan, sourceSHA string, lifecycle *LifecycleSnapshot) []string {
argv := []string{
program, "planning-write",
"--repo", repo,
"--feature", feature,
"--artifact", artifact,
"--source-plan", sourcePlan,
"--source-plan-sha256", sourceSHA,
}
if lifecycle != nil {
argv = append(argv,
"--expected-lifecycle-sha256", lifecycle.Fingerprint,
"--expected-plan-lock-sha256", lifecycle.PlanLockSHA256,
"--expected-observation", lifecycle.ObservationID,
)
}
return argv
}

func posixPlanningEnvelopeFor(argv []string, document []byte) string {
Expand Down Expand Up @@ -176,7 +204,11 @@ func ResolvePlanningBootstrap(options BootstrapOptions) (BootstrapPrescription,
if err != nil {
return BootstrapPrescription{}, err
}
if err := CheckInstallationHealth(repo); err != nil {
healthCheck := CheckInstallationHealth
if bootstrapInstallationHealth != nil {
healthCheck = bootstrapInstallationHealth
}
if err := healthCheck(repo); err != nil {
return BootstrapPrescription{}, fmt.Errorf("bootstrap requires a healthy Boatstack installation: %w", DoctorRepairHint(err))
}
workspace, err := ResolveWorkspaceContext(repo)
Expand All @@ -198,12 +230,12 @@ func ResolvePlanningBootstrap(options BootstrapOptions) (BootstrapPrescription,
if err != nil {
return BootstrapPrescription{}, err
}
disposition, err := bootstrapFeatureDisposition(repo, workspace, options.Feature)
disposition, lifecycle, err := bootstrapFeatureDisposition(repo, workspace, options.Feature)
if err != nil {
return BootstrapPrescription{}, err
}
program := bootstrapProgram(workspace, options.Shell)
argv := planningArgv(program, repo, options.Feature, options.Artifact, sourcePlan, sourceSHA)
argv := planningArgv(program, repo, options.Feature, options.Artifact, sourcePlan, sourceSHA, lifecycle)
if options.Shell == BootstrapShellPOSIX {
// Git Bash accepts Windows drive paths in slash form. Keep the typed argv
// identical to the bytes rendered for that shell.
Expand All @@ -219,7 +251,7 @@ func ResolvePlanningBootstrap(options BootstrapOptions) (BootstrapPrescription,
if err != nil {
return BootstrapPrescription{}, err
}
return BootstrapPrescription{
prescription := BootstrapPrescription{
SchemaVersion: bootstrapPrescriptionSchemaVersion, VerificationStatus: "VERIFIED",
Disposition: disposition, SupervisionMode: workspace.Mode,
Repository: repo, RepositoryID: workspace.RepoID, WorktreeID: workspace.WorktreeID,
Expand All @@ -228,5 +260,12 @@ func ResolvePlanningBootstrap(options BootstrapOptions) (BootstrapPrescription,
Artifact: options.Artifact, ArtifactPath: filepath.Join(workspace.FeatureDir(options.Feature), options.Artifact),
DocumentSHA256: SHA256Bytes(document), Shell: options.Shell, Argv: argv,
PlanningEnvelope: envelope,
}, nil
}
if lifecycle != nil {
prescription.LifecycleState = string(lifecycle.State)
prescription.LifecycleSHA256 = lifecycle.Fingerprint
prescription.ObservationID = lifecycle.ObservationID
prescription.PreviousPlanLock = lifecycle.PlanLockSHA256
}
return prescription, nil
}
2 changes: 0 additions & 2 deletions boatstack/cmd/boatstack-helper/coverage_conformance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ var nonDeliveryVerbs = map[string]bool{
// Planning phase, before a plan is activated into a delivery.
"check-source-plan": true,
"check-plan": true,
"planning-write": true,
"record-approval": true,
"record-autonomy": true,
// Read-only status / diagnostics (observe helpers, not modeled transitions).
"repair-status": true,
Expand Down
11 changes: 11 additions & 0 deletions boatstack/cmd/boatstack-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,9 @@ func planningWriteCommand(arguments []string) int {
artifact := flags.String("artifact", "", "known Markdown planning artifact name")
sourcePlan := flags.String("source-plan", "", "in-repo source plan bound by flow bootstrap")
sourcePlanSHA256 := flags.String("source-plan-sha256", "", "source-plan digest bound by flow bootstrap")
expectedLifecycleSHA256 := flags.String("expected-lifecycle-sha256", "", "active-delivery lifecycle fingerprint bound by flow bootstrap")
expectedPlanLockSHA256 := flags.String("expected-plan-lock-sha256", "", "active plan-lock digest bound by flow bootstrap")
expectedObservation := flags.String("expected-observation", "", "active amendment observation bound by flow bootstrap")
if err := flags.Parse(arguments); err != nil {
return 2
}
Expand All @@ -594,6 +597,9 @@ func planningWriteCommand(arguments []string) int {
path, err := boatstack.WritePlanningArtifact(boatstack.PlanningWriteOptions{
Repo: *repo, Feature: *feature, Artifact: *artifact, Content: content,
SourcePlan: *sourcePlan, SourcePlanSHA256: *sourcePlanSHA256,
ExpectedLifecycleSHA256: *expectedLifecycleSHA256,
ExpectedPlanLockSHA256: *expectedPlanLockSHA256,
ExpectedObservation: *expectedObservation,
})
if err != nil {
return fail(err)
Expand All @@ -610,6 +616,9 @@ func recordApprovalCommand(arguments []string) int {
approvedAt := flags.String("approved-at", "", "RFC3339 approval timestamp")
fingerprint := flags.String("fingerprint", "", "exact fingerprint displayed before approval")
baselineDiffSHA256 := flags.String("baseline-diff-sha256", "", "exact product baseline fingerprint displayed before approval; omit only when clean")
expectedLifecycleSHA256 := flags.String("expected-lifecycle-sha256", "", "exact active lifecycle fingerprint displayed before amendment approval")
expectedPlanLockSHA256 := flags.String("expected-plan-lock-sha256", "", "exact prior plan-lock fingerprint displayed before amendment approval")
expectedObservation := flags.String("expected-observation", "", "exact active amendment observation displayed before approval")
if err := flags.Parse(arguments); err != nil {
return 2
}
Expand All @@ -619,6 +628,8 @@ func recordApprovalCommand(arguments []string) int {
if err := boatstack.RecordApproval(boatstack.ApprovalRecordOptions{
PlanPath: *plan, OutputPath: *output, ApprovedBy: *approvedBy,
ApprovedAt: *approvedAt, Fingerprint: *fingerprint, BaselineDiffSHA256: *baselineDiffSHA256,
ExpectedLifecycleSHA256: *expectedLifecycleSHA256, ExpectedPlanLockSHA256: *expectedPlanLockSHA256,
ExpectedObservation: *expectedObservation,
}); err != nil {
return fail(err)
}
Expand Down
2 changes: 1 addition & 1 deletion boatstack/config_event_registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func TestConfigurationEventRegistryIsComplete(t *testing.T) {
}
sort.Strings(entries)
digest := SHA256Bytes([]byte(strings.Join(entries, "\n")))
const expected = "dbccf0d0263b056656e1626a56966b0c9dea5b67187b8e0f1980cd5745f4d6c5"
const expected = "dfcbdc50fdfbda0d505ce7b04f55f6289622adad74a8fd01fc5fb13e6a00979b"
if digest != expected {
_ = os.WriteFile(filepath.Join(t.TempDir(), "config-events.txt"), []byte(strings.Join(entries, "\n")+"\n"), 0o644)
t.Fatalf("configuration event registry changed: got %s; classify the new or removed site and update the reviewed digest", digest)
Expand Down
2 changes: 2 additions & 0 deletions boatstack/deliverycontrol_parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ var realDeliveryHandlers = map[string]any{
"CheckDeliveryReadyForShip": CheckDeliveryReadyForShip,
"ResolveNext": ResolveNext,
"ResolveRecovery": ResolveRecovery,
"WritePlanningArtifact": WritePlanningArtifact,
"RecordApproval": RecordApproval,
}

func TestRegistryHandlerRefsAreRealFunctions(t *testing.T) {
Expand Down
Loading