@gajae-code/coding-agent 0.3.0 → 0.3.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 (175) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/types/async/job-manager.d.ts +7 -0
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/config/keybindings.d.ts +5 -0
  6. package/dist/types/config/settings-schema.d.ts +4 -4
  7. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  8. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  9. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  10. package/dist/types/eval/py/executor.d.ts +2 -0
  11. package/dist/types/eval/py/kernel.d.ts +2 -0
  12. package/dist/types/exec/bash-executor.d.ts +10 -0
  13. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  14. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  16. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  17. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  18. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  20. package/dist/types/hooks/skill-state.d.ts +21 -0
  21. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  22. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  23. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  24. package/dist/types/internal-urls/types.d.ts +4 -0
  25. package/dist/types/lsp/index.d.ts +10 -10
  26. package/dist/types/modes/bridge/auth.d.ts +12 -0
  27. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  28. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  29. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  30. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  32. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  33. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  34. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  35. package/dist/types/modes/components/status-line.d.ts +2 -0
  36. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  37. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  38. package/dist/types/modes/index.d.ts +1 -0
  39. package/dist/types/modes/interactive-mode.d.ts +1 -0
  40. package/dist/types/modes/jobs-observer.d.ts +57 -0
  41. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  42. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  43. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  44. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  45. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  46. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  47. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  48. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  49. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  50. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  51. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  52. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  53. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  54. package/dist/types/modes/types.d.ts +1 -0
  55. package/dist/types/sdk.d.ts +2 -0
  56. package/dist/types/session/agent-session.d.ts +11 -1
  57. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  58. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  59. package/dist/types/task/id.d.ts +7 -0
  60. package/dist/types/task/index.d.ts +5 -0
  61. package/dist/types/task/receipt.d.ts +85 -0
  62. package/dist/types/task/spawn-gate.d.ts +38 -0
  63. package/dist/types/task/types.d.ts +143 -11
  64. package/dist/types/tools/cron.d.ts +6 -0
  65. package/dist/types/tools/index.d.ts +2 -0
  66. package/dist/types/tools/path-utils.d.ts +1 -0
  67. package/dist/types/tools/subagent.d.ts +15 -0
  68. package/package.json +7 -7
  69. package/scripts/build-binary.ts +7 -0
  70. package/src/async/job-manager.ts +36 -0
  71. package/src/cli/args.ts +9 -2
  72. package/src/commands/deep-interview.ts +1 -0
  73. package/src/commands/harness.ts +289 -19
  74. package/src/commands/launch.ts +2 -2
  75. package/src/commands/state.ts +2 -1
  76. package/src/commands/team.ts +22 -4
  77. package/src/config/keybindings.ts +6 -0
  78. package/src/config/settings-schema.ts +6 -3
  79. package/src/dap/client.ts +17 -3
  80. package/src/debug/crash-diagnostics.ts +223 -0
  81. package/src/debug/runtime-gauges.ts +20 -0
  82. package/src/deep-interview/render-middleware.ts +6 -0
  83. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  84. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  85. package/src/defaults/gjc/skills/ultragoal/SKILL.md +28 -2
  86. package/src/eval/py/executor.ts +21 -1
  87. package/src/eval/py/kernel.ts +15 -0
  88. package/src/exec/bash-executor.ts +41 -0
  89. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  90. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  91. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  92. package/src/gjc-runtime/state-migrations.ts +54 -7
  93. package/src/gjc-runtime/state-runtime.ts +461 -64
  94. package/src/gjc-runtime/state-schema.ts +192 -0
  95. package/src/gjc-runtime/state-writer.ts +32 -1
  96. package/src/gjc-runtime/team-runtime.ts +177 -105
  97. package/src/gjc-runtime/ultragoal-runtime.ts +114 -26
  98. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  99. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  100. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  101. package/src/harness-control-plane/control-endpoint.ts +19 -8
  102. package/src/harness-control-plane/owner.ts +57 -10
  103. package/src/harness-control-plane/state-machine.ts +2 -1
  104. package/src/hooks/skill-state.ts +176 -26
  105. package/src/internal-urls/agent-protocol.ts +68 -21
  106. package/src/internal-urls/artifact-protocol.ts +12 -17
  107. package/src/internal-urls/docs-index.generated.ts +3 -2
  108. package/src/internal-urls/registry-helpers.ts +19 -16
  109. package/src/internal-urls/types.ts +4 -0
  110. package/src/lsp/client.ts +18 -2
  111. package/src/main.ts +21 -5
  112. package/src/modes/bridge/auth.ts +41 -0
  113. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  114. package/src/modes/bridge/bridge-mode.ts +520 -0
  115. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  116. package/src/modes/bridge/event-stream.ts +70 -0
  117. package/src/modes/components/custom-editor.ts +101 -0
  118. package/src/modes/components/hook-selector.ts +61 -18
  119. package/src/modes/components/jobs-overlay-model.ts +109 -0
  120. package/src/modes/components/jobs-overlay.ts +172 -0
  121. package/src/modes/components/status-line/presets.ts +7 -5
  122. package/src/modes/components/status-line/segments.ts +25 -0
  123. package/src/modes/components/status-line/types.ts +2 -0
  124. package/src/modes/components/status-line.ts +9 -1
  125. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  126. package/src/modes/controllers/input-controller.ts +97 -9
  127. package/src/modes/controllers/selector-controller.ts +29 -0
  128. package/src/modes/index.ts +1 -0
  129. package/src/modes/interactive-mode.ts +27 -0
  130. package/src/modes/jobs-observer.ts +204 -0
  131. package/src/modes/rpc/host-tools.ts +1 -186
  132. package/src/modes/rpc/host-uris.ts +1 -235
  133. package/src/modes/rpc/rpc-client.ts +25 -10
  134. package/src/modes/rpc/rpc-mode.ts +12 -381
  135. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  136. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  137. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  138. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  139. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  140. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  141. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  142. package/src/modes/shared/agent-wire/responses.ts +17 -0
  143. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  144. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  145. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  146. package/src/modes/types.ts +1 -0
  147. package/src/prompts/tools/subagent.md +12 -7
  148. package/src/prompts/tools/task-summary.md +3 -9
  149. package/src/prompts/tools/task.md +5 -1
  150. package/src/sdk.ts +4 -0
  151. package/src/session/agent-session.ts +214 -38
  152. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  153. package/src/skill-state/workflow-state-contract.ts +7 -4
  154. package/src/skill-state/workflow-state-version.ts +3 -0
  155. package/src/slash-commands/builtin-registry.ts +8 -0
  156. package/src/task/executor.ts +29 -5
  157. package/src/task/id.ts +33 -0
  158. package/src/task/index.ts +257 -67
  159. package/src/task/output-manager.ts +5 -4
  160. package/src/task/receipt.ts +297 -0
  161. package/src/task/render.ts +48 -131
  162. package/src/task/spawn-gate.ts +132 -0
  163. package/src/task/types.ts +48 -7
  164. package/src/tools/ask.ts +73 -33
  165. package/src/tools/ast-edit.ts +1 -0
  166. package/src/tools/ast-grep.ts +1 -0
  167. package/src/tools/bash.ts +1 -1
  168. package/src/tools/cron.ts +48 -0
  169. package/src/tools/find.ts +4 -1
  170. package/src/tools/index.ts +2 -0
  171. package/src/tools/path-utils.ts +3 -2
  172. package/src/tools/read.ts +1 -0
  173. package/src/tools/search.ts +1 -0
  174. package/src/tools/skill.ts +6 -1
  175. package/src/tools/subagent.ts +237 -84
package/src/task/types.ts CHANGED
@@ -2,12 +2,16 @@ import type { ThinkingLevel } from "@gajae-code/agent-core";
2
2
  import type { Usage } from "@gajae-code/ai";
3
3
  import { $env } from "@gajae-code/utils";
4
4
  import * as z from "zod/v4";
5
+ import { isValidTaskId, TASK_ID_DESCRIPTION } from "./id";
6
+ import type { TaskResultReceipt } from "./receipt";
5
7
  import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
8
+ import type { SpawnPlanReceipt } from "./spawn-gate";
6
9
  import type { NestedRepoPatch } from "./worktree";
7
10
 
8
11
  /** Source of an agent definition */
9
12
  export type AgentSource = "bundled" | "user" | "project";
10
13
  export type ForkContextPolicy = "forbidden" | "allowed";
14
+ export type ForkContextMode = "none" | "receipt" | "last-turn" | "bounded" | "full";
11
15
 
12
16
  const parseNumber = (value: string | undefined, defaultValue: number): number => {
13
17
  if (value) {
@@ -59,16 +63,27 @@ export interface SubagentLifecyclePayload {
59
63
  }
60
64
 
61
65
  const assignmentDescription = "per-task instructions; self-contained";
66
+ const spawnPlanSchema = z
67
+ .object({
68
+ whyParallel: z.string(),
69
+ whyNotLocal: z.string(),
70
+ independence: z.string(),
71
+ expectedReceiptShape: z.string(),
72
+ maxInlineTokens: z.number(),
73
+ })
74
+ .describe("justification required before spawning more than four tasks or reviewer-spawned explore tasks");
62
75
 
63
76
  const createTaskItemSchema = (_contextEnabled: boolean) =>
64
77
  z.object({
65
- id: z.string().max(48).describe("camelcase identifier"),
78
+ id: z.string().max(48).refine(isValidTaskId, TASK_ID_DESCRIPTION).describe("filesystem-safe task identifier"),
66
79
  description: z.string().describe("ui label, not seen by subagent"),
67
80
  assignment: z.string().describe(assignmentDescription),
68
81
  inheritContext: z
69
- .boolean()
82
+ .enum(["none", "receipt", "last-turn", "bounded", "full"])
70
83
  .optional()
71
- .describe("explicit request to seed a subagent with sanitized parent context"),
84
+ .describe(
85
+ "fork-context mode: none/omitted copies no parent context; receipt copies a minimal receipt-sized snapshot; last-turn copies only the latest exchange; bounded copies the bounded default snapshot; full copies a larger sanitized snapshot up to the configured/model token cap",
86
+ ),
72
87
  });
73
88
 
74
89
  /** Single task item for parallel execution (default shape with context enabled). */
@@ -77,11 +92,23 @@ export type TaskItem = z.infer<typeof taskItemSchema>;
77
92
 
78
93
  const createTaskSchema = (options: { isolationEnabled: boolean; simpleMode: TaskSimpleMode }) => {
79
94
  const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(options.simpleMode);
80
- const itemSchema = createTaskItemSchema(contextEnabled);
95
+ let itemSchema = createTaskItemSchema(contextEnabled);
96
+ if (!contextEnabled) {
97
+ itemSchema = itemSchema.superRefine((item, ctx) => {
98
+ if (item.inheritContext !== undefined && item.inheritContext !== "none") {
99
+ ctx.addIssue({
100
+ code: "custom",
101
+ path: ["inheritContext"],
102
+ message: "Independent tasks cannot inherit parent context; omit inheritContext or set it to none.",
103
+ });
104
+ }
105
+ });
106
+ }
81
107
 
82
108
  let schema = z.object({
83
109
  agent: z.string().describe("agent type"),
84
110
  tasks: z.array(itemSchema).describe("tasks to execute in parallel"),
111
+ spawnPlan: spawnPlanSchema.optional(),
85
112
  });
86
113
  if (contextEnabled) {
87
114
  schema = schema.extend({
@@ -139,6 +166,7 @@ export interface TaskParams {
139
166
  agent: string;
140
167
  context?: string;
141
168
  schema?: string;
169
+ spawnPlan?: SpawnPlanReceipt;
142
170
  tasks: TaskItem[];
143
171
  isolated?: boolean;
144
172
  }
@@ -291,6 +319,8 @@ export interface SingleResult {
291
319
  branchName?: string;
292
320
  /** Nested repo patches to apply after parent merge */
293
321
  nestedPatches?: NestedRepoPatch[];
322
+ /** Whether isolated execution produced a non-empty root or nested patch. */
323
+ producedChanges?: boolean;
294
324
  /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
295
325
  extractedToolData?: Record<string, unknown[]>;
296
326
  /**
@@ -304,17 +334,28 @@ export interface SingleResult {
304
334
  errorMessage: string;
305
335
  };
306
336
  /** Output metadata for agent:// URL integration */
307
- outputMeta?: { lineCount: number; charCount: number };
337
+ outputMeta?: { lineCount: number; charCount: number; byteSize?: number; sha256?: string };
338
+ /** Fork-context seed accounting for this subagent, when inherited parent context was cloned. */
339
+ forkContext?: { mode: ForkContextMode; clonedTokens: number };
308
340
  }
309
341
 
310
342
  /** Tool details for TUI rendering */
311
343
  export interface TaskToolDetails {
312
344
  projectAgentsDir: string | null;
313
- results: SingleResult[];
345
+ results: TaskResultReceipt[];
314
346
  totalDurationMs: number;
315
347
  /** Aggregated usage across all subagents. */
316
348
  usage?: Usage;
317
- outputPaths?: string[];
349
+ /** Aggregate cloned tokens copied into fork-context seeds across subagents. */
350
+ forkContextClonedTokens?: number;
351
+ roiSummary?: {
352
+ childCount: number;
353
+ totalTokens: number;
354
+ totalCostTotal?: number;
355
+ totalClonedTokens?: number;
356
+ /** Advisory ids for terminal children that spent tokens without detectable output/review/changes. */
357
+ lowRoiChildIds: string[];
358
+ };
318
359
  progress?: AgentProgress[];
319
360
  async?: {
320
361
  state: "running" | "paused" | "queued" | "completed" | "failed";
package/src/tools/ask.ts CHANGED
@@ -28,7 +28,11 @@ import {
28
28
  } from "@gajae-code/tui";
29
29
  import { prompt, untilAborted } from "@gajae-code/utils";
30
30
  import * as z from "zod/v4";
31
- import { formatDeepInterviewSelectorPrompt, renderDeepInterviewAskQuestion } from "../deep-interview/render-middleware";
31
+ import {
32
+ formatDeepInterviewSelectorPrompt,
33
+ isDeepInterviewAskQuestion,
34
+ renderDeepInterviewAskQuestion,
35
+ } from "../deep-interview/render-middleware";
32
36
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
33
37
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
34
38
  import askDescription from "../prompts/tools/ask.md" with { type: "text" };
@@ -85,7 +89,7 @@ export interface AskToolDetails {
85
89
 
86
90
  const OTHER_OPTION = "Other (type your own)";
87
91
  const RECOMMENDED_SUFFIX = " (Recommended)";
88
- const DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS = 12;
92
+ const DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS = Number.MAX_SAFE_INTEGER;
89
93
 
90
94
  function getDoneOptionLabel(): string {
91
95
  return `${theme.status.success} Done selecting`;
@@ -117,6 +121,17 @@ function stripRecommendedSuffix(label: string): string {
117
121
  return label.endsWith(RECOMMENDED_SUFFIX) ? label.slice(0, -RECOMMENDED_SUFFIX.length) : label;
118
122
  }
119
123
 
124
+ function formatNumberedOptionLabel(label: string, index: number): string {
125
+ if (/^\s*\d+[.)]\s+/.test(label)) {
126
+ return label;
127
+ }
128
+ return `${index + 1}. ${label}`;
129
+ }
130
+
131
+ function numberOptionLabels(labels: string[]): string[] {
132
+ return labels.map(formatNumberedOptionLabel);
133
+ }
134
+
120
135
  // =============================================================================
121
136
  // Question Selection Logic
122
137
  // =============================================================================
@@ -141,6 +156,7 @@ interface AskSingleQuestionOptions {
141
156
  initialSelection?: Pick<SelectionResult, "selectedOptions" | "customInput">;
142
157
  navigation?: NavigationControls;
143
158
  scrollTitleRows?: number;
159
+ otherOptionLabel?: string;
144
160
  }
145
161
 
146
162
  interface UIContext {
@@ -194,7 +210,8 @@ async function askSingleQuestion(
194
210
  const baseHelpText = navigation
195
211
  ? "up/down navigate enter select ←/→ question esc cancel"
196
212
  : "up/down navigate enter select esc cancel";
197
- const helpText = scrollTitleRows === undefined ? baseHelpText : `${baseHelpText} PgUp/PgDn scroll question`;
213
+ const helpText =
214
+ scrollTitleRows === undefined ? baseHelpText : `${baseHelpText} wheel/PgUp/PgDn scroll question`;
198
215
  const dialogOptions = {
199
216
  initialIndex,
200
217
  timeout,
@@ -231,6 +248,7 @@ async function askSingleQuestion(
231
248
  const input = signal ? await untilAborted(signal, showCustomInput) : await showCustomInput();
232
249
  return { input };
233
250
  };
251
+ const otherOptionLabel = options.otherOptionLabel ?? OTHER_OPTION;
234
252
 
235
253
  const promptWithProgress = navigation?.progressText ? `${question} (${navigation.progressText})` : question;
236
254
  if (multi) {
@@ -252,7 +270,7 @@ async function askSingleQuestion(
252
270
  if (!navigation?.allowForward && selected.size > 0) {
253
271
  opts.push(doneLabel);
254
272
  }
255
- opts.push(OTHER_OPTION);
273
+ opts.push(otherOptionLabel);
256
274
 
257
275
  const prefix = selected.size > 0 ? `(${selected.size} selected) ` : "";
258
276
  const {
@@ -273,7 +291,7 @@ async function askSingleQuestion(
273
291
  }
274
292
  if (choice === doneLabel) break;
275
293
 
276
- if (choice === OTHER_OPTION) {
294
+ if (choice === otherOptionLabel) {
277
295
  if (selectTimedOut) {
278
296
  timedOut = true;
279
297
  break;
@@ -315,7 +333,7 @@ async function askSingleQuestion(
315
333
  selectedOptions = Array.from(selected);
316
334
  } else {
317
335
  const displayLabels = addRecommendedSuffix(optionLabels, recommended);
318
- const optionsWithNavigation = [...displayLabels, OTHER_OPTION];
336
+ const optionsWithNavigation = [...displayLabels, otherOptionLabel];
319
337
 
320
338
  let initialIndex = recommended;
321
339
  const previouslySelected = selectedOptions[0];
@@ -344,7 +362,7 @@ async function askSingleQuestion(
344
362
  if (!timedOut) {
345
363
  return { selectedOptions, customInput, timedOut, cancelled: true };
346
364
  }
347
- } else if (choice === OTHER_OPTION) {
365
+ } else if (choice === otherOptionLabel) {
348
366
  if (!selectTimedOut) {
349
367
  const customResult = await promptForCustomInput();
350
368
  if (customResult.input !== undefined) {
@@ -458,25 +476,46 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
458
476
  q: AskParams["questions"][number],
459
477
  options?: { previous?: QuestionResult; navigation?: NavigationControls },
460
478
  ) => {
461
- const optionLabels = q.options.map(o => o.label);
479
+ const rawOptionLabels = q.options.map(o => o.label);
462
480
  try {
463
481
  const deepInterviewPrompt = formatDeepInterviewSelectorPrompt(q.question);
464
482
  const displayQuestion = deepInterviewPrompt ?? q.question;
465
- const { selectedOptions, customInput, navigation, cancelled, timedOut } = await askSingleQuestion(
466
- ui,
467
- displayQuestion,
468
- optionLabels,
469
- q.multi ?? false,
470
- {
471
- recommended: q.recommended,
472
- timeout: timeout ?? undefined,
473
- signal,
474
- initialSelection: options?.previous,
475
- navigation: options?.navigation,
476
- scrollTitleRows: deepInterviewPrompt === null ? undefined : DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS,
477
- },
478
- );
479
- return { optionLabels, selectedOptions, customInput, navigation, cancelled, timedOut };
483
+ const shouldNumberOptions = isDeepInterviewAskQuestion(q.question);
484
+ const optionLabels = shouldNumberOptions ? numberOptionLabels(rawOptionLabels) : rawOptionLabels;
485
+ const initialSelection =
486
+ shouldNumberOptions && options?.previous
487
+ ? {
488
+ ...options.previous,
489
+ selectedOptions: options.previous.selectedOptions.map(selected => {
490
+ const rawIndex = rawOptionLabels.indexOf(selected);
491
+ return rawIndex >= 0 ? (optionLabels[rawIndex] ?? selected) : selected;
492
+ }),
493
+ }
494
+ : options?.previous;
495
+ const {
496
+ selectedOptions: displaySelectedOptions,
497
+ customInput,
498
+ navigation,
499
+ cancelled,
500
+ timedOut,
501
+ } = await askSingleQuestion(ui, displayQuestion, optionLabels, q.multi ?? false, {
502
+ recommended: q.recommended,
503
+ timeout: timeout ?? undefined,
504
+ signal,
505
+ initialSelection,
506
+ navigation: options?.navigation,
507
+ scrollTitleRows: deepInterviewPrompt === null ? undefined : DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS,
508
+ otherOptionLabel: shouldNumberOptions
509
+ ? formatNumberedOptionLabel(OTHER_OPTION, optionLabels.length)
510
+ : undefined,
511
+ });
512
+ const selectedOptions = shouldNumberOptions
513
+ ? displaySelectedOptions.map(selected => {
514
+ const displayIndex = optionLabels.indexOf(selected);
515
+ return displayIndex >= 0 ? (rawOptionLabels[displayIndex] ?? selected) : selected;
516
+ })
517
+ : displaySelectedOptions;
518
+ return { optionLabels: rawOptionLabels, selectedOptions, customInput, navigation, cancelled, timedOut };
480
519
  } catch (error) {
481
520
  if (error instanceof Error && error.name === "AbortError") {
482
521
  throw new ToolAbortError("Ask input was cancelled");
@@ -668,17 +707,17 @@ export const askToolRenderer = {
668
707
  container.addChild(
669
708
  new Text(` ${uiTheme.fg("dim", qBranch)} ${uiTheme.fg("dim", `[${q.id}]`)}${metaStr}`, 0, 0),
670
709
  );
671
- container.addChild(
672
- renderDeepInterviewAskQuestion(q.question, uiTheme) ??
673
- new Markdown(q.question, 3, 0, mdTheme, accentStyle),
674
- );
710
+ const deepInterviewQuestion = renderDeepInterviewAskQuestion(q.question, uiTheme);
711
+ container.addChild(deepInterviewQuestion ?? new Markdown(q.question, 3, 0, mdTheme, accentStyle));
675
712
 
676
713
  const qOptions = q.options;
677
714
  if (qOptions?.length) {
678
715
  const entries = qOptions.map((opt, j) => {
679
716
  const isLastOpt = j === qOptions.length - 1;
680
717
  const optBranch = isLastOpt ? uiTheme.tree.last : uiTheme.tree.branch;
681
- const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
718
+ const shouldNumberOption = deepInterviewQuestion !== null || isDeepInterviewAskQuestion(q.question);
719
+ const displayLabel = shouldNumberOption ? formatNumberedOptionLabel(opt.label, j) : opt.label;
720
+ const optLabel = renderInlineMarkdown(displayLabel, mdTheme, t => uiTheme.fg("muted", t));
682
721
  return {
683
722
  prefix: ` ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("dim", uiTheme.checkbox.unchecked)} `,
684
723
  label: optLabel,
@@ -694,23 +733,24 @@ export const askToolRenderer = {
694
733
  if (!args.question) {
695
734
  return new Text(formatErrorMessage("No question provided", uiTheme), 0, 0);
696
735
  }
736
+ const question = args.question;
697
737
 
698
738
  const container = new Container();
699
739
  const meta: string[] = [];
700
740
  if (args.multi) meta.push("multi");
701
741
  if (args.options?.length) meta.push(`options:${args.options.length}`);
702
742
  container.addChild(new Text(`${label}${formatMeta(meta, uiTheme)}`, 0, 0));
703
- container.addChild(
704
- renderDeepInterviewAskQuestion(args.question, uiTheme) ??
705
- new Markdown(args.question, 1, 0, mdTheme, accentStyle),
706
- );
743
+ const deepInterviewQuestion = renderDeepInterviewAskQuestion(question, uiTheme);
744
+ container.addChild(deepInterviewQuestion ?? new Markdown(question, 1, 0, mdTheme, accentStyle));
707
745
 
708
746
  const options = args.options;
709
747
  if (options?.length) {
710
748
  const entries = options.map((opt, i) => {
711
749
  const isLast = i === options.length - 1;
712
750
  const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
713
- const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
751
+ const shouldNumberOption = deepInterviewQuestion !== null || isDeepInterviewAskQuestion(question);
752
+ const displayLabel = shouldNumberOption ? formatNumberedOptionLabel(opt.label, i) : opt.label;
753
+ const optLabel = renderInlineMarkdown(displayLabel, mdTheme, t => uiTheme.fg("muted", t));
714
754
  return {
715
755
  prefix: ` ${uiTheme.fg("dim", branch)} ${uiTheme.fg("dim", uiTheme.checkbox.unchecked)} `,
716
756
  label: optLabel,
@@ -204,6 +204,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
204
204
  const scope = await resolveToolSearchScope({
205
205
  rawPaths: params.paths,
206
206
  cwd: this.session.cwd,
207
+ getArtifactsDir: this.session.getArtifactsDir,
207
208
  internalUrlAction: "rewrite",
208
209
  });
209
210
  const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
@@ -151,6 +151,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
151
151
  const scope = await resolveToolSearchScope({
152
152
  rawPaths: params.paths,
153
153
  cwd: this.session.cwd,
154
+ getArtifactsDir: this.session.getArtifactsDir,
154
155
  internalUrlAction: "search",
155
156
  });
156
157
  const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
package/src/tools/bash.ts CHANGED
@@ -693,7 +693,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
693
693
  throw error instanceof Error ? error : new Error(String(error));
694
694
  }
695
695
  },
696
- { ownerId },
696
+ { ownerId, metadata: { monitor: true } },
697
697
  );
698
698
  currentJobId = jobId;
699
699
  return { jobId, label, commandCwd: prepared.commandCwd };
package/src/tools/cron.ts CHANGED
@@ -137,6 +137,7 @@ function deleteRecord(ownerId: string | undefined, id: string): boolean {
137
137
  disposeRecord(record);
138
138
  const deleted = state.jobs.delete(id);
139
139
  if (state.jobs.size === 0) schedulesByOwner.delete(key);
140
+ if (deleted) notifyCronChange();
140
141
  return deleted;
141
142
  }
142
143
 
@@ -158,6 +159,7 @@ export function clearOwnerSchedules(ownerId: string | undefined): void {
158
159
  state.jobs.clear();
159
160
  state.cleanupRegistered = false;
160
161
  schedulesByOwner.delete(key);
162
+ notifyCronChange();
161
163
  }
162
164
 
163
165
  /** Reset every owner's schedule store. Test-only. */
@@ -167,6 +169,51 @@ export function resetCronRegistryForTests(): void {
167
169
  clearOwnerSchedules(ownerId);
168
170
  }
169
171
  schedulesByOwner.clear();
172
+ notifyCronChange();
173
+ }
174
+
175
+ /** Module-level listeners notified whenever the cron schedule set changes. */
176
+ const cronChangeListeners = new Set<() => void>();
177
+
178
+ /** Subscribe to cron schedule-set changes. Returns an unsubscribe function. */
179
+ export function onCronChange(cb: () => void): () => void {
180
+ cronChangeListeners.add(cb);
181
+ return () => {
182
+ cronChangeListeners.delete(cb);
183
+ };
184
+ }
185
+
186
+ function notifyCronChange(): void {
187
+ for (const cb of cronChangeListeners) {
188
+ try {
189
+ cb();
190
+ } catch (error) {
191
+ logger.warn("Cron change listener failed", {
192
+ error: error instanceof Error ? error.message : String(error),
193
+ });
194
+ }
195
+ }
196
+ }
197
+
198
+ /** Snapshot the scheduled cron jobs for an owner (or all owners when omitted). */
199
+ export function listCronSnapshots(ownerId?: string): CronJobSnapshot[] {
200
+ const out: CronJobSnapshot[] = [];
201
+ if (ownerId !== undefined) {
202
+ const state = schedulesByOwner.get(ownerKey(ownerId));
203
+ if (state) {
204
+ for (const record of state.jobs.values()) out.push(record.snapshot);
205
+ }
206
+ return out;
207
+ }
208
+ for (const state of schedulesByOwner.values()) {
209
+ for (const record of state.jobs.values()) out.push(record.snapshot);
210
+ }
211
+ return out;
212
+ }
213
+
214
+ /** Delete a scheduled cron job by owner-scoped id. Returns true when removed. */
215
+ export function deleteCronJobById(ownerId: string | undefined, id: string): boolean {
216
+ return deleteRecord(ownerId, id);
170
217
  }
171
218
 
172
219
  const CRON_FIELD_BOUNDS: Array<{ name: string; min: number; max: number }> = [
@@ -448,6 +495,7 @@ function scheduleRecord(ownerId: string | undefined, state: OwnerScheduleState,
448
495
  });
449
496
  record.snapshot.nextFireAt = fireAt;
450
497
  record.timer = setCronTimeout(() => fireRecord(ownerId, state, record.snapshot.id), fireAt - now);
498
+ notifyCronChange();
451
499
  }
452
500
 
453
501
  function scheduleExpiry(ownerId: string | undefined, record: CronScheduleRecord): void {
package/src/tools/find.ts CHANGED
@@ -155,7 +155,10 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
155
155
  if (hasGlobPathChars(rawPattern)) {
156
156
  throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPattern}`);
157
157
  }
158
- const resource = await internalRouter.resolve(rawPattern);
158
+ const resource = await internalRouter.resolve(rawPattern, {
159
+ cwd: this.session.cwd,
160
+ getArtifactsDir: this.session.getArtifactsDir,
161
+ });
159
162
  if (!resource.sourcePath) {
160
163
  throw new ToolError(`Cannot find internal URL without a backing file: ${rawPattern}`);
161
164
  }
@@ -150,6 +150,8 @@ export interface ToolSession {
150
150
  requireYieldTool?: boolean;
151
151
  /** Task recursion depth (0 = top-level, 1 = first child, etc.) */
152
152
  taskDepth?: number;
153
+ /** Current role-agent type/name for nested task sessions. */
154
+ currentAgentType?: string;
153
155
  /** Get session file */
154
156
  getSessionFile: () => string | null;
155
157
  /** Get eval kernel owner ID for session-scoped retained-kernel cleanup. */
@@ -625,6 +625,7 @@ export function resolveReadPath(filePath: string, cwd: string): string {
625
625
  export interface ToolScopeOptions {
626
626
  rawPaths: string[];
627
627
  cwd: string;
628
+ getArtifactsDir?: () => string | null;
628
629
  /** Verb used in the "Cannot {action} internal URL without a backing file: …" message. */
629
630
  internalUrlAction: string;
630
631
  /** Collect absolute paths flagged immutable by their internal-URL handler. */
@@ -655,7 +656,7 @@ export interface ToolScopeResolution {
655
656
  * 5. stat the resolved base path so callers can branch on directory vs file scope.
656
657
  */
657
658
  export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<ToolScopeResolution> {
658
- const { rawPaths: inputs, cwd, internalUrlAction } = opts;
659
+ const { rawPaths: inputs, cwd, internalUrlAction, getArtifactsDir } = opts;
659
660
  const rawPaths = inputs.map(normalizePathLikeInput);
660
661
  if (rawPaths.some(rawPath => rawPath.length === 0)) {
661
662
  throw new ToolError("`paths` must contain non-empty paths or globs");
@@ -671,7 +672,7 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
671
672
  if (hasGlobPathChars(rawPath)) {
672
673
  throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
673
674
  }
674
- const resource = await internalRouter.resolve(rawPath);
675
+ const resource = await internalRouter.resolve(rawPath, { cwd, getArtifactsDir });
675
676
  if (!resource.sourcePath) {
676
677
  throw new ToolError(`Cannot ${internalUrlAction} internal URL without a backing file: ${rawPath}`);
677
678
  }
package/src/tools/read.ts CHANGED
@@ -2045,6 +2045,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2045
2045
  // Resolve the internal URL
2046
2046
  const resource = await internalRouter.resolve(url, {
2047
2047
  cwd: this.session.cwd,
2048
+ getArtifactsDir: this.session.getArtifactsDir,
2048
2049
  settings: this.session.settings,
2049
2050
  signal,
2050
2051
  });
@@ -280,6 +280,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
280
280
  const scope = await resolveToolSearchScope({
281
281
  rawPaths: resolvedPaths,
282
282
  cwd: this.session.cwd,
283
+ getArtifactsDir: this.session.getArtifactsDir,
283
284
  internalUrlAction: "search",
284
285
  trackImmutableSources: true,
285
286
  surfaceExactFilePaths: true,
@@ -138,7 +138,12 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
138
138
  { triggerTurn: false },
139
139
  );
140
140
 
141
- const summary = args ? `Handed off to /skill:${skill.name} ${args}.` : `Handed off to /skill:${skill.name}.`;
141
+ const summary = JSON.stringify({
142
+ callee: skill.name,
143
+ path: skill.filePath,
144
+ args: args || undefined,
145
+ lineCount: built.details.lineCount,
146
+ });
142
147
  return {
143
148
  content: [{ type: "text", text: summary }],
144
149
  details: {