Skip to content

fix(skills): stop headless runs halting at phase boundaries in Opus 5 - #1250

Draft
kieranklaassen wants to merge 14 commits into
mainfrom
lite-skills
Draft

fix(skills): stop headless runs halting at phase boundaries in Opus 5#1250
kieranklaassen wants to merge 14 commits into
mainfrom
lite-skills

Conversation

@kieranklaassen

Copy link
Copy Markdown
Collaborator

Summary

Autonomous /lfg runs stopped partway through. The model would finish a phase, correctly name the remaining steps, and end its turn without doing them. Its closing lines were things like "Returning control to LFG" and "waiting on those before composing" - addressed to a caller that does not exist, because phase transitions load instructions into the same conversation rather than spawning anything.

Eight halts are fixed here. All eight reduce to one error: prose written as if a second party were waiting. A caller to hand back to, helpers to wait for, a next step someone else begins, a consumer who picks up after "done", a recipient for a Return:, a user who installs a missing tool, someone who fixes broken credentials - and, once the mandates were cut too far, nobody to say the phases are required at all.

The corpus also drops from 386,034 to 305,115 words (-21%), with 55 unattended defaults added where skills previously asked a question with no fallback.

Why the diff is mostly deletions

The failing mechanism was documentation of a runtime that had stopped existing. A Skill tool call does not spawn a subprocess: it loads instructions inline and the same turn continues. A failing run's own log shows two Skill calls and ten subagent calls under a single session id, so helper dispatch is a real boundary and the phase seam is not. Under claude -p, an assistant message with no tool call ends the session, which leaves "return a summary to the caller" exactly one expressible form.

# what the prose did what it assumed existed
1 mandated helper dispatch, then waited helpers the harness had already terminated
2 returned control across a phase seam a caller that is the same model
3 ended a step with no instruction to advance someone who starts the next one
4 defined completion and stopped there a consumer who picks up after "done"
5 labeled its output envelope Return: a recipient for the return
6 told the user to install a missing tool, then stopped a user at the keyboard
7 told the agent to resolve broken credentials first someone who can fix credentials
8 left phase invocation to the model's judgment (my own over-cut, see below)

Design decisions worth reviewing

Field names are data; prose is not. Every envelope field, status enum, and greppable terminal marker survives untouched, because three test suites and a live gate depend on them. What went is the justification clause around them. Documentation complete stays as a literal string; "so callers can detect completion" does not.

88 stop sites were deliberately kept. Sorted by one question: who is actually on the other side. Kept when the user is the subject, when nothing remains for anyone to do, when the roles split across a real runtime boundary, when the site is already an unattended default, or when "stop" bounds an activity rather than the turn. ce-explain's check-in pause and ce-brainstorm's one-question interview are the product, not ceremony; they gained an unattended degradation path instead of a deletion.

Removing the dispatch floors caused a regression, and the last commit fixes it. Cutting "you must delegate" was well-supported: vendor guidance says so, and 331 of 332 archived runs never dispatched more than one helper concurrently, which falsifies the wall-clock justification the corpus wrote for its own machinery. But it left nothing saying the pipeline's own phase invocations are mandatory, and a run then shipped a task inline while skipping ce-plan and ce-work entirely, reporting success with no plan artifact. 4cf86bfe draws the line: a skill decides its own internal delegation, but whether a step runs at all is not its call.

Two shipped documents changed, not just skills. docs/solutions/skill-design/portable-agent-skill-authoring.md gains capable-model-first as a conditional deviation, because the guide's own rule says a recurring exception belongs in the guide rather than in 535 local carve-outs - and because this repo has a prior learning concluding the opposite, which deserves to be contradicted explicitly rather than by implication.

Validation

Measured on a probe brief that traverses every phase boundary, same model and effort throughout.

before after
pipeline completed 7 of 12 8 of 8
p under the old rate - 0.0128 (1 in 78)
output-token spread, same task 7.12x 2.00x
halt signatures in final message present none

The before column is an A/A control: two identical copies of the pre-change plugin, which completed 7 of 12 with output tokens ranging 21,872 to 155,682. That spread is why the sample size was fixed in advance at 8 runs rather than chosen after seeing results, and why earlier small-sample claims about this pipeline were unreliable.

Separately, a run against a repository with a configured remote exercised the shipping branch that no prior run had taken: the branch pushed, an impossible gh pr create was handled with a reasoned skip rather than a halt, and both the plan artifact and the residual-findings record were written. A live pull request with real CI is still unexercised.

bun run test 2582 pass / 0 fail, release:validate and plugin:validate --strict clean. Two test retargets, each net-adding an assertion; no test deleted or weakened.

Test plan

  • bun run test, bun run release:validate, bun run plugin:validate
  • New: tests/no-unreleased-model-names.test.ts fails if a pre-release model identifier reaches this public repo. It matches the shape of such an identifier rather than a list of names, since a test enumerating them would publish what it protects, and reports file:line only because CI logs are public too.

Compound Engineering

kieranklaassen and others added 13 commits July 23, 2026 14:57
…tion ceremony

Under headless runs the pipeline halted partway: skills asked blocking questions
with no documented fallback, and hand-off prose addressed a "caller" that, inline,
is the same model. Measured over 367 archived benchmark runs plus 12 controlled
runs on unchanged code: /lfg pipeline adherence sits at 58% while the underlying
task completes 75%, and the models that fail do so at one seam 47-58% of runs
where succeeding models fail there 0-8%.

Adds 55 inline unattended defaults across 22 skills so an unattended run takes a
documented default, records the assumption, and continues instead of stopping.
Removes self-verification passes over generated output, dispatch floors that
mandated subagent fan-out regardless of warrant, no-skip-on-cost clauses, stale
inventory, and per-skill boilerplate.

Two supporting facts drove the removals. Current frontier models self-verify, so
mandated verification passes cost tokens without improving results. And of 332
archived runs dispatching 5+ helpers, 331 never dispatched more than one
concurrently, which falsifies the wall-clock justification this corpus wrote for
its own mandated fan-out.

386,034 -> 304,764 words (-21.1%), 221 -> 190 markdown files. Every proposed cut
was adversarially defended against docs/solutions learnings and tests first; 81
of 616 were skipped as load-bearing. Machine-readable contracts other skills
grep, platform gotchas where wrong behavior looks like success, and genuinely
interactive product behavior were held intact.

Deliberately excluded: the hand-off/turn-boundary rewrite, which needs one
complete pass with a test-retargeting budget.

This is not yet a measured performance result. Mechanical gates green means
nothing broke, not that adherence improved; against a 58% floor with 7.1x token
variance that claim needs ~39 runs per arm.

Co-Authored-By: Claude <noreply@anthropic.com>
…repo

Pre-release model ids are under NDA until launch, and the realistic leak path is
transcription: measuring a change against an unreleased model, then pasting the
resulting table, benchmark row, or config snippet into docs/.

Matches the shape of an early-access identifier rather than a list of codenames,
because a test enumerating them would publish the very thing it protects. The
forbidden suffix is assembled from fragments so the file does not contain it, and
failures report file:line only, never the matched text, since CI logs are public
too. Released model ids are deliberately not matched.

Verified both directions: catches three-part ids, env assignments, pasted
benchmark rows and the bare acronym, with no false positives on "cheapest",
"reap", "heap", "leap", "release-please", or any public model id.

Co-Authored-By: Claude <noreply@anthropic.com>
…e regression

A new model performed worse driving this plugin than with no plugin at all. The
obvious response, reading the skills and rewriting the bad sentences, produces a
plausible fix list with no way to tell whether any item matters.

Records the measurement path: mine the existing run archive before spending new
runs, exclude broken runs as a first-class outcome (20% of ours scored as model
failures and falsified our first headline), keep "followed the process" and "did
the job" as separate metrics, localize the defect with a model split rather than
by reading prose, and measure the A/A noise floor before claiming any effect.

Includes the hypotheses this killed, which matter more than the confirmations:
mandated fan-out does not cause the halts (the correlation runs the other way and
is confounded by progress), the monotone effort effect was an artifact of broken
runs, and roughly half of an outside analyst's paste-ready fix list did not apply
to the current tree.

Model identities are omitted; what transfers is the shape of the result.

Co-Authored-By: Claude <noreply@anthropic.com>
…sh state

Three artifacts had been sitting untracked: a nightly-plugin requirements
brainstorm, an explainer-topics ideation page, and an evidence-based reduction
plan derived from transcript analysis. All three are durable analysis in the
conventional docs/ paths, so they belong in the repo rather than in a working
tree indefinitely.

Also ignores .herenow/, which holds published-site URLs written by the here-now
skill. That is local tooling state, not repo content.

Scanned all three for pre-release model identifiers and credentials before
committing, since committing makes them public. Clean.

Co-Authored-By: Claude <noreply@anthropic.com>
…tion

The guide says to test whether a redundant-looking line is insurance for a more
literal model before removing it, and to update the guide rather than multiply
local carve-outs when the same justified exception recurs. A corpus-wide reduction
took that exception 535 times across 31 skills, so it belongs here.

Two gaps this closes. The existing instruction assumes the test exists; sometimes
it cannot be run at all, because the benchmark harness refuses to load a modified
skill on the weaker runtime, which makes the insurance hypothesis unfalsifiable
with the available lever. And the evaluation priority list leads with the weakest
realistic layer, which is the wrong order when the measured harm is on the
strongest runtime available.

Adds the resolution order for an untestable insurance hypothesis, the three
conditions a capable-model-first program must meet, the priority inversion with
its precondition, and a requirement to establish a noise floor from two identical
copies before crediting any change.

Also names the reversal explicitly: this repo has a prior learning concluding a
defensive fix should ship even when a capable model cannot reproduce the failure,
because weaker harnesses are targets too. A capable-model-first program reverses
that reasoning and should say so rather than let a large diff imply it.

Net addition to a guide used in a reduction program, deliberately: this is a
named exception with a measurement precondition, which is protocol, not ceremony.

Co-Authored-By: Claude <noreply@anthropic.com>
A matched pair on a full brief isolated this. The pre-cut arm died at ce-plan
waiting on helpers the harness had terminated. The post-cut arm cleared that
phase, wrote a 2,034-line plan, ran document review, then ended its turn with
"Returning control to LFG" after 105 minutes and 617k output tokens.

Phase transitions do not spawn anything. The Skill tool loads instructions into
the same conversation: one session id, one model, one turn. The failing run shows
two Skill calls and ten Agent calls in a single session file, so helper dispatch
is a real boundary and the phase seam is not. Every "return control to the
caller" is written about the seam that is not a call, which makes it a message
the model addresses to itself, and a message with no tool call ends the session.

Rewrites 53 sites in ten phrase families: return-control-to-a-party, ownership
disclaimers across a same-model seam, envelope-as-return (verb swap only, every
field name and status enum untouched), model-subject end-the-turn where work
remains, output-format-as-stop, the filler-tool-call guard, terminal-sentinel
justifications, and three families inside subagent prompt assets.

Those last three matter more than their count suggests. Assets saying "report and
stop" or "return only a gist" become live session terminators the moment a
documented no-subagent fallback inlines them into the orchestrator, which several
skills do. Fixed only in skills that document such a path.

88 sites verified as legitimate survivors across five screens: the user is the
subject of the stop, the stop is a terminal no-op with no work left, the roles
split across a real runtime boundary, the site is already an unattended default
from the previous pass, or "stop" bounds an activity rather than the turn.
Genuinely interactive stops are intact and one gained a missing unattended
degradation path.

Greppable markers keep their literal strings and lose the justification that
taught the model a separate consumer was waiting. review.json already carries the
machine-readable payload, so the reply is no longer declared the only channel.

Two test retargets, both net-adding an assertion; no test deleted or weakened.

Co-Authored-By: Claude <noreply@anthropic.com>
The pipeline had no instruction to advance. Steps 2 to 3, 3 to 4, 4 to 5, 5 to 6,
6 to 7, 7 to 8 and 8 to 9 each simply ended: the model finished a step and nothing
told it to begin the next. Every "proceed" in the file was negative ("Do NOT
proceed to step 2"), conditional, or jumped straight to step 10. Two phase gates
were also headed "GATE: STOP.", which reads as an instruction to halt rather than
to verify.

That combination is enough on its own to end a headless run, independent of the
hand-off framing removed in the previous commit: reaching a boundary with no
stated next move leaves ending the turn as the only reading.

Adds an explicit "continue to step N" at each transition, replaces both gate
headers with checkpoint wording, and states once up front that a gate means
verify and continue rather than stop and wait. No test pinned either gate string.

Found by prediction rather than by another benchmark run: the two prior halts both
occurred at a phase boundary, so the boundaries themselves were audited instead of
waiting three hours to observe the next one.

Co-Authored-By: Claude <noreply@anthropic.com>
…arve-out inline

Re-reading the failing trace corrected the diagnosis. It recorded
`ce-plan > ce-doc-review`, which looked like two pipeline phases completing — but
`ce-doc-review` is invoked by `ce-plan` as its own sub-step 5.3.8, so the run never
reached the pipeline's second stage. It stopped on the way out of the first.

Three defects, all the same shape: a stop instruction separated from the condition
that exempts it.

The non-interactive contract defined completion and stopped there: "`ce-plan` is
done once the plan file is written, the confidence check has run, and
`ce-doc-review` has run headless." Being done was never connected to what happens
next, and "I am done" reads as the end of the turn.

The pre-response check told the agent to render the handoff menu and wait, two
paragraphs after the sentence that exempts pipeline runs from it. A more literal
reader loses a distant qualifier, so the exemption is now stated at the action it
governs rather than earlier in the file.

Source-document disambiguation asked the user and waited with no unattended path.
It now takes the most recently modified match and records that as an explicit
assumption in the plan.

Co-Authored-By: Claude <noreply@anthropic.com>
The Return-to-Caller envelope was introduced by the bare label "Return:", the last
instance of that verb left in the skill that historically absorbs most pipeline
failures. Its prose had already been corrected to "records ... the remaining gates
run later in the same session", so the heading contradicted the paragraph above it.

Now reads "Record these fields, then continue with the run's next stage", which
supplies the forward motion the label never had. Field names and the status enum
are untouched; the surrounding contract test slices on section boundaries and does
not pin the label, so no test change was needed.

Co-Authored-By: Claude <noreply@anthropic.com>
…skipped step

Two sites stopped the whole run over a step that merely could not execute, and both
were invisible to every test run so far: the probe used for verification has no
browser surface, so this skill never fires in it.

The driver bootstrap told the user to install `agent-browser` and stop. The skill
already declares itself unattended in `mode:pipeline` ("never block on a question"),
and its dev-server check already carves out pipeline mode — this path did not. A
missing optional CLI now records every planned route as Skip, reports PARTIAL, and
continues.

Same fix in the pipeline orchestration reference, where a dev server that never
bound to its port also stopped the run.

Both use vocabulary the skill already had: unreachable flows are logged as Skip
with a reason and the run keeps going, with an overall PARTIAL result.

This matters more than the two lines suggest. Most benchmark briefs are web apps,
so any machine without `agent-browser` — or a dev server slow to bind — would have
halted at the browser step while the browserless probe stayed green.

Left alone: four nearby "report and stop" sites that are genuine terminal no-ops
(no commits to describe, no open PR, no feature-branch work). Nothing remains for
anyone to do there, so continuing would be the defect.

Co-Authored-By: Claude <noreply@anthropic.com>
…resolved

The pre-create PR re-check told the agent to resolve `gh auth status` or
connectivity before creating the PR. In an unattended run there is nobody to fix
either, and no fallback was stated, so the instruction had no reachable outcome.

Now reports the failure and skips PR creation — the commits and push already stand
— then continues with the run's next stage. The sibling rule two lines down already
says never to strand the flow between commit and PR; this applies the same
principle to the case where gh itself is the thing that failed.

Co-Authored-By: Claude <noreply@anthropic.com>
A remote-backed run completed the task, pushed a branch, handled an impossible PR
creation gracefully, emitted DONE, and produced no halt signature. It also never
invoked ce-plan or ce-work. Its own summary: "I ran planning and implementation
myself rather than delegating ... the delegation overhead bought nothing. That's a
deviation from the LFG script."

So the task shipped while the plan artifact, the review, and the verification
evidence the later steps read did not exist, and the run reported success anyway.

This is a regression from removing the dispatch floors. That removal was right for
what it targeted: prose mandating helper fan-out inside a skill regardless of
warrant, contradicted by 331 of 332 archived runs never dispatching more than one
helper concurrently. But it left nothing saying that the pipeline's own phase
invocations are mandatory, and the model reasonably filled that silence with its
own judgment about overhead.

Now states once, up front, that each step's invocation is mandatory unless that
step documents a skip condition (3, 6, 7 and 9 do), that task size is not a skip
condition anywhere else, and that a skill decides its own internal delegation while
whether the step runs at all is not its call. A genuinely uninvokable skill must be
named and the run continued, never silently replaced by an inline substitute.

Found by the two-metric split: task completion and pipeline adherence are tracked
separately, so a run that did the work without the process reads as a defect rather
than a pass.

Co-Authored-By: Claude <noreply@anthropic.com>
Four commits landed on main during this work and collided with it in 70 hunks
across 31 files. The overlap was almost entirely one feature: configurable
docs_root replaced hardcoded `docs/` paths with a `<root>/` placeholder, in
exactly the prose this branch had compressed.

Resolved by keeping both intents rather than choosing a side: the branch's
compressed, de-halted wording carrying main's `<root>/` placeholder. Verified
after resolution that the branch's purpose survived: zero conflict markers, the
halt-prose sweep back to its pre-merge count of one (a pre-existing mode:agent
contract table cell, outside every conflict region), lfg's nine `continue to
step N` connectives intact, six `<root>/` references ported in, and zero stale
`docs/residual-review-findings` paths left behind.

Also drops three analysis artifacts committed earlier in the branch that did not
belong in the repo, leaving that commit's `.gitignore` entry for `.herenow/` as
its only surviving content. Keeps deleted the three persona files this branch
replaced with ce-pov's grounding checklist, which main had only touched to apply
the docs_root rename.

Co-Authored-By: Claude <noreply@anthropic.com>
@kieranklaassen

Copy link
Copy Markdown
Collaborator Author

Testing a few hours locally

@kieranklaassen kieranklaassen changed the title fix(skills): stop headless runs halting at phase boundaries fix(skills): stop headless runs halting at phase boundaries in Opus 5 Jul 24, 2026
Five commits landed on main since the last merge, including the ce-retune skill
and a fix that probes for a working Python interpreter instead of hardcoding
python3. All four conflicts were the same shape: the interpreter-probe fix landed
inside command blocks this branch had also edited. Resolved by taking main's side
in every hunk — it is a genuine cross-platform fix, and none of it is turn-ending
prose (its exit-1 guards bound a command, not the run).

The halt-prose sweep now returns three hits, all inside
ce-retune/references/halt-taxonomy.md, which documents those exact strings as
grep patterns for finding the regression class — detection instructions, not
live ones. The pre-merge baseline of one (a mode:agent contract table cell in
ce-code-review) is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
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