diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1291a4d9..969f7da3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# aicodeman
+## 0.9.11
+
+### Patch Changes
+
+- Fix a terminal freeze on hover (catastrophic regex backtracking) and a CSP violation that disabled the terminal's anti-throttling worker.
+
+ **Tab-freezing hover bug**: the terminal link provider's `cmdPattern` (which turns `tail -f /path`-style text into clickable links) used an empty-matchable, unbounded arg group — `(?:[^\s\/]*\s+)*` — that backtracks exponentially on real Claude output, e.g. wrapped `git commit -m "$(cat <<'EOF'` heredoc lines or aligned table rows. Hovering the mouse over such a line hung the page's main thread for minutes ("page unresponsive"). The pattern now uses non-empty tokens with bounded repetition (linear time); all intended command+path link forms still match. New `test/link-provider-regex.test.ts` extracts the shipped patterns from source and pins linear-time behavior on the killer line shapes.
+
+ **Blob worker CSP fix**: `worker-src 'self' blob:` is now always present in the CSP (previously only with `CODEMAN_GESTURE=1`). The terminal's `_safeYield` anti-throttling tick worker is created from a Blob URL and was silently blocked on every install, logging a CSP violation on each page load and disabling the worker leg of the render-yield fallback chain.
+
## 0.9.10
### Patch Changes
diff --git a/CLAUDE.md b/CLAUDE.md
index 4d410606..3225f349 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -56,7 +56,7 @@ When user says "COM":
CI runs `npm run check:lockfile` on every push/PR, so lockfile drift fails the build even if the `version-packages` script is bypassed.
-**Version**: 0.9.10 (must match `package.json`)
+**Version**: 0.9.11 (must match `package.json`)
## Project Overview
@@ -127,7 +127,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
| **Tasks** | `src/task.ts`, `src/task-queue.ts`, `src/task-tracker.ts` | |
| **State** | `src/state-store.ts`, `src/run-summary.ts`, `src/session-lifecycle-log.ts` | |
| **Infra** | `src/hooks-config.ts`, `src/push-store.ts`, `src/tunnel-manager.ts`, `src/image-watcher.ts`, `src/file-stream-manager.ts` | |
-| **Plan** | `src/plan-orchestrator.ts`, `src/prompts/*.ts`, `src/templates/claude-md.ts` | |
+| **Plan** | `src/plan-orchestrator.ts`, `src/prompts/*.ts`, `src/templates/` (`claude-md.ts` + `case-template.md`, the CLAUDE.md scaffold generated into new cases) | |
| **Web** | `src/web/server.ts` ★, `src/web/sse-events.ts`, `src/web/routes/*.ts` (15 route modules + barrel; `session-routes.ts` ★), `src/web/route-helpers.ts`, `src/web/ports/*.ts`, `src/web/middleware/auth.ts`, `src/web/schemas.ts`, `src/web/self-update.ts` | |
| **Frontend** | `src/web/public/app.js` (~3.6K lines, core) + 5 infra modules (`constants.js`, `mobile-handlers.js`, `voice-input.js`, `notification-manager.js`, `keyboard-accessory.js`) + 7 domain modules (`terminal-ui.js`, `respawn-ui.js`, `ralph-panel.js`, `orchestrator-panel.js`, `settings-ui.js`, `panels-ui.js`, `session-ui.js`) + 5 feature modules (`ralph-wizard.js`, `api-client.js`, `subagent-windows.js`, `input-cjk.js`, `image-input.js`) + `sw.js` | |
| **Types** | `src/types/index.ts` (barrel) → 15 domain files; also `src/types.ts` root re-export | See `@fileoverview` in index.ts |
@@ -157,7 +157,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
**External CLI modes (OpenCode, Codex)**: `isExternalCliMode()` in `session.ts` gates Claude-specific behavior — Ralph tracker, BashToolParser, token/CLI-info parsing, and ❯-prompt readiness detection are all skipped (these CLIs render their own TUIs; readiness = output stabilization instead). Both modes **require tmux — no direct PTY fallback** — because secrets are injected via `tmux setenv`, never on the spawn command line: OpenCode gets `OPENCODE_CONFIG_CONTENT` etc., Codex gets `OPENAI_API_KEY`/`CODEX_API_KEY`/`CODEX_HOME` (`setCodexEnvVars` in `tmux-manager.ts`). Codex specifics: command built by `buildCodexCommand()` (`--model`, `resume `, `--dangerously-bypass-approvals-and-sandbox` from the `codexConfig` payload / `codexDangerouslyBypassApprovals` app setting; `renderMode` is schema-coerced to `'hybrid'`, the only supported mode); tmux exports `COLORTERM=truecolor` + unsets `NO_COLOR` (other modes unset `COLORTERM`); availability via `GET /api/codex/status` — session/quick-start routes fail with `OPERATION_FAILED` and an install hint (`npm install -g @openai/codex`) when the binary is missing. Frontend: run-mode dropdown → `runCodex()` in `session-ui.js` ("Run CX" label), App Settings → Codex CLI tab; Respawn/Ralph options are Claude-only, so session options open on the Summary tab for external CLI sessions. Tests: `test/run-mode-ui.test.ts` (vm-sandbox harness, no real DOM).
-**Hook events**: Claude Code hooks trigger via `/api/hook-event`. Key events: `permission_prompt`, `elicitation_dialog`, `idle_prompt`, `stop`, `teammate_idle`, `task_completed`. See `src/hooks-config.ts`.
+**Hook events**: Claude Code hooks trigger via `/api/hook-event`. Key events: `permission_prompt`, `elicitation_dialog`, `idle_prompt`, `stop`, `teammate_idle`, `task_completed`. See `src/hooks-config.ts`; upstream hook semantics mirrored in `docs/claude-code-hooks-reference.md`.
**Agent Teams**: `TeamWatcher` polls `~/.claude/teams/`, matches to sessions via `leadSessionId`. Teammates are in-process threads appearing as subagents. Enable: `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`. See `docs/agent-teams/`.
@@ -211,7 +211,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
~135 handlers across 15 route files in `src/web/routes/`: system (41, incl. self-update `check`/`status`/`POST /api/system/update`, `POST /api/system/span-displays` → spawns `scripts/span-codeman.sh`, and `GET /api/codex/status`), sessions (28), orchestrator (10), cases (9), ralph (9), plan (8), respawn (7), files (6), mux (5), push (4), scheduled (4), teams (2), hooks (1), clipboard (1), ws (1 WebSocket). Each file has `@fileoverview` with endpoint details.
-**HTTP contract** (stable since 0.9.x, see `docs/versioning-policy.md`): responses use the `ApiResponse` envelope — `{ success: true, data? }` or `{ success: false, error, errorCode }` (`src/types/api.ts`). `/api/v1/*` is a versioned alias of `/api/*` (URL rewrite in `server.ts`).
+**HTTP contract** (stable since 0.9.x, see `docs/versioning-policy.md`; full envelope/status/error-code/SSE spec in `docs/api-reference.md`): responses use the `ApiResponse` envelope — `{ success: true, data? }` or `{ success: false, error, errorCode }` (`src/types/api.ts`). `/api/v1/*` is a versioned alias of `/api/*` (URL rewrite in `server.ts`).
## Adding Features
@@ -249,7 +249,7 @@ Raw `npx vitest` skips `config/vitest.config.ts`; always use `npm test --` or pa
**Ports**: Pick unique ports manually. Search `const PORT =` before adding new tests.
-**Respawn tests**: Use `MockSession` from `test/respawn-test-utils.ts`. **Route tests**: `app.inject({ method, url, payload })` in `test/routes/` — no live port needed. **Mobile tests**: Playwright suite in `test/mobile/` (135 device profiles).
+**Respawn tests**: Use `MockSession` from `test/respawn-test-utils.ts`. **Route tests**: `app.inject({ method, url, payload })` in `test/routes/` — no live port needed. **Mobile tests**: Playwright suite in `test/mobile/` (135 device profiles). Browser-testing infra and practices: `docs/browser-testing-guide.md`.
## Debugging
diff --git a/README.md b/README.md
index 76e04f36..d750bcf9 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,10 @@
-The missing control plane for AI coding agents
+Mission control for AI coding agents
- Agent Visualization • Zero-Lag Input • Mobile-First UI • Hardened Security
+ Claude Code • OpenCode • Codex — One Dashboard • Zero-Lag Mobile Input • Any Device
diff --git a/README.zh-CN.md b/README.zh-CN.md
index ed004e34..e1863762 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -2,10 +2,10 @@
-为 AI 编程智能体而生的「控制平面」
+AI 编程智能体的任务控制中心
- 智能体可视化 • 零延迟输入 • 自主编排器 • 重生控制器 • 移动优先 UI • 安全加固
+ Claude Code • OpenCode • Codex —— 统一仪表盘 • 零延迟移动输入 • 任意设备
diff --git a/package-lock.json b/package-lock.json
index 65405eb0..035ff78a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "aicodeman",
- "version": "0.9.10",
+ "version": "0.9.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aicodeman",
- "version": "0.9.10",
+ "version": "0.9.11",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
diff --git a/package.json b/package.json
index ed6bb0b1..069f3b6a 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "aicodeman",
- "version": "0.9.10",
- "description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
+ "version": "0.9.11",
+ "description": "Mission control for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/packages/xterm-zerolag-input/README.md b/packages/xterm-zerolag-input/README.md
index 79a14d22..ee511c17 100644
--- a/packages/xterm-zerolag-input/README.md
+++ b/packages/xterm-zerolag-input/README.md
@@ -39,7 +39,7 @@ Server echoes 'h' ←───────────────────
## Origin
-This library was extracted from [Codeman](https://github.com/Ark0N/Codeman), the missing control plane for AI coding agents — multi-session management, real-time agent visualization, autonomous respawn loops, and a mobile-first web UI for Claude Code and OpenCode. The local echo system was built to make mobile and remote access feel instant, then battle-tested across thousands of hours of real usage. After 3 deep code audits, it was extracted into this standalone library with 78 tests covering every state transition.
+This library was extracted from [Codeman](https://github.com/Ark0N/Codeman), mission control for AI coding agents — multi-session management, real-time agent visualization, autonomous respawn loops, and a mobile-first web UI for Claude Code, OpenCode, and Codex. The local echo system was built to make mobile and remote access feel instant, then battle-tested across thousands of hours of real usage. After 3 deep code audits, it was extracted into this standalone library with 78 tests covering every state transition.
## Install
diff --git a/src/prompts/index.ts b/src/prompts/index.ts
index 9d881357..3e91bc30 100644
--- a/src/prompts/index.ts
+++ b/src/prompts/index.ts
@@ -8,3 +8,4 @@
export { RESEARCH_AGENT_PROMPT } from './research-agent.js';
export { PLANNER_PROMPT } from './planner.js';
export { PHASE_EXECUTION_PROMPT, TEAM_LEAD_PROMPT, REPLAN_PROMPT, SINGLE_TASK_PROMPT } from './orchestrator.js';
+export { RALPH_STATUS_CONTRACT, buildRalphLoopPrompt, type RalphLoopPromptOptions } from './ralph.js';
diff --git a/src/prompts/ralph.ts b/src/prompts/ralph.ts
new file mode 100644
index 00000000..82ae20fa
--- /dev/null
+++ b/src/prompts/ralph.ts
@@ -0,0 +1,85 @@
+/**
+ * @fileoverview Ralph Loop prompt construction
+ *
+ * Builds the full `@ralph_prompt.md` content written for a new Ralph loop
+ * session, including the RALPH_STATUS block contract. The contract travels
+ * with the loop prompt (not the generated CLAUDE.md) so every Ralph session
+ * emits parseable status blocks regardless of the project's CLAUDE.md.
+ *
+ * @module prompts/ralph
+ */
+
+/**
+ * Structured status-reporting contract appended to every Ralph loop prompt.
+ *
+ * `RalphStatusParser` (src/ralph-status-parser.ts) parses this block from
+ * session output — keep the field names and enum values in sync with its
+ * patterns.
+ */
+export const RALPH_STATUS_CONTRACT = `## Status Reporting
+
+End EVERY response with exactly this block — Codeman parses it to track the loop:
+
+\`\`\`
+---RALPH_STATUS---
+STATUS: IN_PROGRESS | COMPLETE | BLOCKED
+TASKS_COMPLETED_THIS_LOOP:
+FILES_MODIFIED:
+TESTS_STATUS: PASSING | FAILING | NOT_RUN
+WORK_TYPE: IMPLEMENTATION | TESTING | DOCUMENTATION | REFACTORING
+EXIT_SIGNAL: false | true
+RECOMMENDATION:
+---END_RALPH_STATUS---
+\`\`\`
+
+Rules:
+- \`EXIT_SIGNAL: true\` only when ALL tasks are verifiably done — then also output the completion phrase
+- \`STATUS: BLOCKED\` when you need human input; describe the blocker in RECOMMENDATION
+- Never set \`EXIT_SIGNAL: true\` while tests are failing
+`;
+
+export interface RalphLoopPromptOptions {
+ /** The user's task description (becomes the prompt header) */
+ taskDescription: string;
+ /** Completion phrase the session must emit inside */
+ completionPhrase: string;
+ /** Whether a @fix_plan.md task plan was generated for this loop */
+ hasPlan: boolean;
+}
+
+/**
+ * Builds the full Ralph loop prompt written to `@ralph_prompt.md`.
+ */
+export function buildRalphLoopPrompt({ taskDescription, completionPhrase, hasPlan }: RalphLoopPromptOptions): string {
+ let fullPrompt = taskDescription + '\n\n---\n\n';
+ if (hasPlan) {
+ fullPrompt += '## Task Plan\n\n';
+ fullPrompt += 'A task plan has been written to `@fix_plan.md`. Use this to track progress:\n';
+ fullPrompt += '- Reference the plan at the start of each iteration\n';
+ fullPrompt += '- Update task checkboxes as you complete items\n';
+ fullPrompt += '- Work through items in priority order (P0 > P1 > P2)\n\n';
+ }
+ fullPrompt += '## Iteration Protocol\n\n';
+ fullPrompt += 'This is an autonomous loop. Files from previous iterations persist. On each iteration:\n';
+ fullPrompt += '1. Check what work has already been done\n';
+ fullPrompt += '2. Make incremental progress toward completion\n';
+ fullPrompt += '3. Commit meaningful changes with descriptive messages\n\n';
+ fullPrompt += '## Verification\n\n';
+ fullPrompt += 'After each significant change:\n';
+ fullPrompt += '- Run tests to verify (npm test, pytest, etc.)\n';
+ fullPrompt += '- Check for type/lint errors if applicable\n';
+ fullPrompt += '- If tests fail, read the error, fix it, and retry\n\n';
+ fullPrompt += '## Completion Criteria\n\n';
+ fullPrompt += `Output \`${completionPhrase}\` when ALL of the following are true:\n`;
+ fullPrompt += '- All requirements from the task description are implemented\n';
+ fullPrompt += '- All tests pass\n';
+ fullPrompt += '- Changes are committed\n\n';
+ fullPrompt += '## If Stuck\n\n';
+ fullPrompt += 'If you encounter the same error for 3+ iterations:\n';
+ fullPrompt += "1. Document what you've tried\n";
+ fullPrompt += '2. Identify the specific blocker\n';
+ fullPrompt += '3. Try an alternative approach\n';
+ fullPrompt += '4. If truly blocked, output `BLOCKED` with an explanation\n\n';
+ fullPrompt += RALPH_STATUS_CONTRACT;
+ return fullPrompt;
+}
diff --git a/src/templates/case-template.md b/src/templates/case-template.md
index ff762be1..c684ec1b 100644
--- a/src/templates/case-template.md
+++ b/src/templates/case-template.md
@@ -56,42 +56,3 @@ This session is managed by Codeman and runs inside tmux (`CODEMAN_MUX=1` confirm
- NEVER kill your own session: no `tmux kill-session`, `pkill tmux`, or `pkill claude`.
- The session persists across disconnects — your work is safe.
- Hooks may auto-format or validate after writes; unexpected tool behavior usually means a hook ran. Keep working.
-
-## Ralph Loop (Codeman Autonomous Mode)
-
-Start: `/ralph-loop:ralph-loop` · Cancel: `/ralph-loop:cancel-ralph` · Help: `/ralph-loop:help`
-
-You are in a Ralph loop when the prompt contains a completion phrase (e.g. `COMPLETE`). While looping:
-
-- Work incrementally: one sub-task at a time — implement, verify, commit, move on.
-- Fix failing tests and lint errors before starting the next task.
-- Check `git log --oneline -10` / `git diff HEAD~1` before retrying an approach that already failed.
-- Keep testing to ~20% of total effort; prioritize implementation. Don't refactor working code or add unrequested
- features as busy work.
-- If the prompt sets a minimum duration (e.g. "work for 4 hours"), record the start time (`date +%s`) and, when the
- primary tasks finish early, keep generating useful related work (edge cases, coverage, docs, cleanup, hardening)
- until the minimum time is reached.
-
-Output the completion phrase ONLY when every requirement is verifiably done: all tests pass, lint is clean, the build
-succeeds, and changes are committed. Never output it early — and never withhold it for busy work once everything is
-genuinely done.
-
-### RALPH_STATUS block (required)
-
-End EVERY response during a Ralph loop with exactly this block — Codeman parses it to track the loop:
-
-```
----RALPH_STATUS---
-STATUS: IN_PROGRESS | COMPLETE | BLOCKED
-TASKS_COMPLETED_THIS_LOOP:
-FILES_MODIFIED:
-TESTS_STATUS: PASSING | FAILING | NOT_RUN
-WORK_TYPE: IMPLEMENTATION | TESTING | DOCUMENTATION | REFACTORING
-EXIT_SIGNAL: false | true
-RECOMMENDATION:
----END_RALPH_STATUS---
-```
-
-- `EXIT_SIGNAL: true` only when ALL tasks are verifiably done — then also output the completion phrase.
-- `STATUS: BLOCKED` when you need human input; describe the blocker in RECOMMENDATION.
-- Never set `EXIT_SIGNAL: true` while tests are failing.
diff --git a/src/web/middleware/auth.ts b/src/web/middleware/auth.ts
index 1e2a6b27..0bd268c5 100644
--- a/src/web/middleware/auth.ts
+++ b/src/web/middleware/auth.ts
@@ -205,7 +205,12 @@ export function registerSecurityHeaders(app: FastifyInstance, https: boolean): v
const scriptSrc =
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net" + (gesture ? " 'wasm-unsafe-eval'" : '');
const connectSrc = "connect-src 'self' wss://api.deepgram.com";
- const workerSrc = gesture ? "; worker-src 'self' blob:" : '';
+ // blob: workers are needed unconditionally: terminal-ui's _safeYield tick
+ // worker (throttling escape) is created from a Blob URL. Without this, every
+ // page load logs a CSP violation and the worker leg of _safeYield is dead.
+ // Risk is minimal — only same-origin scripts (already governed by script-src)
+ // can construct blob workers.
+ const workerSrc = "; worker-src 'self' blob:";
const csp =
`default-src 'self'; ${scriptSrc}; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; ` +
`img-src 'self' data: blob:; ${connectSrc}; font-src 'self' https://cdn.jsdelivr.net; frame-ancestors 'self'${workerSrc}`;
diff --git a/src/web/public/ralph-wizard.js b/src/web/public/ralph-wizard.js
index 9e81420d..33938801 100644
--- a/src/web/public/ralph-wizard.js
+++ b/src/web/public/ralph-wizard.js
@@ -378,7 +378,10 @@ Object.assign(CodemanApp.prototype, {
prompt += `Output \`${config.completionPhrase}\` when done\n\n`;
prompt += '## If Stuck\n';
- prompt += 'Output `BLOCKED` with explanation';
+ prompt += 'Output `BLOCKED` with explanation\n\n';
+
+ prompt += '## Status Reporting\n';
+ prompt += '• End every response with a `RALPH_STATUS` block (parsed by Codeman)';
// Show preview with highlighting (escape first, then apply formatting)
const escapedPrompt = escapeHtml(prompt);
diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js
index 8e250841..cb476169 100644
--- a/src/web/public/terminal-ui.js
+++ b/src/web/public/terminal-ui.js
@@ -839,7 +839,11 @@ Object.assign(CodemanApp.prototype, {
// Pattern 1: Commands with file paths (tail -f, cat, head, grep pattern, etc.)
// Handles: tail -f /path, grep pattern /path, cat -n /path
- const cmdPattern = /(tail|cat|head|less|grep|watch|vim|nano)\s+(?:[^\s\/]*\s+)*(\/[^\s"'<>|;&\n\x00-\x1f]+)/g;
+ // ⚠ The arg group must stay linear-time: `(?:[^\s\/]*\s+)*` (empty-matchable
+ // token, unbounded) backtracks exponentially on lines with a trigger word
+ // followed by multi-space runs (e.g. wrapped heredoc/table output) — froze
+ // the whole tab on hover. Non-empty token + bounded reps is O(n).
+ const cmdPattern = /\b(tail|cat|head|less|grep|watch|vim|nano)\s+(?:[^\s\/]+\s+){0,4}(\/[^\s"'<>|;&\n\x00-\x1f]+)/g;
// Pattern 2: Paths with common extensions
const extPattern =
diff --git a/src/web/routes/ralph-routes.ts b/src/web/routes/ralph-routes.ts
index ab971a51..b49ce136 100644
--- a/src/web/routes/ralph-routes.ts
+++ b/src/web/routes/ralph-routes.ts
@@ -16,6 +16,7 @@ import { SseEvent } from '../sse-events.js';
import { autoConfigureRalph, CASES_DIR, SETTINGS_PATH, findSessionOrFail, parseBody } from '../route-helpers.js';
import { writeHooksConfig, stripCaseEnvKeys } from '../../hooks-config.js';
import { generateClaudeMd } from '../../templates/claude-md.js';
+import { buildRalphLoopPrompt } from '../../prompts/index.js';
import { getLifecycleLog } from '../../session-lifecycle-log.js';
import type { SessionPort, EventPort, RespawnPort, ConfigPort, InfraPort } from '../ports/index.js';
import { MAX_CONCURRENT_SESSIONS } from '../../config/map-limits.js';
@@ -382,37 +383,12 @@ export function registerRalphRoutes(
writeFileSync(fixPlanPath, planContent, 'utf-8');
}
- // Build full prompt
- const hasPlan = enabledItems.length > 0;
- let fullPrompt = taskDescription + '\n\n---\n\n';
- if (hasPlan) {
- fullPrompt += '## Task Plan\n\n';
- fullPrompt += 'A task plan has been written to `@fix_plan.md`. Use this to track progress:\n';
- fullPrompt += '- Reference the plan at the start of each iteration\n';
- fullPrompt += '- Update task checkboxes as you complete items\n';
- fullPrompt += '- Work through items in priority order (P0 > P1 > P2)\n\n';
- }
- fullPrompt += '## Iteration Protocol\n\n';
- fullPrompt += 'This is an autonomous loop. Files from previous iterations persist. On each iteration:\n';
- fullPrompt += '1. Check what work has already been done\n';
- fullPrompt += '2. Make incremental progress toward completion\n';
- fullPrompt += '3. Commit meaningful changes with descriptive messages\n\n';
- fullPrompt += '## Verification\n\n';
- fullPrompt += 'After each significant change:\n';
- fullPrompt += '- Run tests to verify (npm test, pytest, etc.)\n';
- fullPrompt += '- Check for type/lint errors if applicable\n';
- fullPrompt += '- If tests fail, read the error, fix it, and retry\n\n';
- fullPrompt += '## Completion Criteria\n\n';
- fullPrompt += `Output \`${completionPhrase}\` when ALL of the following are true:\n`;
- fullPrompt += '- All requirements from the task description are implemented\n';
- fullPrompt += '- All tests pass\n';
- fullPrompt += '- Changes are committed\n\n';
- fullPrompt += '## If Stuck\n\n';
- fullPrompt += 'If you encounter the same error for 3+ iterations:\n';
- fullPrompt += "1. Document what you've tried\n";
- fullPrompt += '2. Identify the specific blocker\n';
- fullPrompt += '3. Try an alternative approach\n';
- fullPrompt += '4. If truly blocked, output `BLOCKED` with an explanation\n';
+ // Build full prompt (includes the RALPH_STATUS contract)
+ const fullPrompt = buildRalphLoopPrompt({
+ taskDescription,
+ completionPhrase,
+ hasPlan: enabledItems.length > 0,
+ });
// Write prompt to file
const promptPath = join(casePath, '@ralph_prompt.md');
diff --git a/test/link-provider-regex.test.ts b/test/link-provider-regex.test.ts
new file mode 100644
index 00000000..23590b10
--- /dev/null
+++ b/test/link-provider-regex.test.ts
@@ -0,0 +1,89 @@
+/**
+ * @fileoverview Regression guard for the terminal link-provider regexes in
+ * `src/web/public/terminal-ui.js`.
+ *
+ * The link provider runs its patterns against every hovered terminal line
+ * (logical lines — xterm re-joins wrapped rows, so inputs reach multiple KB).
+ * A pattern with ambiguous backtracking freezes the entire tab on hover:
+ * 0.9.10's `cmdPattern` used `(?:[^\s\/]*\s+)*` (empty-matchable token,
+ * unbounded), which went exponential on real Claude output — wrapped
+ * `git commit -m "$(cat <<'EOF'` heredoc lines hung the main thread for
+ * minutes per hover.
+ *
+ * This test extracts the pattern literals FROM THE SHIPPED SOURCE (no copies
+ * that can drift) and asserts they stay linear-time on those killer shapes,
+ * and that `cmdPattern` still links the command+path forms it exists for.
+ */
+import { describe, it, expect } from 'vitest';
+import { readFileSync } from 'fs';
+import { join } from 'path';
+
+const SOURCE = readFileSync(join(__dirname, '..', 'src', 'web', 'public', 'terminal-ui.js'), 'utf-8');
+
+/** Extract `const = /.../g;` from the shipped source and build the RegExp. */
+function shippedPattern(name: string): RegExp {
+ const m = SOURCE.match(new RegExp(`const ${name} =\\s*\\n?\\s*(/(?:[^/\\\\\\n]|\\\\.)+/[a-z]*)`));
+ if (!m) throw new Error(`pattern ${name} not found in terminal-ui.js`);
+ const lit = m[1];
+ const lastSlash = lit.lastIndexOf('/');
+ return new RegExp(lit.slice(1, lastSlash), lit.slice(lastSlash + 1));
+}
+
+const PATTERN_NAMES = ['urlPattern', 'cmdPattern', 'extPattern', 'bashPattern'];
+
+/** Lines that made 0.9.10's cmdPattern backtrack exponentially (>2s each). */
+const KILLER_LINES = [
+ // wrapped git-commit heredoc from real Claude tool output (the 0.9.10 freeze)
+ ` /Users/arbbot/codeman-cases/topagent-control commit -m "$(cat <<'EOF'${' '.repeat(3000)}`,
+ // aligned table row: trigger word + multi-space-separated columns + mid-token slash
+ 'watch ' + 'col '.repeat(40) + ' BTC/USDT',
+ // trigger word followed by many tokens and no token-initial path
+ 'cat ' + 'word '.repeat(800) + 'no-path-here',
+ // long URL-ish and path-ish soup for the other patterns
+ 'https://example.com/' + 'a/'.repeat(1500) + ' ' + '/home/x/'.repeat(400) + '.'.repeat(2000),
+ 'Bash(' + 'x'.repeat(4000),
+];
+
+describe('terminal link-provider regexes (shipped source)', () => {
+ it('all patterns stay linear-time on killer lines', () => {
+ const patterns = PATTERN_NAMES.map((n) => [n, shippedPattern(n)] as const);
+ const start = Date.now();
+ for (const [, re] of patterns) {
+ for (const line of KILLER_LINES) {
+ re.lastIndex = 0;
+ while (re.exec(line) !== null) {
+ /* drain all matches like the provider does */
+ }
+ }
+ }
+ const elapsed = Date.now() - start;
+ // 20 pattern×line runs over multi-KB inputs: linear patterns finish in a few
+ // ms; the 0.9.10 cmdPattern alone needed minutes for ONE line.
+ expect(elapsed).toBeLessThan(500);
+ });
+
+ it('cmdPattern still links command + path forms', () => {
+ const cmd = shippedPattern('cmdPattern');
+ const cases: Array<[string, string]> = [
+ ['tail -f /var/log/app.log', '/var/log/app.log'],
+ ['cat -n /tmp/x.json', '/tmp/x.json'],
+ ['grep -rn pattern /home/user/src', '/home/user/src'],
+ ['watch ls /opt/data', '/opt/data'],
+ ['head -c 100 /etc/hosts', '/etc/hosts'],
+ ];
+ for (const [line, want] of cases) {
+ cmd.lastIndex = 0;
+ const m = cmd.exec(line);
+ expect(m, line).not.toBeNull();
+ expect(m![2]).toBe(want);
+ }
+ });
+
+ it('cmdPattern arg group cannot match empty tokens (the exponential trigger)', () => {
+ // structural guard: the dangerous construct is an empty-matchable token
+ // inside a repeated group — `[^\s\/]*\s+` repeated. Check the pattern
+ // literal itself (not the whole file — the warning comment quotes it).
+ const lit = shippedPattern('cmdPattern').source;
+ expect(lit).not.toContain('[^\\s\\/]*\\s+)*');
+ });
+});
diff --git a/test/ralph-prompt.test.ts b/test/ralph-prompt.test.ts
new file mode 100644
index 00000000..b1b3aa7f
--- /dev/null
+++ b/test/ralph-prompt.test.ts
@@ -0,0 +1,92 @@
+/**
+ * @fileoverview Tests for Ralph loop prompt construction
+ *
+ * Verifies buildRalphLoopPrompt() output, and that the RALPH_STATUS contract
+ * embedded in the prompt stays in sync with what RalphStatusParser parses.
+ */
+
+import { describe, it, expect } from 'vitest';
+import { buildRalphLoopPrompt, RALPH_STATUS_CONTRACT } from '../src/prompts/ralph.js';
+import { RalphStatusParser } from '../src/ralph-status-parser.js';
+
+describe('buildRalphLoopPrompt', () => {
+ const baseOptions = {
+ taskDescription: 'Add CRUD endpoints for todos',
+ completionPhrase: 'COMPLETE',
+ hasPlan: false,
+ };
+
+ it('starts with the task description', () => {
+ const prompt = buildRalphLoopPrompt(baseOptions);
+
+ expect(prompt.startsWith('Add CRUD endpoints for todos\n\n---\n\n')).toBe(true);
+ });
+
+ it('embeds the completion phrase in the completion criteria', () => {
+ const prompt = buildRalphLoopPrompt({ ...baseOptions, completionPhrase: 'ALL_DONE' });
+
+ expect(prompt).toContain('ALL_DONE');
+ expect(prompt).toContain('## Completion Criteria');
+ });
+
+ it('includes the task plan section only when a plan exists', () => {
+ const withPlan = buildRalphLoopPrompt({ ...baseOptions, hasPlan: true });
+ const withoutPlan = buildRalphLoopPrompt(baseOptions);
+
+ expect(withPlan).toContain('## Task Plan');
+ expect(withPlan).toContain('@fix_plan.md');
+ expect(withoutPlan).not.toContain('## Task Plan');
+ });
+
+ it('always appends the RALPH_STATUS contract', () => {
+ const prompt = buildRalphLoopPrompt(baseOptions);
+
+ expect(prompt).toContain(RALPH_STATUS_CONTRACT);
+ expect(prompt).toContain('---RALPH_STATUS---');
+ expect(prompt).toContain('---END_RALPH_STATUS---');
+ });
+
+ it('documents every field RalphStatusParser expects', () => {
+ for (const field of [
+ 'STATUS: IN_PROGRESS | COMPLETE | BLOCKED',
+ 'TASKS_COMPLETED_THIS_LOOP: ',
+ 'FILES_MODIFIED: ',
+ 'TESTS_STATUS: PASSING | FAILING | NOT_RUN',
+ 'WORK_TYPE: IMPLEMENTATION | TESTING | DOCUMENTATION | REFACTORING',
+ 'EXIT_SIGNAL: false | true',
+ 'RECOMMENDATION:',
+ ]) {
+ expect(RALPH_STATUS_CONTRACT).toContain(field);
+ }
+ });
+
+ it('teaches a block format that RalphStatusParser actually parses', () => {
+ // A response following the contract to the letter
+ const conformingBlock = [
+ '---RALPH_STATUS---',
+ 'STATUS: IN_PROGRESS',
+ 'TASKS_COMPLETED_THIS_LOOP: 2',
+ 'FILES_MODIFIED: 5',
+ 'TESTS_STATUS: PASSING',
+ 'WORK_TYPE: IMPLEMENTATION',
+ 'EXIT_SIGNAL: false',
+ 'RECOMMENDATION: Continue with the next endpoint',
+ '---END_RALPH_STATUS---',
+ ];
+
+ const parser = new RalphStatusParser();
+ for (const line of conformingBlock) {
+ parser.processLine(line);
+ }
+
+ const block = parser.lastStatusBlock;
+ expect(block).not.toBeNull();
+ expect(block?.status).toBe('IN_PROGRESS');
+ expect(block?.tasksCompletedThisLoop).toBe(2);
+ expect(block?.filesModified).toBe(5);
+ expect(block?.testsStatus).toBe('PASSING');
+ expect(block?.workType).toBe('IMPLEMENTATION');
+ expect(block?.exitSignal).toBe(false);
+ expect(block?.recommendation).toBe('Continue with the next endpoint');
+ });
+});
diff --git a/test/templates.test.ts b/test/templates.test.ts
index 2b8cd3ad..dbf331f5 100644
--- a/test/templates.test.ts
+++ b/test/templates.test.ts
@@ -68,24 +68,6 @@ describe('generateClaudeMd', () => {
expect(result).toContain('conventional commits');
});
- it('should include Ralph Loop section with slash commands', () => {
- const result = generateClaudeMd('my-project');
-
- expect(result).toContain('## Ralph Loop');
- expect(result).toContain('/ralph-loop:ralph-loop');
- expect(result).toContain('/ralph-loop:cancel-ralph');
- });
-
- it('should include the RALPH_STATUS contract parsed by ralph-status-parser', () => {
- const result = generateClaudeMd('my-project');
-
- expect(result).toContain('---RALPH_STATUS---');
- expect(result).toContain('---END_RALPH_STATUS---');
- expect(result).toContain('STATUS: IN_PROGRESS | COMPLETE | BLOCKED');
- expect(result).toContain('EXIT_SIGNAL: false | true');
- expect(result).toContain('completion phrase');
- });
-
it('should stay under the 200-line CLAUDE.md guidance', () => {
const result = generateClaudeMd('my-project');
@@ -99,6 +81,10 @@ describe('generateClaudeMd', () => {
expect(result).not.toContain('TodoWrite');
expect(result).not.toContain('## Planning Mode');
expect(result).not.toContain('[TECHNOLOGIES_USED]');
+ // Ralph loop instructions live in the loop prompt (wizard) and plugin,
+ // not in every project's CLAUDE.md
+ expect(result).not.toContain('RALPH_STATUS');
+ expect(result).not.toContain('/ralph-loop:');
});
});