Skip to content

Commit da22a5b

Browse files
Merge pull request #1461 from mongrel-intelligence/dev
chore: release - merge dev into main
2 parents fccd6d4 + e67e8dc commit da22a5b

43 files changed

Lines changed: 1811 additions & 302 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Dockerfile.worker

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:22-slim AS builder
1+
FROM node:24-slim AS builder
22
WORKDIR /app
33

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

1414
# Production image
15-
FROM node:22-bookworm AS production
15+
FROM node:24-bookworm AS production
1616
WORKDIR /app
1717

1818
# `cascade.managed=true` is the contract the router's dangling-image cleanup

docs/architecture/01-services.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,13 @@ Every tRPC request builds a context containing:
177177
- `token` — the session token, used by `auth.setActiveOrg` to switch the session's active org
178178

179179
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).
180+
181+
### Cross-process debug-analysis status
182+
183+
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.
184+
185+
**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.
186+
187+
**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`.
188+
189+
**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.

docs/architecture/03-trigger-system.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,12 @@ This includes:
238238
- Agent execution in `agent-execution-runtime.ts`: call `runAgent()` with the resolved input plus project, config, and remaining budget.
239239
- 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`.
240240
- 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.
241-
- 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.
241+
- 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.
242242

243243
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.
244+
245+
### Debug-analysis status (durable, cross-process)
246+
247+
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.
248+
249+
`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.

docs/architecture/08-config-credentials.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ interface ProjectConfig {
5656
}
5757
```
5858

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

6168
`src/config/updateChannel.ts`

docs/architecture/09-database.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ erDiagram
2424
agent_runs ||--o| agent_run_logs : "has"
2525
agent_runs ||--o{ agent_run_llm_calls : "logs"
2626
agent_runs ||--o| debug_analyses : "analyzed by"
27+
agent_runs ||--o| debug_analysis_status : "status"
2728
2829
users ||--o{ sessions : "has"
2930
users ||--o{ org_memberships : "has"
@@ -151,6 +152,7 @@ erDiagram
151152
| `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`) |
152153
| `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 ||
153154
| `debug_analyses` | AI debug analysis results ||
155+
| `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 |
154156

155157
## Repositories
156158

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

182184
## Connection Management
183185

src/agents/prompts/templates/implementation.eta

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@ You are an expert software engineer implementing features and fixing issues base
2525

2626
### Phase 4: Verify & Ship
2727

28-
7. **Run linting** (with fixing first, then without) **and type checking**
28+
7. **Run type checking and lint changed files only**
29+
- Type check: `npm run typecheck` (or `npx tsc --noEmit`)
30+
- 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`:
31+
```
32+
{ git diff --name-only HEAD; git ls-files --others --exclude-standard; } \
33+
| grep -E '\.(ts|tsx|js|jsx)$' | sort -u | xargs -r npx eslint --fix --max-warnings 0
34+
```
35+
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.
2936
8. **Create a PR** using the `CreatePR` gadget (it handles commit, push, and PR creation atomically)
3037
- **FORBIDDEN**: Do NOT run `gh pr create` or `git push` via Tmux — they will FAIL
3138
- IMPORTANT: DO NOT PROCEED FURTHER UNTIL YOU HAVE CONFIRMED the CreatePR output shows a `prUrl`.

src/agents/prompts/templates/partials/tmux.eta

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Use the Tmux gadget for ALL shell commands:
1515

1616
**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.
1717

18+
**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.
19+
1820
**Command Format:** Pass command as a shell string. Pipes, &&, ||, redirects, and globs all work:
1921
- Simple: `command="npm test"`
2022
- Chained: `command="npm run lint && npm test"`

src/agents/shared/repository.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,16 +238,27 @@ export async function setupRepository(options: SetupRepositoryOptions): Promise<
238238
const setupScriptPath = join(repoDir, '.cascade', 'setup.sh');
239239
if (existsSync(setupScriptPath)) {
240240
log.info('Running project setup script', { path: '.cascade/setup.sh', agentType });
241-
const setupResult = await runCommand('bash', [setupScriptPath], repoDir, {
242-
AGENT_PROFILE_NAME: agentType,
243-
});
241+
const setupResult = await runCommand(
242+
'bash',
243+
[setupScriptPath],
244+
repoDir,
245+
{ AGENT_PROFILE_NAME: agentType },
246+
// Disable idle timeout: setup.sh may compile language runtimes (e.g. Ruby via
247+
// asdf/ruby-build) whose make output is suppressed, causing false idle-timeout
248+
// kills. The wall timeout (10 min) remains the safety net for truly hung setups.
249+
{ idleTimeoutMs: 0 },
250+
);
244251
log.info('Setup script completed', {
245252
exitCode: setupResult.exitCode,
253+
reason: setupResult.reason,
246254
stdout: setupResult.stdout.slice(-500),
247255
stderr: setupResult.stderr.slice(-500),
248256
});
249257
if (setupResult.exitCode !== 0) {
250-
log.warn('Setup script exited with non-zero code', { exitCode: setupResult.exitCode });
258+
log.warn('Setup script exited with non-zero code', {
259+
exitCode: setupResult.exitCode,
260+
reason: setupResult.reason,
261+
});
251262
}
252263
}
253264

0 commit comments

Comments
 (0)