Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6c215e3
feat(linear): support custom workflow statuses
maksymilian-majer May 10, 2026
322d176
fix: support custom agent prompt defaults
maksymilian-majer May 10, 2026
447dbf4
fix: include custom agents in stats filter
maksymilian-majer May 11, 2026
2bb0789
fix: include work item id in create result
maksymilian-majer May 11, 2026
eaa184d
feat: add workflow status CLI
maksymilian-majer May 11, 2026
443c813
fix: block duplicate coalesced dispatches
maksymilian-majer May 12, 2026
01fd20a
fix: route manual runs through execution lifecycle
maksymilian-majer May 12, 2026
8317340
fix: align workflow status setup with provider metadata
maksymilian-majer May 12, 2026
ec8da4a
test: improve workflow status coverage
maksymilian-majer May 12, 2026
3805432
fix: restore friction slot in Linear status mapping wizard
maksymilian-majer May 13, 2026
5dc43e7
fix: preserve custom workflow status behavior
maksymilian-majer May 13, 2026
00d95a9
fix: address workflow status review regressions
maksymilian-majer May 13, 2026
d4da4d8
fix: validate workflow status dispatch agents
maksymilian-majer May 13, 2026
166b025
fix: close workflow status dispatch gaps
maksymilian-majer May 14, 2026
e70ecdb
fix: preserve locks during coalesced supersede
maksymilian-majer May 14, 2026
baf475f
fix: guard agent definition create dispatch compatibility
maksymilian-majer May 14, 2026
6ad2ede
fix: address feedback
May 18, 2026
d92e06a
feat(jira): route custom mapped statuses through workflow-definition …
aaight May 18, 2026
58108f9
fix(pm): preserve custom status mappings in Trello/JIRA lifecycle con…
aaight May 18, 2026
a64644a
feat(trello): route custom mapped lists to workflow-definition agents…
aaight May 18, 2026
9a99537
feat(pm-wizard): map custom workflow statuses in Trello and JIRA wiza…
aaight May 18, 2026
e865e34
docs: custom workflow status mapping parity across PM providers (#1377)
aaight May 18, 2026
9f6fb8d
fix(triggers): revalidate work-item freshness before implementation d…
May 18, 2026
7d6ca16
fix: address feedback
May 18, 2026
0ca616f
Merge pull request #1378 from mongrel-intelligence/fix/MNG-1053-imple…
zbigniewsobiecki May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ Some triggers take params (e.g. `review` + `scm:check-suite-success` accepts `{"

**Work-item concurrency lock** — the router prevents duplicate agent runs via a per-agent-type lock on `(projectId, workItemId, agentType)`. Only same-type duplicates are blocked; **different agent types can run concurrently** on the same work item (e.g. review starts while implementation's container is still cleaning up). The lock has a 30-minute TTL hard ceiling that auto-clears stale entries after router restart.

**Implementation freshness gate** — MNG-1053. PM router adapters intentionally embed a pre-resolved `TriggerResult` for delayed/coalesced PM jobs, so the work-item lock alone cannot prevent a stale implementation snapshot from running. The shared execution pipeline at `src/triggers/shared/agent-execution.ts` now runs a worker-side freshness gate (`src/triggers/shared/implementation-freshness-gate.ts`) before `persistAgentWorkItemLinks()` / `prepareForAgent()`. The gate only fires for `agentType === 'implementation'` with a resolved `workItemId` — review/respond-to-* and follow-up agents bypass it. It reloads live PM work-item state and terminal checklists (`Implementation Steps`, `Acceptance Criteria`), counts active same-type runs, and verifies linked PRs by resolving the implementer GitHub persona token before calling `githubClient.getPR()` (so manual/retry pipeline callers do not depend on ambient GitHub scope). Open or merged PRs and fully-complete terminal checklists block dispatch with a durable `Implementation not started:` PM comment (updating the existing ack comment when present). Checklist read uncertainty always falls into `needs_human_reconciliation`; PR lookup uncertainty does the same when a DB/run-linked PR candidate exists. Closed-unmerged PRs do NOT permanently block reimplementation.

**Post-completion review dispatch** — when an implementation agent succeeds with a PR, the execution pipeline checks CI status and fires the review agent deterministically (before the container exits). This guarantees review dispatch within seconds of implementation completion, regardless of GitHub webhook timing. Uses the same `claimReviewDispatch` dedup key as the `check-suite-success` trigger, so the two paths cannot double-enqueue.

**Deferred re-check** — a trigger handler can return `TriggerResult.deferredRecheck: { delayMs, coalesceKey, recheckKind? }` (with `agentType: null`) to schedule a bare delayed job via `scheduleCoalescedJob`. The router scheduling is adapter-agnostic, but **bare re-dispatch is currently GitHub-only**: `GitHubRouterAdapter.buildJob()` strips `triggerResult` from the job so the GitHub worker re-dispatches through the trigger registry for fresh provider state. Non-GitHub adapters (Trello, JIRA, Linear, Sentry) embed `triggerResult` in the job; their workers pass it to `resolveTriggerResult()`, which returns the pre-resolved `agentType: null` result without re-dispatching — a non-GitHub handler using this field would schedule a job that reuses the same result rather than re-evaluating provider state. There are two recheck kinds, controlled by the optional `recheckKind` field on `deferredRecheck`: **mergeability re-check** (no `recheckKind`, sets `mergeabilityRecheckAttempt: 1` on the job) — one-shot; if the re-check still cannot resolve state, the worker Sentry-captures under `mergeability_recheck_exhausted` and stops without re-queueing. **Check-suite re-check** (`recheckKind: 'check-suite'`, sets `checkSuiteRecheckAttempt: 1` on the job) — safe rescheduling; if the Actions API is still stale when the job fires, the worker reschedules another coalesced delayed job instead of exhausting, so review/respond-to-ci dispatch stays alive until the API catches up. Used by `check-suite-success` and `check-suite-failure` handlers for the Actions-API-lag case (ucho PR #394/MNG-683, 2026-05-11).
Expand Down
5 changes: 4 additions & 1 deletion docs/architecture/03-trigger-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@ flowchart TD
A[Trigger matched] --> B[Guard and context setup]
B --> C[Validation and budget preflight]
C -->|Blocked| D[Notify PM/callbacks and stop]
C -->|Allowed| E[Persist work-item and PR links]
C -->|Allowed| FG[Implementation freshness gate]
FG -->|Blocked| FGSKIP[Post durable PM skip comment and stop]
FG -->|Passed| E[Persist work-item and PR links]
E --> F[PM lifecycle: prepareForAgent]
F --> G[Run agent via engine]
G --> H[Post-run side effects]
Expand All @@ -231,6 +233,7 @@ flowchart TD
This includes:
- Context setup in `agent-execution-runtime.ts`: build the `PMLifecycleManager`, load agent lifecycle hooks, and re-resolve `workItemId` from PR links when a webhook arrived before the DB mapping existed.
- Validation and lifecycle preflight in `agent-execution-lifecycle.ts`: validate PM/SCM integrations, notify PM/callbacks on validation failure, check `workItemBudgetUsd`, and run `prepareForAgent`.
- Implementation freshness gate in `implementation-freshness-gate.ts` (MNG-1053): only fires for `agentType === 'implementation'` with a resolved `workItemId`. Reloads the live PM work item, terminal checklists (`Implementation Steps`, `Acceptance Criteria`), and `agent_runs` ownership state, then verifies any linked PRs through GitHub using the implementer persona token. Open or merged PRs, fully-completed terminal checklists, and active same-type runs short-circuit dispatch with a stable `Implementation not started:` PM comment (updating the existing ack comment when present, posting a new comment otherwise). Checklist read uncertainty always falls into `needs_human_reconciliation`; PR lookup uncertainty does the same when a DB/run-linked PR candidate exists. Closed-unmerged PRs do NOT permanently block reimplementation.
- Work-item and PR traceability in `agent-work-items.ts`: create/update work-item records, maintain PR/work-item links before and after execution, fetch PR titles, and backfill run PR numbers.
- Agent execution in `agent-execution-runtime.ts`: call `runAgent()` with the resolved input plus project, config, and remaining budget.
- Post-run PM behavior in `agent-pm-summary.ts` and `agent-execution-lifecycle.ts`: post review/output summaries to the PM work item, handle artifacts, post budget warnings, clean up processing state, and call `handleSuccess` or `handleFailure`.
Expand Down
45 changes: 45 additions & 0 deletions docs/architecture/04-agent-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,51 @@ Key functions:
- `resolveAllAgentDefinitions()` — merge DB + YAML
- `resolveKnownAgentTypes()` — list all known types

### CLI management

Custom agent definitions and custom workflow status definitions can be managed
without the dashboard UI:

```bash
# Register a custom agent definition from YAML or JSON
cascade definitions create --agent-type prd --file prd-agent.yaml

# Import-or-update an agent definition from an exported file
cascade definitions import --file prd-agent.yaml --update

# Register a custom workflow status that dispatches that agent
cascade workflow-statuses create \
--key prd \
--label PRD \
--agent-type prd \
--sort-order 1000

# Inspect or update workflow status dispatch
cascade workflow-statuses list
cascade workflow-statuses update prd --agent-type story
cascade workflow-statuses update prd --no-agent
```

Built-in workflow statuses cannot be modified through the CLI; create custom
statuses for project-specific workflows and map them in the PM integration.

### Custom workflow statuses across PM providers

CASCADE separates two concepts that custom workflows need both of:

1. **The status definition itself** — `key`, `label`, dispatch `agentType`, and `sortOrder`. Built-in definitions live in `BUILTIN_WORKFLOW_STATUSES` (`src/workflow/statusDefinitions.ts`); custom ones live in the `workflow_status_definitions` table and are managed via `cascade workflow-statuses {create,list,update,delete}` or the superadmin tRPC router at `src/api/routers/workflowStatuses.ts`.
2. **The provider-native mapping** — the actual Trello list, JIRA status, or Linear workflow state that the custom status corresponds to on the board. This lives in the PM integration config (`project_integrations.config`) under the same provider-native key shape used for built-in statuses; see [`08-config-credentials.md`](./08-config-credentials.md#custom-workflow-status-mappings) for the per-provider storage layout.

All three production providers (Trello, JIRA, Linear) support custom statuses with the same dispatch contract:

- **Trello** (`src/triggers/trello/status-changed.ts`) — `TrelloCustomStatusChangedTrigger` matches `createCard` / `updateCard` events whose destination list ID maps to a custom (non-built-in) key in `trello.lists.<customKey>`, then resolves the dispatch agent through `resolvePMStatusAgentByIdFromWorkflowDefinitions`. Built-in keys (e.g. `todo`, `planning`) continue to flow through the per-list `TrelloStatusChanged*Trigger` handlers.
- **JIRA** (`src/triggers/jira/status-changed.ts`) — `JiraStatusChangedTrigger` resolves the new status name against `jira.statuses` via `resolvePMStatusAgentByNameFromWorkflowDefinitions`, picking up custom keys alongside built-ins.
- **Linear** (`src/triggers/linear/status-changed.ts`) — `LinearStatusChangedTrigger` resolves the new state UUID against `linear.statuses` via `resolvePMStatusAgentByIdFromWorkflowDefinitions`.

All three paths share `resolvePMStatusAgentFromWorkflowDefinitions` in `src/triggers/shared/pm-status.ts` and obey the same dispatch precondition: a custom status only dispatches an agent when its definition has a non-null `agentType` AND a `pm:status-changed` trigger config is enabled for that agent. A custom status with `agentType: null` (created via `cascade workflow-statuses update <key> --no-agent` or set without `--agent-type`) renders in the wizard and persists in the provider config, but the trigger handlers return `null` instead of dispatching — useful for board columns that should appear in CASCADE's wizard without spawning agents.

The PM wizards (Trello, JIRA, Linear) pull the full definition list via `trpc.workflowStatuses.list` and render mapping rows for every key — built-in and custom alike. Saving the wizard auto-enables the `pm:status-changed` trigger config for any custom-status agent the operator mapped, through `buildMissingStatusTriggerConfigs` (`web/src/components/projects/pm-providers/save-trigger-configs.ts`). See `src/integrations/README.md` for the provider parity contract.

## Built-in Agents

| Agent | Capabilities | Persona | Key Triggers |
Expand Down
39 changes: 39 additions & 0 deletions docs/architecture/08-config-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,45 @@ active PM provider; if that scope is missing the gate fails closed and captures
Sentry under `pipeline_capacity_gate_no_pm_provider`. See
`src/triggers/shared/pipeline-capacity-gate.ts`.

### Custom workflow status mappings

Operators can register custom workflow statuses (e.g. `prd`, `story`,
`phased-plan`) through `cascade workflow-statuses create` or the superadmin
`workflowStatuses.create` tRPC mutation. The definition itself — `key`,
`label`, optional dispatch `agentType`, `sortOrder` — lives in the
`workflow_status_definitions` table alongside the built-in catalog
(`BUILTIN_WORKFLOW_STATUSES` in `src/workflow/statusDefinitions.ts`). See
[`04-agent-system.md`](./04-agent-system.md#custom-workflow-statuses-across-pm-providers)
for the dispatch contract.

The provider-native mapping for each custom key is stored in
`project_integrations.config` under the same key shape used for built-in
slots — there is no separate side table for custom keys:

| Provider | Custom key location | Value shape |
|---|---|---|
| Trello | `lists.<customKey>` | Trello list ID |
| JIRA | `statuses.<customKey>` | JIRA status name |
| Linear | `statuses.<customKey>` | Linear workflow state UUID |

For example, after `cascade workflow-statuses create --key prd --label PRD
--agent-type prd`, a Trello project might persist `lists.prd: "5f8a..."`
in the integration config, while the equivalent JIRA project persists
`statuses.prd: "PRD Ready"` and the Linear project persists
`statuses.prd: "f3c1-..."`. The lifecycle config resolvers in
`src/pm/trello/integration.ts`, `src/pm/jira/integration.ts`, and
`src/pm/linear/integration.ts` spread the full `lists` / `statuses` record so
custom keys survive normalization and reach the `moveOnPrepare` /
`moveOnSuccess` lifecycle hooks for custom agents.

A custom mapped status only dispatches an agent when its definition has a
non-null `agentType` AND the project has an enabled `pm:status-changed`
trigger config for that agent. The PM wizard's save path auto-creates the
missing trigger config when the operator maps a dispatch-capable custom
status, via `buildMissingStatusTriggerConfigs`. Custom statuses with
`agentType: null` render and save normally but the trigger handlers return
`null` instead of dispatching.

## Credential Resolution

CASCADE uses a two-tier credential resolution system, selecting the appropriate resolver based on execution context.
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture/09-database.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ erDiagram
| `agent_configs` | Per-agent-type overrides per project | UNIQUE(`project_id`, `agent_type`), `project_id NOT NULL` |
| `agent_definitions` | Agent YAML definitions (built-in + custom) | UNIQUE(`agent_type`) |
| `agent_trigger_configs` | Trigger enable/disable + parameters per project/agent/event | UNIQUE(`project_id`, `agent_type`, `event`) |
| `workflow_status_definitions` | Custom workflow status definitions (key, label, optional dispatch `agent_type`, sort order). Built-in statuses live in code (`BUILTIN_WORKFLOW_STATUSES`); this table only stores custom definitions. | UNIQUE(`status_key`) |
| `agent_runs` | Agent execution records with status, cost, duration | Indexed on `project_id`, `status`, `started_at` |
| `agent_run_logs` | Cascade log + engine log per run | One-to-one with `agent_runs` |
| `agent_run_llm_calls` | LLM request/response pairs with token/cost tracking | — |
Expand Down Expand Up @@ -157,6 +158,7 @@ Each table has a dedicated repository providing typed query methods. Key reposit
| `agentConfigsRepository` | Per-agent settings CRUD |
| `agentDefinitionsRepository` | Agent definition CRUD (YAML ↔ JSONB) |
| `agentTriggerConfigsRepository` | Trigger enable/disable/params per project/agent/event |
| `workflowStatusDefinitionsRepository` | Custom workflow status definition CRUD; backs `cascade workflow-statuses *` and the `workflowStatuses` tRPC router |
| `integrationsRepository` | Query integration configuration |
| `projectsRepository` | Project CRUD |
| `organizationsRepository` | Organization CRUD |
Expand Down
8 changes: 8 additions & 0 deletions docs/architecture/10-resilience.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ Prevents multiple agents from working on the same card/issue simultaneously. The
- Key: `(projectId, workItemId, agentType)`
- Only same-agent duplicates are blocked; different agent types may run concurrently on the same work item

### Implementation freshness gate (worker-side)

`src/triggers/shared/implementation-freshness-gate.ts`

The router-level work-item lock catches in-flight duplicates of the same agent type, but cannot see (a) a sibling implementation run that already completed via post-completion review chaining or a manual run, (b) a terminal checklist (`Implementation Steps`, `Acceptance Criteria`) that an operator finished while the dispatch sat in the PM coalesce window, or (c) a PR that already exists for the work item. PM router adapters intentionally embed a pre-resolved `TriggerResult` for delayed/coalesced PM jobs, so the freshness gate runs the last-mile check inside the worker execution pipeline before `persistAgentWorkItemLinks()` and `prepareForAgent()`.

The gate is implementation-only (`agentType === 'implementation'` plus a resolved `workItemId`); follow-up agents — `review`, `respond-to-review`, `respond-to-ci`, `respond-to-pr-comment` — bypass it and keep their existing dispatch path. When the gate fires, it reloads the live PM work item / checklists, counts active same-type runs, looks up linked PR candidates from `pr_work_items` + recent runs, and verifies PR state through `githubClient.getPR()` inside a freshly resolved implementer-token scope. Open or merged PRs and fully-complete terminal checklists return `already_implemented` or `implementation_pr_exists`; checklist read uncertainty always returns `needs_human_reconciliation`, and PR lookup uncertainty does the same when a DB/run-linked PR candidate exists. The pipeline posts a durable PM comment (updating the existing ack comment when present, otherwise adding a new one) prefixed with `Implementation not started:` and exits normally so router cleanup releases locks without retrying.

### Agent-type concurrency limit

`src/router/agent-type-lock.ts`
Expand Down
36 changes: 36 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,42 @@ node bin/cascade.js projects trigger-set my-project \
node bin/cascade.js projects trigger-discover --agent implementation
```

### Map a custom workflow status

You can add columns/states beyond the built-in CASCADE stages (Backlog → Splitting → Planning → Todo → In Progress → In Review → Done → Merged) and have them dispatch any custom or built-in agent. This works for Trello, Jira, and Linear with the same wiring:

1. **Register the custom status definition** (superadmin) so the wizard offers a mapping row for it:

```bash
# Dispatches the `prd` agent — the agent must declare a `pm:status-changed` trigger
node bin/cascade.js workflow-statuses create \
--key prd --label PRD --agent-type prd --sort-order 1000

# Or render-only — no agent dispatches when work items land in this status
node bin/cascade.js workflow-statuses create --key icebox --label Icebox

node bin/cascade.js workflow-statuses list
```

2. **Map the custom key to a provider-native list/status** in the PM wizard's **Status Mapping** step. The wizard renders a row for every registered status (built-in + custom) and saves the mapping in the same provider-native shape:

- **Trello** stores the list ID under `lists.prd`
- **Jira** stores the status name under `statuses.prd`
- **Linear** stores the workflow state UUID under `statuses.prd`

3. **Verify trigger config readiness**. Saving the wizard auto-enables `pm:status-changed` for any custom-status agent the operator just mapped. Confirm via the dashboard's **Agent Configs** tab, or:

```bash
node bin/cascade.js projects trigger-list my-project
# Look for: agent=prd event=pm:status-changed enabled=true

# If missing, enable manually:
node bin/cascade.js projects trigger-set my-project \
--agent prd --event pm:status-changed --enable
```

A custom status only dispatches when its definition has an `agent-type` AND the project trigger config for `(agent, pm:status-changed)` is enabled. Statuses created without `--agent-type` (or updated with `--no-agent`) render in the wizard and persist in the provider config, but the trigger handlers return `null` instead of dispatching — useful for board columns CASCADE should know about but never act on.

---

## 11. Test It
Expand Down
2 changes: 2 additions & 0 deletions src/api/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { runsRouter } from './routers/runs.js';
import { usersRouter } from './routers/users.js';
import { webhookLogsRouter } from './routers/webhookLogs.js';
import { webhooksRouter } from './routers/webhooks.js';
import { workflowStatusesRouter } from './routers/workflowStatuses.js';
import { workItemsRouter } from './routers/workItems.js';
import { router } from './trpc.js';

Expand All @@ -33,6 +34,7 @@ export const appRouter = router({
prs: prsRouter,
workItems: workItemsRouter,
users: usersRouter,
workflowStatuses: workflowStatusesRouter,
});

export type AppRouter = typeof appRouter;
Loading
Loading