fix(tasks): resolve/list tasks beyond the 100-task cap (#743)#824
Conversation
CLI resolved id-prefixes in Python over list_tasks (capped at 100, priority-ordered), so any task past the cap was unreachable by cf work start/stop/resume/diagnose, invisible in 'tasks list', and skipped by bulk 'tasks set --all'. - core: add find_by_prefix() — SQL 'id LIKE prefix% ESCAPE' lookup with no LIMIT (user-typed prefix escaped so a stray wildcard can't over-match). Route all 10 CLI prefix-resolution sites through it. - core: list_tasks(limit=None) omits the LIMIT clause; default stays 100 for API pagination. 'tasks list' and bulk 'set --all' pass limit=None. Closes #743
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 50 minutes Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughAdds a ChangesTask truncation fix: prefix resolution and uncapped listing
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
ReviewSolid fix, and the diff matches the PR description exactly (no prior CodeFrame reviews on this PR to reconcile with — only CodeRabbit's auto-summary so far). Went through both call sites and the new test file in detail. Correctness
Minor: test coverage nit Out of scope, flagging for awareness only Nothing else to flag — no security concerns (SQL is fully parameterized, LIKE input is escaped), no perf concerns at this scale, and the new test file otherwise covers the stated ACs well (default cap preserved, |
Demo — both acceptance criteria (real CLI, workspace seeded with 101 tasks)Seeded 101 tasks with priorities AC2 — listing/counting past the cap Old behavior: 100 rows, AC1 — resolving + mutating a task past the cap by id-prefix Old behavior: Backend suite ( |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
codeframe/cli/app.py (2)
2248-2264: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
tasks delete --allconfirm prompt undercounts tasks beyond the cap.
task_list = tasks.list_tasks(workspace)here is capped at 100 by default and is only used for the "Delete all N tasks?" confirmation count — actual deletion goes throughtasks.delete_all(workspace), which is uncapped. If the workspace has more than 100 tasks, the user sees an inaccurate (too-low) count before confirming a destructive bulk operation.🐛 Proposed fix
if all_tasks_flag: # Delete all tasks - task_list = tasks.list_tasks(workspace) + task_list = tasks.list_tasks(workspace, limit=None)🤖 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 `@codeframe/cli/app.py` around lines 2248 - 2264, The `tasks delete --all` confirmation in `app.py` is using `tasks.list_tasks(workspace)`, which is capped and can undercount the real number of tasks. Update the `all_tasks_flag` branch to compute the total task count with an uncapped source before calling `typer.confirm`, so the prompt reflects the full number that `tasks.delete_all(workspace)` will remove. Keep the change localized to the `all_tasks_flag` deletion flow and preserve the existing cancel/force behavior.
3774-3789: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
batch run --all-ready/--all-blockedstill capped at 100.
ids_to_executefor--all-readyand--all-blockedis built fromtasks_module.list_tasks(workspace, status=...)withoutlimit=None. This reintroduces the exact#743truncation the rest of this PR fixes: batch execution silently skips READY/BLOCKED tasks beyond the default 100-task cap, unliketasks listandtasks set --allwhich were updated.🐛 Proposed fix
if all_ready: - ready_tasks = tasks_module.list_tasks(workspace, status=TaskStatus.READY) + ready_tasks = tasks_module.list_tasks(workspace, status=TaskStatus.READY, limit=None) if not ready_tasks: console.print("[yellow]No READY tasks found[/yellow]") return ids_to_execute = [t.id for t in ready_tasks] console.print(f"Found {len(ids_to_execute)} READY tasks") elif all_blocked: from codeframe.core import runtime - blocked_tasks = tasks_module.list_tasks(workspace, status=TaskStatus.BLOCKED) + blocked_tasks = tasks_module.list_tasks(workspace, status=TaskStatus.BLOCKED, limit=None)🤖 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 `@codeframe/cli/app.py` around lines 3774 - 3789, The `batch run` paths for `all_ready` and `all_blocked` are still using the default `tasks_module.list_tasks(...)` cap, so only the first 100 tasks are collected. Update the `ids_to_execute` निर्माण in `app.py` to pass an unlimited fetch (use the same `limit=None` pattern as the other bulk commands) for both READY and BLOCKED branches in the batch-run logic. Keep the fix localized around the `all_ready` / `all_blocked` handling so the full task set is executed.
🧹 Nitpick comments (3)
codeframe/core/tasks.py (2)
217-225: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy liftUncapped LIKE prefix scan may degrade at scale.
SQLite's
LIKEoptimizer only uses an index for prefix matches when the column usesBINARYcollation withcase_sensitive_likeON, orNOCASEcollation with the pragma OFF — by defaultLIKEis case-insensitive and won't use the index, forcing a full table scan. Since this PR's whole purpose is to make lookups work for workspaces beyond 100+ tasks (regression tests already cover 150), this scan will grow linearly with total workspace size on every prefix resolution.Consider using
GLOB(case-sensitive by default, can use an index on aBINARY-collated column) since task ids are UUIDs and case sensitivity is actually desirable here, or add a range predicate (id >= ? AND id < ?) computed from the prefix.🤖 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 `@codeframe/core/tasks.py` around lines 217 - 225, The prefix lookup in tasks query uses LIKE with an escaped prefix, which can trigger a full table scan as workspace size grows. Update the task resolution query in tasks.py to use an index-friendly approach such as GLOB for the id prefix match, or replace the LIKE predicate with a bounded range check on id while keeping the existing workspace_id filter and ordering intact.
219-219: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated SELECT column list (3x).
The full 20-column list is now repeated verbatim in
find_by_prefixand both branches oflist_tasks. Extracting it to a module-level constant would reduce risk of drift when the schema changes.♻️ Proposed refactor
+_TASK_COLUMNS = ( + "id, workspace_id, prd_id, title, description, status, priority, depends_on, " + "estimated_hours, complexity_score, uncertainty_level, created_at, updated_at, " + "github_issue_number, parent_id, lineage, is_leaf, hierarchical_id, " + "requirement_ids, external_url, auto_close_github_issue" +)Then reference
_TASK_COLUMNSvia f-string in all threeSELECTstatements.Also applies to: 347-347, 361-361
🤖 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 `@codeframe/core/tasks.py` at line 219, The SELECT column list is duplicated across find_by_prefix and both branches of list_tasks, so extract the repeated task column set into a module-level constant (for example, _TASK_COLUMNS) in tasks.py and reference it in all three queries. Update the existing SELECT statements in the affected task-related methods to use the shared constant so the schema only needs to be changed in one place.tests/core/test_tasks_truncation.py (1)
49-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAmbiguity assertion is tautological in the common case.
shared = a.id[:1]picks a single hex character; two random UUIDs share it only ~1/16 of the time. When they don't,b.id[0] != sharedis true regardless ofmatches, so the assertion collapses to justa.id in ids— the test rarely actually exercises multi-match/ambiguity behavior despite its name and docstring.Force ambiguity deterministically instead of relying on chance, e.g. use an empty prefix (matches everything via
LIKE '%') or compute the actual shared prefix between the two created ids and assert both appear.♻️ Proposed fix using a guaranteed-ambiguous prefix
def test_find_by_prefix_returns_all_ambiguous_matches(self, workspace): """Ambiguity detection still works — callers report >1 match.""" a = tasks.create(workspace, title="a") b = tasks.create(workspace, title="b") - shared = a.id[:1] - matches = tasks.find_by_prefix(workspace, shared) - ids = {t.id for t in matches} - assert a.id in ids and (b.id in ids or b.id[0] != shared) + # Empty prefix matches every id, guaranteeing ambiguity deterministically. + matches = tasks.find_by_prefix(workspace, "") + ids = {t.id for t in matches} + assert a.id in ids and b.id in ids🤖 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 `@tests/core/test_tasks_truncation.py` around lines 49 - 56, The ambiguity test in test_find_by_prefix_returns_all_ambiguous_matches is effectively probabilistic because shared = a.id[:1] may not produce multiple matches, so the assertion can pass without exercising ambiguity. Update the test to use a deterministic ambiguous prefix in tasks.find_by_prefix, such as an empty prefix or a prefix computed from the created ids, and assert the expected multiple ids are returned so the test always validates the multi-match behavior described by the docstring.
🤖 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 `@codeframe/cli/app.py`:
- Around line 2248-2264: The `tasks delete --all` confirmation in `app.py` is
using `tasks.list_tasks(workspace)`, which is capped and can undercount the real
number of tasks. Update the `all_tasks_flag` branch to compute the total task
count with an uncapped source before calling `typer.confirm`, so the prompt
reflects the full number that `tasks.delete_all(workspace)` will remove. Keep
the change localized to the `all_tasks_flag` deletion flow and preserve the
existing cancel/force behavior.
- Around line 3774-3789: The `batch run` paths for `all_ready` and `all_blocked`
are still using the default `tasks_module.list_tasks(...)` cap, so only the
first 100 tasks are collected. Update the `ids_to_execute` निर्माण in `app.py`
to pass an unlimited fetch (use the same `limit=None` pattern as the other bulk
commands) for both READY and BLOCKED branches in the batch-run logic. Keep the
fix localized around the `all_ready` / `all_blocked` handling so the full task
set is executed.
---
Nitpick comments:
In `@codeframe/core/tasks.py`:
- Around line 217-225: The prefix lookup in tasks query uses LIKE with an
escaped prefix, which can trigger a full table scan as workspace size grows.
Update the task resolution query in tasks.py to use an index-friendly approach
such as GLOB for the id prefix match, or replace the LIKE predicate with a
bounded range check on id while keeping the existing workspace_id filter and
ordering intact.
- Line 219: The SELECT column list is duplicated across find_by_prefix and both
branches of list_tasks, so extract the repeated task column set into a
module-level constant (for example, _TASK_COLUMNS) in tasks.py and reference it
in all three queries. Update the existing SELECT statements in the affected
task-related methods to use the shared constant so the schema only needs to be
changed in one place.
In `@tests/core/test_tasks_truncation.py`:
- Around line 49-56: The ambiguity test in
test_find_by_prefix_returns_all_ambiguous_matches is effectively probabilistic
because shared = a.id[:1] may not produce multiple matches, so the assertion can
pass without exercising ambiguity. Update the test to use a deterministic
ambiguous prefix in tasks.find_by_prefix, such as an empty prefix or a prefix
computed from the created ids, and assert the expected multiple ids are returned
so the test always validates the multi-match behavior described by the
docstring.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c3f127b7-d4bc-4ae9-b3f6-4620389ac253
📒 Files selected for processing (3)
codeframe/cli/app.pycodeframe/core/tasks.pytests/core/test_tasks_truncation.py
Address review findings on PR #824: - batch run --all-ready/--all-blocked: pass limit=None so batches include READY/BLOCKED tasks beyond the 100 cap (was reintroducing the #743 truncation for batch execution). - tasks delete --all: count the confirm prompt with limit=None so it matches the uncapped delete_all() (avoids undercounting before a destructive op). - test: make the ambiguity test deterministic (empty prefix matches all) instead of relying on a ~1/16 first-hex-char collision.
Review triage (CodeRabbit + claude)Thanks both. Dispositions: Fixed in 8af1554
Deferred (Trivial), with reasons
Follow-up filed
|
Review (re-review of 8af1554)Confirmed the second commit resolves everything from the first round cleanly:
Also re-grepped for the old .id.startswith( pattern across codeframe/cli/app.py: the only remaining hits are the four batch_id sites, which is exactly the scope of the follow-up issue (#825) already filed in the triage comment - nothing task-related was missed. New finding - same bug class survives in tasks generate --overwrite codeframe/cli/app.py:1854-1856 and :1892/:1906-1909 (the tasks_generate command, untouched by this PR) still call the capped tasks.list_tasks(workspace) (default limit=100) to snapshot original_ids before regeneration and to delete them after generation succeeds: In a workspace with more than 100 existing tasks - which the PR description itself says is routine for PRD decomposition - cf tasks generate --overwrite will only clear the first 100 (priority ASC, created_at ASC). The remaining pre-existing tasks silently survive alongside the freshly generated set, so --overwrite no longer actually overwrites. It is the same root cause as #743 (an unqualified list_tasks default cap) but in a codepath this PR does not touch and that is not mentioned in the Known limitations section. It is not a regression from this PR, but it is squarely in-theme and a one-line fix (limit=None on both calls) - worth doing here or filing alongside #825 rather than leaving it to be rediscovered separately. Everything else - SQL parameterization, LIKE-escaping order, the _row_to_task column mapping in find_by_prefix, and test coverage for the two ACs - still holds up as noted in my earlier pass. No other bugs, security, or perf concerns. |
|
CodeRabbit's latest pass re-surfaces the same two Functional Correctness findings (delete-all count, batch --all-ready/--all-blocked) as outside-diff comments — both are already fixed in |
Summary
Fixes #743 — CLI task resolution/listing silently truncated at 100 tasks.
list_tasksdefaults tolimit=100(orderedpriority ASC, created_at ASC), and the CLI resolved partial task ids in Python (t.id.startswith(prefix)) over that truncated list. Any task past the first 100 was therefore:cf work start/stop/resume/diagnose(andtasks set,work retry,work update-description),cf tasks list,cf tasks set --all.PRD decomposition routinely exceeds 100 tasks, so this was a real data-integrity gap.
Changes
codeframe/core/tasks.pyfind_by_prefix(workspace, prefix)— resolves task ids at the SQL layer:WHERE id LIKE 'prefix%' ESCAPE '\', noLIMIT, ordered likelist_tasks. Returns all matches so callers keep their ambiguity ("multiple tasks match") handling. The user-typed prefix is escaped so a stray LIKE metachar (%/_) can't over-match and resolve the wrong task before a status change. → AC1list_tasks(limit: Optional[int] = 100)—Noneomits theLIMITclause (uncapped). Default stays100so API pagination (tasks_v2) is unchanged. → AC2codeframe/cli/app.pyfind_by_prefixinstead oflist_tasks+ Pythonstartswith.cf tasks listand bulkcf tasks set --allpasslimit=Noneso tasks beyond the cap are listed, counted, and updated.Acceptance criteria
WHERE id LIKE 'prefix%') bypasses the limit for resolution.limit=None) so tasks beyond 100 are addressable and counted.Testing
tests/core/test_tasks_truncation.py: with 150 priority-ordered tasks —find_by_prefixresolves task feat: Migrate web-ui to shadcn/ui Nova design system #150 (past the cap),list_tasks(limit=None)counts all 150, default still caps at 100, and LIKE-wildcard prefixes (%,_) don't over-match.ruff checkclean.Known limitations / out of scope
list_by_status,get_dependents, and thescheduleCLI display commands (its core already useslimit=1000). None are the addressability surface [P1.16] CLI task resolution/listing silently truncates at 100 tasks #743 names; left unchanged to avoid behavior drift for the API/TUI.Summary by CodeRabbit