Skip to content

fix(work-driver): execute merge step + multi-issue /work with per-issue verdict routing - #245

Merged
randomm merged 2 commits into
mainfrom
fix/work-driver-merge-and-multi-issue
Jun 25, 2026
Merged

fix(work-driver): execute merge step + multi-issue /work with per-issue verdict routing#245
randomm merged 2 commits into
mainfrom
fix/work-driver-merge-and-multi-issue

Conversation

@randomm

@randomm randomm commented Jun 25, 2026

Copy link
Copy Markdown
Owner

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 with status="merged", but both PRs were still OPEN on GitHub. Pre-PR10 runMerged was purely a state-machine mutation; the doctrine at pi-prompts/work.md:277 declared the intent (merge per project merge policy) but nothing executed it.

This PR:

  • runMerged now dispatches ops with label="ops:merge" (mirrors runCommitPr's shape).
  • New inlineMergePrompt instructs ops to read the project's AGENTS.md / CONTRIBUTING.md for merge method, defaults to gh pr merge <PR-N> --squash --delete-branch, ends with merge-commit: <sha>.
  • parseMergeCommit lenient 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-hit step-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 563 now 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 subset
  • PipelineState.droppedIssues?: Array<{issue, verdict, reason}>

Threading:

  • commands.ts parses N tokens; DriverContext gains issues?: number[].
  • runExplore fetches N bodies in parallel; for N>1 calls parsePerIssueVerdicts → splits into active/dropped; all-dropped → existing PR6 aggregate cap-hit path.
  • activeIssuesOf(state) helper consumed by every step body (never reads state.issue directly).
  • inlineCommitPrPrompt emits one Fixes #N per active issue + Companion to #N for dropped.
  • Branch naming: feature/issues-<N1>-<N2>-<slug> for N>1.
  • Handoff renderers surface per-issue verdicts with reasons.

Test plan

  • tsc --noEmit clean
  • biome check clean
  • 26/26 offline smokes pass
  • PR10 §35–§42 cover both concerns end-to-end via injected dispatchFn mocks
  • Live /work N single-issue regression (existing nessie issue)
  • Live /work N M multi-issue happy path (2–3 related open nessie issues)
  • Live /work N M where one is already done — verify driver skips correctly
  • Live merge-failure case (induce by opening PR without required review)

Recovery 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.

randomm added 2 commits June 25, 2026 10:56
…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.
@randomm
randomm merged commit e27a8d4 into main Jun 25, 2026
1 check passed
@randomm
randomm deleted the fix/work-driver-merge-and-multi-issue branch June 25, 2026 09:34
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.
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.
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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant