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 @@ -54,7 +54,7 @@ One verified runtime is cached under the clone's Git common directory and keyed

Independent clones do not share a Git common directory. Committed adapters survive a clone, but the ignored helper and repository-family cache do not; run the installer once in the new clone.

For an update, run `/boatstack-update` from a clean, current default branch. Boatstack creates `chore/update-boatstack-v<version>`, verifies the tagged release and checksum, preserves integrations, and shows the exact generated diff before asking for `open update PR`. Release-check state in `.product-loop/bin/update-state.json` and the platform helper remain ignored; the adapters, generated lock, hook fragments, and merged host settings belong in the update PR.
For an update, run `/boatstack-update` from a clean, current default branch. Boatstack creates `chore/update-boatstack-v<version>`, verifies the tagged release and checksum, preserves integrations, and stores a fingerprinted non-empty update-PR preview under Git-common Boatstack state before asking for `o`. `publish-update-pr` owns the exact commit, normal push, and single-PR reconciliation. Release-check state in `.product-loop/bin/update-state.json`, operation receipts under Git-common `boatstack/operations/v1`, the update preview, and the platform helper remain ignored; the adapters, generated lock, hook fragments, and merged host settings belong in the update PR.

An update refuses feature branches, dirty worktrees, stale default branches, changed generated files, and user-owned collisions. It never merges its own PR.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Ask Boatstack for the next verified stage without changing anything:

Boatstack reads repository-owned plans, approvals, delivery state, and gate receipts, then returns exactly one next action. Chat, terminal, worktree, and running-process observations may add context but never establish a workflow stage. If no managed work remains, Boatstack reports **Feature complete** and **No action required**.

For a small verified feature, `/boatstack-run` in Cursor or Claude Code and `$boatstack run` in Codex drive every declared delivery slice through PR publication. The command fetches `origin` before mutation and pauses for required plan approval, product decisions, and the exact PR open/update confirmation. Merge and deploy remain separate.
For a small verified feature, `/boatstack-run` in Cursor or Claude Code and `$boatstack run` in Codex drive every declared delivery slice through PR publication. The command fetches `origin` before mutation and pauses for required plan approval, product decisions, and the exact PR open/update confirmation. Managed side effects use durable attempt receipts, so a lost response or new chat resumes or reconciles the same work instead of repeating it. Merge and deploy remain separate.

## 1. Install it separately

Expand Down Expand Up @@ -50,7 +50,7 @@ Review and commit the paths printed by the installer. Merge this infrastructure

The installer keeps a versioned, verified runtime under Git's common directory. A linked worktree still starts without the ignored `.product-loop/bin/` directory, but its first guarded Cursor, Codex, or Claude call restores that local runtime automatically before evaluating the original command. This performs no download and changes no tracked files.

Host activation is separate from runtime installation. Codex requires the exact linked-worktree project path and hook definition to be reviewed and trusted through `/hooks`; start a new task after trusting it. Claude Code requires Bash and exposes the active `PreToolUse` hook through `/hooks`. Cursor requires a window reload and enabled `beforeShellExecution` and `beforeMCPExecution` hooks.
Host activation is separate from runtime installation. Codex requires the exact linked-worktree project path and pre/post tool hook definitions to be reviewed and trusted through `/hooks`; start a new task after trusting them. Claude Code requires Bash and exposes `PreToolUse`, `PostToolUse`, and failure observation through `/hooks`. Cursor requires a window reload and enabled before/after native, shell, and MCP hooks.

Different Boatstack versions use separate cached runtimes, so an older worktree is not silently run with a newer helper. A separate clone has a different Git common directory and still needs one installer run.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ Release discovery uses a short, unauthenticated request to GitHub and a 24-hour

Boatstack found an installed generated file that no longer matches its previous lock. Review the named path and move durable project-owned content into `.boatstack-project.json` or repository documentation. Do not overwrite the drift merely to make the update pass.

## A tool call repeats or publication appears stuck

Run `.product-loop/bin/boatstack-helper operation-status --repo . --json`. `EXECUTING` means the exact call already has a live lease, so wait instead of launching it again. `RECONCILE_REQUIRED` means Boatstack did not observe completion; verify the reported Git, GitHub, file, browser, or MCP postcondition before retrying. A successful operation whose response was lost is recovered from that observation. Do not reset the task, repeat a denied push, or open another PR.

Operation receipts are shared by linked worktrees and retry budgets survive new chats and host restarts. If more than one unfinished operation matches, rerun status with the reported operation ID rather than choosing the newest. The receipts contain fingerprints and secret-free observations; no command payload or credential should be added to them.

## An update PR response was interrupted

Keep the update branch and rerun the Boatstack update publication step with the same displayed preview fingerprint. The deterministic publisher queries the exact head branch first and returns the existing PR when GitHub accepted the earlier request. If the update diff changed, regenerate and review the preview; never bypass it with a direct push or `gh pr create`.

## The PR preview is stale

A new commit, changed evidence, changed approval artifact, or base-branch update invalidated the preview. Ask Boatstack to regenerate it. Do not copy the old body forward.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
### Resume side effects without duplicate retries

Boatstack now supervises managed mutations and publication attempts with durable,
fingerprinted operation receipts shared across linked worktrees. Host pre/post tool
events create and complete single-use leases; interrupted or unknown external
effects require reconciliation before a bounded retry. PR and Boatstack-update
publication recover an already-created pull request instead of opening a duplicate,
and `boatstack-run` uses the delivery's persistent three-cycle repair budget across
chats, host restarts, and async notifications. Human plan and publication approvals
remain required under their existing defaults. Machine-parsed subprocess results now
use stdout only; bounded stderr diagnostics can never become paths, refs, URLs, or
workflow authority.
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ Run the project-local helper's read-only `next-status --repo . --json` inspectio

## Run through ship

For `$boatstack run`, `/boatstack-run`, or natural language such as “run Boatstack through ship,” first run the read-only `next-status --repo . --json`. When one saved source plan is ready, enter `auto-plan`; when no source plan exists, stop and ask the user to save the host Plan-mode file. Return **Feature complete** only for a verified completed feature, and stop on unverified, ambiguous, stale, or invalid state. Before the first delivery-stage operation (`build`, `repair`, `test-gate`, `review-gate`, or `ship-gate`), run `run-preflight --repo . --json`. Planning and approval do not require a remote fetch. The preflight fetches `origin` and verifies the current named branch contains the fetched delivery base and is not behind or diverged from its upstream. A failed fetch, missing remote/base, stale base, upstream drift, or constrained branch mismatch blocks before delivery mutation. Never repair freshness by merging, rebasing, switching or creating a constrained branch, discarding changes, force-pushing, or broadening permissions.
For `$boatstack run`, `/boatstack-run`, or natural language such as “run Boatstack through ship,” 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 one saved source plan is ready, enter `auto-plan`; when no source plan exists, stop and ask the user to save the host Plan-mode file. Return **Feature complete** only for a verified completed feature, and stop on unverified, ambiguous, stale, or invalid state. Before the first delivery-stage operation (`build`, `repair`, `test-gate`, `review-gate`, or `ship-gate`), run `run-preflight --repo . --json`. Planning and approval do not require a remote fetch. The preflight fetches `origin` and verifies the current named branch contains the fetched delivery base and is not behind or diverged from its upstream. A failed fetch, missing remote/base, stale base, upstream drift, or constrained branch mismatch blocks before delivery mutation. 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 using the canonical semantics below, verify the resulting repository state, and resolve again. Continue across all declared slices. Pause for explicit `a` plan approval, material product questions, and the exact `o` or `u` PR confirmation; a valid answer resumes the foreground run in the current host session. The run invocation itself is never approval or publication authority. Same-intent test/review failures may be recorded and repaired for at most three complete repair-and-gate cycles per active slice in one invocation. Stop on amendments, ambiguity, safety failures, stale evidence, unsupported recovery, branch mismatch, or an exhausted budget. Do not persist autopilot state or use conversation as workflow evidence. Completion means every slice PR is published for review, never merged or deployed.
After preflight, repeatedly run `next-status --repo . --json`, execute only its verified next operation using the canonical semantics below, verify the resulting repository state, and resolve again. Continue across all declared slices. Pause for explicit `a` plan approval, material product questions, and the exact `o` or `u` PR confirmation; a valid answer resumes the foreground run in the current host session. The run invocation itself is never approval or publication authority. Same-intent test/review failures may be recorded and repaired for at most three complete repair-and-gate cycles per active slice; the durable delivery attempt count does not reset across turns or hosts. Stop on amendments, ambiguity, safety failures, stale evidence, unsupported recovery, branch mismatch, or an exhausted budget. Persist execution facts and retry identity, never autonomous workflow intent; conversation is not workflow evidence. Completion means every slice PR is published for review, never merged or deployed.

## Enforce the irreversible-operation boundary

Expand Down Expand Up @@ -220,8 +220,8 @@ Treat `boatstack-update` as infrastructure maintenance, never as feature work:
2. Fetch the configured default branch without editing product files. Require that branch to be current and clean; otherwise return **Update postponed** and change nothing.
3. Create only `chore/update-boatstack-v<version>`. Run the installer fetched from the exact release tag in update mode with the exact version, repository path, and non-interactive preview acceptance.
4. Preserve `.boatstack-project.json`, all portable adapters, optional integration selections, and unrelated host settings. Block on generated drift, collisions, missing provenance, a failed checksum, a failed `doctor`, or any product-file change.
5. Show the version transition, release notes, integration state, changed infrastructure paths, exact diff, checksums, rollout, and rollback. Respond **Boatstack update ready** and render the one next action as: Reply `o` to open update PR.
6. Only the state-scoped `o` or compatible full reply authorizes staging the reviewed infrastructure paths, committing, normally pushing, and opening the update PR. Never merge it. If GitHub publication is unavailable, retain the prepared branch and provide one manual action.
5. Run `prepare-update-pr --repo . --version <version> --json`; show its exact non-empty fingerprinted package with the version transition, release notes, integration state, changed infrastructure paths, checksums, rollout, and rollback. Respond **Boatstack update ready** and render the one next action as: Reply `o` to open update PR.
6. Only the state-scoped `o` or compatible full reply authorizes `publish-update-pr` with that preview fingerprint. The publisher owns staging, the exact commit, normal push, and one PR. If its response is interrupted, inspect `operation-status` and reconcile the exact branch/PR rather than repeating GitHub mutation. Never merge it. If GitHub publication is unavailable, retain the prepared branch and provide one manual action.

Natural requests such as “Update Boatstack” use this operation. `doctor` may display a cached notice but must remain offline. Do not perform release discovery during planning, approval, build, test, review, or PR preview.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,78 @@ func checkUpdateCommand(arguments []string) int {
return 0
}

func operationStatusCommand(arguments []string) int {
flags := flag.NewFlagSet("operation-status", flag.ContinueOnError)
repo := flags.String("repo", ".", "repository whose durable operation state should be inspected")
operationID := flags.String("operation-id", "", "specific operation identity; omit only when the current branch has at most one unfinished operation")
jsonOutput := flags.Bool("json", false, "emit the versioned JSON projection")
if err := flags.Parse(arguments); err != nil {
return 2
}
status, err := boatstack.ResolveOperationStatus(*repo, *operationID)
if err != nil {
return fail(err)
}
value, err := boatstack.MarshalJSON(status)
if err != nil {
return fail(err)
}
if *jsonOutput {
fmt.Print(string(value))
} else if status.Operation == nil {
fmt.Printf("OPERATION_STATUS=%s\nNEXT_OPERATION=%s\n", status.VerificationStatus, status.NextOperation)
} else {
fmt.Printf("OPERATION_STATUS=%s\nOPERATION_ID=%s\nSTATE=%s\nATTEMPT=%d/%d\nNEXT_OPERATION=%s\n", status.VerificationStatus, status.Operation.OperationID, status.Operation.State, status.Operation.Attempt, status.Operation.MaxAttempts, status.NextOperation)
}
if status.VerificationStatus == "AMBIGUOUS" {
return 1
}
return 0
}

func prepareUpdatePRCommand(arguments []string) int {
flags := flag.NewFlagSet("prepare-update-pr", flag.ContinueOnError)
repo := flags.String("repo", ".", "updated Boatstack repository")
version := flags.String("version", "", "exact installed stable version")
jsonOutput := flags.Bool("json", false, "emit the fingerprinted preview as JSON")
if err := flags.Parse(arguments); err != nil {
return 2
}
preview, err := boatstack.PrepareUpdatePublication(*repo, *version)
if err != nil {
return fail(err)
}
value, err := boatstack.MarshalJSON(preview)
if err != nil {
return fail(err)
}
if *jsonOutput {
fmt.Print(string(value))
} else {
fmt.Printf("UPDATE_PREVIEW=%s\nPREVIEW_FINGERPRINT=%s\nPACKAGE_FINGERPRINT=%s\n", preview.PreviewPath, preview.Fingerprint, preview.PackageFingerprint)
}
return 0
}

func publishUpdatePRCommand(arguments []string) int {
flags := flag.NewFlagSet("publish-update-pr", flag.ContinueOnError)
repo := flags.String("repo", ".", "updated Boatstack repository")
preview := flags.String("preview", "", "exact machine-local update preview path")
fingerprint := flags.String("preview-fingerprint", "", "fingerprint confirmed by the human")
if err := flags.Parse(arguments); err != nil {
return 2
}
if *preview == "" || *fingerprint == "" {
return fail(fmt.Errorf("publish-update-pr requires --preview and --preview-fingerprint"))
}
url, err := boatstack.PublishUpdatePublication(boatstack.UpdatePublishOptions{Repo: *repo, PreviewPath: *preview, ExpectedFingerprint: *fingerprint})
if err != nil {
return fail(err)
}
fmt.Printf("PR_URL=%s\n", url)
return 0
}

func releaseClassifyCommand(arguments []string) int {
flags := flag.NewFlagSet("release-classify", flag.ContinueOnError)
repo := flags.String("repo", ".", "projected Boatstack repository")
Expand Down Expand Up @@ -824,7 +896,7 @@ func workspaceStatusCommand(arguments []string) int {

func run() int {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: boatstack-helper <init|update|check-update|release-classify|next-patch|export|check-source-plan|planning-write|check-plan|record-approval|activate-plan|delivery-status|next-status|recovery-status|run-preflight|record-change|record-delivery-gate|record-pr-visual-evidence|record-pr-visual-publication|check-safety|migrate-config|safety-hook|diagnose-hook|pr-context|check-pr|publish-pr|workspace-cut|workspace-cleanup|workspace-status|doctor|version>")
fmt.Fprintln(os.Stderr, "usage: boatstack-helper <init|update|check-update|operation-status|prepare-update-pr|publish-update-pr|release-classify|next-patch|export|check-source-plan|planning-write|check-plan|record-approval|activate-plan|delivery-status|next-status|recovery-status|run-preflight|record-change|record-delivery-gate|record-pr-visual-evidence|record-pr-visual-publication|check-safety|migrate-config|safety-hook|diagnose-hook|pr-context|check-pr|publish-pr|workspace-cut|workspace-cleanup|workspace-status|doctor|version>")
return 2
}
switch os.Args[1] {
Expand All @@ -834,6 +906,12 @@ func run() int {
return updateCommand(os.Args[2:])
case "check-update":
return checkUpdateCommand(os.Args[2:])
case "operation-status":
return operationStatusCommand(os.Args[2:])
case "prepare-update-pr":
return prepareUpdatePRCommand(os.Args[2:])
case "publish-update-pr":
return publishUpdatePRCommand(os.Args[2:])
case "release-classify":
return releaseClassifyCommand(os.Args[2:])
case "next-patch":
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package boatstack

import (
"bytes"
"fmt"
"os/exec"
"strings"
)

// commandChannels preserves the subprocess transport boundary. Stdout is the
// only authority-bearing channel; stderr is diagnostic even when the command
// exits successfully.
type commandChannels struct {
Stdout []byte
Stderr []byte
}

func runCommandChannels(command *exec.Cmd) (commandChannels, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
command.Stdout = &stdout
command.Stderr = &stderr
err := command.Run()
return commandChannels{Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, err
}

func commandFailure(channels commandChannels, runErr error) error {
message := strings.TrimSpace(string(channels.Stderr))
if message == "" {
message = strings.TrimSpace(string(channels.Stdout))
}
if message == "" && runErr != nil {
message = runErr.Error()
}
return fmt.Errorf("%s", boundedObservation(message))
}

// commandOutput returns only successful stdout for machine parsing. Successful
// stderr can contain warnings, progress, locale text, or host diagnostics and
// must never become a path, ref, URL, fingerprint, or workflow status.
func commandOutput(repo string, name string, arguments ...string) (string, error) {
command := exec.Command(name, arguments...)
command.Dir = repo
channels, err := runCommandChannels(command)
if err != nil {
return "", commandFailure(channels, err)
}
return strings.TrimSpace(string(channels.Stdout)), nil
}
Loading