@gajae-code/coding-agent 0.18.10 → 0.18.12

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 (270) hide show
  1. package/CHANGELOG.md +295 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/cli/accounts-cli.d.ts +2 -2
  7. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  8. package/dist/types/cli/install-activation.d.ts +10 -4
  9. package/dist/types/cli/public-command-errors.d.ts +1 -0
  10. package/dist/types/cli/update-cli.d.ts +14 -1
  11. package/dist/types/cli-ordinary.d.ts +2 -2
  12. package/dist/types/commands/accounts.d.ts +2 -1
  13. package/dist/types/commands/migrate.d.ts +1 -1
  14. package/dist/types/commands/sdk.d.ts +5 -0
  15. package/dist/types/commit/changelog/index.d.ts +1 -0
  16. package/dist/types/config/autorouting-tier-map.d.ts +30 -0
  17. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  18. package/dist/types/config/config-hot-reload.d.ts +39 -0
  19. package/dist/types/config/file-lock.d.ts +8 -1
  20. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  21. package/dist/types/config/model-profile-activation.d.ts +62 -6
  22. package/dist/types/config/model-registry.d.ts +56 -5
  23. package/dist/types/config/model-resolver.d.ts +2 -0
  24. package/dist/types/config/models-config-schema.d.ts +10 -0
  25. package/dist/types/config/settings-schema.d.ts +46 -3
  26. package/dist/types/config/settings.d.ts +33 -1
  27. package/dist/types/dap/config.d.ts +8 -4
  28. package/dist/types/debug/system-info.d.ts +6 -1
  29. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  30. package/dist/types/edit/modes/patch.d.ts +5 -0
  31. package/dist/types/edit/modes/replace.d.ts +3 -0
  32. package/dist/types/exec/bash-executor.d.ts +12 -0
  33. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  34. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  35. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  36. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  37. package/dist/types/migrate/adapters/index.d.ts +10 -1
  38. package/dist/types/migrate/executor.d.ts +3 -1
  39. package/dist/types/migrate/types.d.ts +15 -3
  40. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  42. package/dist/types/modes/theme/theme.d.ts +1 -0
  43. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  44. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  45. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  46. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  47. package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
  48. package/dist/types/sdk/broker/broker.d.ts +7 -1
  49. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  50. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  51. package/dist/types/sdk/broker/ensure.d.ts +136 -7
  52. package/dist/types/sdk/broker/hop.d.ts +36 -0
  53. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  54. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +4 -1
  56. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  57. package/dist/types/sdk/bus/index.d.ts +23 -0
  58. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  59. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  60. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  61. package/dist/types/sdk/host/session-runtime.d.ts +32 -0
  62. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  63. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  64. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  65. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  66. package/dist/types/sdk/session-list.d.ts +1 -1
  67. package/dist/types/session/agent-session.d.ts +58 -2
  68. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  69. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  70. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  71. package/dist/types/session/messages.d.ts +5 -1
  72. package/dist/types/session/session-manager.d.ts +13 -3
  73. package/dist/types/session/startup-auth-config.d.ts +24 -2
  74. package/dist/types/session/streaming-output.d.ts +11 -0
  75. package/dist/types/session/terminal-abort.d.ts +3 -1
  76. package/dist/types/session-import/service.d.ts +1 -1
  77. package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
  78. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  79. package/dist/types/task/executor.d.ts +19 -2
  80. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  81. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  82. package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
  83. package/dist/types/tools/descriptors.d.ts +2 -0
  84. package/dist/types/tools/fetch.d.ts +1 -1
  85. package/dist/types/tools/index.d.ts +2 -0
  86. package/dist/types/tools/python.d.ts +1 -1
  87. package/dist/types/tools/report-issue.d.ts +41 -0
  88. package/dist/types/tools/subagent.d.ts +23 -0
  89. package/dist/types/web/scrapers/types.d.ts +8 -1
  90. package/dist/types/web/search/providers/insane.d.ts +7 -1
  91. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  92. package/package.json +8 -8
  93. package/scripts/build-binary.ts +2 -1
  94. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  95. package/scripts/generate-docs-index.ts +8 -1
  96. package/scripts/generate-sdk-operation-inventory.ts +22 -4
  97. package/src/async/job-manager.ts +356 -56
  98. package/src/async/result-formatting.ts +40 -0
  99. package/src/autoresearch/git.ts +9 -2
  100. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  101. package/src/capability/fs.ts +27 -19
  102. package/src/capability/index.ts +2 -2
  103. package/src/cli/accounts-cli.ts +176 -8
  104. package/src/cli/auth-broker-cli.ts +21 -11
  105. package/src/cli/doctor/install-repairs.ts +35 -5
  106. package/src/cli/fast-help.ts +1 -1
  107. package/src/cli/install-activation.ts +47 -4
  108. package/src/cli/migrate-cli.ts +7 -5
  109. package/src/cli/public-command-errors.ts +4 -0
  110. package/src/cli/root-flags.ts +2 -2
  111. package/src/cli/stats-cli.ts +9 -5
  112. package/src/cli/update-cli.ts +224 -7
  113. package/src/cli-ordinary.ts +7 -2
  114. package/src/commands/accounts.ts +10 -5
  115. package/src/commands/migrate.ts +3 -3
  116. package/src/commands/sdk.ts +199 -42
  117. package/src/commands/ultragoal.ts +16 -8
  118. package/src/commit/changelog/index.ts +39 -1
  119. package/src/commit/message.ts +10 -3
  120. package/src/config/autorouting-tier-map.ts +30 -0
  121. package/src/config/config-hot-reload-trust.ts +148 -0
  122. package/src/config/config-hot-reload.ts +734 -0
  123. package/src/config/file-lock.ts +216 -56
  124. package/src/config/model-bindings-applier.ts +85 -47
  125. package/src/config/model-discovery-manager.ts +10 -0
  126. package/src/config/model-profile-activation.ts +716 -113
  127. package/src/config/model-registry.ts +1334 -158
  128. package/src/config/model-resolver.ts +3 -1
  129. package/src/config/models-config-schema.ts +6 -0
  130. package/src/config/settings-schema.ts +60 -3
  131. package/src/config/settings.ts +458 -24
  132. package/src/coordinator-mcp/policy.ts +26 -1
  133. package/src/crash/sanitize.ts +4 -0
  134. package/src/customization/import.ts +3 -1
  135. package/src/dap/config.ts +31 -11
  136. package/src/dap/defaults.json +1 -1
  137. package/src/debug/crash-diagnostics.ts +2 -1
  138. package/src/debug/report-bundle.ts +19 -0
  139. package/src/debug/system-info.ts +8 -3
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  141. package/src/discovery/agents-md.ts +1 -1
  142. package/src/discovery/helpers.ts +1 -2
  143. package/src/discovery/mcp-compat.ts +120 -17
  144. package/src/edit/apply-patch/index.ts +12 -1
  145. package/src/edit/index.ts +13 -17
  146. package/src/edit/modes/patch.ts +187 -8
  147. package/src/edit/modes/replace.ts +136 -0
  148. package/src/eval/js/context-manager.ts +29 -3
  149. package/src/eval/js/worker-entry.ts +51 -18
  150. package/src/eval/py/executor.ts +14 -4
  151. package/src/eval/py/runner.py +144 -5
  152. package/src/exec/bash-executor.ts +81 -4
  153. package/src/exec/bash-shell-guardian.ts +4 -1
  154. package/src/exec/bash-shell-supervisor.ts +8 -4
  155. package/src/exec/isolated-shell.ts +5 -1
  156. package/src/export/html/index.ts +46 -6
  157. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  158. package/src/extensibility/extensions/loader.ts +8 -1
  159. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  160. package/src/gjc-runtime/gc-runtime.ts +33 -6
  161. package/src/gjc-runtime/launch-tmux.ts +7 -2
  162. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  163. package/src/gjc-runtime/launch-worktree.ts +101 -14
  164. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  165. package/src/gjc-runtime/session-resolution.ts +38 -6
  166. package/src/gjc-runtime/session-state-lock.ts +59 -14
  167. package/src/gjc-runtime/state-runtime.ts +23 -19
  168. package/src/hooks/native-skill-hook.ts +14 -11
  169. package/src/internal-urls/docs-index.generated.ts +14 -13
  170. package/src/internal-urls/local-protocol.ts +142 -8
  171. package/src/lsp/index.ts +29 -0
  172. package/src/lsp/workspace-path.ts +23 -0
  173. package/src/migrate/action-planner.ts +57 -0
  174. package/src/migrate/adapters/claude-code.ts +23 -4
  175. package/src/migrate/adapters/codex.ts +22 -4
  176. package/src/migrate/adapters/index.ts +22 -4
  177. package/src/migrate/adapters/opencode.ts +6 -1
  178. package/src/migrate/executor.ts +84 -7
  179. package/src/migrate/report.ts +19 -2
  180. package/src/migrate/types.ts +14 -4
  181. package/src/modes/DESIGN.md +51 -0
  182. package/src/modes/acp/permission-mode.ts +18 -1
  183. package/src/modes/components/assistant-message.ts +55 -1
  184. package/src/modes/components/settings-selector.ts +103 -37
  185. package/src/modes/controllers/command-controller.ts +38 -2
  186. package/src/modes/controllers/plan-mode-controller.ts +17 -7
  187. package/src/modes/controllers/selector-controller.ts +62 -6
  188. package/src/modes/interactive-mode.ts +68 -0
  189. package/src/modes/theme/theme.ts +15 -0
  190. package/src/prompts/tools/debug.md +1 -1
  191. package/src/prompts/tools/read.md +1 -1
  192. package/src/prompts/tools/report-issue.md +13 -0
  193. package/src/prompts/tools/task.md +1 -1
  194. package/src/runtime-credential-selector.ts +2 -2
  195. package/src/runtime-mcp/content-limits.ts +7 -0
  196. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  197. package/src/runtime-mcp/manager.ts +32 -0
  198. package/src/runtime-mcp/oauth-flow.ts +32 -6
  199. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  200. package/src/runtime-mcp/render.ts +16 -1
  201. package/src/runtime-mcp/tool-bridge.ts +126 -17
  202. package/src/sdk/broker/broker-exit.ts +8 -5
  203. package/src/sdk/broker/broker.ts +342 -82
  204. package/src/sdk/broker/daemon-entry.ts +338 -116
  205. package/src/sdk/broker/doctor-restart.ts +2 -1
  206. package/src/sdk/broker/ensure.ts +1143 -265
  207. package/src/sdk/broker/hop.ts +274 -0
  208. package/src/sdk/broker/lifecycle-ledger.ts +35 -11
  209. package/src/sdk/broker/lock-host-identity.ts +29 -0
  210. package/src/sdk/broker/session-index.ts +62 -12
  211. package/src/sdk/bus/chat-command-policy.ts +16 -3
  212. package/src/sdk/bus/index.ts +52 -21
  213. package/src/sdk/bus/reconciliation-store.ts +201 -8
  214. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  215. package/src/sdk/cli/session-cli.ts +21 -6
  216. package/src/sdk/host/session-runtime.test.ts +118 -23
  217. package/src/sdk/host/session-runtime.ts +352 -47
  218. package/src/sdk/lifecycle/service.ts +1 -0
  219. package/src/sdk/prompt-deadline-flush.ts +78 -5
  220. package/src/sdk/prompt-deadline-manager.ts +68 -14
  221. package/src/sdk/protocol/adapter-validation.ts +53 -11
  222. package/src/sdk/protocol/operation-inventory.generated.json +101 -2
  223. package/src/sdk/reconciliation-extensions.ts +1 -0
  224. package/src/sdk/session-list.ts +1 -0
  225. package/src/sdk/session.ts +114 -61
  226. package/src/session/agent-session.ts +1043 -81
  227. package/src/session/internal/managed-session-scope.ts +126 -39
  228. package/src/session/internal/managed-session-storage.ts +14 -11
  229. package/src/session/internal/native-publish-outcome.ts +5 -1
  230. package/src/session/messages.ts +47 -6
  231. package/src/session/session-manager.ts +236 -14
  232. package/src/session/startup-auth-config.ts +116 -8
  233. package/src/session/streaming-output.ts +262 -4
  234. package/src/session/terminal-abort.ts +14 -2
  235. package/src/session-import/service.ts +1 -0
  236. package/src/skill-state/workflow-mutation-guard.ts +491 -32
  237. package/src/slash-commands/builtin-registry.ts +15 -3
  238. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  239. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  240. package/src/task/executor.ts +103 -12
  241. package/src/task/index.ts +389 -175
  242. package/src/tools/ast-edit-write-path.ts +64 -0
  243. package/src/tools/ast-edit.ts +18 -1
  244. package/src/tools/atomic-file-write.ts +46 -15
  245. package/src/tools/auto-generated-guard.ts +3 -0
  246. package/src/tools/bash-allowed-prefixes.ts +153 -5
  247. package/src/tools/bash-interactive.ts +14 -2
  248. package/src/tools/bash.ts +14 -0
  249. package/src/tools/debug.ts +7 -6
  250. package/src/tools/descriptors.test.ts +44 -0
  251. package/src/tools/descriptors.ts +34 -2
  252. package/src/tools/fetch.ts +3 -2
  253. package/src/tools/index.ts +2 -0
  254. package/src/tools/output-meta.ts +23 -2
  255. package/src/tools/python.ts +1 -1
  256. package/src/tools/read.ts +17 -35
  257. package/src/tools/report-issue.ts +184 -0
  258. package/src/tools/subagent-render.ts +114 -12
  259. package/src/tools/subagent.ts +108 -1
  260. package/src/tools/todo-write.ts +41 -3
  261. package/src/tools/tool-catalog.generated.ts +31 -3
  262. package/src/tools/write.ts +35 -3
  263. package/src/utils/commit-message-generator.ts +9 -2
  264. package/src/utils/file-mentions.ts +29 -1
  265. package/src/utils/title-generator.ts +7 -2
  266. package/src/web/scrapers/index.ts +3 -1
  267. package/src/web/scrapers/types.ts +8 -0
  268. package/src/web/scrapers/youtube.ts +3 -2
  269. package/src/web/search/providers/insane.ts +91 -28
  270. package/src/web/search/providers/searxng.ts +62 -10
@@ -540,8 +540,11 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
540
540
  if (!onUpdate || streamingDisabled) return;
541
541
  try {
542
542
  const result = this.#progressResult(manager, records, true);
543
- const signature = subagentAwaitRenderedStateSignature(result.details?.subagents ?? []);
544
543
  const t = nowMs();
544
+ const snapshots = result.details?.subagents ?? [];
545
+ const signature = this.session.settings.get("task.compactSubagentPanel")
546
+ ? subagentCompactRenderedStateSignature(snapshots, Date.now())
547
+ : subagentAwaitRenderedStateSignature(snapshots);
545
548
  const livenessDue = force || t - lastEmitMs >= livenessIntervalMs;
546
549
  if (!livenessDue && signature === lastEmittedSignature) return;
547
550
  lastEmittedSignature = signature;
@@ -1136,6 +1139,110 @@ export function subagentAwaitRenderedStateSignature(
1136
1139
  });
1137
1140
  }
1138
1141
 
1142
+ /** A running subagent with no activity for longer than this counts as idle in the compact panel. */
1143
+ export const SUBAGENT_COMPACT_IDLE_MS = 30_000;
1144
+
1145
+ /** Group counts behind the compact await panel's single summary line. */
1146
+ export interface SubagentGroupSummary {
1147
+ running: number;
1148
+ idle: number;
1149
+ retrying: number;
1150
+ queued: number;
1151
+ paused: number;
1152
+ completed: number;
1153
+ failed: number;
1154
+ cancelled: number;
1155
+ /** Running subagents in provider retry, per provider; drives the degradation warning. */
1156
+ retryingByProvider: Record<string, number>;
1157
+ }
1158
+
1159
+ export function summarizeSubagentGroup(subagents: readonly SubagentSnapshot[], nowMs: number): SubagentGroupSummary {
1160
+ const summary: SubagentGroupSummary = {
1161
+ running: 0,
1162
+ idle: 0,
1163
+ retrying: 0,
1164
+ queued: 0,
1165
+ paused: 0,
1166
+ completed: 0,
1167
+ failed: 0,
1168
+ cancelled: 0,
1169
+ retryingByProvider: {},
1170
+ };
1171
+ for (const snapshot of subagents) {
1172
+ switch (snapshot.status) {
1173
+ case "running": {
1174
+ summary.running += 1;
1175
+ const live = snapshot.liveProgressAvailable !== false ? snapshot.progress : undefined;
1176
+ if (live?.status === "running" && live.retryState) {
1177
+ summary.retrying += 1;
1178
+ const provider = live.retryState.provider ?? "provider";
1179
+ summary.retryingByProvider[provider] = (summary.retryingByProvider[provider] ?? 0) + 1;
1180
+ }
1181
+ // Before its first activity a subagent has no timestamp; its elapsed job time
1182
+ // is then the age, so one stalled from the start still turns idle.
1183
+ const quietMs = live?.lastActivityMs !== undefined ? nowMs - live.lastActivityMs : snapshot.durationMs;
1184
+ if (quietMs > SUBAGENT_COMPACT_IDLE_MS) summary.idle += 1;
1185
+ break;
1186
+ }
1187
+ case "queued":
1188
+ summary.queued += 1;
1189
+ break;
1190
+ case "paused":
1191
+ summary.paused += 1;
1192
+ break;
1193
+ case "completed":
1194
+ case "already_completed":
1195
+ summary.completed += 1;
1196
+ break;
1197
+ case "failed":
1198
+ case "not_found":
1199
+ summary.failed += 1;
1200
+ break;
1201
+ case "cancelled":
1202
+ summary.cancelled += 1;
1203
+ break;
1204
+ }
1205
+ }
1206
+ return summary;
1207
+ }
1208
+
1209
+ /**
1210
+ * The compact degradation row shows only the most-degraded provider, its count, and
1211
+ * how many other providers degrade (providers with a single retrying subagent are
1212
+ * not a degradation). Signing exactly that keeps invisible per-provider churn from
1213
+ * re-emitting.
1214
+ */
1215
+ function compactDegradationSignature(
1216
+ retryingByProvider: Record<string, number>,
1217
+ ): { provider: string; count: number; moreProviders: number } | null {
1218
+ const groups = Object.entries(retryingByProvider)
1219
+ .filter(([, count]) => count > 1)
1220
+ .sort(([a, countA], [b, countB]) => countB - countA || a.localeCompare(b));
1221
+ const [worst] = groups;
1222
+ return worst ? { provider: worst[0], count: worst[1], moreProviders: groups.length - 1 } : null;
1223
+ }
1224
+
1225
+ /**
1226
+ * Rendered-state signature for the compact await panel. It covers only what the
1227
+ * compact view shows (group counts and the single degradation row), so per-subagent activity that does not move
1228
+ * a count no longer re-emits: the update rate is bounded by count transitions and
1229
+ * the liveness interval instead of growing with the number of live subagents.
1230
+ */
1231
+ export function subagentCompactRenderedStateSignature(
1232
+ subagents: readonly SubagentSnapshot[],
1233
+ nowMs: number,
1234
+ receipt?: Pick<SubagentToolDetails, "awaitOutcome" | "interrupted">,
1235
+ ): string {
1236
+ return JSON.stringify({
1237
+ awaitOutcome: receipt?.awaitOutcome ?? null,
1238
+ interrupted: receipt?.interrupted === true,
1239
+ summary: (({ retryingByProvider, ...counts }) => ({
1240
+ ...counts,
1241
+ degradation: compactDegradationSignature(retryingByProvider),
1242
+ }))(summarizeSubagentGroup(subagents, nowMs)),
1243
+ });
1244
+ }
1245
+
1139
1246
  function canonicalizeSnapshotForSignature(snapshot: SubagentSnapshot): unknown {
1140
1247
  return {
1141
1248
  id: snapshot.id,
@@ -268,6 +268,14 @@ function removeTasks(phases: TodoPhase[], entry: TodoOpEntryValue, errors: strin
268
268
  return phases;
269
269
  }
270
270
 
271
+ /** Type of a value that was expected to be a string, with its article. */
272
+ function describeArgument(value: unknown): string {
273
+ if (value === null) return "null";
274
+ if (Array.isArray(value)) return "an array";
275
+ if (typeof value === "object") return "an object";
276
+ return `a ${typeof value}`;
277
+ }
278
+
271
279
  function applyEntry(phases: TodoPhase[], entry: TodoOpEntryValue, errors: string[]): TodoPhase[] {
272
280
  switch (entry.op) {
273
281
  case "init":
@@ -302,6 +310,16 @@ function applyEntry(phases: TodoPhase[], entry: TodoOpEntryValue, errors: string
302
310
  case "note": {
303
311
  const hit = resolveTaskOrError(phases, entry.task, errors);
304
312
  if (!hit) return phases;
313
+ // The schema types `text`, but bridges and replays reach `execute` without
314
+ // raw validation, and `.replace` on a non-string body used to crash the
315
+ // call. Reject it with a correction instead of storing it.
316
+ if (entry.text !== undefined && typeof entry.text !== "string") {
317
+ errors.push(
318
+ `Note text must be a string; got ${describeArgument(entry.text)}. ` +
319
+ 'Pass the note body as "text", for example { op: "note", task: "<task content>", text: "why" }.',
320
+ );
321
+ return phases;
322
+ }
305
323
  const text = (entry.text ?? "").replace(/\s+$/u, "");
306
324
  if (!text) {
307
325
  errors.push("Missing text for note operation");
@@ -337,6 +355,26 @@ export function applyOpsToPhases(
337
355
  // Markdown round-trip
338
356
  // =============================================================================
339
357
 
358
+ /**
359
+ * Notes are declared as `string[]`, but the array is only typed, never validated:
360
+ * a session persisted elsewhere or a bridged caller that reached `execute`
361
+ * without raw validation can hand over any element, and every render path splits
362
+ * each note on newlines. One non-string element used to crash the call with a
363
+ * minified `.split is not a function` TypeError instead of a tool result. String
364
+ * notes render exactly as before.
365
+ */
366
+ function noteDisplayText(note: unknown): string {
367
+ if (typeof note === "string") return note;
368
+ if (note !== null && typeof note === "object") {
369
+ try {
370
+ return JSON.stringify(note) ?? "[unrenderable note]";
371
+ } catch {
372
+ return "[unrenderable note]";
373
+ }
374
+ }
375
+ return String(note);
376
+ }
377
+
340
378
  const STATUS_TO_MARKER: Record<TodoStatus, string> = {
341
379
  pending: " ",
342
380
  in_progress: "/",
@@ -356,7 +394,7 @@ export function phasesToMarkdown(phases: TodoPhase[]): string {
356
394
  if (task.notes && task.notes.length > 0) {
357
395
  for (let j = 0; j < task.notes.length; j++) {
358
396
  if (j > 0) out.push(" >");
359
- for (const noteLine of task.notes[j].split("\n")) {
397
+ for (const noteLine of noteDisplayText(task.notes[j]).split("\n")) {
360
398
  out.push(noteLine === "" ? " >" : ` > ${noteLine}`);
361
399
  }
362
400
  }
@@ -504,7 +542,7 @@ function formatSummary(phases: TodoPhase[], errors: string[]): string {
504
542
  if (task.status === "in_progress" && task.notes && task.notes.length > 0) {
505
543
  for (let j = 0; j < task.notes.length; j++) {
506
544
  if (j > 0) lines.push(" ---");
507
- for (const noteLine of task.notes[j].split("\n")) {
545
+ for (const noteLine of noteDisplayText(task.notes[j]).split("\n")) {
508
546
  lines.push(` ${noteLine}`);
509
547
  }
510
548
  }
@@ -685,7 +723,7 @@ function renderNoteAttachments(phases: TodoPhase[], uiTheme: Theme): string[] {
685
723
  lines.push(` ${title}`);
686
724
  for (let j = 0; j < task.notes.length; j++) {
687
725
  if (j > 0) lines.push(` ${bar}`);
688
- for (const noteLine of task.notes[j].split("\n")) {
726
+ for (const noteLine of noteDisplayText(task.notes[j]).split("\n")) {
689
727
  lines.push(` ${bar} ${uiTheme.fg("dim", noteLine)}`);
690
728
  }
691
729
  }
@@ -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`); 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 50 lines or 10 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\nBundled skills have no filesystem home, so the skill tool and skill discovery report them as `embedded:gjc/skills/<name>/SKILL.md`; read that identifier verbatim. Bundled skill fragments, when surfaced, likewise use `embedded:gjc/skill-fragments/...` identifiers.\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 50 lines or 10 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\nBundled skills have no filesystem home, so the skill tool and skill discovery report them as `embedded:gjc/skills/<name>/SKILL.md`; read that identifier verbatim. Bundled skill fragments, when surfaced, likewise use `embedded:gjc/skill-fragments/…` identifiers.\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": {
@@ -407,7 +407,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
407
407
  "debug": {
408
408
  "name": "debug",
409
409
  "label": "Debug",
410
- "description": "Provides debugger access through the Debug Adapter Protocol (DAP).\nUse for launching or attaching debuggers, setting breakpoints, stepping through execution, inspecting threads/stack/variables, evaluating expressions, capturing output, and interrupting hung programs.\n\n<instruction>\n- Prefer over bash for program state, breakpoints, stepping, thread inspection, or interrupting a running process.\n- `action: \"launch\"` starts a session; `program` is required, `adapter` optional (auto-selected from target path and workspace).\n For Python, set `adapter: \"debugpy\"` and `program` to the target `.py` file; put interpreter/script flags in `args`.\n- `action: \"attach\"` connects to an existing process: `pid` for local attach, `port` for remote attach (where the adapter supports it), `adapter` to force a specific debugger.\n- **Breakpoints**: `set_breakpoint`/`remove_breakpoint` with source (`file`+`line`) or function (`function`); optional `condition` for conditional breakpoints.\n- **Flow control**: `continue` (resumes; briefly waits to observe whether the program stops or keeps running), `step_over`/`step_in`/`step_out` (single-step), `pause` (interrupt a running program so you can inspect state).\n- **Inspect**: `threads` (list), `stack_trace` (frames for current stopped thread), `scopes` (needs `frame_id` or a current stopped frame), `variables` (needs `variable_ref` or `scope_id`), `evaluate` (needs `expression`; `context: \"repl\"` for raw debugger commands when the adapter supports them), `output` (captured stdout/stderr/console), `sessions` (tracked debug sessions), `terminate`.\n- Timeouts apply per-request, not to the full session lifetime.\n</instruction>\n\n<caution>\n- Only one active debug session is supported at a time.\n- Some adapters require a launched session to receive `configurationDone` before the target actually runs; if the tool says configuration is pending, set breakpoints and then call `continue`.\n- Adapter availability depends on local binaries. Common built-ins: `gdb`, `lldb-dap`, `python -m debugpy.adapter`, `dlv dap`.\n- `program` must be an executable file or debug target, not a directory or interpreter name that resolves to a workspace directory.\n</caution>\n\n<examples>\n# Launch and inspect hang\n1. `debug(action: \"launch\", program: \"./my_app\")`\n2. `debug(action: \"set_breakpoint\", file: \"src/main.c\", line: 42)`\n3. `debug(action: \"continue\")`\n4. If the program appears hung: `debug(action: \"pause\")`\n5. Inspect state with `threads`, `stack_trace`, `scopes`, and `variables`\n# Launch a Python script with debugpy\n`debug(action: \"launch\", adapter: \"debugpy\", program: \"scripts/job.py\", args: [\"--flag\"])`\n# Raw debugger command through repl\n`debug(action: \"evaluate\", expression: \"info registers\", context: \"repl\")`\n</examples>",
410
+ "description": "Provides debugger access through the Debug Adapter Protocol (DAP).\nUse for launching or attaching debuggers, setting breakpoints, stepping through execution, inspecting threads/stack/variables, evaluating expressions, capturing output, and interrupting hung programs.\n\n<instruction>\n- Prefer over bash for program state, breakpoints, stepping, thread inspection, or interrupting a running process.\n- `action: \"launch\"` starts a session; `program` is required, `adapter` optional (auto-selected from target path and workspace).\n For Python, set `adapter: \"debugpy\"` and `program` to the target `.py` file; put interpreter/script flags in `args`.\n- `action: \"attach\"` connects to an existing process: `pid` for local attach, `port` for remote attach (where the adapter supports it), `adapter` to force a specific debugger.\n- **Breakpoints**: `set_breakpoint`/`remove_breakpoint` with source (`file`+`line`) or function (`function`); optional `condition` for conditional breakpoints.\n- **Flow control**: `continue` (resumes; briefly waits to observe whether the program stops or keeps running), `step_over`/`step_in`/`step_out` (single-step), `pause` (interrupt a running program so you can inspect state).\n- **Inspect**: `threads` (list), `stack_trace` (frames for current stopped thread), `scopes` (needs `frame_id` or a current stopped frame), `variables` (needs `variable_ref` or `scope_id`), `evaluate` (needs `expression`; `context: \"repl\"` for raw debugger commands when the adapter supports them), `output` (captured stdout/stderr/console), `sessions` (tracked debug sessions), `terminate`.\n- Timeouts apply per-request, not to the full session lifetime.\n</instruction>\n\n<caution>\n- Only one active debug session is supported at a time.\n- Some adapters require a launched session to receive `configurationDone` before the target actually runs; if the tool says configuration is pending, set breakpoints and then call `continue`.\n- Adapter availability depends on local binaries. Common built-ins: `gdb`, `lldb-dap`, `python -I -m debugpy.adapter`, `dlv dap`.\n- `program` must be an executable file or debug target, not a directory or interpreter name that resolves to a workspace directory.\n</caution>\n\n<examples>\n# Launch and inspect hang\n1. `debug(action: \"launch\", program: \"./my_app\")`\n2. `debug(action: \"set_breakpoint\", file: \"src/main.c\", line: 42)`\n3. `debug(action: \"continue\")`\n4. If the program appears hung: `debug(action: \"pause\")`\n5. Inspect state with `threads`, `stack_trace`, `scopes`, and `variables`\n# Launch a Python script with debugpy\n`debug(action: \"launch\", adapter: \"debugpy\", program: \"scripts/job.py\", args: [\"--flag\"])`\n# Raw debugger command through repl\n`debug(action: \"evaluate\", expression: \"info registers\", context: \"repl\")`\n</examples>",
411
411
  "parameters": {
412
412
  "type": "object",
413
413
  "properties": {
@@ -2096,7 +2096,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2096
2096
  "task": {
2097
2097
  "name": "task",
2098
2098
  "label": "Task",
2099
- "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- Coordinate with running tasks via `irc` when available, using those ids. `subagent` cancel terminates a task and **cannot carry a message**.\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`.\nUse `irc` for live coordination when it is available.\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. Use `irc` to coordinate overlapping work when it is available; otherwise keep file sets independent.\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, unless `irc` is available and A can answer a small clarification.\nStill sequence when one task produces a large, evolving contract (generated types, schema migration, core module API); live coordination is not a substitute for a finished contract.\nParallel when tasks touch disjoint files, are independent refactors/tests, or need only occasional peer clarification.\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>\nBundled role names: executor, architect, planner, critic. A configured agent may override a bundled role name and takes precedence.\nOther configured agents (project, user, plugin) may also be available; calling with an unknown `agent` lists the agents callable in this session.\n</agents>",
2099
+ "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- Coordinate with running tasks via `irc` when available, using those ids. `subagent` cancel terminates a task and **cannot carry a message**.\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`.\nUse `irc` for live coordination when it is available.\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 session's committed cwd and cannot itself authorize relocation. After an authorized session move, new work uses the committed target; already-admitted work retains its source execution inputs. Verified storage relocation of that same parent does not change child execution cwd; a logical session switch does not substitute successor storage. For another approved repository, start a fresh session rooted there (for example, `gjc --cwd <approved-worktree>`) or use the existing user-authorized relocation surface; `move_session` still only narrows to a descendant directory.\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. Use `irc` to coordinate overlapping work when it is available; otherwise keep file sets independent.\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, unless `irc` is available and A can answer a small clarification.\nStill sequence when one task produces a large, evolving contract (generated types, schema migration, core module API); live coordination is not a substitute for a finished contract.\nParallel when tasks touch disjoint files, are independent refactors/tests, or need only occasional peer clarification.\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>\nBundled role names: executor, architect, planner, critic. A configured agent may override a bundled role name and takes precedence.\nOther configured agents (project, user, plugin) may also be available; calling with an unknown `agent` lists the agents callable in this session.\n</agents>",
2100
2100
  "parameters": {
2101
2101
  "type": "object",
2102
2102
  "properties": {
@@ -2955,6 +2955,34 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2955
2955
  "concurrency": "exclusive",
2956
2956
  "intent": "omit"
2957
2957
  },
2958
+ "report_issue": {
2959
+ "name": "report_issue",
2960
+ "label": "Report Issue",
2961
+ "description": "Records a tool that behaved inconsistently with its own description, for local quality tracking.\n\n<instruction>\n- Call this when a tool's output contradicts what its description or parameters promise: ignored or misapplied parameters, wrong or truncated results, misleading errors, silent no-ops.\n- `tool` is the misbehaving tool's name; `report` states what you called, what you expected, and what actually happened, in one or two sentences.\n- Do not report your own mistakes, user-caused failures, or expected errors (missing files, failing tests, network outages).\n- Do not paste secrets, tokens, credentials, or raw tool output into report; summarize it instead.\n- False positives are acceptable. Report once per distinct problem, then continue the task; this never fixes or retries anything.\n</instruction>\n\n<output>\nConfirms whether the report was recorded. Reports stay on this machine.\n</output>",
2962
+ "parameters": {
2963
+ "type": "object",
2964
+ "properties": {
2965
+ "tool": {
2966
+ "type": "string",
2967
+ "description": "name of the tool that misbehaved"
2968
+ },
2969
+ "report": {
2970
+ "type": "string",
2971
+ "description": "what was called, what was expected, what happened"
2972
+ }
2973
+ },
2974
+ "required": [
2975
+ "tool",
2976
+ "report"
2977
+ ],
2978
+ "additionalProperties": false
2979
+ },
2980
+ "strict": true,
2981
+ "deferrable": false,
2982
+ "loadMode": "essential",
2983
+ "summary": "Record a tool that behaved inconsistently with its description",
2984
+ "intent": "omit"
2985
+ },
2958
2986
  "yield": {
2959
2987
  "name": "yield",
2960
2988
  "label": "Submit Result",
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
4
4
  import type { Component } from "@gajae-code/tui";
5
5
  import { Text } from "@gajae-code/tui";
6
- import { isEnoent, isRecord, prompt, untilAborted } from "@gajae-code/utils";
6
+ import { isEnoent, isRecord, pathIsWithin, prompt, untilAborted } from "@gajae-code/utils";
7
7
  import * as z from "zod/v4";
8
8
  import { stripHashlinePrefixes } from "../edit";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -177,6 +177,31 @@ function parseSqliteWriteTarget(subPath: string, queryString: string): { table:
177
177
  return { table, key };
178
178
  }
179
179
 
180
+ /**
181
+ * `new Database` follows symlinks. A lexical path inside the workspace can
182
+ * still name a file outside it, so open only the real path that stays inside
183
+ * the same session workspace an ordinary write resolves against.
184
+ */
185
+ async function realSqliteDatabaseInWorkspace(
186
+ workspaceRoot: string,
187
+ absolutePath: string,
188
+ signal?: AbortSignal,
189
+ ): Promise<string> {
190
+ signal?.throwIfAborted();
191
+ let realPath: string;
192
+ try {
193
+ realPath = await fs.realpath(absolutePath);
194
+ } catch (error) {
195
+ if (isEnoent(error)) throw error;
196
+ throw new ToolError(error instanceof Error ? error.message : String(error));
197
+ }
198
+ signal?.throwIfAborted();
199
+ if (!pathIsWithin(workspaceRoot, realPath)) {
200
+ throw new ToolError(`SQLite write target resolves outside the workspace: ${absolutePath}`);
201
+ }
202
+ return realPath;
203
+ }
204
+
180
205
  /**
181
206
  * Write tool implementation.
182
207
  *
@@ -372,6 +397,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
372
397
  displayPath: string,
373
398
  content: string,
374
399
  resolvedSqlitePath: ResolvedSqliteWritePath,
400
+ signal?: AbortSignal,
375
401
  ): Promise<AgentToolResult<WriteToolDetails>> {
376
402
  let db: Database | null = null;
377
403
  try {
@@ -379,7 +405,13 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
379
405
  throw new ToolError(`SQLite database '${displayPath}' not found`);
380
406
  }
381
407
 
382
- db = new Database(resolvedSqlitePath.absolutePath, { create: false, strict: true });
408
+ const realDatabasePath = await realSqliteDatabaseInWorkspace(
409
+ this.session.cwd,
410
+ resolvedSqlitePath.absolutePath,
411
+ signal,
412
+ );
413
+ signal?.throwIfAborted();
414
+ db = new Database(realDatabasePath, { create: false, strict: true });
383
415
  db.run("PRAGMA busy_timeout = 3000");
384
416
 
385
417
  const trimmedContent = content.trim();
@@ -718,7 +750,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
718
750
  if (resolvedSqlitePath) {
719
751
  enforcePlanModeWrite(this.session, resolvedSqlitePath.sqlitePath, { op: "update" });
720
752
 
721
- const sqliteResult = await this.#writeSqliteRow(path, cleanContent, resolvedSqlitePath);
753
+ const sqliteResult = await this.#writeSqliteRow(path, cleanContent, resolvedSqlitePath, signal);
722
754
  if (stripped) {
723
755
  const firstText = sqliteResult.content.find(
724
756
  (block): block is { type: "text"; text: string } =>
@@ -5,7 +5,8 @@
5
5
  import type { ThinkingLevel } from "@gajae-code/agent-core";
6
6
  import type { Api, Model } from "@gajae-code/ai/core";
7
7
  import { completeSimple } from "@gajae-code/ai/core";
8
- import { logger, prompt } from "@gajae-code/utils";
8
+ import { logger, prompt, redactCrashSecrets } from "@gajae-code/utils";
9
+ import { redactChangelogEntry } from "../commit/changelog";
9
10
  import type { ModelRegistry } from "../config/model-registry";
10
11
  import { resolveModelRoleValue } from "../config/model-resolver";
11
12
  import type { Settings } from "../config/settings";
@@ -35,6 +36,11 @@ function filterDiffNoise(diff: string): string {
35
36
  return filtered.join("\n");
36
37
  }
37
38
 
39
+ /** Keep changelog patterns, then scrub known credential shapes on the whole string. */
40
+ function redactCommitMessageText(text: string): string {
41
+ return redactCrashSecrets(redactChangelogEntry(text));
42
+ }
43
+
38
44
  function getModelCandidates(
39
45
  registry: ModelRegistry,
40
46
  settings: Settings,
@@ -87,7 +93,7 @@ export async function generateCommitMessage(
87
93
  logger.debug("commit-msg-generator: diff is empty after noise filtering");
88
94
  return null;
89
95
  }
90
- const userMessage = `<diff>\n${truncatedDiff}\n</diff>`;
96
+ const userMessage = `<diff>\n${redactCommitMessageText(truncatedDiff)}\n</diff>`;
91
97
 
92
98
  for (const candidate of candidates) {
93
99
  const apiKey = await registry.getApiKey(candidate.model, sessionId);
@@ -120,6 +126,7 @@ export async function generateCommitMessage(
120
126
 
121
127
  // Clean up: remove wrapping quotes, backticks, trailing period
122
128
  msg = msg.replace(/^[`"']|[`"']$/g, "").replace(/\.$/, "");
129
+ msg = redactCommitMessageText(msg);
123
130
 
124
131
  logger.debug("commit-msg-generator: generated", { model: candidate.model.id, msg });
125
132
  return msg;
@@ -80,6 +80,7 @@ type MentionCandidate = {
80
80
  path: string;
81
81
  pathLower: string;
82
82
  normalizedPath: string;
83
+ segments: string[];
83
84
  };
84
85
 
85
86
  function normalizeMentionQuery(query: string): string {
@@ -117,11 +118,37 @@ async function listMentionCandidates(cwd: string): Promise<MentionCandidate[]> {
117
118
  if (normalizedPath.length === 0) {
118
119
  continue;
119
120
  }
120
- candidates.push({ path: entry, pathLower, normalizedPath });
121
+ const segments = pathLower.split(PATH_SEPARATOR_REGEX).filter(segment => segment.length > 0);
122
+ candidates.push({ path: entry, pathLower, normalizedPath, segments });
121
123
  }
122
124
  return candidates;
123
125
  }
124
126
 
127
+ /**
128
+ * True when `query` can be spelled as prefixes of path segments taken in order
129
+ * (segments may be skipped), e.g. `httpserap` → `http_server_api_tests`.
130
+ * Plain subsequence matching would also accept letters scattered mid-word across
131
+ * any long path, so a stray `@handle` or `@word` would resolve to an unrelated file.
132
+ */
133
+ function matchesSegmentPrefixes(query: string, segments: string[]): boolean {
134
+ const failed = new Set<number>();
135
+ const visit = (segmentIndex: number, queryIndex: number): boolean => {
136
+ if (queryIndex === query.length) return true;
137
+ if (segmentIndex === segments.length) return false;
138
+ const key = segmentIndex * (query.length + 1) + queryIndex;
139
+ if (failed.has(key)) return false;
140
+ const segment = segments[segmentIndex]!;
141
+ for (let taken = 1; taken <= segment.length && queryIndex + taken <= query.length; taken++) {
142
+ if (segment[taken - 1] !== query[queryIndex + taken - 1]) break;
143
+ if (visit(segmentIndex + 1, queryIndex + taken)) return true;
144
+ }
145
+ if (visit(segmentIndex + 1, queryIndex)) return true;
146
+ failed.add(key);
147
+ return false;
148
+ };
149
+ return visit(0, 0);
150
+ }
151
+
125
152
  async function resolveMentionPath(
126
153
  filePath: string,
127
154
  cwd: string,
@@ -148,6 +175,7 @@ async function resolveMentionPath(
148
175
  }
149
176
 
150
177
  const scored = candidates
178
+ .filter(candidate => matchesSegmentPrefixes(normalizedQuery, candidate.segments))
151
179
  .map(candidate => ({ candidate, match: fuzzyMatch(normalizedQuery, candidate.normalizedPath) }))
152
180
  .filter(entry => entry.match.matches)
153
181
  .sort((a, b) => {
@@ -5,7 +5,7 @@ import * as path from "node:path";
5
5
 
6
6
  import type { AgentMessage } from "@gajae-code/agent-core";
7
7
  import { type Api, type AssistantMessage, completeSimple, type Model, type Tool } from "@gajae-code/ai/core";
8
- import { logger, prompt } from "@gajae-code/utils";
8
+ import { logger, prompt, redactCrashSecrets } from "@gajae-code/utils";
9
9
  import type { ModelRegistry } from "../config/model-registry";
10
10
  import { resolveRoleSelection } from "../config/model-resolver";
11
11
  import type { Settings } from "../config/settings";
@@ -159,7 +159,12 @@ ${truncatedMessage}
159
159
  userMessage,
160
160
  maxTokens,
161
161
  };
162
- logger.debug("title-generator: request", request);
162
+ logger.debug("title-generator: request", {
163
+ model: request.model,
164
+ systemPrompt: request.systemPrompt,
165
+ userMessage: redactCrashSecrets(request.userMessage),
166
+ maxTokens: request.maxTokens,
167
+ });
163
168
 
164
169
  try {
165
170
  const response = await completeSimple(
@@ -169,7 +169,9 @@ specialHandlers.push(
169
169
  handleGitHub,
170
170
  handleGitLab,
171
171
  // Video/Media
172
- (url, timeout, signal) => handleYouTube(url, timeout, signal),
172
+ // Registered directly (not through an argument-dropping adapter) so the
173
+ // handler receives the caller's storage and settings.
174
+ handleYouTube,
173
175
  handleVimeo,
174
176
  handleSpotify,
175
177
  handleDiscogs,
@@ -2,6 +2,7 @@
2
2
  * Shared types and utilities for web-fetch handlers
3
3
  */
4
4
  import { ptree } from "@gajae-code/utils";
5
+ import type { Settings } from "../../config/settings";
5
6
  import type { AgentStorage } from "../../session/agent-storage";
6
7
  import { ToolAbortError } from "../../tools/tool-errors";
7
8
  import type { AddressResolver } from "../insane/url-guard";
@@ -20,11 +21,18 @@ export interface RenderResult {
20
21
  notes: string[];
21
22
  }
22
23
 
24
+ /**
25
+ * `settings` is the caller's session settings. Handlers must read through it
26
+ * rather than the process-global singleton: the SDK/ACP host path runs tools
27
+ * without ever calling `Settings.init()`, where a global read throws
28
+ * "Settings not initialized" instead of scraping the page.
29
+ */
23
30
  export type SpecialHandler = (
24
31
  url: string,
25
32
  timeout: number,
26
33
  signal?: AbortSignal,
27
34
  storage?: AgentStorage | null,
35
+ settings?: Settings,
28
36
  ) => Promise<RenderResult | null>;
29
37
 
30
38
  export const MAX_OUTPUT_CHARS = 500_000;
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { ptree, Snowflake } from "@gajae-code/utils";
5
- import { settings } from "../../config/settings";
5
+ import type { Settings } from "../../config/settings";
6
6
  import type { AgentStorage } from "../../session/agent-storage";
7
7
  import { throwIfAborted } from "../../tools/tool-errors";
8
8
  import { ensureTool } from "../../utils/tools-manager";
@@ -115,6 +115,7 @@ export const handleYouTube: SpecialHandler = async (
115
115
  timeout: number,
116
116
  userSignal?: AbortSignal,
117
117
  storage?: AgentStorage | null,
118
+ settings?: Settings,
118
119
  ): Promise<RenderResult | null> => {
119
120
  throwIfAborted(userSignal);
120
121
  const yt = parseYouTubeUrl(url);
@@ -126,7 +127,7 @@ export const handleYouTube: SpecialHandler = async (
126
127
  const videoUrl = `https://www.youtube.com/watch?v=${yt.videoId}`;
127
128
 
128
129
  // Prefer Parallel extract when credentials are available
129
- if (settings.get("providers.parallelFetch") && findParallelApiKey(storage)) {
130
+ if (settings?.get("providers.parallelFetch") === true && findParallelApiKey(storage)) {
130
131
  try {
131
132
  const parallelResult = await extractWithParallel(
132
133
  [videoUrl],