From ec8dbbb569d0e71276eafdd66c2f16e1b02fd1be Mon Sep 17 00:00:00 2001 From: Serge Margovsky Date: Fri, 17 Apr 2026 13:31:59 -0500 Subject: [PATCH 1/6] wip --- app/components/CardDetail.tsx | 16 ++++++++ app/components/SessionView.tsx | 49 ++++++++++++++++++++----- src/server/controllers/card-sessions.ts | 19 ++++++++++ src/shared/ws-protocol.ts | 1 + 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/app/components/CardDetail.tsx b/app/components/CardDetail.tsx index 2daec09..c6a39a0 100644 --- a/app/components/CardDetail.tsx +++ b/app/components/CardDetail.tsx @@ -448,6 +448,22 @@ export const CardDetail = observer(function CardDetail({ cardId, onClose, clearS )} + {/* PR URL */} + {hasSession && ( +
+ + { + const val = e.target.value.trim() || null; + if (val !== card.prUrl) cardStore.updateCard({ id: card.id, prUrl: val }); + }} + placeholder="https://github.com/org/repo/pull/123" + /> +
+ )} + {/* Model & Thinking */} {!hasSession && (
diff --git a/app/components/SessionView.tsx b/app/components/SessionView.tsx index f85a365..b1f9530 100644 --- a/app/components/SessionView.tsx +++ b/app/components/SessionView.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useEffect } from 'react'; import { observer } from 'mobx-react-lite'; -import { Send, Square, Play, AlertCircle, ChevronDown, Paperclip, X, WifiOff } from 'lucide-react'; +import { Send, Square, Play, AlertCircle, ChevronDown, Paperclip, X, WifiOff, MessageSquareWarning } from 'lucide-react'; import { MessageBlock } from './MessageBlock'; import { Button } from '~/components/ui/button'; import { Textarea } from '~/components/ui/textarea'; @@ -353,15 +353,28 @@ export const SessionView = observer(function SessionView({ {isStopping ? 'Stopping...' : 'Stop'} ) : sessionId ? ( - +
+ {card?.column === 'review' && card?.prUrl && ( + + )} + +
) : null}
)} @@ -390,6 +403,22 @@ export const SessionView = observer(function SessionView({ ); }); +function buildPrCommentsPrompt(prUrl: string): string { + return `Review and address all comments on this pull request: ${prUrl} + +Steps: +1. Run \`gh pr view ${prUrl} --json comments,reviews\` to get all PR comments and review comments +2. Also run \`gh api repos/{owner}/{repo}/pulls/{number}/comments\` for inline code comments +3. For each comment: + - Evaluate whether the feedback is actionable + - If it requires a code change, make the fix + - If it's a question, add a reply via \`gh pr comment\` +4. After making all changes, commit with a message referencing the PR review +5. Push the changes + +Be thorough — address every comment, don't skip any.`; +} + // --- Status badge --- function StatusBadge({ status }: { status: string }) { diff --git a/src/server/controllers/card-sessions.ts b/src/server/controllers/card-sessions.ts index 721f99f..60233a8 100644 --- a/src/server/controllers/card-sessions.ts +++ b/src/server/controllers/card-sessions.ts @@ -72,6 +72,25 @@ export function initOrcdRouter( } } } + + if (sdkEvent.type === 'tool_use_summary') { + const summary = sdkEvent as { tool_result?: string }; + if (summary.tool_result) { + const prMatch = summary.tool_result.match( + /https:\/\/(?:github\.com|gitlab\.com)\/[^\s]+\/pull\/\d+/, + ); + if (prMatch) { + const card = await repo().findOneBy({ id: cardId }); + if (card && !card.prUrl) { + card.prUrl = prMatch[0]; + card.updatedAt = new Date().toISOString(); + await repo().save(card); + console.log(`[oc:${cardId}] auto-detected prUrl: ${prMatch[0]}`); + bus.publish('board:changed', { card, oldColumn: card.column, newColumn: card.column }); + } + } + } + } } if (msg.type === 'result') { diff --git a/src/shared/ws-protocol.ts b/src/shared/ws-protocol.ts index 17ad565..e2473b8 100644 --- a/src/shared/ws-protocol.ts +++ b/src/shared/ws-protocol.ts @@ -73,6 +73,7 @@ export const cardCreateSchema = z.object({ summarizeThreshold: z.number().min(0).max(1).optional(), worktreeBranch: z.string().nullable().optional(), sourceBranch: z.enum(['main', 'dev']).nullable().optional(), + prUrl: z.string().nullable().optional(), archiveOthers: z.boolean().optional(), }); From 05edd78a044c433997e6181ec710ee0fb4ae94b3 Mon Sep 17 00:00:00 2001 From: Serge Margovsky Date: Fri, 17 Apr 2026 13:53:37 -0500 Subject: [PATCH 2/6] wip --- app/components/SessionView.tsx | 64 ++++++++++++++++++++++++++++------ src/server/init.ts | 36 +++++++++++++++++++ 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/app/components/SessionView.tsx b/app/components/SessionView.tsx index b1f9530..a0270df 100644 --- a/app/components/SessionView.tsx +++ b/app/components/SessionView.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useEffect } from 'react'; import { observer } from 'mobx-react-lite'; -import { Send, Square, Play, AlertCircle, ChevronDown, Paperclip, X, WifiOff, MessageSquareWarning } from 'lucide-react'; +import { Send, Square, Play, AlertCircle, ChevronDown, Paperclip, X, WifiOff, GitPullRequestArrow, LoaderCircle } from 'lucide-react'; import { MessageBlock } from './MessageBlock'; import { Button } from '~/components/ui/button'; import { Textarea } from '~/components/ui/textarea'; @@ -355,15 +355,11 @@ export const SessionView = observer(function SessionView({ ) : sessionId ? (
{card?.column === 'review' && card?.prUrl && ( - + handleSend(prompt)} + /> )} + ); +} + // --- Status badge --- function StatusBadge({ status }: { status: string }) { diff --git a/src/server/init.ts b/src/server/init.ts index c047192..bdafcfc 100644 --- a/src/server/init.ts +++ b/src/server/init.ts @@ -58,6 +58,42 @@ export async function initBackend(): Promise<{ res.json({ files: refs }); }); + // PR status check + const { execFile } = await import('child_process'); + const { promisify } = await import('util'); + const execFileAsync = promisify(execFile); + + router.post('/api/pr-check', async (req: Request, res: Response) => { + const prUrl = req.body?.prUrl as string | undefined; + if (!prUrl) { + console.warn('[rest:pr-check] missing prUrl in request body'); + res.status(400).json({ error: 'prUrl required' }); + return; + } + + try { + const { stdout } = await execFileAsync('gh', [ + 'pr', 'view', prUrl, + '--json', 'state,comments,reviews', + ], { timeout: 15_000 }); + + const pr = JSON.parse(stdout) as { + state: string; + comments: unknown[]; + reviews: { body: string; state: string }[]; + }; + + const merged = pr.state === 'MERGED'; + const reviewComments = pr.reviews?.filter((r) => r.body || r.state === 'CHANGES_REQUESTED') ?? []; + const hasComments = (pr.comments?.length ?? 0) > 0 || reviewComments.length > 0; + + res.json({ state: pr.state, merged, hasComments }); + } catch (err) { + console.error('[rest:pr-check]', err); + res.status(502).json({ error: 'Failed to check PR status' }); + } + }); + // OpenAPI spec + Swagger UI const { readFileSync } = await import('fs'); const { resolve } = await import('path'); From 4a4e8c2ce450c32e5457dfbc087ab20e6c3f8da5 Mon Sep 17 00:00:00 2001 From: Serge Margovsky Date: Fri, 17 Apr 2026 14:25:35 -0500 Subject: [PATCH 3/6] wip --- app/components/ProjectForm.tsx | 2 +- config.example.yaml | 1 + scripts/backup-db.sh | 5 +++-- src/lib/memory-upsert.ts | 6 ++++-- src/lib/session-compactor.ts | 9 ++++++--- src/orcd/config.ts | 2 ++ src/orcd/index.ts | 2 +- src/orcd/session.ts | 5 ++++- src/orcd/socket-server.ts | 4 ++++ src/shared/config.ts | 2 ++ 10 files changed, 28 insertions(+), 10 deletions(-) diff --git a/app/components/ProjectForm.tsx b/app/components/ProjectForm.tsx index 7539c9b..2df9265 100644 --- a/app/components/ProjectForm.tsx +++ b/app/components/ProjectForm.tsx @@ -121,7 +121,7 @@ export default observer(function ProjectForm({ project, onDone }: ProjectFormPro type="text" value={path} onChange={(e) => setPath(e.target.value)} - placeholder="/home/ryan/Code/my-project" + placeholder="~/Code/my-project" className="font-mono" />
diff --git a/config.example.yaml b/config.example.yaml index d058fb8..f377fb8 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -6,6 +6,7 @@ socket: ~/.orc/orcd.sock # orcd UNIX socket path defaultProvider: anthropic defaultModel: claude-sonnet-4-6 defaultCwd: ~/Code # where new cards default their working directory +# claudeCodePath: /usr/local/bin/claude # path to claude CLI binary (omit to use PATH default) providers: # Claude via Anthropic API. diff --git a/scripts/backup-db.sh b/scripts/backup-db.sh index c02d3ff..89e5f00 100755 --- a/scripts/backup-db.sh +++ b/scripts/backup-db.sh @@ -1,8 +1,9 @@ #!/bin/bash set -ex -DB="/home/ryan/Code/orchestrel/data/orchestrel.db" -BACKUP_DIR="/mnt/D/Sync/orchestra-backups" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DB="${ORC_DB_PATH:-$SCRIPT_DIR/../data/orchestrel.db}" +BACKUP_DIR="${ORC_BACKUP_DIR:-/mnt/D/Sync/orchestra-backups}" MAX_AGE_DAYS=3 # SQLite-safe backup using .backup command diff --git a/src/lib/memory-upsert.ts b/src/lib/memory-upsert.ts index 92c5654..7572e7a 100644 --- a/src/lib/memory-upsert.ts +++ b/src/lib/memory-upsert.ts @@ -27,6 +27,7 @@ export interface MemoryUpsertOpts { projectName: string; model: string; env?: Record; + claudeCodePath?: string; memoryBaseUrl: string; memoryApiKey: string; maxExcerptChars?: number; @@ -69,8 +70,9 @@ async function extractFacts( excerpt: string, model: string, env?: Record, + claudeCodePath?: string, ): Promise { - const { text } = await queryAgentSdk(EXTRACTION_PROMPT + excerpt, model, env); + const { text } = await queryAgentSdk(EXTRACTION_PROMPT + excerpt, model, env, claudeCodePath); const facts: MemoryFact[] = []; for (const line of text.split('\n')) { @@ -231,7 +233,7 @@ export async function upsertMemories(opts: MemoryUpsertOpts): Promise; + claudeCodePath?: string; ratio?: number; maxExcerptChars?: number; dryRun?: boolean; @@ -173,6 +174,7 @@ export async function queryAgentSdk( prompt: string, model: string, env?: Record, + claudeCodePath?: string, ): Promise<{ text: string; durationMs: number }> { const { query: sdkQuery } = await import('@anthropic-ai/claude-agent-sdk'); const t0 = Date.now(); @@ -184,7 +186,7 @@ export async function queryAgentSdk( maxTurns: 1, permissionMode: 'bypassPermissions', allowDangerouslySkipPermissions: true, - pathToClaudeCodeExecutable: '/home/ryan/.local/bin/claude', + ...(claudeCodePath ? { pathToClaudeCodeExecutable: claudeCodePath } : {}), ...(env ? { env } : {}), }, }); @@ -230,8 +232,9 @@ async function summarize( excerpt: string, model: string, env?: Record, + claudeCodePath?: string, ): Promise<{ summary: string; durationMs: number }> { - const { text: summary, durationMs } = await queryAgentSdk(SUMMARIZE_PROMPT + excerpt, model, env); + const { text: summary, durationMs } = await queryAgentSdk(SUMMARIZE_PROMPT + excerpt, model, env, claudeCodePath); return { summary, durationMs }; } @@ -322,7 +325,7 @@ export async function prepareCompaction(opts: CompactOpts): Promise; memoryUpsert?: MemoryUpsertConfig; } @@ -41,6 +42,7 @@ function toOrcdShape(cfg: OrchestrelConfig): OrcdConfig { defaultProvider: cfg.defaultProvider, defaultModel: cfg.defaultModel, defaultCwd: cfg.defaultCwd, + claudeCodePath: cfg.claudeCodePath, providers, memoryUpsert: cfg.memoryUpsert, }; diff --git a/src/orcd/index.ts b/src/orcd/index.ts index de72885..698abda 100644 --- a/src/orcd/index.ts +++ b/src/orcd/index.ts @@ -13,7 +13,7 @@ async function main() { const server = new OrcdServer(socketPath, config.providers, { provider: config.defaultProvider, model: config.defaultModel, - }, config.memoryUpsert); + }, config.memoryUpsert, config.claudeCodePath); await server.start(); diff --git a/src/orcd/session.ts b/src/orcd/session.ts index 5334b9e..e1fc84e 100644 --- a/src/orcd/session.ts +++ b/src/orcd/session.ts @@ -31,6 +31,7 @@ export class OrcdSession { readonly model: string; readonly provider: string; readonly contextWindow: number | undefined; + readonly claudeCodePath: string | undefined; readonly summarizeThreshold: number; readonly buffer: RingBuffer; @@ -48,6 +49,7 @@ export class OrcdSession { bufferSize?: number; sessionId?: string; // For resume — use existing CC session UUID contextWindow?: number; + claudeCodePath?: string; summarizeThreshold?: number; }) { this.id = opts.sessionId ?? randomUUID(); @@ -55,6 +57,7 @@ export class OrcdSession { this.model = opts.model; this.provider = opts.provider; this.contextWindow = opts.contextWindow; + this.claudeCodePath = opts.claudeCodePath; this.summarizeThreshold = opts.summarizeThreshold ?? 0; this.buffer = new RingBuffer(opts.bufferSize ?? 1000); } @@ -111,7 +114,7 @@ export class OrcdSession { allowDangerouslySkipPermissions: true, settingSources: ['user', 'project'], includePartialMessages: true, - pathToClaudeCodeExecutable: '/home/ryan/.local/bin/claude', + ...(this.claudeCodePath ? { pathToClaudeCodeExecutable: this.claudeCodePath } : {}), env: opts.env, ...thinkingOpts, ...(autoCompactWindow ? { settings: { autoCompactWindow } } : {}), diff --git a/src/orcd/socket-server.ts b/src/orcd/socket-server.ts index 1b21d2f..0a72d22 100644 --- a/src/orcd/socket-server.ts +++ b/src/orcd/socket-server.ts @@ -29,6 +29,7 @@ export class OrcdServer { private providers: Record, private defaults: { provider: string; model: string }, memoryConfig?: OrcdConfig['memoryUpsert'], + private claudeCodePath?: string, ) { this.memoryConfig = memoryConfig; } @@ -145,6 +146,7 @@ export class OrcdServer { sessionId: action.sessionId, contextWindow: action.contextWindow, summarizeThreshold: action.summarizeThreshold, + claudeCodePath: this.claudeCodePath, }); this.store.add(session); @@ -307,6 +309,7 @@ export class OrcdServer { projectName: session.cwd.split('/').pop() ?? 'unknown', model: session.model, env, + claudeCodePath: this.claudeCodePath, memoryBaseUrl: this.memoryConfig.baseUrl, memoryApiKey: this.memoryConfig.apiKey, }); @@ -340,6 +343,7 @@ export class OrcdServer { projectPath: session.cwd, model: session.model, env, + claudeCodePath: this.claudeCodePath, }); if (this.turnActive.has(sid)) { diff --git a/src/shared/config.ts b/src/shared/config.ts index d830ef4..5eca5ca 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -33,6 +33,7 @@ export interface OrchestrelConfig { defaultProvider: string; defaultModel: string; defaultCwd?: string; + claudeCodePath?: string; providers: Record; memoryUpsert?: MemoryUpsertConfig; } @@ -96,6 +97,7 @@ export function parseConfig( defaultProvider: String(raw.defaultProvider ?? 'anthropic'), defaultModel: String(raw.defaultModel ?? 'claude-sonnet-4-6'), defaultCwd: raw.defaultCwd != null ? String(raw.defaultCwd) : undefined, + claudeCodePath: raw.claudeCodePath != null ? resolveEnvVars(String(raw.claudeCodePath), env) : undefined, providers, memoryUpsert, }; From 3760d56364addb39e54fbc3c070b521216c1c65e Mon Sep 17 00:00:00 2001 From: Serge Margovsky Date: Thu, 30 Apr 2026 10:14:04 -0500 Subject: [PATCH 4/6] wip --- config.example.yaml | 5 +++ src/orcd/config.ts | 2 ++ src/orcd/index.ts | 6 +++- src/orcd/session.ts | 75 +++++++++++++++++++++++++++++++-------- src/orcd/socket-server.ts | 2 ++ src/shared/config.ts | 6 ++++ 6 files changed, 80 insertions(+), 16 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index f377fb8..9b6df06 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -8,6 +8,11 @@ defaultModel: claude-sonnet-4-6 defaultCwd: ~/Code # where new cards default their working directory # claudeCodePath: /usr/local/bin/claude # path to claude CLI binary (omit to use PATH default) +# Extra Claude settings files loaded into every session (highest priority). +# Permissions arrays merge; other fields use last-wins. +# extraSettings: +# - ~/rula-projects/.claude/settings.local.json + providers: # Claude via Anthropic API. # Omit baseUrl + apiKey to fall through to Claude Max OAuth (claude-agent-sdk default). diff --git a/src/orcd/config.ts b/src/orcd/config.ts index 0f0f2ec..ec264eb 100644 --- a/src/orcd/config.ts +++ b/src/orcd/config.ts @@ -17,6 +17,7 @@ export interface OrcdConfig { defaultModel: string; defaultCwd?: string; claudeCodePath?: string; + extraSettings?: string[]; providers: Record; memoryUpsert?: MemoryUpsertConfig; } @@ -43,6 +44,7 @@ function toOrcdShape(cfg: OrchestrelConfig): OrcdConfig { defaultModel: cfg.defaultModel, defaultCwd: cfg.defaultCwd, claudeCodePath: cfg.claudeCodePath, + extraSettings: cfg.extraSettings, providers, memoryUpsert: cfg.memoryUpsert, }; diff --git a/src/orcd/index.ts b/src/orcd/index.ts index 698abda..d9a71fc 100644 --- a/src/orcd/index.ts +++ b/src/orcd/index.ts @@ -10,10 +10,14 @@ async function main() { // Resolve ~ in socket path const socketPath = config.socket.replace(/^~/, homedir()); + const extraSettings = (config.extraSettings ?? []).map( + (p) => p.replace(/^~/, homedir()), + ); + const server = new OrcdServer(socketPath, config.providers, { provider: config.defaultProvider, model: config.defaultModel, - }, config.memoryUpsert, config.claudeCodePath); + }, config.memoryUpsert, config.claudeCodePath, extraSettings); await server.start(); diff --git a/src/orcd/session.ts b/src/orcd/session.ts index e1fc84e..f9ce8d0 100644 --- a/src/orcd/session.ts +++ b/src/orcd/session.ts @@ -1,12 +1,55 @@ -import { randomUUID } from 'crypto'; +import type { Options, Query } from '@anthropic-ai/claude-agent-sdk'; import { query as sdkQuery } from '@anthropic-ai/claude-agent-sdk'; -import type { Query, Options } from '@anthropic-ai/claude-agent-sdk'; +import { randomUUID } from 'crypto'; +import { readFileSync } from 'fs'; import { AUTO_COMPACT_RATIO } from '../shared/constants'; +import type { + ContextUsageMessage, + SessionErrorMessage, + SessionExitMessage, + SessionResultMessage, + StreamEventMessage, +} from '../shared/orcd-protocol'; import { RingBuffer } from './ring-buffer'; import type { SessionState } from './types'; -import type { StreamEventMessage, SessionErrorMessage, SessionResultMessage, SessionExitMessage, ContextUsageMessage } from '../shared/orcd-protocol'; -export type SessionEventCallback = (msg: StreamEventMessage | SessionResultMessage | SessionErrorMessage | SessionExitMessage | ContextUsageMessage) => void; +type SettingsObj = Record; + +function loadAndMergeSettings(paths: string[]): SettingsObj | undefined { + if (paths.length === 0) { + console.log(`[orcd:effort] settings → no paths`); + return undefined; + } + const merged: SettingsObj = {}; + for (const p of paths) { + try { + const raw = JSON.parse(readFileSync(p, 'utf-8')) as SettingsObj; + for (const [k, v] of Object.entries(raw)) { + if (k === 'permissions' && typeof v === 'object' && v !== null) { + const existing = (merged.permissions ?? {}) as Record; + const incoming = v as Record; + for (const [pk, pv] of Object.entries(incoming)) { + if (Array.isArray(pv) && Array.isArray(existing[pk])) { + existing[pk] = [...(existing[pk] as unknown[]), ...pv]; + } else { + existing[pk] = pv; + } + } + merged.permissions = existing; + } else { + merged[k] = v; + } + } + } catch (err) { + console.warn(`[orcd] failed to load extra settings ${p}: ${err instanceof Error ? err.message : err}`); + } + } + return Object.keys(merged).length > 0 ? merged : undefined; +} + +export type SessionEventCallback = ( + msg: StreamEventMessage | SessionResultMessage | SessionErrorMessage | SessionExitMessage | ContextUsageMessage, +) => void; /** * Map effort string to SDK options for thinking/effort. @@ -32,6 +75,7 @@ export class OrcdSession { readonly provider: string; readonly contextWindow: number | undefined; readonly claudeCodePath: string | undefined; + readonly extraSettings: string[]; readonly summarizeThreshold: number; readonly buffer: RingBuffer; @@ -47,9 +91,10 @@ export class OrcdSession { model: string; provider: string; bufferSize?: number; - sessionId?: string; // For resume — use existing CC session UUID + sessionId?: string; // For resume — use existing CC session UUID contextWindow?: number; claudeCodePath?: string; + extraSettings?: string[]; summarizeThreshold?: number; }) { this.id = opts.sessionId ?? randomUUID(); @@ -58,6 +103,7 @@ export class OrcdSession { this.provider = opts.provider; this.contextWindow = opts.contextWindow; this.claudeCodePath = opts.claudeCodePath; + this.extraSettings = opts.extraSettings ?? []; this.summarizeThreshold = opts.summarizeThreshold ?? 0; this.buffer = new RingBuffer(opts.bufferSize ?? 1000); } @@ -89,12 +135,7 @@ export class OrcdSession { * Start or resume a session. * Consumes the Agent SDK async iterator and broadcasts events. */ - async run(opts: { - prompt: string; - resume?: boolean; - env?: Record; - effort?: string; - }): Promise { + async run(opts: { prompt: string; resume?: boolean; env?: Record; effort?: string }): Promise { const log = (msg: string) => console.log(`[orcd:${this.id.slice(0, 8)}] ${msg}`); const thinkingOpts = effortToOptions(opts.effort); @@ -104,6 +145,12 @@ export class OrcdSession { ? Math.max(Math.floor(this.contextWindow * AUTO_COMPACT_RATIO), 100_000) : undefined; + const extraMerged = loadAndMergeSettings(this.extraSettings); + const settings: SettingsObj = { + ...(extraMerged ?? {}), + ...(autoCompactWindow ? { autoCompactWindow } : {}), + }; + const q = sdkQuery({ prompt: opts.prompt, options: { @@ -117,7 +164,7 @@ export class OrcdSession { ...(this.claudeCodePath ? { pathToClaudeCodeExecutable: this.claudeCodePath } : {}), env: opts.env, ...thinkingOpts, - ...(autoCompactWindow ? { settings: { autoCompactWindow } } : {}), + ...(Object.keys(settings).length > 0 ? { settings } : {}), }, }); @@ -149,9 +196,7 @@ export class OrcdSession { const u = msg?.usage as Record | undefined; if (u) { lastInputTokens = - (u.input_tokens ?? 0) + - (u.cache_creation_input_tokens ?? 0) + - (u.cache_read_input_tokens ?? 0); + (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0); } } else if (inner?.type === 'message_delta' && lastInputTokens === 0) { const u = inner.usage as Record | undefined; diff --git a/src/orcd/socket-server.ts b/src/orcd/socket-server.ts index 0a72d22..41d366a 100644 --- a/src/orcd/socket-server.ts +++ b/src/orcd/socket-server.ts @@ -30,6 +30,7 @@ export class OrcdServer { private defaults: { provider: string; model: string }, memoryConfig?: OrcdConfig['memoryUpsert'], private claudeCodePath?: string, + private extraSettings?: string[], ) { this.memoryConfig = memoryConfig; } @@ -147,6 +148,7 @@ export class OrcdServer { contextWindow: action.contextWindow, summarizeThreshold: action.summarizeThreshold, claudeCodePath: this.claudeCodePath, + extraSettings: this.extraSettings, }); this.store.add(session); diff --git a/src/shared/config.ts b/src/shared/config.ts index 5eca5ca..766e5ec 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -34,6 +34,7 @@ export interface OrchestrelConfig { defaultModel: string; defaultCwd?: string; claudeCodePath?: string; + extraSettings?: string[]; providers: Record; memoryUpsert?: MemoryUpsertConfig; } @@ -92,12 +93,17 @@ export function parseConfig( } : undefined; + const extraSettings = Array.isArray(raw.extraSettings) + ? (raw.extraSettings as unknown[]).map((s) => resolveEnvVars(String(s), env)) + : undefined; + return { socket: String(raw.socket ?? '~/.orc/orcd.sock'), defaultProvider: String(raw.defaultProvider ?? 'anthropic'), defaultModel: String(raw.defaultModel ?? 'claude-sonnet-4-6'), defaultCwd: raw.defaultCwd != null ? String(raw.defaultCwd) : undefined, claudeCodePath: raw.claudeCodePath != null ? resolveEnvVars(String(raw.claudeCodePath), env) : undefined, + extraSettings, providers, memoryUpsert, }; From 5dc6ee74d538d8de2e9e15126fbae255c9005ba0 Mon Sep 17 00:00:00 2001 From: Serge Margovsky Date: Thu, 30 Apr 2026 12:07:00 -0500 Subject: [PATCH 5/6] fix ci --- app/components/SessionView.tsx | 30 +++++++++++++++++++++++++++++- src/server/init.ts | 8 ++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/app/components/SessionView.tsx b/app/components/SessionView.tsx index 205ccca..faffd3d 100644 --- a/app/components/SessionView.tsx +++ b/app/components/SessionView.tsx @@ -321,6 +321,26 @@ Steps: Be thorough — address every comment, don't skip any.`; } +function buildFailedChecksPrompt( + prUrl: string, + failedChecks: { name: string; conclusion: string; detailsUrl: string }[], +): string { + const checkList = failedChecks.map((c) => `- **${c.name}** (${c.conclusion})${c.detailsUrl ? `: ${c.detailsUrl}` : ''}`).join('\n'); + return `CI checks failed on this pull request: ${prUrl} + +Failed checks: +${checkList} + +Steps: +1. For each failed check, run \`gh run view --log-failed\` to get failure logs (extract run ID from the details URL) +2. Analyze the failure — is it a test failure, lint error, build error, or flaky test? +3. Fix the root cause in code +4. After making all fixes, commit with a message referencing the CI failures +5. Push the changes + +If a check failed due to a flaky test (not related to this PR's changes), note it but focus on genuine failures.`; +} + // --- Check PR button --- function CheckPrButton({ @@ -345,10 +365,18 @@ function CheckPrButton({ }); if (!res.ok) return; - const data = (await res.json()) as { merged: boolean; hasComments: boolean }; + const data = (await res.json()) as { + merged: boolean; + hasComments: boolean; + failedChecks: { name: string; conclusion: string; detailsUrl: string }[]; + }; if (data.merged) { await cardStore.updateCard({ id: cardId, column: 'done' }); + } else if (data.failedChecks?.length > 0 && data.hasComments) { + onAddressComments(buildFailedChecksPrompt(prUrl, data.failedChecks) + '\n\n---\n\nAlso, ' + buildPrCommentsPrompt(prUrl)); + } else if (data.failedChecks?.length > 0) { + onAddressComments(buildFailedChecksPrompt(prUrl, data.failedChecks)); } else if (data.hasComments) { onAddressComments(buildPrCommentsPrompt(prUrl)); } diff --git a/src/server/init.ts b/src/server/init.ts index bdafcfc..4939a95 100644 --- a/src/server/init.ts +++ b/src/server/init.ts @@ -74,20 +74,24 @@ export async function initBackend(): Promise<{ try { const { stdout } = await execFileAsync('gh', [ 'pr', 'view', prUrl, - '--json', 'state,comments,reviews', + '--json', 'state,comments,reviews,statusCheckRollup', ], { timeout: 15_000 }); const pr = JSON.parse(stdout) as { state: string; comments: unknown[]; reviews: { body: string; state: string }[]; + statusCheckRollup: { name: string; status: string; conclusion: string; detailsUrl: string }[]; }; const merged = pr.state === 'MERGED'; const reviewComments = pr.reviews?.filter((r) => r.body || r.state === 'CHANGES_REQUESTED') ?? []; const hasComments = (pr.comments?.length ?? 0) > 0 || reviewComments.length > 0; + const failedChecks = (pr.statusCheckRollup ?? []).filter( + (c) => c.conclusion === 'FAILURE' || c.conclusion === 'TIMED_OUT' || c.conclusion === 'CANCELLED', + ); - res.json({ state: pr.state, merged, hasComments }); + res.json({ state: pr.state, merged, hasComments, failedChecks }); } catch (err) { console.error('[rest:pr-check]', err); res.status(502).json({ error: 'Failed to check PR status' }); From 782281b3901aef233328c2c4605458c60b004681 Mon Sep 17 00:00:00 2001 From: Serge Margovsky Date: Thu, 7 May 2026 16:09:28 -0500 Subject: [PATCH 6/6] example path --- config.example.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index fa4b7d3..677a5c2 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -2,16 +2,16 @@ # Used by both orcd (daemon) and orc (backend/UI). # Copy to config.yaml and fill in your providers. config.yaml is gitignored. -socket: ~/.orc/orcd.sock # orcd UNIX socket path +socket: ~/.orc/orcd.sock # orcd UNIX socket path defaultProvider: anthropic defaultModel: sonnet -defaultCwd: ~/Code # where new cards default their working directory +defaultCwd: ~/Code # where new cards default their working directory # claudeCodePath: /usr/local/bin/claude # path to claude CLI binary (omit to use PATH default) # Extra Claude settings files loaded into every session (highest priority). # Permissions arrays merge; other fields use last-wins. # extraSettings: -# - ~/rula-projects/.claude/settings.local.json +# - ~/Me/.claude/settings.local.json providers: # Claude via Anthropic API. @@ -19,9 +19,9 @@ providers: anthropic: label: Anthropic models: - opus: { label: "Opus 4.7", modelID: claude-opus-4-7, contextWindow: 1000000 } - sonnet: { label: "Sonnet 4.6", modelID: claude-sonnet-4-6, contextWindow: 1000000 } - haiku: { label: "Haiku 4.5", modelID: claude-haiku-4-5-20251001, contextWindow: 200000 } + opus: { label: 'Opus 4.7', modelID: claude-opus-4-7, contextWindow: 1000000 } + sonnet: { label: 'Sonnet 4.6', modelID: claude-sonnet-4-6, contextWindow: 1000000 } + haiku: { label: 'Haiku 4.5', modelID: claude-haiku-4-5-20251001, contextWindow: 200000 } # Example: a local proxy (like a Claude-compatible pool proxy). # proxy: