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
@@ -0,0 +1,16 @@
### Next steps now name their actor, so status replies never assign you the agent's work

Every prescribed next step is now typed by who performs it. A step belongs to
the operator only when it owes operator knowledge or authority — an approval,
a publish or cleanup reply, a feature choice, a source-plan path, a correction
fact. Every other step belongs to the agent, including steps whose evidence
the agent produces by doing the work, such as test runs and plan checks.

The rendered response marks agent-owned steps with "This step is mine to do"
and offers a single delegation key: reply `g` and the agent executes the step,
re-renders, and continues until the next step reaches the operator frontier.
A working response may no longer end by describing work the agent still has
to do; when a step repeats without progress the agent stops and reports the
block instead of looping. `flow next --json` exposes the typing as
`next_actor`, and the classifier fails closed to the operator, so a step it
cannot place behaves exactly as before.
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ func BuildExportBundle(configPath string, config ProjectConfig, rawConfig []byte
}

operations := map[string]string{
"boatstack-next": "Run the project-local helper next-status --repo . --format response and present its output as the response. This operation is strictly read-only: do not run the reported operation, edit artifacts, contact GitHub beyond the helper's bounded published-PR inspection, or advance a gate. The helper renders the canonical response contract deterministically — the outcome line and the single ### Next step block with the exact runnable command when one is prescribable; never override, re-derive, or add a second next action. Conversation, terminal, worktree, or process observations may be included as clearly labeled context only and must never override the repository-backed result.",
"boatstack-next": "Run the project-local helper next-status --repo . --format response and present its output as the response. This operation is strictly read-only: do not run the reported operation, edit artifacts, contact GitHub beyond the helper's bounded published-PR inspection, or advance a gate. The helper renders the canonical response contract deterministically — the outcome line and the single ### Next step block with the exact runnable command when one is prescribable; never override, re-derive, or add a second next action. The helper also types the step's actor: when the rendered step is marked \"This step is mine to do\", the step is the agent's, and the one next action is the delegation reply g. Only after the exact reply g, execute the prescribed step, re-render next-status --repo . --format response, and continue through further agent-owned steps until the next step belongs to the operator (an approval, a publish or cleanup reply, a feature choice, a product fact) or no action is required. Stop immediately when a step does not change the prescribed next step — repetition without progress is a stall; report the block and hand the turn to the operator. Never end a response by describing work the agent still has to do. Conversation, terminal, worktree, or process observations may be included as clearly labeled context only and must never override the repository-backed result.",
"boatstack-run": "First run the read-only next-status --repo . --json and operation-status --repo . --json. If an operation is executing, wait and report it instead of launching it again; if reconciliation is required, verify its exact postcondition before retrying. If NOT_STARTED, respond Start a Boatstack feature and ask the user for the plan produced in the host conversation, then execute auto-plan with its path via --plan (Boatstack does not scan directories for plans) without Git preflight, pausing at its normal decision or approval boundary; do not fetch or require a feature branch. If PUBLISHED, report that the PR is awaiting or lacks verified completion and make reviewing its checks the one next action; do not claim completion. If FEATURE_COMPLETE, respond Feature complete with No action required. Stop on UNVERIFIED, BLOCKED, ambiguous, stale, or invalid state. Before executing the first delivery-stage next_operation (build, repair, test-gate, review-gate, or ship-gate), run the project-local helper run-preflight --repo . --json; planning and plan-gate do not require it. Stop on a blocked preflight; never merge, rebase, force-push, discard changes, switch branches, or create a constrained delivery branch to repair freshness. Then execute exactly the verified next_operation using the canonical operation semantics, verify the resulting repository state, and resolve again. Continue across every declared delivery slice. Pause for the exact plan approval reply a, any material product decision, and the exact PR publication reply o or u; after a valid reply in the current host session, automatically continue the run. A run request never supplies approval or publication authority. For a same-intent test or review failure, use repair, record the observation, and retry from the returned stage. The delivery state's durable repair_attempt is the budget; stop after three complete automated repair-and-gate cycles even across new turns, host restarts, or async notifications. Stop immediately on an amendment, ambiguity, unsafe or destructive capability, stale evidence, branch mismatch, unsupported recovery, or exhausted repair budget. If Cursor reports MainThreadShellExec not initialized, explain that Cursor failed before the Boatstack hook started and make Developer: Reload Window the one recovery action; do not recommend reinstall unless Boatstack reports a missing, drifted, unsafe, or checksum-invalid runtime. Do not use conversation as workflow evidence. Durable operation receipts store execution facts and retry budgets, never autonomous workflow intent. Report the feature, active slice, stages completed, completion or pause reason, durable repair-cycle count, and exactly one next action. Ship means publishing every declared slice PR for review; never merge or deploy.",
"root-cause": "Perform failure-mode elimination on a bug, not a patch. This operation is strictly read-only: do not edit product code, create or update artifacts, advance a gate, or contact GitHub; the user supplies the symptom, stack trace, error log, or failing signal as the argument. Locate the failure below its surface symptom and classify it against the failure classes in @.product-loop/failure-moves.md; name the failure CLASS, not the one instance, and if no class fits, name the new class in that vocabulary. Investigate with read-only tools and produce a numbered root-cause chain in which every step is cited to file:line and which distinguishes the crashing frame (the victim) from the true origin (the cause); label authoritative repository facts DISCOVERED and any inference PROPOSED. State the blast radius: every other call site or path exposed to the same class. Propose the minimal STRUCTURAL elimination that makes the whole class unreachable and covers every exposed site, reusing an existing repository pattern or utility where one exists, rather than a local guard on the single line in the trace. Present this as a material product decision with the same tiered paths auto-plan uses under boundary_analysis: [1a] Symptom Patch or [1b] Programmatic Enforcement (a boundary that eliminates the class), and recommend one. Require a regression that reproduces the failure mode before the fix plus the project's own gates as the proof the class is gone, and name related latent hazards left out of scope as non-goals. Then format the result as a host Plan-mode source plan (symptom, root-cause chain, failure mode, blast radius, elimination, non-goals, verification, delivery base branch) and respond Root cause found, making the one next action: save this plan to a durable in-repo path and run auto-plan with it via --plan. Do not implement the fix; hand off to the plan gate.",
"auto-plan": "Take the plan produced in the host conversation, supplied explicitly via --plan <path> (Boatstack never scans directories for plans), and refine it into a Markdown-only draft feature package whose canonical structured artifact is plan.md. Run check-plan read-only. If workflow.boundary_analysis is true, evaluate if the change is a symptom of a missing systemic boundary and perform a rapid codebase scan for other vulnerabilities. Present this as a material product decision with tiered paths: [1a] Symptom Patch or [1b] Programmatic Enforcement (Slice 1 for the boundary, Slice 2 for the feature). When workflow.pr_visual_evidence is suggest or require, record a structural pr_visual_evidence decision: relevant with one to three entry/state/viewport/expected scenarios, or not_relevant with a reason. Discover existing visual tooling but never require a frontend framework or add repository tooling during planning. When a scenario is relevant but no capability command resolves, surface a material provisioning decision with tiered paths: [1a] provision the capture capability now as its own ordered delivery slice, [1b] bundle the capture harness into the feature slice, or [1c] record the gap and defer; this is a surfaced choice, never an imposed framework. Record affected_paths and structured side_effects for external writes; use an immutable target identity, transactional or fix-forward recovery, and destructive=false. When workflow.maintain_changelog is true, include CHANGELOG.md in every delivery slice's affected paths. Keep internal phases as tasks in one delivery slice. Only when the accepted outcome explicitly needs multiple PRs, declare ordered delivery_slices and assign every task exactly once; plan approval never authorizes publication. Do not implement, create JSON or locks, or imply acceptance. If ready, respond with Plan ready and make Run /plan-gate the one next action. If decisions remain, respond with I need your input and ask only 1-3 material questions. If an earlier hand-authored draft was never registered and its plan cannot be verified, the guard denies every product mutation at INVALID_STATE with next operation repair-state; run repair-state to quarantine that unregistered malformed draft and return to auto-plan, then re-author the planning Markdown through the owned planning-write channel (stdin), never a raw file write. It is reversible, refuses any feature carrying a plan lock, pr.md, delivery state, tracked files, or an active or published delivery, and never edits product code.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,73 @@ const (
MarkerRecoveryRepair = deliverycontrol.TransitionID("recovery.repair_state")
)

// NextActor names who performs the prescribed next step. The operator owns a
// step only when it owes operator knowledge or authority — an approval, a
// publish decision, a feature choice, a plan path, a correction fact. The
// agent owns every other step, including steps whose owed inputs are evidence
// the agent produces by doing the work (test runs, the review protocol). A
// working response may end only on an operator-owned step or a terminal
// state; that boundary is the operator frontier.
// control-law: turn-ends-only-at-the-operator-frontier
type NextActor string

const (
// NextActorAgent — the coding agent performs this step now. A working
// response never ends on an agent-owned step; the read-only status view
// renders it as a one-key delegation instead of executing it.
NextActorAgent NextActor = "agent"
// NextActorOperator — the step owes operator knowledge or authority; the
// response may end here.
NextActorOperator NextActor = "operator"
// NextActorNone — terminal; nobody owes an action.
NextActorNone NextActor = "none"
)

// operatorOwedFlags are the prescribed-command inputs that carry operator
// knowledge or authority rather than work-derivable evidence. A prescription
// owing any of these belongs to the operator. The evidence flags (--status,
// --evidence, --reviewer-identity, --review-method) are deliberately absent:
// the agent obtains those by doing the work — never by fabrication — so owing
// them does not move the step across the frontier.
// control-law: turn-ends-only-at-the-operator-frontier
var operatorOwedFlags = map[string]bool{
"--plan": true, // which source plan: product knowledge
"--feature": true, // which delivery: the operator names the slug
"--mutation": true, // which receipt to reverse: an operator decision
"--preview-fingerprint": true, // publish authority is human-confirmed
"--message": true, // correction facts are human knowledge
"--source-stage": true,
"--classification": true,
}

// classifyNextActor types the next step by who must act. Fail-closed: anything
// it cannot place returns operator, which preserves prescribe-and-stop — the
// worst misclassification is today's behavior, never a runaway agent.
// control-law: turn-ends-only-at-the-operator-frontier
func classifyNextActor(status NextStatus, next FlowNext) NextActor {
switch {
case status.ObservedStage == "FEATURE_COMPLETE",
status.ObservedStage == "PUBLISHED" && status.Lifecycle == "PUBLISHED_MERGED":
return NextActorNone
case status.ObservedStage == "PUBLISHED":
// Reviewing the open pull request is the operator's act.
return NextActorOperator
case next.Prescribed == nil:
// Ambiguity and unprescribed blocks resolve only by operator choice.
return NextActorOperator
case next.Prescribed.Transition == PublishTransition:
// Opening or updating a PR is operator-confirmed (`o`/`u`), regardless
// of which flags happen to be owed.
return NextActorOperator
}
for _, flag := range next.Prescribed.RequiresHumanInput {
if operatorOwedFlags[flag] {
return NextActorOperator
}
}
return NextActorAgent
}

// FlowNext is the advisory answer for `flow next`: the current delivery-flow
// state, the real recommended operation (from ResolveNext — the authoritative
// next-move table), and the oracle's lowest-cost next control plus the remaining
Expand Down Expand Up @@ -138,6 +205,12 @@ type FlowNext struct {
// Advisory, never a second primary: the rendering keeps exactly one Run line.
// control-law: solution-set-derives-from-guard-declarations
Alternatives []PrescribedCommand `json:"alternatives,omitempty"`
// Actor names who performs the next step: "agent" when the step is the
// coding agent's to do now, "operator" when it owes operator knowledge or
// authority (the response may end there — the operator frontier), "none"
// when the flow is terminal.
// control-law: turn-ends-only-at-the-operator-frontier
Actor NextActor `json:"next_actor"`
}

// PrescribedCommand is the exact next command that makes the oracle's lowest-cost
Expand Down Expand Up @@ -377,6 +450,7 @@ func nextControlFromStatus(repo string, status NextStatus) (FlowNext, error) {
out.FollowUp = followUp
}
out.Alternatives = alternativesFor(repo, status, out)
out.Actor = classifyNextActor(status, out)
return out, nil
}
out.State = state
Expand All @@ -402,6 +476,7 @@ func nextControlFromStatus(repo string, status NextStatus) (FlowNext, error) {
}
}
out.Alternatives = alternativesFor(repo, status, out)
out.Actor = classifyNextActor(status, out)
return out, nil
}

Expand All @@ -415,6 +490,9 @@ func FormatFlowNext(next FlowNext) string {
if next.Reason != "" {
fmt.Fprintf(&b, "Reason: %s\n", next.Reason)
}
if next.Actor != "" {
fmt.Fprintf(&b, "Next actor: %s\n", next.Actor)
}
if next.Resolved {
fmt.Fprintf(&b, "Flow state: %s -> goal %s\n", next.State, next.Goal)
fmt.Fprintf(&b, "Advisory (flow oracle): next %s, remaining cost %d\n", next.OracleNext, next.RemainingCost)
Expand Down
Loading
Loading