report in-progress count in the incomplete-subtasks refusal - #88
Conversation
The guard refuses on pending > 0 OR inProgress > 0, but the message interpolated pending alone. A task blocked purely by [/] in-progress items therefore failed with 'incomplete subtasks: 0 pending' — a count of zero offered as the reason for refusing. Message now reads 'incomplete subtasks: N pending, M in-progress'. Adds a regression test for the [/]-only case asserting the in-progress count appears. The existing pending-only assertion still matches, since the new text keeps the pending clause as its prefix; vault_dispatcher tests stub their own error strings and are unaffected.
There was a problem hiding this comment.
Suite file exists — suite-test-file-required finding is a false positive from the mechanical funnel.
Summary of review:
The PR makes a focused 7-line change to fix a misleading error message and adds a regression test. The fix is correct: checkSubtaskCompletion already guarded on pending > 0 || inProgress > 0 but only reported pending in the message. Now it reports both counts.
The mechanical funnel found 122 pre-existing issues in complete.go and complete_test.go. The changed lines (the error format string and the new test) are correct. All mechanical findings are in pre-existing code adjacent to the change.
No issues are introduced by this PR. The fix is sound and the test is well-chosen.
PR Review Report
PR #88: report in-progress count in the incomplete-subtasks refusal
Base → Head: master → fix/incomplete-subtask-count-message
Files changed: CHANGELOG.md, pkg/ops/complete.go, pkg/ops/complete_test.go
Must Fix (Critical)
None. The change is correct.
Should Fix (Important)
-
pkg/ops/complete.go:335—markGoalCheckboxloop iterates over split goal content without actx.Done()check between iterations. If a goal file has thousands of lines, cancellation is not honored mid-iteration. (rule:go-context/cancel-check-in-loop) -
pkg/ops/complete.go:385—updateDailyNoteloop has the same gap. (rule:go-context/cancel-check-in-loop) -
pkg/ops/complete.go:127—slog.Warninside a loop without a sampler guard. If a task has many goals, this could produce unbounded log volume. (rule:go-logging/no-tight-loop-without-sampler)
Note: All three issues above are pre-existing in code adjacent to (not introduced by) this PR. They existed before this change and are out of scope for this bugfix review.
Nice to Have (Optional)
pkg/ops/complete.go—CompleteOperationinterface (line 22) lacks a//counterfeiter:generatedirective in this repo (the mock is inmocks/complete-operation.gobut generated with a local directive). The directive at line 21 is present in the file I read, so the finding may be stale or from a different location. No action needed.pkg/ops/complete_test.go— 55 instances of mock setter calls (e.g.mockTaskStorage.FindTaskByNameReturns(task, nil)) discard return values insideBeforeEachblocks. These are hand-written mock recorders, not fakes; the pattern is widespread across this test file and pre-existing.
Selector Mode Traceability
- Candidates: judgment rules triggered by changed files
- Applicable:
go-context/cancel-check-in-loop(triggered bypkg/ops/complete.go) - Skipped: all other judgment rules — diff scope (3 files, 34 lines) does not intersect their
triggerglobs - Mechanical funnel: 122 findings folded in (all pre-existing, none in the actual change)
- Precommit: skipped (selector mode) — CI covers lint+test
Next Steps
None. The bugfix is correct and the test provides good coverage. The pre-existing architectural issues in complete.go should be addressed separately.
Verdict
{
"verdict": "approve",
"summary": "Bugfix is correct: checkSubtaskCompletion now interpolates both pending and in-progress counts into the refusal message, matching the guard condition. Regression test covers the [/]-only case. All 122 mechanical findings are pre-existing in unchanged code.",
"comments": [
{
"file": "pkg/ops/complete.go",
"line": 335,
"severity": "major",
"message": "markGoalCheckbox loop lacks ctx.Done() check between iterations. Cancellation not honored mid-loop over potentially large goal content."
},
{
"file": "pkg/ops/complete.go",
"line": 385,
"severity": "major",
"message": "updateDailyNote loop lacks ctx.Done() check between iterations. Same issue as markGoalCheckbox."
},
{
"file": "pkg/ops/complete.go",
"line": 127,
"severity": "minor",
"message": "slog.Warn inside goal loop without sampler guard — unbounded log volume if task has many goals."
}
],
"concerns_addressed": [
"correctness: checkSubtaskCompletion guard checked pending>0||inProgress>0 but message only interpolated pending — fixed by including inProgress in format string",
"tests: regression test added for [/]-only case (0 pending, 3 in-progress), asserting exact error message substring"
]
}
Why
checkSubtaskCompletionrefuses whenpending > 0 || inProgress > 0, but the error interpolated onlypending:So a task blocked purely by in-progress
[/]items fails with:A count of zero given as the reason for refusing — misleading at exactly the moment someone is trying to close out work correctly, and it costs real debugging time chasing a pending item that does not exist.
What
Plus a regression test for the
[/]-only case (0 pending, 3 in-progress) asserting the in-progress count is named.Compatibility
ContainSubstring("incomplete subtasks: 1 pending")still passes — the new text keeps the pending clause as its prefix.pkg/ops/vault_dispatcher_test.gobuilds its own error strings witherrors.New(...)rather than exercising this path, so it is unaffected.Testing
make precommitPASS — go vet clean, osv-scanner 0, trivy 0 vulns / 0 secrets, CHANGELOG OK, license headers OK.Context
The second of two defects tracked in the same task as the missing
--forceflag (shipped in v0.111.0, PR #83). Both were first reported 2026-08-11.