Skip to content

feat(linear): support custom workflow statuses#1364

Open
maksymilian-majer wants to merge 16 commits into
mongrel-intelligence:devfrom
maksymilian-majer:dev
Open

feat(linear): support custom workflow statuses#1364
maksymilian-majer wants to merge 16 commits into
mongrel-intelligence:devfrom
maksymilian-majer:dev

Conversation

@maksymilian-majer
Copy link
Copy Markdown
Contributor

@maksymilian-majer maksymilian-majer commented May 12, 2026

Summary

Adds database-backed workflow status definitions and wires them through Linear project setup, custom agent dispatch, dashboard stats filters, prompt loading, and CLI management. This lets Linear statuses map to custom Cascade workflow stages and custom agents while preserving the built-in statuses as immutable defaults.

Also routes manual/retry runs through the normal execution lifecycle so successful implementation retries still create/link PRs and advance the work item lifecycle.

Screenshots

Custom statuses

Screenshot 2026-05-12 at 20 48 03

Status mapping to agent

Screenshot 2026-05-12 at 20 48 52

Custom agent triggers

Screenshot 2026-05-12 at 20 49 29

Test Plan

  • Unit tests pass (npm test)
  • Linter passes (npm run lint)
  • Type check passes (npm run typecheck)
  • Tested manually: configured Linear custom statuses/agents locally, exercised PRD/story/phased-plan/implementation flow, verified retry/manual implementation lifecycle behavior

Additional verification run locally:

  • npm run build
  • npm run verify
  • pre-push npm run test:fast

Checklist

  • My code follows the project's code style
  • I have added tests for new functionality
  • I have updated documentation if needed
  • My commits follow Conventional Commits

@codecov
Copy link
Copy Markdown

codecov Bot commented May 12, 2026

@zbigniewsobiecki zbigniewsobiecki requested a review from aaight May 13, 2026 10:34
Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Requesting changes: the Linear wizard no longer exposes the existing friction status slot once workflow statuses load, which regresses friction reporting setup.

Code Issues

Should Fix

  • web/src/components/projects/pm-providers/linear/wizard.ts:184 — Switching the Linear mapping UI to workflowStatuses.list drops the friction slot because BUILTIN_WORKFLOW_STATUSES does not include it. statuses.friction is still part of the Linear config contract and is required by friction materialization, so new Linear projects can no longer configure friction reports through the wizard.

🕵️ codex · gpt-5.5 · run details

Comment thread web/src/components/projects/pm-providers/linear/wizard.ts
Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Requesting changes because custom Linear statuses can dispatch but do not participate in lifecycle moves, and Linear setup can auto-enable backlog-manager unexpectedly.

Code Issues

Should Fix

  • src/pm/linear/integration.ts:92 - Custom workflow statuses saved in linearConfig.statuses are not included in the normalized lifecycle config. A custom agent can declare lifecycle.moveOnSuccess: story because the schema accepts arbitrary strings, but PMLifecycleManager.handleSuccess() resolves moves through pmConfig.statuses[moveOnSuccess]; for story that lookup is undefined, so safeMove skips the transition. Any PRD/story/phased-plan style flow that relies on the normal lifecycle will dispatch when a user manually moves into the custom state, but it will not advance to the next custom Linear state on success. Include custom status mappings in resolveLifecycleConfig and widen ProjectPMConfig.statuses accordingly.
  • web/src/components/projects/pm-providers/save-trigger-configs.ts:30 - Linear setup now builds trigger configs for every mapped workflow status with an agent type. Since BUILTIN_WORKFLOW_STATUSES gives backlog agentType: 'backlog-manager', a project that maps Backlog during Linear setup gets a DB override enabling backlog-manager for pm:status-changed, even though that trigger's YAML default is false and the Trello/JIRA setup paths still only enable implementation/splitting/planning. After that, moving any Linear issue into Backlog can fire backlog-manager and pull work forward unexpectedly. Filter default-disabled built-ins such as backlog-manager out of the auto-created configs, or make the allowlist explicit.

🕵️ codex · gpt-5.5 · run details

Comment thread web/src/components/projects/pm-providers/save-trigger-configs.ts
@maksymilian-majer
Copy link
Copy Markdown
Contributor Author

Updated the PR after the latest review pass.

Changes pushed in 5dc43e73:

  • Linear lifecycle config now preserves all configured status mappings, including custom workflow status keys, so lifecycle moves can target custom statuses like prd, story, or phased-plan.
  • The PM wizard trigger setup now auto-enables only the default built-in status triggers that should start runs (splitting, planning, todo). Built-in slots such as backlog remain opt-in, while custom mapped statuses with agent definitions still get trigger configs.
  • Added regression coverage for custom Linear mappings, lifecycle prepare/success moves to custom keys, built-in friction/no-agent status metadata, and trigger auto-enable behavior.

Verification:

  • npm test -- tests/unit/pm/linear-integration.test.ts tests/unit/pm/linear/integration.test.ts tests/unit/pm/lifecycle.test.ts tests/unit/pm/lifecycle-config-shape.test.ts tests/unit/web/pm-wizard-hooks.test.ts tests/unit/pm/workflow-statusDefinitions.test.ts
  • npm run typecheck
  • pre-push npm run test:fast -- --changed origin/dev ... passed: 316 files, 6084 tests, 23 skipped.

Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Requesting changes: manual/retry runs now always use PM lifecycle defaults, which regresses PR-based agent retries by mutating linked PM work items.

Code Issues

Should Fix

  • src/triggers/shared/manual-runner.ts:152 — PR-based manual/retry runs now enter runAgentExecutionPipeline without the GitHub execution config. The normal GitHub path explicitly sets skipPrepareForAgent, skipHandleFailure, and handleSuccessOnlyForAgentType so review/respond-to-ci/respond-to-review runs do not add/remove PM lifecycle labels or post PM failure comments. A retry of a linked review run can now resolve the work item from the PR, call prepareForAgent, then handleSuccess, and mark the PM item processing/processed even though no implementation lifecycle occurred. Preserve the GitHub execution config for PR-based manual/retry runs, or scope this new lifecycle behavior to the PM/implementation retry case.

🕵️ codex · gpt-5.5 · run details

Comment thread src/triggers/shared/manual-runner.ts Outdated
@maksymilian-majer
Copy link
Copy Markdown
Contributor Author

Fixed the latest manual/retry lifecycle review issue in 00d95a99.

Changes:

  • PR-based manual/retry runs for non-PM agents now preserve the GitHub execution config: skipPrepareForAgent, skipHandleFailure, and handleSuccessOnlyForAgentType: 'implementation'.
  • PM-focused PR-based agents keep the default PM lifecycle, so implementation-style retries can still advance the work item correctly.
  • Deleting a custom DB-backed agent now clears matching workflow_status_definitions.agent_type references to avoid stale dispatch mappings. No FK and no migration were added, because YAML-only agents remain valid references.
  • Added regression coverage for both areas.

Verification:

  • npm run typecheck
  • npm test -- tests/unit/triggers/manual-runner.test.ts tests/unit/triggers/github-integration.test.ts tests/unit/triggers/agent-execution.test.ts tests/unit/api/routers/agentDefinitions.test.ts tests/unit/db/repositories/workflowStatusDefinitionsRepository.test.ts
  • TEST_DATABASE_URL=postgresql://cascade_test:cascade_test@127.0.0.1:5433/cascade_test npx vitest run --project integration tests/integration/db/workflowStatusDefinitionsRepository.test.ts

Note: pre-push npm run test:fast -- --changed origin/dev was run and reached 315/316 passed files, but stopped on an unrelated timeout in tests/unit/agents/definitions/async-resolver.test.ts seed idempotency. Re-running that specific seed test with a larger timeout passes locally; the test spends about 11.7s in the seed path and exceeded its hard 15s timeout under full-suite load.

Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Requesting changes because a custom workflow status can be mapped to an agent that the trigger resolver will never enable, making the core Linear custom-status dispatch path silently no-op.

Code Issues

Should Fix

  • src/api/routers/workflowStatuses.ts:54validateAgentType only checks that the agent definition exists. A valid custom definition can still have no pm:status-changed trigger; when a Linear issue enters the mapped state, LinearStatusChangedTrigger resolves that agent but checkTriggerEnabledWithParams() delegates to getResolvedTriggerConfig(), which returns null if the definition does not declare the event, even if the wizard inserted an agent_trigger_configs row. The webhook is then skipped. Reject these mappings with a clear error, or make workflow-status dispatch explicitly synthesize/honor status-trigger configs for mapped agents.

🕵️ codex · gpt-5.5 · run details

Comment thread src/api/routers/workflowStatuses.ts Outdated
async function validateAgentType(agentType: string | null | undefined) {
if (!agentType) return;
try {
await resolveAgentDefinition(agentType);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This accepts any existing agent as a workflow-status dispatch target, but the status trigger later goes through checkTriggerEnabledWithParams()/getResolvedTriggerConfig(), which returns null unless that agent definition declares pm:status-changed. A custom agent with triggers: [] is valid and passes this check, so the UI/CLI can save the mapping and even upsert a trigger config, but Linear status changes will silently skip it. Please either validate that the mapped agent supports pm:status-changed here or make the workflow-status dispatch path honor these mappings without requiring a declared trigger.

@maksymilian-majer
Copy link
Copy Markdown
Contributor Author

PR 1364 Workflow Status Audit

PR: #1364

This document describes the intended behavior for custom Linear workflow statuses,
custom dispatch agents, and status-triggered execution. It also traces the
implementation paths that satisfy each acceptance criterion.

Summary

PR 1364 introduces global workflow status definitions. Built-in statuses remain
code-owned, while custom statuses are stored in PostgreSQL and can point at custom
agent definitions. Linear uses the combined built-in plus custom list for status
mapping and dispatch. Trello and JIRA keep their existing fixed setup surfaces.

The critical runtime chain is:

  1. A workflow status definition maps a Cascade status key to an optional agent type.
  2. A Linear project maps that Cascade status key to a Linear workflow state ID.
  3. The Linear status webhook resolves the state ID back to a Cascade status key.
  4. The workflow definition resolves the dispatch agent.
  5. The trigger config resolver confirms the project has the agent enabled and the
    agent declares/enables pm:status-changed.
  6. The router schedules a coalesced worker job.
  7. The PM lifecycle moves the Linear issue according to the agent's lifecycle hooks.

Acceptance Criteria

AC1: Global workflow statuses merge built-ins and custom DB rows

  • Built-in statuses are always available in stable order: backlog, splitting,
    planning, todo, inProgress, inReview, done, merged, alerts, friction.
  • Custom DB statuses are appended in sortOrder, then key order.
  • Custom DB statuses cannot override built-in keys.
  • Built-ins cannot be updated or deleted through the workflow status API.

Implementation path:

  • src/workflow/statusDefinitions.ts
  • src/api/routers/workflowStatuses.ts
  • src/db/repositories/workflowStatusDefinitionsRepository.ts

Verification:

  • tests/unit/pm/workflow-statusDefinitions.test.ts
  • tests/unit/api/routers/workflowStatuses.test.ts
  • tests/integration/db/workflowStatusDefinitionsRepository.test.ts

AC2: Workflow status dispatch agents are runtime-compatible

  • A workflow status can have no dispatch agent (agentType = null).
  • If an agent type is configured, it must resolve to an existing agent definition.
  • The configured agent must declare pm:status-changed; existence alone is not
    enough because runtime dispatch goes through getResolvedTriggerConfig().
  • Updating or resetting an agent definition must not remove pm:status-changed
    while any workflow status still references that agent.

Implementation path:

  • src/api/routers/workflowStatuses.ts
  • src/api/routers/agentDefinitions.ts
  • src/triggers/config-resolver.ts

Verification:

  • tests/unit/api/routers/workflowStatuses.test.ts
  • tests/unit/api/routers/agentDefinitions.test.ts

AC3: Custom agent deletion does not leave stale workflow status mappings

  • Deleting a DB-backed custom agent clears matching
    workflow_status_definitions.agent_type references.
  • Deleting a YAML-backed built-in agent remains forbidden.
  • No foreign key is added because YAML-only agents are valid dispatch targets but
    do not necessarily have rows in agent_definitions.
  • No defensive migration/backfill is required; this is an application-level
    delete-path cleanup.

Implementation path:

  • src/api/routers/agentDefinitions.ts
  • src/db/repositories/workflowStatusDefinitionsRepository.ts

Verification:

  • tests/unit/api/routers/agentDefinitions.test.ts
  • tests/unit/db/repositories/workflowStatusDefinitionsRepository.test.ts
  • tests/integration/db/workflowStatusDefinitionsRepository.test.ts

AC4: Linear setup exposes custom statuses and creates safe trigger configs

  • Linear status mapping uses workflowStatuses.list, so custom statuses appear in
    the mapping UI.
  • Trello and JIRA continue to use their existing fixed status slots.
  • Linear setup auto-creates missing pm:status-changed trigger configs only for:
    • custom mapped statuses with dispatch agents
    • allowlisted built-ins: splitting, planning, todo
  • Linear setup must not auto-enable backlog-manager merely because Backlog is
    mapped.
  • The global workflow status editor only offers agents that declare
    pm:status-changed.

Implementation path:

  • web/src/components/projects/pm-providers/linear/wizard.ts
  • web/src/components/projects/pm-providers/save-trigger-configs.ts
  • web/src/components/projects/pm-wizard-hooks.ts
  • web/src/routes/global/definitions.tsx

Verification:

  • tests/unit/web/pm-wizard-hooks.test.ts
  • tests/unit/web/global-definitions-route.test.ts

AC5: Linear status-changed webhooks dispatch custom agents

  • Linear Issue create/update events are considered only when a state ID is present.
  • Linear state ID is matched against the project's linear.statuses mapping.
  • The matched Cascade status key resolves through built-in/custom workflow
    definitions.
  • Statuses with no dispatch agent return no trigger result.
  • The resolved agent is checked through checkTriggerEnabledWithParams() for
    pm:status-changed, including project agent enablement, DB override, YAML/DB
    trigger definition, onCreate, and onMove.
  • The worker job uses a coalesce key scoped to (projectId, workItemId).

Implementation path:

  • src/triggers/linear/status-changed.ts
  • src/triggers/shared/pm-status.ts
  • src/triggers/shared/trigger-check.ts
  • src/router/queue.ts

Verification:

  • tests/unit/triggers/linear-status-changed.test.ts
  • tests/unit/triggers/shared/pm-status.test.ts
  • tests/unit/router/queue.test.ts

AC6: Linear ready-label dispatch uses workflow definitions without changing semantics

  • The Linear ready-label trigger still fires only for the configured
    readyToProcess label.
  • It resolves the issue's current state through the same workflow status
    definition path as status-changed dispatch.
  • It still gates on pm:label-added; custom agents that only declare
    pm:status-changed are intentionally not ready-label dispatch targets.

Implementation path:

  • src/triggers/linear/label-added.ts
  • src/triggers/shared/pm-label.ts
  • src/triggers/shared/pm-status.ts

Verification:

  • tests/unit/triggers/linear-label-added.test.ts
  • tests/unit/triggers/shared/pm-status.test.ts

AC7: Linear lifecycle moves support custom status keys

  • Linear lifecycle config preserves all configured status mappings, not just the
    built-in keys.
  • Agent lifecycle hooks can use custom status keys such as story or
    phased-plan.
  • Missing status mappings remain no-op moves through existing safeMove()
    semantics.

Implementation path:

  • src/pm/linear/integration.ts
  • src/pm/lifecycle.ts

Verification:

  • tests/unit/pm/linear/integration.test.ts
  • tests/unit/pm/lifecycle.test.ts
  • tests/unit/pm/lifecycle-config-shape.test.ts

AC8: Manual and retry runs preserve the correct lifecycle semantics

  • PM-focused manual/retry runs use the normal PM lifecycle path so implementation
    style retries can move Linear work items.
  • PR-based non-PM agents such as review/respond-to-ci/respond-to-review keep the
    GitHub execution config:
    • skipPrepareForAgent
    • skipHandleFailure
    • handleSuccessOnlyForAgentType: 'implementation'
  • Retrying a linked PR review run must not add/remove PM lifecycle labels or mark
    the Linear issue processed.

Implementation path:

  • src/triggers/shared/manual-runner.ts
  • src/triggers/shared/agent-execution.ts

Verification:

  • tests/unit/triggers/manual-runner.test.ts
  • tests/unit/triggers/agent-execution.test.ts
  • tests/unit/triggers/github-integration.test.ts

AC9: Custom agent prompts and stats work with DB-backed agent types

  • Prompt defaults for DB-backed custom agents load from the agent definition when
    no disk .eta template exists.
  • Stats filters include custom agent types, not just YAML built-ins.

Implementation path:

  • src/api/routers/prompts.ts
  • web/src/components/settings/agent-definition-prompts.tsx
  • web/src/components/projects/stats-filters.tsx
  • web/src/routes/projects/$projectId.stats.tsx

Verification:

  • tests/unit/api/routers/prompts.test.ts
  • tests/unit/web/stats-filters.test.ts

Audit Findings

Fixed locally before push

  • workflowStatuses.create/update now rejects agents that exist but do not
    declare pm:status-changed.
  • agentDefinitions.update and agentDefinitions.reset now reject removing
    pm:status-changed from an agent while any workflow status still references it.
  • The global workflow status editor now filters its agent dropdown to
    status-dispatch-capable agents.

Intentional behavior to keep

  • Trigger configs do not enable an agent by themselves. Runtime dispatch still
    requires an agent_configs row for the project. This matches the existing
    isAgentEnabledForProject() contract and the project Agents UI.
  • Ready-label dispatch remains separate from status-changed dispatch. A custom
    status agent can be status-only unless its definition also declares
    pm:label-added.
  • Trello and JIRA custom workflow status support is out of scope for this PR.

Verification Commands

Targeted checks for this PR surface:

npm test -- \
  tests/unit/api/routers/workflowStatuses.test.ts \
  tests/unit/api/routers/agentDefinitions.test.ts \
  tests/unit/db/repositories/workflowStatusDefinitionsRepository.test.ts \
  tests/unit/triggers/linear-status-changed.test.ts \
  tests/unit/triggers/linear-label-added.test.ts \
  tests/unit/triggers/shared/pm-status.test.ts \
  tests/unit/triggers/manual-runner.test.ts \
  tests/unit/triggers/agent-execution.test.ts \
  tests/unit/triggers/github-integration.test.ts \
  tests/unit/pm/lifecycle.test.ts \
  tests/unit/pm/linear/integration.test.ts \
  tests/unit/pm/lifecycle-config-shape.test.ts \
  tests/unit/web/pm-wizard-hooks.test.ts \
  tests/unit/web/global-definitions-route.test.ts \
  tests/unit/api/routers/prompts.test.ts \
  tests/unit/web/stats-filters.test.ts
TEST_DATABASE_URL=postgresql://cascade_test:cascade_test@127.0.0.1:5433/cascade_test \
  npx vitest run --project integration \
  tests/integration/db/workflowStatusDefinitionsRepository.test.ts
npm run typecheck

Known local caveat: full changed-suite pre-push previously reached 315/316 test
files and failed on an unrelated timeout in
tests/unit/agents/definitions/async-resolver.test.ts. The specific seed test
passes locally with a larger timeout.

Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Requesting changes because the new coalesced-dispatch lock shortcut can still enqueue duplicate same-type work when a pending job exists for the same coalesce key.

Code Issues

Blocking

  • src/router/webhook-trigger-outcomes.ts:193 — When any pending coalesced job exists for the work item, the code skips checkDispatchLocks() entirely. That is safe only for the pending job's own in-memory lock; it also skips real active same-type work-item locks and agent-type concurrency limits, so a replacement delayed job can be scheduled while the same agent is already running.

🕵️ codex · gpt-5.5 · run details

Comment thread src/router/webhook-trigger-outcomes.ts Outdated
}

try {
const hasPendingForKey = await hasPendingCoalescedJob(result.coalesceKey);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hasPendingForKey shortcut treats any pending coalesced job for the key as if it is the only lock that needs to be ignored, so it skips checkDispatchLocks() entirely. That also skips real active same-type and agent-concurrency locks. For example: an implementation run for TF-38 is already active, a delayed planning job exists for ats:TF-38, then the issue is moved back to a status mapped to implementation. hasPendingCoalescedJob() returns true, this branch skips the active implementation lock, and the replacement delayed implementation job is added; when it fires there is no later lock check. The supersede path needs to release or ignore only the pending job's own lock and still check for active/other concurrency before adding the replacement.

Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Requesting changes for one remaining workflow-status compatibility gap.

Code Issues

Should Fix

  • src/api/routers/agentDefinitions.ts:181 — The new compatibility guard is not applied on agentDefinitions.create. This endpoint explicitly supports creating DB overrides for YAML-backed built-in agent types (isBuiltinAgentType(input.agentType) is passed through on create, and there is a unit test for implementation). On a fresh DB, a superadmin can create an implementation override with triggers: []; the built-in todo -> implementation workflow status still exists, but checkTriggerEnabledWithParams() later returns disabled/null because the definition no longer declares pm:status-changed, so Linear moves into Todo silently stop dispatching implementation. The update/reset paths now prevent this, but create needs the same assertWorkflowStatusDispatchCompatibility(input.agentType, input.definition) check before upsertAgentDefinition.

🕵️ codex · gpt-5.5 · run details

Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Requesting changes for a coalesced PM dispatch lock bug: the replacement path can clear the pending job's lock and then fail to schedule, leaving the still-pending job unlocked and allowing duplicate same-type dispatches.

Code Issues

Should Fix

  • src/router/webhook-trigger-outcomes.ts:199 — The pending job's locks are released before the replacement job is guaranteed to be scheduled. If scheduleCoalescedJob() throws before removing the old BullMQ job, the catch path does not restore those locks, so the old delayed job remains queued without its in-memory work-item/agent-type protection and a later webhook can enqueue a duplicate run.

🕵️ codex · gpt-5.5 · run details

projectId: project.id,
result: result as TriggerResult & { agentType: string },
});
if (shouldIgnorePendingOwnLock) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This releases the pending job's in-memory locks before scheduleCoalescedJob() has actually replaced the BullMQ job. If the later schedule call throws, for example Redis fails during its second getDelayed/getWaiting call before the old delayed job is removed, the catch path logs and returns without restoring the locks. The old pending job remains queued but no longer has the work-item/agent-type lock, so a follow-up webhook can pass checkDispatchLocks() and enqueue a duplicate same-type run for the same work item. Please restore the released pending locks on scheduling failure when the old job is still pending, or avoid mutating the lock maps until the replacement has succeeded.

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