Skip to content

fix(tasks): count indented sub-tasks so a change with unfinished work isn't reported complete - #1486

Merged
clay-good merged 2 commits into
mainfrom
fix/parser-encoding-tolerance
Jul 29, 2026
Merged

fix(tasks): count indented sub-tasks so a change with unfinished work isn't reported complete#1486
clay-good merged 2 commits into
mainfrom
fix/parser-encoding-tolerance

Conversation

@clay-good

@clay-good clay-good commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Status: Ready. Closes #1485. One commit. Full suite green (3405 tests, 116 files), lint and typecheck clean, verified end to end and differentially against a build of main.

What was wrong

A tasks.md with indented sub-tasks reported the wrong progress: only checkboxes at column 0 were counted. A change whose sub-tasks were unfinished showed as done and archived without a warning.

## 1. Implementation
- [x] 1.1 Parent task
  - [ ] 1.1.1 Nested sub-task not done
  - [ ] 1.1.2 Another nested sub-task
- [x] 1.2 Second parent
before after
openspec list ✓ Complete 2/4 tasks
openspec archive --json (no --yes) archived it archive_tasks_incomplete: 2 incomplete task(s)
openspec instructions apply listed 2 tasks lists all 4

The same blind spot hid the remaining work from agents resuming a change, since the apply task list is where they read what is left.

How it was fixed

Two regexes anchored at column 0 — src/utils/task-progress.ts for counting, src/commands/workflow/instructions.ts for the apply task list — are replaced by one shared parseTaskLines() that allows leading whitespace. Both surfaces now read the same lines of the same file, and after this change there is exactly one checkbox regex left in src/.

Why it can't take work away from a user

The new pattern matches a superset of what both old patterns matched — every part of it was relaxed, none tightened (^^\s*, \s+\s*, [ xX][\sxX], (.+)$(.*)). Keeping \s inside the brackets preserves the old counting pattern's tolerance for a tab or non-breaking space there.

So task counts never fall, which pins down the surfaces that matter:

  • no change can flip from "N tasks" to "No tasks" in list/view;
  • archive's incomplete-task gate can only become stricter, never silently vanish;
  • the completed count is monotonic too, so progress can never overstate how done a change is.

Evidence:

  • A line-level differential fuzz over millions of inputs (exhaustive over the structural alphabet, plus random strings with \r, NBSP, ideographic space and BOM) found zero cases where an old pattern matched and the new one did not, and zero description differences.
  • A file-level differential against a build of main over hand-built fixtures (nested, CRLF, BOM, bare checkbox, HTML comment, indented code block, fenced examples, stray unterminated fence, a 50k-line file) and the repo's own 120 tasks.md files: 0 files count fewer tasks, 0 lose an incomplete-task warning. 5 of the 120 count more, all of them genuine sub-tasks that were invisible.

The one cost, stated plainly

Checkboxes are counted wherever they appear — inside a code fence, an HTML comment, or an indented block. That was already true on main for lines at column 0; allowing indentation extends it to indented ones. So a tasks.md that shows a checklist as a format example can now count that example as work and make archive ask for --yes.

Fence-awareness was built to avoid this and then deliberately dropped. Every rule for deciding which fence is "real" has an input where a stray or unbalanced ``` swallows genuine tasks: the parity-based guard I first wrote let a mid-file unterminated fence be "closed" by the next fence opener, hiding every task between them — ✓ Complete with two unfinished tasks, on a file `main` handled correctly. Counting a documented example as work is a loud, bypassable false positive; losing a real task is a silent false negative that disables exactly the gate this PR exists to restore. The `parseTaskLines` docblock records that reasoning so the next person does not retry it.

Proof

  • test/utils/task-progress.test.ts — sub-tasks at every depth, CRLF and trimming, bare checkboxes, a guard asserting every shape the old patterns accepted still counts, and the fenced/unterminated-fence behavior pinned as the documented limitation.
  • test/commands/apply-instructions-tasks.test.ts — apply lists sub-tasks, agrees with getTaskProgressForChange, and skips a checkbox carrying no text.
  • test/core/archive.test.ts — the gate warns on unfinished sub-tasks (#1485), and prompts then cancels when the user declines.
  • test/core/list.test.ts / test/core/view.test.ts — no ✓ Complete, and the change stays in Active.

Reverting ^\s* to ^ fails 10 tests across all five files.

Notes

  • A checkbox with no text after it is left out of the apply list — there is nothing for an agent to act on or tick off — but it still counts toward apply's progress numbers, which are taken from every parsed line. So apply, list and archive always agree on how much work is left. The displayed rows also decide apply's "nothing to work on" state, so a file of nothing but text-less checkboxes now asks to be rewritten instead of reporting itself done. main listed such a line when it happened to carry trailing whitespace, an artifact of (.+) backtracking; that row is now consistently hidden. Covered by tests asserting state and progress, not just the row list.
  • This PR unifies the line parsing across the two surfaces, not their file resolution: apply reads one tracked file while progress globs the tracked-tasks artifact.
  • The apply task list renders every task flush-left, so a newly included sub-task loses its indentation there. Harmless for the X.Y.Z numbering the template prescribes; worth a follow-up if unnumbered task files become common.
  • Three agent-facing templates (archive-change.ts, bulk-archive-change.ts, verify-change.ts) still tell agents to count - [ ] vs - [x] themselves and say nothing about sub-tasks. Worth a follow-up; it needs a golden-hash regen, so it is out of scope here. schemas/spec-driven/schema.yaml is untouched, so no generated skills or hashes move.
  • Related, not closed by this: [Feature suggest] Openspec task tracking is wishy washy and could stand to be implemented as a command #220 (asks for task tracking as a CLI command) and Strenghten linting support: validate unchecked tasks in the archive directory #205 (unchecked tasks in archived changes). Feedback: openspec view does not detect nested tasks.md files #1202 fixed the sibling case of nested tasks.md files.

🤖 Generated with Claude Code

@clay-good
clay-good requested a review from a team as a code owner July 29, 2026 17:05
@clay-good
clay-good requested review from alfred-openspec and removed request for a team July 29, 2026 17:05
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Task checkbox parsing is centralized in parseTaskLines, providing consistent handling of indented subtasks across progress calculation, apply instructions, list, view, and archive validation. Tests cover nested tasks, empty descriptions, whitespace, CRLF, and fenced-content cases.

Changes

Task parsing and progress

Layer / File(s) Summary
Shared parser and progress counting
src/utils/task-progress.ts, test/utils/task-progress.test.ts
Adds structured checkbox parsing, counts indented tasks, and validates parsing and edge-case behavior.
Apply instructions integration
src/commands/workflow/instructions.ts, test/commands/apply-instructions-tasks.test.ts
Uses shared parsed tasks for apply instructions, skips empty descriptions, and verifies progress consistency.
Command status validation
test/core/list.test.ts, test/core/view.test.ts, test/core/archive.test.ts, .changeset/count-indented-subtasks.md
Verifies nested-task status and archive warnings, and documents the updated checkbox behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant tasks.md
  participant parseTaskLines
  participant countTasksFromContent
  participant generateApplyInstructions
  tasks.md->>parseTaskLines: provide task content
  parseTaskLines-->>countTasksFromContent: return ordered ParsedTask entries
  parseTaskLines-->>generateApplyInstructions: return parsed tasks
  countTasksFromContent-->>generateApplyInstructions: provide consistent progress counts
Loading

Possibly related PRs

Suggested reviewers: alfred-openspec, tabishb, showms

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR counts indented sub-tasks in progress, archive, and apply instructions, matching the requirements in #1485.
Out of Scope Changes check ✅ Passed The extra parser edge-case coverage stays within the same task-parsing refactor and doesn't introduce unrelated features.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: counting indented sub-tasks so incomplete work is no longer reported as complete.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/parser-encoding-tolerance

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Clean fix. Sharing one indentation-aware parser across progress, archive gating, and apply instructions removes the silent-completion mismatch; the focused tests and exact-head build pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/core/archive.test.ts`:
- Around line 257-261: Align both archive warning tests in
test/core/archive.test.ts:257-261 and test/core/archive.test.ts:309-313 with the
contract by removing { yes: true } when asserting warnings, or instead assert
that --yes suppresses them. Apply the same correction to the unterminated-fence
case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 15a32424-0c67-4843-b276-01f1ce24aae5

📥 Commits

Reviewing files that changed from the base of the PR and between 2ef8412 and c605353.

📒 Files selected for processing (10)
  • .changeset/count-indented-subtasks.md
  • src/commands/workflow/instructions.ts
  • src/core/parsers/code-fence.ts
  • src/utils/task-progress.ts
  • test/commands/apply-instructions-tasks.test.ts
  • test/core/archive.test.ts
  • test/core/list.test.ts
  • test/core/parsers/code-fence.test.ts
  • test/core/view.test.ts
  • test/utils/task-progress.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/utils/task-progress.ts
  • test/utils/task-progress.test.ts
  • src/commands/workflow/instructions.ts

Comment thread test/core/archive.test.ts
Comment on lines +257 to +261
await archiveCommand.execute(changeName, { yes: true });

expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('Warning: 2 incomplete task(s) found')
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align --yes expectations with the stated archive contract.

The PR objective says incomplete-task warnings occur unless --yes is used, but both tests pass { yes: true } and require a warning. This locks in the opposite behavior.

  • test/core/archive.test.ts#L257-L261: exercise the warning path without yes, or assert that yes bypasses the warning.
  • test/core/archive.test.ts#L309-L313: apply the same correction for the unterminated-fence case.
📍 Affects 1 file
  • test/core/archive.test.ts#L257-L261 (this comment)
  • test/core/archive.test.ts#L309-L313
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/archive.test.ts` around lines 257 - 261, Align both archive warning
tests in test/core/archive.test.ts:257-261 and test/core/archive.test.ts:309-313
with the contract by removing { yes: true } when asserting warnings, or instead
assert that --yes suppresses them. Apply the same correction to the
unterminated-fence case.

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The code and focused suite look clean, and CodeRabbit's warning-test comment is not valid because --yes explicitly logs the warning before continuing. One blocking wording fix remains: the changeset and stale PR body say every old match is preserved and task totals can never fall, but closed-fence checkboxes are now intentionally excluded; please correct that claim and note that apply still omits bare checkboxes before re-review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/count-indented-subtasks.md:
- Around line 5-7: Update the “Both surfaces now share one parser” wording in
the changeset to refer to all affected commands, such as “All affected
commands,” while preserving the rest of the explanation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2eb797ea-09ed-48a8-96a5-4a06931e774a

📥 Commits

Reviewing files that changed from the base of the PR and between c605353 and 50b25f4.

📒 Files selected for processing (5)
  • .changeset/count-indented-subtasks.md
  • src/utils/task-progress.ts
  • test/commands/apply-instructions-tasks.test.ts
  • test/core/archive.test.ts
  • test/utils/task-progress.test.ts
💤 Files with no reviewable changes (2)
  • test/commands/apply-instructions-tasks.test.ts
  • test/core/archive.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/utils/task-progress.test.ts

Comment thread .changeset/count-indented-subtasks.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands/workflow/instructions.ts (1)

328-349: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not derive apply progress from the filtered task list.

parseTasksFile removes empty-description checkboxes, but generateApplyInstructions calculates total, complete, and state from that filtered array. A file containing one completed task and one unchecked blank checkbox can therefore report all_done, while list and archive still report incomplete work. Keep progress accounting based on all parsed checkbox entries, and filter only the displayed/actionable tasks; add a regression test for this mixed case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/workflow/instructions.ts` around lines 328 - 349, Update
generateApplyInstructions to calculate total, complete, and state from all
checkbox entries returned by parseTaskLines, while filtering empty-description
entries only from the displayed/actionable tasks array. Preserve existing
instruction generation for non-empty tasks and add a regression test covering
one completed task plus one unchecked blank checkbox, which must report
incomplete progress.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/commands/workflow/instructions.ts`:
- Around line 328-349: Update generateApplyInstructions to calculate total,
complete, and state from all checkbox entries returned by parseTaskLines, while
filtering empty-description entries only from the displayed/actionable tasks
array. Preserve existing instruction generation for non-empty tasks and add a
regression test covering one completed task plus one unchecked blank checkbox,
which must report incomplete progress.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 394329bc-6bb4-411d-b409-ab96c3fd1773

📥 Commits

Reviewing files that changed from the base of the PR and between 7e7a24c and 4252948.

📒 Files selected for processing (3)
  • .changeset/count-indented-subtasks.md
  • src/commands/workflow/instructions.ts
  • test/commands/apply-instructions-tasks.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/commands/apply-instructions-tasks.test.ts
  • .changeset/count-indented-subtasks.md

@clay-good

Copy link
Copy Markdown
Collaborator Author

Review disposition (four adversarial review passes, all findings triaged):

Fixed

  • Indented checkboxes inside a code fence counted as work. Fence-awareness was added, then removed again after it was proven to lose real tasks: the unterminated-fence guard is parity-based, so a stray ``` mid-file is "closed" by the next fence opener and every task between them disappears — ✓ Complete on a file `main` counted correctly. The parser now counts checkboxes wherever they are, and the cost is documented instead of engineered away.
  • - [x] with no text flipped apply from "contains no tasks" to "ready to archive". The apply list skips text-less checkboxes again, as before.
  • Missing end-to-end coverage: added tests for archive's gate (warning path and the interactive prompt/cancel path), openspec list no longer printing ✓ Complete, and openspec view keeping the change in Active. Reverting ^\s* to ^ now fails 10 tests across five files.
  • - [ ] (checkbox followed only by whitespace) used to survive as an empty-description task via (.+) backtracking; it is now skipped. Pinned with a test and disclosed in the notes above rather than claimed as "unchanged".
  • Changeset wording and the PR body, which had gone stale against the code.

Not changed, with reasons

  • "Both archive tests pass { yes: true } and assert a warning, contradicting the contract": --yes does print Warning: N incomplete task(s) found. Continuing due to --yes flag. (src/core/archive.ts:424) — it bypasses the block, not the warning, and the tests match the file's existing should warn about incomplete tasks. The blocking half is covered separately by the new prompt/cancel test.
  • Agent-facing templates that tell agents to count - [ ] themselves: real, but changing them needs a golden-hash regen. Follow-up, noted in the PR body.

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The copy/fence correction is clean and all exact-head checks pass, but the latest CodeRabbit finding is valid. I reproduced - [x] 1.1 Done plus an unchecked blank checkbox returning state: all_done, 1/1, while list/archive count 1/2; compute progress/state from all parsed checkboxes and filter only the displayed task rows, then add the mixed-case regression before re-review.

@clay-good
clay-good force-pushed the fix/parser-encoding-tolerance branch from 4252948 to 35309d3 Compare July 29, 2026 18:35

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The force-squashed head still contains the blocker from the previous review. generateApplyInstructions calculates progress from parseTasksFile() after blank rows are filtered, and the new mixed test asserts only the displayed tasks plus list-side count; exact-head reproduction still returns all_done and 1/1 for one completed named task plus one unchecked blank task while list/archive report 1/2. Please compute progress/state from all parsed checkboxes, filter only returned display rows, and assert apply's progress/state in that mixed test.

@clay-good
clay-good force-pushed the fix/parser-encoding-tolerance branch from 35309d3 to ea90878 Compare July 29, 2026 18:48

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Clean now. Progress/state count every parsed checkbox while the apply list filters only blank display rows; the exact mixed-case reproduction returns 1/2 and ready, all 123 focused tests pass, and the hosted matrix plus CodeRabbit are green.

Both checkbox parsers anchored the bullet at column 0, so an indented
sub-task was invisible to `openspec list`/`view` progress, to the apply
task list, and to archive's incomplete-task check. A change whose
sub-tasks were unfinished reported "✓ Complete" and archived with no
warning.

One shared `parseTaskLines()` now backs both surfaces and allows leading
whitespace. It matches every line the two patterns it replaces matched,
and more - including a tab or non-breaking space inside the brackets,
which the old counting pattern accepted - so task counts can rise but
never fall: no change starts reporting less work than before, and
archive's gate can only get stricter.

Checkboxes still count wherever they sit, including inside a code fence.
Skipping fenced ones was implemented and dropped: every rule for deciding
which fence is real has an input where a stray or unbalanced ``` swallows
genuine tasks, which is the silent failure this fix exists to remove.

Verified differentially against a build of main over hand-built fixtures
and the repo's own 120 tasks.md files: 0 files count fewer tasks, 0 lose
an incomplete-task warning.

Closes #1485

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Clean wording-only follow-up. It preserves the verified progress/display split, clarifies the all-textless-checkbox blocked state, the exact-head build and apply tests pass, and the full hosted matrix plus CodeRabbit are green.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying openspec-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 48fc37c
Status: ✅  Deploy successful!
Preview URL: https://c5173f54.openspec-docs.pages.dev
Branch Preview URL: https://fix-parser-encoding-toleranc.openspec-docs.pages.dev

View logs

@clay-good
clay-good added this pull request to the merge queue Jul 29, 2026
Merged via the queue into main with commit 427abf4 Jul 29, 2026
17 checks passed
@clay-good
clay-good deleted the fix/parser-encoding-tolerance branch July 29, 2026 22:47
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.

Indented sub-tasks are invisible to task progress: change reports "✓ Complete" and archives with unfinished work

2 participants