fix(work-driver): execute merge step + multi-issue /work with per-issue verdict routing - #245
Merged
Merged
Conversation
…ue verdict routing
PR10 — two distinct concerns surfaced together by today's live testing
of /work on nessie. Folded into a single PR per user choice.
# Bug — step-9 merge was a silent no-op
Live evidence (/work 561 and /work 562 on nessie 2026-06-25): driver
reported MERGED ✓ and exited with status="merged", but both PRs were
still OPEN on GitHub. User had to manually `gh pr merge` for both.
The pre-PR10 runMerged was purely a state-machine mutation — flipped
status="merged" without dispatching anything. Doctrine at
pi-prompts/work.md:277 ("On green CI + APPROVED review: merge per
project merge policy") declared the intent; nothing executed it.
## Fix
- runMerged now dispatches ops with label="ops:merge" via
runSingleDispatch (same shape as runCommitPr).
- New inlineMergePrompt instructs ops to read project's AGENTS.md /
CONTRIBUTING.md for merge method, defaults to
`gh pr merge <PR-N> --squash --delete-branch`, and ends with
`merge-commit: <sha>` for driver capture.
- parseMergeCommit pure helper (lenient regex tolerates markdown
emphasis around the marker).
- STEP_FAILURE_POLICY[merged]: DEGRADED_OK → HALT. The merge step
CAN fail now (auth / branch protection / conflicts / missing
required review). Silently flipping status="merged" on failure
would be exactly the bug we're fixing. PR5's halt-cascade router
intercepts → cap-hit 'step-failed:merged' → handoff.
- New explainCap entry for 'step-failed:merged' with the
"merge manually" recovery hint.
- The merged event now carries an optional mergeCommit SHA.
# Feature — multi-issue /work
User: "it would be VERY useful to be able to give more than one issue
number to /work ... in the past I gave many and asked PM just to figure
it out and get the work done in a single PR".
Pre-PR1 legacy PM-driven /work accepted multiple issue numbers; the
compiled driver dropped this when it became single-issue-driven.
commands.ts took only the first token; everything downstream read
WorkState.issue: number.
## Design (per user AskUserQuestion choice)
`/work N M P` accepts multiple issue tokens. Explore returns structured
per-issue verdicts; the driver filters to the NEEDS_WORK subset and
proceeds with that, surfacing ALREADY_COMPLETE / NEEDS_CLARIFICATION
issues in the PR body + handoff comment.
## Schema (additive, no version bump)
- WorkState.issues?: number[] — all issues passed to /work. Implicit
fallback to [WorkState.issue] for back-compat with pre-PR10 state
files. Primary issue (WorkState.issue) remains the state-file
anchor.
- PipelineState.activeIssues?: number[] — NEEDS_WORK subset after
explore. Implicit fallback to [WorkState.issue].
- PipelineState.droppedIssues?: Array<{issue, verdict, reason}> —
ALREADY_COMPLETE / NEEDS_CLARIFICATION issues filtered out.
## Plumbing
- commands.ts: parses N tokens into issues[]; notify mentions all.
DriverContext gains optional `issues?: number[]`.
- runWorkDriver: persists ctx.issues to state.issues on first run
(not on resume — honour the saved list to avoid silently widening
scope on re-invocation).
- runExplore: fetches N issue bodies in parallel (Promise.allSettled
over `gh issue view N`); each cached as a claim-check artifact.
For N>1 calls parsePerIssueVerdicts → splits into activeIssues +
droppedIssues; if ALL dropped, synthesises aggregate cap-hit via
existing PR6 routing.
- activeIssuesOf(state) helper: precedence
`pipelineState.activeIssues → state.issues → [state.issue]`.
Step bodies use this, never `state.issue` directly.
- inlineExplorePrompt / inlinePlanPrompt / inlineBranchPrompt /
inlineCommitPrPrompt / inlineMergePrompt accept issues[]. Commit-pr
emits one `Fixes #N` line per active issue + a `Companion to #N`
line per dropped issue (PR body context).
- Branch naming: `feature/issues-<N1>-<N2>-<slug>` for N>1, existing
`feature/issue-<N>-<slug>` for N=1.
- renderHandoffUserMessage + renderHandoffMarkdown surface per-issue
verdicts with reasons for multi-issue cycles.
# Smoke tests (~420 LOC additions)
In test-work-driver.ts:
- §35: parsePerIssueVerdicts pure helper (3 verdicts, missing
per-issue fallback to overall, missing overall defaults to
NEEDS_WORK).
- §36: parseMergeCommit pure helper (plain marker, markdown
emphasis + backticks, multi-line, missing/undefined inputs).
- §37: runMerged real ops:merge dispatch on happy path; merged
event captures parsed mergeCommit SHA.
- §38: runMerged dispatch failure → cap-hit 'step-failed:merged' →
handoff (PR5 halt-cascade router fires for new HALT-class merged).
- §39: multi-issue all-NEEDS_WORK → activeIssues includes all,
droppedIssues empty, plan prompt threads all issue numbers.
- §40: multi-issue mixed verdict → activeIssues filtered to
NEEDS_WORK only; droppedIssues carries the rest with verdict +
reason; commit-pr prompt has Fixes for active only +
Companion-to for dropped.
- §41: multi-issue all-dropped → cap-hit 'explore-already-complete'
(aggregate); NO plan/develop dispatch.
- §42: renderHandoffUserMessage + renderHandoffMarkdown multi-issue
surfacing (header lists all, per-issue verdicts with reasons).
In test-command-flow.ts:
- /work 561 562 563 → notify mentions all 3 issues + primary
state-file path.
# Quality gate (all green per AGENTS.md §1)
* bunx tsc --noEmit: clean
* bun run check (biome): clean
* 26/26 offline smokes pass
# Docs
- README.md /work descriptor: mention multi-issue + auto-merge.
- AGENTS.md §7: note multi-issue + merge-step now executes.
- docs/troubleshooting.md: schema additions (issues / activeIssues
/ droppedIssues), new `step-failed:merged` cap entry.
# Explicitly NOT in scope (per AGENTS.md §6 minimalism)
- Merge-method auto-detection from AGENTS.md — ops reads it on
dispatch and decides (--squash vs --merge vs --rebase). Driver
doesn't try to parse merge policy.
- Issue grouping / auto-decomposition based on issue similarity —
plan handles workstream decomposition on the filtered active set.
- Per-issue cap-hits — existing PR8 fanout halts the whole cycle on
any failure. Per-issue retry is speculative.
- State file path change — primary issue stays the path anchor.
CI for PR #245 surfaced a pre-existing flake in test-runs: the graceful-skip branch only handles "dir absent" but not "dir exists with empty date subdirs". Fresh CI runners can have the directory auto-created (skill-setup paths, etc.) but no actual spawn has populated it with .json transcripts yet, so totalFiles === 0 trips the assertion. The "no live spawn evidence" case is the same condition the dir-absent branch already handles cleanly — extend the same exit- clean path to cover the empty-subdir case. Local-dev runs (which have real transcripts from earlier spawns) keep exercising the shape assertions unchanged. Not introduced by PR #245's functional changes; surfaced by the CI runner happening to land in the empty-but-present state on this PR's build.
Merged
7 tasks
randomm
added a commit
that referenced
this pull request
Jul 24, 2026
…vidence, not transcripts) (#270) Every quality gate before this PR was LLM judgment — adversarial + six lenses reading diffs and transcripts. Nothing driver-side ever EXECUTED anything until post-PR CI. Agents claim "done" and the driver trusted the claim; the #245/#253 silent-merge incidents were exactly this failure class (MAST: verification failures = 21.3% of multi-agent failures). Top-ranked item from the harness gap analysis — unanimous across operator-pain, competitive-gap, and reliability lenses. New machinery (all pure driver code, zero LLM tokens): - `verifyStepOutcome(ctx, state, step)` — the gate. - develop (runs when every branch claims success): at least one worktree has a real diff (porcelain, or commits ahead of baseSha); the project's verify command exits 0 in each changed worktree. - commit-pr (runs when the consolidation gate passes): commits exist ahead of origin/<base>; the parsed PR number resolves via `gh pr view`. Missing `pr:` marker triggers a `gh pr list --head` repair that ADOPTS the found number into pipelineState (pre-PR17 a missing marker silently degraded handoff/CI targeting). - `verifyCmdFor(repoRoot)` — verify-command discovery: `.pi/verify-cmd` file > package.json typecheck/test script with lockfile-detected runner (bun/pnpm/yarn/npm) > Cargo.toml → `cargo check --quiet` > none (diff-evidence-only mode, noted). - `PipelineState.baseSha` — recorded at branch step (git rev-parse HEAD right after branch creation); optional, no schema bump. - `PipelineState.verifyEvidence` — per-check failure evidence, rendered into the handoff body by explainCap. - New cap shape `verify-failed:<step>` → handoff. - `DriverContext.verifyExecFn` — test injection point (mirrors issueBodyFetcherFn). Env knobs: PI_ENSEMBLE_VERIFY=0 escape hatch; PI_ENSEMBLE_VERIFY_TIMEOUT_MS caps the verify command (default 10 min). Tests: 21 new assertions (discovery precedence, hollow-claim detection, verify-cmd failure evidence, PR adoption repair, explainCap rendering). The gate is disabled globally in test-work-driver.ts flow tests (fake tmp dirs aren't git repos); dedicated gate tests re-enable it with an injected executor. Verified: tsc + biome + all 26 offline smokes pass.
4 tasks
randomm
added a commit
that referenced
this pull request
Jul 25, 2026
…on, commit, push, PR creation (#274) Round-2 gap analysis item #2 (2/3 lenses convergent). Every worst-class incident in the harness's history — #245/#253 silent merges, v0.12.13 shipping 1-of-3 workstreams — was an LLM ops dispatch improvising fully-enumerable git/gh operations, and the ~22-fix permission/cd-chain friction class (vipune 55fca4bf) exists only because an LLM emits the shell. Deleting the failure source beats detecting its failures. `mechanizedCommitPr()` executes the recipe the PR14 prompt previously NARRATED to ops: 1. Ensure repoRoot is on the integration branch. 2. Per worktree: verify uncommitted work exists (clean worktree → bail to fallback, never ship a partial slice); stage porcelain paths explicitly (no `git add -A` — the #553 root-pollution doctrine); sibling worktrees: capture `git diff --cached` → `git apply --index` at repoRoot. Staging-before-capture is an improvement over the LLM recipe: untracked NEW files are now included (`git diff HEAD` alone silently missed them). 3. Templated commit: issue title from the cached body artifact, `Fixes #N` per active issue, `Companion to` per dropped issue, workstream summary for N>1. 4. Push; `gh pr create --body-file`; PR number parsed from the URL. Success emits the same step-started + dispatch-completed event shapes the dispatch path produces (role "driver", summary carrying `pr: <N>`), so parsePrNumber + verifyConsolidation + verifyStepOutcome run identically for both paths — the PR17/PR18 gates remain the unchanged correctness oracle. ANY mechanized failure emits a plumb-report with the reason and falls back to the LLM ops dispatch (unchanged from PR14) — judgmental recovery for the env variance the sandbox-era fixes warn about. Escape hatch: PI_ENSEMBLE_MECHANIZE_OPS=0 forces the LLM path. Tests: 12 new assertions across 4 full-driver integration tests — M1 happy path (3 worktrees consolidated 3-of-3, PR number parsed, LLM ops:commit-pr provably never dispatched — the mock throws on it), M2 apply-conflict fallback, M3 empty-worktree guard, M4 escape hatch. Verified: tsc + biome + all 26 offline smokes pass.
This was referenced Aug 2, 2026
randomm
added a commit
that referenced
this pull request
Aug 6, 2026
… issue (#363) The driver never asked whether an issue already had a PR. runBranch did no lookup, and mechanizedCommitPr calls `gh pr create` unconditionally, so `--restart` — which wipes the state file but not GitHub — made the driver treat an in-flight issue as greenfield. Live evidence, issue #5 on 2026-08-05: PR #358 was open when `/work 5 --restart` ran. The driver picked a near-identical slug, rebuilt the whole issue, and merged it as #359. #358 is orphaned (its issue is closed, so it can never auto-close), a full cycle was paid for twice, and the two implementations had diverged — #359 guards `toolUses.length === 0`, #358 does not. Merge order decided the winner. The load-bearing detail is that the second cycle chose a DIFFERENT branch name, so a branch-scoped lookup would have missed it. The idempotency key has to be the issue number: a closing keyword in the PR body (the driver writes `Fixes #N` for every active issue), falling back to the head branch naming the issue for human-authored PRs. Both reject a numeric continuation so #5 does not match #55. Halts rather than adopts. Attaching new commits to a PR whose head is a different branch is the false-MERGED class (#245/#253), and choosing between resume / retarget / close is judgment — so per §7 this is a cap-hit with a structured handoff, not a question. Adoption needs mechanized branch setup and belongs to #287. The check runs BEFORE the ops dispatch, so a duplicate cycle costs zero tokens, and fails OPEN on any gh or parse error: the cost of missing a duplicate is one wasted cycle, the cost of a false halt is every cycle. explainCap and both handoff renderers get a case for the new cap, so it does not fall into the `else` branch that advises raising a spawn timeout. Also corrects the stale claim at work-driver-context.ts:177 that branch-step existing-branch detection already existed. Escape hatch: PI_ENSEMBLE_PR_PREFLIGHT=0. Fixes #362
This was referenced Aug 7, 2026
randomm
added a commit
that referenced
this pull request
Aug 7, 2026
pi-ensemble kept three escape hatches that did not disable a check — they
resurrected a superseded ARCHITECTURE. Each restored the exact shape that
caused a documented incident, and each was dead weight every future change
had to keep working.
PI_ENSEMBLE_WORK_DRIVER=0 the PM-driven /work prose flow: no state
file, no verification gates, no structured
cap-hits. The class the driver replaced.
PI_ENSEMBLE_ALWAYS_WORKTREE=0 worktrees = {default: repoRoot} for N=1 —
#287's incident, stale repoRoot residue
swept into a merged PR.
PI_ENSEMBLE_MECHANIZE_OPS=0 LLM ops narrating git/gh instead of
executing it — #245/#253 silent merges,
and v0.12.13 shipping 1 of 3 workstreams.
AGENTS.md already stated the principle: every worst-class incident in this
harness's history was LLM ops improvising these operations, and deleting
the failure source beats detecting its failures. These knobs kept the
failure source alive.
`pi-prompts/work.md` (296 lines) existed ONLY to serve WORK_DRIVER=0. It
documented a step sequence that no longer matched the driver, so it was
stale documentation that read as authoritative. Deleted, along with the 18
comments across src/ that cited it as a live spec — extension/src/
work-driver.ts is now the definition of a cycle rather than a translation
of one.
Also removed: `fetchMergedDiff` / `fetchAllMergedDiffs` (80 lines). Under
always-worktree the worktrees stay DETACHED at baseSha, so reading
`git diff origin/<base>..HEAD` from inside one is empty BY CONSTRUCTION —
that fallback could only ever return "nothing to review", which is exactly
the inference #384 established must never be drawn from an absent answer.
Operator-facing recovery no longer points at deleted paths: the crash
message, the schema-mismatch message and troubleshooting §C all named
WORK_DRIVER=0 as the way out.
The LLM fallback ON MECHANIZED FAILURE stays everywhere it existed. That
is recovery from environment variance, not an opt-out, and a plumb-report
still records every occurrence.
Tests: every fixture that set one of these knobs is deleted or rewritten
to assert the now-unconditional behaviour, not left asserting a branch
that can no longer be taken.
- M4 (mechanized-commit) DELETED — it tested WORK_DRIVER's sibling knob;
the path it covered does not exist. M2/M3 still cover the fallback.
- the mechanizedMerge bypass test now asserts the OPPOSITE: mechanization
is always attempted and no env knob can switch it off.
- merged-flow T19 reaches the LLM fallback the way production does —
mechanization genuinely failing — instead of by flipping a knob.
- skeleton fixture B asserted "no plumb-report when git fails"; that was
only true because mechanization was switched off. A fallback SHOULD
report, and it now asserts the report names mechanization.
- M1/M2 stubs rekeyed to driver-chosen worktree paths.
- command-flow: /work queues no prompt message and pi-prompts/work.md is
no longer expected to exist.
AGENTS.md: the driver section was a single 20,900-character run-on, and my
own rebase conflict resolutions had duplicated blocks inside it (#384 3x,
#279 3x, #386 2x, the WORK_DRIVER=0 sentence 3x). Rewritten as nine
readable subsections with every topic appearing exactly once.
Closes #393
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two distinct concerns surfaced together by today's live testing of /work on nessie. User chose to fold them into one PR.
Bug fix — step-9 merge was a silent no-op
Live evidence (/work 561 and /work 562 on nessie 2026-06-25): driver reported
MERGED ✓and exited withstatus="merged", but both PRs were still OPEN on GitHub. Pre-PR10runMergedwas purely a state-machine mutation; the doctrine atpi-prompts/work.md:277declared the intent (merge per project merge policy) but nothing executed it.This PR:
runMergednow dispatchesopswithlabel="ops:merge"(mirrorsrunCommitPr's shape).inlineMergePromptinstructs ops to read the project'sAGENTS.md/CONTRIBUTING.mdfor merge method, defaults togh pr merge <PR-N> --squash --delete-branch, ends withmerge-commit: <sha>.parseMergeCommitlenient helper extracts the SHA marker.STEP_FAILURE_POLICY[merged]: DEGRADED_OK → HALT. On failure (auth / branch protection / conflicts / missing review), PR5's halt-cascade router fires → cap-hitstep-failed:merged→ handoff with operator recovery hint.Feature — multi-issue /work
User: "VERY useful to be able to give more than one issue number to /work ... in the past I gave many and asked PM just to figure it out and get the work done in a single PR".
/work 561 562 563now bundles into a single PR. Explore returns structured per-issue verdicts; the driver filters to NEEDS_WORK only, surfacing ALREADY_COMPLETE / NEEDS_CLARIFICATION issues in the PR body + handoff (per user's structured-verdict-routing choice via AskUserQuestion).Schema additions (additive, no version bump):
WorkState.issues?: number[]— all issues passed (fallback[issue])PipelineState.activeIssues?: number[]— NEEDS_WORK subsetPipelineState.droppedIssues?: Array<{issue, verdict, reason}>Threading:
commands.tsparses N tokens;DriverContextgainsissues?: number[].runExplorefetches N bodies in parallel; for N>1 callsparsePerIssueVerdicts→ splits into active/dropped; all-dropped → existing PR6 aggregate cap-hit path.activeIssuesOf(state)helper consumed by every step body (never readsstate.issuedirectly).inlineCommitPrPromptemits oneFixes #Nper active issue +Companion to #Nfor dropped.feature/issues-<N1>-<N2>-<slug>for N>1.Test plan
dispatchFnmocksRecovery for pre-v0.12.9 PRs left unmerged: just
gh pr merge <N> --squash --delete-branch— those cycles were always green; the driver just didn't push the button.