Skip to content

Surface thinking-only model output clearly instead of returning empty te - #359

Merged
randomm merged 2 commits into
mainfrom
feature/issue-5-surface-thinking-only-model-output
Aug 5, 2026
Merged

Surface thinking-only model output clearly instead of returning empty te#359
randomm merged 2 commits into
mainfrom
feature/issue-5-surface-thinking-only-model-output

Conversation

@randomm

@randomm randomm commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Automated by pi-ensemble /work driver (mechanized commit-pr).

Fixes #5

@randomm
randomm merged commit fc061db into main Aug 5, 2026
1 check passed
@randomm
randomm deleted the feature/issue-5-surface-thinking-only-model-output branch August 5, 2026 23:21
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 6, 2026
…365)

* feat(#287): repoRoot is never a dev tree — always-worktree isolation

Development happened in the operator's own checkout. runBranch set
`worktrees = {default: repoRoot}` for N=1, the ops branch dispatch ran
fetch/checkout/pull against repoRoot, and mechanizedCommitPr staged in
place there. That single fact caused three separate failure classes:

  - stale repoRoot residue swept into a merged PR (incident #602);
  - an aborted step left a dirty tree, and because the branch step
    refused to run against one, that wedged EVERY downstream issue in
    the queue until a human intervened;
  - parallel groups were impossible — two cycles would fight over one
    checkout.

After this, repoRoot is an integration point only.

A. Mechanized branch setup (work-driver-branch-mechanized.ts). Resolves
   the base from origin/<mainline> rather than repoRoot's HEAD, names
   the branch deterministically, and creates one DETACHED worktree per
   workstream — including the degenerate N=1 `default`. `git fetch` is
   the only repoRoot command and it mutates refs, never the tree, so an
   operator's uncommitted work is both untouched and no longer able to
   block the cycle. The deterministic slug also removes the #358/#359
   precondition where one issue got two LLM-authored branch names.

B. integrate() (work-driver-integrate.ts) is now the ONLY writer to
   repoRoot: preflight clean → checkout -B <branch> <baseSha> → per
   worktree stage + `git diff --cached` → `git apply --index` → commit →
   push. Concentrating it here is what makes "no git at repoRoot between
   branch and commit-pr" a checkable property rather than a convention.
   #283's dirty-tree gate moves here, where it actually guards something.

C. Lens-fix re-integration. lens-fix runs in the worktree, so committing
   at repoRoot found a clean tree and silently skipped — the fix never
   reached the PR. Follow-up integration fixes that latent bug.

D. fetchIntegratedDiff reads origin/<base>..origin/<branch> at repoRoot.
   Worktrees stay detached at baseSha, so the old inside-the-worktree
   read would have returned empty on every cycle and skipped six-pass
   review — the same failure PR11 fixed once, from the other direction.

E. runMerged tears down the cycle's worktrees. worktreeRemove had been
   exported and never invoked, so worktrees accumulated indefinitely.

worktree.ts is rewritten: it had no importers, shelled out in
process.cwd(), always created a scratch branch, and returned relative
paths. It now takes the driver's ExecFn, so worktree setup is observable
through the same injection seam the verify gates use.

requireAllNonEmpty preserves an important existing behaviour I initially
regressed: if ANY workstream produced no diff, commit-pr refuses rather
than consolidating the rest. That is the v0.12.13 incident where 1 of 3
workstreams shipped and the issue closed with the root fix missing. The
existing M3 fixture caught it. Lens-fix opts out — a fix round
legitimately touches only the worktree that had findings.

mechanizeOpsEnabled moves to the integrate leaf so the branch step can
read it without closing an import cycle (#356 flags the same shape).

Escape hatch PI_ENSEMBLE_ALWAYS_WORKTREE=0 restores the prior behaviour;
the pre-existing mechanized-commit and skeleton fixtures now run under
it, which makes the whole legacy suite the standing proof that the hatch
works.

Refs #287

* fix(#287): real-git test catches two showstoppers; worktree-aware handoff

Adds test-work-driver-integrate-realgit.ts — the same flow against the
real git binary in a throwaway repo with a local bare origin. The mocked
sibling proves we ASK git the right things; it cannot prove git DOES the
right thing. It immediately found two bugs that would have made #287
broken end-to-end in production:

1. `.worktrees/` read as untracked residue at the repo root, so
   integrate()'s dirty-root preflight refused EVERY integration, forever.
   Fixed by writing `.worktrees/` to .git/info/exclude (per-clone, never
   committed — the convention AGENTS.md §7 already mandates for tmp/),
   plus a defensive filter in the preflight for when that write fails.

2. After integrating, the worktree kept its slice staged, so the next
   integration re-captured the same patch. That is exactly how a lens-fix
   round would have silently re-committed the pre-fix content. Worktrees
   now advance to the commit their work became; reset --hard is safe there
   precisely because everything porcelain listed was staged and applied a
   moment earlier, so the commit is a superset of the worktree state.

Also makes captureWorktreeSnapshot worktree-aware. The developer's
uncommitted work lives in the worktrees now, so snapshotting the repo root
alone reported "0 files modified" on exactly the handoffs where the
operator needs to know what survived. Paths are prefixed with the
workstream id when there is more than one.

Docs: README gains PI_ENSEMBLE_ALWAYS_WORKTREE and PI_ENSEMBLE_PR_PREFLIGHT;
work.md Step 2 no longer claims single-task cycles skip worktrees.

63/63 offline smoke tests, tsc + biome clean.

Refs #287
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.

Surface thinking-only model output clearly instead of returning empty text

1 participant