omegon 0.8.4 → 0.9.1
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/rpc-child.ts +269 -0
- package/extensions/cleave/types.ts +52 -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/scripts/check-vendor-dist.mjs +56 -0
- 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";
|
|
@@ -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
|
+
}
|
|
@@ -237,6 +237,58 @@ export interface ReunificationResult {
|
|
|
237
237
|
readyToClose: boolean;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
// ─── RPC Child Communication ─────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Events received from a child process running in RPC mode.
|
|
244
|
+
*
|
|
245
|
+
* This is a discriminated union covering:
|
|
246
|
+
* - AgentEvent types (agent lifecycle, turn, message, tool execution)
|
|
247
|
+
* - AgentSessionEvent extensions (auto_compaction, auto_retry)
|
|
248
|
+
* - RPC response events (command acknowledgements)
|
|
249
|
+
* - Synthetic pipe_closed event (stdout closed)
|
|
250
|
+
*
|
|
251
|
+
* We define this as our own union rather than importing from pi-mono
|
|
252
|
+
* to avoid pulling in transitive dependencies and to add the synthetic
|
|
253
|
+
* pipe_closed event.
|
|
254
|
+
*/
|
|
255
|
+
export type RpcChildEvent =
|
|
256
|
+
// Agent lifecycle
|
|
257
|
+
| { type: "agent_start" }
|
|
258
|
+
| { type: "agent_end"; messages: unknown[] }
|
|
259
|
+
// Turn lifecycle
|
|
260
|
+
| { type: "turn_start" }
|
|
261
|
+
| { type: "turn_end"; message: unknown; toolResults: unknown[] }
|
|
262
|
+
// Message lifecycle
|
|
263
|
+
| { type: "message_start"; message?: unknown }
|
|
264
|
+
| { type: "message_update"; message?: unknown; assistantMessageEvent?: unknown }
|
|
265
|
+
| { type: "message_end"; message?: unknown }
|
|
266
|
+
// Tool execution
|
|
267
|
+
| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: unknown }
|
|
268
|
+
| { type: "tool_execution_update"; toolCallId: string; toolName: string; args: unknown; partialResult: unknown }
|
|
269
|
+
| { type: "tool_execution_end"; toolCallId: string; toolName: string; result: unknown; isError: boolean }
|
|
270
|
+
// Session extensions
|
|
271
|
+
| { type: "auto_compaction_start"; reason: "threshold" | "overflow" }
|
|
272
|
+
| { type: "auto_compaction_end"; result?: unknown; aborted: boolean; willRetry: boolean; errorMessage?: string }
|
|
273
|
+
| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
|
|
274
|
+
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
|
|
275
|
+
// Extension UI requests (from child extensions calling ui.select/ui.confirm)
|
|
276
|
+
| { type: "extension_ui_request"; requestId: string; extensionId: string; method: string; params: unknown }
|
|
277
|
+
// RPC command response
|
|
278
|
+
| { type: "response"; id?: string; command: string; success: boolean; data?: unknown; error?: string }
|
|
279
|
+
// Synthetic: stdout pipe closed (graceful degradation)
|
|
280
|
+
| { type: "pipe_closed" };
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Structured progress update derived from an RPC child event.
|
|
284
|
+
* Used by the dashboard to display child status.
|
|
285
|
+
*/
|
|
286
|
+
export interface RpcProgressUpdate {
|
|
287
|
+
kind: "tool" | "lifecycle" | "error";
|
|
288
|
+
summary: string;
|
|
289
|
+
toolName?: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
240
292
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
241
293
|
|
|
242
294
|
export interface CleaveConfig {
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
This package contains type definitions for node (https://nodejs.org/).
|
|
6
6
|
|
|
7
7
|
# Details
|
|
8
|
-
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node.
|
|
8
|
+
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node/v22.
|
|
9
9
|
|
|
10
10
|
### Additional Details
|
|
11
|
-
* Last updated:
|
|
11
|
+
* Last updated: Fri, 06 Mar 2026 00:57:44 GMT
|
|
12
12
|
* Dependencies: [undici-types](https://npmjs.com/package/undici-types)
|
|
13
13
|
|
|
14
14
|
# Credits
|
|
15
|
-
These definitions were written by [Microsoft TypeScript](https://github.com/Microsoft), [Alberto Schiabel](https://github.com/jkomyno), [Andrew Makarov](https://github.com/r3nya), [Benjamin Toueg](https://github.com/btoueg), [David Junger](https://github.com/touffy), [Mohsen Azimi](https://github.com/mohsen1), [Nikita Galkin](https://github.com/galkin), [Sebastian Silbermann](https://github.com/eps1lon), [Wilco Bakker](https://github.com/WilcoBakker), [Marcin Kopacz](https://github.com/chyzwar), [Trivikram Kamat](https://github.com/trivikr), [Junxiao Shi](https://github.com/yoursunny), [Ilia Baryshnikov](https://github.com/qwelias), [ExE Boss](https://github.com/ExE-Boss), [Piotr Błażejewicz](https://github.com/peterblazejewicz), [Anna Henningsen](https://github.com/addaleax), [Victor Perin](https://github.com/victorperin), [NodeJS Contributors](https://github.com/NodeJS), [Linus Unnebäck](https://github.com/LinusU), [wafuwafu13](https://github.com/wafuwafu13), [Matteo Collina](https://github.com/mcollina), [Dmitry Semigradsky](https://github.com/Semigradsky), [René](https://github.com/Renegade334)
|
|
15
|
+
These definitions were written by [Microsoft TypeScript](https://github.com/Microsoft), [Alberto Schiabel](https://github.com/jkomyno), [Andrew Makarov](https://github.com/r3nya), [Benjamin Toueg](https://github.com/btoueg), [David Junger](https://github.com/touffy), [Mohsen Azimi](https://github.com/mohsen1), [Nikita Galkin](https://github.com/galkin), [Sebastian Silbermann](https://github.com/eps1lon), [Wilco Bakker](https://github.com/WilcoBakker), [Marcin Kopacz](https://github.com/chyzwar), [Trivikram Kamat](https://github.com/trivikr), [Junxiao Shi](https://github.com/yoursunny), [Ilia Baryshnikov](https://github.com/qwelias), [ExE Boss](https://github.com/ExE-Boss), [Piotr Błażejewicz](https://github.com/peterblazejewicz), [Anna Henningsen](https://github.com/addaleax), [Victor Perin](https://github.com/victorperin), [NodeJS Contributors](https://github.com/NodeJS), [Linus Unnebäck](https://github.com/LinusU), [wafuwafu13](https://github.com/wafuwafu13), [Matteo Collina](https://github.com/mcollina), [Dmitry Semigradsky](https://github.com/Semigradsky), and [René](https://github.com/Renegade334).
|