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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,10 @@ branch, and `DEVKIT_STATE_ROOT` state sandbox. The rule that makes it safe is
edited by both (the sandbox prevents *state* collisions, not *source* merge conflicts).

The flow: `parallel plan` clusters candidate work by footprint → launch a lane per
disjoint cluster (`scripts/dev_session.sh new`) → each lane works to a draft-green PR →
the cockpit reconciles every lane and merges. Each lane gets an effort tier and a merge
class (self-merge vs operator-merge) assigned at plan time.
disjoint cluster (`scripts/dev_session.sh new … --merge-class self|operator`) → each
lane works to a draft-green PR → the cockpit reconciles every lane and completes the
recorded merge path. A self-merge must go through `scripts/dev_session.sh merge`; an
operator-merge remains an explicit cockpit decision.

Full walkthrough — the lane contract, the live board, reconciliation, and a worked
example — in **[`docs/parallel-dev.md`](docs/parallel-dev.md)**. For step-by-step
Expand Down
81 changes: 55 additions & 26 deletions docs/agentic-dev-kit/workflows/parallel.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ deliberately:
not at merge time, stops a batch stalling on ad-hoc "can I merge this?" calls and
tells each lane whether it may self-merge or must hand back.

1. **Launch each + relay a kickoff.** Run `<engine-dir>/dev_session.sh new <scope>` per
chosen ticket (see below) and relay each copy-paste line **with a kickoff prompt**
1. **Launch each + relay a kickoff.** Run `<engine-dir>/dev_session.sh new <scope>
--merge-class <self|operator>` per chosen ticket (see below) and relay each copy-paste line **with a kickoff prompt**
the operator pastes as the session's first message:

> Read tracker ticket `<ID>` (+ any recipe in `<handoff>`). Pre-flight its
Expand Down Expand Up @@ -156,16 +156,24 @@ decision (and so an autonomous batch knows what it may close itself):
| **Mechanical** / **Standard** | **self-merge** *(autonomous/headless batches only)* | the lane (or cockpit), once green-and-clean with one independent review pass |
| **High-stakes** | **operator-merge** | always the operator — never self-merged, even when green |

Operator-merge is the floor for anything in your project's high-risk classes:
The class is persisted in the session metadata and headless JSON descriptor;
missing/unknown metadata defaults to **operator**. Operator-merge is the floor for anything in your project's high-risk classes:
data-shape / fetcher / config-semantics changes, shared primitives, a **guard / gate /
verifier / send-path / kill-path** (see
`docs/agentic-dev-kit/safety-critical-changes.md`), security, PII,
or anything touching production cron/CI or shared `state/`. Normal **interactive**
sessions leave *all* merges to the operator regardless of class — self-merge is an
autonomous-session behavior (see
the configured autonomous-session playbook (`paths.playbook`).
autonomous-session behavior (see the configured autonomous-session playbook at
`paths.playbook`).
When unsure, classify **operator-merge**.

For an autonomous self-merge, run `<engine-dir>/dev_session.sh merge <scope>`.
That deterministic wrapper re-polls `pr-watch` at act time, requires current-head
independent-review evidence, validates the exact repository/branch/base, and pins the
merge to the reviewed head commit. It refuses any lane whose persisted class is not
`self`. Operator-class lanes intentionally cannot use the wrapper; the operator lands
those directly after the required review and sign-off.

**How the tier reaches the lane.** It depends on the launch mechanism:

- **Headless lanes — fan out through a multi-agent workflow when it exposes a real
Expand Down Expand Up @@ -257,7 +265,7 @@ in-agent workflow cannot `cd` the operator into a new worktree and open a fresh
terminal there). When asked to prepare one, run:

```bash
<engine-dir>/dev_session.sh new <scope>
<engine-dir>/dev_session.sh new <scope> --merge-class <self|operator>
```

substituting a lowercase slug for `<scope>` (e.g. `feat-graduation-flow`). Pass
Expand All @@ -266,9 +274,11 @@ override it for this lane. The script
prints a copy-paste line — `cd <worktree> && export DEVKIT_STATE_ROOT=… && export
DEVKIT_ROOT=… && <your agent CLI>`. **Relay that line to the operator** and tell them
to run it in a new terminal; don't try to start the session yourself. Options:
`--base <branch>` (default `main`), `--prefix <p>` (default `dev` — parallel-session
`--base <branch>` (default `vcs.protected_branch`), `--prefix <p>` (default
`vcs.dev_branch_prefix` — parallel-session
branches get their own namespace to avoid colliding with hand-named feature branches),
`--branch <full>` to override the whole name.
`--branch <full>` to override the whole name. Omitting `--merge-class` fails safe to
`operator`.

### Unattended / headless launch — `new --headless`

Expand All @@ -278,7 +288,7 @@ rule above says *don't start the session yourself*). That's the wrong shape for
*sandboxed* lane without a human in the loop. `--headless` is for exactly that:

```bash
<engine-dir>/dev_session.sh new --headless <scope>
<engine-dir>/dev_session.sh new --headless <scope> --merge-class <self|operator>
```

It creates the worktree + sandbox exactly as `new` does, but instead of the human
Expand All @@ -294,10 +304,13 @@ block it:
it's unaffected.)
1. **Prints a JSON descriptor to stdout** (diagnostics go to stderr, so stdout is
clean JSON): `{"scope","branch","worktree","state_root","repo_root","base",
"prompt_preamble","env","runtime","launcher"}`. `prompt_preamble` is the canonical lane-contract text
"merge_class","prompt_preamble","env","runtime","launcher"}`. `prompt_preamble` is the canonical lane-contract text
below — the launcher **MUST** prepend it verbatim to the lane's task prompt. `env`
(currently `{"DEVKIT_REFUSE_UNSANDBOXED_STATE": "1"}`) flips the unsandboxed-write
guard from *warn* to *refuse*, by default, for every headless lane — so a lane
carries lane-specific `DEVKIT_STATE_ROOT`, `DEVKIT_ROOT`, and
`DEVKIT_REFUSE_UNSANDBOXED_STATE=1`. The launcher **MUST replace inherited values
with this map**: the resolver gives an explicit env root precedence over the marker,
so inheriting the cockpit's root would collapse every child lane into one sandbox.
The refusal flag flips the unsandboxed-write guard from *warn* to *refuse* — so a lane
whose marker resolution somehow fails (deleted marker, cwd escaped the worktree)
hard-errors on a `state/` write instead of silently landing in prod. Interactive
`new` and cron/CI never set either field.
Expand All @@ -324,45 +337,43 @@ every lane prompt**; runtime capability changes how tiers are applied, not wheth
the safety contract binds.

**Preferred when available — a workflow launcher with a real effort dial.** Run
`new --headless <scope>` once per chosen scope, collect each one's JSON descriptor
`new --headless <scope> --merge-class <class>` once per chosen scope, collect each one's JSON descriptor
into a list (attaching the per-lane `effort`/`model` tier from the plan's risk read),
then drive the lanes from a *single* fan-out that gives each sub-agent its own
`{effort, model}` — the one path on which the tier's `effort` half actually takes
effect. Pseudocode:

```js
// args.lanes = [{scope, worktree, branch, ticket, effort, model, prompt_preamble}, …]
// args.lanes = [{scope, worktree, branch, ticket, effort, model, merge_class, prompt_preamble, env}, …]
// — one per `new --headless` descriptor (prompt_preamble copied straight off it).
// effort ∈ low|medium|high|max (omit ⇒ inherit cockpit effort); model ∈ cheap|default|expensive (omit ⇒ inherit).
runInParallel(args.lanes.map(lane => () =>
spawnAgent(
`${lane.prompt_preamble}\n\n` +
`Work in worktree ${lane.worktree} on branch ${lane.branch} (cd there first — its state sandbox is active via the on-disk marker, so your state/ writes isolate automatically). ` +
`Read tracker ticket ${lane.ticket}, pre-flight its premise against the live code, implement, draft PR on first push, drive it to green-and-clean, then hand off per the contract above.`,
{ label: lane.scope, effort: lane.effort, model: lane.model } // omit effort/model to inherit (default-safe)
{ label: lane.scope, effort: lane.effort, model: lane.model, env: lane.env }
)
))
```

Four things to keep right: **(1)** `lane.prompt_preamble` is prepended verbatim,
Five things to keep right: **(1)** `lane.prompt_preamble` is prepended verbatim,
ahead of everything else, on every lane — never abbreviated to "follow the usual
contract" (that's exactly the prose-reference that failed to bind a lane before).
**(2)** do **not** open a second worktree on top of `--headless` — it already owns the
worktree+sandbox, so a second one would have no marker and lose isolation. **(3)** A
lane with no assigned tier omits `effort`/`model` and inherits the cockpit's — the
same default-safe fallback as everywhere else. **(4)** Check what compute budget your
same default-safe fallback as everywhere else. **(4)** replace the spawned process's
environment roots with `lane.env`; do not merge them over inherited cockpit roots.
**(5)** Check what compute budget your
fan-out mechanism draws from before running a large batch, and monitor it via
whatever live-progress view your runtime exposes, plus `list --watch` on the lanes'
branches/PRs.

Note on the `env` field: if your fan-out or background-agent tool doesn't expose a
parameter to inject env vars into the spawned lane's process, `DEVKIT_REFUSE_
UNSANDBOXED_STATE` cannot be force-set through that launch path the way
`prompt_preamble` can be force-injected into the prompt text. In the normal case this
is moot: the marker already resolves the sandbox, so the guard never fires. The `env`
field exists for launchers that DO control the spawned process's environment (a
custom subprocess-based batch driver) — read it and export it there. Treat this as a
documented, conservative gap, not a silent one.
The `env` field is mandatory for unattended launches. If a fan-out/background-agent
tool cannot replace the spawned process's environment, do not use it for a headless
state-writing lane; use an env-capable subprocess/fresh terminal or keep the work
attended. Prompt injection cannot override an inherited `DEVKIT_STATE_ROOT`.

**Fallback — a single background sub-agent per lane (model-only).** Parse the
descriptor and spawn a background sub-agent whose prompt is **the `prompt_preamble`
Expand Down Expand Up @@ -392,7 +403,25 @@ interactive paths are unaffected.

## Finishing a session

After its PR has merged:
From the cockpit, drive a lane's review loop through the scope-aware wrapper so its
seen-set and receipt land in the same sandbox the merge gate reads:

```bash
<engine-dir>/dev_session.sh pr-watch <scope> --json
<engine-dir>/dev_session.sh pr-watch <scope> --mark-seen
<engine-dir>/dev_session.sh pr-watch <scope> --record-review <source> --head <polled-sha>
```

The receipt command uses the `head` from the exact poll whose diff was independently
reviewed and refuses if a push landed before recording. For a `self` lane, once this
scope-aware `pr-watch` has converged, merge through the deterministic class gate:

```bash
<engine-dir>/dev_session.sh merge <scope>
```

For an `operator` lane, the wrapper refuses; the operator performs the reviewed,
signed-off merge directly. After its PR has merged:

```bash
<engine-dir>/dev_session.sh rm <scope>
Expand Down
53 changes: 44 additions & 9 deletions docs/agentic-dev-kit/workflows/pr-watch.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,23 @@ Engine: `<engine-dir>/pr_watch.py` (deterministic — check rollup + comment uni
issue/review/inline surfaces, noise-filtered, diffed against a per-PR seen-set). You
drive the loop + apply the judgment.

For a lane coordinated from the cockpit, invoke the same engine through
`<engine-dir>/dev_session.sh pr-watch <scope> ...`. That scope-aware wrapper pins the
repository and stores polls, acknowledgments, and review receipts in the lane sandbox
that `dev_session.sh merge <scope>` re-checks.

## Loop

Repeat until the report says **done**:

1. **Poll.** `uv run <engine-dir>/pr_watch.py <PR#> --json` (omit `<PR#>` for the current
branch). Read `done`, `checks` (`all_green`, `failing[]`, `pending`), and
`new_comments[]`.
branch). Read `done`, `checks` (`all_green`, `failing[]`, `pending`),
`merge_blockers[]`, `review_evidence`, and `new_comments[]`.

1. **If `done` (checks all green + nothing new):** stop the loop and report — PR #,
the green check count, and "no outstanding review findings." You're finished.
1. **If `done` (checks all green + nothing new + PR open/ready/mergeable with no
requested changes + independent review evidence bound to the current head):**
stop the loop and report — PR #, the green check count, review source, and "no
outstanding review findings." You're finished.

1. **If checks are still `pending` and there are no new comments:** nothing to do yet
— wait and re-poll (see Pacing). CI can take 20–30 min; that's expected, keep
Expand All @@ -42,6 +49,16 @@ Repeat until the report says **done**:

1. **If there are `new_comments`:** handle each with judgment —

- **Reviewer unavailable** (`review_unavailable_reason` is set — rate limit,
skipped review, no credits): run the current runtime's configured
`review.fallback_commands` pass. A blocked bot is an action signal, never
auto-noise or a review waiver. Acknowledge the notice only after the fallback
review has completed and every finding from it is handled. Then record the
pass against the exact `head` from the poll you reviewed with `uv run
<engine-dir>/pr_watch.py <PR#> --record-review "fallback:<runtime>" --head
<polled-sha>`. For a lane, use `<engine-dir>/dev_session.sh pr-watch <scope>
--record-review "fallback:<runtime>" --head <polled-sha>` instead.

- **Real finding** (a bug, a missing guard, a correctness/clarity issue): fix it in
the code, commit, push. Re-running the local gate first.
- **Nitpick you disagree with** (style preference, out-of-scope, already-correct):
Expand All @@ -62,6 +79,15 @@ Repeat until the report says **done**:
prior poll (nothing pending) acks nothing and says so (`note` in the output) —
always poll-and-read first.

1. **Record the independent pass:** run `--record-review <source> --head <polled-sha>`
only after the configured bot, human, or fallback reviewer has completed and all
findings are resolved. `<polled-sha>` is the `head` field from the exact poll whose
diff was reviewed. Recording fails if the PR head changed in the meantime. The
receipt is persisted with that exact `headRefOid`; any later push invalidates it and
requires another independent pass. A platform `APPROVED` state is still recorded
explicitly so the engine never assumes that an approval predates no later push.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Marking comments seen never creates review evidence.

1. **Pace the next poll** (see below), then go to step 1.

## Pacing
Expand All @@ -74,10 +100,17 @@ Self-pace on a bounded cadence — don't busy-wait:
is fine.
- After you push a fix, expect a fresh CI run + possibly a re-review — keep looping;
don't declare done off a stale poll.
- A transient `merge state is UNKNOWN` blocker is expected immediately after GitHub
receives new state; re-poll until it resolves. `BLOCKED`, `DIRTY`, `BEHIND`, a
draft bit, or `CHANGES_REQUESTED` needs action rather than acknowledgment.
- `UNSTABLE` remains blocking unless every real check is green and its only remaining
status contexts are names explicitly classified as informational by the engine. A
current-head independent-review receipt is still required in that case.

## Stop conditions

- **Done** — `done: true` (green + clean). Report and finish. This is the goal.
- **Done** — `done: true` (green + clean + current-head independent review evidence).
Report and finish. This is the goal.
- **Stuck / needs a decision** — a check fails for a reason you can't resolve (a
flaky-infra failure that won't clear on re-run; an external dependency; a finding
that needs an operator product/design call). Stop, report the specific blocker, and
Expand All @@ -89,10 +122,12 @@ Self-pace on a bounded cadence — don't busy-wait:

- The seen-set lives at `state/pr-watch/<PR#>.json` (gitignored). It's per-PR, so
re-running on a different PR starts fresh.
- Known auto-noise from your review bots (billing/usage notices, walkthrough / "no
actionable comments" summaries) is filtered out by the engine — edit the
noise-marker list in `<engine-dir>/pr_watch.py` for your own bot mix; those never count
as findings.
- Known auto-noise from your review bots (walkthrough / "no actionable comments"
summaries) is filtered out by the engine. Reviewer-unavailable notices are
deliberately *not* noise: they surface and block `done`; acknowledging one still
leaves the current-head review-evidence blocker until the configured fallback runs
and records its receipt. Edit the marker lists in `<engine-dir>/pr_watch.py` for
your own bot mix.
- This is interactive-only. A scheduled job that opens its own PRs should be excluded
from this loop by your cron/CI runner's job-name signal, so an automated open never
silently enters an unattended watch loop.
14 changes: 11 additions & 3 deletions docs/autonomous-session-playbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,21 @@ Follow this top to bottom, per ticket.
bot's last review rather than waiting indefinitely for a re-review that may not come.
Fix real findings (commit + push → re-watch); reply-with-reason to nitpicks you keep. If
every configured bot is unavailable (rate-limited, no credits, skipped the PR), run
the current runtime's `review.fallback_commands` value as the substitute independent pass — a blocked bot is not a
waiver on review.
the current runtime's `review.fallback_commands` value as the substitute independent
pass, handle its findings, then bind that evidence to the current head with
`uv run <engine-dir>/pr_watch.py <PR#> --record-review "fallback:<runtime>" --head
<polled-sha>` (use the `head` from the exact reviewed poll; a lane uses the
scope-aware `dev_session.sh pr-watch` wrapper) — a blocked bot is not a waiver on
review.

### Merge + close out

- Merge only once checks are green, the PR is mergeable, and every finding has been fixed
or explicitly addressed:
or explicitly addressed. A lane created by `dev_session.sh` must honor its persisted
merge class at act time: `self` lanes merge through
`<engine-dir>/dev_session.sh merge <scope>` (which re-polls `pr-watch`); missing,
unknown, or `operator` metadata refuses autonomous merge and requires operator
sign-off. For non-lane/operator merges:
```sh
gh pr merge <PR#> --squash --delete-branch
git checkout <protected_branch> && git pull --ff-only origin <protected_branch>
Comment on lines +87 to 90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Pin direct terminal merges to the validated head.

This command can merge a push that lands after the final review poll. Require an immediate done: true poll and pass its head through --match-head-commit.

Proposed fix
-gh pr merge <PR#> --squash --delete-branch
+gh pr merge <PR#> --squash --delete-branch --match-head-commit <validated-head>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sign-off. For non-lane/operator merges:
```sh
gh pr merge <PR#> --squash --delete-branch
git checkout <protected_branch> && git pull --ff-only origin <protected_branch>
sign-off. For non-lane/operator merges:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/autonomous-session-playbook.md` around lines 87 - 90, Update the
non-lane/operator merge instructions to require an immediate poll confirming
done: true, capture its validated head commit, and pass that head to gh pr merge
via --match-head-commit; preserve the existing squash and branch-deletion
options.

Expand Down
Loading
Loading