Skip to content

fix(pm): unify listWorkItems contract so backlog-manager sees Linear cards#1133

Merged
zbigniewsobiecki merged 1 commit intodevfrom
fix/listworkitems-unification
Apr 16, 2026
Merged

fix(pm): unify listWorkItems contract so backlog-manager sees Linear cards#1133
zbigniewsobiecki merged 1 commit intodevfrom
fix/listworkitems-unification

Conversation

@zbigniewsobiecki
Copy link
Copy Markdown
Member

Summary

Backlog-manager for Linear-backed projects (e.g. llmist) saw BACKLOG as empty even when items existed (MNG-97 confirmed in backlog). The chained backlog-manager run for MNG-96/PR-571 reported:

## Pipeline Check
**Active pipeline count:** 0 (TODO: 0, IN_PROGRESS: 0, IN_REVIEW: 0)
The pipeline has capacity (0 < 1), but the BACKLOG is empty

Root cause: PMProvider.listWorkItems(containerId, filter?) had incompatible per-provider semantics for containerId:

Provider What containerId meant What happened with status-as-containerId
Trello The list ID directly ✅ Works (lists ARE statuses)
JIRA The project key ❌ JQL project = "<status name>" returned 0
Linear The team ID listIssues({ teamId: <state-uuid> }) returned 0

The snapshot loader (contextSteps.fetchPipelineLists) called provider.listWorkItems(list.id) where list.id was a status identifier — silently returning [] for both JIRA and Linear since the day PR #1131 made buildPipelineLists populate Linear statuses.

Fix — unify the abstraction

  • PMProvider.listWorkItems(containerId: string | undefined, filter?)containerId is now optional. Each provider self-resolves the natural scope from its own config when omitted: Trello uses config.lists[filter.status], JIRA defaults to config.projectKey, Linear defaults to config.teamId. The filter.status is the CASCADE-canonical key ('backlog', 'todo', ...), mapped to the provider's native identifier internally.
  • contextSteps.fetchPipelineLists — calls provider.listWorkItems(undefined, { status: list.statusKey }) for all 3 providers in one code path.
  • PipelineList shape: { name, statusKey } (dropped the dead id field). Snapshot output now shows ## BACKLOG (status: backlog) instead of (list ID: <UUID>) — the agent uses CASCADE keys with move-work-item, not the underlying provider IDs.
  • backlog-check.ts:isPipelineAtCapacity — collapsed Trello/JIRA dispatch into one unified path (~50 lines deleted). Now also works for Linear without code change. The per-provider knowledge survives only in isProviderMisconfigured, which preserves the conservative-fallback behaviour and uses an exhaustive switch over PMType (with assertNeverPMType in default) so a 4th provider fails the build.
  • agent-execution.ts:propagateAutoLabelAfterSplitting — same simplification (~25 lines deleted).
  • TrelloPMProvider constructor now requires TrelloConfig. TrelloIntegration.createProvider falls back to an empty config when getTrelloConfig returns undefined (the CLI's CredentialScopedCommand synthesizes a pm: { type: 'trello' } shell with no trello field for gadget-scope purposes; gadgets always pass containerId explicitly, so self-resolution is never exercised on that path).

Tests

  • tests/unit/pm/{trello,jira,linear}-adapter.test.ts — 10 new tests for self-resolution from each provider's config + backwards-compat for explicit containerId.
  • tests/unit/agents/definitions/pipelineSnapshot.test.ts — updated to assert unified call shape; updated list ID:status: in output assertions.
  • tests/unit/triggers/shared/backlog-check.test.ts — dropped the STATUS_KEY_BY_FIXTURE translation shim, rewrote 27 fixture keys to CASCADE form, added 5 Linear-specific tests, replaced unsupported-provider test with .rejects.toThrow(/Unhandled PMType/) exhaustiveness check.
  • tests/helpers/factories.ts — new createMockLinearProject.
  • tests/unit/triggers/agent-execution.test.ts + tests/unit/triggers/shared/agent-execution.test.ts — updated 5 assertions to use the unified call shape; replaced mockResolvedValue with per-status mockImplementation so capacity checks see empty in-flight statuses correctly.

7845 unit + 524 integration tests pass. lint+typecheck+build clean.

Out of scope

  • ❌ The CLI gadget (cascade-tools pm list-work-items --containerId X) keeps its existing explicit-containerId form.
  • ❌ Backfilling MNG-97 manually — once this lands, the next chained backlog-manager will see it.
  • ❌ Removing the remaining double-read of provider config in isProviderMisconfigured (read here AND in adapter self-resolution). Acceptable architectural trade-off — the per-provider knowledge is correctly isolated to misconfiguration detection.

Test plan

  • npm test (7845 pass)
  • npm run test:integration (524 pass, ~5.5 min)
  • npm run typecheck, npm run lint clean
  • npm run build produces dist artifacts
  • After deploy: trigger a Linear-backed chain on llmist, watch backlog-manager pick up MNG-97 and move it to TODO. Snapshot output should show ## BACKLOG (status: backlog) with MNG-97 listed.

🤖 Generated with Claude Code

…cards

Backlog-manager for Linear-backed projects (e.g. llmist) saw BACKLOG as
empty even when items existed (MNG-97 confirmed in backlog). Tracing
the chained backlog-manager run for MNG-96/PR-571 confirmed:

```
## Pipeline Check
**Active pipeline count:** 0 (TODO: 0, IN_PROGRESS: 0, IN_REVIEW: 0)
The pipeline has capacity (0 < 1), but the BACKLOG is empty
```

Root cause: `PMProvider.listWorkItems(containerId, filter?)` had
incompatible per-provider semantics for `containerId`:

- Trello: containerId = list ID directly → works (lists ARE statuses)
- JIRA: containerId = project key, status filter via `filter.status` →
  passing a status name as containerId returned 0 from JQL
- Linear: containerId = team ID, status filter via `filter.status` →
  passing a state UUID as containerId queried `listIssues({ teamId:
  <state-uuid> })` → returned 0

The snapshot loader (`contextSteps.fetchPipelineLists`) called
`provider.listWorkItems(list.id)` where `list.id` was a status
identifier — silently returning [] for both JIRA and Linear since the
day PR #1131 made `buildPipelineLists` populate Linear statuses.

This commit unifies the abstraction:

- `PMProvider.listWorkItems(containerId: string | undefined, filter?)`
  — containerId becomes optional. Each provider self-resolves the
  natural scope from its own config when omitted: Trello uses
  `config.lists[filter.status]`, JIRA defaults to `config.projectKey`,
  Linear defaults to `config.teamId`. The `filter.status` is the
  CASCADE-canonical key (`'backlog'`, `'todo'`, ...), mapped to the
  provider's native identifier internally.

- `contextSteps.fetchPipelineLists` calls
  `provider.listWorkItems(undefined, { status: list.statusKey })` —
  one code path for all 3 providers.

- `PipelineList` shape: `{ name, statusKey }` (dropped the dead `id`
  field). Snapshot output now shows `## BACKLOG (status: backlog)`
  instead of `(list ID: <UUID>)` — the agent uses CASCADE keys with
  `move-work-item`, not the underlying provider IDs.

- `backlog-check.ts:isPipelineAtCapacity` collapsed Trello/JIRA
  dispatch into one unified path (~50 lines deleted). Now also works
  for Linear without code change. The per-provider knowledge survives
  only in `isProviderMisconfigured`, which preserves the pre-existing
  conservative behaviour: when a project's config is incomplete,
  return `'misconfigured'` (caller runs the agent anyway) rather than
  silently treating it as `'backlog-empty'` (which would skip the
  agent run).

  `isProviderMisconfigured` uses an exhaustive `switch` over
  `PMType` with `assertNeverPMType(provider.type)` in the default
  branch — TypeScript fails the build when a 4th `PMType` member is
  added without updating the switch.

- `agent-execution.ts:propagateAutoLabelAfterSplitting` similarly
  collapsed (~25 lines deleted).

- `TrelloPMProvider` constructor now takes `TrelloConfig` (was
  optional before this change made it useful). The CLI's
  `CredentialScopedCommand` synthesizes a minimal Trello shell with
  no `trello` field for gadget-scope purposes; `TrelloIntegration.
  createProvider` falls back to an empty config in that case so the
  adapter still constructs cleanly (the CLI's gadget callers always
  pass containerId explicitly, so self-resolution is never exercised
  on this path).

Tests added/updated:

- `tests/unit/pm/{trello,jira,linear}-adapter.test.ts` — 10 new tests
  for self-resolution from each provider's config + backwards-compat
  fallback for explicit containerId.
- `tests/unit/agents/definitions/pipelineSnapshot.test.ts` — updated
  to assert unified call shape; updated `list ID:` → `status:` in
  output assertions.
- `tests/unit/triggers/shared/backlog-check.test.ts` — dropped the
  `STATUS_KEY_BY_FIXTURE` translation shim, rewrote 27 fixture keys
  to CASCADE form (so test fixtures correctly say what they ARE),
  added 5 Linear-specific tests, replaced unsupported-provider test
  with `.rejects.toThrow(/Unhandled PMType/)` exhaustiveness check.
- `tests/helpers/factories.ts` — new `createMockLinearProject` next
  to the existing Trello/JIRA factories.
- `tests/unit/triggers/agent-execution.test.ts` and `tests/unit/
  triggers/shared/agent-execution.test.ts` — updated 5 assertions to
  use the unified call shape; replaced `mockResolvedValue` with
  per-status `mockImplementation` so capacity checks see empty
  in-flight statuses correctly.

7845 unit + 524 integration tests pass. lint+typecheck+build clean.

Out of scope: the CLI gadget (`cascade-tools pm list-work-items
--containerId X`) keeps its existing explicit-containerId form;
backfilling MNG-97 manually (next chained backlog-manager will see
it after this lands).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@zbigniewsobiecki zbigniewsobiecki merged commit db6a5bb into dev Apr 16, 2026
8 checks passed
@zbigniewsobiecki zbigniewsobiecki deleted the fix/listworkitems-unification branch April 16, 2026 19:22
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 97.80220% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/pm/jira/adapter.ts 88.88% 1 Missing ⚠️
src/pm/trello/adapter.ts 87.50% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

zbigniewsobiecki added a commit that referenced this pull request Apr 16, 2026
…near projects

The splitting agent for MNG-98 on llmist (Linear-backed) ran today. Every
`cascade-tools pm <cmd>` call from inside the worker container threw:

```
Error: Linear integration requires teamId in config
    at LinearIntegration.createProvider (file:///app/dist/pm/linear/integration.js:65)
```

The agent fell back to direct Linear API calls and got the work done, but
the CLI gadget path was broken for every Linear-backed project. Three
combined causes:

1. `src/cli/base.ts` cast `process.env.CASCADE_PM_TYPE` as `'trello' |
   'jira' | undefined` — but `secretBuilder.ts` injects the actual
   `project.pm.type`, so `'linear'` was arriving unboxed at runtime.
2. The `pmProject` synthesis only had a JIRA conditional spread; for
   `pmType === 'linear'` it produced `{ pm: { type: 'linear' } }` with no
   `linear` field. `LinearIntegration.createProvider` then read
   `getLinearConfig(project)` → undefined → threw.
3. Even after the synthesis was fixed, no `withLinearCredentials` wrap
   meant gadgets calling Linear API would fail with "No Linear credentials
   in scope". Lines 44-58 wrapped GitHub/Trello/JIRA but not Linear.

And on the worker-spawn side, `secretBuilder.ts:46-53` injected
`CASCADE_JIRA_*` env vars from `getJiraConfig(project)` but had no Linear
equivalent — so even if the CLI tried to read them, they wouldn't be
there.

This is the third generation of the same architectural omission: the CLI
was Trello/JIRA-aware long before Linear existed (spec 006). PR #1131
unblocked the registry (cascade-tools commands actually load), PR #1133
tightened provider validation — both made the latent bug louder. Now
LinearIntegration throws cleanly when it has no teamId, and that throw
surfaces here.

Fix mirrors the JIRA pattern end-to-end:

- `src/backends/secretBuilder.ts` — when `getLinearConfig(project)` is
  truthy, inject `CASCADE_LINEAR_TEAM_ID`, optional
  `CASCADE_LINEAR_PROJECT_ID`, and `CASCADE_LINEAR_STATUSES` into the
  worker's env. Mirrors the JIRA injection block.

- `src/cli/base.ts`:
  - Replace the `'trello' | 'jira' | undefined` type lie with `PMType |
    undefined` (canonical type already exists in `src/pm/types.ts:6`),
    so future provider additions can't reintroduce this footgun.
  - Add a `withLinearCredentials` wrap when `LINEAR_API_KEY` is set,
    mirroring the GitHub/Trello/JIRA wrap pattern.
  - Add a Linear-config synthesis branch, reading
    `CASCADE_LINEAR_TEAM_ID`/`PROJECT_ID`/`STATUSES`.
  - Add `LINEAR_API_KEY`-based pmType inference as a fallback when
    `CASCADE_PM_TYPE` isn't set (mirrors the JIRA-baseUrl inference).
    Worker-spawned use is unaffected because `secretBuilder` always sets
    `CASCADE_PM_TYPE` explicitly; this just helps human-invoked CLI use.
  - Refactor `run()` into three small helpers
    (`wrapWithCredentialScopes`, `resolvePmType`,
    `synthesizeProjectFromEnv`) to keep cognitive complexity inside
    biome's threshold.

Tests: +3 secretBuilder Linear-injection tests, +3 credential-scoping
Linear tests (withLinearCredentials wrap, populated linear synthesis,
LINEAR_API_KEY-based inference). 7851 unit + 524 integration tests pass.
Lint + typecheck + build clean.

Out of scope:
- Refactoring CLI to load full project config from DB instead of
  synthesising from env vars. Bigger change; env-var pattern works for
  JIRA and is the established convention.
- `--linear-team-id` flag override. Worker-injected env vars suffice.
- Backfilling MNG-98 splitting outputs (agent created them via direct
  Linear API; they're correct).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
zbigniewsobiecki added a commit that referenced this pull request Apr 16, 2026
…near projects (#1134)

The splitting agent for MNG-98 on llmist (Linear-backed) ran today. Every
`cascade-tools pm <cmd>` call from inside the worker container threw:

```
Error: Linear integration requires teamId in config
    at LinearIntegration.createProvider (file:///app/dist/pm/linear/integration.js:65)
```

The agent fell back to direct Linear API calls and got the work done, but
the CLI gadget path was broken for every Linear-backed project. Three
combined causes:

1. `src/cli/base.ts` cast `process.env.CASCADE_PM_TYPE` as `'trello' |
   'jira' | undefined` — but `secretBuilder.ts` injects the actual
   `project.pm.type`, so `'linear'` was arriving unboxed at runtime.
2. The `pmProject` synthesis only had a JIRA conditional spread; for
   `pmType === 'linear'` it produced `{ pm: { type: 'linear' } }` with no
   `linear` field. `LinearIntegration.createProvider` then read
   `getLinearConfig(project)` → undefined → threw.
3. Even after the synthesis was fixed, no `withLinearCredentials` wrap
   meant gadgets calling Linear API would fail with "No Linear credentials
   in scope". Lines 44-58 wrapped GitHub/Trello/JIRA but not Linear.

And on the worker-spawn side, `secretBuilder.ts:46-53` injected
`CASCADE_JIRA_*` env vars from `getJiraConfig(project)` but had no Linear
equivalent — so even if the CLI tried to read them, they wouldn't be
there.

This is the third generation of the same architectural omission: the CLI
was Trello/JIRA-aware long before Linear existed (spec 006). PR #1131
unblocked the registry (cascade-tools commands actually load), PR #1133
tightened provider validation — both made the latent bug louder. Now
LinearIntegration throws cleanly when it has no teamId, and that throw
surfaces here.

Fix mirrors the JIRA pattern end-to-end:

- `src/backends/secretBuilder.ts` — when `getLinearConfig(project)` is
  truthy, inject `CASCADE_LINEAR_TEAM_ID`, optional
  `CASCADE_LINEAR_PROJECT_ID`, and `CASCADE_LINEAR_STATUSES` into the
  worker's env. Mirrors the JIRA injection block.

- `src/cli/base.ts`:
  - Replace the `'trello' | 'jira' | undefined` type lie with `PMType |
    undefined` (canonical type already exists in `src/pm/types.ts:6`),
    so future provider additions can't reintroduce this footgun.
  - Add a `withLinearCredentials` wrap when `LINEAR_API_KEY` is set,
    mirroring the GitHub/Trello/JIRA wrap pattern.
  - Add a Linear-config synthesis branch, reading
    `CASCADE_LINEAR_TEAM_ID`/`PROJECT_ID`/`STATUSES`.
  - Add `LINEAR_API_KEY`-based pmType inference as a fallback when
    `CASCADE_PM_TYPE` isn't set (mirrors the JIRA-baseUrl inference).
    Worker-spawned use is unaffected because `secretBuilder` always sets
    `CASCADE_PM_TYPE` explicitly; this just helps human-invoked CLI use.
  - Refactor `run()` into three small helpers
    (`wrapWithCredentialScopes`, `resolvePmType`,
    `synthesizeProjectFromEnv`) to keep cognitive complexity inside
    biome's threshold.

Tests: +3 secretBuilder Linear-injection tests, +3 credential-scoping
Linear tests (withLinearCredentials wrap, populated linear synthesis,
LINEAR_API_KEY-based inference). 7851 unit + 524 integration tests pass.
Lint + typecheck + build clean.

Out of scope:
- Refactoring CLI to load full project config from DB instead of
  synthesising from env vars. Bigger change; env-var pattern works for
  JIRA and is the established convention.
- `--linear-team-id` flag override. Worker-injected env vars suffice.
- Backfilling MNG-98 splitting outputs (agent created them via direct
  Linear API; they're correct).

Co-authored-by: Claude Opus 4.6 (1M context) <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