@gajae-code/coding-agent 0.5.2 → 0.5.4
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/CHANGELOG.md +23 -0
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/config/model-profiles.d.ts +10 -0
- package/dist/types/dap/client.d.ts +2 -1
- package/dist/types/edit/read-file.d.ts +6 -0
- package/dist/types/eval/js/context-manager.d.ts +3 -0
- package/dist/types/eval/js/executor.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime/process-lifecycle.d.ts +108 -0
- package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +29 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/streaming-output.d.ts +12 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
- package/dist/types/tools/sqlite-reader.d.ts +2 -1
- package/dist/types/web/search/providers/codex.d.ts +4 -4
- package/package.json +7 -7
- package/src/async/job-manager.ts +181 -43
- package/src/config/file-lock.ts +9 -1
- package/src/config/model-profile-activation.ts +71 -3
- package/src/config/model-profiles.ts +39 -14
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
- package/src/edit/read-file.ts +19 -1
- package/src/eval/js/context-manager.ts +228 -65
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/js/worker-core.ts +10 -6
- package/src/eval/py/executor.ts +68 -19
- package/src/eval/py/kernel.ts +46 -22
- package/src/eval/py/runner.py +68 -14
- package/src/exec/bash-executor.ts +49 -13
- package/src/gjc-runtime/deep-interview-runtime.ts +14 -13
- package/src/gjc-runtime/ralplan-runtime.ts +10 -0
- package/src/gjc-runtime/state-runtime.ts +73 -0
- package/src/gjc-runtime/tmux-gc.ts +86 -37
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-runtime.ts +8 -4
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/client.ts +64 -26
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/modes/bridge/bridge-mode.ts +21 -0
- package/src/modes/components/assistant-message.ts +10 -2
- package/src/modes/components/bash-execution.ts +5 -1
- package/src/modes/components/eval-execution.ts +5 -1
- package/src/modes/components/model-selector.ts +34 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
- package/src/modes/components/skill-message.ts +24 -16
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +33 -6
- package/src/modes/controllers/input-controller.ts +19 -0
- package/src/modes/controllers/selector-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +1 -1
- package/src/runtime/process-lifecycle.ts +400 -0
- package/src/runtime-mcp/manager.ts +164 -50
- package/src/runtime-mcp/transports/http.ts +12 -11
- package/src/runtime-mcp/transports/stdio.ts +64 -38
- package/src/runtime-mcp/types.ts +3 -0
- package/src/sdk.ts +27 -0
- package/src/session/agent-session.ts +271 -25
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/session-manager.ts +29 -13
- package/src/session/streaming-output.ts +95 -3
- package/src/setup/model-onboarding-guidance.ts +10 -3
- package/src/skill-state/active-state.ts +79 -7
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/registry.ts +17 -1
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +2 -6
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/web/search/providers/codex.ts +6 -5
package/src/eval/py/kernel.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Speaks NDJSON with `runner.py` over stdin/stdout. One subprocess per kernel
|
|
5
5
|
* instance; sessions reuse a single subprocess across executions. Cancellation
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* sends SIGINT to the runner process group, which raises a real
|
|
7
|
+
* `KeyboardInterrupt` inside user code and any foreground magic subprocess.
|
|
8
|
+
* Shutdown writes `{"type":"exit"}` and escalates to SIGTERM/SIGKILL on
|
|
8
9
|
* timeout.
|
|
9
10
|
*/
|
|
10
11
|
import * as fs from "node:fs";
|
|
@@ -173,6 +174,7 @@ interface PendingExecution {
|
|
|
173
174
|
kernelKilled: boolean;
|
|
174
175
|
settled: boolean;
|
|
175
176
|
escalationTimer?: NodeJS.Timeout;
|
|
177
|
+
finalize?: () => void;
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
export class PythonKernel {
|
|
@@ -227,6 +229,9 @@ export class PythonKernel {
|
|
|
227
229
|
stdout: "pipe",
|
|
228
230
|
stderr: "pipe",
|
|
229
231
|
windowsHide: true,
|
|
232
|
+
// Run as its own session/process-group leader so SIGINT/SIGTERM can be
|
|
233
|
+
// delivered to the whole runner tree (incl. foreground magic subprocesses).
|
|
234
|
+
detached: true,
|
|
230
235
|
});
|
|
231
236
|
kernel.#proc = proc;
|
|
232
237
|
kernel.#stdin = proc.stdin;
|
|
@@ -377,10 +382,30 @@ export class PythonKernel {
|
|
|
377
382
|
|
|
378
383
|
async interrupt(): Promise<void> {
|
|
379
384
|
if (!this.#proc || this.#disposed) return;
|
|
385
|
+
this.#signalProcessGroup("SIGINT");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
#signalProcessGroup(signal: NodeJS.Signals): void {
|
|
389
|
+
const proc = this.#proc;
|
|
390
|
+
if (!proc) return;
|
|
391
|
+
try {
|
|
392
|
+
if (process.platform !== "win32") {
|
|
393
|
+
process.kill(-proc.pid, signal);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
} catch (err) {
|
|
397
|
+
logger.warn("Failed to signal python runner process group", {
|
|
398
|
+
signal,
|
|
399
|
+
error: err instanceof Error ? err.message : String(err),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
380
402
|
try {
|
|
381
|
-
|
|
403
|
+
proc.kill(signal);
|
|
382
404
|
} catch (err) {
|
|
383
|
-
logger.warn("Failed to
|
|
405
|
+
logger.warn("Failed to signal python runner", {
|
|
406
|
+
signal,
|
|
407
|
+
error: err instanceof Error ? err.message : String(err),
|
|
408
|
+
});
|
|
384
409
|
}
|
|
385
410
|
}
|
|
386
411
|
|
|
@@ -412,24 +437,16 @@ export class PythonKernel {
|
|
|
412
437
|
|
|
413
438
|
const exited = this.#waitForExitWithTimeout(timeoutMs);
|
|
414
439
|
let result = await exited;
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
proc.kill("SIGTERM");
|
|
418
|
-
} catch {
|
|
419
|
-
/* ignore */
|
|
420
|
-
}
|
|
440
|
+
if (result === null) {
|
|
441
|
+
this.#signalProcessGroup("SIGTERM");
|
|
421
442
|
result = await this.#waitForExitWithTimeout(timeoutMs);
|
|
422
443
|
}
|
|
423
|
-
if (
|
|
424
|
-
|
|
425
|
-
proc.kill("SIGKILL");
|
|
426
|
-
} catch {
|
|
427
|
-
/* ignore */
|
|
428
|
-
}
|
|
444
|
+
if (result === null) {
|
|
445
|
+
this.#signalProcessGroup("SIGKILL");
|
|
429
446
|
result = await this.#waitForExitWithTimeout(timeoutMs);
|
|
430
447
|
}
|
|
431
448
|
|
|
432
|
-
const confirmed =
|
|
449
|
+
const confirmed = result !== null;
|
|
433
450
|
this.#shutdownConfirmed = confirmed;
|
|
434
451
|
this.#disposed = true;
|
|
435
452
|
return { confirmed };
|
|
@@ -441,17 +458,21 @@ export class PythonKernel {
|
|
|
441
458
|
this.#pending.clear();
|
|
442
459
|
const kernelKilledDefault = options?.kernelKilled ?? false;
|
|
443
460
|
for (const entry of pending) {
|
|
461
|
+
entry.cancelled = true;
|
|
462
|
+
entry.status = "error";
|
|
463
|
+
entry.kernelKilled = entry.kernelKilled || kernelKilledDefault;
|
|
464
|
+
entry.finalize?.();
|
|
444
465
|
if (entry.settled) continue;
|
|
445
466
|
entry.settled = true;
|
|
446
467
|
void entry.options?.onChunk?.(`[kernel] ${reason}\n`);
|
|
447
468
|
entry.resolve({
|
|
448
|
-
status:
|
|
449
|
-
cancelled:
|
|
469
|
+
status: entry.status,
|
|
470
|
+
cancelled: entry.cancelled,
|
|
450
471
|
timedOut: entry.timedOut,
|
|
451
472
|
stdinRequested: entry.stdinRequested,
|
|
452
473
|
executionCount: entry.executionCount,
|
|
453
474
|
error: entry.error,
|
|
454
|
-
kernelKilled: entry.kernelKilled
|
|
475
|
+
kernelKilled: entry.kernelKilled,
|
|
455
476
|
});
|
|
456
477
|
}
|
|
457
478
|
}
|
|
@@ -655,11 +676,14 @@ export class PythonKernel {
|
|
|
655
676
|
#waitForExitWithTimeout(timeoutMs: number): Promise<number | null> {
|
|
656
677
|
if (!this.#exitedPromise) return Promise.resolve(0);
|
|
657
678
|
const exitedPromise = this.#exitedPromise;
|
|
658
|
-
|
|
679
|
+
return new Promise(resolve => {
|
|
659
680
|
const timer = setTimeout(() => resolve(null), Math.max(0, timeoutMs));
|
|
660
681
|
timer.unref?.();
|
|
682
|
+
exitedPromise.then(code => {
|
|
683
|
+
clearTimeout(timer);
|
|
684
|
+
resolve(code);
|
|
685
|
+
});
|
|
661
686
|
});
|
|
662
|
-
return Promise.race([exitedPromise.then(code => code as number | null), timeout]);
|
|
663
687
|
}
|
|
664
688
|
}
|
|
665
689
|
|
package/src/eval/py/runner.py
CHANGED
|
@@ -290,6 +290,44 @@ def _emit_status(op: str, **data: Any) -> None:
|
|
|
290
290
|
_emit({"type": "display", "id": rid, "bundle": bundle})
|
|
291
291
|
|
|
292
292
|
|
|
293
|
+
def _terminate_process_group(proc: subprocess.Popen[Any]) -> None:
|
|
294
|
+
if proc.poll() is not None:
|
|
295
|
+
return
|
|
296
|
+
try:
|
|
297
|
+
if os.name == "posix":
|
|
298
|
+
os.killpg(proc.pid, signal.SIGTERM)
|
|
299
|
+
else:
|
|
300
|
+
proc.terminate()
|
|
301
|
+
except ProcessLookupError:
|
|
302
|
+
return
|
|
303
|
+
except Exception:
|
|
304
|
+
try:
|
|
305
|
+
proc.terminate()
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
try:
|
|
309
|
+
proc.wait(timeout=1)
|
|
310
|
+
return
|
|
311
|
+
except subprocess.TimeoutExpired:
|
|
312
|
+
pass
|
|
313
|
+
try:
|
|
314
|
+
if os.name == "posix":
|
|
315
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
316
|
+
else:
|
|
317
|
+
proc.kill()
|
|
318
|
+
except ProcessLookupError:
|
|
319
|
+
return
|
|
320
|
+
except Exception:
|
|
321
|
+
try:
|
|
322
|
+
proc.kill()
|
|
323
|
+
except Exception:
|
|
324
|
+
pass
|
|
325
|
+
try:
|
|
326
|
+
proc.wait(timeout=1)
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
|
|
330
|
+
|
|
293
331
|
@line_magic("pip")
|
|
294
332
|
def _magic_pip(args: str) -> None:
|
|
295
333
|
argv = shlex.split(args) if args else ["--help"]
|
|
@@ -300,18 +338,26 @@ def _magic_pip(args: str) -> None:
|
|
|
300
338
|
stderr=subprocess.STDOUT,
|
|
301
339
|
text=True,
|
|
302
340
|
bufsize=1,
|
|
341
|
+
start_new_session=(os.name == "posix"),
|
|
303
342
|
)
|
|
304
343
|
installed_packages: list[str] = []
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
344
|
+
try:
|
|
345
|
+
assert proc.stdout is not None
|
|
346
|
+
try:
|
|
347
|
+
for raw_line in proc.stdout:
|
|
348
|
+
sys.stdout.write(raw_line)
|
|
349
|
+
m = re.search(r"Successfully installed\s+(.+)$", raw_line)
|
|
350
|
+
if m:
|
|
351
|
+
for token in m.group(1).split():
|
|
352
|
+
# Token is name-version; drop the version suffix.
|
|
353
|
+
pkg = token.rsplit("-", 1)[0]
|
|
354
|
+
installed_packages.append(pkg.replace("_", "-"))
|
|
355
|
+
finally:
|
|
356
|
+
proc.stdout.close()
|
|
357
|
+
proc.wait()
|
|
358
|
+
except KeyboardInterrupt:
|
|
359
|
+
_terminate_process_group(proc)
|
|
360
|
+
raise
|
|
315
361
|
if installed_packages:
|
|
316
362
|
import importlib
|
|
317
363
|
|
|
@@ -500,11 +546,19 @@ def _run_shell_body(body: str, *, shell_arg: str) -> int:
|
|
|
500
546
|
stderr=subprocess.STDOUT,
|
|
501
547
|
text=True,
|
|
502
548
|
bufsize=1,
|
|
549
|
+
start_new_session=(os.name == "posix"),
|
|
503
550
|
)
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
551
|
+
try:
|
|
552
|
+
assert proc.stdout is not None
|
|
553
|
+
try:
|
|
554
|
+
for raw_line in proc.stdout:
|
|
555
|
+
sys.stdout.write(raw_line)
|
|
556
|
+
finally:
|
|
557
|
+
proc.stdout.close()
|
|
558
|
+
proc.wait()
|
|
559
|
+
except KeyboardInterrupt:
|
|
560
|
+
_terminate_process_group(proc)
|
|
561
|
+
raise
|
|
508
562
|
return proc.returncode
|
|
509
563
|
|
|
510
564
|
|
|
@@ -32,6 +32,8 @@ export interface BashExecutorOptions {
|
|
|
32
32
|
/** Artifact path/id for full output storage */
|
|
33
33
|
artifactPath?: string;
|
|
34
34
|
artifactId?: string;
|
|
35
|
+
/** Execute without retaining a native Shell in the persistent session registry. */
|
|
36
|
+
oneShot?: boolean;
|
|
35
37
|
/**
|
|
36
38
|
* Invoked when the native minimizer rewrote the command's output, giving
|
|
37
39
|
* the caller a chance to persist the lossless original capture (typically
|
|
@@ -59,6 +61,8 @@ export interface BashResult {
|
|
|
59
61
|
|
|
60
62
|
const shellSessions = new Map<string, Shell>();
|
|
61
63
|
const brokenShellSessions = new Set<string>();
|
|
64
|
+
const retiringShellSessions = new Set<Shell>();
|
|
65
|
+
const CANCEL_CLEANUP_WAIT_MS = 250;
|
|
62
66
|
|
|
63
67
|
/** Number of persistent shell sessions currently retained (owner gauge). */
|
|
64
68
|
export function getShellSessionCount(): number {
|
|
@@ -76,10 +80,14 @@ export async function disposeAllShellSessions(): Promise<void> {
|
|
|
76
80
|
// Snapshot and drop strong references up front so concurrent callers cannot
|
|
77
81
|
// reuse a session that is being torn down, then await every native abort so
|
|
78
82
|
// shutdown/signal cleanup does not return before resources are released.
|
|
79
|
-
|
|
83
|
+
// Include retiring shells whose JS call returned after bounded abort cleanup
|
|
84
|
+
// while the native run is still unwinding; they are no longer reusable but
|
|
85
|
+
// remain owned until their run promise settles.
|
|
86
|
+
const sessions = new Set([...shellSessions.values(), ...retiringShellSessions]);
|
|
80
87
|
shellSessions.clear();
|
|
88
|
+
retiringShellSessions.clear();
|
|
81
89
|
brokenShellSessions.clear();
|
|
82
|
-
await Promise.allSettled(sessions.map(session => session.abort()));
|
|
90
|
+
await Promise.allSettled([...sessions].map(session => session.abort()));
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
postmortem.register("bash-executor:shell-sessions", () => disposeAllShellSessions());
|
|
@@ -151,14 +159,12 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
151
159
|
};
|
|
152
160
|
}
|
|
153
161
|
|
|
162
|
+
const usePersistentShell = options?.oneShot !== true;
|
|
154
163
|
const sessionKey = buildSessionKey(shell, prefix, snapshotPath, shellEnv, options?.sessionKey, minimizer);
|
|
155
|
-
const persistentSessionBroken = brokenShellSessions.has(sessionKey);
|
|
156
|
-
if (persistentSessionBroken) {
|
|
157
|
-
shellSessions.delete(sessionKey);
|
|
158
|
-
}
|
|
164
|
+
const persistentSessionBroken = usePersistentShell && brokenShellSessions.has(sessionKey);
|
|
159
165
|
|
|
160
|
-
let shellSession = persistentSessionBroken ? undefined : shellSessions.get(sessionKey);
|
|
161
|
-
if (!shellSession && !persistentSessionBroken) {
|
|
166
|
+
let shellSession = persistentSessionBroken || !usePersistentShell ? undefined : shellSessions.get(sessionKey);
|
|
167
|
+
if (!shellSession && !persistentSessionBroken && usePersistentShell) {
|
|
162
168
|
shellSession = new Shell({
|
|
163
169
|
sessionEnv: shellEnv,
|
|
164
170
|
snapshotPath: snapshotPath ?? undefined,
|
|
@@ -172,16 +178,29 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
172
178
|
if (!runAbortController.signal.aborted) {
|
|
173
179
|
runAbortController.abort();
|
|
174
180
|
}
|
|
175
|
-
if (shellSession) {
|
|
176
|
-
|
|
177
|
-
void shellSession.abort();
|
|
181
|
+
if (shellSession && !abortPromise) {
|
|
182
|
+
abortPromise = shellSession.abort();
|
|
178
183
|
}
|
|
179
184
|
};
|
|
180
185
|
const abortDeferred = Promise.withResolvers<"abort">();
|
|
186
|
+
let abortPromise: Promise<unknown> | undefined;
|
|
181
187
|
const abortHandler = () => {
|
|
182
188
|
abortCurrentExecution();
|
|
183
189
|
abortDeferred.resolve("abort");
|
|
184
190
|
};
|
|
191
|
+
const awaitAbortCleanup = async (runPromise: Promise<unknown>): Promise<boolean> => {
|
|
192
|
+
const settled = await Promise.race([
|
|
193
|
+
runPromise.then(
|
|
194
|
+
() => true,
|
|
195
|
+
() => true,
|
|
196
|
+
),
|
|
197
|
+
Bun.sleep(CANCEL_CLEANUP_WAIT_MS).then(() => false),
|
|
198
|
+
]);
|
|
199
|
+
if (abortPromise) {
|
|
200
|
+
await Promise.race([abortPromise.catch(() => undefined), Bun.sleep(CANCEL_CLEANUP_WAIT_MS)]);
|
|
201
|
+
}
|
|
202
|
+
return settled;
|
|
203
|
+
};
|
|
185
204
|
if (userSignal) {
|
|
186
205
|
userSignal.addEventListener("abort", abortHandler, { once: true });
|
|
187
206
|
}
|
|
@@ -198,6 +217,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
198
217
|
}
|
|
199
218
|
|
|
200
219
|
let resetSession = false;
|
|
220
|
+
let runSettled = false;
|
|
201
221
|
|
|
202
222
|
try {
|
|
203
223
|
const runPromise = shellSession
|
|
@@ -243,8 +263,24 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
243
263
|
acceptingChunks = false;
|
|
244
264
|
if (shellSession) {
|
|
245
265
|
resetSession = true;
|
|
266
|
+
retiringShellSessions.add(shellSession);
|
|
246
267
|
brokenShellSessions.add(sessionKey);
|
|
247
|
-
|
|
268
|
+
shellSessions.delete(sessionKey);
|
|
269
|
+
runSettled = await awaitAbortCleanup(runPromise);
|
|
270
|
+
if (runSettled) {
|
|
271
|
+
brokenShellSessions.delete(sessionKey);
|
|
272
|
+
retiringShellSessions.delete(shellSession);
|
|
273
|
+
} else {
|
|
274
|
+
void runPromise
|
|
275
|
+
.finally(() => {
|
|
276
|
+
brokenShellSessions.delete(sessionKey);
|
|
277
|
+
retiringShellSessions.delete(shellSession);
|
|
278
|
+
if (shellSessions.get(sessionKey) === shellSession) {
|
|
279
|
+
shellSessions.delete(sessionKey);
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
.catch(() => undefined);
|
|
283
|
+
}
|
|
248
284
|
} else {
|
|
249
285
|
void runPromise.catch(() => undefined);
|
|
250
286
|
}
|
|
@@ -337,7 +373,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
337
373
|
if (userSignal) {
|
|
338
374
|
userSignal.removeEventListener("abort", abortHandler);
|
|
339
375
|
}
|
|
340
|
-
if (resetSession) {
|
|
376
|
+
if (resetSession && runSettled && shellSessions.get(sessionKey) === shellSession) {
|
|
341
377
|
shellSessions.delete(sessionKey);
|
|
342
378
|
}
|
|
343
379
|
}
|
|
@@ -124,8 +124,8 @@ interface ResolvedDeepInterviewArgs {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
interface DeepInterviewLanguagePreference {
|
|
127
|
-
code: "en" | "
|
|
128
|
-
label: "English" | "
|
|
127
|
+
code: "en" | "user";
|
|
128
|
+
label: "English" | "User language";
|
|
129
129
|
source: "explicit-user-request" | "initial-idea";
|
|
130
130
|
instruction: string;
|
|
131
131
|
}
|
|
@@ -239,21 +239,22 @@ function englishLanguagePreference(): DeepInterviewLanguagePreference {
|
|
|
239
239
|
};
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
function userLanguagePreference(): DeepInterviewLanguagePreference {
|
|
243
|
+
return {
|
|
244
|
+
code: "user",
|
|
245
|
+
label: "User language",
|
|
246
|
+
source: "initial-idea",
|
|
247
|
+
instruction:
|
|
248
|
+
"Ask every user-facing deep-interview question in the user/session language inferred from the initial idea unless the user explicitly requests another language. Keep code identifiers, file paths, commands, settings/JSON keys, library/API names, and quoted source text unchanged when appropriate.",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
242
252
|
function resolveDeepInterviewLanguagePreference(idea: string): DeepInterviewLanguagePreference | undefined {
|
|
243
253
|
if (/\b(?:answer|ask|respond|reply|write|use|speak)\s+(?:only\s+)?in\s+English\b/i.test(idea)) {
|
|
244
254
|
return englishLanguagePreference();
|
|
245
255
|
}
|
|
246
|
-
if (/
|
|
247
|
-
return
|
|
248
|
-
}
|
|
249
|
-
if (/\p{Script=Hangul}/u.test(idea)) {
|
|
250
|
-
return {
|
|
251
|
-
code: "ko",
|
|
252
|
-
label: "Korean",
|
|
253
|
-
source: "initial-idea",
|
|
254
|
-
instruction:
|
|
255
|
-
"Ask every user-facing deep-interview question in Korean unless the user explicitly requests another language.",
|
|
256
|
-
};
|
|
256
|
+
if (/[^\p{Script=Latin}\p{Script=Common}\p{Script=Inherited}]/u.test(idea)) {
|
|
257
|
+
return userLanguagePreference();
|
|
257
258
|
}
|
|
258
259
|
return undefined;
|
|
259
260
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./ledger-event-renderer";
|
|
14
14
|
import { isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
|
|
15
15
|
import { migrateWorkflowState } from "./state-migrations";
|
|
16
|
+
import { runNativeStateCommand } from "./state-runtime";
|
|
16
17
|
import {
|
|
17
18
|
appendJsonlIdempotent,
|
|
18
19
|
readExistingStateForMutation,
|
|
@@ -104,6 +105,10 @@ export function isRalplanArtifactWriteInvocation(args: readonly string[]): boole
|
|
|
104
105
|
return hasFlag(args, "--write");
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
function isRalplanDoctorInvocation(args: readonly string[]): boolean {
|
|
109
|
+
return args[0] === "doctor";
|
|
110
|
+
}
|
|
111
|
+
|
|
107
112
|
function assertSafePathComponent(value: string, label: string): void {
|
|
108
113
|
if (!PATH_COMPONENT_RE.test(value) || value.includes("..")) {
|
|
109
114
|
throw new RalplanCommandError(2, `invalid path component for --${label}: ${value}`);
|
|
@@ -854,10 +859,15 @@ async function handleConsensusHandoff(args: readonly string[], cwd: string): Pro
|
|
|
854
859
|
return { status: 0, stdout };
|
|
855
860
|
}
|
|
856
861
|
|
|
862
|
+
async function handleDoctor(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
|
|
863
|
+
return await runNativeStateCommand(["doctor", "--skill", "ralplan", ...args.slice(1)], cwd);
|
|
864
|
+
}
|
|
865
|
+
|
|
857
866
|
/* -------------------------------- entry --------------------------------- */
|
|
858
867
|
|
|
859
868
|
export async function runNativeRalplanCommand(args: string[], cwd = process.cwd()): Promise<RalplanCommandResult> {
|
|
860
869
|
try {
|
|
870
|
+
if (isRalplanDoctorInvocation(args)) return await handleDoctor(args, cwd);
|
|
861
871
|
if (isRalplanArtifactWriteInvocation(args)) return await handleArtifactWrite(args, cwd);
|
|
862
872
|
return await handleConsensusHandoff(args, cwd);
|
|
863
873
|
} catch (error) {
|
|
@@ -450,6 +450,52 @@ function activeFlag(value: unknown): boolean {
|
|
|
450
450
|
return isPlainObject(value) && value.active !== false;
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
+
function phaseFromActiveValue(value: unknown): string | undefined {
|
|
454
|
+
if (!isPlainObject(value) || typeof value.phase !== "string") return undefined;
|
|
455
|
+
const phase = value.phase.trim();
|
|
456
|
+
return phase || undefined;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const RALPLAN_CANONICAL_PHASE_OVERRIDES = new Set([
|
|
460
|
+
"final",
|
|
461
|
+
"handoff",
|
|
462
|
+
"complete",
|
|
463
|
+
"completed",
|
|
464
|
+
"failed",
|
|
465
|
+
"cancelled",
|
|
466
|
+
"canceled",
|
|
467
|
+
"inactive",
|
|
468
|
+
]);
|
|
469
|
+
|
|
470
|
+
function modeStatePhase(value: unknown): string | undefined {
|
|
471
|
+
if (!isPlainObject(value) || typeof value.current_phase !== "string") return undefined;
|
|
472
|
+
const phase = value.current_phase.trim();
|
|
473
|
+
if (!phase) return undefined;
|
|
474
|
+
if (value.active === false && !RALPLAN_CANONICAL_PHASE_OVERRIDES.has(phase)) return undefined;
|
|
475
|
+
return phase;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function pushPhaseDriftProblem(options: {
|
|
479
|
+
problems: DoctorProblem[];
|
|
480
|
+
pathValue: string;
|
|
481
|
+
skill: CanonicalGjcWorkflowSkill;
|
|
482
|
+
entryKind: "active entry" | "active snapshot";
|
|
483
|
+
entrySkill: string;
|
|
484
|
+
entryPhase: string | undefined;
|
|
485
|
+
statePhase: string | undefined;
|
|
486
|
+
}): void {
|
|
487
|
+
if (!options.entryPhase || !options.statePhase || options.entryPhase === options.statePhase) return;
|
|
488
|
+
options.problems.push(
|
|
489
|
+
doctorProblem(
|
|
490
|
+
"stale_active_state",
|
|
491
|
+
options.pathValue,
|
|
492
|
+
`${options.entryKind} for ${options.entrySkill} phase ${options.entryPhase} differs from canonical mode-state phase ${options.statePhase}`,
|
|
493
|
+
`gjc state ${options.skill} clear`,
|
|
494
|
+
options.skill,
|
|
495
|
+
),
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
453
499
|
async function collectDoctorSummary(
|
|
454
500
|
cwd: string,
|
|
455
501
|
skill: CanonicalGjcWorkflowSkill | undefined,
|
|
@@ -460,6 +506,7 @@ async function collectDoctorSummary(
|
|
|
460
506
|
const problems: DoctorProblem[] = [];
|
|
461
507
|
let filesScanned = 0;
|
|
462
508
|
let journalsScanned = 0;
|
|
509
|
+
const invalidModeStates = new Set<string>();
|
|
463
510
|
|
|
464
511
|
for (const currentSkill of skills) {
|
|
465
512
|
const filePath = modeStateFile(cwd, currentSkill, sessionId);
|
|
@@ -476,6 +523,7 @@ async function collectDoctorSummary(
|
|
|
476
523
|
currentSkill,
|
|
477
524
|
),
|
|
478
525
|
);
|
|
526
|
+
invalidModeStates.add(currentSkill);
|
|
479
527
|
continue;
|
|
480
528
|
}
|
|
481
529
|
const validation = validateWorkflowStateEnvelope(currentSkill, raw.value);
|
|
@@ -489,6 +537,7 @@ async function collectDoctorSummary(
|
|
|
489
537
|
currentSkill,
|
|
490
538
|
),
|
|
491
539
|
);
|
|
540
|
+
invalidModeStates.add(currentSkill);
|
|
492
541
|
}
|
|
493
542
|
const mismatch = await detectWorkflowEnvelopeIntegrityMismatch(filePath);
|
|
494
543
|
if (mismatch) {
|
|
@@ -501,6 +550,7 @@ async function collectDoctorSummary(
|
|
|
501
550
|
currentSkill,
|
|
502
551
|
),
|
|
503
552
|
);
|
|
553
|
+
invalidModeStates.add(currentSkill);
|
|
504
554
|
}
|
|
505
555
|
}
|
|
506
556
|
|
|
@@ -553,6 +603,17 @@ async function collectDoctorSummary(
|
|
|
553
603
|
),
|
|
554
604
|
);
|
|
555
605
|
}
|
|
606
|
+
if (canonical && activeFlag(entry.value) && !invalidModeStates.has(canonical)) {
|
|
607
|
+
pushPhaseDriftProblem({
|
|
608
|
+
problems,
|
|
609
|
+
pathValue: entryPath,
|
|
610
|
+
skill: canonical,
|
|
611
|
+
entryKind: "active entry",
|
|
612
|
+
entrySkill,
|
|
613
|
+
entryPhase: phaseFromActiveValue(entry.value),
|
|
614
|
+
statePhase: modeStatePhase(state.value),
|
|
615
|
+
});
|
|
616
|
+
}
|
|
556
617
|
}
|
|
557
618
|
if (isPlainObject(snapshot.value)) {
|
|
558
619
|
const activeSkills = Array.isArray(snapshot.value.active_skills) ? snapshot.value.active_skills : [];
|
|
@@ -572,6 +633,18 @@ async function collectDoctorSummary(
|
|
|
572
633
|
),
|
|
573
634
|
);
|
|
574
635
|
}
|
|
636
|
+
if (canonical && activeFlag(entry) && !invalidModeStates.has(canonical)) {
|
|
637
|
+
const state = await readRawJson(modeStateFile(cwd, canonical, scopeSessionId));
|
|
638
|
+
pushPhaseDriftProblem({
|
|
639
|
+
problems,
|
|
640
|
+
pathValue: snapshotPath,
|
|
641
|
+
skill: canonical,
|
|
642
|
+
entryKind: "active snapshot",
|
|
643
|
+
entrySkill,
|
|
644
|
+
entryPhase: phaseFromActiveValue(entry),
|
|
645
|
+
statePhase: modeStatePhase(state.value),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
575
648
|
}
|
|
576
649
|
}
|
|
577
650
|
};
|