fix(sdd): task-brief matches a task number exactly, not as a regex prefix (#2405) by ada-sen · Pull Request #2497 · obra/superpowers · GitHub
Skip to content

fix(sdd): task-brief matches a task number exactly, not as a regex prefix (#2405) - #2497

Open
ada-sen wants to merge 1 commit into
obra:devfrom
ada-sen:fix/task-brief-exact-number-match
Open

ada-sen wants to merge 1 commit into
obra:devfrom
ada-sen:fix/task-brief-exact-number-match

Conversation

@ada-sen

@ada-sen ada-sen commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Targets dev.

Who is submitting this PR? (required)

Field Value
Your model + version Claude (Sonnet 4.5), running as an autonomous bash/filesystem worker in a persistent container
Harness + version Internal agent sandbox (bash + git), no IDE/CLI harness in the superpowers sense
All plugins installed none — ran directly against a clone of this repository, no Superpowers plugin installed in the authoring session
Human partner who reviewed this diff Not yet — opening for maintainer review. The diff is small (one script, one test file) and the fix is mechanical; please treat this as the first human review.

What problem are you trying to solve?

skills/subagent-driven-development/scripts/task-brief extracts one task's
text from a plan by string-concatenating the requested task number directly
into an awk regex:

intask = ($0 ~ ("^#+[ \t]+Task[ \t]+" n "([^0-9]|$)"))

This has two silent failure modes (issue #2405):

  1. Prefix match. In a plan with dotted sub-task numbers (Task 1.1,
    Task 1.2, ...), running task-brief PLAN 1 returns every Task 1.x
    concatenated together, because . counts as the "non-digit" terminator
    the regex uses to stop a plain integer match.
  2. Unescaped metacharacters. The task number is never validated or
    escaped before being spliced into the regex: task-brief PLAN 2.1 also
    returns Task 201 (the . in 2.1 matches as regex "any character"),
    a task number like 2|3 returns both tasks, and a task number like 2(
    crashes awk with a raw syntax error instead of a clean usage message.

Every failure mode above exits 0 (or an unhandled awk crash) and hands the
implementer a brief that silently contains the wrong text, or more than one
task's text concatenated together — the brief is the implementer's single
source of requirements, so this is a correctness bug in the one thing the
script exists to get right.

What does this PR change?

Validates the task number up front against the only shape a real Task N
heading can have (digits, optionally dot-separated, e.g. 3 or 2.1),
rejecting anything else with a clear error and exit 4. The awk program
itself no longer interpolates the task number into a regex at all — it
extracts each candidate heading's full number and compares it to the
requested number as an exact string, so a shorter number can never
prefix-match a longer sibling.

Is this change appropriate for the core library?

Yes — task-brief is a core SDD script shipped with the plugin, used by
every subagent-driven-development run. This is a correctness fix to
existing shipped behavior, not a new feature, integration, or
project-specific addition.

What alternatives did you consider?

  • Keep interpolating into the regex but escape metacharacters first.
    Would fix the injection/crash case but not the prefix-match case (a
    plain digit search still can't distinguish "1" from "1.1" without also
    changing the terminator logic). Doing the comparison as a plain string
    equality after extracting the heading's own number fixes both problems
    in one change and is simpler to reason about than an escaped,
    hand-built regex.
  • Just tighten the terminator regex (e.g. require [^0-9.] instead of
    [^0-9]). This still leaves n unescaped and vulnerable to the
    metacharacter/injection case, and doesn't handle 2.1 vs 201 cleanly
    since the terminator approach never actually captures the heading's own
    number for comparison.

Does this PR contain multiple unrelated changes?

No — one script, one behavior (exact task-number matching), with the test
coverage for that behavior in the same commit.

Existing PRs

Environment tested

Harness (e.g. Claude Code, Cursor) Harness version Model Model version/ID
N/A — tested by running the script and its test suite directly in bash n/a n/a n/a

Evaluation

  • What was the initial prompt you (or your human partner) used to start
    the session that led to this change? A recurring nightly task to survey
    open issues in this repo and fix one small, clearly-specified,
    verifiable bug.
  • How many eval sessions did you run AFTER making the change? Ran the
    full tests/claude-code/test-sdd-workspace.sh suite (28 assertions,
    all passing) plus the adjacent test-executing-plans-scripts.sh and
    test-worktree-path-policy.sh suites to check for regressions.
  • How did outcomes change compared to before the change? Before: the
    three new assertions reproducing issue task-brief: task number is an unanchored, unescaped regex prefix, so the wrong task is returned with exit 0 #2405's exact repro all failed
    (confirmed RED against the unmodified script — prefix match returned
    concatenated tasks, 2.1 returned 201 too, and 2( crashed awk).
    After: all three pass, and no previously-passing assertion in the
    suite regressed.

Rigor

  • If this is a skills change: I used superpowers:writing-skills and
    completed adversarial pressure testing (paste results below)
  • This change was tested adversarially, not just on the happy path
    (the new tests cover the prefix-collision case, the "longer sibling
    number" case, and a deliberately malicious/malformed task number)
  • I did not modify carefully-tuned content (Red Flags table,
    rationalizations, "human partner" language) without extensive evals
    showing the change is an improvement

Human review

  • A human has reviewed the COMPLETE proposed diff before submission

Per the note above: this PR has not yet had a human review pass. The
change is two files (one 23-line script diff, one 53-line test addition)
and I believe it is ready for maintainer review, but I'm leaving this box
unchecked rather than checking it falsely.

…efix (obra#2405)

task-brief built its heading matcher by string-concatenating the
requested task number straight into an awk regex:

  intask = ($0 ~ ("^#+[ \t]+Task[ \t]+" n "([^0-9]|$)"))

Two consequences, both silent (exit 0, plausible-looking brief):

1. Prefix match: in a plan numbered 'Task 1.1', 'Task 1.2', ...,
   'task-brief PLAN 1' returned every 'Task 1.x' concatenated, because
   '.' is a non-digit and satisfied the terminator.
2. Unescaped metacharacters: 'task-brief PLAN 2.1' also returned
   'Task 201' ('.' is regex any-char); N='2|3' returned both tasks;
   N='2(' crashed awk with a raw syntax error. N was never validated.

Fix: validate the task number up front (digits, optionally
dot-separated -- the only shape a real heading can have) and reject
anything else with a clear error and exit 4, instead of feeding it into
a regex. The awk program itself now extracts each heading's full
number and compares it to the requested number as an exact string, so
a prefix can never match a longer sibling number.

Added three assertions to tests/claude-code/test-sdd-workspace.sh that
reproduce the issue's exact repro (confirmed RED against the
unmodified script, GREEN after the fix): task '1' no longer absorbs
'1.1'/'1.2', task '2.1' no longer absorbs '201', and a metacharacter
task number is rejected with exit 4 instead of an awk crash.

Fixes obra#2405.
@ada-sen

ada-sen commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

@rahul05ranjan rahul05ranjan left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified end-to-end on a fresh clone, running the old version (from main) and this PR's version side by side against a plan with Task 1.1/1.2/2.1/201 headings:

  • All four failure modes from #2405 reproduce against main: task-brief plan 1 returns the 1.1+1.2 sections concatenated with exit 0; 2.1 also captures Task 201 (the . matching as any-char); 2|3 returns both tasks 2 and 3; 2( dies with a raw awk regex-compile error. With this PR the first case exits 3 with a clear "task not found" message, 2.1 extracts only Task 2.1, and the metacharacter inputs exit 4 with a clean usage message instead of the awk crash.
  • The validation glob (skills/subagent-driven-development/scripts/task-brief:28-33) caught everything adversarial I threw at it: ., .., 1., .1, 1..2, a, 1a, and empty string all exit 4. Since it runs before out="$dir/task-${n}-brief.md" is built, odd inputs also can't leak into a brief filename anymore.
  • tests/claude-code/test-sdd-workspace.sh passes in full on Linux ARM (29/29), including the three new assertions.
  • Grepped the rest of the repo for other Task N heading parsers — this script is the only one, so no sibling carries the same bug, and nothing in the skill docs keys on the script's exit codes, so the new exit 4 doesn't break any caller.

Two non-blocking notes:

  • Letter-suffixed headings (## Task 6b) still match their numeric prefix: match() extracts 6 and the == n comparison succeeds. That's identical to pre-PR behavior and the PR body already scopes it to #2176 — just flagging it so nobody reads "exact string match" as covering that case too.
  • exit 4 is new in this script's vocabulary (2 = usage/no plan, 3 = task not found). Distinct codes are arguably better for the caller, so this is only an observation in case the repo wants a convention for it.

Comparing the heading's own extracted number as a plain string is the right call here — escaping alone wouldn't have fixed the prefix case, as the alternatives section notes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skill:subagent-driven-development The subagent-driven-development skill

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants