fix(pm): unify listWorkItems contract so backlog-manager sees Linear cards#1133
Merged
zbigniewsobiecki merged 1 commit intodevfrom Apr 16, 2026
Merged
fix(pm): unify listWorkItems contract so backlog-manager sees Linear cards#1133zbigniewsobiecki merged 1 commit intodevfrom
zbigniewsobiecki merged 1 commit intodevfrom
Conversation
…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>
Codecov Report❌ Patch coverage is
📢 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>
4 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Root cause:
PMProvider.listWorkItems(containerId, filter?)had incompatible per-provider semantics forcontainerId:containerIdmeantproject = "<status name>"returned 0listIssues({ teamId: <state-uuid> })returned 0The snapshot loader (
contextSteps.fetchPipelineLists) calledprovider.listWorkItems(list.id)wherelist.idwas a status identifier — silently returning[]for both JIRA and Linear since the day PR #1131 madebuildPipelineListspopulate Linear statuses.Fix — unify the abstraction
PMProvider.listWorkItems(containerId: string | undefined, filter?)—containerIdis now optional. Each provider self-resolves the natural scope from its own config when omitted: Trello usesconfig.lists[filter.status], JIRA defaults toconfig.projectKey, Linear defaults toconfig.teamId. Thefilter.statusis the CASCADE-canonical key ('backlog','todo', ...), mapped to the provider's native identifier internally.contextSteps.fetchPipelineLists— callsprovider.listWorkItems(undefined, { status: list.statusKey })for all 3 providers in one code path.PipelineListshape:{ name, statusKey }(dropped the deadidfield). Snapshot output now shows## BACKLOG (status: backlog)instead of(list ID: <UUID>)— the agent uses CASCADE keys withmove-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 inisProviderMisconfigured, which preserves the conservative-fallback behaviour and uses an exhaustiveswitchoverPMType(withassertNeverPMTypeindefault) so a 4th provider fails the build.agent-execution.ts:propagateAutoLabelAfterSplitting— same simplification (~25 lines deleted).TrelloPMProviderconstructor now requiresTrelloConfig.TrelloIntegration.createProviderfalls back to an empty config whengetTrelloConfigreturns undefined (the CLI'sCredentialScopedCommandsynthesizes apm: { type: 'trello' }shell with notrellofield 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; updatedlist ID:→status:in output assertions.tests/unit/triggers/shared/backlog-check.test.ts— dropped theSTATUS_KEY_BY_FIXTUREtranslation 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— newcreateMockLinearProject.tests/unit/triggers/agent-execution.test.ts+tests/unit/triggers/shared/agent-execution.test.ts— updated 5 assertions to use the unified call shape; replacedmockResolvedValuewith per-statusmockImplementationso capacity checks see empty in-flight statuses correctly.7845 unit + 524 integration tests pass. lint+typecheck+build clean.
Out of scope
cascade-tools pm list-work-items --containerId X) keeps its existing explicit-containerId form.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 lintcleannpm run buildproduces dist artifacts## BACKLOG (status: backlog)with MNG-97 listed.🤖 Generated with Claude Code