Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Dockerfile.worker
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:22-slim AS builder
FROM node:24-slim AS builder
WORKDIR /app

# Install dependencies (including dev for build)
Expand All @@ -12,7 +12,7 @@ COPY src ./src
RUN npm run build

# Production image
FROM node:22-bookworm AS production
FROM node:24-bookworm AS production
WORKDIR /app

# `cascade.managed=true` is the contract the router's dangling-image cleanup
Expand Down
10 changes: 10 additions & 0 deletions docs/architecture/01-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,13 @@ Every tRPC request builds a context containing:
- `token` — the session token, used by `auth.setActiveOrg` to switch the session's active org

Procedure types enforce auth levels: `publicProcedure`, `protectedProcedure`, `adminProcedure`, `superAdminProcedure`. User-management permission checks additionally consume `resolveActorRoleInOrg()` so the caller's **per-org** membership role — not the global `users.role` — governs (an org admin who switches into an org where they are only a member cannot act as an admin there).

### Cross-process debug-analysis status

Post-mortem [debug analysis](./03-trigger-system.md) runs in a **separate worker container** from the dashboard API that reports its progress. Its running/failed lifecycle is therefore tracked in a **durable, cross-process** signal — the `debug_analysis_status` table (see [09-database](./09-database.md)) — rather than an in-process flag, which would be invisible across the process boundary. An earlier worker-local in-memory `Set` (`debug-status.ts`) was removed for exactly this reason (MNG-1667); status is now read from the table **uniformly in queue mode and local dev**, with no separate non-queue path.

**Status writers** — every `triggerDebugAnalysis()` run (the automatic post-failure path and the manual "Run Analysis" button alike) marks `running` around the analysis, clears the row on success (a persisted `debug_analyses` content row is then the `completed` signal), and marks `failed` on a catchable in-process error. The dashboard additionally marks `running` at manual-trigger time to cover the enqueue→spawn window.

**Status reads** — `runs.getDebugAnalysisStatus` derives status from `debug_analysis_status` plus the `debug_analyses` content row; the re-trigger `CONFLICT` guard reads the same `debug_analysis_status` row (active-`running` check only). BullMQ job state is deliberately **not** consulted: the dashboard job reaches `completed` at container *spawn*, not at analysis completion (the debug agent then runs for tens of seconds to minutes), so it cannot represent a still-running analysis. Read precedence: active `running` → `completed` (a persisted analysis wins) → `failed` → `idle`. A `running` row older than `DEBUG_ANALYSIS_RUNNING_STALE_MS` (2h, comfortably above the 30-min worker timeout) is treated as stale `idle`, so a crashed/OOM-killed worker never wedges the run as permanently `running`. `failed` covers catchable in-process errors only; a hard kill (watchdog/OOM) leaves the `running` row to self-stale to `idle`.

**Deterministic job id (dedup, not status)** — in queue mode the manual trigger enqueues the `debug-analysis` dashboard job under the deterministic id `debug-analysis-<runId>` (`debugAnalysisJobId()` in `src/queue/client.ts`). One job per analyzed run makes the queue self-deduplicating: a re-run removes any prior terminal job and re-submits the same id, and a near-simultaneous second trigger that slips past the guard cannot spawn a duplicate worker container (which would double LLM cost and post a duplicate PM comment). The automatic post-run path instead calls the runner in-process (fire-and-forget) and does not use this job id, but writes the same durable status.
8 changes: 7 additions & 1 deletion docs/architecture/03-trigger-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@ This includes:
- 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`.
- Follow-up dispatch in `agent-execution-followups.ts`: dispatch review after a successful implementation PR once CI is passing and the review dedup key is claimed, and chain backlog-manager after a successful splitting run when the auto label/capacity checks allow it.
- Auto-debug in `agent-auto-debug.ts`: fire-and-forget debug analysis for eligible failed or timed-out runs after callbacks and follow-up dispatch complete.
- Auto-debug in `agent-auto-debug.ts`: fire-and-forget debug analysis for eligible failed or timed-out runs after callbacks and follow-up dispatch complete. It calls the shared `triggerDebugAnalysis()` runner, whose running/failed lifecycle is durable and cross-process — see [Debug-analysis status](#debug-analysis-status-durable-cross-process) below.

Credential scoping still happens before the facade runs. PM webhook handling enters provider credentials and PM provider scope before dispatch; GitHub and Sentry use `webhook-execution.ts` / `credential-scope.ts` to inject LLM keys, PM credentials, PM provider scope, and GitHub persona tokens as needed.

### Debug-analysis status (durable, cross-process)

Post-mortem debug analysis runs in a separate worker container, so its running/failed lifecycle is tracked in the **durable, cross-process** `debug_analysis_status` table (see the [`debug_analysis_status` table](./09-database.md)) rather than an in-process flag invisible to the dashboard process. Both entry points — the automatic `agent-auto-debug.ts` path above and the manual dashboard "Run Analysis" button — drive the shared `triggerDebugAnalysis()` runner, which marks `running` around the analysis, clears the row on success (a persisted `debug_analyses` row is then the `completed` signal), and marks `failed` on a catchable in-process error.

`runs.getDebugAnalysisStatus` reads this table **uniformly in queue mode and local dev** with precedence active `running` → `completed` → `failed` → `idle`; a `running` row older than `DEBUG_ANALYSIS_RUNNING_STALE_MS` (2h) self-heals to `idle` so a crashed worker never wedges the run. The deterministic `debug-analysis-<runId>` dashboard job (queue mode) provides idempotent re-enqueue and double-trigger dedup — **not** the status signal, since a BullMQ job reaches `completed` at container spawn rather than at analysis completion. An earlier in-memory `Set` (`debug-status.ts`) was removed because it was never visible to the dashboard process (MNG-1667). See [Cross-process debug-analysis status](./01-services.md) for the service-level view.
7 changes: 7 additions & 0 deletions docs/architecture/08-config-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ interface ProjectConfig {
}
```

**Run links.** When `runLinksEnabled` is `true`, agent comments carry a subtle dashboard
footer linking back to the run — `/runs/<id>` once a run row exists, or the work-item runs
page `/work-items/<projectId>/<workItemId>` posted at ack time before the worker has committed
the run row. Because such a link can be opened before the run row exists, the work-item runs
page renders a transient "Run is starting…" state and keeps polling through a bounded grace
window rather than flashing a terminal "No runs found".

### Agent update channel

`src/config/updateChannel.ts`
Expand Down
4 changes: 3 additions & 1 deletion docs/architecture/09-database.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ erDiagram
agent_runs ||--o| agent_run_logs : "has"
agent_runs ||--o{ agent_run_llm_calls : "logs"
agent_runs ||--o| debug_analyses : "analyzed by"
agent_runs ||--o| debug_analysis_status : "status"

users ||--o{ sessions : "has"
users ||--o{ org_memberships : "has"
Expand Down Expand Up @@ -151,6 +152,7 @@ erDiagram
| `org_memberships` | Multi-org membership: links a user to an org with a per-org role, so one account can belong to many orgs. `users.org_id`/`users.role` remain the home org + global role. Read by effective-org resolution + the per-org actor-role helper (spec 021 plan 2); written by the grant mutation (`users.addExistingUserToOrg`) and the membership-mirroring create, and read by membership-based member listing (spec 021 plan 3). The listing returns BOTH the per-org `role` and the global `users.role` so the Settings → Users editor keeps targeting the global role until the plan-4 UI reconciliation. The idempotent home-org backfill runs in migration `0053` and is re-run by `0054` so accounts created via the old `createUser` (and bootstrap superadmins) never vanish from the inner-join listing. | UNIQUE(`user_id`, `org_id`) |
| `sessions` | Session tokens for cookie auth (30-day expiry); `active_org_id` (nullable) tracks the org the session is currently acting in for multi-org | — |
| `debug_analyses` | AI debug analysis results | — |
| `debug_analysis_status` | Durable, cross-process lifecycle status (`running` / `failed`) for a debug analysis. The analysis runs in a separate worker container, so an in-memory flag is invisible to the dashboard API; the worker (and the dashboard at trigger time) writes this row instead. It is deleted on success — a present `debug_analyses` row is then the `completed` signal — and a `running` row older than `DEBUG_ANALYSIS_RUNNING_STALE_MS` is treated as stale (`idle`) so a crashed worker never wedges the run. Status read precedence (uniform in queue mode + local dev): active `running` → `completed` (a persisted `debug_analyses` row wins over a stale terminal status row) → `failed` → `idle`. `failed` is written only for catchable in-process errors (the runner's `catch`, plus the pre-runner project-config-load failure); a hard kill (watchdog/OOM) leaves the `running` row to self-stale to `idle` rather than surfacing `failed`, with router-side reconciliation on non-zero container exit the deliberate follow-up. | PK on `analyzed_run_id`, FK → `agent_runs` ON DELETE CASCADE |

## Repositories

Expand All @@ -177,7 +179,7 @@ Each table has a dedicated repository providing typed query methods. Key reposit
| `partialsRepository` | Prompt partial CRUD |
| `prWorkItemsRepository` | PR ↔ work item mapping |
| `webhookLogsRepository` | Webhook audit trail |
| `debugAnalysisRepository` | Debug analysis results |
| `debugAnalysisRepository` | Debug analysis results + durable cross-process analysis lifecycle status (`debug_analysis_status`: mark running/failed, clear on success, read run state, staleness check) |

## Connection Management

Expand Down
9 changes: 8 additions & 1 deletion src/agents/prompts/templates/implementation.eta
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@ You are an expert software engineer implementing features and fixing issues base

### Phase 4: Verify & Ship

7. **Run linting** (with fixing first, then without) **and type checking**
7. **Run type checking and lint changed files only**
- Type check: `npm run typecheck` (or `npx tsc --noEmit`)
- Lint: run the linter directly on changed files — **never** `npm run lint` / `npm run eslint:fix` unscoped; full-tree lint can take 10+ min on large codebases. Scope to changed **and newly-created (untracked)** files: you have not committed yet (commit happens inside `CreatePR` at step 8), so new modules/test files are untracked and `git diff --name-only HEAD` alone would skip them — pick them up with `git ls-files --others --exclude-standard`:
```
{ git diff --name-only HEAD; git ls-files --others --exclude-standard; } \
| grep -E '\.(ts|tsx|js|jsx)$' | sort -u | xargs -r npx eslint --fix --max-warnings 0
```
Swap `eslint --fix --max-warnings 0` for `biome check --write` (or your project's linter) as appropriate; `xargs -r` skips the linter entirely when nothing matches.
8. **Create a PR** using the `CreatePR` gadget (it handles commit, push, and PR creation atomically)
- **FORBIDDEN**: Do NOT run `gh pr create` or `git push` via Tmux — they will FAIL
- IMPORTANT: DO NOT PROCEED FURTHER UNTIL YOU HAVE CONFIRMED the CreatePR output shows a `prUrl`.
Expand Down
2 changes: 2 additions & 0 deletions src/agents/prompts/templates/partials/tmux.eta
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Use the Tmux gadget for ALL shell commands:

**Avoid interactive/watch modes:** Commands run inside Tmux have a TTY, so tools may default to interactive or watch mode. Always pass flags to force non-interactive, single-run execution (e.g., `--run`, `--watchAll=false`, `--no-watch`, `--ci`, or similar). If a session hangs with `status=running` after your command should have finished, it is likely in watch/interactive mode — kill it and retry with the correct flag.

**Do NOT poll indefinitely.** If a command runs much longer than expected and a more targeted scope is possible (e.g. lint only changed files instead of the full project), kill it and retry with narrower scope.

**Command Format:** Pass command as a shell string. Pipes, &&, ||, redirects, and globs all work:
- Simple: `command="npm test"`
- Chained: `command="npm run lint && npm test"`
Expand Down
19 changes: 15 additions & 4 deletions src/agents/shared/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,16 +238,27 @@ export async function setupRepository(options: SetupRepositoryOptions): Promise<
const setupScriptPath = join(repoDir, '.cascade', 'setup.sh');
if (existsSync(setupScriptPath)) {
log.info('Running project setup script', { path: '.cascade/setup.sh', agentType });
const setupResult = await runCommand('bash', [setupScriptPath], repoDir, {
AGENT_PROFILE_NAME: agentType,
});
const setupResult = await runCommand(
'bash',
[setupScriptPath],
repoDir,
{ AGENT_PROFILE_NAME: agentType },
// Disable idle timeout: setup.sh may compile language runtimes (e.g. Ruby via
// asdf/ruby-build) whose make output is suppressed, causing false idle-timeout
// kills. The wall timeout (10 min) remains the safety net for truly hung setups.
{ idleTimeoutMs: 0 },
);
log.info('Setup script completed', {
exitCode: setupResult.exitCode,
reason: setupResult.reason,
stdout: setupResult.stdout.slice(-500),
stderr: setupResult.stderr.slice(-500),
});
if (setupResult.exitCode !== 0) {
log.warn('Setup script exited with non-zero code', { exitCode: setupResult.exitCode });
log.warn('Setup script exited with non-zero code', {
exitCode: setupResult.exitCode,
reason: setupResult.reason,
});
}
}

Expand Down
Loading
Loading