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/e43a4ed28726995d848266cb39bb20902bbb1574/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/2359acff52f0c7a7568fbd7daf1b62a79a86f7b5/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
59 changes: 31 additions & 28 deletions UPSTREAM.json

Large diffs are not rendered by default.

42 changes: 29 additions & 13 deletions boatstack/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ type AttachOptions struct {

// AttachResult is the deterministic outcome of an attach request.
type AttachResult struct {
SchemaVersion int `json:"schema_version"`
VerificationStatus string `json:"verification_status"` // VERIFIED | BLOCKED
Mode string `json:"mode,omitempty"`
RepoID string `json:"repo_id,omitempty"`
RepoRoot string `json:"repo_root,omitempty"`
ControlRoot string `json:"control_root,omitempty"`
WorktreeID string `json:"worktree_id,omitempty"`
Reason string `json:"reason"`
SchemaVersion int `json:"schema_version"`
VerificationStatus string `json:"verification_status"` // VERIFIED | BLOCKED
Mode string `json:"mode,omitempty"`
RepoID string `json:"repo_id,omitempty"`
RepoRoot string `json:"repo_root,omitempty"`
ControlRoot string `json:"control_root,omitempty"`
WorktreeID string `json:"worktree_id,omitempty"`
Reason string `json:"reason"`
FeatureMigrations []DetachedFeatureMigration `json:"feature_migrations,omitempty"`
}

func blockedAttach(reason string) AttachResult {
Expand Down Expand Up @@ -62,12 +63,22 @@ func AttachDetached(opts AttachOptions) (AttachResult, error) {

ctx := detachedContextFromIdentity(stateRoot, identity)

// Synthesize configuration from the repository (test command, default branch,
// context) exactly as embedded init does.
config := defaultConfig(root, detectTestCommand(root))
rawConfig, err := MarshalJSON(config)
// Prefer the repository's declared source configuration during explicit
// reattachment. Falling back to discovery is valid only when no source exists.
configPath := filepath.Join(root, sourceConfigName)
config, rawConfig, err := LoadConfig(configPath)
if os.IsNotExist(err) {
config = defaultConfig(root, detectTestCommand(root))
rawConfig, err = MarshalJSON(config)
}
if err != nil {
return blockedAttach(err.Error()), nil
return blockedAttach("Boatstack could not load the repository source configuration: " + err.Error()), nil
}
imports, migrationResults, migrationErr := planDetachedFeatureImports(root, ctx)
if migrationErr != nil {
result := blockedAttach("Boatstack refused detached feature migration: " + migrationErr.Error())
result.FeatureMigrations = migrationResults
return result, nil
}

// Generate the controller bundle and write it under the external control root.
Expand All @@ -86,6 +97,10 @@ func AttachDetached(opts AttachOptions) (AttachResult, error) {
if err := os.WriteFile(ctx.SourceConfigPath(), rawConfig, 0o644); err != nil {
return blockedAttach(err.Error()), nil
}
migrationResults, err = applyDetachedFeatureImports(imports, migrationResults)
if err != nil {
return blockedAttach("Boatstack could not import embedded feature state: " + err.Error()), nil
}

// Write the binding and index it in the registry.
binding := DetachedBinding{
Expand Down Expand Up @@ -132,6 +147,7 @@ func AttachDetached(opts AttachOptions) (AttachResult, error) {
RepoRoot: root,
ControlRoot: ctx.controlRoot,
WorktreeID: identity.WorktreeID,
FeatureMigrations: migrationResults,
Reason: "Attached Boatstack in detached mode. The repository was not modified; all controller state lives under the external control root.",
}, nil
}
Expand Down
11 changes: 10 additions & 1 deletion boatstack/cmd/boatstack-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ func checkPlanCommand(arguments []string) int {
readinessFingerprint := ""
if version, _ := check.Plan["schema_version"].(float64); version >= 3 {
readiness, readinessErr := boatstack.CheckPlanReadiness(*plan)
repo, _ := boatstack.ResolveRepository(filepath.Dir(*plan))
repo, _ := boatstack.ResolveControllerRepository(filepath.Dir(*plan))
if readinessErr != nil {
boatstack.RecordFlowAttribution(repo, "readiness", deliverycontrol.CostQuery, true, readinessErr.Error())
return fail(readinessErr)
Expand Down Expand Up @@ -1102,7 +1102,16 @@ func doctorCommand(arguments []string) int {
if err := boatstack.DoctorRepairHint(boatstack.Doctor(*repo)); err != nil {
return fail(err)
}
root, err := boatstack.ResolveRepository(*repo)
if err != nil {
return fail(err)
}
ctx, err := boatstack.ResolveWorkspaceContext(root)
if err != nil {
return fail(err)
}
fmt.Printf("PASS: Boatstack %s installation and generated adapters are healthy\n", boatstack.Version)
fmt.Printf("SUPERVISION_MODE=%s\nCONTROLLER_ROOT=%s\nHEALTH=VERIFIED\n", ctx.Mode, ctx.ExportRoot())
hosts, err := boatstack.DoctorHookHosts(*repo)
if err != nil {
return fail(err)
Expand Down
2 changes: 1 addition & 1 deletion boatstack/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func ProjectOperatorContext(repoPath, operation, host string) (OperatorContext,
}
out := OperatorContext{
SchemaVersion: detachedSchemaVersion, Mode: string(SupervisionEmbedded),
RepoRoot: root, Operation: operation, Host: host,
RepoRoot: root, ControlRoot: root, Operation: operation, Host: host,
}

if ctx, ok, verifyErr := detachedContextFor(root); verifyErr != nil {
Expand Down
14 changes: 7 additions & 7 deletions boatstack/delivery.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ func archiveDeliveryReceipt(repo, feature, sliceID, gate, observationID string)
}

func appendChangeObservation(repo string, observation ChangeObservation) error {
path := filepath.Join(repo, ".product-loop", "features", observation.Feature, "changes.md")
path := filepath.Join(WorkspaceFor(repo).FeatureDir(observation.Feature), "changes.md")
existing, err := os.ReadFile(path)
if err != nil && !os.IsNotExist(err) {
return err
Expand All @@ -577,7 +577,7 @@ func appendChangeObservation(repo string, observation ChangeObservation) error {
}

func nextChangeObservationID(repo, feature string, fallback int) string {
path := filepath.Join(repo, ".product-loop", "features", feature, "changes.md")
path := filepath.Join(WorkspaceFor(repo).FeatureDir(feature), "changes.md")
value, err := os.ReadFile(path)
if err != nil {
return fmt.Sprintf("CHG-%03d", fallback)
Expand Down Expand Up @@ -625,7 +625,7 @@ func RecordChangeObservation(options ChangeObservationOptions) (ChangeObservatio
evidenceHash := SHA256Bytes([]byte(strings.TrimSpace(options.Evidence)))
mechanismHash := SHA256Bytes([]byte(strings.TrimSpace(options.Mechanism)))
if repairClass {
changePath := filepath.Join(repo, ".product-loop", "features", options.Feature, "changes.md")
changePath := filepath.Join(WorkspaceFor(repo).FeatureDir(options.Feature), "changes.md")
if prior, readErr := os.ReadFile(changePath); readErr == nil {
for _, block := range strings.Split(string(prior), "\n## ") {
if strings.Contains(block, "- Classification: `"+classification+"`") &&
Expand Down Expand Up @@ -905,7 +905,7 @@ func resolveAddressableSliceByBranch(state DeliveryState, branch string) (int, D
}

func checkDeliveryPlanLock(repo, feature string, state DeliveryState) error {
lockPath := filepath.Join(repo, ".product-loop", "features", feature, "plan.lock.json")
lockPath := filepath.Join(WorkspaceFor(repo).FeatureDir(feature), "plan.lock.json")
lockHash, err := SHA256File(lockPath)
if err != nil {
return fmt.Errorf("managed delivery requires its current plan lock: %w", err)
Expand Down Expand Up @@ -1109,7 +1109,7 @@ func RecordDeliveryGate(options DeliveryGateOptions) (DeliveryGateReceipt, error
}
evidencePath := strings.TrimSpace(options.EvidencePath)
if evidencePath == "" {
evidencePath = featureEvidencePath(filepath.Join(repo, ".product-loop", "features", options.Feature))
evidencePath = featureEvidencePath(WorkspaceFor(repo).FeatureDir(options.Feature))
} else if !filepath.IsAbs(evidencePath) {
evidencePath = filepath.Join(repo, evidencePath)
}
Expand All @@ -1129,7 +1129,7 @@ func RecordDeliveryGate(options DeliveryGateOptions) (DeliveryGateReceipt, error
if recorded := deliveryEvidenceGateStatus(string(evidenceValue), gateLabel, slice.ID, explicit); recorded != status {
return DeliveryGateReceipt{}, fmt.Errorf("evidence ledger must mark the %s gate for delivery slice %s as %s; found %q", gate, slice.ID, status, recorded)
}
relEvidence, err := repositoryRelativePath(repo, evidencePath)
relEvidence, err := repositoryRelativePath(WorkspaceFor(repo).ExportRoot(), evidencePath)
if err != nil {
return DeliveryGateReceipt{}, err
}
Expand Down Expand Up @@ -1451,7 +1451,7 @@ type DiscardDeliveryResult struct {
// orphan, so discard-delivery must clear it. It refuses a dir carrying a
// plan.lock.json (a registered, live feature) so it never touches active work.
func discardOrphanFeatureArtifacts(repo, feature string) (DiscardDeliveryResult, bool, error) {
dir := filepath.Join(repo, ".product-loop", "features", feature)
dir := WorkspaceFor(repo).FeatureDir(feature)
info, statErr := os.Stat(dir)
if os.IsNotExist(statErr) {
return DiscardDeliveryResult{}, false, nil
Expand Down
Loading
Loading