@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.
Files changed (99) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/async/job-manager.d.ts +6 -0
  3. package/dist/types/config/model-profiles.d.ts +10 -0
  4. package/dist/types/dap/client.d.ts +2 -1
  5. package/dist/types/edit/read-file.d.ts +6 -0
  6. package/dist/types/eval/js/context-manager.d.ts +3 -0
  7. package/dist/types/eval/js/executor.d.ts +1 -0
  8. package/dist/types/exec/bash-executor.d.ts +2 -0
  9. package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
  10. package/dist/types/lsp/types.d.ts +2 -0
  11. package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
  12. package/dist/types/modes/components/model-selector.d.ts +2 -0
  13. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  14. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
  15. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  16. package/dist/types/modes/interactive-mode.d.ts +1 -0
  17. package/dist/types/modes/types.d.ts +1 -0
  18. package/dist/types/runtime/process-lifecycle.d.ts +108 -0
  19. package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
  20. package/dist/types/runtime-mcp/types.d.ts +2 -0
  21. package/dist/types/session/agent-session.d.ts +29 -1
  22. package/dist/types/session/artifacts.d.ts +4 -1
  23. package/dist/types/session/streaming-output.d.ts +12 -0
  24. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
  25. package/dist/types/tools/bash.d.ts +1 -0
  26. package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
  27. package/dist/types/tools/sqlite-reader.d.ts +2 -1
  28. package/dist/types/web/search/providers/codex.d.ts +4 -4
  29. package/package.json +7 -7
  30. package/src/async/job-manager.ts +181 -43
  31. package/src/config/file-lock.ts +9 -1
  32. package/src/config/model-profile-activation.ts +71 -3
  33. package/src/config/model-profiles.ts +39 -14
  34. package/src/dap/client.ts +105 -64
  35. package/src/dap/session.ts +44 -7
  36. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -2
  37. package/src/defaults/gjc/skills/ralplan/SKILL.md +2 -2
  38. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
  39. package/src/edit/read-file.ts +19 -1
  40. package/src/eval/js/context-manager.ts +228 -65
  41. package/src/eval/js/executor.ts +2 -0
  42. package/src/eval/js/index.ts +1 -0
  43. package/src/eval/js/worker-core.ts +10 -6
  44. package/src/eval/py/executor.ts +68 -19
  45. package/src/eval/py/kernel.ts +46 -22
  46. package/src/eval/py/runner.py +68 -14
  47. package/src/exec/bash-executor.ts +49 -13
  48. package/src/gjc-runtime/deep-interview-runtime.ts +14 -13
  49. package/src/gjc-runtime/ralplan-runtime.ts +10 -0
  50. package/src/gjc-runtime/state-runtime.ts +73 -0
  51. package/src/gjc-runtime/tmux-gc.ts +86 -37
  52. package/src/gjc-runtime/tmux-sessions.ts +44 -6
  53. package/src/gjc-runtime/ultragoal-runtime.ts +8 -4
  54. package/src/internal-urls/artifact-protocol.ts +10 -1
  55. package/src/internal-urls/docs-index.generated.ts +2 -2
  56. package/src/lsp/client.ts +64 -26
  57. package/src/lsp/index.ts +2 -1
  58. package/src/lsp/lspmux.ts +33 -9
  59. package/src/lsp/types.ts +2 -0
  60. package/src/modes/bridge/bridge-mode.ts +21 -0
  61. package/src/modes/components/assistant-message.ts +10 -2
  62. package/src/modes/components/bash-execution.ts +5 -1
  63. package/src/modes/components/eval-execution.ts +5 -1
  64. package/src/modes/components/model-selector.ts +34 -2
  65. package/src/modes/components/oauth-selector.ts +5 -0
  66. package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
  67. package/src/modes/components/skill-message.ts +24 -16
  68. package/src/modes/components/tool-execution.ts +6 -0
  69. package/src/modes/controllers/extension-ui-controller.ts +33 -6
  70. package/src/modes/controllers/input-controller.ts +19 -0
  71. package/src/modes/controllers/selector-controller.ts +6 -1
  72. package/src/modes/interactive-mode.ts +13 -0
  73. package/src/modes/types.ts +1 -0
  74. package/src/modes/utils/ui-helpers.ts +5 -2
  75. package/src/prompts/agents/executor.md +1 -1
  76. package/src/runtime/process-lifecycle.ts +400 -0
  77. package/src/runtime-mcp/manager.ts +164 -50
  78. package/src/runtime-mcp/transports/http.ts +12 -11
  79. package/src/runtime-mcp/transports/stdio.ts +64 -38
  80. package/src/runtime-mcp/types.ts +3 -0
  81. package/src/sdk.ts +27 -0
  82. package/src/session/agent-session.ts +271 -25
  83. package/src/session/artifacts.ts +17 -2
  84. package/src/session/blob-store.ts +36 -2
  85. package/src/session/session-manager.ts +29 -13
  86. package/src/session/streaming-output.ts +95 -3
  87. package/src/setup/model-onboarding-guidance.ts +10 -3
  88. package/src/skill-state/active-state.ts +79 -7
  89. package/src/slash-commands/builtin-registry.ts +30 -3
  90. package/src/slash-commands/helpers/fast-status-report.ts +111 -0
  91. package/src/tools/archive-reader.ts +10 -1
  92. package/src/tools/bash.ts +11 -4
  93. package/src/tools/browser/registry.ts +17 -1
  94. package/src/tools/browser/tab-supervisor.ts +22 -0
  95. package/src/tools/browser.ts +38 -4
  96. package/src/tools/cron.ts +2 -6
  97. package/src/tools/read.ts +11 -12
  98. package/src/tools/sqlite-reader.ts +19 -5
  99. package/src/web/search/providers/codex.ts +6 -5
package/src/dap/client.ts CHANGED
@@ -1,6 +1,9 @@
1
- import { logger, ptree } from "@gajae-code/utils";
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
- proc: DapClientState["proc"],
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.#readable = options?.readable ?? (proc.stdout as ReadableStream<Uint8Array>);
103
- this.#writeSink = options?.writeSink ?? proc.stdin;
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 proc = ptree.spawn([adapter.resolvedCommand, ...adapter.args], {
134
+ const owner = spawnOwnedProcess([adapter.resolvedCommand, ...adapter.args], {
120
135
  cwd,
121
136
  stdin: "pipe",
122
137
  env,
123
- detached: true,
138
+ name: `dap:${adapter.name}`,
124
139
  });
125
- const client = new DapClient(adapter, cwd, proc);
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 proc = ptree.spawn([adapter.resolvedCommand, ...adapter.args, `--listen=unix:${socketPath}`], {
178
+ const owner = spawnOwnedProcess([adapter.resolvedCommand, ...adapter.args, `--listen=unix:${socketPath}`], {
163
179
  cwd,
164
180
  stdin: "pipe",
165
181
  env,
166
- detached: true,
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
- // Wait for the socket file to appear (dlv needs to start listening)
170
- await waitForCondition(
171
- () => {
172
- try {
173
- Bun.file(socketPath).size;
174
- return true;
175
- } catch {
176
- return false;
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
- const { readable, writeSink, socket } = await connectSocket({ unix: socketPath });
184
- const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket });
185
- proc.exited.then(() => client.#handleProcessExit());
186
- void client.#startMessageReader();
187
- return client;
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 proc = ptree.spawn([adapter.resolvedCommand, ...adapter.args, `--client-addr=127.0.0.1:${port}`], {
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
- detached: true,
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, proc, { readable, writeSink, socket });
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.proc.kill();
447
+ await this.#owner.dispose();
448
+ await this.#owner.awaitExit({ timeoutMs: 1_000 });
418
449
  } catch (error) {
419
- logger.debug("Failed to kill DAP adapter", {
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
- Bun.connect({
618
- unix: options.unix,
619
- socket: {
620
- open(socket) {
621
- resolve({
622
- readable,
623
- writeSink: socketToSink(socket),
624
- socket,
625
- });
626
- },
627
- data(_socket, data) {
628
- streamController.enqueue(new Uint8Array(data));
629
- },
630
- close() {
631
- try {
632
- streamController.close();
633
- } catch {
634
- /* already closed */
635
- }
636
- },
637
- error(_socket, err) {
638
- try {
639
- streamController.error(err);
640
- } catch {
641
- /* already closed */
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
  }
@@ -1,7 +1,8 @@
1
1
  import * as path from "node:path";
2
2
  import * as timers from "node:timers/promises";
3
- import { logger, ptree, untilAborted } from "@gajae-code/utils";
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 proc = ptree.spawn(args.args, {
981
+ const owner = spawnOwnedProcess(args.args, {
961
982
  cwd: args.cwd ?? session.cwd,
962
- stdin: "pipe",
983
+ stdin: "ignore",
963
984
  env: {
964
985
  ...Bun.env,
965
986
  ...NON_INTERACTIVE_ENV,
966
987
  ...env,
967
988
  },
968
- detached: true,
989
+ name: `dap:${session.id}:runInTerminal`,
969
990
  });
970
- return { processId: proc.pid } satisfies DapRunInTerminalResponse;
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
- void session.client.dispose().catch(() => {});
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`; for example Korean initial ideas must receive Korean deep-interview questions unless the user explicitly requests another language
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, infer the user/session language from `{{ARGUMENTS}}` only when it is obvious. Do not surprise a Korean session with English questions.
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 in-memory subagent record (and a persistent parent session an in-memory parent yields `resumable:false`), not just a session file. The `.gjc` run-state record is an audit/routing hint, NOT a durable cross-process subagent registry. After a process restart, a missing record, or any unavailable/failed resume, use the fresh Planner fallback.
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 a substantive reason, approver, and same-surface fallback artifacts.
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
 
@@ -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 Bun.file(absolutePath).text();
31
+ return await file.text();
14
32
  } catch (error) {
15
33
  if (isEnoent(error)) {
16
34
  throw new Error(`File not found: ${path}`);