Skip to content

feat(tasks): auto-scope linear tasks to the cwd's Linear project - #41

Merged
muqsitnawaz merged 3 commits into
mainfrom
feat/cwd-project-scope
Sep 1, 2026
Merged

feat(tasks): auto-scope linear tasks to the cwd's Linear project#41
muqsitnawaz merged 3 commits into
mainfrom
feat/cwd-project-scope

Conversation

@muqsitnawaz

@muqsitnawaz muqsitnawaz commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Problem

Muqsit opens Claude Code sessions in various project subfolders (e.g. ~/src/…/agents/prix). The SessionStart injection and a bare linear tasks both list every project's tasks regardless of which folder the agent is in, so agents pick up work from the wrong project.

Fix

linear tasks (and --board) now auto-scope to the Linear project bound to the current working directory. The directory→project mapping is owned by the agents projects CLI — agents projects for-cwd --json does a longest-match over every bound root and monorepo subpath, which a basename comparison can't. cwd behaves like an implicit --project.

Precedence: explicit --project wins → --all forces the whole team → otherwise the cwd binding scopes the queue → else unscoped.

Overrides: --all (whole team), --project X, or autoScope: false in ~/.linear-cli/config.json.

Fail-open (two bounded subprocess calls, ~0.3s each): no agents on PATH, no def for the cwd, a def without a Linear binding, or a slow/broken/timed-out call all leave the queue unscoped exactly as before. resolve_cwd_project() never raises — it runs on the hot path of the most-used command.

--json gains project: {id, name, auto} (null when unscoped) so consumers can distinguish an auto-scoped queue from the full workspace.

Live verification (real Linear, from this fleet)

[1] cd agents/prix && linear tasks --json
    project: {'id': '84849630-…', 'name': 'Prix', 'auto': True}   scope: all   count: 62
[2] cd agents/prix && linear tasks --all --json
    project: None   count: 121
[3] cd /tmp && linear tasks --json
    project: None   scope: active   count: 112

Human-readable banner:

Scope: Prix  (auto-detected from this directory; --all for the whole team, --project X to override)
All issues -- 62 task(s)  (8 yours, 54 unowned)

Tests

python3 -m unittest test_linear123 passed, hermetic (0.018s). Added ResolveCwdProjectTest (6: resolves, unbound, missing binary, nonzero exit, no-binding, timeout — all fail-open) and CwdAutoScopeTest / board tests (no-binding unscoped, binding auto-scopes + widens to all cycles, --all disables, explicit --project overrides cwd, autoScope:false opts out). The shared harness stubs resolve_cwd_project so pre-existing tests stay deterministic and never shell out.

Docs

CHANGELOG 0.20.0, README (For humans and agents + examples), skill.md. Version bumped 0.19.1 → 0.20.0.

Agents launched in a project subfolder listed every project's tasks and
picked up the wrong project's work. `linear tasks` (and `--board`) now
auto-scope to the Linear project bound to the current directory, resolved
from `agents projects for-cwd` (longest-match over bound roots + monorepo
subpaths). cwd behaves like an implicit `--project`.

Precedence: explicit `--project` wins; `--all` forces the whole team;
otherwise the cwd binding scopes the queue. Opt out with `autoScope: false`
in config. Fail-open in two bounded subprocess calls: no `agents` on PATH,
no def for the cwd, or a slow/broken call leaves the queue unscoped exactly
as before — never raises on the hot path.

--json gains `project: {id, name, auto}` (null when unscoped) so consumers
can distinguish an auto-scoped queue from the full workspace.

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

prix-cloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

Code Reviewer

Verdict: Ready to merge with one note

Build: clean (Python syntax OK)
Tests: 123 passed, 0 failed, 0.027s (hermetic as claimed)
Files reviewed: linear, test_linear.py, CHANGELOG.md, README.md, skill.md
Instructions read: none found — no CLAUDE.md, AGENTS.md, or GEMINI.md exists at the repo root or in any diff-touched directory.

Changes that work well

  • Fail-open designresolve_cwd_project() catches OSError, ValueError, and SubprocessError comprehensively (including FileNotFoundError and TimeoutExpired), returns (None, None) on any failure, and is properly stubbed in the test harness so tests remain hermetic.
  • UUID passthrough in resolve_project_id (line 1135-1136) — already detects UUID format and returns it without a network call, so auto-scoped project IDs incur zero extra API cost.
  • _ListTasksHarness backward compatibility — defaults cwd_project=(None, None) so all 100+ pre-existing tests keep their unscoped behavior and never shell out.
  • Precedence logic — explicit --project--all → auto-scope → unscoped is clear and correctly implemented in both list_tasks and show_board.
  • Clean test coverage — 6 ResolveCwdProjectTest cases cover every fail-open path, 5 CwdAutoScopeTest cases cover binding/unbinding/all/project/config opt-out, and 3 board scope tests cover auto-scope/all. All run via shared stubs.

Issue to consider

Cycle widening differs between list_tasks and show_board under auto-scope

In list_tasks (line 2095), the auto-scoped project UUID is passed to resolve_task_scope(), which — by its documented contract (line 1949-1950) — widens the cycle to "all" when a project is set:

scope = resolve_task_scope(args.cycle, project, ...)

In show_board (lines 2296, 2312-2324), the cycle scope is resolved before the project auto-scope, so the cycle stays on the default "active":

scope = args.cycle or "active"          # line 2296 — project not yet known
fragment, scope_label, cycle_meta = build_cycle_scope(...)  # "active" stays
...
pid, auto_project_name = resolve_cwd_project()   # line 2322 — too late
if pid:
    filters.append(...)

The comment at line 2309-2311 says "Same project scoping as list_tasks", but the behavior is different:

  • linear tasks (auto-scoped) → scope: "all" — all cycles in the project
  • linear tasks --board (auto-scoped) → scope: "active" — only the active cycle

This may be intentional — a board across all cycles could be noisy. But the comment is misleading, and the inconsistency could surprise someone who expects auto-scope to behave the same way across both commands (especially since resolve_task_scope says project → all cycles). Two options:

  1. Align the behavior: Resolve the project first in show_board, then pass it to resolve_task_scope(args.cycle or None, pid, None) so both commands widen cycles the same way.
  2. Align the comment only: Update "Same project scoping as list_tasks" to note the board intentionally stays on the active cycle, and add a test (test_board_auto_scope_keeps_active_cycle) documenting that choice.

Either is fine — the important thing is that the documentation matches the implementation.

Things to verify manually

Nothing beyond what was already tested. The fail-open path (agents not on PATH) is fully simulated in unit tests, and the real end-to-end behavior was demonstrated in the PR description's live verification output.


Reviewed by Code Reviewer — actually ran the build and tests on this branch.

Muqsit and others added 2 commits September 1, 2026 09:26
Review of #41 found the BLOCKER: the auto-detected cwd project id was fed to
resolve_project_id(strict=True) — the same abort-on-typo path used for an
explicit --project. A stale/malformed `linear.projectId` from `agents projects`
(drift outside this CLI's control) then hard-exited the single most-common
command instead of falling back, breaking the documented fail-open guarantee.

New resolve_auto_project_id() validates the auto-detected id/name against the
team's LIVE projects and returns None (never raises/exits) on no match — which
also catches a stale-but-UUID-shaped id that resolve_project_id's shape-only
UUID passthrough would have waved into a filter that silently matches nothing.
Both list_tasks and show_board now share this one policy (the SHOULD): explicit
--project resolves strictly and aborts on a typo; the cwd auto path validates
fail-open and, on drift, shows the whole team with a one-line stderr note.

Tests: +8 (resolve_auto_project_id unit coverage; drifted-UUID and malformed
non-UUID bindings fall back to unscoped for both tasks and board). 131 pass.
Live: prix still scopes to Prix (now validated), /tmp unscoped, no spurious note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The automated reviewer noted show_board resolved the cycle scope BEFORE the
project, so an auto-scoped (or explicit --project) board stayed on the active
cycle while `linear tasks` widened to all cycles — contradicting the "same as
list_tasks" comment. Resolve the project first and widen `scope` to "all" when
a project is set, so both surfaces treat a project scope as the whole
deliverable across cycles. Unscoped boards still default to the active cycle.

Tests: board auto-scope now asserts scope=="all"; added an unscoped-board
stays-active case. 132 pass. Live: `tasks --board` from prix/ → Prix, scope all.

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

prix-cloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

Code Reviewer

Verdict: Ready to merge

Build: N/A — single-file Python (stdlib only), no build step
Tests: 132 passed, 0 failed, 0.024s (hermetic)

Changes that work well

  • Fail-open design is thoughtfully implemented. resolve_cwd_project() catches every edge case (missing binary, nonzero exit, missing keys, timeout, non-list response) and returns (None, None) — never raises. The _agents_json inner function's three-layer defense (OSError for missing binary, returncode guard, json.loads try/except) is complete.

  • resolve_auto_project_id() is the right decoupling. Splitting the "validate against live projects" step from the blind resolve_project_id(strict=True) path means an auto-detected binding that drifted (renamed/recreated project) degrades gracefully to the unscoped view instead of aborting the command. This is the key behavioral difference the PR description promises, and the tests cover it (two drift test cases: test_drifted_cwd_binding_falls_back_to_unscoped and test_drifted_non_uuid_binding_does_not_crash).

  • Full parity between list_tasks and show_board. The board auto-scopes the same way, widens the cycle to all when scoped to a project, validates fail-open, and emits the same project field in --json output. No surprising divergence between the two entry points.

  • Test harness refactoring. Extracting _run_board() and threading cwd_project through _ListTasksHarness means the 6 new board tests and 7 new CwdAutoScopeTest tests are hermetic — they stub resolve_cwd_project and list_team_projects so no real agents binary or API calls are needed. Pre-existing tests continue to pass because the default cwd_project=(None, None) preserves their unscoped behavior.

  • The _last_filter capture in both _run_board and _run_list/_ListTasksHarness is a nice touch — it lets the tests assert not just on the output JSON but on the actual GraphQL filter string being sent, which catches incorrect filter composition beyond what the output shape alone would reveal.

Issues that need attention

None. The diff is clean, all edge cases in the fail-open contract are covered by tests, the documentation is updated (CHANGELOG, README, skill.md, --help text), and the version is bumped. I could not find a stub, placeholder, TODO without a ticket reference, or silent failure mode in the changed code.

Things to verify manually

  • The agents projects CLI is not present in this sandbox so I couldn't exercise the end-to-end shell-out path. The ResolveCwdProjectTest unit tests cover the subprocess boundary with a fake subprocess.run, but you may want to smoke-test the real for-cwdlistresolve_auto_project_id chain on a machine where agents is installed.

Reviewed by Code Reviewer — actually ran the build and tests on this branch.

@prix-cloud

prix-cloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

Code Reviewer

Verdict: Ready to merge

Build: No build step — Python script with zero external dependencies. Syntax check passes.
Tests: 131 passed, 0 failed, 0 skipped (0.024s)

Read: no CLAUDE.md/AGENTS.md in this repo — linear is a standalone Python script (/usr/bin/env python3), so the review follows the default toolchain detection.

Changes that work well

  • Fail-open design is thorough and correct. resolve_cwd_project() catches every boundary condition (OSError for missing binary, subprocess.SubprocessError for timeouts, ValueError/TypeError for bad JSON) and returns (None, None) in every case. The auto-scoping path never calls sys.exit()resolve_auto_project_id validates against the live project list and returns None for drift, a deliberate split from resolve_project_id(strict=True)'s abort-on-mismatch policy.

  • Test coverage is comprehensive. ResolveCwdProjectTest (6 cases) exercises the happy path, unbound cwd, missing binary, nonzero exit, def-without-linear-binding, and timeout. CwdAutoScopeTest (7 cases) covers no-binding, auto-scope + cycle widen, --all override, explicit --project override, autoScope: false opt-out, drifted-UUID fallback, and malformed-non-UUID fallback. Board parity tests mirror the same scenarios. All hermetic — no real agents binary or network calls.

  • Parity between list_tasks and show_board. Both functions implement the identical scoping precedence and fail-open policy. _run_board and _run_list test helpers are symmetrical, making maintenance easier.

  • The project: {id, name, auto} JSON field is a clean consumer-level signal. auto: True vs False lets automated workflows distinguish "scoped by cwd" from "scoped by explicit flag" without parsing the command line.

  • No stubs, TODOs, or placeholders in the new code. Everything is fully implemented and tested.

Things to verify manually (can't be automated here)

  • The agents projects for-cwd --json / agents projects list --json subprocess contract. The code assumes for-cwd --json returns {"name": "..."} and list --json returns [{name, linear: {projectId, name}}]. If the actual agents CLI output format differs, the auto-detection silently degrades to unscoped (fail-open by design — no crash, just no auto-scope). The author has verified this against the real fleet output shown in the PR description.
  • Smoke test with a real Linear API key to confirm the --json output shape matches what downstream consumers expect.

Reviewed by Code Reviewer — actually ran the build and tests on this branch.

@muqsitnawaz
muqsitnawaz merged commit 8abd6a2 into main Sep 1, 2026
5 checks passed
muqsitnawaz pushed a commit that referenced this pull request Sep 1, 2026
Review of #41 found the BLOCKER: the auto-detected cwd project id was fed to
resolve_project_id(strict=True) — the same abort-on-typo path used for an
explicit --project. A stale/malformed `linear.projectId` from `agents projects`
(drift outside this CLI's control) then hard-exited the single most-common
command instead of falling back, breaking the documented fail-open guarantee.

New resolve_auto_project_id() validates the auto-detected id/name against the
team's LIVE projects and returns None (never raises/exits) on no match — which
also catches a stale-but-UUID-shaped id that resolve_project_id's shape-only
UUID passthrough would have waved into a filter that silently matches nothing.
Both list_tasks and show_board now share this one policy (the SHOULD): explicit
--project resolves strictly and aborts on a typo; the cwd auto path validates
fail-open and, on drift, shows the whole team with a one-line stderr note.

Tests: +8 (resolve_auto_project_id unit coverage; drifted-UUID and malformed
non-UUID bindings fall back to unscoped for both tasks and board). 131 pass.
Live: prix still scopes to Prix (now validated), /tmp unscoped, no spurious note.

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

Copy link
Copy Markdown
Contributor Author

Updated resolve_cwd_project() to a single agents projects view . --json call (was for-cwd --json + list --json), reading name + linear.projectId from the one response. Return contract (projectId, projectName) and all fail-open guarantees preserved; 132 tests green.

Release ordering: this calls the installed agents binary. view . --json only exists once agents-cli PR phnx-labs/agi-cli#3380 (PHNX-3704) ships and is installed. Until then this fails open to the unscoped team view — never a crash — so it is safe to land ahead of that release. Do not treat the switch as active until the agents-cli release is installed.

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