@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
package/src/tools/read.ts CHANGED
@@ -479,8 +479,7 @@ function formatSummaryCapFooter(
479
479
  totalLines: number,
480
480
  direction: TruncationDirection = "head",
481
481
  ): string {
482
- const retained = direction === "last" ? "last" : direction === "both" ? "first and last" : "first";
483
- const directionPart = direction === "head" ? "" : `; retained ${retained} summary units`;
482
+ const directionPart = direction === "head" ? "" : `; truncation: ${direction}`;
484
483
  return `[Summary truncated at ${Math.ceil(maxBytes / 1024)} KiB${directionPart}; re-read ${readPath}:1-${totalLines} or ${readPath}:raw for the full source]`;
485
484
  }
486
485
  const READ_CHUNK_SIZE = 8 * 1024;
@@ -2498,12 +2497,16 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2498
2497
  }
2499
2498
  }
2500
2499
 
2501
- #renderSummary(summary: SummaryResult): {
2500
+ #renderSummary(
2501
+ summary: SummaryResult,
2502
+ direction: TruncationDirection,
2503
+ ): {
2502
2504
  text: string;
2503
2505
  displayText: string;
2504
2506
  elidedSpans: number;
2505
2507
  elidedLines: number;
2506
2508
  capped: boolean;
2509
+ omissionNotice: string;
2507
2510
  } {
2508
2511
  const displayMode = resolveFileDisplayMode(this.session);
2509
2512
  const shouldAddHashLines = displayMode.hashLines;
@@ -2561,14 +2564,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2561
2564
  i++;
2562
2565
  }
2563
2566
 
2564
- const modelParts: string[] = [];
2565
- const displayParts: string[] = [];
2566
2567
  const capBytes = this.session.settings.get("read.summaryMaxBytes") * 1024;
2567
- let usedBytes = 0;
2568
- let elidedSpans = 0;
2569
- let elidedLines = 0;
2570
- let capDropped = false;
2571
- for (const unit of units) {
2568
+ const rendered = units.map(unit => {
2572
2569
  let model: string;
2573
2570
  let display: string;
2574
2571
  let implicitElidedLines = 0;
@@ -2591,31 +2588,82 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2591
2588
  model = formatSingleLine(unit.line, unit.text, shouldAddHashLines, shouldAddLineNumbers);
2592
2589
  display = unit.text;
2593
2590
  }
2594
- const separatorBytes = modelParts.length > 0 ? 1 : 0;
2595
- if (usedBytes + separatorBytes + Buffer.byteLength(model, "utf-8") > capBytes) {
2596
- capDropped = true;
2597
- elidedSpans++;
2598
- elidedLines += unit.kind === "line" ? 1 : unit.endLine - unit.startLine + 1;
2599
- continue;
2591
+ return { model, display, implicitElidedLines };
2592
+ });
2593
+ // Prefix sums include each unit's separator. Removing the final separator
2594
+ // gives the exact UTF-8 body size, without charging recovery footers.
2595
+ const bytes = [0];
2596
+ for (const part of rendered) {
2597
+ bytes.push(bytes[bytes.length - 1] + Buffer.byteLength(part.model, "utf-8") + 1);
2598
+ }
2599
+ const omissionMarker = (head: number, tail: number): string => {
2600
+ const first = units[head];
2601
+ const last = units[tail - 1];
2602
+ const start = first.kind === "line" ? first.line : first.startLine;
2603
+ const end = last.kind === "line" ? last.line : last.endLine;
2604
+ return `[… source lines ${start}-${end} omitted …]`;
2605
+ };
2606
+ let head = units.length;
2607
+ let tail = units.length;
2608
+ const capDropped = bytes[units.length] - 1 > capBytes;
2609
+ if (capDropped) {
2610
+ head = 0;
2611
+ const fits = (nextHead: number, nextTail: number): boolean =>
2612
+ bytes[nextHead] +
2613
+ bytes[units.length] -
2614
+ bytes[nextTail] +
2615
+ Buffer.byteLength(omissionMarker(nextHead, nextTail), "utf-8") <=
2616
+ capBytes;
2617
+ let headBlocked = direction === "last";
2618
+ let tailBlocked = direction === "head";
2619
+ // Keep contiguous edge windows: an oversized unit blocks that edge,
2620
+ // never splits an anchor or turns a prefix/suffix into scattered lines.
2621
+ while (head + 1 < tail && (!headBlocked || !tailBlocked)) {
2622
+ const takeHead = !headBlocked && (tailBlocked || bytes[head] <= bytes[units.length] - bytes[tail]);
2623
+ if (takeHead) {
2624
+ if (fits(head + 1, tail)) head++;
2625
+ else headBlocked = true;
2626
+ } else {
2627
+ if (fits(head, tail - 1)) tail--;
2628
+ else tailBlocked = true;
2629
+ }
2600
2630
  }
2601
- usedBytes += separatorBytes + Buffer.byteLength(model, "utf-8");
2602
- modelParts.push(model);
2603
- displayParts.push(display);
2604
- if (unit.kind === "elided" || unit.kind === "merged") {
2631
+ }
2632
+ const modelParts: string[] = [];
2633
+ const displayParts: string[] = [];
2634
+ let elidedSpans = 0;
2635
+ let elidedLines = 0;
2636
+ let omissionNotice = "";
2637
+ for (let index = 0; index < units.length; index++) {
2638
+ const unit = units[index];
2639
+ if (index >= head && index < tail) {
2640
+ if (index === head) {
2641
+ const marker = omissionMarker(head, tail);
2642
+ // A sub-marker budget can retain no units. Keep its source-range
2643
+ // notice with the recovery footers rather than exceeding the body cap.
2644
+ if (Buffer.byteLength(marker, "utf-8") > capBytes) omissionNotice = marker;
2645
+ else {
2646
+ modelParts.push(marker);
2647
+ displayParts.push(marker);
2648
+ }
2649
+ }
2605
2650
  elidedSpans++;
2606
- elidedLines += implicitElidedLines;
2651
+ elidedLines += unit.kind === "line" ? 1 : unit.endLine - unit.startLine + 1;
2652
+ } else {
2653
+ const part = rendered[index];
2654
+ modelParts.push(part.model);
2655
+ displayParts.push(part.display);
2656
+ if (unit.kind !== "line") elidedSpans++;
2657
+ elidedLines += part.implicitElidedLines;
2607
2658
  }
2608
2659
  }
2609
- if (capDropped) {
2610
- modelParts.push("…");
2611
- displayParts.push("…");
2612
- }
2613
2660
  return {
2614
2661
  text: modelParts.join("\n"),
2615
2662
  displayText: displayParts.join("\n"),
2616
2663
  elidedSpans,
2617
2664
  elidedLines,
2618
2665
  capped: capDropped,
2666
+ omissionNotice,
2619
2667
  };
2620
2668
  }
2621
2669
 
@@ -2911,27 +2959,25 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2911
2959
  (this.session.settings.get("read.summarize.prose") || !PROSE_SUMMARY_EXTENSIONS.has(ext))
2912
2960
  ) {
2913
2961
  const summary = await this.#trySummarize(absolutePath, fileSize, signal);
2914
- if (params.truncation !== undefined && params.truncation !== "head" && summary?.parsed) {
2915
- throw new ToolError(
2916
- "Explicit truncation for structural summaries is not yet supported; summary direction will be applied after summary units become directional.",
2917
- );
2918
- }
2919
2962
  if (summary?.parsed && summary.elided) {
2920
- const renderedSummary = this.#renderSummary(summary);
2963
+ const direction = resolveEffectiveDirection(params.truncation, "local-summary", this.session.settings);
2964
+ const renderedSummary = this.#renderSummary(summary, direction);
2921
2965
  const footer = formatSummaryElisionFooter(
2922
2966
  localReadPath,
2923
2967
  renderedSummary.elidedSpans,
2924
2968
  renderedSummary.elidedLines,
2925
2969
  );
2926
- const summaryTotalLines = Math.max(...summary.segments.map(segment => segment.endLine));
2970
+ const summaryTotalLines = summary.totalLines;
2927
2971
  const modelText = [
2928
2972
  renderedSummary.text,
2973
+ renderedSummary.omissionNotice,
2929
2974
  footer,
2930
2975
  renderedSummary.capped
2931
2976
  ? formatSummaryCapFooter(
2932
2977
  localReadPath,
2933
2978
  this.session.settings.get("read.summaryMaxBytes") * 1024,
2934
2979
  summaryTotalLines,
2980
+ direction,
2935
2981
  )
2936
2982
  : "",
2937
2983
  ]
@@ -3,6 +3,7 @@ import { prompt, untilAborted } from "@gajae-code/utils";
3
3
  import * as z from "zod/v4";
4
4
  import {
5
5
  describeDisabledSkillScopes,
6
+ describeNoSkillMatch,
6
7
  discoverRuntimeSkills,
7
8
  type RuntimeSkillDiscoveryCandidate,
8
9
  } from "../extensibility/runtime-skill-discovery";
@@ -14,7 +15,9 @@ const skillDiscoverySchema = z
14
15
  query: z
15
16
  .string()
16
17
  .optional()
17
- .describe("words to match against skill name, description, source, or use conditions"),
18
+ .describe(
19
+ "words to match against skill name, description, source, or use conditions; every term must appear (conjunctive substring), or one term must equal the exact skill name",
20
+ ),
18
21
  source: z.enum(["all", "project", "user"]).default("all").optional().describe("skill source scope to search"),
19
22
  limit: z.number().min(1).max(50).default(20).optional().describe("maximum results"),
20
23
  })
@@ -26,10 +29,12 @@ export interface SkillDiscoveryToolDetails {
26
29
  candidates: RuntimeSkillDiscoveryCandidate[];
27
30
  count: number;
28
31
  /**
29
- * Present only when zero candidates were returned AND discovery config gates
30
- * (`skills.enabled` / `skills.trustProjectSkills` / `skills.trustUserSkills`)
31
- * prevented some or all of the requested scope from being searched. Without
32
- * this, a disabled config is indistinguishable from "no skills exist".
32
+ * Present only when zero candidates were returned. Either discovery config
33
+ * gates (`skills.enabled` / `skills.trustProjectSkills` /
34
+ * `skills.trustUserSkills`) prevented some or all of the requested scope from
35
+ * being searched, a non-empty query filtered out every scanned skill, or
36
+ * diagnostics were produced for observed skills that were skipped or filtered.
37
+ * Without this, these cases are indistinguishable from "no skills exist".
33
38
  */
34
39
  notice?: string;
35
40
  /**
@@ -44,7 +49,7 @@ export interface SkillDiscoveryToolDetails {
44
49
  export class SkillDiscoveryTool implements AgentTool<typeof skillDiscoverySchema, SkillDiscoveryToolDetails> {
45
50
  readonly name = "skill_discovery";
46
51
  readonly label = "SkillDiscovery";
47
- readonly summary = "Discover project and user runtime skills by thin metadata";
52
+ readonly summary = "Discover bundled GJC workflow, project, and user runtime skills by thin metadata";
48
53
  readonly loadMode = "essential";
49
54
  readonly description: string;
50
55
  readonly parameters = skillDiscoverySchema;
@@ -95,7 +100,12 @@ export class SkillDiscoveryTool implements AgentTool<typeof skillDiscoverySchema
95
100
  };
96
101
  if (result.diagnostics.messages.length > 0) details.diagnostics = result.diagnostics.messages;
97
102
  if (result.candidates.length === 0) {
98
- const notice = describeDisabledSkillScopes(source, this.#getRuntimeSkillPolicy());
103
+ const notice =
104
+ describeDisabledSkillScopes(source, this.#getRuntimeSkillPolicy()) ??
105
+ describeNoSkillMatch(input.query, result.scanned) ??
106
+ (result.scanned > 0 || result.diagnostics.messages.length > 0
107
+ ? `No skill candidates were returned (${result.scanned} skill${result.scanned === 1 ? "" : "s"} scanned); see diagnostics for filtered or skipped skills.`
108
+ : undefined);
99
109
  if (notice) details.notice = notice;
100
110
  }
101
111
  return {
@@ -27,7 +27,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
27
27
  "read": {
28
28
  "name": "read",
29
29
  "label": "Read",
30
- "description": "Read files, directories, archives, SQLite databases, images, documents, internal resources, and web URLs through a single `path` string.\n\n<instruction>\n- One tool for filesystem, archives, SQLite, images, documents (PDF/DOCX/PPTX/XLSX/RTF/EPUB/ipynb), internal URIs, and web URLs (reader-mode by default).\n- You SHOULD parallelize independent reads when exploring related files.\n- You SHOULD reach for `read` — not a browser/puppeteer tool — for fetching web content.\n</instruction>\n\n## Parameters\n\n- `path` — required. Local path, internal URI (`agent://`, `artifact://`, `rule://`, `local://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).\n- `truncation` — optional `head` | `last` | `both`; selects which end of an over-budget result to retain. Configured default: last (factory default: `last`); non-file routes such as URLs, directories and converted documents default to `head`. A line-range selector still bounds the selection — this only picks which end of that selection survives the byte/line cap. SQLite row queries page via their own `limit`/`offset` and ignore it.\n## Selectors\nAppend `:<sel>` to `path`. The bare path falls back to the default mode.\n\n- _(none)_ — parseable code → structural summary (signatures kept, bodies elided); a plain text file → a bounded receipt of about undefined lines or undefined KiB, whichever is smaller; the configured truncation direction is last (factory default: last). Line+hash anchors keep their real file line numbers and a footer names the omitted range. Archive members use the larger 3000-line / 50 KiB budget. Converted documents, notebooks, URLs and directory listings still start from the beginning.\n- `:50` / `:50-` — read from line 50 onward.\n- `:50-200` — lines 50–200 inclusive.\n- `:50+150` — 150 lines starting at line 50.\n- `:20+1` — exactly one line.\n- `:5-16,960-973` — multiple ranges in one call (sorted, overlaps merged).\n- `:raw` — verbatim text; no anchors, no summary, no line prefixes.\n- `:2-4:raw` or `:raw:2-4` — range AND verbatim; the two compose in either order.\n- `:conflicts` — one-line-per-block index of every unresolved git merge conflict.\n\n# Files\n\n- Reading a directory path returns a depth-limited dirent listing.\n- Parseable code without a selector returns a **structural summary**: declarations kept, large bodies collapsed to `..` (merged brace pair) or `…` (standalone). Summarized output ends with a footer of the form:\n\n `[NN lines across MM elided regions; read <path>:raw or a line range like <path>:1-9999 for verbatim content]`\n\n If the elided body is what you actually need, re-issue the **exact selector the footer names**. NEVER guess what's inside `..` / `…` — those markers carry no content.\n- Directional windows identify the retained first/last lines and the omitted range; use the `re-read <path>:1-<total>` or `:raw` hint in the footer to recover the full content.\n\n# Documents & Notebooks\n\nExtracts text from PDF, Word, PowerPoint, Excel, RTF, and EPUB. Notebooks (`.ipynb`) are shown as editable `# %% [type] cell:N` text; edits round-trip back to the underlying JSON preserving notebook metadata. Add `:raw` to a notebook to bypass the converter and read the JSON directly.\n\n# Images\n\nReading an image path returns the image itself for visual inspection by a vision-capable model.\n\n# Archives\n\nSupports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read a member, and append a normal selector to the inner path: `archive.zip:dir/file.ts:50-60`.\n\n# SQLite\n\nFor `.sqlite`, `.sqlite3`, `.db`, `.db3`:\n- `file.db` — list tables with row counts\n- `file.db:table` — schema + sample rows\n- `file.db:table:key` — single row by primary key\n- `file.db:table?limit=50&offset=100` — paginated rows\n- `file.db:table?where=status='active'&order=created:desc` — filtered rows\n- `file.db?q=SELECT …` — read-only SELECT query\n\n# URLs\n\n- Default reader-mode: HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs → clean text/markdown.\n- `:raw` returns untouched HTML; line selectors (`:50`, `:50-100`, `:50+150`) paginate the cached fetched output.\n- Bare `host:port` URLs collide with the selector grammar — add a trailing slash before the selector: `https://example.com/:80`.\n\n# Internal URIs\n\n`agent://<id>`, `artifact://<id>`, `rule://<name>`, and `local://<name>.md` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.\n\n<critical>\n- Always include `path`; never call `read` with `{}`.\n- For line ranges, append the selector to `path`.\n- Re-issue the selector named by a summary footer before relying on elided content.\n</critical>",
30
+ "description": "Read files, directories, archives, SQLite databases, images, documents, internal resources, and web URLs through a single `path` string.\n\n<instruction>\n- One tool for filesystem, archives, SQLite, images, documents (PDF/DOCX/PPTX/XLSX/RTF/EPUB/ipynb), internal URIs, and web URLs (reader-mode by default).\n- You SHOULD parallelize independent reads when exploring related files.\n- You SHOULD reach for `read` — not a browser/puppeteer tool — for fetching web content.\n</instruction>\n\n## Parameters\n\n- `path` — required. Local path, internal URI (`agent://`, `artifact://`, `rule://`, `local://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).\n- `truncation` — optional `head` | `last` | `both`; selects which end of an over-budget result to retain. Configured default: last (factory default: `last`); structural summaries and routes such as URLs, directories and converted documents default to `head`. A line-range selector still bounds the selection — this only picks which end of that selection survives the byte/line cap. SQLite row queries page via their own `limit`/`offset` and ignore it.\n## Selectors\nAppend `:<sel>` to `path`. The bare path falls back to the default mode.\n\n- _(none)_ — parseable code → structural summary (signatures kept, bodies elided); a plain text file → a bounded receipt of about undefined lines or undefined KiB, whichever is smaller; the configured truncation direction is last (factory default: last). Line+hash anchors keep their real file line numbers and a footer names the omitted range. Archive members use the larger 3000-line / 50 KiB budget. Converted documents, notebooks, URLs and directory listings still start from the beginning.\n- `:50` / `:50-` — read from line 50 onward.\n- `:50-200` — lines 50–200 inclusive.\n- `:50+150` — 150 lines starting at line 50.\n- `:20+1` — exactly one line.\n- `:5-16,960-973` — multiple ranges in one call (sorted, overlaps merged).\n- `:raw` — verbatim text; no anchors, no summary, no line prefixes.\n- `:2-4:raw` or `:raw:2-4` — range AND verbatim; the two compose in either order.\n- `:conflicts` — one-line-per-block index of every unresolved git merge conflict.\n\n# Files\n\n- Reading a directory path returns a depth-limited dirent listing.\n- Parseable code without a selector returns a **structural summary**: declarations kept, large bodies collapsed to `..` (merged brace pair) or `…` (standalone). Summarized output ends with a footer of the form:\n\n `[NN lines across MM elided regions; read <path>:raw or a line range like <path>:1-9999 for verbatim content]`\n\n If the elided body is what you actually need, re-issue the **exact selector the footer names**. NEVER guess what's inside `..` / `…` — those markers carry no content.\n- Structural summaries honor explicit `truncation`: `head` retains the beginning, `last` the end, and `both` both ends within the summary byte budget. Complete summary units and their original source anchors stay intact and in source order; budget omissions are marked where they occur. This does not switch to raw source. Summaries that fit are unchanged by direction.\n- Directional windows identify the retained first/last lines and the omitted range; use the `re-read <path>:1-<total>` or `:raw` hint in the footer to recover the full content.\n\n# Documents & Notebooks\n\nExtracts text from PDF, Word, PowerPoint, Excel, RTF, and EPUB. Notebooks (`.ipynb`) are shown as editable `# %% [type] cell:N` text; edits round-trip back to the underlying JSON preserving notebook metadata. Add `:raw` to a notebook to bypass the converter and read the JSON directly.\n\n# Images\n\nReading an image path returns the image itself for visual inspection by a vision-capable model.\n\n# Archives\n\nSupports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read a member, and append a normal selector to the inner path: `archive.zip:dir/file.ts:50-60`.\n\n# SQLite\n\nFor `.sqlite`, `.sqlite3`, `.db`, `.db3`:\n- `file.db` — list tables with row counts\n- `file.db:table` — schema + sample rows\n- `file.db:table:key` — single row by primary key\n- `file.db:table?limit=50&offset=100` — paginated rows\n- `file.db:table?where=status='active'&order=created:desc` — filtered rows\n- `file.db?q=SELECT …` — read-only SELECT query\n\n# URLs\n\n- Default reader-mode: HTML pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom, JSON endpoints, PDFs → clean text/markdown.\n- `:raw` returns untouched HTML; line selectors (`:50`, `:50-100`, `:50+150`) paginate the cached fetched output.\n- Bare `host:port` URLs collide with the selector grammar — add a trailing slash before the selector: `https://example.com/:80`.\n\n# Internal URIs\n\n`agent://<id>`, `artifact://<id>`, `rule://<name>`, and `local://<name>.md` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.\n\n<critical>\n- Always include `path`; never call `read` with `{}`.\n- For line ranges, append the selector to `path`.\n- Re-issue the selector named by a summary footer before relying on elided content.\n</critical>",
31
31
  "parameters": {
32
32
  "type": "object",
33
33
  "properties": {
@@ -1109,7 +1109,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
1109
1109
  "browser": {
1110
1110
  "name": "browser",
1111
1111
  "label": "Browser",
1112
- "description": "Drives a real Chromium tab with full puppeteer access via JS execution.\n\n<instruction>\n- For static web content (articles, docs, issues/PRs, JSON, PDFs, feeds), prefer the `read` tool with a URL. Use this tool only when you need JS execution, authentication, or interactive actions.\n- Four actions:\n - `open` — acquire (or reuse) a named tab. `name` defaults to `\"main\"`. Optional `url`, `viewport`, and `dialogs: \"accept\" | \"dismiss\"` (auto-handles `alert`/`confirm`/`beforeunload`). The `app` field selects the browser kind (spawned binary, saved Chrome profile, or existing CDP endpoint); omitted means headless Chromium with stealth patches.\n - `close` — release a tab by `name`, or every tab with `all: true`. `kill: true` also terminates a spawned-app process tree.\n - `act` — run a list of structured `actions` against an existing tab without writing JS (preferred for routine navigation/interaction). Each step is `{ verb, … }`; verbs: `navigate {url, wait_until?}`, `click {id|selector}`, `type {id|selector, text}`, `fill {selector, value}`, `select {selector, values}`, `press {key, selector?}`, `scroll {dx?, dy?}`, `back`, `wait {selector?|ms?}`, `observe {viewport_only?, include_all?}`, `extract {format?}`, `screenshot`. Address elements by the numeric `id` from a prior `observe` (preferred) or a selector. Steps run in order; the tool returns per-step results.\n - `run` — execute JS against an existing tab. `code` is the body of an async function with `page`, `browser`, `tab`, `display`, `assert`, `wait` in scope. The return value is JSON-stringified into the tool result; `display(value)` calls accumulate text/images. Use `run` only when an `act` verb does not cover what you need.\n- Tabs survive across `run` calls and across in-process subagents. Open once, reuse many times.\n- Browser kinds: no `app` launches headless Chromium; `app.path` reuses CDP or kills stale same-path processes before spawning — NEVER use it for a daily Chrome profile; use explicit `app.browser: \"chrome\"` profile mode instead. In profile mode, `path` defaults to installed Chrome/Chromium and `profile_directory` defaults to `\"Default\"`, but `user_data_dir` must name a separate non-default Chrome data directory: Chrome 136+ disables remote debugging for its default data directory. Only Chrome/Chromium executables are admitted; Edge, Brave, Vivaldi, Opera, unknown browser brands, and default Chrome data roots are rejected. Use `app.cdp_url` to attach to an already-authorized browser. Saved-profile/CDP automation has access to that profile's cookies and authenticated accounts. Profile mode refuses a matching non-CDP Chrome instead of killing/relaunching it, and `kill: true` can terminate only a Chrome process GJC launched; `app.cdp_url` is externally owned and disconnect-only. CDP must stay on `127.0.0.1`: it grants full browser-account access.\n- Inside `run`, `tab` exposes high-level helpers (`goto`, `observe`, `id`, `click`, `type`, `fill`, `press`, `waitFor`, `screenshot`, `extract`, …); reach for `page` (raw puppeteer Page) when they don't cover it.\n- Selectors accept CSS as well as puppeteer query handlers: `aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`.\n- Runtime diagnostics are opt-in: pass `diagnostics: true` to `open` to subscribe the tab to page `Runtime.exceptionThrown` and `console.error` events. The next successful `act`/`run` response then includes at most 20 `runtimeDiagnostics` entries plus `runtimeDiagnosticsDropped`, then drains them. Entries contain only kind, time, origin-only URL, line/column, and a built-in error class from a fixed allowlist — never path segments, query strings, messages, console arguments, values, or stacks. Output is byte-bounded and marks truncation explicitly.\n- Full reference — helpers, browser kinds, CDP/security details, and more examples — read `gjc://tools/browser.md`.\n</instruction>\n\n<critical>\n- You MUST call `open` before `run` or `act`. Neither implicitly creates a tab.\n- You MUST observe before taking a screenshot to understand page state; screenshot only when visual appearance matters.\n- After a `tab.goto()` or any navigation, prior element ids from `tab.observe()` are invalidated. Re-observe before referencing them.\n- `code` runs with full Node access. Treat it as your code, not sandboxed code.\n</critical>\n\n<examples>\n# Open a tab and read structured page data\n`{\"action\":\"open\",\"name\":\"docs\",\"url\":\"https://example.com\"}`\n`{\"action\":\"act\",\"name\":\"docs\",\"actions\":[{\"verb\":\"observe\"}]}`\n\n# Click an observed element, then fill and submit a form\n`{\"action\":\"act\",\"name\":\"docs\",\"actions\":[{\"verb\":\"click\",\"id\":12},{\"verb\":\"fill\",\"selector\":\"input[name=email]\",\"value\":\"me@example.com\"},{\"verb\":\"click\",\"selector\":\"text/Continue\"}]}`\n\n# Use `run` only when `act` has no suitable verb\n`{\"action\":\"run\",\"name\":\"docs\",\"code\":\"const count = await page.locator('canvas').count(); return { count };\"}`\n</examples>\n\n<output>\n- Per call: any `display(value)` outputs (text/images) followed by the JSON-stringified return value of the `code` function. `run` always produces at least a status line.\n</output>",
1112
+ "description": "Drives a real Chromium tab with full puppeteer access via JS execution.\n\n<instruction>\n- For static web content (articles, docs, issues/PRs, JSON, PDFs, feeds), prefer the `read` tool with a URL. Use this tool only when you need JS execution, authentication, or interactive actions.\n- Four actions:\n - `open` — acquire (or reuse) a named tab. `name` defaults to `\"main\"`. Optional `url`, `viewport`, and `dialogs: \"accept\" | \"dismiss\"` (auto-handles `alert`/`confirm`/`beforeunload`). The `app` field selects the browser kind (spawned binary, saved Chrome profile, or existing CDP endpoint); omitted means headless Chromium with stealth patches.\n - `close` — release a tab by `name`, or every tab with `all: true`. `kill: true` also terminates a spawned-app process tree.\n - `act` — run a list of structured `actions` against an existing tab without writing JS (preferred for routine navigation/interaction). Each step is `{ verb, … }`; verbs: `navigate {url, wait_until?}`, `click {id|selector}`, `type {id|selector, text}`, `fill {selector, value}`, `select {selector, values}`, `press {key, selector?}`, `scroll {dx?, dy?}`, `back`, `wait {selector?|ms?}`, `observe {viewport_only?, include_all?}`, `extract {format?}`, `screenshot`. Address elements by the numeric `id` from a prior `observe` (preferred) or a selector. Steps run in order; the tool returns per-step results.\n - `run` — execute JS against an existing tab. `code` is the body of an async function with `page`, `browser`, `tab`, `display`, `assert`, `wait` in scope. The return value is JSON-stringified into the tool result; `display(value)` calls accumulate text/images. Use `run` only when an `act` verb does not cover what you need.\n- Tabs survive across `run` calls and across in-process subagents. Open once, reuse many times.\n- Browser kinds: no `app` launches headless Chromium; `app.path` reuses CDP or kills stale same-path processes before spawning — NEVER use it for a daily Chrome profile; use explicit `app.browser: \"chrome\"` profile mode instead. In profile mode, `path` defaults to installed Chrome/Chromium and `profile_directory` defaults to `\"Default\"`, but `user_data_dir` must name a separate non-default Chrome data directory: Chrome 136+ disables remote debugging for its default data directory. Only Chrome/Chromium executables are admitted; Edge, Brave, Vivaldi, Opera, unknown browser brands, and default Chrome data roots are rejected. Use `app.cdp_url` to attach to an already-authorized browser. Saved-profile/CDP automation has access to that profile's cookies and authenticated accounts. Profile mode refuses a matching non-CDP Chrome instead of killing/relaunching it, and `kill: true` can terminate only a Chrome process GJC launched; `app.cdp_url` is externally owned and disconnect-only. CDP must stay on `127.0.0.1`: it grants full browser-account access.\n- Inside `run`, `tab` exposes high-level helpers (`goto`, `observe`, `id`, `click`, `type`, `fill`, `press`, `waitFor`, `screenshot`, `extract`, …); reach for `page` (raw puppeteer Page) when they don't cover it.\n- Selectors must be non-empty strings: CSS or puppeteer query handlers (`aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`). In `run`, click numeric observation ids with `(await tab.id(58)).click()`, not `tab.click(58)`. Omit optional selectors for unfocused `tab.press()` or page-level `tab.screenshot()`; null and blank selectors are invalid.\n- Runtime diagnostics are opt-in: pass `diagnostics: true` to `open` to subscribe the tab to page `Runtime.exceptionThrown` and `console.error` events. The next successful `act`/`run` response then includes at most 20 `runtimeDiagnostics` entries plus `runtimeDiagnosticsDropped`, then drains them. Entries contain only kind, time, origin-only URL, line/column, and a built-in error class from a fixed allowlist — never path segments, query strings, messages, console arguments, values, or stacks. Output is byte-bounded and marks truncation explicitly.\n- Full reference — helpers, browser kinds, CDP/security details, and more examples — read `gjc://tools/browser.md`.\n</instruction>\n\n<critical>\n- You MUST call `open` before `run` or `act`. Neither implicitly creates a tab.\n- You MUST observe before taking a screenshot to understand page state; screenshot only when visual appearance matters.\n- After a `tab.goto()` or any navigation, prior element ids from `tab.observe()` are invalidated. Re-observe before referencing them.\n- `code` runs with full Node access. Treat it as your code, not sandboxed code.\n</critical>\n\n<examples>\n# Open a tab and read structured page data\n`{\"action\":\"open\",\"name\":\"docs\",\"url\":\"https://example.com\"}`\n`{\"action\":\"act\",\"name\":\"docs\",\"actions\":[{\"verb\":\"observe\"}]}`\n\n# Click an observed element, then fill and submit a form\n`{\"action\":\"act\",\"name\":\"docs\",\"actions\":[{\"verb\":\"click\",\"id\":12},{\"verb\":\"fill\",\"selector\":\"input[name=email]\",\"value\":\"me@example.com\"},{\"verb\":\"click\",\"selector\":\"text/Continue\"}]}`\n\n# Use `run` only when `act` has no suitable verb\n`{\"action\":\"run\",\"name\":\"docs\",\"code\":\"const count = await page.locator('canvas').count(); return { count };\"}`\n</examples>\n\n<output>\n- Per call: any `display(value)` outputs (text/images) followed by the JSON-stringified return value of the `code` function. `run` always produces at least a status line.\n</output>",
1113
1113
  "parameters": {
1114
1114
  "type": "object",
1115
1115
  "properties": {
@@ -2068,7 +2068,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2068
2068
  "task": {
2069
2069
  "name": "task",
2070
2070
  "label": "Task",
2071
- "description": "Launches subagents to parallelize workflows.\n\n- Results are delivered automatically when complete.\n- The tool result lists the assigned task ids (e.g. `0-AuthLoader`) — those are the live agent ids.\n- Use `subagent` action `inspect` or `list` to snapshot manager state.\n- To wait or cancel, use the `subagent` tool; its await/cancel doctrine is authoritative.\n\nSubagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in `context` or `assignment`.\n\n<parameters>\n- `agent`: agent type for all tasks\n- `tasks`: tasks to execute in parallel\n - `.id`: filesystem-safe, ≤48 chars, matching `[A-Za-z0-9][A-Za-z0-9_-]*`; prefer CamelCase\n - `.description`: UI label only — subagent never sees it\n - `.assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED\n- `context`: shared background prepended to every assignment; session-specific only\n- `.inheritContext` (optional): fork-context mode for seeding the subagent with sanitized parent conversation. Omit it or set `\"none\"` for no copied context. `\"receipt\"` copies a minimal receipt-sized snapshot, `\"last-turn\"` copies only the latest exchange, `\"bounded\"` copies the bounded default snapshot, and `\"full\"` copies a larger snapshot up to the configured/model token cap. Non-`none` modes work only when global `task.forkContext.enabled` is true and the target agent declares `forkContext: allowed`; otherwise the call is rejected. Bundled agents that support it: `executor`, `architect`. Use inherited context only when the subagent's value depends on parent context; cloned tokens are billed to the child as fresh input and surfaced in task receipts as fork-context cloned-token accounting.\n\n- `schema`: JTD schema for expected structured output (do not put format rules in assignments)\n- `spawnPlan` (optional): required before any batch with more than 4 tasks; include whyParallel, whyNotLocal, independence, expectedReceiptShape, and maxInlineTokens.\n</parameters>\n\n<rules>\n- HARD runtime gate: calls with more than 4 tasks are rejected before any child launches unless `spawnPlan` is complete.\n- NEVER assign tasks to run project-wide build/test/lint. Caller verifies after the batch.\n- **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates and formatters. You run them once at the end across the union of changed files — avoids redundant runs and racing formatter passes.\n- Each task: ≤3–5 explicit files. No globs, no \"update all\", no package-wide scope. Fan out to a cluster instead.\n- Pass large payloads via `local://<path>` URIs, not inline.\n- Put shared constraints in `context` once; do not duplicate across assignments.\n- Prefer agents that investigate **and** edit in one pass; only spin a read-only discovery step when affected files are genuinely unknown.\n</rules>\n\n<parallelization>\nTest: can task B run correctly without seeing A's output? If no, sequence A → B.\nSequential when one task produces a contract (types, API, schema, core module) the other consumes.\nParallel when tasks touch disjoint files or are independent refactors/tests.\n</parallelization>\n\n<context-fmt>\n# Goal ← one sentence: what the batch accomplishes\n# Constraints ← MUST/NEVER rules and session decisions\n# Contract ← exact types/signatures if tasks share an interface\n</context-fmt>\n\n<assignment-fmt>\n# Target ← exact files and symbols; explicit non-goals\n# Change ← step-by-step add/remove/rename; APIs and patterns\n# Acceptance ← observable result; no project-wide commands\n</assignment-fmt>\n\n<agents>\n# executor\nAutonomous implementation agent for bounded code changes, fixes, and verification-ready edits\n\n# architect\nRead-only architecture and code-review agent with severity-rated findings and status verdicts\n\n# planner\nRead-only planning agent for sequencing, acceptance criteria, risks, and handoff shape\n\n# critic\nRead-only plan critic that approves only actionable, verifiable execution plans\n</agents>",
2071
+ "description": "Launches subagents to parallelize workflows.\n\n- Results are delivered automatically when complete.\n- The tool result lists the assigned task ids (e.g. `0-AuthLoader`) — those are the live agent ids.\n- Use `subagent` action `inspect` or `list` to snapshot manager state.\n- To wait or cancel, use the `subagent` tool; its await/cancel doctrine is authoritative.\n\nSubagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in `context` or `assignment`.\n\n<parameters>\n- `agent`: agent type for all tasks\n- `tasks`: tasks to execute in parallel\n - `.id`: filesystem-safe, ≤48 chars, matching `[A-Za-z0-9][A-Za-z0-9_-]*`; prefer CamelCase\n - `.description`: UI label only — subagent never sees it\n - `.assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED\n- `context`: shared background prepended to every assignment; session-specific only\n- `.inheritContext` (optional): fork-context mode for seeding the subagent with sanitized parent conversation. Omit it or set `\"none\"` for no copied context. `\"receipt\"` copies a minimal receipt-sized snapshot, `\"last-turn\"` copies only the latest exchange, `\"bounded\"` copies the bounded default snapshot, and `\"full\"` copies a larger snapshot up to the configured/model token cap. Non-`none` modes work only when global `task.forkContext.enabled` is true and the target agent declares `forkContext: allowed`; otherwise the call is rejected. Bundled agents that support it: `executor`, `architect`. Use inherited context only when the subagent's value depends on parent context; cloned tokens are billed to the child as fresh input and surfaced in task receipts as fork-context cloned-token accounting.\n\n- `schema`: JTD schema for expected structured output (do not put format rules in assignments)\n- `spawnPlan` (optional): required before any batch with more than 4 tasks; include whyParallel, whyNotLocal, independence, expectedReceiptShape, and maxInlineTokens.\n\n- `repositoryBinding`: optional authoritative repository/worktree identity. It must match the active session and cannot authorize a foreign repository from this session. For another approved repository, start a fresh session rooted there (for example, `gjc --cwd <approved-worktree>`); do not use `/move` because materialized tasks retain their original binding.\n</parameters>\n\n<rules>\n- HARD runtime gate: calls with more than 4 tasks are rejected before any child launches unless `spawnPlan` is complete.\n- NEVER assign tasks to run project-wide build/test/lint. Caller verifies after the batch.\n- **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates and formatters. You run them once at the end across the union of changed files — avoids redundant runs and racing formatter passes.\n- Each task: ≤3–5 explicit files. No globs, no \"update all\", no package-wide scope. Fan out to a cluster instead.\n- Pass large payloads via `local://<path>` URIs, not inline.\n- Put shared constraints in `context` once; do not duplicate across assignments.\n- Prefer agents that investigate **and** edit in one pass; only spin a read-only discovery step when affected files are genuinely unknown.\n</rules>\n\n<parallelization>\nTest: can task B run correctly without seeing A's output? If no, sequence A → B.\nSequential when one task produces a contract (types, API, schema, core module) the other consumes.\nParallel when tasks touch disjoint files or are independent refactors/tests.\n</parallelization>\n\n<context-fmt>\n# Goal ← one sentence: what the batch accomplishes\n# Constraints ← MUST/NEVER rules and session decisions\n# Contract ← exact types/signatures if tasks share an interface\n</context-fmt>\n\n<assignment-fmt>\n# Target ← exact files and symbols; explicit non-goals\n# Change ← step-by-step add/remove/rename; APIs and patterns\n# Acceptance ← observable result; no project-wide commands\n</assignment-fmt>\n\n<agents>\n# executor\nAutonomous implementation agent for bounded code changes, fixes, and verification-ready edits\n\n# architect\nRead-only architecture and code-review agent with severity-rated findings and status verdicts\n\n# planner\nRead-only planning agent for sequencing, acceptance criteria, risks, and handoff shape\n\n# critic\nRead-only plan critic that approves only actionable, verifiable execution plans\n</agents>",
2072
2072
  "parameters": {
2073
2073
  "type": "object",
2074
2074
  "properties": {
@@ -2123,7 +2123,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2123
2123
  ]
2124
2124
  },
2125
2125
  "repositoryBinding": {
2126
- "description": "authoritative repository identity; omitted items are stamped from session cwd before discovery/spawn and still fail closed on sibling drift",
2126
+ "description": "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.",
2127
2127
  "type": "object",
2128
2128
  "properties": {
2129
2129
  "schema": {
@@ -2753,12 +2753,12 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2753
2753
  "skill_discovery": {
2754
2754
  "name": "skill_discovery",
2755
2755
  "label": "SkillDiscovery",
2756
- "description": "Discover project and user runtime skills without loading full skill content.\n\n<instruction>\n- Searches canonical GJC skill locations in precedence order: project `.gjc/skills` (ancestors from cwd to repo root, closest first), then user locations under the home directory: canonical `<config>/agent/skills`, configured legacy `<config>/skills`, and historical legacy `.gjc/skills`. `<config>` is the home-relative directory name from `GJC_CONFIG_DIR`, then `PI_CONFIG_DIR`, then `.gjc`; even an absolute-looking configured name is joined beneath `<home>`. Project scope shadows user scope; within a scope, earlier locations above win. Bundled GJC workflow skills (`autoresearch`, `deep-interview`, `ralplan`, `ultragoal`) are always available and cannot be replaced by filesystem skills.\n- Returns thin metadata only: name, description, source scope, path, and use conditions when present.\n- Claude Code (`.claude/skills`) and Codex (`.codex/skills`) layouts are explicit import sources into `.gjc`, never invokable candidates. They are not returned as candidates; instead, each convention skill found in a trusted scope is reported in `diagnostics` with the exact copy command that enables it (copy into `.gjc/skills`), so a skill placed in a documented convention location is discoverable in a normal session without being silently loaded.\n- Discovery is on by default in a normal session. When zero candidates are returned because discovery config is disabled (`skills.enabled` master switch, or `skills.trustProjectSkills` / `skills.trustUserSkills` scope trust), the result carries a `notice` explaining which setting blocked the search — an empty result without a `notice` means the searched scopes genuinely contain no matching skills.\n- When skills were scanned but not advertised (protected-name collision with a bundled workflow skill, include/ignore/disable policy filters, invalid frontmatter, shadowing), the result carries a bounded `diagnostics` list explaining why.\n- To load a selected skill's full `SKILL.md`, invoke it through the existing `skill` tool with the exact `name` returned here.\n</instruction>\n\nInput:\n- `query` (optional): words to match against skill name, description, source, or use conditions.\n- `source` (optional): `all`, `project`, or `user`.\n- `limit` (optional): maximum results, 1-50.",
2756
+ "description": "Discover bundled GJC workflow, project, and user runtime skills without loading full skill content.\n\n<instruction>\n- Includes the four bundled GJC workflow skills (`autoresearch`, `deep-interview`, `ralplan`, `ultragoal`) as `source: \"bundled\"` candidates from the generated bundled catalog; they are always available, cannot be replaced by filesystem copies, and are included for `source: \"all\"` but excluded by explicit `source: \"project\"`/`\"user\"` requests.\n- Returns thin metadata only: name, description, source scope, path, and use conditions when present. Bundled paths are stable catalog identifiers, not filesystem paths.\n- In trusted Git projects, `.gjc/skills` may be a symlink to a directory inside the same repository (for example `../.agents/skills`). Link and target identities remain checked through body loading; external targets and symlinked user/profile roots are refused. Do not replace a safe project link with copied files to make discovery work.\n- Claude Code (`.claude/skills`) and Codex (`.codex/skills`) layouts are explicit import sources into `.gjc`, never invokable candidates. They are not returned as candidates; instead, each convention skill found in a trusted scope is reported in `diagnostics` with the exact copy command that enables it (copy into `.gjc/skills`), so a skill placed in a documented convention location is discoverable in a normal session without being silently loaded.\n- Discovery is on by default in a normal session. `query` matching is conjunctive substring: every whitespace-separated term must appear in a skill's name, description, source, or use conditions, unless a term equals the exact skill name — one keyword that appears nowhere in the candidate's metadata drops every candidate, so query with exact names or few terms known to appear literally, not topic keyword lists. When zero candidates are returned, the result carries a `notice` if discovery config disabled a requested scope, a non-empty query filtered out scanned skills, or diagnostics explain observed skills that were skipped or filtered. Only an empty result without a `notice` means no skills were scanned and no diagnostics were produced; never report a zero-candidate result as proof no skill exists.\n- When skills were scanned but not advertised (protected-name collision with a bundled workflow skill, include/ignore/disable policy filters, invalid frontmatter, shadowing), the result carries a bounded `diagnostics` list explaining why.\n- To load a selected skill's full `SKILL.md`, invoke it through the existing `skill` tool with the exact `name` returned here.\n</instruction>\n\nInput:\n- `query` (optional): words to match against skill name, description, source, or use conditions. Every term must appear (conjunctive substring), or one term must equal the exact skill name.\n- `source` (optional): `all`, `project`, or `user`.\n- `limit` (optional): maximum results, 1-50.",
2757
2757
  "parameters": {
2758
2758
  "type": "object",
2759
2759
  "properties": {
2760
2760
  "query": {
2761
- "description": "words to match against skill name, description, source, or use conditions",
2761
+ "description": "words to match against skill name, description, source, or use conditions; every term must appear (conjunctive substring), or one term must equal the exact skill name",
2762
2762
  "type": "string"
2763
2763
  },
2764
2764
  "source": {
@@ -2784,7 +2784,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2784
2784
  "strict": true,
2785
2785
  "deferrable": false,
2786
2786
  "loadMode": "essential",
2787
- "summary": "Discover project and user runtime skills by thin metadata"
2787
+ "summary": "Discover bundled GJC workflow, project, and user runtime skills by thin metadata"
2788
2788
  },
2789
2789
  "telegram_send": {
2790
2790
  "name": "telegram_send",
@@ -2904,7 +2904,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2904
2904
  "move_session": {
2905
2905
  "name": "move_session",
2906
2906
  "label": "Move Session",
2907
- "description": "Rescope the session to a narrower working directory.\n\nUse this only when the session's working directory is a broad launcher root (for example a\nmulti-repo workspace like `~/Projects`) and the task has clearly converged on one subdirectory\nor repository: after this call, every later turn resolves relative paths and the bash default\ncwd from the new directory, and project-scoped plugins/capabilities reload for it.\n\n- `path` must be an existing directory; relative paths resolve against the current session cwd.\n The canonical target must be strictly inside the current session directory — moves to a\n parent, a sibling project, or an unrelated absolute path are refused.\n- A session can be moved this way at most once, and never while another move is running; a\n rejected call does not consume the move. Use it once the target repo is identified — not\n speculatively — because the session file and caches move with the session.\n- This tool is unavailable in subagent sessions and restricted profiles; ask the top-level\n session to rescope instead.",
2907
+ "description": "Rescope the session to a narrower working directory.\n\nUse this only when the session's working directory is a broad launcher root (for example a\nmulti-repo workspace like `~/Projects`) and the task has clearly converged on one subdirectory\nor repository: after this call, every later turn resolves relative paths and the bash default\ncwd from the new directory, and project-scoped plugins/capabilities reload for it.\n\n- `path` must be an existing directory; relative paths resolve against the current session cwd.\n The canonical target must be strictly inside the current session directory — moves to a\n parent, a sibling project, or an unrelated absolute path are refused.\n- A session can be moved this way at most once, and never while another move is running; a\n rejected call does not consume the move. Use it once the target repo is identified — not\n speculatively — because the session file and caches move with the session.\n- This tool is unavailable in subagent sessions and restricted profiles; ask the top-level\n session to rescope instead.\n- For an explicitly requested sibling or unrelated repository, start a new session with\n `gjc --cwd <approved-worktree>` and delegate from there. A task's `repositoryBinding`\n verifies the session's repository; it cannot authorize relocation. Already-created task\n tools retain their original repository binding even after a user-driven cwd move.",
2908
2908
  "parameters": {
2909
2909
  "type": "object",
2910
2910
  "properties": {
@@ -11,28 +11,16 @@ const ULTRAGOAL_ASK_GUARD = Symbol.for("gajae-code.ultragoalAskGuard");
11
11
  type GuardedTool = AgentTool & { [ULTRAGOAL_ASK_GUARD]?: true };
12
12
 
13
13
  export interface UltragoalAskGuardContext {
14
- activeSkillState?: { skill?: string; session_id?: string } | null;
14
+ /**
15
+ * GJC session id of the session asking the question. This is the guard's only
16
+ * identity input: durable Ultragoal state and the nudge budget are bound to it
17
+ * exclusively. Active-skill metadata is deliberately NOT part of this context —
18
+ * a snapshot restored from another session carries that session's id, and
19
+ * treating it as the caller's identity leaked a foreign run into the guard.
20
+ */
15
21
  sessionId?: string | null;
16
22
  }
17
23
 
18
- const UPSTREAM_PLANNING_ASK_SKILLS = new Set(["deep-interview", "ralplan"]);
19
-
20
- function normalizedActiveSkill(context?: UltragoalAskGuardContext): string | undefined {
21
- const skill = context?.activeSkillState?.skill?.trim();
22
- return skill || undefined;
23
- }
24
-
25
- function sessionScopedAskGuardId(
26
- context: UltragoalAskGuardContext,
27
- activeSkill: string | undefined,
28
- ): string | undefined {
29
- if (activeSkill !== "ultragoal" && !UPSTREAM_PLANNING_ASK_SKILLS.has(activeSkill ?? "")) return undefined;
30
- const activeSessionId = context.activeSkillState?.session_id?.trim();
31
- if (activeSessionId) return activeSessionId;
32
- const sessionId = context.sessionId?.trim();
33
- return sessionId || undefined;
34
- }
35
-
36
24
  export function formatUltragoalAskBlockMessage(diagnostic: UltragoalAskBlockDiagnostic): string {
37
25
  return [
38
26
  diagnostic.message,
@@ -46,13 +34,13 @@ export async function assertUltragoalAskAllowed(
46
34
  context: UltragoalAskGuardContext = {},
47
35
  agentDir?: string,
48
36
  ): Promise<void> {
49
- const activeSkill = normalizedActiveSkill(context);
50
- // Deep-interview and ralplan are upstream planning workflows whose core gates
51
- // are `ask` calls. Scope their Ultragoal check to the current session so stale
52
- // or ambiguous Ultragoal durable state from another session cannot hijack those
53
- // prompts; same-session active Ultragoal state still falls through to the
54
- // blocker/nudge checks below.
55
- const sessionId = sessionScopedAskGuardId(context, activeSkill);
37
+ // Durable Ultragoal state is session-scoped, so only the caller's own run may
38
+ // block `ask`. An anonymous caller has no run attributable to it, so the guard
39
+ // fails open instead of falling back to GJC_SESSION_ID or an auto-detected
40
+ // session: either can name another (possibly dead) session's abandoned run and
41
+ // would make `ask` unusable for every session in the repository.
42
+ const sessionId = context.sessionId?.trim();
43
+ if (!sessionId) return;
56
44
  const diagnostic = await isUltragoalAskBlocked(cwd, { sessionId });
57
45
  if (!diagnostic.active) return;
58
46
  const nudge = await consumeUltragoalAskNudge(cwd, sessionId, agentDir);
@@ -76,6 +64,8 @@ export function guardToolForUltragoalAsk<T extends AgentTool>(
76
64
  return async (...args: unknown[]): Promise<unknown> => {
77
65
  // The wrapper runs BEFORE AskTool.execute(): resolve the nudge
78
66
  // budget against the SESSION profile, never the process-global one.
67
+ // The caller session id is the only binding, so a foreign session's
68
+ // durable run can never gate this ask.
79
69
  await assertUltragoalAskAllowed(getCwd(), getContext(), getSessionAgentDir());
80
70
  return Reflect.apply(target.execute, target, args);
81
71
  };
@@ -87,9 +87,15 @@ export async function parseChangelog(changelogPath: string): Promise<ChangelogEn
87
87
  * dev runs, and `GJC_PACKAGE_DIR` / `PI_PACKAGE_DIR` overrides (which scope to
88
88
  * optional package assets like docs/examples and do not influence the
89
89
  * binary-identity changelog).
90
+ *
91
+ * Parsed once per process and cached: the embedded text is immutable, and the
92
+ * parse runs on the interactive startup path plus every /changelog invocation.
90
93
  */
94
+ let displayChangelogEntriesCache: ChangelogEntry[] | undefined;
95
+
91
96
  export function getDisplayChangelogEntries(): ChangelogEntry[] {
92
- return parseChangelogContent(CHANGELOG_TEXT);
97
+ displayChangelogEntriesCache ??= parseChangelogContent(CHANGELOG_TEXT);
98
+ return displayChangelogEntriesCache;
93
99
  }
94
100
 
95
101
  export function getInstalledVersionChangelogEntry(
package/src/utils/git.ts CHANGED
@@ -1266,7 +1266,9 @@ export async function clone(url: string, targetDir: string, options: CloneOption
1266
1266
  const args = ["clone", "--depth", "1"];
1267
1267
  if (options.ref) args.push("--branch", options.ref, "--single-branch");
1268
1268
  else args.push("--single-branch");
1269
- args.push(url, absoluteTarget);
1269
+ // `--` ends option parsing, so a URL that begins with `-` can never be read as
1270
+ // a git option (`--upload-pack=...` and friends).
1271
+ args.push("--", url, absoluteTarget);
1270
1272
 
1271
1273
  try {
1272
1274
  await runEffect(path.dirname(absoluteTarget), args, { signal: options.signal });
@@ -1,6 +1,7 @@
1
1
  import { untilAborted } from "@gajae-code/utils";
2
2
  import type { Markit, StreamInfo } from "markit-ai";
3
3
  import { ToolAbortError } from "../tools/tool-errors";
4
+ import { ensureMupdfWasmResolution } from "./mupdf-wasm";
4
5
 
5
6
  export interface MarkitConversionResult {
6
7
  content: string;
@@ -18,6 +19,11 @@ let markit: () => Markit | Promise<Markit> = async () => {
18
19
  return promise;
19
20
  };
20
21
 
22
+ // #5433: mupdf's wasm asset is not reachable at the Emscripten loader's
23
+ // default bunfs path in compiled binaries. Seed the embedded-asset hook
24
+ // before any markit conversion (and therefore before any mupdf import).
25
+ ensureMupdfWasmResolution();
26
+
21
27
  function normalizeExtension(extension: string): string {
22
28
  const trimmed = extension.trim().toLowerCase();
23
29
  if (!trimmed) return ".bin";
@@ -31,6 +37,22 @@ function normalizeError(error: unknown): string {
31
37
  return "Conversion failed";
32
38
  }
33
39
 
40
+ /**
41
+ * markit-ai's PDF converter swallows the real mupdf import failure behind a
42
+ * generic "Install it" message (#5433). Re-run the import here so release
43
+ * diagnostics carry the actual module/asset/initialization error.
44
+ */
45
+ async function describeConversionError(error: unknown): Promise<string> {
46
+ const base = normalizeError(error);
47
+ if (!base.includes("PDF support requires 'mupdf'")) return base;
48
+ try {
49
+ await import("mupdf");
50
+ } catch (cause) {
51
+ return `${base} (mupdf import failed: ${normalizeError(cause)})`;
52
+ }
53
+ return base;
54
+ }
55
+
34
56
  async function runMarkitConversion<T>(task: (markit: Markit) => Promise<T>, signal?: AbortSignal): Promise<T> {
35
57
  try {
36
58
  const instance = await markit();
@@ -62,7 +84,7 @@ export async function convertFileWithMarkit(filePath: string, signal?: AbortSign
62
84
  if (error instanceof ToolAbortError) {
63
85
  throw error;
64
86
  }
65
- return { content: "", ok: false, error: normalizeError(error) };
87
+ return { content: "", ok: false, error: await describeConversionError(error) };
66
88
  }
67
89
  }
68
90
 
@@ -84,6 +106,6 @@ export async function convertBufferWithMarkit(
84
106
  if (error instanceof ToolAbortError) {
85
107
  throw error;
86
108
  }
87
- return { content: "", ok: false, error: normalizeError(error) };
109
+ return { content: "", ok: false, error: await describeConversionError(error) };
88
110
  }
89
111
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * MuPDF wasm asset resolution inside compiled binaries (#5433).
3
+ *
4
+ * `mupdf`'s Emscripten loader resolves `mupdf-wasm.wasm` relative to
5
+ * `import.meta.url` (or `scriptDirectory`). Inside a `bun build --compile`
6
+ * bunfs that path points at the bunfs root where the asset does not exist,
7
+ * so every mupdf import aborted with
8
+ * `ENOENT: ... /$bunfs/root/mupdf-wasm.wasm`.
9
+ *
10
+ * The wasm is embedded via `with { type: "file" }`, which lands it at a
11
+ * hashed bunfs path. MuPDF's top-level factory call reads
12
+ * `globalThis["$libmupdf_wasm_Module"]`, so seeding that config with a
13
+ * `locateFile` hook that returns the embedded asset path makes the loader
14
+ * read the wasm from the bunfs directly — no disk sidecar needed.
15
+ *
16
+ * This must run before the first `import("mupdf")` anywhere in the process.
17
+ */
18
+ import mupdfWasmPath from "../../../../node_modules/mupdf/dist/mupdf-wasm.wasm" with { type: "file" };
19
+
20
+ const MODULE_CONFIG_KEY = "$libmupdf_wasm_Module";
21
+
22
+ export function ensureMupdfWasmResolution(): void {
23
+ const globalScope = globalThis as typeof globalThis & Record<string, unknown>;
24
+ if (globalScope[MODULE_CONFIG_KEY] !== undefined) return;
25
+ globalScope[MODULE_CONFIG_KEY] = { locateFile: () => String(mupdfWasmPath) };
26
+ }