@gajae-code/coding-agent 0.16.7 → 0.17.1

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 (321) hide show
  1. package/CHANGELOG.md +180 -1
  2. package/dist/types/capability/fs.d.ts +37 -4
  3. package/dist/types/capability/index.d.ts +19 -0
  4. package/dist/types/capability/types.d.ts +9 -0
  5. package/dist/types/cli/doctor/args.d.ts +36 -0
  6. package/dist/types/cli/doctor/checks.d.ts +18 -0
  7. package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
  8. package/dist/types/cli/doctor/context.d.ts +89 -0
  9. package/dist/types/cli/doctor/files.d.ts +27 -0
  10. package/dist/types/cli/doctor/ids.d.ts +20 -0
  11. package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
  12. package/dist/types/cli/doctor/journal.d.ts +43 -0
  13. package/dist/types/cli/doctor/managed-link.d.ts +39 -0
  14. package/dist/types/cli/doctor/native-probe.d.ts +2 -0
  15. package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
  16. package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
  17. package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
  18. package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
  19. package/dist/types/cli/doctor/probe-client.d.ts +9 -0
  20. package/dist/types/cli/doctor/probe-types.d.ts +18 -0
  21. package/dist/types/cli/doctor/probe.d.ts +3 -0
  22. package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
  23. package/dist/types/cli/doctor/repairs.d.ts +28 -0
  24. package/dist/types/cli/doctor/report.d.ts +27 -0
  25. package/dist/types/cli/doctor/runner.d.ts +4 -0
  26. package/dist/types/cli/doctor/safety.d.ts +10 -0
  27. package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
  28. package/dist/types/cli/doctor/service-targets.d.ts +41 -0
  29. package/dist/types/cli/doctor/types.d.ts +145 -0
  30. package/dist/types/cli/doctor-cli.d.ts +7 -0
  31. package/dist/types/cli/doctor-supervisor.d.ts +121 -0
  32. package/dist/types/cli/doctor-worker.d.ts +29 -0
  33. package/dist/types/cli/install-activation.d.ts +158 -0
  34. package/dist/types/cli/macos-community-app.d.ts +41 -0
  35. package/dist/types/cli/mcp-cli.d.ts +0 -11
  36. package/dist/types/cli/update-cli.d.ts +41 -4
  37. package/dist/types/cli-main.d.ts +174 -0
  38. package/dist/types/cli.d.ts +1 -171
  39. package/dist/types/commands/doctor.d.ts +7 -0
  40. package/dist/types/commands/sdk.d.ts +17 -8
  41. package/dist/types/config/autorouting-tier-map.d.ts +19 -0
  42. package/dist/types/config/file-lock.d.ts +27 -3
  43. package/dist/types/config/model-preset-registry.d.ts +1 -0
  44. package/dist/types/config/model-registry.d.ts +12 -5
  45. package/dist/types/config/model-resolver.d.ts +16 -3
  46. package/dist/types/config/models-config-schema.d.ts +2 -0
  47. package/dist/types/config/settings-schema.d.ts +1 -1
  48. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
  49. package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
  50. package/dist/types/cursor.d.ts +9 -9
  51. package/dist/types/discovery/agents-md.d.ts +6 -1
  52. package/dist/types/discovery/helpers.d.ts +36 -7
  53. package/dist/types/discovery/index.d.ts +1 -1
  54. package/dist/types/discovery/mcp-json.d.ts +2 -0
  55. package/dist/types/extensibility/extensions/loader.d.ts +22 -1
  56. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
  57. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
  58. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  59. package/dist/types/extensibility/plugins/manager.d.ts +33 -4
  60. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
  61. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
  62. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
  63. package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
  64. package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
  65. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
  66. package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
  67. package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
  68. package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
  69. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  70. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
  71. package/dist/types/main.d.ts +19 -1
  72. package/dist/types/modes/acp/acp-agent.d.ts +40 -0
  73. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  74. package/dist/types/modes/components/model-selector.d.ts +2 -0
  75. package/dist/types/modes/interactive-mode.d.ts +1 -1
  76. package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
  77. package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
  78. package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
  79. package/dist/types/runtime-mcp/types.d.ts +2 -2
  80. package/dist/types/sdk/acp/adapter.d.ts +5 -1
  81. package/dist/types/sdk/broker/broker.d.ts +51 -0
  82. package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
  83. package/dist/types/sdk/broker/discovery.d.ts +38 -0
  84. package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
  85. package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
  86. package/dist/types/sdk/broker/ensure.d.ts +14 -0
  87. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  88. package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
  89. package/dist/types/sdk/broker/runtime.d.ts +26 -0
  90. package/dist/types/sdk/broker/session-index.d.ts +13 -4
  91. package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
  92. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
  93. package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
  94. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
  95. package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
  96. package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
  97. package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
  98. package/dist/types/sdk/bus/index.d.ts +2 -0
  99. package/dist/types/sdk/bus/notification-service.d.ts +3 -0
  100. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
  101. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  102. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
  103. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
  104. package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
  105. package/dist/types/sdk/cli/session-cli.d.ts +1 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  107. package/dist/types/sdk/host/surface-policy.d.ts +3 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +44 -10
  109. package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
  111. package/dist/types/sdk/prompt-failure.d.ts +47 -0
  112. package/dist/types/sdk/prompt-status.d.ts +19 -0
  113. package/dist/types/sdk/router/session-router.d.ts +3 -4
  114. package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
  115. package/dist/types/sdk/session-authority.d.ts +9 -0
  116. package/dist/types/sdk/session.d.ts +3 -1
  117. package/dist/types/session/agent-session.d.ts +1 -1
  118. package/dist/types/session/blob-store.d.ts +1 -1
  119. package/dist/types/session/fallback-chain-controller.d.ts +3 -6
  120. package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
  121. package/dist/types/session/primary-control-surface.d.ts +4 -0
  122. package/dist/types/session/session-manager.d.ts +6 -0
  123. package/dist/types/system-prompt.d.ts +6 -0
  124. package/dist/types/tools/ask.d.ts +25 -0
  125. package/dist/types/tools/image-gen.d.ts +0 -4
  126. package/dist/types/tools/skill-discovery.d.ts +7 -5
  127. package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
  128. package/dist/types/utils/changelog.d.ts +0 -9
  129. package/dist/types/utils/mupdf-wasm.d.ts +1 -0
  130. package/package.json +9 -8
  131. package/scripts/compile-args.ts +8 -4
  132. package/src/capability/fs.ts +395 -17
  133. package/src/capability/index.ts +273 -10
  134. package/src/capability/types.ts +9 -0
  135. package/src/cli/customize-doctor.ts +18 -12
  136. package/src/cli/doctor/args.ts +236 -0
  137. package/src/cli/doctor/checks.ts +431 -0
  138. package/src/cli/doctor/config-repairs.ts +517 -0
  139. package/src/cli/doctor/context.ts +149 -0
  140. package/src/cli/doctor/files.ts +149 -0
  141. package/src/cli/doctor/ids.ts +115 -0
  142. package/src/cli/doctor/install-repairs.ts +335 -0
  143. package/src/cli/doctor/journal.ts +423 -0
  144. package/src/cli/doctor/managed-link.ts +514 -0
  145. package/src/cli/doctor/native-probe.ts +13 -0
  146. package/src/cli/doctor/permission-repairs.ts +242 -0
  147. package/src/cli/doctor/plugin-quarantine.ts +377 -0
  148. package/src/cli/doctor/plugin-restore.ts +59 -0
  149. package/src/cli/doctor/plugin-targets.ts +373 -0
  150. package/src/cli/doctor/probe-client.ts +195 -0
  151. package/src/cli/doctor/probe-types.ts +21 -0
  152. package/src/cli/doctor/probe.ts +42 -0
  153. package/src/cli/doctor/projection-probe.ts +249 -0
  154. package/src/cli/doctor/repairs.ts +1157 -0
  155. package/src/cli/doctor/report.ts +176 -0
  156. package/src/cli/doctor/runner.ts +342 -0
  157. package/src/cli/doctor/safety.ts +40 -0
  158. package/src/cli/doctor/service-repairs.ts +452 -0
  159. package/src/cli/doctor/service-targets.ts +274 -0
  160. package/src/cli/doctor/types.ts +164 -0
  161. package/src/cli/doctor-cli.ts +62 -0
  162. package/src/cli/doctor-supervisor.ts +571 -0
  163. package/src/cli/doctor-worker.ts +181 -0
  164. package/src/cli/install-activation.ts +666 -0
  165. package/src/cli/macos-community-app.ts +1449 -0
  166. package/src/cli/mcp-cli.ts +11 -21
  167. package/src/cli/nofile-limit.ts +5 -1
  168. package/src/cli/plugin-cli.ts +6 -1
  169. package/src/cli/update-cli.ts +362 -264
  170. package/src/cli-main.ts +517 -0
  171. package/src/cli.ts +64 -433
  172. package/src/commands/autoresearch.ts +11 -5
  173. package/src/commands/completion.ts +1 -1
  174. package/src/commands/doctor.ts +13 -0
  175. package/src/commands/sdk.ts +58 -10
  176. package/src/commands/ultragoal.ts +11 -5
  177. package/src/config/autorouting-tier-map.ts +7 -0
  178. package/src/config/file-lock.ts +736 -71
  179. package/src/config/mcp-schema.json +3 -3
  180. package/src/config/model-preset-registry.ts +1 -0
  181. package/src/config/model-registry.ts +80 -27
  182. package/src/config/model-resolver.ts +33 -4
  183. package/src/config/models-config-schema.ts +2 -0
  184. package/src/config/settings-schema.ts +6 -2
  185. package/src/coordinator-mcp/question-gate-codec.ts +1 -0
  186. package/src/coordinator-mcp/server.ts +534 -51
  187. package/src/coordinator-mcp/session-reaper.ts +64 -8
  188. package/src/cursor.ts +223 -53
  189. package/src/discovery/agents-md.ts +74 -8
  190. package/src/discovery/agents.ts +32 -7
  191. package/src/discovery/builtin.ts +184 -75
  192. package/src/discovery/claude-plugins.ts +85 -17
  193. package/src/discovery/cline.ts +38 -10
  194. package/src/discovery/cursor.ts +19 -6
  195. package/src/discovery/gemini.ts +62 -22
  196. package/src/discovery/github.ts +13 -3
  197. package/src/discovery/helpers.ts +483 -89
  198. package/src/discovery/index.ts +1 -0
  199. package/src/discovery/mcp-json.ts +36 -10
  200. package/src/discovery/opencode.ts +36 -19
  201. package/src/discovery/ssh.ts +11 -8
  202. package/src/discovery/windsurf.ts +12 -3
  203. package/src/exec/bash-executor.ts +10 -3
  204. package/src/exec/isolated-shell.ts +15 -2
  205. package/src/extensibility/extensions/loader.ts +88 -26
  206. package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
  207. package/src/extensibility/gjc-plugins/paths.ts +4 -1
  208. package/src/extensibility/gjc-plugins/types.ts +27 -0
  209. package/src/extensibility/plugins/manager.ts +200 -22
  210. package/src/extensibility/plugins/marketplace/cache.ts +55 -2
  211. package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
  212. package/src/extensibility/plugins/marketplace/registry.ts +153 -0
  213. package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
  214. package/src/extensibility/runtime-skill-discovery.ts +61 -9
  215. package/src/extensibility/skill-management.ts +7 -1
  216. package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
  217. package/src/gjc-runtime/launch-worktree.test.ts +248 -0
  218. package/src/gjc-runtime/launch-worktree.ts +275 -38
  219. package/src/gjc-runtime/repository-binding.ts +26 -1
  220. package/src/gjc-runtime/session-layout.ts +10 -0
  221. package/src/gjc-runtime/session-state-lock.ts +330 -23
  222. package/src/gjc-runtime/session-state-sidecar.ts +328 -80
  223. package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
  224. package/src/harness-control-plane/session-lease.ts +2 -2
  225. package/src/internal-urls/docs-index.generated.ts +28 -26
  226. package/src/main.ts +150 -29
  227. package/src/memories/index.ts +24 -2
  228. package/src/modes/acp/acp-agent.ts +522 -68
  229. package/src/modes/components/agent-dashboard.ts +3 -1
  230. package/src/modes/components/model-selector.ts +28 -19
  231. package/src/modes/components/plugin-settings.ts +2 -1
  232. package/src/modes/controllers/selector-controller.ts +15 -9
  233. package/src/modes/interactive-mode.ts +21 -3
  234. package/src/prompts/system/system-prompt.md +5 -1
  235. package/src/prompts/tools/browser.md +1 -1
  236. package/src/prompts/tools/move-session.md +4 -0
  237. package/src/prompts/tools/read.md +2 -1
  238. package/src/prompts/tools/skill-discovery.md +6 -5
  239. package/src/prompts/tools/task.md +1 -0
  240. package/src/runtime-mcp/config-writer.ts +228 -0
  241. package/src/runtime-mcp/oauth-discovery.ts +114 -14
  242. package/src/runtime-mcp/policy-json-edit.ts +129 -0
  243. package/src/runtime-mcp/startup-policy.ts +14 -0
  244. package/src/runtime-mcp/types.ts +2 -2
  245. package/src/sdk/acp/adapter.ts +27 -12
  246. package/src/sdk/broker/broker.ts +273 -44
  247. package/src/sdk/broker/daemon-entry.ts +119 -0
  248. package/src/sdk/broker/discovery.ts +173 -20
  249. package/src/sdk/broker/doctor-restart.ts +206 -0
  250. package/src/sdk/broker/endpoint-authority.ts +36 -0
  251. package/src/sdk/broker/ensure.ts +49 -14
  252. package/src/sdk/broker/lifecycle.ts +174 -42
  253. package/src/sdk/broker/process-incarnation.ts +86 -2
  254. package/src/sdk/broker/runtime.ts +145 -21
  255. package/src/sdk/broker/session-index.ts +136 -22
  256. package/src/sdk/broker/spawn-substrate.ts +4 -1
  257. package/src/sdk/broker/transport.ts +5 -0
  258. package/src/sdk/broker/worktree-occupancy.ts +21 -1
  259. package/src/sdk/bus/chat-daemon-cli.ts +63 -0
  260. package/src/sdk/bus/chat-daemon-control.ts +260 -64
  261. package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
  262. package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
  263. package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
  264. package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
  265. package/src/sdk/bus/index.ts +252 -51
  266. package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
  267. package/src/sdk/bus/notification-service.ts +21 -3
  268. package/src/sdk/bus/reconciliation-store.ts +17 -11
  269. package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
  270. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  271. package/src/sdk/bus/telegram-daemon-control.ts +47 -0
  272. package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
  273. package/src/sdk/bus/telegram-daemon.ts +178 -13
  274. package/src/sdk/cli/session-cli.ts +100 -12
  275. package/src/sdk/host/host.ts +2 -7
  276. package/src/sdk/host/session-runtime.test.ts +329 -8
  277. package/src/sdk/host/session-runtime.ts +202 -50
  278. package/src/sdk/host/surface-policy.ts +8 -1
  279. package/src/sdk/lifecycle/service.ts +199 -22
  280. package/src/sdk/mcp/server.ts +1 -1
  281. package/src/sdk/prompt-deadline-lease.ts +12 -4
  282. package/src/sdk/prompt-deadline-manager.ts +5 -7
  283. package/src/sdk/prompt-failure.ts +148 -0
  284. package/src/sdk/prompt-status.ts +30 -1
  285. package/src/sdk/prompt-watchdog.ts +11 -3
  286. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  287. package/src/sdk/protocol/operation-registry.ts +20 -8
  288. package/src/sdk/router/session-router.ts +8 -3
  289. package/src/sdk/service-artifact-paths.ts +26 -0
  290. package/src/sdk/session-authority.ts +10 -0
  291. package/src/sdk/session.ts +361 -194
  292. package/src/session/agent-session.ts +398 -93
  293. package/src/session/blob-store.ts +9 -9
  294. package/src/session/fallback-chain-controller.ts +3 -7
  295. package/src/session/internal/managed-session-scope.ts +445 -29
  296. package/src/session/internal/managed-session-storage.ts +38 -6
  297. package/src/session/primary-control-surface.ts +12 -0
  298. package/src/session/session-manager.ts +11 -3
  299. package/src/session/session-storage.ts +1 -1
  300. package/src/session-import/codex.ts +6 -1
  301. package/src/session-import/redact.ts +7 -0
  302. package/src/skill-state/workflow-mutation-guard.ts +645 -39
  303. package/src/slash-commands/builtin-registry.ts +7 -3
  304. package/src/system-prompt.ts +8 -0
  305. package/src/task/executor.ts +36 -3
  306. package/src/task/types.ts +27 -5
  307. package/src/tools/ask.ts +114 -34
  308. package/src/tools/browser/actions.ts +2 -2
  309. package/src/tools/browser/tab-worker.ts +11 -8
  310. package/src/tools/descriptors.ts +6 -1
  311. package/src/tools/fetch.ts +7 -2
  312. package/src/tools/image-gen.ts +33 -34
  313. package/src/tools/read.ts +78 -32
  314. package/src/tools/skill-discovery.ts +17 -7
  315. package/src/tools/tool-catalog.generated.ts +8 -8
  316. package/src/tools/ultragoal-ask-guard.ts +16 -26
  317. package/src/utils/changelog.ts +7 -1
  318. package/src/utils/git.ts +3 -1
  319. package/src/utils/markit.ts +24 -2
  320. package/src/utils/mupdf-wasm.ts +26 -0
  321. package/src/web/search/providers/openai-compatible.ts +191 -14
@@ -436,7 +436,11 @@ async function resolveModelCommandSelection(
436
436
  const providerRef = parseProviderQualifiedSelector(selector);
437
437
  const discoverableProviders = runtime.session.modelRegistry?.getDiscoverableProviders?.() ?? [];
438
438
  if (providerRef && discoverableProviders.includes(providerRef.provider)) {
439
- await runtime.session.modelRegistry.refreshProvider?.(providerRef.provider, "online");
439
+ await runtime.session.modelRegistry.refreshProvider?.(
440
+ providerRef.provider,
441
+ "online",
442
+ runtime.session.credentialSessionId,
443
+ );
440
444
  availableModels = runtime.session.getAvailableModels?.() ?? [];
441
445
  const refreshedSelection = resolveModelCommandSelectionFromAvailable(
442
446
  runtime,
@@ -1699,7 +1703,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1699
1703
  models: parsed.models,
1700
1704
  force: parsed.force,
1701
1705
  });
1702
- await runtime.session.modelRegistry.refresh("offline");
1706
+ await runtime.session.modelRegistry.refresh("offline", runtime.session.credentialSessionId);
1703
1707
  await runtime.output(formatProviderSetupResult(result));
1704
1708
  await runtime.notifyConfigChanged?.();
1705
1709
  return commandConsumed();
@@ -1740,7 +1744,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1740
1744
  models: parsed.models,
1741
1745
  force: parsed.force,
1742
1746
  });
1743
- await runtime.ctx.session.modelRegistry.refresh("offline");
1747
+ await runtime.ctx.session.modelRegistry.refresh("offline", runtime.ctx.session.credentialSessionId);
1744
1748
  runtime.ctx.showStatus(formatProviderSetupResult(result));
1745
1749
  } catch (err) {
1746
1750
  runtime.ctx.showError(`Provider setup failed: ${errorMessage(err)}`);
@@ -406,6 +406,12 @@ export interface BuildSystemPromptOptions {
406
406
  alwaysApplyRules?: AlwaysApplyRule[];
407
407
  /** Whether secret obfuscation is active. When true, explains the redaction format in the prompt. */
408
408
  secretsEnabled?: boolean;
409
+ /**
410
+ * Whether the `task` tool advertises the `isolated` parameter in this session
411
+ * (`task.isolation.mode !== "none"`). Worktree routing guidance depends on it:
412
+ * without isolation the agent must use a plain `git worktree`, never refuse.
413
+ */
414
+ taskIsolationEnabled?: boolean;
409
415
  /** Pre-loaded workspace tree (skips discovery if provided). May be a Promise to allow early kick-off. */
410
416
  workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
411
417
  /**
@@ -570,6 +576,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
570
576
  toolDiscoveryActive = false,
571
577
  eagerTasks = false,
572
578
  secretsEnabled = false,
579
+ taskIsolationEnabled = false,
573
580
  workspaceTree: providedWorkspaceTree,
574
581
  subagent = false,
575
582
  } = options;
@@ -749,6 +756,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
749
756
  toolDiscoveryActive: toolDiscoveryActive && hasHiddenToolDiscoveryTool,
750
757
  eagerTasks,
751
758
  secretsEnabled,
759
+ taskIsolationEnabled,
752
760
  subagent,
753
761
  };
754
762
  const rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
@@ -34,7 +34,11 @@ import { logger, prompt, untilAborted } from "@gajae-code/utils";
34
34
  import { AsyncJobManager } from "../async";
35
35
  import { AUTOROUTING_SELECTOR_MAX_LENGTH, type AutoroutingReasonCode } from "../config/autorouting-contract";
36
36
  import { ModelRegistry } from "../config/model-registry";
37
- import { formatModelString, resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
37
+ import {
38
+ formatModelString,
39
+ isExplicitProviderModelOverride,
40
+ resolveModelOverrideWithAuthFallback,
41
+ } from "../config/model-resolver";
38
42
  import type { PromptTemplate } from "../config/prompt-templates";
39
43
  import { Settings } from "../config/settings";
40
44
  import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
@@ -921,7 +925,13 @@ function formatExecutionError(error: unknown): string {
921
925
  const causes = error.errors.map(cause => (cause instanceof Error ? cause.message : String(cause))).join("; ");
922
926
  return causes ? `${error.message}: ${causes}` : error.message;
923
927
  }
924
- return error instanceof Error ? error.stack || error.message : String(error);
928
+ if (!(error instanceof Error)) return String(error);
929
+ const message = error.message;
930
+ const stack = error.stack;
931
+ if (!stack) return message;
932
+ const stackHeadline = stack.split(/\r?\n/u, 1)[0] ?? "";
933
+ if (!message || stackHeadline.includes(message)) return stack;
934
+ return `${error.name || "Error"}: ${message}\n${stack}`;
925
935
  }
926
936
 
927
937
  function transportFactsFromError(error: unknown): TransportFailureFacts | undefined {
@@ -1730,7 +1740,12 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
1730
1740
  }
1731
1741
  checkAbort();
1732
1742
  if (!registryFromParent) {
1733
- await awaitAbortable(modelRegistry.refresh());
1743
+ await awaitAbortable(
1744
+ modelRegistry.refresh(
1745
+ "online-if-uncached",
1746
+ options.parentCredentialSessionId ?? options.parentSessionId,
1747
+ ),
1748
+ );
1734
1749
  } else {
1735
1750
  logger.debug("runSubagent: reusing parent modelRegistry; skipping refresh");
1736
1751
  }
@@ -1763,6 +1778,24 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
1763
1778
  canonicalChildScope,
1764
1779
  ),
1765
1780
  );
1781
+ if (!model && isExplicitProviderModelOverride(modelPatterns)) {
1782
+ const skipReasons = [...new Set(skips.map(skip => skip.reason))];
1783
+ if ((options.preflightProbe || options.preflightDurable) && skipReasons.includes("unauthenticated")) {
1784
+ preflightOperation = "auth_resolve";
1785
+ throw Object.assign(new Error("autorouting credential unavailable"), {
1786
+ transient: false,
1787
+ credentialMissing: true,
1788
+ });
1789
+ }
1790
+ const reason = skipReasons.includes("unauthenticated")
1791
+ ? "authentication failed"
1792
+ : skipReasons.includes("unknown_model")
1793
+ ? "is missing from the catalog"
1794
+ : "could not be resolved";
1795
+ throw new Error(
1796
+ `Requested model ${modelPatterns.join(", ")} ${reason}. Explicit provider/model selectors fail closed and do not fall back to the parent session model.`,
1797
+ );
1798
+ }
1766
1799
  if (
1767
1800
  (options.preflightProbe || options.preflightDurable) &&
1768
1801
  model &&
package/src/task/types.ts CHANGED
@@ -92,7 +92,9 @@ const repositoryBindingSchema = z
92
92
  branch: z.string().min(1).optional(),
93
93
  })
94
94
  .strict()
95
- .describe("authoritative repository identity for multi-repo fail-closed spawn");
95
+ .describe(
96
+ "authoritative repository identity for multi-repo fail-closed spawn; must match the active session worktree. To target another approved repository, start a new session there (for example, gjc --cwd <approved-worktree>); changing tasks[].repositoryBinding cannot authorize a foreign repository.",
97
+ );
96
98
 
97
99
  const createTaskItemSchema = (_contextEnabled: boolean) =>
98
100
  z.object({
@@ -118,7 +120,7 @@ const createTaskItemSchema = (_contextEnabled: boolean) =>
118
120
  repositoryBinding: repositoryBindingSchema
119
121
  .optional()
120
122
  .describe(
121
- "authoritative repository identity; omitted items are stamped from session cwd before discovery/spawn and still fail closed on sibling drift",
123
+ "authoritative repository identity; omitted items are stamped from session cwd before discovery/spawn and still fail closed on sibling drift. A declared binding must match the active session; use a new session rooted in the approved worktree (for example, gjc --cwd <approved-worktree>) for another repository.",
122
124
  ),
123
125
  duplicate_policy: z.enum(["warn", "supersede"]).optional().describe("duplicate launch policy; defaults to warn"),
124
126
  });
@@ -352,12 +354,31 @@ const SETUP_FAILURE_SUMMARY_MAX_CHARS = 280;
352
354
  const SETUP_FAILURE_SUMMARY_MAX_BYTES = 1_024;
353
355
  const AUTHORIZATION_HEADER_VALUE_PATTERN = /(["']?(?:Proxy-)?Authorization\b["']?\s*:\s*)[^\r\n]*/gi;
354
356
  const COOKIE_HEADER_VALUE_PATTERN = /(["']?(?:Set-)?Cookie\b["']?\s*:\s*)[^\r\n]*/gi;
355
- const URL_CREDENTIAL_PATTERN = /([a-z][a-z0-9+.-]*:\/\/)[^/?#\s:@]+:[^@/?#\s]+@/gi;
357
+ // The scheme-character run is boundary anchored, so the unbounded suffix is
358
+ // attempted once per maximal run instead of once at every prefix. Keeping the
359
+ // leading non-letter characters in the capture preserves redaction for URLs
360
+ // embedded after digits or scheme punctuation without imposing an arbitrary
361
+ // scheme-length cap.
362
+ const URL_CREDENTIAL_PATTERN = /(?<![A-Za-z0-9+.-])([0-9+.-]*[a-z][a-z0-9+.-]*:\/\/)[^/?#\s:@]+:[^@/?#\s]+@/gi;
356
363
  const API_KEY_LABEL_VALUE_PATTERN = /(["']?api\s+key["']?\s*:\s*)(?:"[^"]*"|'[^']*'|[^\s&]+)/gi;
364
+ // Both name-prefix repetitions are bounded. `(?:[A-Za-z][A-Za-z0-9]*[_.-])*?`
365
+ // nests an unbounded quantifier inside an unbounded quantified group, so at every
366
+ // offset of a long identifier run the inner class scans to the end of the run
367
+ // before backtracking to look for a separator. That is quadratic in the length of
368
+ // the failure text: 50 KB cost ~1.7s. Sixteen segments of at most 64 characters is
369
+ // far past any real credential name, and the suffix group below still absorbs
370
+ // arbitrary trailing segments.
357
371
  const SENSITIVE_SETUP_FAILURE_VALUE_PATTERN =
358
- /(["']?(?:(?:[A-Za-z][A-Za-z0-9]*[_.-])*?)(?:access[_-]?token|refresh[_-]?token|session[_-]?token|id[_-]?token|api[_-]?key|client[_-]?secret|private[_-]?key|signing[_-]?key|secret|password|passwd|pwd|authorization|credential|token)(?:[_.-][A-Za-z0-9]+)*["']?)(\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s&]+)/gi;
372
+ /(["']?(?:[A-Za-z][A-Za-z0-9]{0,63}[_.-]){0,16}?(?:access[_-]?token|refresh[_-]?token|session[_-]?token|id[_-]?token|api[_-]?key|client[_-]?secret|private[_-]?key|signing[_-]?key|secret|password|passwd|pwd|authorization|credential|token)(?:[_.-][A-Za-z0-9]+)*["']?)(\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s&]+)/gi;
373
+ // `AKIA` is only one of four AWS access-key id prefixes, and the npm, GitLab,
374
+ // Stripe and Hugging Face shapes are the ones `crash/upstream/envelope.ts`
375
+ // already classifies as credential-like. Stripe and Hugging Face separate with
376
+ // `_`, so the `sk-` alternatives above never matched them.
359
377
  const BARE_PROVIDER_TOKEN_PATTERN =
360
- /\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|sk-(?:proj-)?[A-Za-z0-9_-]{20,}|sk-ant-[A-Za-z0-9_-]{20,}|AIza[A-Za-z0-9_-]{35}|AKIA[A-Z0-9]{16})\b/g;
378
+ /\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|sk-(?:proj-)?[A-Za-z0-9_-]{20,}|sk-ant-[A-Za-z0-9_-]{20,}|AIza[A-Za-z0-9_-]{35}|(?:AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}|npm_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{20,}|(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}|hf_[A-Za-z0-9]{20,})\b/g;
379
+ // A PEM block carries the key material itself and is redacted whole, before the
380
+ // narrower rules can consume its base64 body and leave a truncated key behind.
381
+ const PEM_PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g;
361
382
  const LOCAL_ABSOLUTE_PATH_PATTERN = /(^|[\s("'`=])((?:\/(?!\/)[^\s/:?"'`()[\]{},;<>]+){2,})/g;
362
383
  const WINDOWS_ABSOLUTE_PATH_PATTERN =
363
384
  /(^|[\s("'`=])([A-Za-z]:\\(?:[^\\/:?"'`()[\]{},;<>\s]+\\)+[^\\/:?"'`()[\]{},;<>\s]+)/g;
@@ -417,6 +438,7 @@ function capSetupFailureSummary(value: string): string {
417
438
  export function createSetupFailureSummary(error: unknown): SetupFailureSummary {
418
439
  const message = normalizeSetupFailureText(error instanceof Error ? error.message : String(error));
419
440
  const summary = message
441
+ .replace(PEM_PRIVATE_KEY_PATTERN, "[redacted]")
420
442
  .replace(AUTHORIZATION_HEADER_VALUE_PATTERN, "$1[redacted]")
421
443
  .replace(COOKIE_HEADER_VALUE_PATTERN, "$1[redacted]")
422
444
  .replace(URL_CREDENTIAL_PATTERN, "$1[redacted]@")
package/src/tools/ask.ts CHANGED
@@ -109,6 +109,37 @@ export interface AskToolDetails {
109
109
  export const OTHER_OPTION = "Other (type your own)";
110
110
  export const ASK_CLARIFICATION_OPTION = "Ask about these choices";
111
111
  export const RECOMMENDED_SUFFIX = " (Recommended)";
112
+ /**
113
+ * Environment override bounding how long a headless ask waits for its answer, in
114
+ * milliseconds. Unset, empty, non-positive, non-integer, or larger than
115
+ * {@link MAX_ASK_ANSWER_DEADLINE_MS} disables the bound.
116
+ */
117
+ export const GJC_ASK_ANSWER_DEADLINE_MS_ENV = "GJC_ASK_ANSWER_DEADLINE_MS";
118
+ /**
119
+ * Largest deadline the runtime timer can represent. `setTimeout` silently clamps
120
+ * a longer delay to one millisecond, which would invert the contract and abort a
121
+ * headless ask almost immediately, so a value beyond this is not a usable bound
122
+ * and is treated as disabled exactly like any other invalid value.
123
+ */
124
+ export const MAX_ASK_ANSWER_DEADLINE_MS = 2_147_483_647;
125
+
126
+ /**
127
+ * How long a headless ask may wait for an answer before it gives up.
128
+ *
129
+ * A headless ask has no local selector, so it is answered only by a remote
130
+ * answer source or by a workflow gate waiting for a remote responder. Neither
131
+ * channel reports that the question was taken up, so the only observable event
132
+ * is the answer itself: this is an answer deadline, not an acknowledgement.
133
+ * `GJC_ASK_ANSWER_DEADLINE_MS` arms it. It stays disabled by default: an
134
+ * attended remote responder may legitimately answer arbitrarily late, and
135
+ * silently cutting such a wait short would corrupt a real answer into an abort.
136
+ */
137
+ export function resolveAskAnswerDeadlineMs(env: Record<string, string | undefined> = process.env): number | null {
138
+ const raw = env[GJC_ASK_ANSWER_DEADLINE_MS_ENV];
139
+ if (raw === undefined) return null;
140
+ const parsed = Number(raw);
141
+ return Number.isSafeInteger(parsed) && parsed > 0 && parsed <= MAX_ASK_ANSWER_DEADLINE_MS ? parsed : null;
142
+ }
112
143
  const REMOTE_NAVIGATION_FORWARD = "\u0000ask-navigation-forward";
113
144
  const HEADLESS_CHECKBOX_CHECKED = "[x]";
114
145
  const HEADLESS_CHECKBOX_UNCHECKED = "[ ]";
@@ -825,10 +856,7 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
825
856
  ): Promise<AgentToolResult<AskToolDetails>> {
826
857
  await assertUltragoalAskAllowed(
827
858
  this.session.cwd,
828
- {
829
- activeSkillState: this.session.getActiveSkillState?.(),
830
- sessionId: this.session.getSessionId?.() ?? null,
831
- },
859
+ { sessionId: this.session.getSessionId?.() ?? null },
832
860
  this.session.getSessionAgentDir?.() ?? this.session.settings.getAgentDir(),
833
861
  );
834
862
  assertDeepInterviewStructuredResponseWithinLimit(params);
@@ -871,7 +899,55 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
871
899
  throw new ToolAbortError("Ask tool requires interactive mode");
872
900
  }
873
901
 
902
+ // Only a headless ask needs an answer deadline: an interactive UI answers
903
+ // locally, and its own `ask.timeout` already applies there.
904
+ const askAnswerDeadlineMs = hasInteractiveUi ? null : resolveAskAnswerDeadlineMs();
905
+ const askDeadlineExpired = Symbol("ask-deadline-expired");
906
+ /**
907
+ * Bound one headless answer wait. No answer source reports that it has taken
908
+ * the question, so the only observable event is the answer itself; on expiry
909
+ * the ask must fail with a distinguishable origin instead of degrading into
910
+ * the empty answer the model would otherwise read as "the user declined",
911
+ * and the enclosing turn is aborted so the stall cannot outlive the call.
912
+ */
913
+ const awaitAskAnswer = async <T>(work: () => Promise<T>): Promise<T> => {
914
+ if (askAnswerDeadlineMs === null) return await work();
915
+ const expired = Promise.withResolvers<typeof askDeadlineExpired>();
916
+ const timer = setTimeout(() => expired.resolve(askDeadlineExpired), askAnswerDeadlineMs);
917
+ try {
918
+ const settled = await Promise.race([work(), expired.promise]);
919
+ if (settled !== askDeadlineExpired) return settled;
920
+ logger.warn("ask_answer_deadline_exceeded", {
921
+ sessionId: this.session.getSessionId?.() ?? null,
922
+ // Re-derive the routed channel at expiry: the answer source can be
923
+ // registered or removed after the ask entered `execute`.
924
+ answerSource:
925
+ !this.session.getAskAnswerSource?.() && gateEmitter?.supportsRemoteGateAnswers() === true
926
+ ? "workflow_gate"
927
+ : "remote",
928
+ deadlineMs: askAnswerDeadlineMs,
929
+ });
930
+ await settleActiveRemote({ kind: "resolve_without_commit", reason: "aborted" });
931
+ activeRemoteRequest = undefined;
932
+ context?.abort();
933
+ throw new ToolAbortError(
934
+ `Ask was aborted: no answer was received within ${askAnswerDeadlineMs / 1000}s of the headless ask answer deadline`,
935
+ );
936
+ } finally {
937
+ clearTimeout(timer);
938
+ }
939
+ };
940
+
874
941
  const extensionUi = context?.ui;
942
+ const throwRemoteCancellation = (remoteSignal: AbortSignal, toolSignal?: AbortSignal): never => {
943
+ // A headless remote answer source owns the only interactive surface. When it
944
+ // explicitly closes the ask, abort the enclosing turn as well; otherwise the
945
+ // tool throws while the foreground prompt keeps running and the next client
946
+ // prompt is rejected as already active. An abort already initiated by the
947
+ // caller must not be reported as a second, remote cancellation.
948
+ if (!extensionUi && !remoteSignal.aborted && !toolSignal?.aborted) context?.abort();
949
+ throw new ToolAbortError("Ask was cancelled by the remote client");
950
+ };
875
951
  const ui: UIContext = {
876
952
  select: (prompt, options, dialogOptions) => {
877
953
  const source = this.session.getAskAnswerSource?.();
@@ -908,7 +984,7 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
908
984
  )
909
985
  .then((answer): RemoteRaceResult | Promise<RemoteRaceResult> => {
910
986
  if (answer === undefined) {
911
- if (!extensionUi) throw new ToolAbortError("Ask was cancelled by the remote client");
987
+ if (!extensionUi) return throwRemoteCancellation(remoteController.signal, toolSignal);
912
988
  return new Promise<never>(() => {});
913
989
  }
914
990
  const receipt = typeof answer === "string" ? legacyAskReceipt(answer) : answer;
@@ -989,20 +1065,22 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
989
1065
  // The losing selector may reject when aborted after the race already settled;
990
1066
  // swallow that so it is not an unhandled rejection (the race result is unaffected).
991
1067
  void local.catch(() => undefined);
992
- return Promise.race([local, remote])
993
- .then(async result => {
994
- if (result.winner === "remote") {
995
- localController.abort();
996
- if (result.settlement) await result.receipt.settle(result.settlement);
997
- else activeRemoteReceipt = result.receipt;
998
- } else {
999
- void remote.then(remoteResult =>
1000
- remoteResult.receipt.settle({ kind: "resolve_without_commit", reason: "aborted" }),
1001
- );
1002
- }
1003
- return result.value;
1004
- })
1005
- .finally(() => toolSignal?.removeEventListener("abort", abortRace));
1068
+ return awaitAskAnswer(() =>
1069
+ Promise.race([local, remote])
1070
+ .then(async result => {
1071
+ if (result.winner === "remote") {
1072
+ localController.abort();
1073
+ if (result.settlement) await result.receipt.settle(result.settlement);
1074
+ else activeRemoteReceipt = result.receipt;
1075
+ } else {
1076
+ void remote.then(remoteResult =>
1077
+ remoteResult.receipt.settle({ kind: "resolve_without_commit", reason: "aborted" }),
1078
+ );
1079
+ }
1080
+ return result.value;
1081
+ })
1082
+ .finally(() => toolSignal?.removeEventListener("abort", abortRace)),
1083
+ );
1006
1084
  },
1007
1085
  editor: (title, prefill, dialogOptions, editorOptions) => {
1008
1086
  const source = this.session.getAskAnswerSource?.();
@@ -1041,7 +1119,7 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1041
1119
  )
1042
1120
  .then((answer): RemoteRaceResult | Promise<RemoteRaceResult> => {
1043
1121
  if (answer === undefined) {
1044
- if (!extensionUi) throw new ToolAbortError("Ask was cancelled by the remote client");
1122
+ if (!extensionUi) return throwRemoteCancellation(remoteController.signal, toolSignal);
1045
1123
  return new Promise<never>(() => {});
1046
1124
  }
1047
1125
  const receipt = typeof answer === "string" ? legacyAskReceipt(answer) : answer;
@@ -1078,19 +1156,21 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1078
1156
  })
1079
1157
  : new Promise<never>(() => {});
1080
1158
  void local.catch(() => undefined);
1081
- return Promise.race([local, remote])
1082
- .then(result => {
1083
- if (result.winner === "remote") {
1084
- activeRemoteReceipt = result.receipt;
1085
- localController.abort();
1086
- } else {
1087
- void remote.then(remoteResult =>
1088
- remoteResult.receipt.settle({ kind: "resolve_without_commit", reason: "aborted" }),
1089
- );
1090
- }
1091
- return result.value;
1092
- })
1093
- .finally(() => toolSignal?.removeEventListener("abort", abortRace));
1159
+ return awaitAskAnswer(() =>
1160
+ Promise.race([local, remote])
1161
+ .then(result => {
1162
+ if (result.winner === "remote") {
1163
+ activeRemoteReceipt = result.receipt;
1164
+ localController.abort();
1165
+ } else {
1166
+ void remote.then(remoteResult =>
1167
+ remoteResult.receipt.settle({ kind: "resolve_without_commit", reason: "aborted" }),
1168
+ );
1169
+ }
1170
+ return result.value;
1171
+ })
1172
+ .finally(() => toolSignal?.removeEventListener("abort", abortRace)),
1173
+ );
1094
1174
  },
1095
1175
  };
1096
1176
 
@@ -1137,7 +1217,7 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
1137
1217
  allowEmpty: q.multi === true && params.questions.length > 1,
1138
1218
  navigationLabel: questionIndex === params.questions.length - 1 ? "Done" : "Next",
1139
1219
  };
1140
- const answer = await gateEmitter.emitGate(questionToGate(gateQuestion));
1220
+ const answer = await awaitAskAnswer(() => gateEmitter.emitGate(questionToGate(gateQuestion)));
1141
1221
  const decoded = gateAnswerToResult(gateQuestion, answer);
1142
1222
  return {
1143
1223
  optionLabels: rawOptionLabels,
@@ -154,7 +154,7 @@ for (const s of __steps) {
154
154
  __results.push({ verb: "select", selected: await tab.select(s.selector, ...(s.values || [])) });
155
155
  break;
156
156
  case "press":
157
- await tab.press(s.key, s.selector ? { selector: s.selector } : undefined);
157
+ await tab.press(s.key, s.selector !== undefined ? { selector: s.selector } : undefined);
158
158
  __results.push({ verb: "press", key: s.key });
159
159
  break;
160
160
  case "scroll":
@@ -166,7 +166,7 @@ for (const s of __steps) {
166
166
  __results.push({ verb: "back" });
167
167
  break;
168
168
  case "wait":
169
- if (s.selector) { await tab.waitFor(s.selector); }
169
+ if (s.selector !== undefined) { await tab.waitFor(s.selector); }
170
170
  else { await new Promise(r => setTimeout(r, s.ms)); }
171
171
  __results.push({ verb: "wait", selector: s.selector ?? null, ms: s.ms ?? null });
172
172
  break;
@@ -137,8 +137,12 @@ interface TabApi {
137
137
  id(n: number): Promise<ElementHandle>;
138
138
  }
139
139
 
140
- function normalizeSelector(selector: string): string {
141
- if (!selector) return selector;
140
+ function normalizeSelector(selector: unknown): string {
141
+ if (typeof selector !== "string" || selector.trim().length === 0) {
142
+ throw new ToolError(
143
+ "Selector must be a non-empty string (CSS or a Puppeteer query handler). For numeric observation ids, use (await tab.id(id)).click() instead of tab.click(id).",
144
+ );
145
+ }
142
146
  if (selector.startsWith("p-") && !LEGACY_SELECTOR_PREFIXES.some(prefix => selector.startsWith(prefix))) {
143
147
  throw new ToolError(
144
148
  `Unsupported selector prefix. Use CSS or puppeteer query handlers (aria/, text/, xpath/, pierce/). Got: ${selector}`,
@@ -735,7 +739,7 @@ export class WorkerCore {
735
739
  },
736
740
  press: async (key, opts) => {
737
741
  const selector = opts?.selector;
738
- if (selector) await untilAborted(signal, () => page.focus(normalizeSelector(selector)));
742
+ if (selector !== undefined) await untilAborted(signal, () => page.focus(normalizeSelector(selector)));
739
743
  await untilAborted(signal, () => page.keyboard.press(key));
740
744
  },
741
745
  scroll: async (deltaX, deltaY) => {
@@ -829,12 +833,11 @@ export class WorkerCore {
829
833
  opts: ScreenshotOptions = {},
830
834
  ): Promise<ScreenshotResult> {
831
835
  const page = this.#requirePage();
832
- const fullPage = opts.selector ? false : (opts.fullPage ?? false);
836
+ const selector = opts.selector === undefined ? undefined : normalizeSelector(opts.selector);
837
+ const fullPage = selector !== undefined ? false : (opts.fullPage ?? false);
833
838
  let buffer: Buffer;
834
- if (opts.selector) {
835
- const handle = (await untilAborted(signal, () =>
836
- page.$(normalizeSelector(opts.selector!)),
837
- )) as ElementHandle | null;
839
+ if (selector !== undefined) {
840
+ const handle = (await untilAborted(signal, () => page.$(selector))) as ElementHandle | null;
838
841
  if (!handle) throw new ToolError("Screenshot selector did not resolve to an element");
839
842
  try {
840
843
  buffer = (await untilAborted(signal, () => handle.screenshot({ type: "png" }))) as Buffer;
@@ -452,7 +452,12 @@ const names: Array<[name: string, label: string, summary: string | undefined, lo
452
452
  ["todo_write", "Todo Write", "Write a structured todo list", "discoverable"],
453
453
  ["web_search", "Web Search", "Search the web for up-to-date information", "discoverable"],
454
454
  ["search_tool_bm25", "SearchTools", undefined, "essential"],
455
- ["skill_discovery", "SkillDiscovery", "Discover project and user runtime skills by thin metadata", "essential"],
455
+ [
456
+ "skill_discovery",
457
+ "SkillDiscovery",
458
+ "Discover bundled GJC workflow, project, and user runtime skills by thin metadata",
459
+ "essential",
460
+ ],
456
461
  ["telegram_send", "TelegramSend", "Send a workspace file to Telegram", "discoverable"],
457
462
  ["write", "Write", "Write content to a file", "discoverable"],
458
463
  ["skill", "Skill", "Chain into another available skill", "essential"],
@@ -764,7 +764,9 @@ async function renderUrl(
764
764
  let convertedText: string | null = null;
765
765
  const converted = await convertWithMarkit(binary.buffer, conversionExtension, timeout, signal);
766
766
  if (converted.ok) {
767
- if (converted.content.trim().length > 50) {
767
+ // See the non-empty conversion note near the document path
768
+ // below (#5433): short conversions still beat raw fallback.
769
+ if (converted.content.trim().length > 0) {
768
770
  notes.push("Converted with markit");
769
771
  convertedText = converted.content;
770
772
  } else {
@@ -872,7 +874,10 @@ async function renderUrl(
872
874
  const ext = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
873
875
  const converted = await convertWithMarkit(binary.buffer, ext, timeout, signal);
874
876
  if (converted.ok) {
875
- if (converted.content.trim().length > 50) {
877
+ // Any non-empty markit conversion is preferable to a raw-bytes
878
+ // fallback (#5433): short documents like the W3C dummy.pdf
879
+ // ("Dummy PDF file", 14 chars) are legitimately converted text.
880
+ if (converted.content.trim().length > 0) {
876
881
  notes.push("Converted with markit");
877
882
  const output = finalizeOutput(converted.content);
878
883
  return {
@@ -31,7 +31,6 @@ import imageGenDescription from "../prompts/tools/image-gen.md" with { type: "te
31
31
  import { isPrivateOrSpecialAddress, validatePublicHttpUrl } from "../web/insane/url-guard";
32
32
  import { resolveReadPath } from "./path-utils";
33
33
 
34
- const DEFAULT_MODEL = "gemini-3-pro-image-preview";
35
34
  const IMAGE_TIMEOUT = 3 * 60 * 1000; // 3 minutes
36
35
  const MAX_IMAGE_SIZE = 35 * 1024 * 1024;
37
36
  const MAX_IMAGE_REDIRECTS = 5;
@@ -70,7 +69,35 @@ export function redactImageProviderText(value: unknown, activeApiKey?: string):
70
69
  /(?:\b|["'])(?:authorization|api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|token)["']?\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;}]+)/gi,
71
70
  match => match.replace(/([:=]\s*).*/, `$1${REDACTED_PROVIDER_SECRET}`),
72
71
  )
73
- .replace(/\b(?:sk|rk|pk|sess|ghp|gho|github_pat|xox[baprs])-[-A-Za-z0-9._]{8,}\b/gi, REDACTED_PROVIDER_SECRET)
72
+ // A PEM block is redacted whole and runs before the narrower rules, which
73
+ // would otherwise consume its base64 body and leave a truncated key behind.
74
+ .replace(
75
+ /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
76
+ REDACTED_PROVIDER_SECRET,
77
+ )
78
+ .replace(/\b(?:sk|rk|pk|sess|xox[baprs])-[-A-Za-z0-9._]{8,}\b/gi, REDACTED_PROVIDER_SECRET)
79
+ // GitHub tokens separate with `_`, not the `-` the rule above requires, so
80
+ // listing `ghp`/`gho`/`github_pat` there never matched one. Short tokens
81
+ // then fell through the 40-character catch-all below entirely.
82
+ .replace(/\b(?:gh[opsur]_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,})\b/g, REDACTED_PROVIDER_SECRET)
83
+ // The shapes `crash/upstream/envelope.ts` classifies as credential-like.
84
+ // A GitLab PAT and a Hugging Face token are both shorter than the 40-character
85
+ // catch-all below, and Stripe separates with `_` so the `sk|rk|pk`-hyphen rule
86
+ // above never matched one.
87
+ .replace(
88
+ /\b(?:npm_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{20,}|(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}|hf_[A-Za-z0-9]{20,})\b/g,
89
+ REDACTED_PROVIDER_SECRET,
90
+ )
91
+ // AWS access-key ids are 20 characters, so the catch-all never reached them.
92
+ .replace(/\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/g, REDACTED_PROVIDER_SECRET)
93
+ // A Google API key is exactly 39 characters — one short of the catch-all.
94
+ .replace(/\bAIza[0-9A-Za-z_-]{35}\b/g, REDACTED_PROVIDER_SECRET)
95
+ // Basic-auth credentials in a URL. The scheme repetition is bounded so a
96
+ // long alphabetic run cannot be re-tried at every prefix (#5346).
97
+ .replace(
98
+ /(?<![A-Za-z0-9+.-])([a-z][a-z0-9+.-]{0,15}:\/\/)[^/\s:@]{1,256}:[^/\s@]{1,256}@/gi,
99
+ `$1${REDACTED_PROVIDER_SECRET}@`,
100
+ )
74
101
  .replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, REDACTED_PROVIDER_SECRET)
75
102
  .replace(/\b[A-Za-z0-9+/_-]{40,}={0,2}\b/g, REDACTED_PROVIDER_SECRET);
76
103
  return text.length > MAX_PROVIDER_TEXT_LENGTH ? `${text.slice(0, MAX_PROVIDER_TEXT_LENGTH - 1)}…` : text;
@@ -815,16 +842,6 @@ function extractOpenRouterImageUrls(message: OpenRouterMessage | undefined): str
815
842
  return urls;
816
843
  }
817
844
 
818
- /** Provider → default image model mapping for fallbacks */
819
- export const IMAGE_PROVIDER_DEFAULTS: Record<string, string> = {
820
- openai: "gpt-image-2",
821
- alibaba: "wan2.7-image",
822
- "openai-codex": "gpt-image-2",
823
- antigravity: "gemini-3-pro-image",
824
- gemini: "gemini-3-pro-image-preview",
825
- openrouter: "google/gemini-3-pro-image-preview",
826
- };
827
-
828
845
  /**
829
846
  * Resolve the image-generation model from the `modelRoles.image` settings entry.
830
847
  * Returns the resolved Model (with provider identity) or undefined when no
@@ -847,12 +864,6 @@ export function resolveImageRoleModel(
847
864
  return resolved.model;
848
865
  }
849
866
 
850
- /** Resolve the effective image model for a configured provider */
851
- export function resolveImageModel(provider: string, modelOverride: string | null): string {
852
- if (modelOverride) return modelOverride;
853
- return IMAGE_PROVIDER_DEFAULTS[provider] ?? DEFAULT_MODEL;
854
- }
855
-
856
867
  interface ParsedAntigravityCredentials {
857
868
  accessToken: string;
858
869
  projectId?: string;
@@ -941,10 +952,11 @@ async function findImageApiKey(
941
952
  settings: ModelRoleSettings,
942
953
  sessionId?: string,
943
954
  credentialSessionId?: string,
944
- ): Promise<ImageApiKey | null> {
955
+ ): Promise<(ImageApiKey & { model: Model }) | null> {
945
956
  const imageModel = resolveImageRoleModel(settings, modelRegistry, { sessionId, credentialSessionId });
946
957
  if (!imageModel) return null;
947
- return resolveCredentialsForImageModel(imageModel, modelRegistry, sessionId);
958
+ const credentials = await resolveCredentialsForImageModel(imageModel, modelRegistry, sessionId);
959
+ return credentials ? { ...credentials, model: imageModel } : null;
948
960
  }
949
961
 
950
962
  /**
@@ -1492,16 +1504,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1492
1504
  }
1493
1505
 
1494
1506
  const provider = apiKey.provider;
1495
- const imageModel = apiKey.model;
1496
- const model = imageModel
1497
- ? imageModel.id
1498
- : provider === "antigravity"
1499
- ? resolveImageModel("antigravity", null)
1500
- : provider === "alibaba"
1501
- ? resolveImageModel("alibaba", null)
1502
- : provider === "openrouter"
1503
- ? resolveImageModel("openrouter", null)
1504
- : resolveImageModel("gemini", null);
1507
+ const model = apiKey.model.id;
1505
1508
  const resolvedModel = provider === "openrouter" ? resolveOpenRouterModel(model) : model;
1506
1509
  const cwd = ctx.sessionManager.getCwd();
1507
1510
 
@@ -1515,10 +1518,6 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1515
1518
  const requestSignal = ptree.combineSignals(signal, IMAGE_TIMEOUT);
1516
1519
 
1517
1520
  if (provider === "openai" || provider === "openai-codex") {
1518
- if (!apiKey.model) {
1519
- throw new Error("Missing active GPT model for OpenAI image generation");
1520
- }
1521
-
1522
1521
  const parsed = await generateOpenAIHostedImage(
1523
1522
  apiKey.apiKey,
1524
1523
  apiKey.model,