Skip to content

feat: add cycle support (issues --cycle, cycles) - #40

Closed
seraph-pixelperfect wants to merge 1 commit into
feat/projects-commandfrom
feat/cycle-support
Closed

seraph-pixelperfect wants to merge 1 commit into
feat/projects-commandfrom
feat/cycle-support

Conversation

@seraph-pixelperfect

Copy link
Copy Markdown
Collaborator

Closes #21
Depends on #39

What

  • issues --cycle <current|number> — filter the issue list to a cycle.
    • --cycle current → cycle: { isActive: { eq: true } } in the IssueFilter (composes with --team, --state, --label, --project, --search, --limit).
    • --cycle <number> → cycle: { number: { eq: N } }, requires --team (loud VALIDATION_ERROR otherwise) because cycle numbers are only unique within a team — see Ambiguities.
  • cycles [--team KEY] — new command listing recent cycles (number, date range, progress, plus status and team attribution). Default 10, most recently updated first, auto-paginated 50/batch like projects.

Schema verification (mandatory check) — the issue's guess was right

Source: generated documents from @linear/sdk 90.0.0 (npm pack @linear/sdk, inspected dist/index-zxW4m1xd.d.mts), cross-checked against the SDK's own Team_CyclesDocument GraphQL string in dist/index.mjs.

Verified shapes:

What Verified shape
IssueFilter.cycle InputMaybe<NullableCycleFilter> — "Filters that the issues cycle must satisfy"
NullableCycleFilter.isActive InputMaybe<BooleanComparator> — "Comparator for the filtering active cycle"
BooleanComparator { eq?: Boolean; neq?: Boolean }
NullableCycleFilter.number InputMaybe<NumberComparator>; NumberComparator = { eq?: Float, gt, gte, in, lt, lte, neq, nin }
Cycle fields used number: Float, startsAt: DateTime, endsAt: DateTime, progress: Float (0–1, "(completed estimate points + 0.25 * in-progress estimate points) / total estimate points", 0 when unestimated)
Team.cycles args (TeamCyclesArgs) filter: CycleFilter, first/after/before/last: Int/String, orderBy: PaginationOrderBy
Root Query.cycles CycleConnection — "All cycles accessible to the user", same args as Team.cycles

Deviation call-out: none needed for the filter. The issue's guessed cycle: { isActive: { eq: true } } is schema-valid as-is (it lives on NullableCycleFilter, reachable through IssueFilter.cycle), so no isActiveDate hack or cycle-id resolution round trip is required. Numeric filtering uses cycle: { number: { eq: N } }, also verified.

One deliberate schema-driven choice: PaginationOrderBy only offers createdAt | updatedAt (no number ordering), so cycles orders by updatedAt (recent-first), matching the issues/projects convention.

Command-path choices:

  • cycles --team KEY resolves the team via the existing resolveTeamId helper, then queries team(id: $id) { cycles(first, after, orderBy: updatedAt) } exactly as the issue specifies. Unknown team key → loud error (Team "X" not found in your workspace).
  • cycles with no --team queries the root cycles connection in a single request (instead of N per-team queries) and selects team { key } per node so mixed-team rows are attributed. Small deviation from the letter of the issue ("via the team { cycles } connection"), kept for the team-scoped path; flagged for the reviewer.

Demo (real output)

Before (base feat/projects-command):

$ pnpm exec tsx bin/linear-axi.ts cycles --key lin_api_fake
error: "Unknown command: cycles"
code: VALIDATION_ERROR

$ pnpm exec tsx bin/linear-axi.ts issues --cycle current --team LIN --key lin_api_fake
error: unknown flag --cycle
code: VALIDATION_ERROR

After — both parse, dispatch, and reach the Linear API layer (mapped 401 with a fake key):

$ pnpm exec tsx bin/linear-axi.ts cycles --key lin_api_fake
error: Linear auth required — set LINEAR_API_KEY or pass --key <key>
code: AUTH_REQUIRED
help[2]: ...

$ pnpm exec tsx bin/linear-axi.ts issues --cycle current --team LIN --key lin_api_fake
error: Linear auth required — set LINEAR_API_KEY or pass --key <key>
code: AUTH_REQUIRED

Offline loud validation (no network involved):

$ pnpm exec tsx bin/linear-axi.ts issues --cycle 42 --key lin_api_fake
error: "--cycle <number> requires --team (cycle numbers restart per team)"
code: VALIDATION_ERROR
help[2]: e.g. --cycle 42 --team LIN, Or use --cycle current without --team for every team's active cycle
exit: 2

Rendered TOON (fetch stubbed at the transport; exactly the GraphQL this PR sends — note the issued filter):

issued GraphQL filter: filter: { team: { key: { eq: "LIN" } }, cycle: { isActive: { eq: true } } }, first

count: 2
issues[2]{identifier,title,state,team,updated}:
  LIN-101,Ship cycle filter,started,LIN,just now
  LIN-102,Write cycle docs,in progress,LIN,just now
count: 3
cycles[3]{number,team,status,range,progress}:
  43,LIN,upcoming,2026-08-21..2026-09-04,0
  42,LIN,active,2026-08-14..2026-08-28,46
  41,ENG,past,2026-07-31..2026-08-14,83
help[1]:
  Run `linear-axi issues --cycle current --team LIN` to list the active cycle's issues

Local gates (real output)

  • pnpm build — clean (tsc, no output)
  • pnpm lint — clean (eslint ., no output)
  • pnpm run build:skill -- --check — skills/linear-axi/SKILL.md is up to date.
  • pnpm test:
 Test Files  10 passed (10)
      Tests  97 passed (97)

78 → 97 tests (19 new in test/cycles.test.ts: root + team cycles document shape/ordering/variables, rendering, statuses, empty state, hints, loud-validation errors, auto-pagination, and all issues --cycle filter compositions).

  • pnpm run format:check — RED with exactly the 18 pre-existing files (verified identical set on the base branch; src/cli.ts, src/linear.ts, src/commands/issues.ts, skills/linear-axi/SKILL.md were already flagged before this PR). The two new files (src/commands/cycles.ts, test/cycles.test.ts) pass pnpm exec prettier --check. No newly-flagged files.

Ambiguities (flagged, decisions documented in --help too)

  1. Numeric --cycle without --team — cycle numbers restart per team, so --cycle 42 is ambiguous. Picked the AXI loud-validation route: hard error with both fixes suggested (--cycle 42 --team LIN, or --cycle current). Not silent all-teams matching.
  2. --cycle current without --team — the filter is well-defined (isActive eq true matches issues in any team's active cycle), so it is allowed and documented as such; in multi-team workspaces the result mixes teams (rows still carry team { key }). Reviewer: happy to require --team here too if preferred.
  3. Progress rendering — Cycle.progress is a 0–1 float over estimate points (0 when nothing is estimated), rendered as an integer 0–100 percent, same as projects. status (past/active/upcoming) is derived from startsAt/endsAt; Linear's own Cycle.isActive additionally excludes manually early-completed cycles, which dates alone can't see — close enough for a listing, and no extra field is fetched.
  4. No-flag cycles uses the root cycles connection (one request, team { key } per node) rather than N team { cycles } queries — see schema section.

Stacked PR note

Stacked on #39 (stack: #37 → #38 → #39); base is feat/projects-command, not main. No CI checks appear — ci.yml only triggers on PRs targeting main. CI runs when retargeted to main after the stack merges; local gates above are the evidence. Not retargeting.

CI quota-block caveat: this org has seen spending-limit/billing rejections that look like check failures. If a check shows red after retarget, inspect gh run view <run-id> / the jobs API for a billing annotation, or empty steps with ~3-4s duration — that is a quota block, not a code failure; rely on the local gates above rather than retrying.

Identity note

Automated agent dispatch authenticated as seraph-pixelperfect, submitted for human review — not self-approved. Merge is a human decision.

@ghostinprod-pixelperfect

Copy link
Copy Markdown
Collaborator

Superseded by merged rc/0.1 PR #51 (commit 694a564), which consolidated this read/discovery stack with validation and review fixes.

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.

2 participants