coxpit 6.1.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/board.ts +118 -71
- package/src/cockpit.ts +548 -52
- package/src/db/index.ts +2 -0
- package/src/db/schema.ts +4 -0
- package/src/humanize.ts +62 -0
- package/src/orchestrator.ts +57 -14
- package/src/server.ts +18 -9
package/src/db/index.ts
CHANGED
|
@@ -115,4 +115,6 @@ export async function ensureSchema(): Promise<void> {
|
|
|
115
115
|
try { await client.execute("ALTER TABLE repos ADD COLUMN kind TEXT NOT NULL DEFAULT 'git'"); } catch { /* exists */ }
|
|
116
116
|
try { await client.execute("ALTER TABLE agent_runs ADD COLUMN title TEXT NOT NULL DEFAULT ''"); } catch { /* exists */ }
|
|
117
117
|
try { await client.execute('ALTER TABLE agent_runs ADD COLUMN in_place INTEGER NOT NULL DEFAULT 0'); } catch { /* exists */ }
|
|
118
|
+
// DEFAULT 1 = 기존 run 은 real 로 남는다. 모르는 과거를 dry 로 칠하지 않기 위한 기본값이다(v5.28 H1).
|
|
119
|
+
try { await client.execute('ALTER TABLE agent_runs ADD COLUMN real INTEGER NOT NULL DEFAULT 1'); } catch { /* exists */ }
|
|
118
120
|
}
|
package/src/db/schema.ts
CHANGED
|
@@ -93,6 +93,10 @@ export const agentRuns = sqliteTable('agent_runs', {
|
|
|
93
93
|
sessionId: text('session_id').notNull().default(''), // 에이전트 세션(steer 용 --resume 키)
|
|
94
94
|
prUrl: text('pr_url').notNull().default(''), // PR 모드로 올린 pull request URL
|
|
95
95
|
model: text('model').notNull().default(''), // 런치별 모델 지정(빈값 = CLI 기본)
|
|
96
|
+
// v5.28 Part H — 이 run 이 진짜 에이전트로 돌았나(real), 모의 스트림이었나(dry).
|
|
97
|
+
// 기본 true(real)는 **의도**다: 이 컬럼이 생기기 전 run 은 알 수 없고, 모르는 것을 dry 라
|
|
98
|
+
// 부르면 거짓 경보가 된다. 아는 dry 만 표시한다 — dry 를 배지하되, dry 를 추측하지 않는다.
|
|
99
|
+
real: integer('real', { mode: 'boolean' }).notNull().default(true),
|
|
96
100
|
filesChanged: integer('files_changed').notNull().default(0),
|
|
97
101
|
agentPid: integer('agent_pid').notNull().default(0), // detached sh pgid — 재시작 후 생존 판정/stop
|
|
98
102
|
logOffset: integer('log_offset').notNull().default(0), // 내구 로그에서 tail 이 소비한 바이트(재-adopt 시 여기부터)
|
package/src/humanize.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// 이벤트 인간화 — 보드와 코크핏이 **같은 한 벌**을 쓴다(사본 금지).
|
|
2
|
+
// 보드 모달의 타임라인과 코크핏 활동 페인(v5.28 E)이 같은 줄을 같은 규칙으로 보여야 하므로,
|
|
3
|
+
// 클라이언트 JS 를 여기 한 곳에 두고 두 페이지의 <script> 에 그대로 끼워 넣는다(icons.ts 전례).
|
|
4
|
+
// 문자열 안의 이스케이프는 **클라이언트 기준**이다: \\n 은 클라이언트의 \n 이 된다.
|
|
5
|
+
export const HUMANIZE_JS = `/* 이벤트 인간화 — JSON 원문 대신 사람이 읽는 한 줄로. null = 표시 생략(노이즈). */
|
|
6
|
+
function humanize(e){
|
|
7
|
+
const kind = e.kind, payload = e.payload;
|
|
8
|
+
if (kind === 'rate_limit_event') return null;
|
|
9
|
+
if (kind === 'steer') return { k:'steer', t:'→ '+payload };
|
|
10
|
+
if (kind === 'ask') return { k:'ask', t:'? '+payload };
|
|
11
|
+
if (kind === 'sync') return { k:'sync', t:payload };
|
|
12
|
+
if (kind === 'export'){ try{ const o=JSON.parse(payload); return { k:'export', t:o.copied+' file(s) → '+o.dest }; }catch{ return { k:'export', t:payload }; } }
|
|
13
|
+
if (kind === 'pr') return { k:'pr', t:payload };
|
|
14
|
+
if (kind === 'stderr') return { k:'stderr', t:payload };
|
|
15
|
+
try{
|
|
16
|
+
const o = JSON.parse(payload);
|
|
17
|
+
if (o.type === 'system'){
|
|
18
|
+
if (o.subtype === 'init' || !o.subtype) return { k:'session',
|
|
19
|
+
t:'started'+(o.model?' · '+String(o.model).replace(/\\u001b\\[[0-9;]*m/g,'')
|
|
20
|
+
.replace(/\\x1b\\[[0-9;]*m/g,'') : '') };
|
|
21
|
+
if (o.subtype === 'permission_denied') return { k:'denied', t:'⛔ '+(o.tool_name||o.tool||'tool use')+' blocked — attach the Terminal to approve, or widen COXPIT_AGENT_PERM' };
|
|
22
|
+
return null; // thinking_tokens 등 스트림 잡음
|
|
23
|
+
}
|
|
24
|
+
if (o.type === 'user') return null; // tool 결과 회신 — 노이즈
|
|
25
|
+
if (o.type === 'assistant' && o.message){
|
|
26
|
+
const parts = [];
|
|
27
|
+
for (const x of (o.message.content||[])){
|
|
28
|
+
if (x.type === 'text' && x.text) parts.push({ k:'said', t:x.text });
|
|
29
|
+
else if (x.type === 'tool_use'){
|
|
30
|
+
const i = x.input || {};
|
|
31
|
+
const arg = i.file_path || i.command || i.path || i.pattern || '';
|
|
32
|
+
parts.push({ k:'tool', t:'▸ '+x.name+(arg?' — '+String(arg).split('/').slice(-2).join('/').slice(0,60):'') });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return parts.length ? parts : null;
|
|
36
|
+
}
|
|
37
|
+
if (o.type === 'assistant' && o.text) return { k:'said', t:o.text };
|
|
38
|
+
if (o.type === 'result') return { k:'done', t:o.result || 'finished' };
|
|
39
|
+
if (kind === 'meta' && o.subtask) return { k:'swarm', t:'↳ spawned task #'+o.subtask+' — '+String(o.title||'').slice(0,60)+' ('+((o.runs||[]).map(x=>'r'+x).join(' '))+')' };
|
|
40
|
+
if (kind === 'meta') return { k:'start', t:'worktree '+String(o.worktree||'').split('/').slice(-2).join('/') };
|
|
41
|
+
return { k:kind, t:payload.slice(0,140) };
|
|
42
|
+
}catch{
|
|
43
|
+
// 파싱 실패(과거에 잘려 저장된 이벤트 등) — JSON 잔해를 그대로 보여주지 않는다:
|
|
44
|
+
// text 조각만 구제하고, 없으면 생략.
|
|
45
|
+
if (payload.trim().startsWith('{')){
|
|
46
|
+
const texts = [];
|
|
47
|
+
const re = /"text":"((?:[^"\\\\]|\\\\.)*)"/g; let m;
|
|
48
|
+
while ((m = re.exec(payload)) && texts.length < 2) texts.push(m[1].replace(/\\\\n/g,' ').slice(0,140));
|
|
49
|
+
return texts.length ? { k:'said', t:texts.join(' · ') } : null;
|
|
50
|
+
}
|
|
51
|
+
return { k:kind, t:payload };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function humanLines(events){
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const e of (events||[])){
|
|
57
|
+
const h = humanize(e);
|
|
58
|
+
if (!h) continue;
|
|
59
|
+
if (Array.isArray(h)) out.push(...h); else out.push(h);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}`;
|
package/src/orchestrator.ts
CHANGED
|
@@ -344,10 +344,10 @@ export async function spawnSubtasks(parentRunId: number, title: string, prompt:
|
|
|
344
344
|
const runIds: number[] = [];
|
|
345
345
|
for (let i = 0; i < n; i++) {
|
|
346
346
|
const rIns = await db.insert(agentRuns).values({
|
|
347
|
-
taskId: task.id, machineId: pr.machineId, agent: pr.agent, model: pr.model, status: 'pending',
|
|
347
|
+
taskId: task.id, machineId: pr.machineId, agent: pr.agent, model: pr.model, status: 'pending', real,
|
|
348
348
|
}).returning();
|
|
349
349
|
const run = rIns[0]!;
|
|
350
|
-
broadcast({ type: 'run', runId: run.id, taskId: task.id, status: 'pending', agent: run.agent, branch: '', filesChanged: 0 });
|
|
350
|
+
broadcast({ type: 'run', runId: run.id, taskId: task.id, status: 'pending', agent: run.agent, real, branch: '', filesChanged: 0 });
|
|
351
351
|
void launchRun(run.id, real);
|
|
352
352
|
runIds.push(run.id);
|
|
353
353
|
}
|
|
@@ -484,7 +484,9 @@ export async function launchRun(runId: number, real?: boolean): Promise<void> {
|
|
|
484
484
|
const session = `coxpit-r${runId}`;
|
|
485
485
|
|
|
486
486
|
try {
|
|
487
|
-
|
|
487
|
+
// real 은 여기서 각인한다 — 명령을 고르는 그 값이 곧 run 의 사실이다(v5.28 H1).
|
|
488
|
+
// 'preparing' 에 실어 두면 worktree 단계에서 넘어져도 "이 run 은 모의였다"가 남는다.
|
|
489
|
+
await setRun(runId, { status: 'preparing', real: !!useReal, branch, worktreePath: wtPath, tmuxWindow: session, startedAt: new Date() });
|
|
488
490
|
|
|
489
491
|
// 1) worktree 생성(격리 브랜치) — in-place 는 건너뛴다(격리가 없는 것이 요점).
|
|
490
492
|
if (!inPlace) {
|
|
@@ -607,6 +609,17 @@ async function runAgentChild(runId: number, machine: MachineTarget, wtPath: stri
|
|
|
607
609
|
void notifySettle(runId, status, filesChanged, exitSummary);
|
|
608
610
|
}
|
|
609
611
|
|
|
612
|
+
/**
|
|
613
|
+
* verify 러너 — 명령 한 번, 꼬리 한 줌. 어디서 돌리든 판정 규칙은 하나여야 해서
|
|
614
|
+
* verifyRun(run 의 worktree)·verifyBase(머지된 base)가 이 함수를 같이 쓴다.
|
|
615
|
+
*/
|
|
616
|
+
async function runVerifyCmd(cmd: string, cwd: string, machine: MachineTarget): Promise<{ status: string; output: string }> {
|
|
617
|
+
const r = await runShellOn(machine, `cd ${shq(cwd)} && ( ${cmd} )`, 180000);
|
|
618
|
+
const merged = [r.stdout, r.stderr].filter(Boolean).join('\n').trim();
|
|
619
|
+
const output = merged.length > 6000 ? '…' + merged.slice(-6000) : merged;
|
|
620
|
+
return { status: r.ok ? 'pass' : r.code === -1 ? 'error' : 'fail', output };
|
|
621
|
+
}
|
|
622
|
+
|
|
610
623
|
/**
|
|
611
624
|
* Verify in-loop — repo.verifyCmd 를 run 의 worktree 에서 실행해 pass/fail 을 기록.
|
|
612
625
|
* verifyCmd 미설정이면 상태를 비우고 no-op. 정착 훅이 자동 호출(done+변경), 수동 재검증도 지원.
|
|
@@ -626,12 +639,34 @@ export async function verifyRun(runId: number): Promise<{ ok: boolean; status: s
|
|
|
626
639
|
return { ok: false, status: 'error', detail: 'worktree missing' };
|
|
627
640
|
}
|
|
628
641
|
await setRun(runId, { verifyStatus: 'running', verifyOutput: '' });
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
642
|
+
const v = await runVerifyCmd(cmd, run.worktreePath, ctx.machine);
|
|
643
|
+
await setRun(runId, { verifyStatus: v.status, verifyOutput: v.output });
|
|
644
|
+
return { ok: true, status: v.status };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* 머지된 base 검증(v5.28 J1) — repo.verifyCmd 를 **repo.path**(머지가 막 내려앉은 기본 브랜치)에서
|
|
649
|
+
* 돌린다. 승자가 내려앉은 그 호흡에 같은 명령을 돌려 pass/fail 을 그 자리에서 말하기 위한 것이라,
|
|
650
|
+
* 지나간 worktree 가 아니라 base 를 본다.
|
|
651
|
+
* - verifyCmd 가 비어 있으면 아무 명령도 추측하지 않고 no-op(status '').
|
|
652
|
+
* - best-effort — 실패는 보고일 뿐, 머지를 되돌리지 않는다.
|
|
653
|
+
*/
|
|
654
|
+
export async function verifyBase(repoId: number): Promise<{ status: string; output: string }> {
|
|
655
|
+
const none = { status: '', output: '' };
|
|
656
|
+
const rp = await db.select().from(repos).where(eq(repos.id, repoId)).limit(1);
|
|
657
|
+
const repo = rp[0];
|
|
658
|
+
if (!repo) return none;
|
|
659
|
+
const cmd = (repo.verifyCmd ?? '').trim();
|
|
660
|
+
if (!cmd) return none;
|
|
661
|
+
const mr = await db.select().from(machines).where(eq(machines.id, repo.machineId)).limit(1);
|
|
662
|
+
const m = mr[0];
|
|
663
|
+
if (!m) return none;
|
|
664
|
+
const machine: MachineTarget = { slug: m.slug, kind: m.kind, address: m.address, sshUser: m.sshUser };
|
|
665
|
+
try {
|
|
666
|
+
return await runVerifyCmd(cmd, repo.path, machine);
|
|
667
|
+
} catch (e) {
|
|
668
|
+
return { status: 'error', output: String(e).slice(0, 300) };
|
|
669
|
+
}
|
|
635
670
|
}
|
|
636
671
|
|
|
637
672
|
/**
|
|
@@ -1267,9 +1302,9 @@ export async function launchGroupTask(
|
|
|
1267
1302
|
const machineId = rp[0]!.machineId;
|
|
1268
1303
|
const tIns = await db.insert(tasks).values({ repoId, title: title.slice(0, 140), prompt, groupId }).returning();
|
|
1269
1304
|
const task = tIns[0]!;
|
|
1270
|
-
const rIns = await db.insert(agentRuns).values({ taskId: task.id, machineId, agent: 'claude-code', status: 'pending' }).returning();
|
|
1305
|
+
const rIns = await db.insert(agentRuns).values({ taskId: task.id, machineId, agent: 'claude-code', status: 'pending', real }).returning();
|
|
1271
1306
|
const run = rIns[0]!;
|
|
1272
|
-
broadcast({ type: 'run', runId: run.id, taskId: task.id, status: 'pending', agent: run.agent, branch: '', filesChanged: 0 });
|
|
1307
|
+
broadcast({ type: 'run', runId: run.id, taskId: task.id, status: 'pending', agent: run.agent, real, branch: '', filesChanged: 0 });
|
|
1273
1308
|
void launchRun(run.id, real);
|
|
1274
1309
|
return { id: task.id, title: task.title, runId: run.id };
|
|
1275
1310
|
}
|
|
@@ -1512,6 +1547,8 @@ export interface IntegrateResult {
|
|
|
1512
1547
|
detail?: string;
|
|
1513
1548
|
integrationTaskId?: number;
|
|
1514
1549
|
integrationRunId?: number;
|
|
1550
|
+
/** 머지된 건에 한해 base 검증 결과(v5.28 J1). verifyCmd 가 없으면 status ''. */
|
|
1551
|
+
verify?: { status: string; output: string };
|
|
1515
1552
|
}
|
|
1516
1553
|
|
|
1517
1554
|
/**
|
|
@@ -1529,7 +1566,13 @@ export async function integrateRuns(runIds: number[], real?: boolean): Promise<I
|
|
|
1529
1566
|
if (run.status === 'merged') { results.push({ runId: id, status: 'skipped', detail: 'already merged' }); continue; }
|
|
1530
1567
|
|
|
1531
1568
|
const m = await mergeRun(id);
|
|
1532
|
-
if (m.ok) {
|
|
1569
|
+
if (m.ok) {
|
|
1570
|
+
// 내려앉은 그 호흡에 base 를 검증한다(J1) — 실패해도 머지는 그대로 서 있고, 보고만 된다.
|
|
1571
|
+
const mc = await loadContext(id);
|
|
1572
|
+
const verify = mc ? await verifyBase(mc.repoId) : { status: '', output: '' };
|
|
1573
|
+
results.push({ runId: id, status: 'merged', verify });
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1533
1576
|
if (!m.conflict) { results.push({ runId: id, status: 'skipped', detail: m.detail }); continue; }
|
|
1534
1577
|
|
|
1535
1578
|
// 충돌 → 통합 태스크 자동 발사 (에이전트가 머지를 대신 푼다)
|
|
@@ -1544,9 +1587,9 @@ export async function integrateRuns(runIds: number[], real?: boolean): Promise<I
|
|
|
1544
1587
|
`Do not modify files unrelated to the conflicts.`;
|
|
1545
1588
|
const tIns = await db.insert(tasks).values({ repoId: ctx.repoId, title, prompt }).returning();
|
|
1546
1589
|
const newTask = tIns[0]!;
|
|
1547
|
-
const rIns = await db.insert(agentRuns).values({ taskId: newTask.id, machineId: ctx.machineId, agent: 'claude-code', status: 'pending' }).returning();
|
|
1590
|
+
const rIns = await db.insert(agentRuns).values({ taskId: newTask.id, machineId: ctx.machineId, agent: 'claude-code', status: 'pending', real: real ?? true }).returning();
|
|
1548
1591
|
const newRun = rIns[0]!;
|
|
1549
|
-
broadcast({ type: 'run', runId: newRun.id, taskId: newTask.id, status: 'pending', agent: newRun.agent, branch: '', filesChanged: 0 });
|
|
1592
|
+
broadcast({ type: 'run', runId: newRun.id, taskId: newTask.id, status: 'pending', agent: newRun.agent, real: real ?? true, branch: '', filesChanged: 0 });
|
|
1550
1593
|
void launchRun(newRun.id, real ?? true);
|
|
1551
1594
|
results.push({ runId: id, status: 'conflict', detail: m.detail, integrationTaskId: newTask.id, integrationRunId: newRun.id });
|
|
1552
1595
|
}
|
package/src/server.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { db } from './db';
|
|
|
21
21
|
import { machines, repos, tasks, agentRuns, agentEvents, designCaptures, shareLinks, taskGroups, secrets } from './db/schema';
|
|
22
22
|
import { BOOKMARKLET_JS } from './design';
|
|
23
23
|
import { runShellOn, shq } from './exec';
|
|
24
|
-
import { launchRun, cleanupRun, stopRun, getRunDiff, loadRunDocs, mergeRun, getRunTermInfo, steerRun, exportRun, prRun, integrateRuns, planFanout, reviewTask, syncRun, openWorkbench, spawnSubtasks, listSubtasks, resolveAgentToken, taskCloseRisk, launchGroupTask, isRunLive, liveInPlaceRun, askGroupCoordinator, computeRunOutputs, normalizeOutputs, listReclaimableWorktrees, pruneWorktrees, worktreeDisk, listOrphanTmux, killTmuxSessions, noopSignal, groupOverlap, landTarget, mergePreview, startLandResolve, listDocuments, verifyRun, openSessionAt, deleteSession, getScrollback, getRunPwd, getSessionChat } from './orchestrator';
|
|
24
|
+
import { launchRun, cleanupRun, stopRun, getRunDiff, loadRunDocs, mergeRun, getRunTermInfo, steerRun, exportRun, prRun, integrateRuns, planFanout, reviewTask, syncRun, openWorkbench, spawnSubtasks, listSubtasks, resolveAgentToken, taskCloseRisk, launchGroupTask, isRunLive, liveInPlaceRun, askGroupCoordinator, computeRunOutputs, normalizeOutputs, listReclaimableWorktrees, pruneWorktrees, worktreeDisk, listOrphanTmux, killTmuxSessions, noopSignal, groupOverlap, landTarget, mergePreview, startLandResolve, listDocuments, verifyRun, verifyBase, openSessionAt, deleteSession, getScrollback, getRunPwd, getSessionChat } from './orchestrator';
|
|
25
25
|
import { openTerm } from './term';
|
|
26
26
|
import { attach as agentAttach, feed as agentFeed, input as agentInput, onExit as agentExit, detach as agentDetach, allAgentStates, spottedPorts } from './agentstate';
|
|
27
27
|
import { scanListeners, scanPort, killPid, dropCache as dropListenerCache } from './procscan';
|
|
@@ -383,7 +383,8 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
383
383
|
machines: ms, repos: rs, tasks: ts, captures: dcs, groups: gs,
|
|
384
384
|
runs: rns.map((r) => {
|
|
385
385
|
const sig = noopSignal(r.status, r.filesChanged, r.exitSummary, taskOut.get(r.taskId) ?? '[]');
|
|
386
|
-
|
|
386
|
+
// real 은 언제나 불리언으로 나간다 — 클라이언트는 real===false 하나만 보고 dry 칩을 그린다.
|
|
387
|
+
return { ...r, real: !!r.real, events: (byRun.get(r.id) ?? []).slice(-EVENT_CAP), noop: sig.noop, noopReason: sig.reason };
|
|
387
388
|
}),
|
|
388
389
|
counts: { activeTasks: activeTasks.length, closedTasks: closedCount },
|
|
389
390
|
// 지금 터미널이 붙어 있는 run 의 에이전트 상태(runId → {state,detail,ts}).
|
|
@@ -500,7 +501,7 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
500
501
|
taskId: t.id, title: t.title, repoName: repoName.get(t.repoId) ?? '?',
|
|
501
502
|
groupTitle: t.groupId != null ? grpTitle.get(t.groupId) ?? null : null,
|
|
502
503
|
closedAt: t.closedAt ? Math.floor(t.closedAt.getTime() / 1000) : (t.createdAt ? Math.floor(t.createdAt.getTime() / 1000) : 0),
|
|
503
|
-
runs: rs.map((r) => ({ id: r.id, status: r.status, filesChanged: r.filesChanged, agent: r.agent, model: r.model })),
|
|
504
|
+
runs: rs.map((r) => ({ id: r.id, status: r.status, filesChanged: r.filesChanged, agent: r.agent, model: r.model, real: !!r.real })),
|
|
504
505
|
});
|
|
505
506
|
}
|
|
506
507
|
// status 필터가 있으면 total 은 근사(페이지 내 필터) — UI 는 rows 로만 판단하니 total0 유지.
|
|
@@ -958,7 +959,7 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
958
959
|
const tr = await db.select().from(tasks).where(eq(tasks.id, id)).limit(1);
|
|
959
960
|
if (!tr[0]) return reply.code(404).send({ error: 'not found' });
|
|
960
961
|
const runs = await db.select().from(agentRuns).where(eq(agentRuns.taskId, id));
|
|
961
|
-
return { task: tr[0], runs };
|
|
962
|
+
return { task: tr[0], runs: runs.map((r) => ({ ...r, real: !!r.real })) };
|
|
962
963
|
});
|
|
963
964
|
|
|
964
965
|
// 태스크 이름 변경(=세션 이름 변경) + v6.0 S2 승격(repoId 재부모화).
|
|
@@ -1089,16 +1090,19 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
1089
1090
|
});
|
|
1090
1091
|
}
|
|
1091
1092
|
}
|
|
1093
|
+
// dry/real 은 run 의 성질이다 — 만들 때부터 각인한다(v5.28 H1). body 가 말이 없으면
|
|
1094
|
+
// launchRun 이 쓰게 될 그 기본값(config.agent.real)을 그대로 쓴다: 행과 명령이 갈리면 안 된다.
|
|
1095
|
+
const useReal = b.real === undefined ? config.agent.real : !!b.real;
|
|
1092
1096
|
const created: Array<typeof agentRuns.$inferSelect> = [];
|
|
1093
1097
|
for (let i = 0; i < count; i++) {
|
|
1094
1098
|
const ins = await db.insert(agentRuns)
|
|
1095
|
-
.values({ taskId: id, machineId: rp[0].machineId, agent, model, title, inPlace, status: 'pending' })
|
|
1099
|
+
.values({ taskId: id, machineId: rp[0].machineId, agent, model, title, inPlace, status: 'pending', real: useReal })
|
|
1096
1100
|
.returning();
|
|
1097
1101
|
created.push(ins[0]!);
|
|
1098
1102
|
}
|
|
1099
1103
|
// 보드가 taskId 를 알도록 생성 브로드캐스트 후 백그라운드 시작.
|
|
1100
1104
|
for (const r of created) {
|
|
1101
|
-
broadcast({ type: 'run', runId: r.id, taskId: id, status: 'pending', agent, title, inPlace, branch: '', filesChanged: 0 });
|
|
1105
|
+
broadcast({ type: 'run', runId: r.id, taskId: id, status: 'pending', agent, title, inPlace, real: useReal, branch: '', filesChanged: 0 });
|
|
1102
1106
|
void launchRun(r.id, b.real);
|
|
1103
1107
|
}
|
|
1104
1108
|
return reply.code(202).send({ ok: true, runs: created.map((r) => ({ id: r.id, status: r.status })) });
|
|
@@ -1124,7 +1128,7 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
1124
1128
|
const runsOut = [];
|
|
1125
1129
|
for (const r of trs) {
|
|
1126
1130
|
const d = await getRunDiff(r.id);
|
|
1127
|
-
runsOut.push({ ...r, diff: d.ok ? d.diff : '', stat: d.ok ? d.stat : d.stat });
|
|
1131
|
+
runsOut.push({ ...r, real: !!r.real, diff: d.ok ? d.diff : '', stat: d.ok ? d.stat : d.stat });
|
|
1128
1132
|
}
|
|
1129
1133
|
return { task: tr[0], runs: runsOut };
|
|
1130
1134
|
});
|
|
@@ -1165,7 +1169,7 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
1165
1169
|
const rr = await db.select().from(agentRuns).where(eq(agentRuns.id, id)).limit(1);
|
|
1166
1170
|
if (!rr[0]) return reply.code(404).send({ error: 'not found' });
|
|
1167
1171
|
const events = await db.select().from(agentEvents).where(eq(agentEvents.runId, id));
|
|
1168
|
-
return { run: rr[0], events };
|
|
1172
|
+
return { run: { ...rr[0], real: !!rr[0].real }, events };
|
|
1169
1173
|
});
|
|
1170
1174
|
|
|
1171
1175
|
// v6.0 T4 — run 의 역할 이름 변경(탭 더블클릭). title 만 받는다.
|
|
@@ -1226,7 +1230,11 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
1226
1230
|
if (!rr[0]) return reply.code(404).send({ error: 'not found' });
|
|
1227
1231
|
const res = await mergeRun(id);
|
|
1228
1232
|
if (!res.ok) return reply.code(409).send(res);
|
|
1229
|
-
|
|
1233
|
+
// 내려앉은 그 호흡에 머지된 base 를 검증한다(v5.28 J1). verifyCmd 가 없으면 status ''(no-op),
|
|
1234
|
+
// 실패해도 머지는 되돌리지 않는다 — 보고만 한다.
|
|
1235
|
+
const tr = await db.select().from(tasks).where(eq(tasks.id, rr[0].taskId)).limit(1);
|
|
1236
|
+
const verify = tr[0] ? await verifyBase(tr[0].repoId) : { status: '', output: '' };
|
|
1237
|
+
return { ...res, verify };
|
|
1230
1238
|
});
|
|
1231
1239
|
|
|
1232
1240
|
// 후속 지시(steer) — 정착한 run 을 같은 세션(--resume)·같은 worktree 로 계속.
|
|
@@ -1445,6 +1453,7 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
1445
1453
|
const runs = g.rows.map(({ run, task }) => ({
|
|
1446
1454
|
runId: run.id, taskId: task.id, title: task.title, status: run.status,
|
|
1447
1455
|
agent: run.agent, model: run.model, branch: run.branch, filesChanged: run.filesChanged,
|
|
1456
|
+
real: !!run.real,
|
|
1448
1457
|
live: isRunLive(run.id), steerable: isSteerable(run),
|
|
1449
1458
|
// 수렴 콕핏 결정 행용: 태스크 닫힘 여부 + worktree 생존(터미널 가드·머지 가능성 판단).
|
|
1450
1459
|
taskStatus: task.status, hasWorktree: !!run.worktreePath,
|