Skip to content

feat(list): work outside a repo; group task list by project + session fallback#5

Merged
alex-mextner merged 2 commits into
mainfrom
blitz/task-non-repo-list
Jun 17, 2026
Merged

feat(list): work outside a repo; group task list by project + session fallback#5
alex-mextner merged 2 commits into
mainfrom
blitz/task-non-repo-list

Conversation

@alex-mextner

Copy link
Copy Markdown
Owner

What

Make task's read/list/global ops work outside any git repo, and turn task list into a cross-project, session-aware view. Implements two ROADMAP items:

  • Commands should work OUTSIDE a repo; task list groups by repo/project
  • task list: fallback to all-tasks + session-vs-all messaging

Behavior

  • task list outside any repo → ALL tasks across the registered projects, grouped by project (a heading per project, tickets beneath). A project whose backend errors is shown as a degraded group; it never aborts the aggregation.
  • task list inside a repo → session scope, falling back to all of that repo's tickets (with a showing all project tasks line) when there's no agent session or the session has no tickets. --all gives the grouped cross-project view from anywhere.
  • task read / status / find work outside a repo: an id is routed to a registered project (Linear HYP-3 by team; #123 when exactly one GitHub project is registered); an ambiguous id fails with a clear 3-part error.
  • Only task create is repo-bound → a 3-part WHAT/WHY/HOW error outside a repo (honest HOW: --repo owner/name for GitHub, linear.team for Linear).

Design (brainstormed, then reviewed)

A new pure tasklib/projects.py models the projects: registry (in the config cascade, usually the global ~/.config/task-cli/config.yaml). Each entry becomes a config overlay that the existing get_backend() resolves unchanged — zero backend special-casing. Projects dedup by backend coordinate (repo/team), not display name. The multi-backend fan-out is an effect and lives in cli.py. LoadedConfig.with_overlay() is the new (immutable) seam.

_current_project_overlay returns None ONLY when genuinely outside a git work tree; a broken in-repo origin surfaces its real error instead of masquerading as "no projects". Linear list/search now scope to the backend's team (and project, if pinned) so the cross-project view doesn't double-count or leak workspace-wide hits.

Acceptance evidence

task list outside any repo, registry of 3 projects (one Linear group degraded on auth):

$ task list   # OUTSIDE any git repo
showing all project tasks (`task list` defaults to tasks created in the agent session)
web-app · github-issues · 2
  #142 [in-progress] Fix login redirect
  #139 [todo] Upgrade Node 20

api · github-issues · 1
  #88 [todo] Rate-limit /search

HYP · linear · degraded
  ! linear: no team with key 'HYP' (run linear auth)

3-part errors outside a repo (no traceback, exit 2):

$ task list           → error: no projects to list. You are outside a git repo …  (why/fix)
$ task create …       → error: cannot create a ticket: no project context …  (why/fix: --repo owner/name)
$ task read #5        → error: cannot resolve which project ticket '#5' belongs to …

Tests

160 passed (pytest) + tests/smoke.sh green (adds an outside-a-repo clean-degradation check). New/extended:

  • tests/test_projects.py — registry parsing, coordinate dedup, hostile input.
  • tests/test_cli_non_repo.py — grouped list, degraded groups, JSON shape, id routing (GitHub single / ambiguous / Linear-by-team / two-same-team), session fallback, current marker (incl. repo-in-registry), broken-remote-surfaces-error, 3-part errors (GitHub + Linear).
  • tests/test_cli.py--label filters the session view; a filter that excludes all session tickets does NOT fall back (no cross-session leak).
  • tests/test_backends.py — Linear list team+project filter; search team scoping.

Review

Reviewed across models via review --staged (3 rounds). All P1/P2 correctness findings addressed and regression-tested (filter-before-fallback leak, Linear same-team routing, current-marker-when-registered, swallowed remote error). Remaining findings are subjective style/refactor — declined per smallest-change.

🤖 Generated with Claude Code

… fallback

Make task's read/list/global ops work OUTSIDE any git repo, and turn
`task list` into a cross-project, session-aware view (ROADMAP: "Commands
should work OUTSIDE a repo; task list groups by repo/project" + "task list:
fallback to all-tasks + pagination + session-vs-all messaging").

Behavior:
- `task list` outside any repo -> ALL tasks across the registered projects,
  GROUPED by project (heading per project, tickets beneath). A project whose
  backend errors is shown as a degraded group; it never aborts the aggregation.
- `task list` inside a repo -> session scope, FALLING BACK to all of that
  repo's tickets (with a "showing all project tasks" line) when there's no
  agent session or the session has no tickets. `--all` gives the grouped
  cross-project view from anywhere.
- `task read` / `status` / `find` work outside a repo: an id is routed to a
  registered project (Linear `HYP-3` by team; `#123` when one GitHub project
  is registered); an ambiguous id fails with a clear 3-part error.
- Only `task create` is repo-bound -> a 3-part WHAT/WHY/HOW error outside a repo.

Implementation:
- New pure `tasklib/projects.py`: the `projects:` registry model (parse +
  config overlays), deduped by backend COORDINATE (repo/team), hostile-input
  tolerant. The multi-backend fan-out is an effect and lives in cli.py, reusing
  `get_backend` unchanged via a config overlay (`LoadedConfig.with_overlay`).
- `_current_project_overlay` returns None ONLY when genuinely outside a git
  work tree; a broken in-repo origin surfaces its error, not "no projects".
- Linear `list`/`search` scope to the backend's team (and project, if pinned)
  so the cross-project view doesn't double-count or leak workspace-wide hits.

Docs: README (outside-repo section + `projects:` registry + JSON shape),
AGENTS.md (the new rule), install.py SKILL_MD. Smoke: outside-a-repo `task
list` degrades cleanly (exit 2, no traceback).

Tests: 160 pass (test_projects, test_cli_non_repo, extended test_cli +
test_backends). Covers grouping, degraded groups, session fallback, --label
session filter (no cross-session leak), id routing, current marker, broken
remote, and the 3-part errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d629cb49a7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tasklib/cli.py Outdated
Comment thread tasklib/projects.py
…ands under a linear default

Two codex P2 review findings on the outside-a-repo routing:

1. cli.py: `backend: linear` with no `linear.team` returned None, demoting a real
   in-repo misconfiguration to the "outside a repo" path (lying error, masking the
   actionable "requires a team key"). Now mirror the github `repo: auto` branch:
   inside a work tree, surface the backend error; only genuinely outside a repo
   return None for the grouped/registry view.

2. projects.py: a registry entry's backend was defaulted from the MERGED top-level
   backend, so inside a linear-backed repo a global `{repo: acme/web}` GitHub
   shorthand was reinterpreted as a teamless linear entry and dropped from
   `task list --all`. Infer the backend from the entry's own coordinate shape
   (repo/github vs team/linear) before falling back to the merged default; an
   explicit per-entry `backend` still wins.

Review (multi-model) findings addressed: NoReturn terminal instead of a
pragma'd `return None`; anchored the regression assertion to the real error text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alex-mextner

Copy link
Copy Markdown
Owner Author

Both P2 review findings fixed in 9143977:

  1. Teamless in-repo linear (cli.py): _current_project_overlay no longer returns None for a backend: linear repo missing linear.team. Inside a git work tree it now surfaces the backend's actionable linear backend requires a team key error (mirroring the github repo: auto branch), instead of masking it as 'outside a repo'. Regression test: test_list_in_repo_linear_missing_team_surfaces_config_error_not_outside_repo.

  2. GitHub shorthands under a linear default (projects.py): added _backend_from_shape — a registry entry's backend is inferred from its own coordinate keys (repo/github vs team/linear) before falling back to the merged top-level default, so a global {repo: acme/web} is no longer dropped from task list --all inside a linear-backed repo. An explicit per-entry backend still wins. Regression tests: test_github_shorthand_survives_linear_merged_default, test_explicit_entry_backend_still_wins_over_shape.

@alex-mextner alex-mextner merged commit 43767d0 into main Jun 17, 2026
4 checks passed
@alex-mextner alex-mextner deleted the blitz/task-non-repo-list branch June 17, 2026 12:13
alex-mextner added a commit that referenced this pull request Jun 17, 2026
…imit (#6)

Adds the pagination half of the ROADMAP items "Commands should work OUTSIDE
a repo; task list groups by repo/project" + "task list: fallback to all-tasks
+ pagination + session-vs-all messaging" (the outside-repo/grouping/session
fallback shipped in #5; this is the missing pager + interactive-limit refinement
"task list pager: interactive-only + higher limit there").

Behavior (git's pager model):
- Human (non-json) list/find output pages through less ONLY when stdout is an
  interactive TTY; piped/scripted output is plain text (scriptable). Short
  output that fits one screen prints directly (less -F).
- Result cap follows interactivity: 100 in a terminal (the pager scrolls), 30
  when piped, unless an explicit count is given (which always wins).
- Opt out with the no-pager flag, NO_PAGER (any non-empty value), or an empty
  PAGER/TASK_PAGER (git's "cat, don't page"). Pager resolves
  TASK_PAGER -> PAGER -> less -> more; LESS defaults to FRX.
- JSON output is never paged (machine-readable stays un-paged, un-decorated).

Implementation:
- New tasklib/pager.py: pure should_page decision (TTY + not-opted-out + a
  pager resolves) and a defensive page() that pipes through the pager and falls
  back to a direct write on any failure. Pipe is forced utf-8 (ticket titles can
  be non-ASCII; a locale-encoding crash would otherwise escape). stdin is always
  closed in finally so the pager sees EOF.
- cli._emit_list is the single call site; _format_tickets/_format_groups now
  return strings (were direct prints) so output can be routed; JSON paths print
  straight to stdout via _print_tickets_json/_print_groups_json.
- _effective_limit picks the cap by interactivity; the count default is None so
  an explicit value is distinguishable.

Tests: tasklib/pager.py decision + routing (real child-process pager via
TASK_PAGER, utf-8, broken-pipe/non-zero-exit/missing-binary fallbacks,
LESS=FRX seeding, empty/whitespace pager, closed-stream isatty); cmd_list/cmd_find
wiring (interactive paging, piped-not-paged-even-with-pager-configured, no-pager,
NO_PAGER, JSON never paged, explicit count flows into the backend, grouped paging
on a TTY). Docs: README pagination section, AGENTS.md rule, install.py SKILL_MD.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant