@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/dap/client.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import { logger } from "@gajae-code/utils";
|
|
2
4
|
import { formatCrashDiagnosticNotice, writeCrashReport } from "../debug/crash-diagnostics";
|
|
3
5
|
import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
|
|
6
|
+
import { type OwnedProcess, spawnOwnedProcess } from "../runtime/process-lifecycle";
|
|
4
7
|
import { ToolAbortError } from "../tools/tool-errors";
|
|
5
8
|
import type {
|
|
6
9
|
DapCapabilities,
|
|
@@ -69,10 +72,21 @@ function toErrorMessage(value: unknown): string {
|
|
|
69
72
|
return String(value);
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
async function drainReadable(readable: ReadableStream<Uint8Array>): Promise<void> {
|
|
76
|
+
const reader = readable.getReader();
|
|
77
|
+
try {
|
|
78
|
+
while (!(await reader.read()).done) {}
|
|
79
|
+
} catch {
|
|
80
|
+
/* drain best-effort */
|
|
81
|
+
} finally {
|
|
82
|
+
reader.releaseLock();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
72
85
|
export class DapClient {
|
|
73
86
|
readonly adapter: DapResolvedAdapter;
|
|
74
87
|
readonly cwd: string;
|
|
75
88
|
readonly proc: DapClientState["proc"];
|
|
89
|
+
readonly #owner: OwnedProcess;
|
|
76
90
|
/** ReadableStream of DAP bytes — from proc.stdout (stdio) or a socket (socket mode). */
|
|
77
91
|
readonly #readable: ReadableStream<Uint8Array>;
|
|
78
92
|
/** Write sink — proc.stdin (stdio) or a socket (socket mode). */
|
|
@@ -93,14 +107,15 @@ export class DapClient {
|
|
|
93
107
|
constructor(
|
|
94
108
|
adapter: DapResolvedAdapter,
|
|
95
109
|
cwd: string,
|
|
96
|
-
|
|
110
|
+
owner: OwnedProcess,
|
|
97
111
|
options?: { readable?: ReadableStream<Uint8Array>; writeSink?: DapWriteSink; socket?: { end(): void } },
|
|
98
112
|
) {
|
|
99
113
|
this.adapter = adapter;
|
|
100
114
|
this.cwd = cwd;
|
|
101
|
-
this.proc = proc;
|
|
102
|
-
this.#
|
|
103
|
-
this.#
|
|
115
|
+
this.proc = owner.child as DapClientState["proc"];
|
|
116
|
+
this.#owner = owner;
|
|
117
|
+
this.#readable = options?.readable ?? (this.proc.stdout as ReadableStream<Uint8Array>);
|
|
118
|
+
this.#writeSink = options?.writeSink ?? this.proc.stdin;
|
|
104
119
|
this.#socket = options?.socket;
|
|
105
120
|
}
|
|
106
121
|
|
|
@@ -116,13 +131,14 @@ export class DapClient {
|
|
|
116
131
|
...Bun.env,
|
|
117
132
|
...NON_INTERACTIVE_ENV,
|
|
118
133
|
};
|
|
119
|
-
const
|
|
134
|
+
const owner = spawnOwnedProcess([adapter.resolvedCommand, ...adapter.args], {
|
|
120
135
|
cwd,
|
|
121
136
|
stdin: "pipe",
|
|
122
137
|
env,
|
|
123
|
-
|
|
138
|
+
name: `dap:${adapter.name}`,
|
|
124
139
|
});
|
|
125
|
-
const client = new DapClient(adapter, cwd,
|
|
140
|
+
const client = new DapClient(adapter, cwd, owner);
|
|
141
|
+
const proc = owner.child as DapClientState["proc"];
|
|
126
142
|
proc.exited.then(() => {
|
|
127
143
|
client.#handleProcessExit();
|
|
128
144
|
});
|
|
@@ -159,32 +175,40 @@ export class DapClient {
|
|
|
159
175
|
env: Record<string, string | undefined>;
|
|
160
176
|
}): Promise<DapClient> {
|
|
161
177
|
const socketPath = `/tmp/dap-${adapter.name}-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`;
|
|
162
|
-
const
|
|
178
|
+
const owner = spawnOwnedProcess([adapter.resolvedCommand, ...adapter.args, `--listen=unix:${socketPath}`], {
|
|
163
179
|
cwd,
|
|
164
180
|
stdin: "pipe",
|
|
165
181
|
env,
|
|
166
|
-
|
|
182
|
+
name: `dap:${adapter.name}:unix-socket`,
|
|
167
183
|
});
|
|
184
|
+
const proc = owner.child as DapClientState["proc"];
|
|
185
|
+
void drainReadable(proc.stdout);
|
|
186
|
+
let transport: SocketTransport | undefined;
|
|
168
187
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
10_000,
|
|
180
|
-
proc,
|
|
181
|
-
);
|
|
188
|
+
try {
|
|
189
|
+
// Wait for the socket file to appear (dlv needs to start listening)
|
|
190
|
+
await waitForCondition(
|
|
191
|
+
// `Bun.file(path).size` returns 0 for a missing file instead of
|
|
192
|
+
// throwing, so it can't gate socket readiness. Use an existence
|
|
193
|
+
// check so the adapter has actually created the listener socket.
|
|
194
|
+
() => existsSync(socketPath),
|
|
195
|
+
10_000,
|
|
196
|
+
proc,
|
|
197
|
+
);
|
|
182
198
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
199
|
+
transport = await connectSocket({ unix: socketPath }, 10_000);
|
|
200
|
+
const client = new DapClient(adapter, cwd, owner, transport);
|
|
201
|
+
proc.exited.then(() => client.#handleProcessExit());
|
|
202
|
+
void client.#startMessageReader();
|
|
203
|
+
return client;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
transport?.socket.end();
|
|
206
|
+
await owner.dispose();
|
|
207
|
+
await owner.awaitExit({ timeoutMs: 1_000 });
|
|
208
|
+
throw err;
|
|
209
|
+
} finally {
|
|
210
|
+
await fs.unlink(socketPath).catch(() => undefined);
|
|
211
|
+
}
|
|
188
212
|
}
|
|
189
213
|
|
|
190
214
|
/** macOS/other: listen on a random TCP port, spawn adapter with --client-addr, accept connection. */
|
|
@@ -214,12 +238,14 @@ export class DapClient {
|
|
|
214
238
|
});
|
|
215
239
|
|
|
216
240
|
const port = server.port;
|
|
217
|
-
const
|
|
241
|
+
const owner = spawnOwnedProcess([adapter.resolvedCommand, ...adapter.args, `--client-addr=127.0.0.1:${port}`], {
|
|
218
242
|
cwd,
|
|
219
243
|
stdin: "pipe",
|
|
220
244
|
env,
|
|
221
|
-
|
|
245
|
+
name: `dap:${adapter.name}:client-addr`,
|
|
222
246
|
});
|
|
247
|
+
const proc = owner.child as DapClientState["proc"];
|
|
248
|
+
void drainReadable(proc.stdout);
|
|
223
249
|
|
|
224
250
|
// Wait for dlv to connect (with timeout)
|
|
225
251
|
let rawSocket: Bun.Socket<undefined>;
|
|
@@ -230,13 +256,17 @@ export class DapClient {
|
|
|
230
256
|
);
|
|
231
257
|
try {
|
|
232
258
|
rawSocket = await Promise.race([connPromise, timeoutPromise]);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
await owner.dispose();
|
|
261
|
+
await owner.awaitExit({ timeoutMs: 1_000 });
|
|
262
|
+
throw err;
|
|
233
263
|
} finally {
|
|
234
264
|
clearTimeout(connectTimeout);
|
|
235
265
|
server.stop();
|
|
236
266
|
}
|
|
237
267
|
|
|
238
268
|
const { readable, writeSink, socket } = wrapBunSocket(rawSocket);
|
|
239
|
-
const client = new DapClient(adapter, cwd,
|
|
269
|
+
const client = new DapClient(adapter, cwd, owner, { readable, writeSink, socket });
|
|
240
270
|
proc.exited.then(() => client.#handleProcessExit());
|
|
241
271
|
void client.#startMessageReader();
|
|
242
272
|
return client;
|
|
@@ -414,14 +444,14 @@ export class DapClient {
|
|
|
414
444
|
/* socket may already be closed */
|
|
415
445
|
}
|
|
416
446
|
try {
|
|
417
|
-
this.
|
|
447
|
+
await this.#owner.dispose();
|
|
448
|
+
await this.#owner.awaitExit({ timeoutMs: 1_000 });
|
|
418
449
|
} catch (error) {
|
|
419
|
-
logger.debug("Failed to
|
|
450
|
+
logger.debug("Failed to dispose DAP adapter", {
|
|
420
451
|
adapter: this.adapter.name,
|
|
421
452
|
error: toErrorMessage(error),
|
|
422
453
|
});
|
|
423
454
|
}
|
|
424
|
-
await this.proc.exited.catch(() => {});
|
|
425
455
|
}
|
|
426
456
|
|
|
427
457
|
async #startMessageReader(): Promise<void> {
|
|
@@ -604,8 +634,8 @@ function socketToSink(socket: Bun.Socket<undefined>): DapWriteSink {
|
|
|
604
634
|
}
|
|
605
635
|
|
|
606
636
|
/** Connect to a unix domain socket and return DAP transport streams. */
|
|
607
|
-
async function connectSocket(options: { unix: string }): Promise<SocketTransport> {
|
|
608
|
-
const { promise, resolve } = Promise.withResolvers<SocketTransport>();
|
|
637
|
+
async function connectSocket(options: { unix: string }, timeoutMs = 10_000): Promise<SocketTransport> {
|
|
638
|
+
const { promise, resolve, reject } = Promise.withResolvers<SocketTransport>();
|
|
609
639
|
let streamController: ReadableStreamDefaultController<Uint8Array>;
|
|
610
640
|
|
|
611
641
|
const readable = new ReadableStream<Uint8Array>({
|
|
@@ -614,35 +644,46 @@ async function connectSocket(options: { unix: string }): Promise<SocketTransport
|
|
|
614
644
|
},
|
|
615
645
|
});
|
|
616
646
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
streamController.
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
|
|
647
|
+
const timeout = setTimeout(() => reject(new Error(`Socket connect timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
648
|
+
let settled = false;
|
|
649
|
+
const settle = (fn: () => void) => {
|
|
650
|
+
if (settled) return;
|
|
651
|
+
settled = true;
|
|
652
|
+
clearTimeout(timeout);
|
|
653
|
+
fn();
|
|
654
|
+
};
|
|
655
|
+
try {
|
|
656
|
+
const socketPromise = Bun.connect({
|
|
657
|
+
unix: options.unix,
|
|
658
|
+
socket: {
|
|
659
|
+
open(socket) {
|
|
660
|
+
settle(() =>
|
|
661
|
+
resolve({
|
|
662
|
+
readable,
|
|
663
|
+
writeSink: socketToSink(socket),
|
|
664
|
+
socket,
|
|
665
|
+
}),
|
|
666
|
+
);
|
|
667
|
+
},
|
|
668
|
+
data(_socket, data) {
|
|
669
|
+
streamController.enqueue(new Uint8Array(data));
|
|
670
|
+
},
|
|
671
|
+
close() {
|
|
672
|
+
try {
|
|
673
|
+
streamController.close();
|
|
674
|
+
} catch {
|
|
675
|
+
/* already closed */
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
error(_socket, err) {
|
|
679
|
+
settle(() => reject(err));
|
|
680
|
+
},
|
|
643
681
|
},
|
|
644
|
-
}
|
|
645
|
-
|
|
682
|
+
});
|
|
683
|
+
void socketPromise.catch(err => settle(() => reject(err)));
|
|
684
|
+
} catch (err) {
|
|
685
|
+
settle(() => reject(err));
|
|
686
|
+
}
|
|
646
687
|
|
|
647
688
|
return promise;
|
|
648
689
|
}
|
package/src/dap/session.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import * as timers from "node:timers/promises";
|
|
3
|
-
import { logger,
|
|
3
|
+
import { logger, untilAborted } from "@gajae-code/utils";
|
|
4
4
|
import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
|
|
5
|
+
import { type OwnedProcess, spawnOwnedProcess } from "../runtime/process-lifecycle";
|
|
5
6
|
import { DapClient } from "./client";
|
|
6
7
|
import type {
|
|
7
8
|
DapAttachArguments,
|
|
@@ -63,6 +64,24 @@ import type {
|
|
|
63
64
|
DapWriteMemoryResponse,
|
|
64
65
|
} from "./types";
|
|
65
66
|
|
|
67
|
+
function drainStream(stream: ReadableStream<Uint8Array> | null | undefined): void {
|
|
68
|
+
if (!stream) return;
|
|
69
|
+
void (async () => {
|
|
70
|
+
try {
|
|
71
|
+
const reader = stream.getReader();
|
|
72
|
+
try {
|
|
73
|
+
while (!(await reader.read()).done) {
|
|
74
|
+
// drain only
|
|
75
|
+
}
|
|
76
|
+
} finally {
|
|
77
|
+
reader.releaseLock();
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Process stream closed or was already consumed.
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
}
|
|
84
|
+
|
|
66
85
|
interface DapSession {
|
|
67
86
|
id: string;
|
|
68
87
|
adapter: DapResolvedAdapter;
|
|
@@ -87,6 +106,7 @@ interface DapSession {
|
|
|
87
106
|
initializedSeen: boolean;
|
|
88
107
|
needsConfigurationDone: boolean;
|
|
89
108
|
configurationDoneSent: boolean;
|
|
109
|
+
runInTerminalProcesses: Set<OwnedProcess>;
|
|
90
110
|
}
|
|
91
111
|
|
|
92
112
|
export interface DapOutputSnapshot {
|
|
@@ -948,6 +968,7 @@ export class DapSessionManager {
|
|
|
948
968
|
initializedSeen: false,
|
|
949
969
|
needsConfigurationDone: false,
|
|
950
970
|
configurationDoneSent: false,
|
|
971
|
+
runInTerminalProcesses: new Set(),
|
|
951
972
|
};
|
|
952
973
|
client.onReverseRequest("runInTerminal", async rawArgs => {
|
|
953
974
|
const args = (rawArgs ?? {}) as DapRunInTerminalArguments;
|
|
@@ -957,17 +978,21 @@ export class DapSessionManager {
|
|
|
957
978
|
const env = Object.fromEntries(
|
|
958
979
|
Object.entries(args.env ?? {}).filter((entry): entry is [string, string] => entry[1] !== null),
|
|
959
980
|
);
|
|
960
|
-
const
|
|
981
|
+
const owner = spawnOwnedProcess(args.args, {
|
|
961
982
|
cwd: args.cwd ?? session.cwd,
|
|
962
|
-
stdin: "
|
|
983
|
+
stdin: "ignore",
|
|
963
984
|
env: {
|
|
964
985
|
...Bun.env,
|
|
965
986
|
...NON_INTERACTIVE_ENV,
|
|
966
987
|
...env,
|
|
967
988
|
},
|
|
968
|
-
|
|
989
|
+
name: `dap:${session.id}:runInTerminal`,
|
|
969
990
|
});
|
|
970
|
-
|
|
991
|
+
drainStream(owner.child.stdout);
|
|
992
|
+
drainStream(owner.child.stderr);
|
|
993
|
+
session.runInTerminalProcesses.add(owner);
|
|
994
|
+
owner.exited.finally(() => session.runInTerminalProcesses.delete(owner));
|
|
995
|
+
return { processId: owner.pid } satisfies DapRunInTerminalResponse;
|
|
971
996
|
});
|
|
972
997
|
client.onReverseRequest("startDebugging", async rawArgs => {
|
|
973
998
|
const startArgs = (rawArgs ?? {}) as Partial<DapStartDebuggingArguments>;
|
|
@@ -1294,12 +1319,24 @@ export class DapSessionManager {
|
|
|
1294
1319
|
return session;
|
|
1295
1320
|
}
|
|
1296
1321
|
|
|
1297
|
-
#disposeSession(session: DapSession) {
|
|
1322
|
+
async #disposeSession(session: DapSession) {
|
|
1298
1323
|
if (this.#activeSessionId === session.id) {
|
|
1299
1324
|
this.#activeSessionId = null;
|
|
1300
1325
|
}
|
|
1301
1326
|
this.#sessions.delete(session.id);
|
|
1302
|
-
|
|
1327
|
+
await this.#disposeRunInTerminalProcesses(session);
|
|
1328
|
+
await session.client.dispose().catch(() => {});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
async #disposeRunInTerminalProcesses(session: DapSession): Promise<void> {
|
|
1332
|
+
const owners = [...session.runInTerminalProcesses];
|
|
1333
|
+
session.runInTerminalProcesses.clear();
|
|
1334
|
+
await Promise.allSettled(
|
|
1335
|
+
owners.map(async owner => {
|
|
1336
|
+
await owner.dispose();
|
|
1337
|
+
await owner.awaitExit({ timeoutMs: 1_000 });
|
|
1338
|
+
}),
|
|
1339
|
+
);
|
|
1303
1340
|
}
|
|
1304
1341
|
}
|
|
1305
1342
|
|
|
@@ -39,7 +39,8 @@ Inspired by the [Ouroboros project](https://github.com/Q00/ouroboros) which demo
|
|
|
39
39
|
|
|
40
40
|
<Execution_Policy>
|
|
41
41
|
- Ask ONE question at a time -- never batch multiple questions
|
|
42
|
-
- Preserve the user/session language for every user-facing announcement, topology confirmation, option label, and interview question when state includes `language.instruction`;
|
|
42
|
+
- Default to English when no language preference is explicit or obvious. Preserve the user/session language for every user-facing announcement, topology confirmation, option label, and interview question when state includes `language.instruction`; do not add language-specific special cases
|
|
43
|
+
- Before emitting any user-facing natural-language prose governed by `language.instruction`, perform one silent, best-effort self-proofread in the preserved session language for obvious spelling, spacing, grammar, inflection/particle, and word-choice errors, using the same language-agnostic pass for whatever language is active rather than special-casing any single language. Apply it only to newly generated prose and never announce the proofreading, show before/after text, apologize for it, or re-emit a corrected copy. Do not alter code blocks or identifiers, file paths, CLI commands, JSON/configuration keys, `ask` metadata keys, table/round structure, fixed labels, numeric scores, component ids, status tokens, user quotes or source text, Phase 0 threshold markers such as `Deep Interview threshold: <resolvedThresholdPercent> (source: <resolvedThresholdSource>)`, or fixed paths such as `.gjc/specs/deep-interview-{slug}.md`; still apply the self-proofread to generated natural-language clauses or cells inside those structures, including Why now rationale, gap text, next-target phrasing, and coverage notes
|
|
43
44
|
- Target the WEAKEST clarity dimension with each question
|
|
44
45
|
- Before Round 1 ambiguity scoring, run a one-time Round 0 topology enumeration gate that confirms the top-level component list and locks it into state
|
|
45
46
|
- Make weakest-dimension targeting explicit every round: name the weakest dimension, state its score/gap, and explain why the next question is aimed there
|
|
@@ -96,7 +97,7 @@ Deep Interview threshold: <resolvedThresholdPercent> (source: <resolvedThreshold
|
|
|
96
97
|
- Substitute `<resolvedThreshold>`, `<resolvedThresholdPercent>`, and `<resolvedThresholdSource>` throughout the remaining instructions before continuing.
|
|
97
98
|
- Include `threshold_source` in the first `gjc state write` payload and preserve it on later state updates; do not edit `.gjc/state` files directly unless an explicit force override is active.
|
|
98
99
|
- Include both threshold and source in the final spec metadata.
|
|
99
|
-
- Read any `language` object from active deep-interview state and carry `language.instruction` forward mechanically. If absent,
|
|
100
|
+
- Read any `language` object from active deep-interview state and carry `language.instruction` forward mechanically. If absent, default to English unless `{{ARGUMENTS}}` makes another user/session language obvious or the user explicitly requests another language. Do not add language-specific special cases.
|
|
100
101
|
|
|
101
102
|
## Phase 1: Initialize
|
|
102
103
|
|
|
@@ -175,6 +176,8 @@ The first line of this announcement MUST be exactly the Phase 0 threshold marker
|
|
|
175
176
|
> **Project type:** {greenfield|brownfield}
|
|
176
177
|
> **Current ambiguity:** 100% (we haven't started yet)
|
|
177
178
|
|
|
179
|
+
Before emitting the prose lines in this announcement, apply the `<Execution_Policy>` self-proofread once; keep the required threshold marker and the quoted `{initial_idea}` unchanged.
|
|
180
|
+
|
|
178
181
|
## Round 0: Topology Enumeration Gate
|
|
179
182
|
|
|
180
183
|
Run this gate exactly once after Phase 1 initialization and before any Phase 2 ambiguity scoring. The goal is to lock the **shape** of the user's scope before depth-first Socratic questioning can overfit to the most-described component.
|
|
@@ -293,6 +296,8 @@ Round {n} | Component: {target_component_name} | Targeting: {weakest_dimension}
|
|
|
293
296
|
|
|
294
297
|
Options should include contextually relevant choices plus free-text, translated/localized according to `language.instruction` when present.
|
|
295
298
|
|
|
299
|
+
After applying `language.instruction` to the visible question, options, and generated rationale, apply the self-proofread once to new prose only; preserve only the Round/Component/Targeting/Ambiguity line structure, fixed labels, numeric ambiguity value, component/target identifiers, and `deepInterview.*` metadata keys. Do not exempt generated natural-language rationale such as Why now.
|
|
300
|
+
|
|
296
301
|
When calling `ask`, SHOULD include optional structured metadata so the runtime can record the round without manual state writes: `deepInterview.round_id?`, `deepInterview.round`, `deepInterview.component`, `deepInterview.dimension`, and `deepInterview.ambiguity`. Keep this metadata aligned with the visible Round/Component/Targeting/Ambiguity line; if metadata cannot be supplied, the legacy formatted question text remains the fallback.
|
|
297
302
|
|
|
298
303
|
### Step 2b′: Auto-Answer Opted-Out Questions
|
|
@@ -436,6 +441,8 @@ Round {n} complete.
|
|
|
436
441
|
|
|
437
442
|
Apply `language.instruction` when present before showing this progress report so status text, gaps, and next-target phrasing stay in the preserved session language.
|
|
438
443
|
|
|
444
|
+
Then apply the self-proofread once to narrative status text, generated prose cells, gaps, and next-target phrasing; preserve only table structure, fixed status labels, scores, weights, component ids, and trigger tokens.
|
|
445
|
+
|
|
439
446
|
### Step 2e: Update State
|
|
440
447
|
|
|
441
448
|
Update state in two phases. The `ask` answer is first recorded by the runtime as an `answered` shell. Scoring then enriches the same round record to `scored` with global scores, per-component `topology.components[].clarity_scores`, `topology.components[].weakest_dimension`, trigger metadata, established-facts changes, ontology snapshot, `topology.last_targeted_component_id`, `auto_researched_rounds`, `auto_answered_rounds`, and `architect_failures`. When `deepInterview` ask metadata is present, no manual per-round `gjc state write` is required for the answer shell; only scoring enrichment/state maintenance remains. When metadata is absent, use the legacy `gjc state write` path to persist the new round and never patch `.gjc/state` directly unless an explicit force override is active.
|
|
@@ -486,6 +493,7 @@ When ambiguity ≤ threshold (or hard cap / early exit):
|
|
|
486
493
|
|
|
487
494
|
1. **Generate the specification** using opus model with the prompt-safe transcript. If the full interview transcript or initial context is too large, include the summary plus all concrete decisions, acceptance criteria, unresolved gaps, and ontology snapshots; never overflow the prompt with raw oversized context.
|
|
488
495
|
- Apply `language.instruction` when present so user-facing prose in the spec preserves the session language; keep code identifiers, file paths, commands, JSON/settings keys, and quoted source text unchanged.
|
|
496
|
+
- Apply the self-proofread once to newly generated spec prose before persistence, including generated natural-language table cells such as coverage notes, while preserving transcript answers, quoted/source text, code identifiers, file paths, commands, JSON/settings keys, table structure/fixed labels, and `.gjc/specs/deep-interview-{slug}.md` unchanged.
|
|
489
497
|
2. **Write the final spec through the workflow CLI**: persist the artifact at `.gjc/specs/deep-interview-{slug}.md`
|
|
490
498
|
- Always use this exact final spec path. Do not write temporary working files to the repo root or other ad hoc paths; repos may allowlist `.gjc/` for planning artifacts while protecting product branches.
|
|
491
499
|
- Use the native deep-interview write command with `--write --stage final --slug {slug} --spec <markdown-or-path> [--json]` for artifact and state persistence; direct `.gjc/` file edits are forbidden unless an explicit force override is active.
|
|
@@ -785,6 +793,7 @@ Why bad: 45% ambiguity means nearly half the requirements are unclear. The mathe
|
|
|
785
793
|
<Final_Checklist>
|
|
786
794
|
- [ ] Phase 0 ran before anything: threshold resolved and first line emitted as `Deep Interview threshold: <resolvedThresholdPercent> (source: <resolvedThresholdSource>)`; state and spec metadata record both `threshold` and `threshold_source`
|
|
787
795
|
- [ ] `language.instruction` preserved across announcements, questions, options, progress reports, and spec prose when present
|
|
796
|
+
- [ ] User-facing natural-language prose, including generated prose clauses/cells inside round lines or tables, was silently self-proofread once according to `language.instruction`, while code/paths/commands/keys/table or round structure/fixed labels/status tokens/quotes/threshold markers/fixed paths remained unchanged
|
|
788
797
|
- [ ] Oversized initial context/history summarized before scoring, question generation, spec generation, or handoff
|
|
789
798
|
- [ ] Round 0 topology gate completed before scoring; `topology.confirmed_at` persisted
|
|
790
799
|
- [ ] Ambiguity scored and displayed every round, naming the weakest component/dimension target (rotating across active components when N > 1)
|
|
@@ -94,7 +94,7 @@ Follow the Plan skill's full documentation for consensus mode details.
|
|
|
94
94
|
|
|
95
95
|
The Planner is a **same-session persisted subagent**: launched detached once, awaited before the Architect, then **resumed** with consolidated Architect + Critic feedback on every re-review pass instead of being re-spawned. The Architect and Critic stay **fresh, independent spawns each pass** so their verdicts remain reproducible from their pass artifacts alone. Do NOT modify the subagent control surface; this orchestration uses the existing `subagent` resume/steer controls only.
|
|
96
96
|
|
|
97
|
-
**Persistence boundary:** this is same-parent, active-session continuity only. Resumability depends on the
|
|
97
|
+
**Persistence boundary:** this is same-parent, active-session continuity only. Resumability depends on the manager's retained subagent resume metadata and a persistent parent session (an in-memory parent yields `resumable:false`), not just the `.gjc` run-state record. A terminal subagent whose live job record was evicted can still be resumed when its retained resume descriptor points at a saved subagent session file. After a process restart, missing resume metadata, or any unavailable/failed resume, use the fresh Planner fallback.
|
|
98
98
|
|
|
99
99
|
**Resume routing table** (per re-review pass, when resuming the persisted Planner id):
|
|
100
100
|
|
|
@@ -102,7 +102,7 @@ The Planner is a **same-session persisted subagent**: launched detached once, aw
|
|
|
102
102
|
|---|---|
|
|
103
103
|
| `running` | `steer`/inject the consolidated feedback to the same id, then await — do NOT fresh-spawn |
|
|
104
104
|
| `queued` | retain/update the queued message or await the same id — do NOT fresh-spawn just because it is queued |
|
|
105
|
-
| `context_unavailable`, `not_found`, `no_runner`, `resume_failed` | fresh Planner spawn for that pass; record the fallback metadata |
|
|
105
|
+
| `context_unavailable`, `not_found`, `no_runner`, `resume_failed` | fresh Planner spawn for that pass; record the fallback metadata. `not_found` should only mean same-session resume metadata is unavailable, not merely that a terminal live job was evicted. |
|
|
106
106
|
| terminal (`completed`/`failed`/`cancelled`) + revision message | resume the same id when context is available; otherwise use the fresh fallback above |
|
|
107
107
|
|
|
108
108
|
**Recording persisted-Planner metadata** (audit/routing only — never claim `subagent list` proves resumability, since the snapshot does not expose `resumable`). Ride these optional flags on the normal `--write` for the planner/revision stage of the pass:
|
|
@@ -192,7 +192,7 @@ An ultragoal story cannot be checkpointed `complete` until the active agent has
|
|
|
192
192
|
5. Delegate an `executor` QA/red-team lane to build and run the e2e/read-teaming QA suite appropriate for the story. This lane must try to break the change, not just confirm the happy path. It must start from the approved plan/spec/acceptance criteria, then user-facing contracts, and only then implementation code as supporting evidence. Plan/code mismatches are blockers, not items to paper over with implementation intent.
|
|
193
193
|
6. The executor QA/red-team lane must prove evidence by the real surface under test:
|
|
194
194
|
- GUI/web surfaces require a valid automation transcript plus a non-uniform screenshot. Bare `inlineEvidence` text or typed receipts never prove live GUI/web execution.
|
|
195
|
-
- CLI surfaces require runtime argv replay: `replaySafe: true`, an allowlisted argv `command`, and replayed normalized stdout matching `recordedStdout`; unsafe commands require audited `replayExempt` metadata plus a structurally valid fallback artifact.
|
|
195
|
+
- CLI surfaces require runtime argv replay: `replaySafe: true`, an allowlisted argv `command`, and replayed normalized stdout matching `recordedStdout`; unsafe commands require audited `replayExempt` metadata with exact fields `reasonCode`, `reason`, `approvedBy`, and `fallbackArtifactRefs` plus a structurally valid fallback artifact. Allowed `reasonCode` values are exactly `unsafe_side_effect`, `requires_credentials`, `requires_network`, `non_deterministic_external`, `destructive`, `interactive_only`, and `platform_unavailable`.
|
|
196
196
|
- Native/desktop/tui surfaces require a structurally valid screenshot, PTY capture with terminal control codes, or app-automation transcript.
|
|
197
197
|
- API/package/algorithm/math surfaces require a real artifact file or typed receipt. Bare `inlineEvidence` text alone is not sufficient for any surface.
|
|
198
198
|
7. The executor QA/red-team lane must report a matrix using `executorQa.contractCoverage`, `executorQa.surfaceEvidence`, `executorQa.adversarialCases`, and `executorQa.artifactRefs`. Not-applicable rows are allowed only in `contractCoverage` and `surfaceEvidence`; each `status: "not_applicable"` row requires `contractRef` plus `reason`. `adversarialCases` rows cannot be not-applicable.
|
|
@@ -316,7 +316,7 @@ The native `checkpoint --status complete` command rejects missing or shallow gat
|
|
|
316
316
|
}
|
|
317
317
|
```
|
|
318
318
|
|
|
319
|
-
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\")"],"recordedStdout":"ultragoal-cli-ok\n"}`. Use `replayExempt` only for audited unsafe/non-deterministic invocations, with
|
|
319
|
+
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\")"],"recordedStdout":"ultragoal-cli-ok\n"}`. Use `replayExempt` only for audited unsafe/non-deterministic invocations, with exact fields `reasonCode`, `reason`, `approvedBy`, and `fallbackArtifactRefs`. `reason` must be substantive and audited, `approvedBy` must identify the verifier, and `fallbackArtifactRefs` must reference same-surface structurally valid fallback artifacts. Allowed `reasonCode` values are exactly `unsafe_side_effect`, `requires_credentials`, `requires_network`, `non_deterministic_external`, `destructive`, `interactive_only`, and `platform_unavailable`.
|
|
320
320
|
|
|
321
321
|
## Review mode
|
|
322
322
|
|
package/src/edit/read-file.ts
CHANGED
|
@@ -7,10 +7,28 @@
|
|
|
7
7
|
import { isEnoent } from "@gajae-code/utils";
|
|
8
8
|
import { isNotebookPath, readEditableNotebookText, serializeEditedNotebookText } from "./notebook";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Max byte size of a file the edit modes will load whole. Editing loads + normalizes +
|
|
12
|
+
* fuzzy-matches + diffs the entire file on the main thread, so a multi-MB/generated file
|
|
13
|
+
* would block the event loop (F19). Above this, fail fast with an actionable error.
|
|
14
|
+
*/
|
|
15
|
+
export const MAX_EDIT_FILE_BYTES = 8 * 1024 * 1024;
|
|
16
|
+
|
|
10
17
|
export async function readEditFileText(absolutePath: string, path: string): Promise<string> {
|
|
11
18
|
try {
|
|
19
|
+
const file = Bun.file(absolutePath);
|
|
20
|
+
const size = file.size; // 0 for a missing file; the read below then throws ENOENT.
|
|
21
|
+
if (size > MAX_EDIT_FILE_BYTES) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`File too large to edit safely: ${path} is ${size} bytes (limit ${MAX_EDIT_FILE_BYTES}). ` +
|
|
24
|
+
`Editing loads and diffs the whole file on the main thread; make a more targeted change, ` +
|
|
25
|
+
`split the file, or use a specialized tool.`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
// Guard BEFORE the notebook fast-path: a >8 MiB .ipynb would otherwise load + JSON-parse
|
|
29
|
+
// + convert the whole file via readEditableNotebookText, bypassing the F19 freeze guard.
|
|
12
30
|
if (isNotebookPath(absolutePath)) return await readEditableNotebookText(absolutePath, path);
|
|
13
|
-
return await
|
|
31
|
+
return await file.text();
|
|
14
32
|
} catch (error) {
|
|
15
33
|
if (isEnoent(error)) {
|
|
16
34
|
throw new Error(`File not found: ${path}`);
|