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
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ Release discovery uses a short, unauthenticated request to GitHub and a 24-hour

Boatstack classifies the named path before writing. Exact installed state migrates automatically. If the path is provably Boatstack-owned but drifted, an interactive update shows the fingerprinted repair and asks whether to continue; a noninteractive update returns one retry using `--repair`. The repair is backed up outside the worktree and included in the same update PR.

Admission and final diff validation use the same ownership projection. Marker-bounded updates to `.cursorrules`, `CLAUDE.md`, and `GEMINI.md` are accepted only when all content outside the Boatstack markers is byte-equivalent. If final validation rejects a path that preflight classified as owned, stop rather than retrying: that is a controller consistency failure and must not consume another attempt.

Do not use `--repair` for user-owned or mixed changes. Move durable project content into `.boatstack-project.json` or repository documentation first. A downgrade additionally requires `--allow-downgrade`; repair authority alone never removes newer behavior.

## The installed helper or hook prevents updating
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Canonical update ownership supervision

- Reuse one semantic ownership projection across update admission, mutation verification, staging, and preview, including marker-bounded Cursor, Claude, and Gemini interceptors.
- Validate branch and workspace preconditions before creating a durable operation so rejected setup cannot consume retry budget or collide with the corrected attempt.
- Preserve the underlying rollback reason in the operation receipt and cover the reported stale-interceptor upgrade end to end.
45 changes: 18 additions & 27 deletions labs/12-product-engineering-loop/product-engineering-loop/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,21 +302,12 @@ func updateChangedPaths(repo string) []string {
return sortedKeys(seen)
}

func checkUpdateDiffScope(repo string, currentFiles map[string][]byte, previous map[string]string, hookPaths []string) ([]string, error) {
allowed := map[string]bool{".boatstack-project.json": true}
for path := range currentFiles {
allowed[filepath.ToSlash(path)] = true
}
for path := range previous {
allowed[filepath.ToSlash(path)] = true
}
for _, path := range hookPaths {
allowed[filepath.ToSlash(path)] = true
}
func checkUpdateDiffScope(repo string, currentFiles map[string][]byte, previous map[string]string, config ProjectConfig) ([]string, error) {
ownership := newUpdateOwnershipProjection(config, currentFiles, previous)
changed := updateChangedPaths(repo)
unexpected := []string{}
for _, path := range changed {
if !allowed[path] {
if err := ownership.verify(repo, path); err != nil {
unexpected = append(unexpected, path)
}
}
Expand Down Expand Up @@ -502,6 +493,9 @@ func RunInit(options InitOptions) (returnErr error) {
for _, path := range HostHookPaths(config.Adapters) {
fmt.Fprintln(options.Output, " "+path+" (merge Boatstack safety hook; preserve existing settings)")
}
for _, path := range executionInterceptorPaths(config.Adapters) {
fmt.Fprintln(options.Output, " "+path+" (replace only the marker-bounded Boatstack interceptor)")
}
if !configExists {
fmt.Fprintln(options.Output, " .boatstack-project.json (editable repository facts)")
}
Expand Down Expand Up @@ -632,7 +626,7 @@ func RunInit(options InitOptions) (returnErr error) {
return fmt.Errorf("post-install smoke check failed: %w", err)
}
if options.Update {
changed, scopeErr := checkUpdateDiffScope(repo, bundle.Files, previousGenerated, HostHookPaths(config.Adapters))
changed, scopeErr := checkUpdateDiffScope(repo, bundle.Files, previousGenerated, config)
if scopeErr != nil {
return scopeErr
}
Expand Down Expand Up @@ -669,6 +663,7 @@ func RunInit(options InitOptions) (returnErr error) {
}
}
stagePaths = append(stagePaths, HostHookPaths(config.Adapters)...)
stagePaths = append(stagePaths, executionInterceptorPaths(config.Adapters)...)
stageSet := map[string]bool{}
for _, path := range stagePaths {
stageSet[path] = true
Expand Down Expand Up @@ -750,19 +745,9 @@ func injectExecutionInterceptor(repo, file string) error {
}

func InstallExecutionInterceptors(repo string, adapters []string) error {
for _, adapter := range adapters {
if adapter == "gemini" {
if err := injectExecutionInterceptor(repo, "GEMINI.md"); err != nil {
return err
}
} else if adapter == "claude" {
if err := injectExecutionInterceptor(repo, "CLAUDE.md"); err != nil {
return err
}
} else if adapter == "cursor" {
if err := injectExecutionInterceptor(repo, ".cursorrules"); err != nil {
return err
}
for _, path := range executionInterceptorPaths(adapters) {
if err := injectExecutionInterceptor(repo, path); err != nil {
return err
}
}
return nil
Expand Down Expand Up @@ -794,6 +779,12 @@ func RunUpdate(options InitOptions) error {
return err
}
}
// Validate the complete update workspace before creating a durable attempt.
// Invalid branch or diff state must not consume a retry or leave an identity
// that collides with the later, correctly prepared operation.
if err := ValidateUpdateWorkspaceForRepair(repo, config, preflight, options.Repair); err != nil {
return err
}
branch := strings.TrimSpace(gitOutput(repo, "branch", "--show-current"))
repairAuthority := fmt.Sprintf("repair=%t\x00allow-downgrade=%t", options.Repair, options.AllowDowngrade)
packageFingerprint := SHA256Bytes([]byte(Version + "\x00" + SourceCommit + "\x00" + ChecksumsSHA256 + "\x00" + repairAuthority))
Expand All @@ -819,7 +810,7 @@ func RunUpdate(options InitOptions) error {
}
options.Update = true
if err := RunInit(options); err != nil {
_, _ = CompleteOperation(repo, receipt.OperationID, begin.LeaseToken, "RETRYABLE", "the atomic update transaction rolled back", "")
_, _ = CompleteOperation(repo, receipt.OperationID, begin.LeaseToken, "RETRYABLE", "the atomic update transaction rolled back: "+err.Error(), "")
return err
}
_, err = CompleteOperation(repo, receipt.OperationID, begin.LeaseToken, "SUCCEEDED", "post-install doctor and generated projections passed", Version)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ func classifyHookState(repo, host string) []InstallationRepairItem {
}

func classifyExecutionInterceptor(repo, host string) []InstallationRepairItem {
relative := map[string]string{"cursor": ".cursorrules", "claude": "CLAUDE.md", "gemini": "GEMINI.md"}[host]
relative := executionInterceptorPath(host)
if relative == "" {
return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Select a move only after locating the failure below its surface symptom. “Time
| Review miss | Defect found after same-agent review | Independent reviewer; risk checklist; mechanical enforcement | Expensive review everywhere |
| Scope drift | Diff no longer maps to approved outcomes | Re-scope; split PR; update spec with approval | Hiding product changes in implementation |
| Update self-lockout | An installed helper, stale hook event, or damaged owned receipt blocks its own updater | Let the verified target helper classify state; migrate exact provenance automatically or offer fingerprinted `--repair` | Reinstalling blindly, overwriting user settings, or treating `--repair` as downgrade authority |
| Ownership projection contradiction | Update admission classifies a path as Boatstack-owned, then final validation rejects the controller's own bounded mutation | Build one semantic ownership projection before execution; reuse it for admission, mutation, final verification, staging, and preview | Path-only allowlists accepting user content or independently maintained validators disagreeing after a side effect |
| Security/tenancy | Trust boundary or data scope violated | Specialist review; invariant test; deny-by-default guard | Generic prompt mistaken for enforcement |
| Integration/deploy | Local pass but runtime fails | Environment parity; canary; health checks; rollback | Treating staging as identical to production |
| Documentation drift | Durable behavior and docs disagree | Update source-of-truth artifact; drift check | Growing instructions with unverified rules |
Expand All @@ -35,6 +36,7 @@ Select a move only after locating the failure below its surface symptom. “Time
- **Tool failure must not create recovery authority.** The sanitized database incident moved from a partial schema apply failure to an invented reset path. The irreversible-operation guard is `PROPOSED`, not promoted: evaluate its deny corpus, safe corpus, latency, and workflow regressions against the unguarded baseline.
- **Fail-closed controls need an available evaluator.** A linked worktree copied the safety hook but not its ignored helper, so the guard also denied its own repair command. Share only the verified runtime within the Git clone and hydrate local ignored state before judging the original event.
- **A retry needs a new observation.** Identical in-flight calls wait. Unknown non-idempotent calls enter `RECONCILE_REQUIRED`; Git, GitHub, filesystem, browser, and MCP boundaries must observe their exact postcondition before another attempt consumes the persistent budget.
- **Preconditions run before leases.** Wrong branch, stale base, or invalid diff state returns a recovery operation without creating a durable attempt. A rejected precondition cannot consume retry budget or leave an identity that collides with the corrected invocation.

## Move proposal schema

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,8 @@ After successful publication only, the publisher may use the ignored 24-hour rel

For an available version, create `chore/update-boatstack-v<version>` and download and checksum-verify the target helper before consulting the installed runtime. The target helper classifies hook fragments, generated locks, helper provenance, and marker-bounded interceptors. Exact installed state migrates automatically. Recoverable owned drift is fingerprinted and, interactively, offered as **Repair Boatstack-owned state and continue the update? [y/N]**; noninteractive updates stop with one `--repair` retry. Repair backs up the exact paths in Git-common state and remains in the same update PR. User-owned, mixed, malformed, symlinked, or product state stays blocked. Downgrades require both `--repair` and `--allow-downgrade`.

Before a durable update attempt is created, Boatstack verifies the dedicated branch, base commit, repair classification, and current diff. Invalid workspace state consumes no retry budget. The update transaction then reuses one semantic ownership projection for admission, mutation, final verification, staging, and preview. Generated files must match their prepared bytes, host-hook files must preserve their non-Boatstack JSON, and `.cursorrules`, `CLAUDE.md`, and `GEMINI.md` must preserve everything outside their single Boatstack marker boundary.

The update transaction is a durable atomic-local operation. It preserves repository configuration, adapters, integrations, and unrelated host settings, then runs `doctor`. After installation, `prepare-update-pr` verifies that every changed path is Boatstack-owned and atomically stores the exact non-empty publication package in Git-common runtime state. Show release and repair provenance, the exact generated diff, checksums, changed paths, integration state, rollout, and rollback.

Use **Boatstack update ready** and exactly one action: Reply `o` to open update PR. Only the state-scoped `o` or compatible full reply authorizes `publish-update-pr` with that preview fingerprint. The publisher stages only the approved paths, reuses or creates the exact update commit, pushes normally, and reconciles the head branch before opening at most one PR. The PR body records release provenance, changed generated files, verification, rollout, and revert instructions. If a response is lost after GitHub accepted the request, the next invocation observes and returns the existing PR. If publication is unavailable, retain the prepared branch and provide one manual action. Never merge automatically.
Expand Down
Loading