Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c278c64
feat(ralph): seeded synthesis & improved base prompt
plusplusoneplusplus May 29, 2026
023a699
fix(schedule): wait for queued run completion
plusplusoneplusplus May 29, 2026
a79af57
Record missed schedule overlap runs
plusplusoneplusplus May 29, 2026
911963b
Wait for scheduled Ralph session completion
plusplusoneplusplus May 29, 2026
61ce80c
Add schedule lifecycle websocket coverage
plusplusoneplusplus May 29, 2026
9cb7e31
Add scheduled Ralph gap-loop regression test
plusplusoneplusplus May 29, 2026
19ee853
Verify Ralph schedule completion barrier
plusplusoneplusplus May 29, 2026
3879bab
test(schedule): accept 'failed' status in integration trigger tests
plusplusoneplusplus May 29, 2026
337b4a3
Fix Ralph final-check validation prompt
plusplusoneplusplus May 30, 2026
c783141
fix(coc): render historical ask-user questions
plusplusoneplusplus May 30, 2026
3ae15dc
feat(coc): open pull requests by URL or PR number from dashboard
plusplusoneplusplus May 30, 2026
bc2904e
feat(git): add commit SHA lookup in Git tab (feature-flagged)
plusplusoneplusplus May 30, 2026
47e8b8e
docs(coc): update dashboard UX specs to match current implementation
plusplusoneplusplus May 30, 2026
beeace2
feat(ultra-ralph): add bundled skill and thin Ralph prompt call sites
plusplusoneplusplus May 30, 2026
4ee6041
fix: preserve provider in serializeTaskSummary slim payload
plusplusoneplusplus May 30, 2026
c408e9e
Expose CoC tools via Codex enabled_tools
plusplusoneplusplus May 30, 2026
0872b09
fix: preserve Claude tool results for commit detection
plusplusoneplusplus May 30, 2026
ea57a54
feat(queue): add Ralph session queue continuity (AC-01 through AC-06)
plusplusoneplusplus May 30, 2026
8ab637b
feat(skillopt): add SkillOpt TypeScript script for RL-style skill opt…
plusplusoneplusplus May 30, 2026
52aa792
Refactor multi-repo bridge dispatch
plusplusoneplusplus May 30, 2026
bf3d4e5
Grant Claude SDK access to ~/.coc and system temp dir
plusplusoneplusplus May 30, 2026
9c434bd
feat(coc): reasoning-effort picker smart defaults
plusplusoneplusplus May 30, 2026
5894603
test(coc): update admin-prompts test after Ralph prompts moved to bun…
plusplusoneplusplus May 30, 2026
ea92e01
test(coc): fix stale tests after Ralph prompt thinning and reasoning-…
plusplusoneplusplus May 30, 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
14 changes: 14 additions & 0 deletions .github/skills/coc-knowledge/references/dashboard-spa.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ Inside `WhisperCollapsedGroup`, tool calls render as compact "whisper-row" varia
- Single flat row: kind pill + truncated summary + duration + chevron
- Color-coded pills: Read/blue, Grep/Glob/green, Edit/Write/amber, Shell/PS/SQL/purple, Skill/grey

Chat commit strips are detected from real shell output on `powershell`, `shell`,
and `bash` tool calls. The detector only treats commit-creating commands
(`git commit`, `git merge`, `git cherry-pick`, `git revert`) with native git
output such as `[branch abc1234] subject` as commits; assistant prose and
read-only git command output are ignored.

Completed `ask_user` tool calls render as read-only historical question cards via
`AskUserHistoryCard` inside `ConversationTurnBubble`. Live unanswered questions
remain owned by `ChatDetail`/`ConversationArea` through `processDetails.pendingAskUser`
and `AskUserInline`; the history card only displays persisted `args.questions[]`
plus the completed answer/skip result and is kept visible outside whisper
collapse. Generic `ToolCallView` still handles `ask_user` as a fallback and
summarizes `args.questions[0].question` when present.

`toolNormalization.ts` → `normalizeToolName()` canonicalises SDK-specific names before display and storage. Notable aliases: `read_file`/`open_file` → `view`, `edit_file`/`str_replace`/`str_replace_editor` → `edit`, `write_file`/`create_file` → `create`, `command_execution` → `shell`, `file_change` → `apply_patch`, `Skill` (Claude Code SDK PascalCase) → `skill`. All downstream logic (`getToolKindInfo`, `getToolSummary`, `filterWhisperChunks` skill counting) operates on the normalised lowercase name.

## Input Area
Expand Down
31 changes: 27 additions & 4 deletions .github/skills/coc-knowledge/references/ralph.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,20 @@ best-effort recovery of `workingDirectory` / `folderPath` from the latest
iteration process. Final-check gap-fix loops use the same additional-iteration
resolver so per-repo `maxRalphIterations` fallback stays consistent.

## Scheduled Ralph Runs

Prompt schedules with `mode='ralph'` seed a repo-scoped Ralph session before
enqueueing the first iteration. The queued task carries `context.scheduleId`,
`context.scheduleRunId`, and `context.ralph.sessionId`; continuation, final-check,
and gap-fix tasks preserve the schedule context so the originating schedule run
can stay active for the whole Ralph session.

The queue bridge exposes an internal `ralphSessionComplete` callback in addition
to broadcasting the dashboard WebSocket event. `ScheduleExecutor` uses that
callback to finalize scheduled Ralph runs only when the session reaches a
terminal reason. Queue failures or terminal final-check failure reasons mark the
schedule run failed; clean, capped, or normal terminal reasons complete it.

## Final Check Automation

`orchestrateFinalCheck(...)` in
Expand All @@ -124,9 +138,18 @@ final-check result to `progress.md`, reads the session once, and persists a
`sourceIteration`, `taskId`, `processId`, `startedAt`, `completedAt`) plus
outcome-specific metadata.

Final-check tasks are still queued as Ralph chat tasks and still use autopilot
capability, but `RalphExecutor` switches to validation-only system instructions
when `context.ralph.finalCheck` is present. Those instructions allow inspection
and read-only validation commands, forbid file edits/commits/state-changing
tools, and require a `RALPH_FINAL_CHECK_RESULT` response instead of
`RALPH_NEXT`/`RALPH_COMPLETE`.

Terminal paths broadcast `ralph-session-complete`: clean checks use
`reason='signal'`, cap-reached checks use `reason='cap'`, parse failures use
`reason='final-check-failed'`, gap-loop creation failures use
`reason='final-check-gap-loop-start-failed'`, and gap-loop enqueue failures use
`reason='final-check-gap-enqueue-failed'`. A successful gap-fix enqueue does
not broadcast completion because the next loop continues the session.
`reason='final-check-failed'`, final-check setup failures use
`reason='final-check-enqueue-failed'` or `reason='final-check-session-missing'`,
gap-loop creation failures use `reason='final-check-gap-loop-start-failed'`,
and gap-loop enqueue failures use `reason='final-check-gap-enqueue-failed'`. A
successful gap-fix enqueue does not broadcast completion because the next loop
continues the session.
10 changes: 8 additions & 2 deletions .github/skills/coc-knowledge/references/sdk-wrapper.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,12 @@ Claude Code expects hyphenated model IDs for version aliases (for example, `clau

Claude Code permission mode is mapped at the provider boundary: CoC `autopilot` sends `permissionMode: 'bypassPermissions'` plus `allowDangerouslySkipPermissions: true`, while CoC `plan` sends `permissionMode: 'plan'`. Interactive/ask mode leaves Claude Code's default permission behavior in place.

`ClaudeSDKService` widens the agent's filesystem permission scope via the SDK's `additionalDirectories` option (`resolveAdditionalDirectories`). It always grants access to `~/.coc` (CoC data/skills dir) and the system temp directory (`os.tmpdir()`) so out-of-repo skill files and temp artifacts remain readable beyond the per-request `workingDirectory`/`cwd`. Any caller-supplied `SendMessageOptions.additionalDirectories` are merged in; all entries are resolved to absolute paths and de-duplicated (case-insensitively on Windows).

`ClaudeSDKService` wires CoC LLM tools and any caller-provided `mcpServers` into `query({ options: { mcpServers } })`; CoC tools ride a stdio bridge entry (`coc_llm_tools`, `alwaysLoad: true`), are pre-approved via `options.allowedTools` (`mcp__coc_llm_tools__<tool>`) so Claude Code never prompts for them, and bridged `tool_use` names are de-namespaced (see *CoC LLM Tools over MCP*).

Claude tool-call capture treats assistant `tool_use` blocks as start events and user `tool_result` / `tool_use_result` payloads as terminal events. Stored tool calls keep the original input parameters in `args` and preserve the actual tool output in `result` or `error`; the adapter does not synthesize completion results from tool input JSON.

## RequestRunner — sendMessage() Flow (Copilot)

```
Expand Down Expand Up @@ -218,8 +222,10 @@ Provider wiring (per request, only when `options.tools` is non-empty; disposed i
`finally`):
- **Copilot:** native `SendMessageOptions.tools` (unchanged; no bridge).
- **Codex:** a fresh `Codex` client is built with
`config.mcp_servers.coc_llm_tools = { command, args, env }`. Bridged calls arrive
as `mcp_tool_call` items and report bare tool names via existing normalization.
`config.mcp_servers.coc_llm_tools = { command, args, env, enabled_tools }`, where
`enabled_tools` is the de-duplicated set of CoC LLM tool names passed into the
current request. Bridged calls arrive as `mcp_tool_call` items and report bare
tool names via existing normalization.
- **Claude:** the stdio bridge entry is injected into `query({ options: { mcpServers } })`
under `coc_llm_tools` with `alwaysLoad: true`; caller-provided `mcpServers` are
also forwarded (normalized to Claude's shape). Each bridged tool is added to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ The `src/server/` tree is grouped by feature domain. Cross-cutting plumbing stay
| `workspaces/` | global-workspace, my-work, my-life, workspace-summary |
| `processes/` | in-memory store, output-file-manager, stale-task-detector, pin/archive, seen-state, turn-actions, history, resume |
| `queue/` | queue-handler, executor-bridge, multi-repo-router, image-blob-store, partitioner |
| `schedule/` | cron-utils, schedule-handler/manager, run-persistence, yaml-persistence, repo-schedule-loader/overrides |
| `schedule/` | cron-utils, schedule-handler/manager/executor, run-persistence, yaml-persistence, repo-schedule-loader/overrides. Schedule run records stay `running` after enqueue and finalize from queue terminal events; scheduled Ralph runs finalize from the full `ralphSessionComplete` lifecycle, including final checks and gap-fix loops. Overlapping timer fires are recorded as `missed` and the next timer is armed after the active run finishes. |
| `tasks/` | task-types, cache, watcher, migration, root-resolver, generation, read/write handlers, comments/ |
| `notes/` | read/write/comments/AI/file-preview/image/edits handlers, git/ sub-module, notes-root-resolver (multi-root), notes-roots-handler (roots CRUD API) |
| `workflows/` | constants, utils, watcher, read/write handlers |
Expand Down
196 changes: 181 additions & 15 deletions packages/coc-agent-sdk/src/claude-sdk-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import { CocToolRuntime } from './llm-tools/coc-tool-runtime';
import { cocToolBridgeServer } from './llm-tools/bridge-server';
import { buildCocLlmToolsMcpConfig, COC_LLM_TOOLS_MCP_SERVER_NAME } from './llm-tools/mcp-config';
import * as crypto from 'crypto';
import * as os from 'os';
import * as path from 'path';

// ============================================================================
// @anthropic-ai/claude-agent-sdk type stubs
Expand All @@ -59,7 +61,14 @@ interface ClaudeToolUseBlock {
input: unknown;
}

type ClaudeContentBlock = ClaudeTextBlock | ClaudeToolUseBlock;
interface ClaudeToolResultBlock {
type: 'tool_result';
tool_use_id: string;
content?: unknown;
is_error?: boolean;
}

type ClaudeContentBlock = ClaudeTextBlock | ClaudeToolUseBlock | ClaudeToolResultBlock | Record<string, unknown>;

interface ClaudeAssistantMessage {
type: 'assistant';
Expand Down Expand Up @@ -93,6 +102,16 @@ interface ClaudeSystemMessage {
[key: string]: unknown;
}

interface ClaudeUserMessage {
type: 'user';
message?: {
content?: unknown;
};
parent_tool_use_id?: string | null;
tool_use_result?: unknown;
session_id?: string;
}

export interface ClaudeRateLimitInfo {
status: 'allowed' | 'allowed_warning' | 'rejected';
resetsAt?: number;
Expand All @@ -110,7 +129,7 @@ interface ClaudeRateLimitEvent {
session_id?: string;
}

type ClaudeSDKMessage = ClaudeAssistantMessage | ClaudeResultMessage | ClaudeSystemMessage | ClaudeRateLimitEvent | Record<string, unknown>;
type ClaudeSDKMessage = ClaudeAssistantMessage | ClaudeUserMessage | ClaudeResultMessage | ClaudeSystemMessage | ClaudeRateLimitEvent | Record<string, unknown>;
type ClaudePermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk' | 'auto';

/**
Expand All @@ -130,6 +149,8 @@ interface ClaudeQueryOptions {
options?: {
cwd?: string;
model?: string;
/** Extra absolute directories Claude may access beyond `cwd`. */
additionalDirectories?: string[];
customSystemPrompt?: string;
appendSystemPrompt?: string;
permissionMode?: ClaudePermissionMode;
Expand Down Expand Up @@ -401,6 +422,7 @@ export class ClaudeSDKService implements ISDKService {
abortController,
options: {
...(options.workingDirectory ? { cwd: options.workingDirectory } : {}),
additionalDirectories: this.resolveAdditionalDirectories(options),
...(model ? { model } : {}),
...(options.systemMessage?.mode === 'append' ? { appendSystemPrompt: options.systemMessage.content } : {}),
...(options.systemMessage?.mode === 'replace' ? { customSystemPrompt: options.systemMessage.content } : {}),
Expand All @@ -418,13 +440,15 @@ export class ClaudeSDKService implements ISDKService {
publishProviderSessionId(this.extractSessionId(msg));
if (this.isAssistantMessage(msg)) {
for (const block of msg.message.content) {
if (block.type === 'text') {
if (this.isClaudeTextBlock(block)) {
chunks.push(block.text);
options.onStreamingChunk?.(block.text);
} else if (block.type === 'tool_use') {
} else if (this.isClaudeToolUseBlock(block)) {
this.handleClaudeToolUse(block, options, toolCalls, startedToolCalls);
}
}
} else if (this.isUserMessage(msg)) {
this.handleClaudeUserToolResults(msg, options, toolCalls);
} else if (this.isResultMessage(msg)) {
if (msg.subtype !== 'success' || msg.is_error) {
const errText = typeof msg.result === 'string' && msg.result
Expand Down Expand Up @@ -546,6 +570,33 @@ export class ClaudeSDKService implements ISDKService {
);
}

private isClaudeTextBlock(block: ClaudeContentBlock): block is ClaudeTextBlock {
return (
typeof block === 'object' &&
block !== null &&
(block as Record<string, unknown>).type === 'text' &&
typeof (block as Record<string, unknown>).text === 'string'
);
}

private isClaudeToolUseBlock(block: ClaudeContentBlock): block is ClaudeToolUseBlock {
return (
typeof block === 'object' &&
block !== null &&
(block as Record<string, unknown>).type === 'tool_use' &&
typeof (block as Record<string, unknown>).id === 'string' &&
typeof (block as Record<string, unknown>).name === 'string'
);
}

private isUserMessage(msg: ClaudeSDKMessage): msg is ClaudeUserMessage {
return (
typeof msg === 'object' &&
msg !== null &&
(msg as Record<string, unknown>).type === 'user'
);
}

private isResultMessage(msg: ClaudeSDKMessage): msg is ClaudeResultMessage {
return (
typeof msg === 'object' &&
Expand Down Expand Up @@ -593,20 +644,108 @@ export class ClaudeSDKService implements ISDKService {
parameters,
});
}
}

private handleClaudeUserToolResults(
msg: ClaudeUserMessage,
options: SendMessageOptions,
toolCalls: Map<string, ToolCall>,
): void {
const handledIds = new Set<string>();
for (const block of this.getToolResultBlocks(msg)) {
handledIds.add(block.tool_use_id);
this.handleClaudeToolResult(block.tool_use_id, block.content, !!block.is_error, options, toolCalls);
}

const fallbackId = typeof msg.parent_tool_use_id === 'string' && msg.parent_tool_use_id
? msg.parent_tool_use_id
: undefined;
if (fallbackId && !handledIds.has(fallbackId) && msg.tool_use_result !== undefined) {
this.handleClaudeToolResult(fallbackId, msg.tool_use_result, false, options, toolCalls);
}
}

private getToolResultBlocks(msg: ClaudeUserMessage): ClaudeToolResultBlock[] {
const content = msg.message?.content;
if (!Array.isArray(content)) return [];
return content.filter((block): block is ClaudeToolResultBlock => (
typeof block === 'object' &&
block !== null &&
(block as Record<string, unknown>).type === 'tool_result' &&
typeof (block as Record<string, unknown>).tool_use_id === 'string'
));
}

private handleClaudeToolResult(
toolCallId: string,
content: unknown,
isError: boolean,
options: SendMessageOptions,
toolCalls: Map<string, ToolCall>,
): void {
const existing = toolCalls.get(toolCallId);
const toolName = existing?.name ?? 'unknown_tool';
const result = this.stringifyClaudeToolResult(content);
const now = new Date();

// Claude Code SDK emits tool_use blocks in assistant messages when the tool
// completes. Mark it complete immediately.
const existing = toolCalls.get(id);
if (existing) {
existing.status = 'completed';
existing.endTime = new Date();
existing.status = isError ? 'failed' : 'completed';
existing.endTime = now;
if (isError) {
existing.error = result || 'Claude tool failed';
} else {
existing.result = result;
}
} else {
toolCalls.set(toolCallId, {
id: toolCallId,
name: toolName,
status: isError ? 'failed' : 'completed',
startTime: now,
endTime: now,
args: {},
...(isError ? { error: result || 'Claude tool failed' } : { result }),
});
}

this.emitToolEvent(options, isError
? {
type: 'tool-failed',
toolCallId,
toolName,
error: result || 'Claude tool failed',
}
: {
type: 'tool-complete',
toolCallId,
toolName,
result,
});
}

private stringifyClaudeToolResult(content: unknown): string {
if (content == null) return '';
if (typeof content === 'string') return content;
if (Array.isArray(content)) {
return content
.map(item => this.stringifyClaudeToolResult(item))
.filter(text => text.length > 0)
.join('\n');
}
if (typeof content === 'object') {
const record = content as Record<string, unknown>;
if (record.type === 'text' && typeof record.text === 'string') return record.text;
const stdout = typeof record.stdout === 'string' ? record.stdout : '';
const stderr = typeof record.stderr === 'string' ? record.stderr : '';
if (stdout || stderr) return [stdout, stderr].filter(Boolean).join('\n');
if (typeof record.output === 'string') return record.output;
if (typeof record.result === 'string') return record.result;
}
try {
return JSON.stringify(content);
} catch {
return String(content);
}
this.emitToolEvent(options, {
type: 'tool-complete',
toolCallId: id,
toolName,
result: JSON.stringify(parameters),
});
}

private emitToolEvent(options: SendMessageOptions, event: ToolEvent): void {
Expand Down Expand Up @@ -647,6 +786,33 @@ export class ClaudeSDKService implements ISDKService {
return undefined;
}

/**
* Builds the list of absolute directories Claude may access beyond its
* working directory. Always includes `~/.coc` (CoC's data/skills dir) and
* the system temp directory so out-of-repo skill files and temp artifacts
* are readable, plus any caller-provided directories. Paths are resolved
* to absolute form and de-duplicated (case-insensitively on Windows).
*/
private resolveAdditionalDirectories(options: SendMessageOptions): string[] {
const candidates = [
...(options.additionalDirectories ?? []),
path.join(os.homedir(), '.coc'),
os.tmpdir(),
];

const seen = new Set<string>();
const result: string[] = [];
for (const dir of candidates) {
if (!dir) continue;
const resolved = path.resolve(dir);
const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
if (seen.has(key)) continue;
seen.add(key);
result.push(resolved);
}
return result;
}

private resolveClaudePermissionOptions(
mode: SendMessageOptions['mode'],
): Pick<NonNullable<ClaudeQueryOptions['options']>, 'permissionMode' | 'allowDangerouslySkipPermissions'> {
Expand Down
Loading
Loading