@gajae-code/coding-agent 0.11.5 → 0.11.7
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 +37 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +67 -7
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
- package/dist/types/main.d.ts +24 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-status-header.d.ts +1 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +28 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
- package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
- package/dist/types/session/session-manager.d.ts +35 -4
- package/dist/types/session/yield-queue.d.ts +8 -0
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +16 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +34 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +42 -9
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profiles.ts +18 -0
- package/src/config/settings-schema.ts +71 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/ultragoal-guard.ts +88 -10
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/internal-urls/local-protocol.ts +32 -6
- package/src/main.ts +87 -28
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +31 -3
- package/src/modes/components/tool-status-header.ts +6 -1
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -2
- package/src/modes/controllers/input-controller.ts +31 -0
- package/src/modes/controllers/selector-controller.ts +106 -12
- package/src/modes/interactive-mode.ts +16 -7
- package/src/modes/runtime-init.ts +16 -2
- package/src/modes/types.ts +1 -0
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/transports/http.ts +50 -16
- package/src/sdk/bus/chat-daemon-control.ts +20 -7
- package/src/sdk/bus/index.ts +8 -7
- package/src/sdk/bus/lifecycle-commands.ts +16 -7
- package/src/sdk/bus/notification-service.ts +41 -18
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
- package/src/sdk/bus/telegram-daemon-control.ts +26 -8
- package/src/sdk/bus/telegram-daemon.ts +1736 -338
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +14 -5
- package/src/sdk/protocol/operation-inventory.generated.json +29 -0
- package/src/sdk/session.ts +29 -5
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +1212 -768
- package/src/session/blob-store.ts +19 -6
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +313 -57
- package/src/session/internal/managed-session-storage.ts +146 -35
- package/src/session/session-manager.ts +385 -85
- package/src/session/yield-queue.ts +20 -0
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +37 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/tools/ask-answer-registry.ts +15 -1
- package/src/tools/ask.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +301 -20
- package/src/tools/read.ts +2 -0
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +138 -27
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
package/src/cli.ts
CHANGED
|
@@ -287,9 +287,34 @@ export function normalizeResumeAlias(argv: readonly string[]): string[] {
|
|
|
287
287
|
return argv.length === 1 && argv[0] === "resume" ? ["--resume"] : [...argv];
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
function routeLegacyRootArgv(argv: readonly string[]): string[] | undefined {
|
|
291
|
+
if (argv[0] === "coordinator-mcp") return ["mcp-serve", "coordinator", ...argv.slice(1)];
|
|
292
|
+
if (argv[0] !== "--team") return undefined;
|
|
293
|
+
const sizeValues: string[] = [];
|
|
294
|
+
const remaining: string[] = [];
|
|
295
|
+
for (let index = 1; index < argv.length; index++) {
|
|
296
|
+
const arg = argv[index] ?? "";
|
|
297
|
+
if (arg === "--team-size") {
|
|
298
|
+
sizeValues.push(argv[index + 1] ?? "");
|
|
299
|
+
index++;
|
|
300
|
+
} else if (arg.startsWith("--team-size=")) {
|
|
301
|
+
sizeValues.push(arg.slice("--team-size=".length));
|
|
302
|
+
} else {
|
|
303
|
+
remaining.push(arg);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const size = sizeValues[0];
|
|
307
|
+
if (sizeValues.length !== 1 || !size || !/^[1-9]\d*$/.test(size)) {
|
|
308
|
+
return ["team", "0", "invalid legacy --team-size"];
|
|
309
|
+
}
|
|
310
|
+
return ["team", size, ...remaining];
|
|
311
|
+
}
|
|
312
|
+
|
|
290
313
|
/** Apply the same default-launch routing used by runCli after root fast paths. */
|
|
291
314
|
export function routeRootArgv(argv: readonly string[]): string[] {
|
|
292
315
|
const normalizedArgv = normalizeResumeAlias(argv);
|
|
316
|
+
const legacyArgv = routeLegacyRootArgv(normalizedArgv);
|
|
317
|
+
if (legacyArgv) return legacyArgv;
|
|
293
318
|
const first = normalizedArgv[0];
|
|
294
319
|
return first === "--help" || first === "-h" || first === "--version" || first === "-v" || first === "help"
|
|
295
320
|
? normalizedArgv
|
|
@@ -348,11 +373,9 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
348
373
|
process.exitCode = await runFixtureReport(id);
|
|
349
374
|
return;
|
|
350
375
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
if (hasRootHelpFlag(argv)) {
|
|
376
|
+
const normalizedArgv = normalizeResumeAlias(argv);
|
|
377
|
+
const legacyArgv = routeLegacyRootArgv(normalizedArgv);
|
|
378
|
+
if (!legacyArgv && hasRootHelpFlag(normalizedArgv)) {
|
|
356
379
|
const { renderRootHelp } = await import("@gajae-code/utils/cli");
|
|
357
380
|
const { getExtraHelpText } = await import("./cli/fast-help");
|
|
358
381
|
renderRootHelp({ bin: APP_NAME, version: VERSION, commands: new Map([["launch", RootHelpCommand]]) });
|
|
@@ -362,14 +385,16 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
362
385
|
}
|
|
363
386
|
return;
|
|
364
387
|
}
|
|
365
|
-
if (hasRootVersionFlag(
|
|
388
|
+
if (!legacyArgv && hasRootVersionFlag(normalizedArgv)) {
|
|
366
389
|
process.stdout.write(`${APP_NAME}/${VERSION}\n`);
|
|
367
390
|
return;
|
|
368
391
|
}
|
|
392
|
+
const runArgv = legacyArgv ?? routeRootArgv(normalizedArgv);
|
|
393
|
+
if (isStatsHelpFastPath(runArgv)) {
|
|
394
|
+
showStatsFastHelp();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
369
397
|
await installRuntimeGlobals();
|
|
370
|
-
// --help and --version are handled by run() directly, don't rewrite those.
|
|
371
|
-
// Everything else that isn't a known subcommand routes to "launch".
|
|
372
|
-
const runArgv = routeRootArgv(argv);
|
|
373
398
|
return run({ bin: APP_NAME, version: VERSION, argv: runArgv, commands, help: showHelp });
|
|
374
399
|
}
|
|
375
400
|
|
package/src/commands/daemon.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Manage GJC background daemons (status/list/stop/reload).
|
|
3
3
|
*/
|
|
4
|
-
import { Args, Command, Flags } from "@gajae-code/utils/cli";
|
|
4
|
+
import { Args, CliParseError, Command, Flags } from "@gajae-code/utils/cli";
|
|
5
5
|
import {
|
|
6
6
|
type DaemonCommandAction,
|
|
7
7
|
type DaemonCommandArgs,
|
|
@@ -15,6 +15,18 @@ import { runSdkSessionCli } from "../sdk/cli";
|
|
|
15
15
|
|
|
16
16
|
const ACTIONS = [...DAEMON_ACTION_TOKENS, "discord-internal", "slack-internal", "session"] as const;
|
|
17
17
|
|
|
18
|
+
function parsePositiveTimeout(raw: string | undefined, flagName: string): number | undefined {
|
|
19
|
+
if (raw === undefined) return undefined;
|
|
20
|
+
if (!/^[0-9]+$/.test(raw)) {
|
|
21
|
+
throw new CliParseError(`Expected ${flagName} to be a positive safe integer, got "${raw}"`);
|
|
22
|
+
}
|
|
23
|
+
const value = Number(raw);
|
|
24
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
25
|
+
throw new CliParseError(`Expected ${flagName} to be a positive safe integer, got "${raw}"`);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
export default class Daemon extends Command {
|
|
19
31
|
static description =
|
|
20
32
|
"Manage GJC background daemons and SDK sessions. Routine use: `gjc daemon status` to check, `gjc daemon restart` to reload (spawns one if none is running). `stop`/`list` and the escalation flags below are advanced primitives.";
|
|
@@ -41,8 +53,8 @@ export default class Daemon extends Command {
|
|
|
41
53
|
all: Flags.boolean({ description: "Target all registered daemon kinds" }),
|
|
42
54
|
json: Flags.boolean({ description: "Emit JSON output" }),
|
|
43
55
|
force: Flags.boolean({ description: "Allow hard-kill escalation when graceful stop times out" }),
|
|
44
|
-
"graceful-timeout-ms": Flags.
|
|
45
|
-
"kill-timeout-ms": Flags.
|
|
56
|
+
"graceful-timeout-ms": Flags.string({ description: "Cooperative stop timeout before escalation" }),
|
|
57
|
+
"kill-timeout-ms": Flags.string({ description: "Wait for old pid death after SIGKILL" }),
|
|
46
58
|
"spawn-if-stopped": Flags.boolean({ description: "On reload, spawn even when no daemon is running" }),
|
|
47
59
|
smoke: Flags.boolean({ description: "Internal: run worker smoke without configuration or network" }),
|
|
48
60
|
"owner-id": Flags.string({ description: "Internal: daemon owner id" }),
|
|
@@ -64,6 +76,11 @@ export default class Daemon extends Command {
|
|
|
64
76
|
const rawAction = args.action ?? "status";
|
|
65
77
|
const positional = Array.isArray(args.kind) ? args.kind : args.kind ? [args.kind] : [];
|
|
66
78
|
const flagRec = flags as Record<string, unknown>;
|
|
79
|
+
const gracefulTimeoutMs = parsePositiveTimeout(
|
|
80
|
+
flagRec["graceful-timeout-ms"] as string | undefined,
|
|
81
|
+
"--graceful-timeout-ms",
|
|
82
|
+
);
|
|
83
|
+
const killTimeoutMs = parsePositiveTimeout(flagRec["kill-timeout-ms"] as string | undefined, "--kill-timeout-ms");
|
|
67
84
|
if (rawAction === "session") {
|
|
68
85
|
await runSdkSessionCli({
|
|
69
86
|
action: positional[0],
|
|
@@ -91,8 +108,8 @@ export default class Daemon extends Command {
|
|
|
91
108
|
json: Boolean(flags.json),
|
|
92
109
|
force: Boolean(flags.force),
|
|
93
110
|
verbose: Boolean(flags.verbose),
|
|
94
|
-
gracefulTimeoutMs
|
|
95
|
-
killTimeoutMs
|
|
111
|
+
gracefulTimeoutMs,
|
|
112
|
+
killTimeoutMs,
|
|
96
113
|
spawnIfStopped: flagRec["spawn-if-stopped"] as boolean | undefined,
|
|
97
114
|
smoke: Boolean(flags.smoke),
|
|
98
115
|
ownerId: flagRec["owner-id"] as string | undefined,
|
package/src/commands/sdk.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { Args, Command, Flags } from "@gajae-code/utils/cli";
|
|
4
|
+
import { Args, CliParseError, Command, Flags, renderCommandHelp } from "@gajae-code/utils/cli";
|
|
5
5
|
import type { Args as ParsedArgs } from "../cli/args";
|
|
6
6
|
import { Settings } from "../config/settings";
|
|
7
7
|
import { applyStartupModelProfiles, createSessionManager } from "../main";
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type SdkStartupRollbackResult,
|
|
26
26
|
SdkStartupRollbackTracker,
|
|
27
27
|
} from "../sdk/startup-capability";
|
|
28
|
+
import { runSdkServe } from "../sdk/transport/serve-cli";
|
|
28
29
|
import {
|
|
29
30
|
type CapturedSessionTranscriptSnapshot,
|
|
30
31
|
type ResumeSessionIdentity,
|
|
@@ -423,19 +424,51 @@ export async function runSessionHost(
|
|
|
423
424
|
await new Promise<void>(() => {});
|
|
424
425
|
}
|
|
425
426
|
|
|
427
|
+
export type SdkInternalArgv = { action: "broker-internal"; agentDir: string } | { action: "session-host-internal" };
|
|
428
|
+
|
|
429
|
+
/** Parses the exact private argv contracts used by SDK child-process spawns. */
|
|
430
|
+
export function parseSdkInternalArgv(argv: readonly string[]): SdkInternalArgv {
|
|
431
|
+
if (argv[0] === "session-host-internal" && argv.length === 1) return { action: "session-host-internal" };
|
|
432
|
+
if (argv[0] === "broker-internal" && argv.length === 3 && argv[1] === "--agent-dir" && argv[2])
|
|
433
|
+
return { action: "broker-internal", agentDir: argv[2] };
|
|
434
|
+
throw new CliParseError("Invalid internal SDK invocation.");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
class SdkServeHelp extends Command {
|
|
438
|
+
static description = "gjc sdk serve --stdio | --socket <path> [--session <id>] [--pending-ceiling <bytes>]";
|
|
439
|
+
static flags = {
|
|
440
|
+
stdio: Flags.boolean({ description: "Serve SDK frames over standard input and output" }),
|
|
441
|
+
socket: Flags.string({ description: "Serve SDK frames over a Unix socket path" }),
|
|
442
|
+
session: Flags.string({ description: "Attach to a specific SDK session" }),
|
|
443
|
+
"pending-ceiling": Flags.string({ description: "Maximum queued relay bytes per direction" }),
|
|
444
|
+
};
|
|
445
|
+
async run(): Promise<void> {}
|
|
446
|
+
}
|
|
447
|
+
|
|
426
448
|
export default class Sdk extends Command {
|
|
427
|
-
static description = "
|
|
428
|
-
static hidden =
|
|
429
|
-
static
|
|
430
|
-
static
|
|
449
|
+
static description = "gjc sdk serve --stdio | --socket <path> [--session <id>] [--pending-ceiling <bytes>]";
|
|
450
|
+
static hidden = false;
|
|
451
|
+
static delegateHelp = true;
|
|
452
|
+
static args = { action: Args.string({ required: false, options: ["serve"] }) };
|
|
453
|
+
static flags = SdkServeHelp.flags;
|
|
431
454
|
async run(): Promise<void> {
|
|
432
|
-
const
|
|
433
|
-
if (
|
|
455
|
+
const action = this.argv[0];
|
|
456
|
+
if (this.argv.includes("--help") || this.argv.includes("-h")) {
|
|
457
|
+
renderCommandHelp("gjc", action === "serve" ? "sdk serve" : "sdk", action === "serve" ? SdkServeHelp : Sdk);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (action === "serve") {
|
|
461
|
+
await runSdkServe(this.argv.slice(1));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (action !== "broker-internal" && action !== "session-host-internal")
|
|
465
|
+
throw new CliParseError("Expected action to be serve.");
|
|
466
|
+
const internal = parseSdkInternalArgv(this.argv);
|
|
467
|
+
if (internal.action === "session-host-internal") {
|
|
434
468
|
await runSessionHost();
|
|
435
469
|
return;
|
|
436
470
|
}
|
|
437
|
-
const agentDir =
|
|
438
|
-
if (!agentDir) throw new Error("--agent-dir is required for sdk broker-internal.");
|
|
471
|
+
const agentDir = internal.agentDir;
|
|
439
472
|
const broker = new Broker({
|
|
440
473
|
agentDir,
|
|
441
474
|
resolveDirectoryMigration: async cwd => {
|
|
@@ -257,6 +257,20 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
257
257
|
critic: "minimax-code/minimax-m3:xhigh",
|
|
258
258
|
architect: "minimax-code/minimax-m3:xhigh",
|
|
259
259
|
}),
|
|
260
|
+
profile("alibaba-token-plan-balanced", ["alibaba-token-plan"], {
|
|
261
|
+
default: "alibaba-token-plan/qwen3.8-max-preview:medium",
|
|
262
|
+
executor: "alibaba-token-plan/deepseek-v4-pro:xhigh",
|
|
263
|
+
planner: "alibaba-token-plan/glm-5.2:high",
|
|
264
|
+
architect: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
|
|
265
|
+
critic: "alibaba-token-plan/glm-5.2:high",
|
|
266
|
+
}),
|
|
267
|
+
profile("alibaba-token-plan-qwenmaxxing", ["alibaba-token-plan"], {
|
|
268
|
+
default: "alibaba-token-plan/qwen3.8-max-preview:medium",
|
|
269
|
+
executor: "alibaba-token-plan/qwen3.8-max-preview:low",
|
|
270
|
+
planner: "alibaba-token-plan/qwen3.8-max-preview:medium",
|
|
271
|
+
architect: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
|
|
272
|
+
critic: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
|
|
273
|
+
}),
|
|
260
274
|
profile("opus-codex", ["anthropic", "openai-codex"], {
|
|
261
275
|
default: "anthropic/claude-opus-4-8:xhigh",
|
|
262
276
|
executor: "openai-codex/gpt-5.6-terra:low",
|
|
@@ -315,6 +329,8 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
|
|
|
315
329
|
"minimax-eco": { displayName: "MiniMax Eco", providerGroup: "MINIMAX" },
|
|
316
330
|
"minimax-medium": { displayName: "MiniMax Medium", providerGroup: "MINIMAX" },
|
|
317
331
|
"minimax-pro": { displayName: "MiniMax Pro", providerGroup: "MINIMAX" },
|
|
332
|
+
"alibaba-token-plan-balanced": { displayName: "Balanced", providerGroup: "ALIBABA TOKEN PLAN" },
|
|
333
|
+
"alibaba-token-plan-qwenmaxxing": { displayName: "QwenMaxxing", providerGroup: "ALIBABA TOKEN PLAN" },
|
|
318
334
|
"opus-codex": { displayName: "Opus + Codex", providerGroup: "COMBOS" },
|
|
319
335
|
"codex-opencodego": { displayName: "Codex + OpenCodeGo", providerGroup: "COMBOS" },
|
|
320
336
|
"fable-opus-codex": { displayName: "Fable + Opus + Codex", providerGroup: "COMBOS" },
|
|
@@ -330,6 +346,7 @@ const PROFILE_GROUP_ORDER = [
|
|
|
330
346
|
"GROK",
|
|
331
347
|
"CURSOR",
|
|
332
348
|
"MINIMAX",
|
|
349
|
+
"ALIBABA TOKEN PLAN",
|
|
333
350
|
"COMBOS",
|
|
334
351
|
];
|
|
335
352
|
|
|
@@ -347,6 +364,7 @@ const PROFILE_RECOMMENDATIONS: Record<string, string> = {
|
|
|
347
364
|
"grok-build": "grok-build-pro",
|
|
348
365
|
cursor: "cursor-medium",
|
|
349
366
|
"minimax-code": "minimax-medium",
|
|
367
|
+
"alibaba-token-plan": "alibaba-token-plan-balanced",
|
|
350
368
|
};
|
|
351
369
|
|
|
352
370
|
export function getModelProfilePresentation(
|
|
@@ -819,6 +819,15 @@ export const SETTINGS_SCHEMA = {
|
|
|
819
819
|
type: "boolean",
|
|
820
820
|
default: true,
|
|
821
821
|
},
|
|
822
|
+
"statusLine.showActionHints": {
|
|
823
|
+
type: "boolean",
|
|
824
|
+
default: true,
|
|
825
|
+
ui: {
|
|
826
|
+
tab: "appearance",
|
|
827
|
+
label: "Status Line Action Hints",
|
|
828
|
+
description: "Show contextual keyboard shortcuts in the status line",
|
|
829
|
+
},
|
|
830
|
+
},
|
|
822
831
|
|
|
823
832
|
"statusLine.leftSegments": { type: "array", default: [] as StatusLineSegmentId[] },
|
|
824
833
|
|
|
@@ -1566,7 +1575,19 @@ export const SETTINGS_SCHEMA = {
|
|
|
1566
1575
|
ui: {
|
|
1567
1576
|
tab: "context",
|
|
1568
1577
|
label: "Save Handoff Docs",
|
|
1569
|
-
description:
|
|
1578
|
+
description:
|
|
1579
|
+
"Save auto-triggered handoff documents as session artifacts (resolvable artifact:// URIs); manual /handoff does not save",
|
|
1580
|
+
},
|
|
1581
|
+
},
|
|
1582
|
+
|
|
1583
|
+
"compaction.handoffPromptExtension": {
|
|
1584
|
+
type: "string",
|
|
1585
|
+
default: "",
|
|
1586
|
+
ui: {
|
|
1587
|
+
tab: "context",
|
|
1588
|
+
label: "Handoff Prompt Extension",
|
|
1589
|
+
description:
|
|
1590
|
+
"Extra guidance appended to the default handoff-generation prompt for both manual /handoff and auto-handoff. It supplements, and never replaces, the built-in safety- and continuity-critical instructions.",
|
|
1570
1591
|
},
|
|
1571
1592
|
},
|
|
1572
1593
|
|
|
@@ -2439,9 +2460,9 @@ export const SETTINGS_SCHEMA = {
|
|
|
2439
2460
|
default: false,
|
|
2440
2461
|
ui: {
|
|
2441
2462
|
tab: "tools",
|
|
2442
|
-
label: "Insane Search Fallback",
|
|
2463
|
+
label: "Insane Search Fallback (Compatibility)",
|
|
2443
2464
|
description:
|
|
2444
|
-
"
|
|
2465
|
+
"Compatibility-only preference. Remote renderer fallback stays disabled because it cannot preserve validated per-hop network routing.",
|
|
2445
2466
|
},
|
|
2446
2467
|
},
|
|
2447
2468
|
|
|
@@ -3278,25 +3299,66 @@ export const SETTINGS_SCHEMA = {
|
|
|
3278
3299
|
},
|
|
3279
3300
|
"providers.image": {
|
|
3280
3301
|
type: "enum",
|
|
3281
|
-
values: ["auto", "openai", "gemini", "openrouter", "antigravity"] as const,
|
|
3302
|
+
values: ["auto", "openai", "gemini", "openrouter", "antigravity", "custom"] as const,
|
|
3282
3303
|
default: "auto",
|
|
3283
3304
|
ui: {
|
|
3284
3305
|
tab: "providers",
|
|
3285
|
-
label: "Image
|
|
3286
|
-
description: "Provider for image generation tool",
|
|
3306
|
+
label: "Image Generation",
|
|
3307
|
+
description: "Provider and model for image generation tool",
|
|
3287
3308
|
options: [
|
|
3288
3309
|
{
|
|
3289
3310
|
value: "auto",
|
|
3290
3311
|
label: "Auto",
|
|
3291
3312
|
description: "Priority: GPT model image tool > Antigravity > OpenRouter > Gemini",
|
|
3292
3313
|
},
|
|
3293
|
-
{ value: "openai", label: "OpenAI", description: "Uses
|
|
3314
|
+
{ value: "openai", label: "OpenAI", description: "Uses gpt-image-2 via OpenAI Responses/Codex" },
|
|
3294
3315
|
{ value: "gemini", label: "Gemini", description: "Requires GEMINI_API_KEY" },
|
|
3295
3316
|
{ value: "openrouter", label: "OpenRouter", description: "Requires OPENROUTER_API_KEY" },
|
|
3296
3317
|
{ value: "antigravity", label: "Antigravity", description: "Requires login with google-antigravity" },
|
|
3318
|
+
{
|
|
3319
|
+
value: "custom",
|
|
3320
|
+
label: "Custom",
|
|
3321
|
+
description: "OpenAI-compatible endpoint (set providers.imageCustomUrl)",
|
|
3322
|
+
},
|
|
3297
3323
|
],
|
|
3298
3324
|
},
|
|
3299
3325
|
},
|
|
3326
|
+
"providers.imageModel": {
|
|
3327
|
+
type: "string",
|
|
3328
|
+
default: undefined,
|
|
3329
|
+
ui: {
|
|
3330
|
+
tab: "providers",
|
|
3331
|
+
label: "Image Model",
|
|
3332
|
+
description: "Override the default image generation model for the selected provider",
|
|
3333
|
+
},
|
|
3334
|
+
},
|
|
3335
|
+
"providers.imageCustomUrl": {
|
|
3336
|
+
type: "string",
|
|
3337
|
+
default: undefined,
|
|
3338
|
+
ui: {
|
|
3339
|
+
tab: "providers",
|
|
3340
|
+
label: "Image Custom URL",
|
|
3341
|
+
description: "Base URL for custom OpenAI-compatible image endpoint",
|
|
3342
|
+
},
|
|
3343
|
+
},
|
|
3344
|
+
"providers.imageCustomKey": {
|
|
3345
|
+
type: "string",
|
|
3346
|
+
default: undefined,
|
|
3347
|
+
ui: {
|
|
3348
|
+
tab: "providers",
|
|
3349
|
+
label: "Image Custom API Key",
|
|
3350
|
+
description: "API key for custom OpenAI-compatible image endpoint",
|
|
3351
|
+
},
|
|
3352
|
+
},
|
|
3353
|
+
"providers.imageCustomKeyEnv": {
|
|
3354
|
+
type: "string",
|
|
3355
|
+
default: undefined,
|
|
3356
|
+
ui: {
|
|
3357
|
+
tab: "providers",
|
|
3358
|
+
label: "Image Custom API Key Env",
|
|
3359
|
+
description: "Environment variable name holding the API key for custom image endpoint",
|
|
3360
|
+
},
|
|
3361
|
+
},
|
|
3300
3362
|
|
|
3301
3363
|
"providers.kimiApiFormat": {
|
|
3302
3364
|
type: "enum",
|
|
@@ -3643,6 +3705,7 @@ export interface CompactionSettings {
|
|
|
3643
3705
|
reserveTokens: number;
|
|
3644
3706
|
keepRecentTokens: number;
|
|
3645
3707
|
handoffSaveToDisk: boolean;
|
|
3708
|
+
handoffPromptExtension: string;
|
|
3646
3709
|
autoContinue: boolean;
|
|
3647
3710
|
remoteEnabled: boolean;
|
|
3648
3711
|
remoteEndpoint: string | undefined;
|
|
@@ -3727,6 +3790,7 @@ export interface StatusLineSettings {
|
|
|
3727
3790
|
maxRows: number;
|
|
3728
3791
|
showHookStatus: boolean;
|
|
3729
3792
|
showSkillHud: boolean;
|
|
3793
|
+
showActionHints: boolean;
|
|
3730
3794
|
leftSegments: StatusLineSegmentId[];
|
|
3731
3795
|
rightSegments: StatusLineSegmentId[];
|
|
3732
3796
|
segmentOptions: Record<string, unknown>;
|
|
@@ -123,10 +123,12 @@ An active Ultragoal run must not give up on a blocker by pausing the goal and as
|
|
|
123
123
|
- **`resolvable`** — anything the agent can act on: failing tests, missing implementation, a dependency to install, an ambiguous-but-inferable detail, investigation. **Never pause.** Exhaust autonomous resolution first: investigate, `gjc ultragoal steer --kind add_subgoal --title "Investigate blocker" --objective "..." --evidence "..." --rationale "..."`, delegate an `executor`, or preserve the blocker durably with `gjc ultragoal checkpoint --status blocked` / `gjc ultragoal record-review-blockers` and keep scheduling the next goal.
|
|
124
124
|
- **`human_blocked`** — only the user can act: credentials/secrets, a manual or physical step, an external approval/decision, access the agent lacks. Pause is the last resort and is gated.
|
|
125
125
|
|
|
126
|
-
`goal({"op":"pause"})` is **blocked at runtime** while an Ultragoal run is active unless the latest
|
|
126
|
+
`goal({"op":"pause"})` is **blocked at runtime** while an Ultragoal run is active unless the latest `blocker_classified` ledger event is `human_blocked` and a later bound clean pause terminal critic verdict is recorded for it (see [Terminal critic gate](#terminal-critic-gate)). `assertUltragoalPauseAllowed` first consumes a pre-existing give-up nudge (a durable ledger write) before it runs the read-only pause diagnostic; only `isUltragoalPauseBlocked` is a pure reader. To pause, first record the human-only classification and capture its event id, then record the terminal critic's clean bound pause verdict, and only then pause:
|
|
127
127
|
|
|
128
128
|
```sh
|
|
129
129
|
gjc ultragoal classify-blocker --classification human_blocked --evidence "<the specific human-only dependency>" [--goal-id <id>]
|
|
130
|
+
gjc ultragoal record-critic-verdict --terminus pause --classification-event-id <eventId> --verdict OKAY --evidence "<terminal critic evidence>"
|
|
131
|
+
goal({"op":"pause"})
|
|
130
132
|
```
|
|
131
133
|
|
|
132
134
|
Recording `--classification resolvable` is an audit note only; it never authorizes a pause. The `ask` tool stays blocked during active runs regardless of classification — record unresolved decisions as durable blockers instead of prompting.
|
|
@@ -343,6 +345,44 @@ Provide one `artifactRefs` entry per live surface actually exercised, using the
|
|
|
343
345
|
|
|
344
346
|
For CLI replay artifacts, the JSON at `path` must be an object like `{"schemaVersion":1,"kind":"cli-replay","replaySafe":true,"command":["bun","-e","console.log(\"ultragoal-cli-ok\")"],"cwd":".","env":{"LC_ALL":"C"},"timeoutMs":30000,"expectedExitCode":0,"recordedStdout":"ultragoal-cli-ok\n","recordedStderr":"","invariants":[{"type":"substring","value":"ultragoal-cli-ok"},{"type":"not-substring","value":"error"}]}`. Accepted replay fields are `command` (string array), optional `cwd`, safe `env`, `timeoutMs`, `expectedExitCode`, `recordedStdout`, `recordedStderr`, `normalization`, and `invariants`. The conservative command allowlist is intentionally small: `bun --version`, `node --version`, deterministic `bun/node -e "console.log(...)"`, `npm|pnpm|yarn --version`, `npm|pnpm|yarn list`, read-only `git status|rev-parse|merge-base|diff|show|log` with safe args, and `gjc read|status`. `env` must contain only safe deterministic variables, never credentials or machine/user-specific secrets. `normalization` is optional and, when provided, must be exactly the string `"default"` (the built-in normalizer already strips ANSI codes, normalizes line endings, scrubs paths, and trims trailing whitespace); object-shaped normalization is rejected. Invariants may be substring, regex, or not-substring checks; when present, they replace exact `recordedStdout` equality — without `invariants`, replayed normalized stdout must match `recordedStdout` exactly. Unsafe, non-deterministic, credentialed, interactive, or otherwise unallowlisted commands require audited `replayExempt` metadata with exact fields `reasonCode`, `reason`, `approvedBy`, and `fallbackArtifactRefs` plus a structurally valid same-surface fallback artifact. `reason` must be substantive and audited, and `approvedBy` must identify the verifier. Allowed `reasonCode` values are exactly `unsafe_side_effect`, `requires_credentials`, `requires_network`, `non_deterministic_external`, `destructive`, `interactive_only`, and `platform_unavailable`.
|
|
345
347
|
|
|
348
|
+
## Terminal critic gate
|
|
349
|
+
|
|
350
|
+
The terminal critic gate is a fail-closed, once-per-run-terminus review. It guards both terminal exits with a read-only `critic` role agent's `OKAY` verdict; it does not run per story. It is additive to, and does not change, the existing per-story `architect` review and `executor` QA/red-team lanes.
|
|
351
|
+
|
|
352
|
+
### Completion terminus
|
|
353
|
+
|
|
354
|
+
Before assembling the final-aggregate `--quality-gate-json`, the leader delegates the terminal critic. Only the final-aggregate completion checkpoint requires the additional top-level `criticReview` key; `criticReview` is tolerated but ignored on non-final checkpoints. A clean final aggregate requires `verdict: "OKAY"`, non-empty `evidence`, and an empty `blockers` array:
|
|
355
|
+
|
|
356
|
+
```json
|
|
357
|
+
{
|
|
358
|
+
"criticReview": {
|
|
359
|
+
"verdict": "OKAY",
|
|
360
|
+
"evidence": "terminal critic review of the final required-goal state",
|
|
361
|
+
"blockers": []
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Pause/blocked terminus
|
|
367
|
+
|
|
368
|
+
At a `human_blocked` terminus, the leader first runs `gjc ultragoal classify-blocker --classification human_blocked` (capturing that classification's ledger `eventId`), then delegates the terminal critic and records its verdict with `gjc ultragoal record-critic-verdict --terminus pause --classification-event-id <eventId>` before calling `goal({"op":"pause"})`. The pause is allowed only when a later fresh `critic_verdict` ledger receipt exists with `terminus: "pause"`, `verdict: "OKAY"`, non-empty evidence, an empty blockers array, the current `planGeneration`, and a `classificationEventId` bound to the latest `blocker_classified` event, which must be `human_blocked`. Freshness is scoped to the final required-goal state, so required-goal or steer changes stale the receipt, and a newer classification supersedes an older verdict.
|
|
369
|
+
|
|
370
|
+
The critic must verify that the `human_blocked` classification is genuine, including catching false pauses where needed resources exist locally or the asserted blocker is resolvable. A `REJECT` (or `ITERATE`) verdict refuses the terminal pause; the run keeps executing. The pause (`goal({"op":"pause"})`) is the gated terminal park-and-wait exit — a per-goal `gjc ultragoal checkpoint --status blocked` remains available as non-terminal blocker bookkeeping that never signals run completion and keeps the blocker outstanding until resolved.
|
|
371
|
+
|
|
372
|
+
### Invocation and containment
|
|
373
|
+
|
|
374
|
+
At each terminus, the leader gives the read-only `critic` role agent `brief.md`, `goals.json`, `ledger.jsonl`, and the cumulative change set. For completion, invoke it before assembling the final-aggregate gate JSON. For pause, invoke it after the `human_blocked` classification and before `goal({"op":"pause"})`. The terminal critic must not spawn nested `ralplan`, `team`, `deep-interview`, or `ultragoal` workflows. This creates no interactive surface: `ask` remains blocked while an Ultragoal run is active.
|
|
375
|
+
|
|
376
|
+
### Non-OKAY loop and ceiling
|
|
377
|
+
|
|
378
|
+
For completion-side `ITERATE` or `REJECT`, the leader MUST first record the terminal verdict so the run-level counter observes it: `gjc ultragoal record-critic-verdict --terminus completion --verdict <ITERATE|REJECT> --evidence "<critic findings>"`; then record the findings with `gjc ultragoal record-review-blockers` and reopen the run. The dedicated counter ceiling is 5, independently of the give-up nudge budget, and is **RUN-LEVEL**: it counts every non-OKAY terminal-critic verdict across the whole run and all reopen cycles. On reaching that ceiling, both pause and final completion are blocked until a human or leader records `gjc ultragoal record-critic-gate-override --evidence "<authorization evidence>"`. There is no automatic pause override.
|
|
379
|
+
|
|
380
|
+
This gate is always fail-closed and has no grandfathering: in-flight runs must obtain a terminal verdict when they reach a terminus.
|
|
381
|
+
|
|
382
|
+
#### Deferred / out of scope
|
|
383
|
+
|
|
384
|
+
Gating active-aggregate `goal drop` after nudge exhaustion is a known follow-up not covered by this gate; `drop` remains governed by the existing nudge discipline.
|
|
385
|
+
|
|
346
386
|
## Review mode
|
|
347
387
|
|
|
348
388
|
`gjc ultragoal review` runs the same hardened gate against an already implemented PR, branch, or worktree. Use `--pr <number>` for a PR, `--branch <ref>` for a branch diff, omit both for the current worktree, and pass `--spec <path>` when a real contract exists. `--mode review-only` emits the verdict/findings without creating fix work; `--mode review-start` records review blockers for follow-up. Review mode validates the same `executorQa` shape and live-surface artifacts as `checkpoint --status complete`. A thin or derived-only contract can never clean-pass: the verdict is capped at `inconclusive: weak-contract` until a supplied spec or equivalent strong acceptance criteria are available.
|
|
@@ -372,4 +412,5 @@ The skill tool then dispatches `/skill:ralplan` or `/skill:deep-interview` same-
|
|
|
372
412
|
- Never call `goal({"op":"complete"})` unless the aggregate run or legacy per-story goal is actually complete.
|
|
373
413
|
- In aggregate mode, intermediate and final story checkpoints update durable `goals.json` state and append receipt proof to `ledger.jsonl`; the final story checkpoint creates the final aggregate receipt before the agent may call `goal({"op":"complete"})`.
|
|
374
414
|
- Completion checkpoints require `--quality-gate-json` only. Shell commands and hooks must not mutate goal state; the agent reconciles inline goal-tool state after durable completion.
|
|
415
|
+
- Final-aggregate completion additionally requires a `criticReview` `OKAY`; a `human_blocked` pause additionally requires a fresh `OKAY` `critic_verdict` receipt.
|
|
375
416
|
- Treat `ledger.jsonl` as the durable audit trail; checkpoint after every success or failure.
|
package/src/discovery/ssh.ts
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* Priority: 5 (low, project/user config discovery)
|
|
6
6
|
*/
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
-
import { getSSHConfigPath, tryParseJson } from "@gajae-code/utils";
|
|
8
|
+
import { getSSHConfigPath, logger, tryParseJson } from "@gajae-code/utils";
|
|
9
9
|
import { registerProvider } from "../capability";
|
|
10
10
|
import { readFile } from "../capability/fs";
|
|
11
11
|
import { type SSHHost, sshCapability } from "../capability/ssh";
|
|
12
12
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
13
|
+
import { validateSshDestination } from "../ssh/utils";
|
|
13
14
|
import { expandTilde } from "../tools/path-utils";
|
|
14
15
|
import { createSourceMeta, expandEnvVarsDeep } from "./helpers";
|
|
15
16
|
|
|
@@ -58,6 +59,16 @@ function normalizeHost(
|
|
|
58
59
|
warnings.push(`Missing host for SSH entry: ${name}`);
|
|
59
60
|
return null;
|
|
60
61
|
}
|
|
62
|
+
const destinationError = validateSshDestination(raw.username, raw.host);
|
|
63
|
+
if (destinationError) {
|
|
64
|
+
warnings.push(`Invalid destination for SSH entry ${name}: ${destinationError}`);
|
|
65
|
+
logger.warn("Ignoring SSH entry with invalid destination", {
|
|
66
|
+
name,
|
|
67
|
+
path: source.path,
|
|
68
|
+
reason: destinationError,
|
|
69
|
+
});
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
61
72
|
|
|
62
73
|
const port = parsePort(raw.port);
|
|
63
74
|
if (raw.port !== undefined && port === undefined) {
|
package/src/eval/py/kernel.ts
CHANGED
|
@@ -8,9 +8,6 @@
|
|
|
8
8
|
* Shutdown writes `{"type":"exit"}` and escalates to SIGTERM/SIGKILL on
|
|
9
9
|
* timeout.
|
|
10
10
|
*/
|
|
11
|
-
import * as fs from "node:fs";
|
|
12
|
-
import * as os from "node:os";
|
|
13
|
-
import * as path from "node:path";
|
|
14
11
|
import { $env, isBunTestRuntime, logger, Snowflake } from "@gajae-code/utils";
|
|
15
12
|
import type { Subprocess } from "bun";
|
|
16
13
|
import { $ } from "bun";
|
|
@@ -18,7 +15,7 @@ import { Settings } from "../../config/settings";
|
|
|
18
15
|
import { type KernelDisplayOutput, renderKernelDisplay } from "./display";
|
|
19
16
|
import { resolvePythonIpcTrace, resolvePythonSkipCheck } from "./env";
|
|
20
17
|
import { PYTHON_PRELUDE } from "./prelude";
|
|
21
|
-
import
|
|
18
|
+
import { ensureRunnerScript } from "./runner-artifact";
|
|
22
19
|
import { ensurePythonRuntime, filterEnv, type PythonRuntimeOptions } from "./runtime";
|
|
23
20
|
|
|
24
21
|
export type { KernelDisplayOutput, PythonStatusEvent } from "./display";
|
|
@@ -27,23 +24,6 @@ export { renderKernelDisplay } from "./display";
|
|
|
27
24
|
// Dual-read: `GJC_PYTHON_IPC_TRACE` is preferred, then legacy `PI_PYTHON_IPC_TRACE`.
|
|
28
25
|
const TRACE_IPC = resolvePythonIpcTrace($env);
|
|
29
26
|
|
|
30
|
-
// Cache the runner script on disk so the subprocess loads it normally. Cached
|
|
31
|
-
// per script hash so installs don't race across versions.
|
|
32
|
-
const RUNNER_CACHE_DIR = path.join(os.tmpdir(), "gjc-python-runner");
|
|
33
|
-
let RUNNER_SCRIPT_PATH: string | null = null;
|
|
34
|
-
|
|
35
|
-
async function ensureRunnerScript(): Promise<string> {
|
|
36
|
-
if (RUNNER_SCRIPT_PATH) return RUNNER_SCRIPT_PATH;
|
|
37
|
-
await fs.promises.mkdir(RUNNER_CACHE_DIR, { recursive: true });
|
|
38
|
-
const hash = Bun.hash(RUNNER_SCRIPT).toString(36);
|
|
39
|
-
const target = path.join(RUNNER_CACHE_DIR, `runner-${hash}.py`);
|
|
40
|
-
if (!fs.existsSync(target)) {
|
|
41
|
-
await Bun.write(target, RUNNER_SCRIPT);
|
|
42
|
-
}
|
|
43
|
-
RUNNER_SCRIPT_PATH = target;
|
|
44
|
-
return target;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
27
|
const SHUTDOWN_GRACE_MS = 1_000;
|
|
48
28
|
const STARTUP_TIMEOUT_MS = 10_000;
|
|
49
29
|
// How long to wait after SIGINT for the runner to emit `done`. If the cell is
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { postmortem } from "@gajae-code/utils";
|
|
5
|
+
import RUNNER_SCRIPT from "./runner.py" with { type: "text" };
|
|
6
|
+
|
|
7
|
+
const DIRECTORY_MODE = 0o700;
|
|
8
|
+
const FILE_MODE = 0o600;
|
|
9
|
+
const DIRECTORY_PREFIX = "gjc-python-runner-";
|
|
10
|
+
|
|
11
|
+
interface RunnerScriptArtifact {
|
|
12
|
+
directory: string;
|
|
13
|
+
scriptPath: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function initializeRunnerScript(tempRoot: string): Promise<RunnerScriptArtifact> {
|
|
17
|
+
const directory = await fs.mkdtemp(path.join(tempRoot, DIRECTORY_PREFIX));
|
|
18
|
+
try {
|
|
19
|
+
if (process.platform !== "win32") await fs.chmod(directory, DIRECTORY_MODE);
|
|
20
|
+
const scriptPath = path.join(directory, "runner.py");
|
|
21
|
+
const handle = await fs.open(scriptPath, "wx", FILE_MODE);
|
|
22
|
+
try {
|
|
23
|
+
await handle.writeFile(RUNNER_SCRIPT, { encoding: "utf8" });
|
|
24
|
+
if (process.platform !== "win32") await handle.chmod(FILE_MODE);
|
|
25
|
+
} finally {
|
|
26
|
+
await handle.close();
|
|
27
|
+
}
|
|
28
|
+
return { directory, scriptPath };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
await fs.rm(directory, { recursive: true, force: true }).catch(() => {});
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RunnerScriptCache {
|
|
36
|
+
ensureRunnerScript(): Promise<string>;
|
|
37
|
+
cleanup(): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createRunnerScriptCache(tempRoot: string): RunnerScriptCache {
|
|
41
|
+
let initialization: Promise<RunnerScriptArtifact> | null = null;
|
|
42
|
+
let artifact: RunnerScriptArtifact | null = null;
|
|
43
|
+
let cleanupAttempt: Promise<void> | null = null;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
async ensureRunnerScript() {
|
|
47
|
+
if (artifact) return artifact.scriptPath;
|
|
48
|
+
if (!initialization) initialization = initializeRunnerScript(tempRoot);
|
|
49
|
+
try {
|
|
50
|
+
artifact = await initialization;
|
|
51
|
+
return artifact.scriptPath;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (initialization) initialization = null;
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
async cleanup() {
|
|
58
|
+
if (cleanupAttempt) return await cleanupAttempt;
|
|
59
|
+
cleanupAttempt = (async () => {
|
|
60
|
+
const initialized = await initialization?.catch(() => null);
|
|
61
|
+
const directory = artifact?.directory ?? initialized?.directory;
|
|
62
|
+
artifact = null;
|
|
63
|
+
initialization = null;
|
|
64
|
+
if (directory) await fs.rm(directory, { recursive: true, force: true });
|
|
65
|
+
})();
|
|
66
|
+
try {
|
|
67
|
+
await cleanupAttempt;
|
|
68
|
+
} finally {
|
|
69
|
+
cleanupAttempt = null;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @internal Test-only initializer for isolated artifact assertions. */
|
|
76
|
+
export function createRunnerScriptInitializer(tempRoot: string): () => Promise<string> {
|
|
77
|
+
return createRunnerScriptCache(tempRoot).ensureRunnerScript;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const processRunnerScriptCache = createRunnerScriptCache(os.tmpdir());
|
|
81
|
+
|
|
82
|
+
export const ensureRunnerScript = () => processRunnerScriptCache.ensureRunnerScript();
|
|
83
|
+
|
|
84
|
+
postmortem.register("python-runner-artifact", () => processRunnerScriptCache.cleanup());
|