@gajae-code/coding-agent 0.11.9 → 0.11.11

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 (150) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/dist/types/cli.d.ts +8 -1
  3. package/dist/types/config/model-profile-activation.d.ts +1 -0
  4. package/dist/types/config/model-profile-contract.d.ts +2 -0
  5. package/dist/types/config/model-registry.d.ts +2 -0
  6. package/dist/types/config/settings-schema.d.ts +57 -2
  7. package/dist/types/config/settings.d.ts +1 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +21 -0
  9. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
  11. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  12. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  13. package/dist/types/modes/acp/acp-agent.d.ts +14 -9
  14. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  15. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  16. package/dist/types/modes/components/welcome.d.ts +1 -0
  17. package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
  18. package/dist/types/modes/interactive-mode.d.ts +5 -1
  19. package/dist/types/runtime/memory-domain.d.ts +9 -0
  20. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  21. package/dist/types/runtime/memory-guard.d.ts +39 -0
  22. package/dist/types/runtime/memory-limit.d.ts +11 -0
  23. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  24. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  25. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  26. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  27. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  28. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  29. package/dist/types/sdk/bus/index.d.ts +11 -2
  30. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  31. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  32. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
  33. package/dist/types/sdk/bus/telegram-daemon.d.ts +97 -8
  34. package/dist/types/sdk/host/control/operations.d.ts +1 -0
  35. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  36. package/dist/types/sdk/session.d.ts +7 -0
  37. package/dist/types/sdk/startup-capability.d.ts +6 -0
  38. package/dist/types/session/agent-session.d.ts +11 -11
  39. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  40. package/dist/types/session/internal/managed-session-scope.d.ts +28 -1
  41. package/dist/types/session/session-manager.d.ts +34 -0
  42. package/dist/types/task/executor.d.ts +6 -0
  43. package/dist/types/task/provider-retry-status.d.ts +13 -0
  44. package/dist/types/task/types.d.ts +39 -0
  45. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  46. package/dist/types/tools/index.d.ts +3 -1
  47. package/dist/types/tools/resource-gc.d.ts +19 -0
  48. package/package.json +8 -8
  49. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  50. package/scripts/run-test-manifest.ts +6 -4
  51. package/src/cli/args.ts +14 -8
  52. package/src/cli/fast-help.ts +1 -1
  53. package/src/cli/notify-cli.ts +1 -0
  54. package/src/cli.ts +48 -2
  55. package/src/commands/sdk.ts +91 -7
  56. package/src/config/model-profile-activation.ts +1 -0
  57. package/src/config/model-profile-contract.ts +13 -0
  58. package/src/config/model-profiles.ts +31 -7
  59. package/src/config/model-registry.ts +4 -2
  60. package/src/config/settings-schema.ts +61 -2
  61. package/src/config/settings.ts +8 -0
  62. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
  63. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  64. package/src/eval/js/tool-bridge.ts +1 -1
  65. package/src/extensibility/extensions/runner.ts +4 -0
  66. package/src/extensibility/extensions/types.ts +14 -0
  67. package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
  68. package/src/gjc-runtime/deep-interview-draft.ts +2 -1
  69. package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
  70. package/src/gjc-runtime/deep-interview-state.ts +25 -3
  71. package/src/gjc-runtime/linux-proc.ts +70 -36
  72. package/src/gjc-runtime/ralplan-runtime.ts +252 -1
  73. package/src/gjc-runtime/state-runtime.ts +48 -11
  74. package/src/gjc-runtime/state-writer.ts +1 -1
  75. package/src/hooks/skill-state.ts +23 -1
  76. package/src/internal-urls/docs-index.generated.ts +7 -7
  77. package/src/internal-urls/local-protocol.ts +23 -4
  78. package/src/modes/acp/acp-agent.ts +554 -60
  79. package/src/modes/acp/acp-event-mapper.ts +93 -6
  80. package/src/modes/components/model-selector.ts +15 -1
  81. package/src/modes/components/notifications-settings-editor.ts +1 -1
  82. package/src/modes/components/queue-pane.ts +55 -9
  83. package/src/modes/components/queued-message-selector.ts +53 -10
  84. package/src/modes/components/tool-status-header.ts +1 -4
  85. package/src/modes/components/welcome.ts +18 -3
  86. package/src/modes/controllers/command-controller.ts +4 -0
  87. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  88. package/src/modes/controllers/input-controller.ts +44 -3
  89. package/src/modes/controllers/selector-controller.ts +38 -8
  90. package/src/modes/interactive-mode.ts +65 -26
  91. package/src/modes/runtime-init.ts +1 -0
  92. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  93. package/src/prompts/agents/architect.md +8 -1
  94. package/src/prompts/agents/critic.md +8 -1
  95. package/src/prompts/agents/executor.md +1 -1
  96. package/src/prompts/agents/planner.md +8 -1
  97. package/src/runtime/memory-domain.ts +66 -0
  98. package/src/runtime/memory-guard-contract.ts +73 -0
  99. package/src/runtime/memory-guard.ts +292 -0
  100. package/src/runtime/memory-limit.ts +50 -0
  101. package/src/runtime-mcp/manager.ts +26 -6
  102. package/src/sdk/acp/adapter.ts +8 -1
  103. package/src/sdk/acp/final-text.ts +40 -0
  104. package/src/sdk/acp/mcp.ts +26 -0
  105. package/src/sdk/broker/identity.ts +22 -12
  106. package/src/sdk/broker/lifecycle.ts +82 -1
  107. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  108. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  109. package/src/sdk/bus/config.ts +1 -1
  110. package/src/sdk/bus/daemon-paths.ts +2 -0
  111. package/src/sdk/bus/index.ts +222 -27
  112. package/src/sdk/bus/notification-service.ts +19 -5
  113. package/src/sdk/bus/operator-runtime.ts +31 -5
  114. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  115. package/src/sdk/bus/telegram-cli.ts +6 -13
  116. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  117. package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
  118. package/src/sdk/bus/telegram-daemon-control.ts +14 -5
  119. package/src/sdk/bus/telegram-daemon.ts +1191 -164
  120. package/src/sdk/host/control/dispatch.ts +2 -0
  121. package/src/sdk/host/control/operations.ts +1 -0
  122. package/src/sdk/host/host.ts +2 -2
  123. package/src/sdk/lifecycle-session.ts +19 -3
  124. package/src/sdk/protocol/operation-inventory.generated.json +58 -0
  125. package/src/sdk/protocol/operation-registry.ts +1 -0
  126. package/src/sdk/session.ts +24 -1
  127. package/src/sdk/startup-capability.ts +6 -0
  128. package/src/session/agent-session.ts +290 -132
  129. package/src/session/fallback-chain-controller.ts +47 -8
  130. package/src/session/internal/managed-session-scope.ts +173 -127
  131. package/src/session/internal/native-publish-outcome.ts +8 -3
  132. package/src/session/session-manager.ts +608 -25
  133. package/src/skill-state/workflow-mutation-guard.ts +41 -7
  134. package/src/task/executor.ts +152 -42
  135. package/src/task/index.ts +2 -0
  136. package/src/task/provider-retry-status.ts +88 -0
  137. package/src/task/render.ts +45 -26
  138. package/src/task/types.ts +25 -11
  139. package/src/task/ultragoal-redteam-activation.ts +75 -0
  140. package/src/tools/bash-allowed-prefixes.ts +45 -1
  141. package/src/tools/index.ts +3 -1
  142. package/src/tools/resource-gc.ts +496 -120
  143. package/src/tools/subagent-render.ts +74 -16
  144. package/src/tools/subagent.ts +2 -0
  145. package/src/workflow/workflow-intent-diff.ts +4 -1
  146. package/vendor/insane-search/MANIFEST.json +4 -1
  147. package/vendor/insane-search/engine/bias_check.py +5 -0
  148. package/vendor/insane-search/engine/fetch_chain.py +67 -1
  149. package/vendor/insane-search/engine/transport.py +13 -2
  150. package/vendor/insane-search/engine/validators.py +4 -1
package/src/cli.ts CHANGED
@@ -5,8 +5,10 @@
5
5
  * lightweight CLI runner from pi-utils.
6
6
  */
7
7
  import "@gajae-code/utils/postmortem";
8
+ import { THINKING_EFFORTS } from "@gajae-code/ai";
8
9
  import { Args, type CliConfig, Command, type CommandEntry, Flags, run } from "@gajae-code/utils/cli";
9
10
  import { APP_NAME, formatBunRuntimeError, MIN_BUN_VERSION, VERSION } from "@gajae-code/utils/dirs";
11
+ import { loadNative as loadNativeBindings } from "../../natives/native/loader-state.js";
10
12
  import { runFixtureReport } from "./cli/fixture-report";
11
13
  import { admitManagedOwnerBeforeCli, completeManagedOwnerRecovery } from "./gjc-runtime/managed-owner-admission";
12
14
  import {
@@ -174,6 +176,46 @@ async function runChatDaemonInternalFastPath(argv: string[]): Promise<void> {
174
176
  await runChatDaemonInternal(action === "discord-internal" ? "discord" : "slack", argv.slice(2));
175
177
  }
176
178
 
179
+ type MemoryGuardNativeSmokeLoad = () => Record<string, unknown>;
180
+ type WindowsJobMemoryProbeResult = Record<string, unknown> & { kind: string };
181
+ type MemoryGuardNativeSmokeReceipt = {
182
+ api: "memory_guard_windows_job_probe_v1";
183
+ source: "pi_natives";
184
+ result: WindowsJobMemoryProbeResult;
185
+ };
186
+
187
+ export function isMemoryGuardNativeSmokeFastPath(argv: readonly string[]): boolean {
188
+ return (
189
+ argv.length === 3 && argv[0] === "internal" && argv[1] === "memory-guard-native-smoke" && argv[2] === "--json"
190
+ );
191
+ }
192
+
193
+ function parseWindowsJobMemoryProbeResult(value: unknown): WindowsJobMemoryProbeResult {
194
+ if (!value || typeof value !== "object") {
195
+ throw new Error("memory-guard-native-smoke: native probe returned a non-object result");
196
+ }
197
+ const result = value as Record<string, unknown>;
198
+ if (typeof result.kind !== "string") {
199
+ throw new Error("memory-guard-native-smoke: native probe result is missing a string kind tag");
200
+ }
201
+ return result as WindowsJobMemoryProbeResult;
202
+ }
203
+
204
+ export function runMemoryGuardNativeSmokeFastPath(
205
+ options: { loadNative?: MemoryGuardNativeSmokeLoad; writeStdout?: (text: string) => void } = {},
206
+ ): void {
207
+ const probe = (options.loadNative ?? loadNativeBindings)().probeWindowsJobMemory;
208
+ if (typeof probe !== "function") {
209
+ throw new Error("memory-guard-native-smoke: probeWindowsJobMemory export missing from native addon");
210
+ }
211
+ const receipt: MemoryGuardNativeSmokeReceipt = {
212
+ api: "memory_guard_windows_job_probe_v1",
213
+ source: "pi_natives",
214
+ result: parseWindowsJobMemoryProbeResult((probe as () => unknown)()),
215
+ };
216
+ (options.writeStdout ?? (text => process.stdout.write(text)))(`${JSON.stringify(receipt)}\n`);
217
+ }
218
+
177
219
  function rootFixtureArg(argv: string[]): { present: boolean; id: string | undefined } {
178
220
  for (let i = 0; i < argv.length; i++) {
179
221
  const arg = argv[i];
@@ -244,8 +286,8 @@ export class RootHelpCommand extends Command {
244
286
  tmux: Flags.boolean({ description: "Launch interactive startup inside tmux" }),
245
287
  tools: Flags.string({ description: "Comma-separated list of tools to enable (default: all)" }),
246
288
  thinking: Flags.string({
247
- description: "Set thinking level: ultra, high, medium, low",
248
- options: ["ultra", "high", "medium", "low"],
289
+ description: `Set thinking level: ${THINKING_EFFORTS.join(", ")}`,
290
+ options: [...THINKING_EFFORTS],
249
291
  }),
250
292
  hook: Flags.string({ description: "Load a hook/extension file (can be used multiple times)", multiple: true }),
251
293
  extension: Flags.string({
@@ -382,6 +424,10 @@ export async function runCli(argv: string[]): Promise<void> {
382
424
  }
383
425
  // Re-exec could not be spawned; fall through and run in this process.
384
426
  }
427
+ if (isMemoryGuardNativeSmokeFastPath(argv)) {
428
+ runMemoryGuardNativeSmokeFastPath();
429
+ return;
430
+ }
385
431
  if (isTmuxOwnerIsolationCliArgv(argv)) {
386
432
  await runTmuxOwnerIsolationCliFromStdin();
387
433
  return;
@@ -1,11 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
3
4
  import * as path from "node:path";
4
5
  import { Args, CliParseError, Command, Flags, renderCommandHelp } from "@gajae-code/utils/cli";
5
6
  import type { Args as ParsedArgs } from "../cli/args";
6
7
  import { Settings } from "../config/settings";
7
8
  import { applyStartupModelProfiles, createSessionManager } from "../main";
8
9
  import { initializeExtensions } from "../modes/runtime-init";
10
+ import { ACP_MCP_REQUEST_TIMEOUT_MS, ACP_MCP_STARTUP_HEADROOM_MS } from "../sdk/acp/mcp";
9
11
  import { Broker } from "../sdk/broker/broker";
10
12
  import { completeBrokerProcess } from "../sdk/broker/internal";
11
13
  import {
@@ -285,17 +287,99 @@ export async function runSessionHost(
285
287
  throw new Error("SDK startup did not complete before readiness cutoff.");
286
288
  }
287
289
 
288
- let opened: { parsed: ParsedArgs; sessionManager: SessionManager | undefined };
289
- let created: CreateLifecycleAgentSessionResult;
290
- try {
291
- opened = await openLifecycleSessionManager(request, cwd, agentDir);
292
- created = await createLifecycleAgentSession({ cwd, agentDir, sessionManager: opened.sessionManager });
293
- } catch (error) {
290
+ // Inlined rather than extracted to a helper: TypeScript's definite-assignment
291
+ // analysis does not see a `Promise<never>` helper as terminating, so hoisting
292
+ // this would make `opened`/`created` "used before assigned" below.
293
+ const registrationFailure = async (error: unknown): Promise<SdkStartupFailure> => {
294
294
  const rollback = new SdkStartupRollbackTracker();
295
295
  rollback.recordAbsent();
296
296
  const failure = normalizeSdkStartupFailure("registration", "failed", error);
297
297
  await writeFailure(failure, rollback.result);
298
- throw failure;
298
+ return failure;
299
+ };
300
+
301
+ let opened: { parsed: ParsedArgs; sessionManager: SessionManager | undefined };
302
+ let created: CreateLifecycleAgentSessionResult;
303
+ let mcpConfigDirectory: string | undefined;
304
+ try {
305
+ let mcpConfigPath: string | undefined;
306
+ try {
307
+ opened = await openLifecycleSessionManager(request, cwd, agentDir);
308
+ if (request.mcpServers && request.mcpServers.length > 0) {
309
+ mcpConfigDirectory = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "gjc-acp-mcp-")));
310
+ mcpConfigPath = path.join(mcpConfigDirectory, "mcp.json");
311
+ await Bun.write(
312
+ mcpConfigPath,
313
+ JSON.stringify({
314
+ mcpServers: Object.fromEntries(
315
+ request.mcpServers.map(server => [
316
+ server.name,
317
+ "url" in server
318
+ ? {
319
+ type: server.type,
320
+ url: server.url,
321
+ ...(server.headers ? { headers: server.headers } : {}),
322
+ timeout: ACP_MCP_REQUEST_TIMEOUT_MS,
323
+ }
324
+ : {
325
+ type: "stdio",
326
+ command: server.command,
327
+ args: server.args,
328
+ ...(server.env ? { env: server.env } : {}),
329
+ noInheritEnv: true,
330
+ timeout: ACP_MCP_REQUEST_TIMEOUT_MS,
331
+ },
332
+ ]),
333
+ ),
334
+ }),
335
+ );
336
+ }
337
+ } catch (error) {
338
+ throw await registrationFailure(error);
339
+ }
340
+
341
+ // The longer MCP startup ceiling is scoped to ACP lifecycle launches only:
342
+ // it applies when this request actually carried `mcpServers`. Ordinary
343
+ // CLI/SDK `mcpConfigPath` consumers keep the manager's short default.
344
+ //
345
+ // This recheck deliberately sits OUTSIDE the registration catch above.
346
+ // Inside it, the throw would be caught, reclassified as
347
+ // `registration`/`failed`, and written a second time, losing the
348
+ // `startup`/`pending` outcome the readiness cutoff is supposed to report.
349
+ // Session-manager open and MCP config write already consumed part of the
350
+ // budget, so re-read the clock here rather than reusing the earlier check.
351
+ let mcpStartupTimeoutMs: number | undefined;
352
+ if (mcpConfigPath !== undefined) {
353
+ const remaining = request.semanticReadyDeadlineAt - now() - ACP_MCP_STARTUP_HEADROOM_MS;
354
+ if (remaining <= 0) {
355
+ const absent = new SdkStartupRollbackTracker();
356
+ absent.recordAbsent();
357
+ await writeFailure(
358
+ {
359
+ phase: "startup",
360
+ reason: "pending",
361
+ message: "SDK startup did not complete before readiness cutoff.",
362
+ },
363
+ absent.result,
364
+ );
365
+ throw new Error("SDK startup did not complete before readiness cutoff.");
366
+ }
367
+ mcpStartupTimeoutMs = remaining;
368
+ }
369
+
370
+ try {
371
+ created = await createLifecycleAgentSession({
372
+ cwd,
373
+ agentDir,
374
+ sessionManager: opened.sessionManager,
375
+ ...(mcpConfigPath ? { mcpConfigPath } : {}),
376
+ ...(mcpStartupTimeoutMs !== undefined ? { mcpStartupTimeoutMs } : {}),
377
+ });
378
+ } catch (error) {
379
+ throw await registrationFailure(error);
380
+ }
381
+ } finally {
382
+ if (mcpConfigDirectory) await fs.rm(mcpConfigDirectory, { recursive: true, force: true });
299
383
  }
300
384
  const { parsed } = opened;
301
385
  if ("failure" in created) {
@@ -222,6 +222,7 @@ export function materializeActiveModelProfileAssignments(options: MaterializeMod
222
222
  }
223
223
 
224
224
  export class ModelProfileCredentialError extends Error {
225
+ readonly code = "authentication_failed";
225
226
  readonly profileLabel: string;
226
227
  readonly providers: readonly string[];
227
228
 
@@ -10,6 +10,7 @@ export interface ModelProfileCatalogItem {
10
10
  id: string;
11
11
  displayName: string;
12
12
  source: "builtin" | "configured";
13
+ available?: boolean;
13
14
  }
14
15
 
15
16
  export interface UnknownModelProfileDetails {
@@ -124,6 +125,18 @@ export function projectModelProfileCatalog(
124
125
  .sort((left, right) => left.id.localeCompare(right.id));
125
126
  }
126
127
 
128
+ export function isModelProfileProviderAvailable(
129
+ profile: ModelProfileDefinition,
130
+ authenticatedProviders: ReadonlySet<string>,
131
+ ): boolean {
132
+ const alternativeGroups = profile.alternativeProviderGroups ?? [];
133
+ const alternativeProviders = new Set(alternativeGroups.flat());
134
+ for (const provider of profile.requiredProviders) {
135
+ if (!alternativeProviders.has(provider) && !authenticatedProviders.has(provider)) return false;
136
+ }
137
+ return alternativeGroups.every(group => group.some(provider => authenticatedProviders.has(provider)));
138
+ }
139
+
127
140
  export function isModelProfileError(value: unknown): value is {
128
141
  code: ModelProfileErrorCode;
129
142
  message: string;
@@ -100,11 +100,11 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
100
100
  architect: "opencode-go/deepseek-v4-pro",
101
101
  }),
102
102
  profile("claude-opus", ["anthropic"], {
103
- default: "anthropic/claude-opus-4-8:xhigh",
103
+ default: "anthropic/claude-opus-5:xhigh",
104
104
  executor: "anthropic/claude-sonnet-5",
105
- planner: "anthropic/claude-opus-4-8:low",
106
- critic: "anthropic/claude-opus-4-8:high",
107
- architect: "anthropic/claude-opus-4-8:xhigh",
105
+ planner: "anthropic/claude-opus-5:low",
106
+ critic: "anthropic/claude-opus-5:high",
107
+ architect: "anthropic/claude-opus-5:xhigh",
108
108
  }),
109
109
  profile("claude-fable", ["anthropic"], {
110
110
  default: "anthropic/claude-fable-5:xhigh",
@@ -207,6 +207,27 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
207
207
  critic: "xai/grok-4.3:xhigh",
208
208
  architect: "xai/grok-4.3:xhigh",
209
209
  }),
210
+ profile("grok-45-eco", ["xai"], {
211
+ default: "xai/grok-4.5:low",
212
+ executor: "xai/grok-4.5:minimal",
213
+ planner: "xai/grok-4.5:low",
214
+ critic: "xai/grok-4.5:medium",
215
+ architect: "xai/grok-4.5:high",
216
+ }),
217
+ profile("grok-45-medium", ["xai"], {
218
+ default: "xai/grok-4.5:medium",
219
+ executor: "xai/grok-4.5:low",
220
+ planner: "xai/grok-4.5:medium",
221
+ critic: "xai/grok-4.5:high",
222
+ architect: "xai/grok-4.5:high",
223
+ }),
224
+ profile("grok-45-pro", ["xai"], {
225
+ default: "xai/grok-4.5:high",
226
+ executor: "xai/grok-4.5:medium",
227
+ planner: "xai/grok-4.5:high",
228
+ critic: "xai/grok-4.5:high",
229
+ architect: "xai/grok-4.5:high",
230
+ }),
210
231
  profile("grok-build-pro", ["grok-build"], {
211
232
  default: "grok-build/grok-composer-2.5-fast",
212
233
  executor: "grok-build/grok-build",
@@ -271,7 +292,7 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
271
292
  critic: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
272
293
  }),
273
294
  profile("opus-codex", ["anthropic", "openai-codex"], {
274
- default: "anthropic/claude-opus-4-8:xhigh",
295
+ default: "anthropic/claude-opus-5:xhigh",
275
296
  executor: "openai-codex/gpt-5.6-terra:low",
276
297
  planner: "anthropic/claude-sonnet-5",
277
298
  critic: "openai-codex/gpt-5.6-sol:xhigh",
@@ -287,8 +308,8 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
287
308
  profile("fable-opus-codex", ["anthropic", "openai-codex"], {
288
309
  default: "anthropic/claude-fable-5:high",
289
310
  executor: "openai-codex/gpt-5.6-terra:medium",
290
- planner: "anthropic/claude-opus-4-8:medium",
291
- critic: "anthropic/claude-opus-4-8:high",
311
+ planner: "anthropic/claude-opus-5:medium",
312
+ critic: "anthropic/claude-opus-5:high",
292
313
  architect: "openai-codex/gpt-5.6-sol:xhigh",
293
314
  }),
294
315
  ];
@@ -321,6 +342,9 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
321
342
  "grok-eco": { displayName: "Grok Eco", providerGroup: "GROK" },
322
343
  "grok-medium": { displayName: "Grok Medium", providerGroup: "GROK" },
323
344
  "grok-pro": { displayName: "Grok Pro", providerGroup: "GROK" },
345
+ "grok-45-eco": { displayName: "Grok 4.5 Eco", providerGroup: "GROK" },
346
+ "grok-45-medium": { displayName: "Grok 4.5 Medium", providerGroup: "GROK" },
347
+ "grok-45-pro": { displayName: "Grok 4.5 Pro", providerGroup: "GROK" },
324
348
  "grok-build-pro": { displayName: "Grok Build Pro", providerGroup: "GROK" },
325
349
  "cursor-eco": { displayName: "Cursor Eco", providerGroup: "CURSOR" },
326
350
  "cursor-medium": { displayName: "Cursor Medium", providerGroup: "CURSOR" },
@@ -2564,13 +2564,14 @@ export class ModelRegistry {
2564
2564
  async getApiKey(
2565
2565
  model: Model<Api>,
2566
2566
  sessionId?: string,
2567
- options: { credentialSelector?: AuthCredentialSelector } = {},
2567
+ options: { credentialSelector?: AuthCredentialSelector; signal?: AbortSignal } = {},
2568
2568
  ): Promise<string | undefined> {
2569
2569
  return this.#getApiKeyOrNoAuth(model.provider, () =>
2570
2570
  this.authStorage.getApiKey(model.provider, sessionId, {
2571
2571
  baseUrl: model.baseUrl,
2572
2572
  modelId: model.id,
2573
2573
  credentialSelector: options.credentialSelector,
2574
+ signal: options.signal,
2574
2575
  }),
2575
2576
  );
2576
2577
  }
@@ -2582,12 +2583,13 @@ export class ModelRegistry {
2582
2583
  provider: string,
2583
2584
  sessionId?: string,
2584
2585
  baseUrl?: string,
2585
- options: { credentialSelector?: AuthCredentialSelector } = {},
2586
+ options: { credentialSelector?: AuthCredentialSelector; signal?: AbortSignal } = {},
2586
2587
  ): Promise<string | undefined> {
2587
2588
  return this.#getApiKeyOrNoAuth(provider, () =>
2588
2589
  this.authStorage.getApiKey(provider, sessionId, {
2589
2590
  baseUrl,
2590
2591
  credentialSelector: options.credentialSelector,
2592
+ signal: options.signal,
2591
2593
  }),
2592
2594
  );
2593
2595
  }
@@ -309,7 +309,7 @@ export const SETTINGS_SCHEMA = {
309
309
  },
310
310
  "notifications.telegram.toolActivity.enabled": {
311
311
  type: "boolean",
312
- default: true,
312
+ default: false,
313
313
  ui: {
314
314
  tab: "notifications",
315
315
  label: "Telegram Tool Activity",
@@ -501,6 +501,11 @@ export const SETTINGS_SCHEMA = {
501
501
  default: 0.05,
502
502
  validate: (value: number) => Number.isFinite(value) && value > 0 && value <= 1,
503
503
  },
504
+ "gjc.ralplan.maxIterations": {
505
+ type: "number",
506
+ default: 5,
507
+ validate: (value: number) => Number.isInteger(value) && value >= 1 && value <= 20,
508
+ },
504
509
 
505
510
  // ────────────────────────────────────────────────────────────────────────
506
511
  // Appearance
@@ -1244,7 +1249,8 @@ export const SETTINGS_SCHEMA = {
1244
1249
  ui: {
1245
1250
  tab: "interaction",
1246
1251
  label: "Mouse Support",
1247
- description: "Enable SGR mouse wheel scrolling and overlay row selection. Disabled in tmux and screen.",
1252
+ description:
1253
+ "Enable GJC session scrolling, drag-to-copy text selection, and overlay row selection with the mouse. Disabled by default to preserve native terminal or tmux scrollback and selection.",
1248
1254
  },
1249
1255
  },
1250
1256
  // Conversation flow
@@ -2653,6 +2659,47 @@ export const SETTINGS_SCHEMA = {
2653
2659
  description: "How often the resource GC sweeps browser tabs and stale screenshot directories.",
2654
2660
  },
2655
2661
  },
2662
+ "memoryGuard.enabled": {
2663
+ type: "boolean",
2664
+ default: false,
2665
+ },
2666
+ "memoryGuard.checkIntervalMs": {
2667
+ type: "number",
2668
+ default: 30_000,
2669
+ validate: (value: number) => Number.isFinite(value) && value > 0,
2670
+ },
2671
+ "memoryGuard.gcThresholdPercent": {
2672
+ type: "number",
2673
+ default: 70,
2674
+ validate: (value: number) => Number.isFinite(value) && value >= 0 && value <= 100,
2675
+ },
2676
+ "memoryGuard.restartThresholdPercent": {
2677
+ type: "number",
2678
+ default: 85,
2679
+ validate: (value: number) => Number.isFinite(value) && value >= 0 && value <= 100,
2680
+ },
2681
+ "memoryGuard.restartThresholdWindowMs": {
2682
+ type: "number",
2683
+ default: 90_000,
2684
+ validate: (value: number) => Number.isFinite(value) && value > 0,
2685
+ },
2686
+ "memoryGuard.cooldownMs": {
2687
+ type: "number",
2688
+ default: 600_000,
2689
+ validate: (value: number) => Number.isFinite(value) && value >= 0,
2690
+ },
2691
+ "memoryGuard.parentReserveMb": {
2692
+ type: "number",
2693
+ default: 1024,
2694
+ validate: (value: number) =>
2695
+ Number.isFinite(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER / (1024 * 1024),
2696
+ },
2697
+ "memoryGuard.policyLimitMb": {
2698
+ type: "number",
2699
+ default: 0,
2700
+ validate: (value: number) =>
2701
+ Number.isFinite(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER / (1024 * 1024),
2702
+ },
2656
2703
 
2657
2704
  "computer.enabled": {
2658
2705
  type: "boolean",
@@ -3844,6 +3891,17 @@ export interface ShellMinimizerSettings {
3844
3891
  maxCaptureBytes: number;
3845
3892
  }
3846
3893
 
3894
+ export interface MemoryGuardSettings {
3895
+ enabled: boolean;
3896
+ checkIntervalMs: number;
3897
+ gcThresholdPercent: number;
3898
+ restartThresholdPercent: number;
3899
+ restartThresholdWindowMs: number;
3900
+ cooldownMs: number;
3901
+ parentReserveMb: number;
3902
+ policyLimitMb: number;
3903
+ }
3904
+
3847
3905
  export interface NotificationsSettings {
3848
3906
  enabled: boolean;
3849
3907
  telegram: {
@@ -3902,6 +3960,7 @@ export interface GroupTypeMap {
3902
3960
  statusLine: StatusLineSettings;
3903
3961
  thinkingBudgets: ThinkingBudgetsSettings;
3904
3962
  stt: SttSettings;
3963
+ memoryGuard: MemoryGuardSettings;
3905
3964
  modelRoles: Record<string, ModelSelectorValue>;
3906
3965
  modelTags: ModelTagsSettings;
3907
3966
  cycleOrder: string[];
@@ -363,6 +363,7 @@ export class Settings implements NotificationSettingsReader {
363
363
  /** Pending debounced ordinary save; its queue slot is reserved immediately. */
364
364
  #saveTimer?: NodeJS.Timeout;
365
365
  #savePromise?: Promise<void>;
366
+ #changeListeners = new Set<(path: SettingPath) => void>();
366
367
  #pendingSaveSlot?: PendingSaveSlot;
367
368
 
368
369
  /** Legacy fallback migration warnings emitted once per settings instance. */
@@ -522,6 +523,11 @@ export class Settings implements NotificationSettingsReader {
522
523
  return structuredClone(this.#schemaReport);
523
524
  }
524
525
 
526
+ onChanged(listener: (path: SettingPath) => void): () => void {
527
+ this.#changeListeners.add(listener);
528
+ return () => this.#changeListeners.delete(listener);
529
+ }
530
+
525
531
  /**
526
532
  * Set a setting value (sync).
527
533
  * Updates global settings and reserves its background persistence slot before
@@ -555,6 +561,7 @@ export class Settings implements NotificationSettingsReader {
555
561
 
556
562
  const hook = SETTING_HOOKS[path];
557
563
  if (hook) hook(value, prev);
564
+ for (const listener of this.#changeListeners) listener(path);
558
565
  }
559
566
 
560
567
  /**
@@ -579,6 +586,7 @@ export class Settings implements NotificationSettingsReader {
579
586
 
580
587
  const hook = SETTING_HOOKS[path];
581
588
  if (hook) hook(this.get(path), prev);
589
+ for (const listener of this.#changeListeners) listener(path);
582
590
  }
583
591
 
584
592
  /**
@@ -67,14 +67,15 @@ The consensus workflow:
67
67
  - **Plan-only Critic lane**: independently check quality, principle-option consistency, alternatives, risks, acceptance criteria, and verification; when the plan is thin, request concrete expansion rather than only defects. Persist with `gjc ralplan --write --stage critic --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json`, then return receipt/path plus `OKAY`/`ITERATE`/`REJECT`.
68
68
  - **Sequential fallback**: if Critic must evaluate Architect findings, verdict, antithesis, tradeoffs, synthesis, status, or any Architect-produced artifact, await the Architect result before issuing that Architect-dependent Critic pass.
69
69
  4. **Review join gate**: before consensus, revision, reconciliation, finalization, or approval, verify both Architect and Critic receipts/verdicts exist for the same Planner artifact/pass (`path`, `sha256`, `stage_n`). A non-`CLEAR` Architect verdict, non-`APPROVE` Architect decision, or any non-`OKAY` Critic verdict routes back to Planner revision; do not finalize from only one review lane.
70
- 5. **Re-review loop** (max 5 iterations): Any non-`OKAY` Critic verdict (`ITERATE` or `REJECT`) or Architect result that is not `CLEAR`/`APPROVE` MUST run the same full closed loop:
70
+ 5. **Re-review loop** (max 5 iterations; **runtime-enforced**): Any non-`OKAY` Critic verdict (`ITERATE` or `REJECT`) or Architect result that is not `CLEAR`/`APPROVE` MUST run the same full closed loop:
71
71
  a. Collect Architect + Critic feedback
72
72
  b. Revise the plan by resuming the SAME persisted Planner subagent with consolidated Architect + Critic feedback (see **Persisted Planner** below); fall back to a fresh Planner spawn only per the fallback routing table
73
73
  c. Return to the review fan-out or sequential fallback path above
74
74
  - Persist each Planner revision with `gjc ralplan --write --stage revision --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json` before re-review, then pass the receipt/path forward instead of duplicating the full revision markdown in the parent conversation.
75
75
  d. Re-join Architect and Critic verdicts for the same revised Planner artifact/pass
76
76
  e. Repeat this loop until Critic returns `OKAY` **and** Architect is `CLEAR`/`APPROVE` for the same Planner artifact/pass, or 5 iterations are reached
77
- f. If 5 iterations are reached without Critic `OKAY` plus Architect `CLEAR`/`APPROVE`, present the best version to the user
77
+ f. If 5 iterations are reached without Critic `OKAY` plus Architect `CLEAR`/`APPROVE`, **stop opening further planner/revision passes**. Present the best version to the user (interactive) or surface `PLANNING-STUCK` (headless). Do **not** auto-start implementation.
78
+ g. **Runtime budget (#3165):** native `gjc ralplan --write` refuses a new `planner`/`revision` that would open consensus iteration **> max** (default **5**, overridable via `gjc.ralplan.maxIterations` in project/user `.gjc/settings.json`, integer 1..20). Cap uses the same iteration definition as the HUD (`planner`/`revision` openers in `index.jsonl`). Overflow exits **3**, prints operator-visible **`PLANNING-STUCK`** on stdout (and stderr detail; JSON includes `planning_stuck: true`), and still allows `architect`/`critic` within an already-opened pass plus `post-interview`/`adr`/`final` so the best plan can be escalated to `pending approval` without auto-execution. A new `--run-id` starts a fresh budget.
78
79
  6. **Post-ralplan interview** (intent reconciliation gate): After the review join gate has both Critic `OKAY` and Architect `CLEAR`/`APPROVE` for the same Planner artifact/pass, and before the plan is finalized, reconcile the consensus plan against the user's actual intent. The goal is to make sure ralplan did not silently bake in assumptions that conflict with what the user wants.
79
80
  a. **Collect open items** from the run: every assumption the Planner/Architect/Critic resolved by assumption rather than by stated fact, every ambiguity flagged during review, and every decision the loop made without explicit user input. Source these from the persisted `planner`/`architect`/`critic`/`revision` stage artifacts, not from memory.
80
81
  b. **Cross-check prior context for conflicts**: glob `.gjc/_session-{sessionid}/specs/deep-interview-*.md` and other prior specs/plans/context relevant by topic. For each, list points where the consensus plan contradicts, weakens, or expands beyond a previously crystallized decision, constraint, or non-goal. Cite the conflicting artifact and line/section.
@@ -103,6 +104,24 @@ The consensus workflow:
103
104
 
104
105
  > **Important:** Architect and Critic MAY run in the same parallel batch only for the plan-only Critic lane after Planner persistence. Any Architect-dependent Critic pass MUST remain sequential: await Architect before issuing Critic, then apply the same review join gate before consensus.
105
106
 
107
+ ## Consensus iteration cap (operator contract)
108
+
109
+ - Default max consensus iterations: **5** (`gjc.ralplan.maxIterations`).
110
+ - On cap: exit code **3**, marker **`PLANNING-STUCK`** (stdout), no silent re-loop, no automatic ultragoal/team handoff. Opener budget is `max(index.jsonl openers, on-disk stage-*-{planner,revision}.md count)` so a missing/empty/malformed ledger cannot fail open after prior openers.
111
+ - Headless/CI: treat `PLANNING-STUCK` / exit 3 as terminal planning failure for orchestration/watchdogs.
112
+ - Interactive: present best existing plan via the final approval gate; residual critic findings stay as caveats.
113
+ - Override example (project `.gjc/settings.json`):
114
+
115
+ ```json
116
+ {
117
+ "gjc": {
118
+ "ralplan": {
119
+ "maxIterations": 3
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
106
125
 
107
126
  Follow this ralplan-internal consensus workflow for consensus mode details.
108
127
 
@@ -279,7 +279,7 @@ An ultragoal story cannot be checkpointed `complete` until the active agent has
279
279
  - architecture-side: system boundaries, layering, data/control flow, operational risks.
280
280
  - product-side: user-visible behavior, acceptance criteria, edge cases, regressions.
281
281
  - code-side: maintainability, tests, integration points, and unsafe shortcuts.
282
- 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.
282
+ 5. Delegate an `executor` QA/red-team lane with typed `executionMode: "ultragoal-red-team"` (preferred) — or assignment text that explicitly labels Ultragoal completion QA/red-team — to build and run the e2e/red-teaming QA suite appropriate for the story. A bare `executorQa` field-name mention is not enough to activate the mode. 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.
283
283
  6. The executor QA/red-team lane must prove evidence by the real surface under test:
284
284
  - 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.
285
285
  - CLI surfaces require runtime argv replay: `schemaVersion: 1`, `kind: "cli-replay"`, `replaySafe: true`, an allowlisted argv `command`, and replayed output validation. The complete field-by-field replay schema, command allowlist, and `replayExempt` audit contract are specified once in the "For CLI replay artifacts" paragraph below the quality-gate JSON; follow it exactly.
@@ -30,7 +30,7 @@ function toolResultHasError(result: AgentToolResult): boolean {
30
30
  }
31
31
 
32
32
  function getTool(session: ToolSession, name: string): AgentTool {
33
- const tool = session.getToolByName?.(name);
33
+ const tool = session.getToolForExecution ? session.getToolForExecution(name) : session.getToolByName?.(name);
34
34
  if (!tool) {
35
35
  throw new ToolError(`Unknown tool from js runtime: ${name}`);
36
36
  }
@@ -201,6 +201,7 @@ export class ExtensionRunner {
201
201
  #getAllToolsFn: ExtensionContext["getAllTools"] = () => [];
202
202
  #getResolveToolFn: ExtensionContext["resolveTool"] = () => undefined;
203
203
  #cycleModelFn: ExtensionContextActions["cycleModel"] = undefined;
204
+ #setModelProfileFn: ExtensionContextActions["setModelProfile"] = undefined;
204
205
  #cycleThinkingLevelFn: ExtensionContextActions["cycleThinkingLevel"] = undefined;
205
206
  #setQueueModeFn: ExtensionContextActions["setQueueMode"] = undefined;
206
207
  #getSkillStateFn: ExtensionContextActions["getSkillState"] = undefined;
@@ -313,6 +314,7 @@ export class ExtensionRunner {
313
314
  this.#getAllToolsFn = contextActions.getAllTools ?? (() => []);
314
315
  this.#getResolveToolFn = contextActions.resolveTool ?? (() => undefined);
315
316
  this.#cycleModelFn = contextActions.cycleModel;
317
+ this.#setModelProfileFn = contextActions.setModelProfile;
316
318
  this.#cycleThinkingLevelFn = contextActions.cycleThinkingLevel;
317
319
  this.#setQueueModeFn = contextActions.setQueueMode;
318
320
  this.#getSkillStateFn = contextActions.getSkillState;
@@ -562,6 +564,7 @@ export class ExtensionRunner {
562
564
  getAllTools: () => this.#getAllToolsFn(),
563
565
  resolveTool: name => this.#getResolveToolFn(name),
564
566
  cycleModel: async () => await this.#cycleModelFn?.(),
567
+ setModelProfile: async name => (await this.#setModelProfileFn?.(name)) ?? false,
565
568
  cycleThinkingLevel: () => this.#cycleThinkingLevelFn?.(),
566
569
  setQueueMode: (kind, mode) => this.#setQueueModeFn?.(kind, mode) ?? false,
567
570
  invokeSkill: async (name, args) => await this.#invokeSkillFn?.(name, args),
@@ -581,6 +584,7 @@ export class ExtensionRunner {
581
584
  setSdkPermissionProvider: provider => this.#setSdkPermissionProviderFn?.(provider),
582
585
  sdkBindings: () => [
583
586
  ...(this.#cycleModelFn ? ["cycleModel"] : []),
587
+ ...(this.#setModelProfileFn ? ["setModelProfile"] : []),
584
588
  ...(this.#cycleThinkingLevelFn ? ["cycleThinkingLevel"] : []),
585
589
  ...(this.#setQueueModeFn ? ["setQueueMode"] : []),
586
590
  ...(this.#getSkillStateFn ? ["getSkillState"] : []),
@@ -290,6 +290,18 @@ export interface ExtensionTranscriptEntry {
290
290
  textSummary: string;
291
291
  ts: string;
292
292
  body?: string;
293
+ /**
294
+ * Durable, image-free message blocks used by rich transcript consumers such
295
+ * as ACP replay. Binary image payloads intentionally remain unavailable.
296
+ */
297
+ content?: Array<
298
+ | { type: "text"; text: string }
299
+ | { type: "thinking"; thinking: string }
300
+ | { type: "toolCall"; id: string; name: string; arguments: unknown }
301
+ >;
302
+ toolCallId?: string;
303
+ toolName?: string;
304
+ isError?: boolean;
293
305
  }
294
306
 
295
307
  export interface ContextUsage {
@@ -361,6 +373,7 @@ export interface ExtensionContext {
361
373
  resolveTool(name: string): Pick<Tool, "safeSummary" | "safeSummaryFields"> | undefined;
362
374
  /** Session control seams used by the SDK host. */
363
375
  cycleModel(): Promise<{ model: Model; thinkingLevel: ThinkingLevel | undefined } | undefined>;
376
+ setModelProfile?(name: string): Promise<boolean>;
364
377
  cycleThinkingLevel(): ThinkingLevel | undefined;
365
378
  setQueueMode(kind: "steering" | "follow_up" | "interrupt", mode: unknown): boolean;
366
379
  getSkillState(): unknown;
@@ -1429,6 +1442,7 @@ export interface ExtensionContextActions {
1429
1442
  clearContext?: () => Promise<boolean>;
1430
1443
  /** Session control and query seams exposed to the per-session SDK host. */
1431
1444
  cycleModel?: () => Promise<{ model: Model; thinkingLevel: ThinkingLevel | undefined } | undefined>;
1445
+ setModelProfile?: (name: string) => Promise<boolean>;
1432
1446
  cycleThinkingLevel?: () => ThinkingLevel | undefined;
1433
1447
  setQueueMode?: (kind: "steering" | "follow_up" | "interrupt", mode: unknown) => boolean;
1434
1448
  getSkillState?: () => unknown;
@@ -207,8 +207,22 @@ export function deriveAmbiguityMilestone(
207
207
 
208
208
  export function scoreToUnits(score: number): number {
209
209
  if (!Number.isFinite(score) || score < 0 || score > 1) throw new RangeError("score must be finite in [0, 1]");
210
- const units = score * 10_000;
211
- if (!Number.isSafeInteger(units)) throw new RangeError("score must be expressed in integral 1e-4 units");
210
+ /**
211
+ * Scores carry at most four decimal places, but `score * 10_000` misses the
212
+ * integer grid for ordinary values: `0.69 * 10_000` is `6900.000000000001` and
213
+ * `0.07 * 10_000` is `700.0000000000001`. Ambiguity is fed straight back in as
214
+ * `units / 10_000`, so testing the float product rejected most real scores.
215
+ *
216
+ * Decide from the shortest round-trip decimal instead. That is exact: every
217
+ * genuine 1e-4 value has at most four fraction digits, while off-grid
218
+ * precision keeps its digits (`0.00005`, `0.05000000000000001`) and is
219
+ * rejected. An epsilon on the product cannot separate those two cases -- both
220
+ * residuals are ~1e-13. The `[0, 1]` guard plus the four-digit cap already
221
+ * bound the result to an integer in `[0, 10_000]`.
222
+ */
223
+ const decimal = /^(\d+)(?:\.(\d{1,4}))?$/.exec(String(score));
224
+ if (!decimal) throw new RangeError("score must be expressed in integral 1e-4 units");
225
+ const units = Number(decimal[1]) * 10_000 + Number((decimal[2] ?? "").padEnd(4, "0"));
212
226
  return units;
213
227
  }
214
228