@gajae-code/coding-agent 0.4.3 → 0.4.5

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 (92) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/types/async/job-manager.d.ts +19 -1
  3. package/dist/types/cli/fast-help.d.ts +1 -0
  4. package/dist/types/cli/setup-cli.d.ts +16 -1
  5. package/dist/types/commands/coordinator.d.ts +19 -0
  6. package/dist/types/commands/harness.d.ts +3 -0
  7. package/dist/types/commands/mcp-serve.d.ts +24 -0
  8. package/dist/types/commands/setup.d.ts +47 -0
  9. package/dist/types/config/model-registry.d.ts +3 -0
  10. package/dist/types/config/models-config-schema.d.ts +5 -0
  11. package/dist/types/coordinator/contract.d.ts +4 -0
  12. package/dist/types/coordinator-mcp/policy.d.ts +24 -0
  13. package/dist/types/coordinator-mcp/safety.d.ts +26 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +58 -0
  15. package/dist/types/extensibility/extensions/types.d.ts +13 -0
  16. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  17. package/dist/types/harness-control-plane/finalize.d.ts +5 -0
  18. package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
  19. package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
  20. package/dist/types/harness-control-plane/receipts.d.ts +46 -0
  21. package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
  22. package/dist/types/harness-control-plane/types.d.ts +9 -1
  23. package/dist/types/main.d.ts +2 -2
  24. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  25. package/dist/types/modes/utils/abort-message.d.ts +4 -0
  26. package/dist/types/session/session-manager.d.ts +8 -0
  27. package/dist/types/setup/hermes-setup.d.ts +78 -0
  28. package/dist/types/task/fork-context-advisory.d.ts +13 -0
  29. package/dist/types/task/receipt.d.ts +1 -0
  30. package/dist/types/task/render.d.ts +7 -1
  31. package/dist/types/task/roi-reconciliation.d.ts +27 -0
  32. package/dist/types/task/types.d.ts +10 -0
  33. package/dist/types/tools/subagent-render.d.ts +25 -0
  34. package/dist/types/tools/subagent.d.ts +5 -1
  35. package/package.json +8 -7
  36. package/scripts/build-binary.ts +4 -0
  37. package/src/async/job-manager.ts +43 -1
  38. package/src/cli/fast-help.ts +80 -0
  39. package/src/cli/setup-cli.ts +95 -2
  40. package/src/cli.ts +109 -16
  41. package/src/commands/coordinator.ts +113 -0
  42. package/src/commands/harness.ts +92 -9
  43. package/src/commands/mcp-serve.ts +63 -0
  44. package/src/commands/setup.ts +34 -1
  45. package/src/config/models-config-schema.ts +1 -0
  46. package/src/coordinator/contract.ts +21 -0
  47. package/src/coordinator-mcp/policy.ts +160 -0
  48. package/src/coordinator-mcp/safety.ts +80 -0
  49. package/src/coordinator-mcp/server.ts +1519 -0
  50. package/src/cursor.ts +30 -2
  51. package/src/extensibility/extensions/types.ts +13 -0
  52. package/src/gjc-runtime/launch-worktree.ts +12 -1
  53. package/src/gjc-runtime/session-state-sidecar.ts +117 -0
  54. package/src/harness-control-plane/finalize.ts +39 -5
  55. package/src/harness-control-plane/owner.ts +9 -1
  56. package/src/harness-control-plane/phase-rollup.ts +96 -0
  57. package/src/harness-control-plane/receipt-ingest.ts +127 -0
  58. package/src/harness-control-plane/receipts.ts +229 -1
  59. package/src/harness-control-plane/rpc-adapter.ts +8 -0
  60. package/src/harness-control-plane/types.ts +29 -1
  61. package/src/internal-urls/docs-index.generated.ts +6 -4
  62. package/src/main.ts +7 -3
  63. package/src/modes/components/hook-selector.ts +109 -5
  64. package/src/modes/components/status-line.ts +6 -6
  65. package/src/modes/controllers/event-controller.ts +5 -4
  66. package/src/modes/controllers/extension-ui-controller.ts +16 -1
  67. package/src/modes/interactive-mode.ts +4 -5
  68. package/src/modes/print-mode.ts +1 -1
  69. package/src/modes/theme/theme.ts +2 -2
  70. package/src/modes/utils/abort-message.ts +41 -0
  71. package/src/modes/utils/context-usage.ts +15 -8
  72. package/src/modes/utils/ui-helpers.ts +5 -6
  73. package/src/prompts/agents/architect.md +6 -0
  74. package/src/prompts/agents/critic.md +6 -0
  75. package/src/prompts/agents/planner.md +8 -1
  76. package/src/sdk.ts +9 -4
  77. package/src/session/agent-session.ts +22 -5
  78. package/src/session/session-manager.ts +20 -0
  79. package/src/setup/hermes/templates/operator-instructions.v1.md +30 -0
  80. package/src/setup/hermes-setup.ts +484 -0
  81. package/src/task/fork-context-advisory.ts +99 -0
  82. package/src/task/index.ts +33 -2
  83. package/src/task/receipt.ts +2 -0
  84. package/src/task/render.ts +14 -0
  85. package/src/task/roi-reconciliation.ts +90 -0
  86. package/src/task/types.ts +7 -0
  87. package/src/tools/ask.ts +30 -10
  88. package/src/tools/index.ts +2 -2
  89. package/src/tools/renderers.ts +2 -0
  90. package/src/tools/subagent-render.ts +169 -0
  91. package/src/tools/subagent.ts +49 -7
  92. package/src/utils/title-generator.ts +16 -2
@@ -0,0 +1,99 @@
1
+ import type { ForkContextMode } from "./types";
2
+
3
+ export interface ForkContextAdvisory {
4
+ recommendedMode: ForkContextMode;
5
+ reasons: string[];
6
+ estimatedClonedTokens: Record<ForkContextMode, number>;
7
+ callerModeRespected: true;
8
+ }
9
+
10
+ /**
11
+ * Per-mode clone budget ceilings (advisory estimates only). These bound how
12
+ * many parent-context tokens each mode may clone into the child; the actual
13
+ * cloned amount can never exceed the parent context itself.
14
+ */
15
+ const CLONE_BUDGET_BY_MODE = {
16
+ none: 0,
17
+ receipt: 2000,
18
+ "last-turn": 4000,
19
+ bounded: 8000,
20
+ full: 15000,
21
+ } as const satisfies Record<ForkContextMode, number>;
22
+
23
+ const RECEIPT_TRIGGERS = [
24
+ { pattern: /as discussed/i, reason: "prior-session-reference:as-discussed" },
25
+ { pattern: /as decided/i, reason: "prior-session-reference:as-decided" },
26
+ { pattern: /earlier in this session/i, reason: "prior-session-reference:earlier-in-this-session" },
27
+ { pattern: /per the plan above/i, reason: "prior-session-reference:per-the-plan-above" },
28
+ { pattern: /the previous review/i, reason: "prior-session-reference:the-previous-review" },
29
+ { pattern: /\.gjc\/plans\//i, reason: "prior-session-reference:gjc-plans-path" },
30
+ { pattern: /\.gjc\/specs\//i, reason: "prior-session-reference:gjc-specs-path" },
31
+ ] as const;
32
+
33
+ const LAST_TURN_TRIGGERS = [
34
+ { pattern: /the last message/i, reason: "last-turn-reference:the-last-message" },
35
+ { pattern: /the previous turn/i, reason: "last-turn-reference:the-previous-turn" },
36
+ { pattern: /see above/i, reason: "last-turn-reference:see-above" },
37
+ ] as const;
38
+
39
+ /**
40
+ * Estimated tokens cloned into the child per mode: the per-mode budget
41
+ * ceiling, capped by the actual parent context (you can never clone more
42
+ * than exists). Negative parent contexts are normalized to 0.
43
+ */
44
+ function estimateClonedTokens(parentContextTokens: number): Record<ForkContextMode, number> {
45
+ const parent = Math.max(0, parentContextTokens);
46
+ return {
47
+ none: Math.min(parent, CLONE_BUDGET_BY_MODE.none),
48
+ receipt: Math.min(parent, CLONE_BUDGET_BY_MODE.receipt),
49
+ "last-turn": Math.min(parent, CLONE_BUDGET_BY_MODE["last-turn"]),
50
+ bounded: Math.min(parent, CLONE_BUDGET_BY_MODE.bounded),
51
+ full: Math.min(parent, CLONE_BUDGET_BY_MODE.full),
52
+ };
53
+ }
54
+
55
+ export function adviseForkContextMode(input: {
56
+ assignment: string;
57
+ context?: string;
58
+ explicitMode?: ForkContextMode;
59
+ parentContextTokens?: number;
60
+ }): ForkContextAdvisory {
61
+ const parentContextTokens = input.parentContextTokens ?? 0;
62
+ const estimatedClonedTokens = estimateClonedTokens(parentContextTokens);
63
+
64
+ if (input.explicitMode !== undefined) {
65
+ return {
66
+ recommendedMode: input.explicitMode,
67
+ reasons: ["explicit-caller-mode"],
68
+ estimatedClonedTokens,
69
+ callerModeRespected: true,
70
+ };
71
+ }
72
+
73
+ const text = `${input.assignment}\n${input.context ?? ""}`;
74
+ const reasons: string[] = [];
75
+ let recommendedMode: ForkContextMode = "none";
76
+
77
+ for (const trigger of LAST_TURN_TRIGGERS) {
78
+ if (trigger.pattern.test(text)) {
79
+ reasons.push(trigger.reason);
80
+ recommendedMode = "last-turn";
81
+ }
82
+ }
83
+
84
+ for (const trigger of RECEIPT_TRIGGERS) {
85
+ if (trigger.pattern.test(text)) {
86
+ reasons.push(trigger.reason);
87
+ if (recommendedMode === "none") {
88
+ recommendedMode = "receipt";
89
+ }
90
+ }
91
+ }
92
+
93
+ return {
94
+ recommendedMode,
95
+ reasons,
96
+ estimatedClonedTokens,
97
+ callerModeRespected: true,
98
+ };
99
+ }
package/src/task/index.ts CHANGED
@@ -47,11 +47,13 @@ import { generateCommitMessage } from "../utils/commit-message-generator";
47
47
  import * as git from "../utils/git";
48
48
  import { discoverAgents, filterVisibleAgents, getAgent } from "./discovery";
49
49
  import { runSubprocess } from "./executor";
50
+ import { adviseForkContextMode } from "./fork-context-advisory";
50
51
  import { getTaskIdValidationError, validateAllocatedTaskId } from "./id";
51
52
  import { AgentOutputManager } from "./output-manager";
52
53
  import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
53
54
  import { assertNoRawTaskFields, buildTaskReceipt, buildTaskRoiSummary } from "./receipt";
54
55
  import { renderResult, renderCall as renderTaskCall } from "./render";
56
+ import { reconcileSpawnRoi } from "./roi-reconciliation";
55
57
  import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
56
58
  import { DEFAULT_SPAWN_THRESHOLD, evaluateReviewerExploreGate, evaluateSpawnGate } from "./spawn-gate";
57
59
  import {
@@ -578,6 +580,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
578
580
  runMode: message ? "message" : "resume",
579
581
  resumeMessage: message,
580
582
  sessionFiles: new Map([[descriptor.task.id, descriptor.sessionFile]]),
583
+ suppressRoiReconciliation: true,
581
584
  },
582
585
  );
583
586
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
@@ -675,6 +678,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
675
678
  frozenForkSeeds,
676
679
  {
677
680
  sessionFiles: new Map([[uniqueId, subtaskSessionFile]]),
681
+ suppressRoiReconciliation: true,
678
682
  },
679
683
  );
680
684
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
@@ -874,6 +878,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
874
878
  runMode?: "initial" | "resume" | "message";
875
879
  resumeMessage?: string;
876
880
  sessionFiles?: ReadonlyMap<string, string | null>;
881
+ /**
882
+ * Set for per-child async runs: the spawnPlan is carried for gate
883
+ * consistency, but batch-level ROI reconciliation must not be computed
884
+ * against a single child's receipts.
885
+ */
886
+ suppressRoiReconciliation?: boolean;
877
887
  },
878
888
  ): Promise<AgentToolResult<TaskToolDetails>> {
879
889
  const startTime = Date.now();
@@ -1293,6 +1303,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1293
1303
  const forkContext = requestsForkContext(task)
1294
1304
  ? { mode: task.inheritContext, clonedTokens: forkContextSeed?.metadata.approximateTokens ?? 0 }
1295
1305
  : undefined;
1306
+ // Advisory-only recommendation (logged on the receipt); never overrides
1307
+ // the caller's explicit inheritContext mode.
1308
+ const advisory = adviseForkContextMode({
1309
+ assignment: task.assignment,
1310
+ context: sharedContext,
1311
+ explicitMode: task.inheritContext,
1312
+ });
1313
+ const forkContextAdvisory = {
1314
+ recommendedMode: advisory.recommendedMode,
1315
+ reasons: advisory.reasons,
1316
+ };
1296
1317
  const taskSessionFile = overrides?.sessionFile ?? executionOverrides?.sessionFiles?.get(task.id) ?? null;
1297
1318
  if (!isIsolated) {
1298
1319
  const result = await runSubprocess({
@@ -1324,6 +1345,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1324
1345
  progressMap.set(index, {
1325
1346
  ...structuredClone(progress),
1326
1347
  });
1348
+ AsyncJobManager.instance()?.recordSubagentProgress(task.id, progress);
1327
1349
  emitProgress();
1328
1350
  },
1329
1351
  authStorage: this.session.authStorage,
@@ -1340,7 +1362,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1340
1362
  parentTelemetry: this.session.getTelemetry?.(),
1341
1363
  forkContextSeed,
1342
1364
  });
1343
- return forkContext ? { ...result, forkContext } : result;
1365
+ return { ...result, ...(forkContext ? { forkContext } : {}), forkContextAdvisory };
1344
1366
  }
1345
1367
 
1346
1368
  const taskStart = Date.now();
@@ -1384,6 +1406,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1384
1406
  progressMap.set(index, {
1385
1407
  ...structuredClone(progress),
1386
1408
  });
1409
+ AsyncJobManager.instance()?.recordSubagentProgress(task.id, progress);
1387
1410
  emitProgress();
1388
1411
  },
1389
1412
  authStorage: this.session.authStorage,
@@ -1400,7 +1423,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1400
1423
  parentTelemetry: this.session.getTelemetry?.(),
1401
1424
  forkContextSeed,
1402
1425
  });
1403
- const resultWithForkContext = forkContext ? { ...result, forkContext } : result;
1426
+ const resultWithForkContext = {
1427
+ ...result,
1428
+ ...(forkContext ? { forkContext } : {}),
1429
+ forkContextAdvisory,
1430
+ };
1404
1431
  if (mergeMode === "branch" && resultWithForkContext.exitCode === 0) {
1405
1432
  try {
1406
1433
  const commitMsg =
@@ -1695,6 +1722,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1695
1722
 
1696
1723
  const receipts = results.map(buildTaskReceipt);
1697
1724
  const roiSummary = buildTaskRoiSummary(receipts);
1725
+ const roiReconciliation = executionOverrides?.suppressRoiReconciliation
1726
+ ? undefined
1727
+ : reconcileSpawnRoi(params.spawnPlan, receipts);
1698
1728
  const summaries = receipts.map(r => {
1699
1729
  const status = r.status === "merge_failed" ? "merge failed" : r.status;
1700
1730
  return {
@@ -1737,6 +1767,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1737
1767
  usage: hasAggregatedUsage ? aggregatedUsage : undefined,
1738
1768
  forkContextClonedTokens: forkContextClonedTokens > 0 ? forkContextClonedTokens : undefined,
1739
1769
  roiSummary,
1770
+ roiReconciliation,
1740
1771
  };
1741
1772
  assertNoRawTaskFields(details, "task.return.details");
1742
1773
  return {
@@ -46,6 +46,7 @@ export interface TaskResultReceipt {
46
46
  };
47
47
  extractedToolCounts?: Record<string, number>;
48
48
  forkContext?: SingleResult["forkContext"];
49
+ forkContextAdvisory?: SingleResult["forkContextAdvisory"];
49
50
  roi?: TaskRoi;
50
51
  }
51
52
 
@@ -234,6 +235,7 @@ export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
234
235
  review: buildReview(raw),
235
236
  extractedToolCounts,
236
237
  forkContext: raw.forkContext,
238
+ forkContextAdvisory: raw.forkContextAdvisory,
237
239
  roi: buildTaskRoi(raw),
238
240
  };
239
241
  }
@@ -690,6 +690,20 @@ function renderAgentProgress(
690
690
  return lines;
691
691
  }
692
692
 
693
+ /**
694
+ * Public wrapper to render a single subagent's live `AgentProgress` for the
695
+ * `subagent` await panel. Reuses the internal task-progress renderer so the
696
+ * await panel stays at parity with the inline task panel.
697
+ */
698
+ export function renderSubagentLiveProgress(
699
+ progress: AgentProgress,
700
+ expanded: boolean,
701
+ theme: Theme,
702
+ spinnerFrame?: number,
703
+ ): string[] {
704
+ return renderAgentProgress(progress, true, expanded, theme, spinnerFrame);
705
+ }
706
+
693
707
  /**
694
708
  * Render review result with combined verdict + findings in tree structure.
695
709
  */
@@ -0,0 +1,90 @@
1
+ import type { TaskResultReceipt } from "./receipt";
2
+ import type { SpawnPlanReceipt } from "./spawn-gate";
3
+
4
+ /**
5
+ * Pure, advisory-only reconciliation between a spawn plan's inline-token promise
6
+ * and receipt-safe child outputs. These signals never change task success/failure
7
+ * semantics or runtime behavior; they only describe budget/ROI observations for
8
+ * model-facing summaries.
9
+ */
10
+ export interface SpawnRoiChildReconciliation {
11
+ id: string;
12
+ inlineTokens: number;
13
+ maxInlineTokens: number;
14
+ overBudget: boolean;
15
+ overageTokens: number;
16
+ lowRoi: boolean;
17
+ }
18
+
19
+ export interface SpawnRoiReconciliation {
20
+ childCount: number;
21
+ promisedMaxInlineTokens: number;
22
+ children: SpawnRoiChildReconciliation[];
23
+ overBudgetChildIds: string[];
24
+ lowRoiChildIds: string[];
25
+ totalInlineTokens: number;
26
+ totalOverageTokens: number;
27
+ advisoryFlags: string[];
28
+ }
29
+
30
+ function estimateTextTokens(text: string | undefined): number {
31
+ if (!text) return 0;
32
+ return Math.ceil(text.length / 4);
33
+ }
34
+
35
+ /**
36
+ * Estimate model-facing inline cost from receipt-safe fields only. The proxy uses
37
+ * one token per four characters, rounded up independently for the preview and
38
+ * review summaries that may be inlined for the parent model.
39
+ */
40
+ function estimateInlineTokens(receipt: TaskResultReceipt): number {
41
+ const review = receipt.review;
42
+ const reviewSummaryChars =
43
+ (review?.overallCorrectness?.length ?? 0) +
44
+ (review?.findings?.reduce((total, finding) => total + finding.summary.length, 0) ?? 0);
45
+ return estimateTextTokens(receipt.preview) + Math.ceil(reviewSummaryChars / 4);
46
+ }
47
+
48
+ export function reconcileSpawnRoi(
49
+ plan: SpawnPlanReceipt | undefined,
50
+ receipts: readonly TaskResultReceipt[],
51
+ ): SpawnRoiReconciliation | undefined {
52
+ if (!plan) return undefined;
53
+
54
+ const children = receipts.map(receipt => {
55
+ const inlineTokens = estimateInlineTokens(receipt);
56
+ const overageTokens = Math.max(0, inlineTokens - plan.maxInlineTokens);
57
+ return {
58
+ id: receipt.id,
59
+ inlineTokens,
60
+ maxInlineTokens: plan.maxInlineTokens,
61
+ overBudget: overageTokens > 0,
62
+ overageTokens,
63
+ lowRoi: Boolean(receipt.roi?.lowRoi),
64
+ };
65
+ });
66
+ const overBudgetChildIds = children
67
+ .filter(child => child.overBudget)
68
+ .map(child => child.id)
69
+ .toSorted();
70
+ const lowRoiChildIds = children
71
+ .filter(child => child.lowRoi)
72
+ .map(child => child.id)
73
+ .toSorted();
74
+ const advisoryFlags = [
75
+ ...(overBudgetChildIds.length > 0 ? ["over-inline-budget"] : []),
76
+ ...(lowRoiChildIds.length > 0 ? ["low-roi-children"] : []),
77
+ ...(children.length > 0 && lowRoiChildIds.length === children.length ? ["all-children-low-roi"] : []),
78
+ ];
79
+
80
+ return {
81
+ childCount: children.length,
82
+ promisedMaxInlineTokens: plan.maxInlineTokens,
83
+ children,
84
+ overBudgetChildIds,
85
+ lowRoiChildIds,
86
+ totalInlineTokens: children.reduce((total, child) => total + child.inlineTokens, 0),
87
+ totalOverageTokens: children.reduce((total, child) => total + child.overageTokens, 0),
88
+ advisoryFlags,
89
+ };
90
+ }
package/src/task/types.ts CHANGED
@@ -4,6 +4,7 @@ import { $env } from "@gajae-code/utils";
4
4
  import * as z from "zod/v4";
5
5
  import { isValidTaskId, TASK_ID_DESCRIPTION } from "./id";
6
6
  import type { TaskResultReceipt } from "./receipt";
7
+ import type { SpawnRoiReconciliation } from "./roi-reconciliation";
7
8
  import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
8
9
  import type { SpawnPlanReceipt } from "./spawn-gate";
9
10
  import type { NestedRepoPatch } from "./worktree";
@@ -337,6 +338,11 @@ export interface SingleResult {
337
338
  outputMeta?: { lineCount: number; charCount: number; byteSize?: number; sha256?: string };
338
339
  /** Fork-context seed accounting for this subagent, when inherited parent context was cloned. */
339
340
  forkContext?: { mode: ForkContextMode; clonedTokens: number };
341
+ /**
342
+ * Advisory fork-context mode recommendation for this task (logged only;
343
+ * never changes the actual mode selection).
344
+ */
345
+ forkContextAdvisory?: { recommendedMode: ForkContextMode; reasons: string[] };
340
346
  }
341
347
 
342
348
  /** Tool details for TUI rendering */
@@ -356,6 +362,7 @@ export interface TaskToolDetails {
356
362
  /** Advisory ids for terminal children that spent tokens without detectable output/review/changes. */
357
363
  lowRoiChildIds: string[];
358
364
  };
365
+ roiReconciliation?: SpawnRoiReconciliation;
359
366
  progress?: AgentProgress[];
360
367
  async?: {
361
368
  state: "running" | "paused" | "queued" | "completed" | "failed";
package/src/tools/ask.ts CHANGED
@@ -175,6 +175,7 @@ interface UIContext {
175
175
  onLeft?: () => void;
176
176
  onRight?: () => void;
177
177
  helpText?: string;
178
+ customInput?: { optionLabel: string; onSubmit: (text: string) => void };
178
179
  },
179
180
  ): Promise<string | undefined>;
180
181
  editor(
@@ -194,6 +195,7 @@ async function askSingleQuestion(
194
195
  ): Promise<SelectionResult> {
195
196
  const { recommended, timeout, signal, initialSelection, navigation, scrollTitleRows } = options;
196
197
  const doneLabel = getDoneOptionLabel();
198
+ const otherOptionLabel = options.otherOptionLabel ?? OTHER_OPTION;
197
199
  let selectedOptions = [...(initialSelection?.selectedOptions ?? [])];
198
200
  let customInput = initialSelection?.customInput;
199
201
  let timedOut = false;
@@ -202,11 +204,20 @@ async function askSingleQuestion(
202
204
  prompt: string,
203
205
  optionsToShow: string[],
204
206
  initialIndex?: number,
205
- ): Promise<{ choice: string | undefined; timedOut: boolean; navigation?: "back" | "forward" }> => {
207
+ ): Promise<{
208
+ choice: string | undefined;
209
+ timedOut: boolean;
210
+ navigation?: "back" | "forward";
211
+ inlineInput?: string;
212
+ }> => {
206
213
  let timeoutTriggered = false;
207
214
  const onTimeout = () => {
208
215
  timeoutTriggered = true;
209
216
  };
217
+ // Inline custom input: the TUI selector keeps the question and option
218
+ // list on screen and collects the "Other" text below the list, instead
219
+ // of swapping to a separate editor screen that hides the question.
220
+ let inlineInput: string | undefined;
210
221
  let navigationAction: "back" | "forward" | undefined;
211
222
  const baseHelpText = navigation
212
223
  ? "up/down navigate enter select ←/→ question esc cancel"
@@ -222,6 +233,12 @@ async function askSingleQuestion(
222
233
  scrollTitleRows,
223
234
  onTimeout,
224
235
  helpText,
236
+ customInput: {
237
+ optionLabel: otherOptionLabel,
238
+ onSubmit: (text: string) => {
239
+ inlineInput = text;
240
+ },
241
+ },
225
242
  onLeft: navigation?.allowBack
226
243
  ? () => {
227
244
  navigationAction = "back";
@@ -240,16 +257,17 @@ async function askSingleQuestion(
240
257
  if (!timeoutTriggered && choice === undefined && typeof timeout === "number") {
241
258
  timeoutTriggered = Date.now() - startMs >= timeout;
242
259
  }
243
- return { choice, timedOut: timeoutTriggered, navigation: navigationAction };
260
+ return { choice, timedOut: timeoutTriggered, navigation: navigationAction, inlineInput };
244
261
  };
245
262
 
263
+ // Fallback for UI contexts that don't support inline custom input (they
264
+ // resolve the "Other" label without invoking customInput.onSubmit).
246
265
  const promptForCustomInput = async (): Promise<{ input: string | undefined }> => {
247
266
  const dialogOptions = signal ? { signal } : undefined;
248
267
  const showCustomInput = () => ui.editor("Enter your response:", undefined, dialogOptions, { promptStyle: true });
249
268
  const input = signal ? await untilAborted(signal, showCustomInput) : await showCustomInput();
250
269
  return { input };
251
270
  };
252
- const otherOptionLabel = options.otherOptionLabel ?? OTHER_OPTION;
253
271
 
254
272
  const promptWithProgress = navigation?.progressText ? `${question} (${navigation.progressText})` : question;
255
273
  if (multi) {
@@ -278,6 +296,7 @@ async function askSingleQuestion(
278
296
  choice,
279
297
  timedOut: selectTimedOut,
280
298
  navigation: arrowNavigation,
299
+ inlineInput,
281
300
  } = await selectOption(`${prefix}${promptWithProgress}`, opts, cursorIndex);
282
301
 
283
302
  if (arrowNavigation) {
@@ -297,11 +316,11 @@ async function askSingleQuestion(
297
316
  timedOut = true;
298
317
  break;
299
318
  }
300
- const customResult = await promptForCustomInput();
301
- if (customResult.input === undefined) {
319
+ const input = inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input;
320
+ if (input === undefined) {
302
321
  break;
303
322
  }
304
- customInput = customResult.input;
323
+ customInput = input;
305
324
  break;
306
325
  }
307
326
 
@@ -353,6 +372,7 @@ async function askSingleQuestion(
353
372
  choice,
354
373
  timedOut: selectTimedOut,
355
374
  navigation: arrowNavigation,
375
+ inlineInput,
356
376
  } = await selectOption(promptWithProgress, optionsWithNavigation, initialIndex);
357
377
  timedOut = selectTimedOut;
358
378
 
@@ -365,12 +385,12 @@ async function askSingleQuestion(
365
385
  }
366
386
  } else if (choice === otherOptionLabel) {
367
387
  if (!selectTimedOut) {
368
- const customResult = await promptForCustomInput();
369
- if (customResult.input !== undefined) {
370
- customInput = customResult.input;
388
+ const input = inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input;
389
+ if (input !== undefined) {
390
+ customInput = input;
371
391
  selectedOptions = [];
372
392
  }
373
- // If editor was dismissed (undefined), keep prior selectedOptions/customInput intact
393
+ // If input was dismissed (undefined), keep prior selectedOptions/customInput intact
374
394
  }
375
395
  } else {
376
396
  selectedOptions = [stripRecommendedSuffix(choice)];
@@ -403,7 +403,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
403
403
  toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined;
404
404
  const goalEnabled = session.settings.get("goal.enabled");
405
405
  const goalStateToolNames = [...GOAL_MODE_TOOL_NAMES];
406
- if (goalEnabled && session.getGoalRuntime !== undefined && requestedTools && !requestedTools.includes("goal")) {
406
+ if (goalEnabled && requestedTools && !requestedTools.includes("goal")) {
407
407
  requestedTools = [...requestedTools, "goal"];
408
408
  }
409
409
  if (goalEnabled && requestedTools) {
@@ -482,7 +482,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
482
482
  allToolsByRequestName.set(name.toLowerCase(), [name, factory]);
483
483
  }
484
484
  const isToolAllowed = (name: string) => {
485
- if (name === "goal") return goalEnabled && session.getGoalRuntime !== undefined;
485
+ if (name === "goal") return goalEnabled;
486
486
  if (goalStateToolNames.includes(name as (typeof GOAL_MODE_TOOL_NAMES)[number])) return goalEnabled;
487
487
  if (name === "lsp") return enableLsp && session.settings.get("lsp.enabled");
488
488
  if (name === "bash") return true;
@@ -29,6 +29,7 @@ import { resolveToolRenderer } from "./resolve";
29
29
  import { searchToolRenderer } from "./search";
30
30
  import { searchToolBm25Renderer } from "./search-tool-bm25";
31
31
  import { sshToolRenderer } from "./ssh";
32
+ import { subagentToolRenderer } from "./subagent-render";
32
33
  import { todoWriteToolRenderer } from "./todo-write";
33
34
  import { writeToolRenderer } from "./write";
34
35
 
@@ -66,6 +67,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
66
67
  resolve: resolveToolRenderer as ToolRenderer,
67
68
  search_tool_bm25: searchToolBm25Renderer as ToolRenderer,
68
69
  ssh: sshToolRenderer as ToolRenderer,
70
+ subagent: subagentToolRenderer as ToolRenderer,
69
71
  task: taskToolRenderer as ToolRenderer,
70
72
  todo_write: todoWriteToolRenderer as ToolRenderer,
71
73
  github: githubToolRenderer as ToolRenderer,