Skip to content

fix(executing-plans): ledger the test summary, not the trailing duration - #2348

Open
Navaneethp007 wants to merge 1 commit into
obra:devfrom
Navaneethp007:fix-task-done-tap-summary
Open

Navaneethp007 wants to merge 1 commit into
obra:devfrom
Navaneethp007:fix-task-done-tap-summary

Conversation

@Navaneethp007

@Navaneethp007 Navaneethp007 commented Sep 19, 2026 •

Copy link
Copy Markdown

Who is submitting this PR? (required)

Field Value
Your model + version Claude Opus 5 (1M context), claude-opus-5[1m]
Harness + version Claude Code 2.1.278
All plugins installed superpowers@claude-plugins-official 6.3.0, skill-creator@claude-plugins-official, frontend-design@claude-plugins-official. No other plugins installed.
Human partner who reviewed this diff @Navaneethp007 — reviewed the complete diff before submission

What problem are you trying to solve?

Fixes #2342, reported by @koreankop with logs from a real executing-plans session.

scripts/task-done fills the ledger's result field with the last non-blank line of the test log. For TAP output the last line is the duration. task-done always redirects the test command to a file, and node --test emits TAP when stdout is not a TTY, so every completion line in such a run records the runtime instead of a result:

ledger: Task 1: complete (commits 0937a72..0937a72, tests: node --test test/slugify.test.js → # duration_ms 117.7202)

This is not a correctness bug — task-done gates on exit status, so a failing run still records nothing. The cost is that the field stops being evidence. SKILL.md tells the executor to trust the ledger over its own recollection after compaction, and documents this field as → 1/1 pass (line 343). With node --test it cannot distinguish a passing run from any other run, and it silently hides shortfalls: a run of 2 passing plus 1 skipped looked identical to a clean run.

I did not hit this in my own work first. I reproduced it by following the reporter's steps on a throwaway repo (Windows 11 / Git Bash, Node v22.14.0) and got the output above, consistent with their report.

What does this PR change?

task-done now reads the run's own TAP summary counts (# tests / # pass) and records them as <passed>/<total> pass, the shape SKILL.md already documents. Runners that print no TAP summary fall back to the previous last-line behaviour. The exit-status gate is untouched. Adds a regression test to tests/claude-code/test-executing-plans-scripts.sh.

Relationship to #2388, which landed on the same line

#2388 changed the exact line this PR replaces, adding an empty-log guard so grep exiting 1 under set -euo pipefail can no longer abort task-done and drop the ledger line entirely:

last=$(grep -v '^[[:space:]]*$' "$log" | tail -n 1) || last="(no output)"

This PR is rebased onto that and deliberately carries the guard onto the fallback branch:

else
  last=$(grep -v '^[[:space:]]*$' "$log" | tail -n 1) || last="(no output)"
fi

That is load-bearing, not cosmetic. Resolving the conflict the obvious way — keeping this PR's version of the line — silently reintroduces #2385, and none of this PR's own assertions would catch it. I verified that directly; see the third row of the verification table below. The two fixes are complementary: #2388 stops the fallback from aborting, this one makes the TAP path report an actual result.

Is this change appropriate for the core library?

Yes. task-done is core executing-plans infrastructure that Superpowers ships, and the ledger contract it writes is core to how execution survives compaction. The change is not tied to a project, team, or third-party tool: it reads TAP, a cross-language test output format, and leaves every non-TAP runner byte-identical. It adds no dependency.

What alternatives did you consider?

  1. Record the exit status (→ exit 0; <last line>), as the issue suggested. I tried this first and it does not work: task-done returns early on a non-zero status, so rc is always 0 by the time the result field is built. It would record a constant, not evidence.
  2. Parse ok / not ok lines and count them. Rejected as redundant and more fragile — TAP already prints the totals, and counting result lines has to handle subtests, # SKIP and # TODO directives correctly to avoid being wrong in exactly the cases that matter.
  3. Special-case node --test. Rejected as too narrow. Keying on TAP means any TAP-emitting runner benefits, and nothing is node-specific.
  4. Let the caller pass a result extractor per runner. Rejected as scope creep: it changes task-done's interface and pushes work onto every caller to fix a default that can simply be correct. Worth revisiting only if TAP parsing proves insufficient.
  5. Emit pass 4 fail 0. Rejected in favour of 4/4 pass to match the documented format.

Does this PR contain multiple unrelated changes?

No. One problem, one behaviour change, plus the regression test covering it.

Existing PRs

Environment tested

Harness (e.g. Claude Code, Cursor) Harness version Model Model version/ID
Claude Code 2.1.278 Claude Opus 5 (1M context) claude-opus-5[1m]

Shell/OS for the runs: Windows 11, Git Bash (scripts ran under bash via their shebang), Node v22.14.0. I have not tested on macOS or Linux; the reporter's original session was macOS 26.6.2 / zsh.

New harness support

Not applicable — this PR does not add support for a new harness.

Evaluation

Behaviour of the result field, measured on a throwaway repo:

case before after
node --test, 4 passing # duration_ms 117.7202 4/4 pass
non-TAP runner (plain echo output) 12 examples, 0 failures unchanged
empty output (#2385 path) (no output) (no output) — unchanged
failing tests not recorded, non-zero exit not recorded, non-zero exit
2 passing + 1 skipped # duration_ms … 2/3 pass

The skipped case is the most useful one: 2/3 pass surfaces a shortfall the duration line hid entirely.

Each assertion was checked against the code it is meant to guard, so that none of them can pass vacuously:

run task-done used this PR's 2 assertions #2388's 2 assertions
as submitted this PR PASS PASS
our fix removed current dev FAIL PASS
guard dropped from the fallback this PR minus || last="(no output)" PASS FAIL (rc=1)

The third row is why the guard is called out above: the naive conflict resolution passes every assertion this PR adds while re-breaking #2385, and only #2388's tests catch it.

Full suite is green, 14/14, with no pre-existing failures. (An earlier revision of this PR disclosed one unrelated Git Bash failure in task-start prints the brief path; 49b1db5 fixed it upstream, so that no longer applies.) The sibling tests/claude-code/test-sdd-workspace.sh also passes.

The regression test feeds a TAP fixture through printf, so it adds no Node dependency and runs wherever the suite runs.

Rigor

  • If this is a skills change: I used superpowers:writing-skills and completed adversarial pressure testing — not applicable: this is a script + test change, no skill content modified
  • This change was tested adversarially, not just on the happy path — verified the non-TAP fallback, the empty-output path, the failing-command gate, a skipped-test shortfall, and the three-way assertion matrix above
  • I did not modify carefully-tuned content (Red Flags table, rationalizations, "human partner" language) without extensive evals showing the change is an improvement — none of that content is touched

Human review

  • A human has reviewed the COMPLETE proposed diff before submission

@obra obra added the skill:executing-plans The executing-plans skill label Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skill:executing-plans The executing-plans skill

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants