omegon 0.8.3 → 0.9.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/extensions/cleave/dispatcher.ts +213 -20
- package/extensions/cleave/index.ts +24 -8
- package/extensions/cleave/rpc-child.ts +269 -0
- package/extensions/cleave/types.ts +52 -0
- package/extensions/openspec/spec.ts +3 -0
- package/node_modules/@types/node/README.md +3 -3
- package/node_modules/@types/node/assert/strict.d.ts +11 -5
- package/node_modules/@types/node/assert.d.ts +173 -50
- package/node_modules/@types/node/async_hooks.d.ts +8 -28
- package/node_modules/@types/node/buffer.buffer.d.ts +7 -1
- package/node_modules/@types/node/buffer.d.ts +168 -44
- package/node_modules/@types/node/child_process.d.ts +70 -27
- package/node_modules/@types/node/cluster.d.ts +332 -240
- package/node_modules/@types/node/compatibility/disposable.d.ts +14 -0
- package/node_modules/@types/node/compatibility/index.d.ts +9 -0
- package/node_modules/@types/node/compatibility/indexable.d.ts +20 -0
- package/node_modules/@types/node/compatibility/iterators.d.ts +0 -1
- package/node_modules/@types/node/console.d.ts +350 -49
- package/node_modules/@types/node/constants.d.ts +4 -3
- package/node_modules/@types/node/crypto.d.ts +1110 -630
- package/node_modules/@types/node/dgram.d.ts +51 -15
- package/node_modules/@types/node/diagnostics_channel.d.ts +6 -4
- package/node_modules/@types/node/dns/promises.d.ts +4 -4
- package/node_modules/@types/node/dns.d.ts +133 -132
- package/node_modules/@types/node/domain.d.ts +17 -13
- package/node_modules/@types/node/events.d.ts +663 -734
- package/node_modules/@types/node/fs/promises.d.ts +9 -43
- package/node_modules/@types/node/fs.d.ts +411 -628
- package/node_modules/@types/node/globals.d.ts +30 -8
- package/node_modules/@types/node/globals.typedarray.d.ts +0 -63
- package/node_modules/@types/node/http.d.ts +265 -364
- package/node_modules/@types/node/http2.d.ts +715 -551
- package/node_modules/@types/node/https.d.ts +239 -65
- package/node_modules/@types/node/index.d.ts +6 -24
- package/node_modules/@types/node/inspector.d.ts +53 -69
- package/node_modules/@types/node/inspector.generated.d.ts +410 -759
- package/node_modules/@types/node/module.d.ts +186 -52
- package/node_modules/@types/node/net.d.ts +194 -70
- package/node_modules/@types/node/os.d.ts +11 -12
- package/node_modules/@types/node/package.json +3 -13
- package/node_modules/@types/node/path.d.ts +133 -120
- package/node_modules/@types/node/perf_hooks.d.ts +643 -318
- package/node_modules/@types/node/process.d.ts +132 -223
- package/node_modules/@types/node/punycode.d.ts +5 -5
- package/node_modules/@types/node/querystring.d.ts +4 -4
- package/node_modules/@types/node/readline/promises.d.ts +3 -3
- package/node_modules/@types/node/readline.d.ts +120 -68
- package/node_modules/@types/node/repl.d.ts +100 -87
- package/node_modules/@types/node/sea.d.ts +1 -10
- package/node_modules/@types/node/sqlite.d.ts +19 -363
- package/node_modules/@types/node/stream/consumers.d.ts +10 -10
- package/node_modules/@types/node/stream/promises.d.ts +15 -136
- package/node_modules/@types/node/stream/web.d.ts +502 -176
- package/node_modules/@types/node/stream.d.ts +475 -581
- package/node_modules/@types/node/string_decoder.d.ts +4 -4
- package/node_modules/@types/node/test.d.ts +196 -308
- package/node_modules/@types/node/timers/promises.d.ts +4 -4
- package/node_modules/@types/node/timers.d.ts +132 -4
- package/node_modules/@types/node/tls.d.ts +226 -110
- package/node_modules/@types/node/trace_events.d.ts +9 -9
- package/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +7 -1
- package/node_modules/@types/node/ts5.6/globals.typedarray.d.ts +0 -2
- package/node_modules/@types/node/ts5.6/index.d.ts +6 -26
- package/node_modules/@types/node/tty.d.ts +16 -58
- package/node_modules/@types/node/url.d.ts +573 -130
- package/node_modules/@types/node/util.d.ts +1100 -181
- package/node_modules/@types/node/v8.d.ts +8 -76
- package/node_modules/@types/node/vm.d.ts +72 -280
- package/node_modules/@types/node/wasi.d.ts +4 -25
- package/node_modules/@types/node/web-globals/abortcontroller.d.ts +2 -27
- package/node_modules/@types/node/web-globals/events.d.ts +0 -9
- package/node_modules/@types/node/web-globals/fetch.d.ts +0 -14
- package/node_modules/@types/node/web-globals/navigator.d.ts +0 -3
- package/node_modules/@types/node/worker_threads.d.ts +335 -268
- package/node_modules/@types/node/zlib.d.ts +74 -9
- package/node_modules/undici-types/agent.d.ts +12 -13
- package/node_modules/undici-types/api.d.ts +26 -26
- package/node_modules/undici-types/balanced-pool.d.ts +12 -13
- package/node_modules/undici-types/client.d.ts +19 -19
- package/node_modules/undici-types/connector.d.ts +2 -2
- package/node_modules/undici-types/cookies.d.ts +0 -2
- package/node_modules/undici-types/diagnostics-channel.d.ts +10 -18
- package/node_modules/undici-types/dispatcher.d.ts +103 -123
- package/node_modules/undici-types/env-http-proxy-agent.d.ts +3 -4
- package/node_modules/undici-types/errors.d.ts +54 -66
- package/node_modules/undici-types/eventsource.d.ts +4 -9
- package/node_modules/undici-types/fetch.d.ts +20 -22
- package/node_modules/undici-types/file.d.ts +39 -0
- package/node_modules/undici-types/filereader.d.ts +54 -0
- package/node_modules/undici-types/formdata.d.ts +7 -7
- package/node_modules/undici-types/global-dispatcher.d.ts +4 -4
- package/node_modules/undici-types/global-origin.d.ts +5 -5
- package/node_modules/undici-types/handlers.d.ts +8 -8
- package/node_modules/undici-types/header.d.ts +1 -157
- package/node_modules/undici-types/index.d.ts +47 -64
- package/node_modules/undici-types/interceptors.d.ts +8 -64
- package/node_modules/undici-types/mock-agent.d.ts +18 -36
- package/node_modules/undici-types/mock-client.d.ts +4 -6
- package/node_modules/undici-types/mock-errors.d.ts +3 -3
- package/node_modules/undici-types/mock-interceptor.d.ts +20 -21
- package/node_modules/undici-types/mock-pool.d.ts +4 -6
- package/node_modules/undici-types/package.json +1 -1
- package/node_modules/undici-types/patch.d.ts +4 -0
- package/node_modules/undici-types/pool-stats.d.ts +8 -8
- package/node_modules/undici-types/pool.d.ts +13 -15
- package/node_modules/undici-types/proxy-agent.d.ts +4 -5
- package/node_modules/undici-types/readable.d.ts +16 -19
- package/node_modules/undici-types/retry-agent.d.ts +1 -1
- package/node_modules/undici-types/retry-handler.d.ts +10 -19
- package/node_modules/undici-types/util.d.ts +3 -3
- package/node_modules/undici-types/webidl.d.ts +29 -142
- package/node_modules/undici-types/websocket.d.ts +10 -46
- package/package.json +2 -1
- package/skills/cleave/SKILL.md +62 -2
- package/node_modules/@types/node/inspector/promises.d.ts +0 -41
- package/node_modules/@types/node/path/posix.d.ts +0 -8
- package/node_modules/@types/node/path/win32.d.ts +0 -8
- package/node_modules/@types/node/quic.d.ts +0 -910
- package/node_modules/@types/node/test/reporters.d.ts +0 -96
- package/node_modules/@types/node/ts5.6/compatibility/float16array.d.ts +0 -71
- package/node_modules/@types/node/ts5.7/compatibility/float16array.d.ts +0 -72
- package/node_modules/@types/node/ts5.7/index.d.ts +0 -117
- package/node_modules/@types/node/util/types.d.ts +0 -558
- package/node_modules/@types/node/web-globals/blob.d.ts +0 -23
- package/node_modules/@types/node/web-globals/console.d.ts +0 -9
- package/node_modules/@types/node/web-globals/crypto.d.ts +0 -39
- package/node_modules/@types/node/web-globals/encoding.d.ts +0 -11
- package/node_modules/@types/node/web-globals/importmeta.d.ts +0 -13
- package/node_modules/@types/node/web-globals/messaging.d.ts +0 -23
- package/node_modules/@types/node/web-globals/performance.d.ts +0 -45
- package/node_modules/@types/node/web-globals/streams.d.ts +0 -115
- package/node_modules/@types/node/web-globals/timers.d.ts +0 -44
- package/node_modules/@types/node/web-globals/url.d.ts +0 -24
- package/node_modules/undici-types/cache-interceptor.d.ts +0 -173
- package/node_modules/undici-types/client-stats.d.ts +0 -15
- package/node_modules/undici-types/h2c-client.d.ts +0 -73
- package/node_modules/undici-types/mock-call-history.d.ts +0 -111
- package/node_modules/undici-types/round-robin-pool.d.ts +0 -41
- package/node_modules/undici-types/snapshot-agent.d.ts +0 -109
- package/node_modules/undici-types/utility.d.ts +0 -7
|
@@ -21,8 +21,9 @@ import { readFileSync } from "node:fs";
|
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import type { ExtensionAPI } from "@styrene-lab/pi-coding-agent";
|
|
23
23
|
import { DASHBOARD_UPDATE_EVENT, sharedState } from "../lib/shared-state.ts";
|
|
24
|
-
import type { ChildState, CleaveState, ModelTier } from "./types.ts";
|
|
24
|
+
import type { ChildState, CleaveState, ModelTier, RpcChildEvent, RpcProgressUpdate } from "./types.ts";
|
|
25
25
|
import { computeDispatchWaves } from "./planner.ts";
|
|
26
|
+
import { sendRpcCommand, buildPromptCommand, parseRpcEventStream, mapEventToProgress } from "./rpc-child.ts";
|
|
26
27
|
import { executeWithReview, type ReviewConfig, type ReviewExecutor, DEFAULT_REVIEW_CONFIG } from "./review.ts";
|
|
27
28
|
import { saveState } from "./workspace.ts";
|
|
28
29
|
import { resolveTier, getDefaultPolicy, getViableModels, type ProviderRoutingPolicy, type RegistryModel } from "../lib/model-routing.ts";
|
|
@@ -83,7 +84,7 @@ export function resolveModelIdForTier(
|
|
|
83
84
|
export function emitCleaveChildProgress(
|
|
84
85
|
pi: Pick<ExtensionAPI, "events">,
|
|
85
86
|
childId: number,
|
|
86
|
-
patch: { status?: "pending" | "running" | "done" | "failed"; elapsed?: number; startedAt?: number; lastLine?: string; worktreePath?: string },
|
|
87
|
+
patch: { status?: "pending" | "running" | "done" | "failed"; elapsed?: number; startedAt?: number; lastLine?: string; worktreePath?: string; rpcProgress?: RpcProgressUpdate },
|
|
87
88
|
): void {
|
|
88
89
|
const cleaveState = (sharedState as any).cleave;
|
|
89
90
|
if (!cleaveState?.children?.[childId]) return;
|
|
@@ -99,8 +100,16 @@ export function emitCleaveChildProgress(
|
|
|
99
100
|
if (patch.worktreePath !== undefined) {
|
|
100
101
|
cleaveState.children[childId].worktreePath = patch.worktreePath;
|
|
101
102
|
}
|
|
102
|
-
if (patch.
|
|
103
|
-
//
|
|
103
|
+
if (patch.rpcProgress !== undefined) {
|
|
104
|
+
// Structured RPC progress — use summary as lastLine for backward compat
|
|
105
|
+
const summary = patch.rpcProgress.summary;
|
|
106
|
+
cleaveState.children[childId].lastLine = summary;
|
|
107
|
+
const child = cleaveState.children[childId];
|
|
108
|
+
if (!child.recentLines) child.recentLines = [];
|
|
109
|
+
child.recentLines.push(summary);
|
|
110
|
+
if (child.recentLines.length > 30) child.recentLines.splice(0, child.recentLines.length - 30);
|
|
111
|
+
} else if (patch.lastLine !== undefined) {
|
|
112
|
+
// Update lastLine for backward compat (pipe mode)
|
|
104
113
|
cleaveState.children[childId].lastLine = patch.lastLine;
|
|
105
114
|
// Append to ring buffer (cap at 30)
|
|
106
115
|
const child = cleaveState.children[childId];
|
|
@@ -364,7 +373,11 @@ function stripAnsiForStatus(s: string): string {
|
|
|
364
373
|
return s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").trim();
|
|
365
374
|
}
|
|
366
375
|
|
|
367
|
-
|
|
376
|
+
/**
|
|
377
|
+
* Spawn a child in pipe mode (legacy).
|
|
378
|
+
* Uses `pi -p --no-session`, writes prompt to stdin, closes stdin.
|
|
379
|
+
*/
|
|
380
|
+
async function spawnChildPipe(
|
|
368
381
|
prompt: string,
|
|
369
382
|
cwd: string,
|
|
370
383
|
timeoutMs: number,
|
|
@@ -389,15 +402,13 @@ async function spawnChild(
|
|
|
389
402
|
detached: true,
|
|
390
403
|
env: {
|
|
391
404
|
...process.env,
|
|
392
|
-
// Prevent nested detection issues
|
|
393
405
|
PI_CHILD: "1",
|
|
394
|
-
// https://warhammer40k.fandom.com/wiki/Alpha_Legion
|
|
395
406
|
I_AM: "alpharius",
|
|
396
407
|
},
|
|
397
408
|
});
|
|
398
409
|
registerCleaveProc(proc);
|
|
399
410
|
|
|
400
|
-
// Write prompt to stdin
|
|
411
|
+
// Write prompt to stdin and close (pipe mode)
|
|
401
412
|
if (proc.stdin) {
|
|
402
413
|
proc.stdin.write(prompt);
|
|
403
414
|
proc.stdin.end();
|
|
@@ -408,7 +419,6 @@ async function spawnChild(
|
|
|
408
419
|
const chunk = data.toString();
|
|
409
420
|
stdout += chunk;
|
|
410
421
|
if (onLine) {
|
|
411
|
-
// Parse line by line and forward meaningful lines
|
|
412
422
|
lineBuf += chunk;
|
|
413
423
|
const parts = lineBuf.split("\n");
|
|
414
424
|
lineBuf = parts.pop() ?? "";
|
|
@@ -420,7 +430,6 @@ async function spawnChild(
|
|
|
420
430
|
});
|
|
421
431
|
proc.stderr?.on("data", (data) => { stderr += data.toString(); });
|
|
422
432
|
|
|
423
|
-
// SIGKILL escalation helper — sends SIGKILL by process group with fallback
|
|
424
433
|
let escalationTimer: ReturnType<typeof setTimeout> | undefined;
|
|
425
434
|
const scheduleEscalation = () => {
|
|
426
435
|
escalationTimer = setTimeout(() => {
|
|
@@ -434,15 +443,12 @@ async function spawnChild(
|
|
|
434
443
|
}, 5_000);
|
|
435
444
|
};
|
|
436
445
|
|
|
437
|
-
// Timeout enforcement
|
|
438
446
|
const timer = setTimeout(() => {
|
|
439
447
|
killed = true;
|
|
440
448
|
killCleaveProc(proc);
|
|
441
449
|
scheduleEscalation();
|
|
442
450
|
}, timeoutMs);
|
|
443
451
|
|
|
444
|
-
// Abort signal support (with SIGKILL escalation — detached processes
|
|
445
|
-
// won't receive SIGHUP on parent exit, so SIGTERM alone is insufficient)
|
|
446
452
|
const onAbort = () => {
|
|
447
453
|
killed = true;
|
|
448
454
|
killCleaveProc(proc);
|
|
@@ -481,6 +487,168 @@ async function spawnChild(
|
|
|
481
487
|
});
|
|
482
488
|
}
|
|
483
489
|
|
|
490
|
+
/** Events collected during an RPC child session. */
|
|
491
|
+
interface RpcChildResult extends ChildResult {
|
|
492
|
+
events: RpcChildEvent[];
|
|
493
|
+
pipeBroken: boolean;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Spawn a child in RPC mode.
|
|
498
|
+
* Uses `--mode rpc --no-session`, sends prompt via sendRpcCommand on stdin,
|
|
499
|
+
* parses stdout as a JSON event stream. Stdin stays open for the session lifetime.
|
|
500
|
+
*/
|
|
501
|
+
async function spawnChildRpc(
|
|
502
|
+
prompt: string,
|
|
503
|
+
cwd: string,
|
|
504
|
+
timeoutMs: number,
|
|
505
|
+
signal?: AbortSignal,
|
|
506
|
+
localModel?: string,
|
|
507
|
+
onEvent?: (event: RpcChildEvent) => void,
|
|
508
|
+
): Promise<RpcChildResult> {
|
|
509
|
+
const omegon = resolveOmegonSubprocess();
|
|
510
|
+
const args = [...omegon.argvPrefix, "--mode", "rpc", "--no-session"];
|
|
511
|
+
if (localModel) {
|
|
512
|
+
args.push("--model", localModel);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return new Promise<RpcChildResult>((resolve) => {
|
|
516
|
+
let stderr = "";
|
|
517
|
+
let killed = false;
|
|
518
|
+
const events: RpcChildEvent[] = [];
|
|
519
|
+
let pipeBroken = false;
|
|
520
|
+
|
|
521
|
+
const proc = spawn(omegon.command, args, {
|
|
522
|
+
cwd,
|
|
523
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
524
|
+
detached: true,
|
|
525
|
+
env: {
|
|
526
|
+
...process.env,
|
|
527
|
+
PI_CHILD: "1",
|
|
528
|
+
I_AM: "alpharius",
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
registerCleaveProc(proc);
|
|
532
|
+
|
|
533
|
+
// Send prompt via RPC command on stdin — keep stdin open
|
|
534
|
+
if (proc.stdin) {
|
|
535
|
+
const cmd = buildPromptCommand(prompt);
|
|
536
|
+
sendRpcCommand(proc.stdin, cmd);
|
|
537
|
+
// Do NOT close stdin — child may need it for the session lifetime
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Collect stderr
|
|
541
|
+
proc.stderr?.on("data", (data) => { stderr += data.toString(); });
|
|
542
|
+
|
|
543
|
+
// Parse stdout exclusively via RPC event stream (no competing data listener)
|
|
544
|
+
let eventsFinished: Promise<void> = Promise.resolve();
|
|
545
|
+
if (proc.stdout) {
|
|
546
|
+
eventsFinished = (async () => {
|
|
547
|
+
try {
|
|
548
|
+
for await (const event of parseRpcEventStream(proc.stdout!)) {
|
|
549
|
+
events.push(event);
|
|
550
|
+
if (event.type === "pipe_closed") {
|
|
551
|
+
pipeBroken = true;
|
|
552
|
+
}
|
|
553
|
+
onEvent?.(event);
|
|
554
|
+
}
|
|
555
|
+
} catch {
|
|
556
|
+
// Stream parsing error — treat as pipe break
|
|
557
|
+
pipeBroken = true;
|
|
558
|
+
}
|
|
559
|
+
})();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
let escalationTimer: ReturnType<typeof setTimeout> | undefined;
|
|
563
|
+
const scheduleEscalation = () => {
|
|
564
|
+
escalationTimer = setTimeout(() => {
|
|
565
|
+
if (!proc.killed) {
|
|
566
|
+
try {
|
|
567
|
+
if (proc.pid) process.kill(-proc.pid, "SIGKILL");
|
|
568
|
+
} catch {
|
|
569
|
+
try { proc.kill("SIGKILL"); } catch { /* already dead */ }
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}, 5_000);
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const timer = setTimeout(() => {
|
|
576
|
+
killed = true;
|
|
577
|
+
killCleaveProc(proc);
|
|
578
|
+
scheduleEscalation();
|
|
579
|
+
}, timeoutMs);
|
|
580
|
+
|
|
581
|
+
const onAbort = () => {
|
|
582
|
+
killed = true;
|
|
583
|
+
killCleaveProc(proc);
|
|
584
|
+
scheduleEscalation();
|
|
585
|
+
};
|
|
586
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
587
|
+
|
|
588
|
+
let settled = false;
|
|
589
|
+
proc.on("close", async (code) => {
|
|
590
|
+
if (settled) return;
|
|
591
|
+
settled = true;
|
|
592
|
+
deregisterCleaveProc(proc);
|
|
593
|
+
clearTimeout(timer);
|
|
594
|
+
clearTimeout(escalationTimer);
|
|
595
|
+
signal?.removeEventListener("abort", onAbort);
|
|
596
|
+
|
|
597
|
+
// Close stdin if still open (child has exited)
|
|
598
|
+
try { proc.stdin?.end(); } catch { /* already closed */ }
|
|
599
|
+
|
|
600
|
+
// Wait for all RPC events to be consumed before resolving
|
|
601
|
+
await eventsFinished;
|
|
602
|
+
|
|
603
|
+
resolve({
|
|
604
|
+
exitCode: killed ? -1 : (code ?? 1),
|
|
605
|
+
stdout: "",
|
|
606
|
+
stderr: killed ? `Killed (timeout or abort)\n${stderr}` : stderr,
|
|
607
|
+
events,
|
|
608
|
+
pipeBroken,
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
proc.on("error", (err) => {
|
|
613
|
+
if (settled) return;
|
|
614
|
+
settled = true;
|
|
615
|
+
deregisterCleaveProc(proc);
|
|
616
|
+
clearTimeout(timer);
|
|
617
|
+
clearTimeout(escalationTimer);
|
|
618
|
+
signal?.removeEventListener("abort", onAbort);
|
|
619
|
+
resolve({
|
|
620
|
+
exitCode: 1,
|
|
621
|
+
stdout: "",
|
|
622
|
+
stderr: `Failed to spawn pi: ${err.message}`,
|
|
623
|
+
events,
|
|
624
|
+
pipeBroken: true,
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Spawn a child process — dispatches to RPC or pipe mode.
|
|
632
|
+
*
|
|
633
|
+
* @param useRpc When true (default), uses RPC mode with structured events.
|
|
634
|
+
* When false, uses legacy pipe mode.
|
|
635
|
+
*/
|
|
636
|
+
async function spawnChild(
|
|
637
|
+
prompt: string,
|
|
638
|
+
cwd: string,
|
|
639
|
+
timeoutMs: number,
|
|
640
|
+
signal?: AbortSignal,
|
|
641
|
+
localModel?: string,
|
|
642
|
+
onLine?: (line: string) => void,
|
|
643
|
+
useRpc?: boolean,
|
|
644
|
+
onEvent?: (event: RpcChildEvent) => void,
|
|
645
|
+
): Promise<ChildResult> {
|
|
646
|
+
if (useRpc) {
|
|
647
|
+
return spawnChildRpc(prompt, cwd, timeoutMs, signal, localModel, onEvent);
|
|
648
|
+
}
|
|
649
|
+
return spawnChildPipe(prompt, cwd, timeoutMs, signal, localModel, onLine);
|
|
650
|
+
}
|
|
651
|
+
|
|
484
652
|
// ─── Concurrency control ────────────────────────────────────────────────────
|
|
485
653
|
|
|
486
654
|
/**
|
|
@@ -674,8 +842,20 @@ async function dispatchSingleChild(
|
|
|
674
842
|
// Mirror to sharedState for live dashboard updates (include startedAt for elapsed ticker)
|
|
675
843
|
emitCleaveChildProgress(pi, child.childId, { status: "running", startedAt: startedAtMs, worktreePath: child.worktreePath });
|
|
676
844
|
|
|
677
|
-
//
|
|
678
|
-
//
|
|
845
|
+
// ── Progress callbacks ──────────────────────────────────────────────────
|
|
846
|
+
// RPC mode: direct event forwarding (no debounce)
|
|
847
|
+
// Pipe mode: debounced line emitter (legacy)
|
|
848
|
+
const useRpc = true;
|
|
849
|
+
|
|
850
|
+
// RPC event handler — forward structured progress directly
|
|
851
|
+
const onRpcEvent = (event: RpcChildEvent) => {
|
|
852
|
+
const progress = mapEventToProgress(event);
|
|
853
|
+
if (progress) {
|
|
854
|
+
emitCleaveChildProgress(pi, child.childId, { rpcProgress: progress });
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
// Pipe mode fallback: debounced last-line emitter
|
|
679
859
|
let pendingLine: string | undefined;
|
|
680
860
|
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
681
861
|
const flushLine = () => {
|
|
@@ -738,14 +918,15 @@ async function dispatchSingleChild(
|
|
|
738
918
|
// Build executor adapter for the review loop
|
|
739
919
|
const executor: ReviewExecutor = {
|
|
740
920
|
execute: async (execPrompt: string, execCwd: string, execModelFlag?: string) => {
|
|
741
|
-
|
|
921
|
+
// Execution uses RPC mode for structured events
|
|
922
|
+
return spawnChild(execPrompt, execCwd, timeoutMs, signal, execModelFlag,
|
|
923
|
+
useRpc ? undefined : onChildLine, useRpc, useRpc ? onRpcEvent : undefined);
|
|
742
924
|
},
|
|
743
925
|
review: async (reviewPrompt: string, reviewCwd: string) => {
|
|
744
|
-
// Reviews always use
|
|
926
|
+
// Reviews always use pipe mode (Phase 1) + gloriana tier
|
|
745
927
|
const reviewModelId = resolveModelIdForTier("gloriana", registryModels, activePolicy, localModel);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
return spawnChild(reviewPrompt, reviewCwd, timeoutMs, signal, reviewModelId);
|
|
928
|
+
return spawnChild(reviewPrompt, reviewCwd, timeoutMs, signal, reviewModelId,
|
|
929
|
+
undefined, false /* pipe mode for review */);
|
|
749
930
|
},
|
|
750
931
|
readFile: (path: string) => readFileSync(path, "utf-8"),
|
|
751
932
|
};
|
|
@@ -799,6 +980,18 @@ async function dispatchSingleChild(
|
|
|
799
980
|
child.error = result.stderr.slice(0, 2000) || `Exit code ${result.exitCode}`;
|
|
800
981
|
}
|
|
801
982
|
|
|
983
|
+
// RPC pipe-break handling: if stdout closed unexpectedly, mark failed
|
|
984
|
+
// but preserve worktree and branch for recovery
|
|
985
|
+
if (useRpc && "pipeBroken" in result && (result as RpcChildResult).pipeBroken) {
|
|
986
|
+
// Only override status if it wasn't already set to completed (child may
|
|
987
|
+
// have finished before the pipe break was detected)
|
|
988
|
+
if (child.status !== "completed") {
|
|
989
|
+
child.status = "failed";
|
|
990
|
+
child.error = "RPC pipe break: stdout closed unexpectedly — worktree preserved for recovery";
|
|
991
|
+
// Do NOT clean up worktree — preserve for manual recovery
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
802
995
|
// If review escalated, mark the child as failed
|
|
803
996
|
if (reviewResult.finalDecision === "escalated") {
|
|
804
997
|
child.status = "failed";
|
|
@@ -352,7 +352,7 @@ async function checkpointRelatedChanges(
|
|
|
352
352
|
* Prevents infinite loops when the agent repeatedly picks actions
|
|
353
353
|
* that don't resolve the dirty tree.
|
|
354
354
|
*/
|
|
355
|
-
const MAX_PREFLIGHT_ATTEMPTS = 3;
|
|
355
|
+
export const MAX_PREFLIGHT_ATTEMPTS = 3;
|
|
356
356
|
|
|
357
357
|
/**
|
|
358
358
|
* Verify the tree is clean after an action. If only volatile files remain,
|
|
@@ -435,12 +435,23 @@ export async function runDirtyTreePreflight(pi: ExtensionAPI, options: DirtyTree
|
|
|
435
435
|
throw new Error(summary + "\n\nInteractive input is unavailable, so cleave cannot resolve the dirty tree automatically.");
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
-
// Mutable classification — refreshed after each action
|
|
438
|
+
// Mutable classification — refreshed after each resolution action.
|
|
439
439
|
let currentClassification = classification;
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
440
|
+
// Only resolution actions (checkpoint, stash) increment this counter.
|
|
441
|
+
// Invalid input, empty guards, cancel, and proceed-without-cleave do NOT
|
|
442
|
+
// consume attempts — they are navigational, not resolution attempts.
|
|
443
|
+
let resolutionAttempts = 0;
|
|
444
|
+
|
|
445
|
+
// Outer safety cap: total loop iterations including non-resolution turns.
|
|
446
|
+
// Prevents truly pathological loops (e.g. select always returning garbage).
|
|
447
|
+
const MAX_TOTAL_ITERATIONS = MAX_PREFLIGHT_ATTEMPTS * 3;
|
|
448
|
+
let totalIterations = 0;
|
|
449
|
+
|
|
450
|
+
while (resolutionAttempts < MAX_PREFLIGHT_ATTEMPTS) {
|
|
451
|
+
totalIterations++;
|
|
452
|
+
if (totalIterations > MAX_TOTAL_ITERATIONS) {
|
|
453
|
+
break; // Fall through to the exhaustion error below
|
|
454
|
+
}
|
|
444
455
|
|
|
445
456
|
let answer: string | undefined;
|
|
446
457
|
if (hasSelect) {
|
|
@@ -458,6 +469,7 @@ export async function runDirtyTreePreflight(pi: ExtensionAPI, options: DirtyTree
|
|
|
458
469
|
try {
|
|
459
470
|
switch (answer) {
|
|
460
471
|
case "checkpoint": {
|
|
472
|
+
resolutionAttempts++;
|
|
461
473
|
const currentCheckpointPlan = buildCheckpointPlan(currentClassification, { changeName, openspecContext });
|
|
462
474
|
await checkpointRelatedChanges(pi, options.repoPath, currentClassification, currentCheckpointPlan.message, options.ui);
|
|
463
475
|
|
|
@@ -493,6 +505,7 @@ export async function runDirtyTreePreflight(pi: ExtensionAPI, options: DirtyTree
|
|
|
493
505
|
});
|
|
494
506
|
break;
|
|
495
507
|
}
|
|
508
|
+
resolutionAttempts++;
|
|
496
509
|
await stashPaths(pi, options.repoPath, "cleave-preflight-unrelated", toStash);
|
|
497
510
|
const { clean, classification: postClassification } = await verifyCleanAfterAction(
|
|
498
511
|
pi, options.repoPath, changeName, openspecContext, options.onUpdate,
|
|
@@ -509,6 +522,7 @@ export async function runDirtyTreePreflight(pi: ExtensionAPI, options: DirtyTree
|
|
|
509
522
|
});
|
|
510
523
|
break;
|
|
511
524
|
}
|
|
525
|
+
resolutionAttempts++;
|
|
512
526
|
await stashPaths(pi, options.repoPath, "cleave-preflight-volatile", currentClassification.volatile);
|
|
513
527
|
const { clean, classification: postClassification } = await verifyCleanAfterAction(
|
|
514
528
|
pi, options.repoPath, changeName, openspecContext, options.onUpdate,
|
|
@@ -529,6 +543,8 @@ export async function runDirtyTreePreflight(pi: ExtensionAPI, options: DirtyTree
|
|
|
529
543
|
});
|
|
530
544
|
}
|
|
531
545
|
} catch (error) {
|
|
546
|
+
// Resolution action threw (e.g. git commit failed) — still counts
|
|
547
|
+
// as a resolution attempt since work was attempted.
|
|
532
548
|
const message = error instanceof Error ? error.message : String(error);
|
|
533
549
|
options.onUpdate?.({
|
|
534
550
|
content: [{ type: "text", text: `Preflight action failed: ${message}` }],
|
|
@@ -537,7 +553,7 @@ export async function runDirtyTreePreflight(pi: ExtensionAPI, options: DirtyTree
|
|
|
537
553
|
}
|
|
538
554
|
}
|
|
539
555
|
|
|
540
|
-
// Exhausted attempts — report remaining dirty files and bail.
|
|
556
|
+
// Exhausted resolution attempts — report remaining dirty files and bail.
|
|
541
557
|
const remaining = [
|
|
542
558
|
...currentClassification.related,
|
|
543
559
|
...currentClassification.unrelated,
|
|
@@ -545,7 +561,7 @@ export async function runDirtyTreePreflight(pi: ExtensionAPI, options: DirtyTree
|
|
|
545
561
|
...currentClassification.volatile,
|
|
546
562
|
];
|
|
547
563
|
throw new Error(
|
|
548
|
-
`Dirty tree not resolved after ${
|
|
564
|
+
`Dirty tree not resolved after ${resolutionAttempts} resolution attempt(s). Remaining files:\n` +
|
|
549
565
|
remaining.map((f) => ` • ${f.path}`).join("\n") +
|
|
550
566
|
"\n\nResolve manually (git commit/stash/checkout) and retry /cleave.",
|
|
551
567
|
);
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cleave/rpc-child — RPC child communication module.
|
|
3
|
+
*
|
|
4
|
+
* Provides JSON line framing for stdin commands, stdout event stream parsing,
|
|
5
|
+
* event-to-progress mapping, and pipe-break handling for cleave child processes
|
|
6
|
+
* running in `--mode rpc`.
|
|
7
|
+
*
|
|
8
|
+
* This module is a building block for the dispatcher to use when spawning
|
|
9
|
+
* children in RPC mode instead of pipe mode.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Writable, Readable } from "node:stream";
|
|
13
|
+
import { StringDecoder } from "node:string_decoder";
|
|
14
|
+
import type { RpcChildEvent, RpcProgressUpdate } from "./types.ts";
|
|
15
|
+
|
|
16
|
+
// ─── JSON Line Framing (stdin commands) ─────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Serialize and write a JSON command to a child's stdin.
|
|
20
|
+
* Uses strict LF-only JSONL framing (matching pi-mono's serializeJsonLine).
|
|
21
|
+
*
|
|
22
|
+
* @returns true if the write succeeded, false if stdin is not writable
|
|
23
|
+
*/
|
|
24
|
+
export function sendRpcCommand(
|
|
25
|
+
stdin: Writable,
|
|
26
|
+
command: Record<string, unknown>,
|
|
27
|
+
): boolean {
|
|
28
|
+
if (!stdin.writable) return false;
|
|
29
|
+
try {
|
|
30
|
+
stdin.write(`${JSON.stringify(command)}\n`);
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build a prompt command for a cleave child task.
|
|
39
|
+
*/
|
|
40
|
+
export function buildPromptCommand(
|
|
41
|
+
message: string,
|
|
42
|
+
id?: string,
|
|
43
|
+
): Record<string, unknown> {
|
|
44
|
+
const cmd: Record<string, unknown> = { type: "prompt", message };
|
|
45
|
+
if (id !== undefined) cmd.id = id;
|
|
46
|
+
return cmd;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build an abort command.
|
|
51
|
+
*/
|
|
52
|
+
export function buildAbortCommand(id?: string): Record<string, unknown> {
|
|
53
|
+
const cmd: Record<string, unknown> = { type: "abort" };
|
|
54
|
+
if (id !== undefined) cmd.id = id;
|
|
55
|
+
return cmd;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Stdout Event Stream Parser ─────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse an RPC event stream from a child's stdout as an async iterator.
|
|
62
|
+
*
|
|
63
|
+
* Yields typed RpcChildEvent objects for each valid JSON line received.
|
|
64
|
+
* Non-JSON lines are silently skipped (child may emit debug output to stdout).
|
|
65
|
+
*
|
|
66
|
+
* When stdout closes (end event), the iterator emits a synthetic
|
|
67
|
+
* `{ type: "pipe_closed" }` event and completes — it does NOT throw.
|
|
68
|
+
* This enables graceful degradation: the caller decides how to handle
|
|
69
|
+
* pipe breaks vs normal completion.
|
|
70
|
+
*/
|
|
71
|
+
export async function* parseRpcEventStream(
|
|
72
|
+
stdout: Readable,
|
|
73
|
+
): AsyncGenerator<RpcChildEvent, void, undefined> {
|
|
74
|
+
// We implement a manual async iteration over the stream data,
|
|
75
|
+
// using LF-only splitting (matching pi-mono's jsonl.ts approach).
|
|
76
|
+
const decoder = new StringDecoder("utf8");
|
|
77
|
+
let buffer = "";
|
|
78
|
+
let done = false;
|
|
79
|
+
|
|
80
|
+
// Queue for parsed events, with a resolver for the consumer
|
|
81
|
+
const queue: RpcChildEvent[] = [];
|
|
82
|
+
let waitResolve: (() => void) | null = null;
|
|
83
|
+
|
|
84
|
+
function enqueue(event: RpcChildEvent) {
|
|
85
|
+
queue.push(event);
|
|
86
|
+
if (waitResolve) {
|
|
87
|
+
const r = waitResolve;
|
|
88
|
+
waitResolve = null;
|
|
89
|
+
r();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function processBuffer() {
|
|
94
|
+
while (true) {
|
|
95
|
+
const idx = buffer.indexOf("\n");
|
|
96
|
+
if (idx === -1) break;
|
|
97
|
+
const line = buffer.slice(0, idx);
|
|
98
|
+
buffer = buffer.slice(idx + 1);
|
|
99
|
+
// Strip optional CR
|
|
100
|
+
const clean = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
101
|
+
if (clean.length === 0) continue;
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(clean);
|
|
104
|
+
if (parsed && typeof parsed === "object" && typeof parsed.type === "string") {
|
|
105
|
+
enqueue(parsed as RpcChildEvent);
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// Non-JSON line — skip silently
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const onData = (chunk: Buffer | string) => {
|
|
114
|
+
buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
|
|
115
|
+
processBuffer();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const onEnd = () => {
|
|
119
|
+
buffer += decoder.end();
|
|
120
|
+
processBuffer();
|
|
121
|
+
// Emit synthetic pipe_closed event
|
|
122
|
+
enqueue({ type: "pipe_closed" });
|
|
123
|
+
done = true;
|
|
124
|
+
// Wake up consumer if waiting
|
|
125
|
+
if (waitResolve) {
|
|
126
|
+
const r = waitResolve;
|
|
127
|
+
waitResolve = null;
|
|
128
|
+
r();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const onError = (_err: Error) => {
|
|
133
|
+
enqueue({ type: "pipe_closed" });
|
|
134
|
+
done = true;
|
|
135
|
+
if (waitResolve) {
|
|
136
|
+
const r = waitResolve;
|
|
137
|
+
waitResolve = null;
|
|
138
|
+
r();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
stdout.on("data", onData);
|
|
143
|
+
stdout.on("end", onEnd);
|
|
144
|
+
stdout.on("error", onError);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
while (true) {
|
|
148
|
+
if (queue.length > 0) {
|
|
149
|
+
const event = queue.shift()!;
|
|
150
|
+
yield event;
|
|
151
|
+
if (event.type === "pipe_closed") return;
|
|
152
|
+
} else if (done) {
|
|
153
|
+
return;
|
|
154
|
+
} else {
|
|
155
|
+
// Wait for next event
|
|
156
|
+
await new Promise<void>((resolve) => {
|
|
157
|
+
waitResolve = resolve;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
stdout.off("data", onData);
|
|
163
|
+
stdout.off("end", onEnd);
|
|
164
|
+
stdout.off("error", onError);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Event-to-Progress Mapping ──────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Map an RpcChildEvent to a structured progress update for the dashboard.
|
|
172
|
+
*
|
|
173
|
+
* Returns null for events that don't produce meaningful progress (e.g. turn_start).
|
|
174
|
+
*/
|
|
175
|
+
export function mapEventToProgress(event: RpcChildEvent): RpcProgressUpdate | null {
|
|
176
|
+
switch (event.type) {
|
|
177
|
+
case "agent_start":
|
|
178
|
+
return { kind: "lifecycle", summary: "Agent started" };
|
|
179
|
+
|
|
180
|
+
case "agent_end":
|
|
181
|
+
return { kind: "lifecycle", summary: "Agent completed" };
|
|
182
|
+
|
|
183
|
+
case "turn_start":
|
|
184
|
+
return null; // No meaningful progress
|
|
185
|
+
|
|
186
|
+
case "turn_end":
|
|
187
|
+
return { kind: "lifecycle", summary: "Turn completed" };
|
|
188
|
+
|
|
189
|
+
case "message_start":
|
|
190
|
+
return null; // Wait for content
|
|
191
|
+
|
|
192
|
+
case "message_update":
|
|
193
|
+
return null; // Too noisy for dashboard
|
|
194
|
+
|
|
195
|
+
case "message_end":
|
|
196
|
+
return { kind: "lifecycle", summary: "Message completed" };
|
|
197
|
+
|
|
198
|
+
case "tool_execution_start":
|
|
199
|
+
return {
|
|
200
|
+
kind: "tool",
|
|
201
|
+
summary: `tool: ${event.toolName}${formatToolArgs(event.toolName, event.args)}`,
|
|
202
|
+
toolName: event.toolName,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
case "tool_execution_update":
|
|
206
|
+
return null; // Partial results too noisy
|
|
207
|
+
|
|
208
|
+
case "tool_execution_end":
|
|
209
|
+
return {
|
|
210
|
+
kind: "tool",
|
|
211
|
+
summary: `tool: ${event.toolName} ${event.isError ? "✗" : "✓"}`,
|
|
212
|
+
toolName: event.toolName,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
case "auto_compaction_start":
|
|
216
|
+
return { kind: "lifecycle", summary: "Compacting context…" };
|
|
217
|
+
|
|
218
|
+
case "auto_compaction_end":
|
|
219
|
+
return { kind: "lifecycle", summary: event.aborted ? "Compaction aborted" : "Compaction done" };
|
|
220
|
+
|
|
221
|
+
case "auto_retry_start":
|
|
222
|
+
return { kind: "lifecycle", summary: `Retry ${event.attempt}/${event.maxAttempts}` };
|
|
223
|
+
|
|
224
|
+
case "auto_retry_end":
|
|
225
|
+
return { kind: "lifecycle", summary: event.success ? "Retry succeeded" : "Retry failed" };
|
|
226
|
+
|
|
227
|
+
case "response":
|
|
228
|
+
// RPC response to our command — not progress-relevant
|
|
229
|
+
return null;
|
|
230
|
+
|
|
231
|
+
case "pipe_closed":
|
|
232
|
+
return { kind: "error", summary: "Pipe closed" };
|
|
233
|
+
|
|
234
|
+
case "extension_ui_request":
|
|
235
|
+
// UI requests from child extensions — not progress-relevant
|
|
236
|
+
return null;
|
|
237
|
+
|
|
238
|
+
default: {
|
|
239
|
+
// Exhaustiveness check: if a new event type is added to RpcChildEvent
|
|
240
|
+
// but not handled here, TypeScript will report an error on this line.
|
|
241
|
+
const _exhaustive: never = event;
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Format tool arguments for display in a concise summary line.
|
|
249
|
+
*/
|
|
250
|
+
function formatToolArgs(toolName: string, args: unknown): string {
|
|
251
|
+
if (!args || typeof args !== "object") return "";
|
|
252
|
+
const a = args as Record<string, unknown>;
|
|
253
|
+
switch (toolName) {
|
|
254
|
+
case "read":
|
|
255
|
+
case "write":
|
|
256
|
+
case "view":
|
|
257
|
+
return a.path ? ` ${a.path}` : "";
|
|
258
|
+
case "edit":
|
|
259
|
+
return a.path ? ` ${a.path}` : "";
|
|
260
|
+
case "bash":
|
|
261
|
+
if (typeof a.command === "string") {
|
|
262
|
+
const cmd = a.command.length > 60 ? a.command.slice(0, 57) + "…" : a.command;
|
|
263
|
+
return ` ${cmd}`;
|
|
264
|
+
}
|
|
265
|
+
return "";
|
|
266
|
+
default:
|
|
267
|
+
return "";
|
|
268
|
+
}
|
|
269
|
+
}
|