@gajae-code/coding-agent 0.8.0 → 0.8.2

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 (116) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/types/cli/completion-cli.d.ts +41 -0
  3. package/dist/types/cli/config-cli.d.ts +1 -0
  4. package/dist/types/cli/notify-cli.d.ts +7 -0
  5. package/dist/types/cli.d.ts +124 -1
  6. package/dist/types/commands/completion.d.ts +29 -0
  7. package/dist/types/commands/config.d.ts +3 -0
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +7 -6
  10. package/dist/types/config/keybindings.d.ts +2 -2
  11. package/dist/types/config/model-profile-activation.d.ts +6 -0
  12. package/dist/types/config/settings.d.ts +8 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  14. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  15. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
  16. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  17. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
  19. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  21. package/dist/types/main.d.ts +4 -1
  22. package/dist/types/modes/components/model-selector.d.ts +4 -0
  23. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  24. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  25. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  26. package/dist/types/modes/print-mode.d.ts +13 -1
  27. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  28. package/dist/types/notifications/config.d.ts +8 -1
  29. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  30. package/dist/types/notifications/index.d.ts +6 -3
  31. package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
  32. package/dist/types/notifications/telegram-daemon.d.ts +18 -3
  33. package/dist/types/notifications/telegram-reference.d.ts +0 -5
  34. package/dist/types/runtime-api-key.d.ts +7 -0
  35. package/dist/types/session/agent-session.d.ts +10 -0
  36. package/dist/types/session/agent-storage.d.ts +1 -0
  37. package/dist/types/task/executor.d.ts +2 -0
  38. package/dist/types/tools/computer.d.ts +2 -2
  39. package/dist/types/tools/skill.d.ts +1 -1
  40. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  41. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  42. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  43. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  44. package/package.json +7 -7
  45. package/src/cli/completion-cli.ts +234 -0
  46. package/src/cli/config-cli.ts +37 -9
  47. package/src/cli/fast-help.ts +1 -0
  48. package/src/cli/notify-cli.ts +91 -7
  49. package/src/cli.ts +3 -1
  50. package/src/commands/completion.ts +84 -0
  51. package/src/commands/config.ts +2 -0
  52. package/src/commands/deep-interview.ts +2 -1
  53. package/src/config/file-lock.ts +73 -34
  54. package/src/config/keybindings.ts +2 -2
  55. package/src/config/model-profile-activation.ts +65 -0
  56. package/src/config/model-registry.ts +14 -2
  57. package/src/config/settings.ts +22 -0
  58. package/src/coordinator-mcp/server.ts +67 -12
  59. package/src/defaults/gjc/skills/deep-interview/SKILL.md +38 -5
  60. package/src/defaults/gjc/skills/ralplan/SKILL.md +15 -11
  61. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  62. package/src/extensibility/extensions/runner.ts +2 -0
  63. package/src/extensibility/extensions/types.ts +9 -0
  64. package/src/extensibility/skills.ts +30 -2
  65. package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
  66. package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
  67. package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
  68. package/src/gjc-runtime/deep-interview-state.ts +13 -0
  69. package/src/gjc-runtime/session-state-sidecar.ts +59 -11
  70. package/src/gjc-runtime/state-runtime.ts +12 -2
  71. package/src/gjc-runtime/team-runtime.ts +23 -0
  72. package/src/gjc-runtime/ultragoal-runtime.ts +73 -19
  73. package/src/hooks/native-skill-hook.ts +29 -0
  74. package/src/internal-urls/docs-index.generated.ts +2 -2
  75. package/src/main.ts +52 -41
  76. package/src/modes/acp/acp-agent.ts +7 -6
  77. package/src/modes/components/model-selector.ts +54 -11
  78. package/src/modes/components/queued-message-selector.ts +92 -0
  79. package/src/modes/components/settings-selector.ts +4 -0
  80. package/src/modes/components/welcome.ts +160 -44
  81. package/src/modes/controllers/command-controller.ts +4 -4
  82. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  83. package/src/modes/controllers/input-controller.ts +186 -25
  84. package/src/modes/controllers/selector-controller.ts +122 -27
  85. package/src/modes/interactive-mode.ts +43 -16
  86. package/src/modes/print-mode.ts +43 -4
  87. package/src/modes/prompt-action-autocomplete.ts +24 -13
  88. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  89. package/src/modes/utils/ui-helpers.ts +2 -2
  90. package/src/notifications/config.ts +9 -1
  91. package/src/notifications/daemon-paths.ts +2 -0
  92. package/src/notifications/index.ts +39 -9
  93. package/src/notifications/lifecycle-commands.ts +37 -7
  94. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  95. package/src/notifications/telegram-daemon-control.ts +3 -1
  96. package/src/notifications/telegram-daemon.ts +231 -43
  97. package/src/notifications/telegram-reference.ts +0 -16
  98. package/src/prompts/system/system-prompt.md +3 -0
  99. package/src/prompts/tools/computer.md +2 -2
  100. package/src/runtime-api-key.ts +16 -0
  101. package/src/sdk.ts +20 -6
  102. package/src/session/agent-session.ts +225 -31
  103. package/src/session/agent-storage.ts +4 -0
  104. package/src/session/session-manager.ts +44 -2
  105. package/src/slash-commands/builtin-registry.ts +153 -71
  106. package/src/task/executor.ts +7 -4
  107. package/src/task/index.ts +9 -3
  108. package/src/task/output-manager.ts +16 -6
  109. package/src/task/receipt.ts +26 -7
  110. package/src/tools/computer.ts +246 -43
  111. package/src/tools/skill.ts +1 -1
  112. package/src/tools/telegram-send.ts +6 -0
  113. package/src/workflow/workflow-intent-diff.ts +29 -6
  114. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  115. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  116. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -6,12 +6,13 @@ import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
6
6
  import { Spacer, Text } from "@gajae-code/tui";
7
7
  import { setProjectDir } from "@gajae-code/utils";
8
8
  import { jobElapsedMs } from "../async";
9
- import { materializeActiveModelProfileAssignment } from "../config/model-profile-activation";
9
+ import { materializeActiveModelProfileAssignments } from "../config/model-profile-activation";
10
10
  import {
11
11
  GJC_MODEL_ASSIGNMENT_TARGET_IDS,
12
12
  GJC_MODEL_ASSIGNMENT_TARGETS,
13
13
  type GjcModelAssignmentTargetId,
14
14
  } from "../config/model-registry";
15
+
15
16
  import {
16
17
  extractExplicitThinkingSelector,
17
18
  formatModelSelectorValue,
@@ -51,6 +52,13 @@ export type { BuiltinSlashCommand, SubcommandDef } from "./types";
51
52
  /** TUI-specific runtime accepted by `executeBuiltinSlashCommand`. */
52
53
  export type BuiltinSlashCommandRuntime = TuiSlashCommandRuntime;
53
54
 
55
+ type GjcModelBatchAssignmentTargetId = "all-role-agents" | "all-targets";
56
+ type ParsedModelCommandArgs =
57
+ | { kind: "summary" }
58
+ | { kind: "assign"; targetId: GjcModelAssignmentTargetId | GjcModelBatchAssignmentTargetId; selector: string };
59
+
60
+ const GJC_MODEL_ROLE_AGENT_TARGET_IDS: GjcModelAssignmentTargetId[] = ["executor", "architect", "planner", "critic"];
61
+
54
62
  function fastStatusRoleTargets(): Array<{ id: GjcModelAssignmentTargetId; label: string; isSubagentRole: boolean }> {
55
63
  return GJC_MODEL_ASSIGNMENT_TARGET_IDS.map(id => ({
56
64
  id,
@@ -59,6 +67,32 @@ function fastStatusRoleTargets(): Array<{ id: GjcModelAssignmentTargetId; label:
59
67
  }));
60
68
  }
61
69
 
70
+ function toSlashCommandRuntime(runtime: TuiSlashCommandRuntime): SlashCommandRuntime {
71
+ const ctx = runtime.ctx;
72
+ return {
73
+ session: ctx.session,
74
+ sessionManager: ctx.sessionManager,
75
+ settings: ctx.settings,
76
+ cwd: ctx.sessionManager.getCwd(),
77
+ output: (text: string) => {
78
+ ctx.showStatus(text);
79
+ },
80
+ refreshCommands: () => ctx.refreshSlashCommandState(),
81
+ reloadPlugins: async () => {
82
+ const projectPath = await resolveActiveProjectRegistryPath(ctx.sessionManager.getCwd());
83
+ clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
84
+ await ctx.refreshSlashCommandState();
85
+ await ctx.session.refreshSshTool({ activateIfAvailable: true });
86
+ },
87
+ notifyTitleChanged: () => {
88
+ ctx.statusLine.invalidate();
89
+ ctx.updateEditorBorderColor();
90
+ ctx.ui.requestRender();
91
+ },
92
+ notifyConfigChanged: () => ctx.notifyConfigChanged?.(),
93
+ };
94
+ }
95
+
62
96
  function parseProviderSetupSlashArgs(args: string): {
63
97
  preset?: string;
64
98
  compat?: string;
@@ -146,22 +180,37 @@ function formatModelAssignmentSummary(runtime: SlashCommandRuntime): string {
146
180
  return lines.join("\n");
147
181
  }
148
182
 
149
- function parseModelCommandArgs(args: string): { targetId: GjcModelAssignmentTargetId; selector: string } {
183
+ function parseModelCommandArgs(args: string): ParsedModelCommandArgs {
150
184
  const tokens = args.trim().split(/\s+/).filter(Boolean);
151
185
  const first = tokens[0]?.toLowerCase();
152
- const explicitTarget = GJC_MODEL_ASSIGNMENT_TARGET_IDS.includes(first as GjcModelAssignmentTargetId)
153
- ? (first as GjcModelAssignmentTargetId)
154
- : undefined;
186
+ if (first === "roles" || first === "assignments") return { kind: "summary" };
187
+
188
+ const parseTarget = (
189
+ token: string | undefined,
190
+ ): GjcModelAssignmentTargetId | GjcModelBatchAssignmentTargetId | undefined => {
191
+ const normalized = token?.toLowerCase();
192
+ if (GJC_MODEL_ASSIGNMENT_TARGET_IDS.includes(normalized as GjcModelAssignmentTargetId)) {
193
+ return normalized as GjcModelAssignmentTargetId;
194
+ }
195
+ if (normalized === "all-role-agents" || normalized === "all-targets") return normalized;
196
+ return undefined;
197
+ };
198
+
199
+ if (first === "assign") {
200
+ const targetId = parseTarget(tokens[1]);
201
+ if (targetId) return { kind: "assign", targetId, selector: tokens.slice(2).join(" ") };
202
+ return { kind: "assign", targetId: "default", selector: tokens.slice(1).join(" ") };
203
+ }
204
+
205
+ const explicitTarget = parseTarget(first);
155
206
  if (explicitTarget) {
156
- return { targetId: explicitTarget, selector: tokens.slice(1).join(" ") };
207
+ return { kind: "assign", targetId: explicitTarget, selector: tokens.slice(1).join(" ") };
157
208
  }
158
209
  if (first === "set") {
159
- const second = tokens[1]?.toLowerCase();
160
- if (GJC_MODEL_ASSIGNMENT_TARGET_IDS.includes(second as GjcModelAssignmentTargetId)) {
161
- return { targetId: second as GjcModelAssignmentTargetId, selector: tokens.slice(2).join(" ") };
162
- }
210
+ const targetId = parseTarget(tokens[1]);
211
+ if (targetId) return { kind: "assign", targetId, selector: tokens.slice(2).join(" ") };
163
212
  }
164
- return { targetId: "default", selector: args.trim() };
213
+ return { kind: "assign", targetId: "default", selector: args.trim() };
165
214
  }
166
215
 
167
216
  function splitExplicitThinkingSelector(selector: string): { baseSelector: string; thinkingLevel?: ThinkingLevel } {
@@ -296,11 +345,33 @@ async function resolveModelCommandSelection(
296
345
  };
297
346
  }
298
347
 
348
+ function getModelAssignmentTargetIds(
349
+ targetId: GjcModelAssignmentTargetId | GjcModelBatchAssignmentTargetId,
350
+ ): GjcModelAssignmentTargetId[] {
351
+ if (targetId === "all-role-agents") return [...GJC_MODEL_ROLE_AGENT_TARGET_IDS];
352
+ if (targetId === "all-targets") return [...GJC_MODEL_ASSIGNMENT_TARGET_IDS];
353
+ return [targetId];
354
+ }
355
+
356
+ function formatModelAssignmentSuccess(
357
+ targetId: GjcModelAssignmentTargetId | GjcModelBatchAssignmentTargetId,
358
+ selector: string,
359
+ ): string {
360
+ if (targetId === "all-role-agents") {
361
+ return `Role-agent models set to ${selector} for EXECUTOR, ARCHITECT, PLANNER, CRITIC.`;
362
+ }
363
+ if (targetId === "all-targets") {
364
+ return `All model targets set to ${selector} for DEFAULT, EXECUTOR, ARCHITECT, PLANNER, CRITIC.`;
365
+ }
366
+ if (targetId === "default") return `Default model set to ${selector}.`;
367
+ return `${targetId} agent model set to ${selector}.`;
368
+ }
369
+
299
370
  function modelSelectionUsage(runtime: SlashCommandRuntime, currentModelLine?: string): string {
300
371
  return [
301
372
  currentModelLine,
302
373
  formatModelAssignmentSummary(runtime),
303
- "ACP/text mode: use /model <model> for DEFAULT, or /model <target> <model> for EXECUTOR, ARCHITECT, PLANNER, or CRITIC.",
374
+ "Use /model <model> for DEFAULT, or /model <target> <model[:effort]> for EXECUTOR, ARCHITECT, PLANNER, or CRITIC.",
304
375
  formatModelOnboardingGuidance(),
305
376
  ]
306
377
  .filter((line): line is string => Boolean(line))
@@ -372,9 +443,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
372
443
  acpDescription: "Show current model selection",
373
444
  inlineHint: "[target] <model>",
374
445
  acpInputHint: "[target] <model>",
446
+ allowArgs: true,
375
447
  handle: async (command, runtime) => {
376
448
  if (command.args) {
377
449
  const parsedArgs = parseModelCommandArgs(command.args);
450
+ if (parsedArgs.kind === "summary") {
451
+ await runtime.output(formatModelAssignmentSummary(runtime));
452
+ return commandConsumed();
453
+ }
454
+
455
+ const targetIds = getModelAssignmentTargetIds(parsedArgs.targetId);
378
456
  const modelId = parsedArgs.selector;
379
457
  if (!modelId) {
380
458
  return usage(
@@ -388,24 +466,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
388
466
  }
389
467
  const { selection } = resolution;
390
468
  try {
391
- const persistedSelector = formatModelSelectorValue(selection.selector, selection.thinkingLevel);
392
- if (parsedArgs.targetId === "default") {
393
- await runtime.session.setModel(selection.model, "default", {
394
- selector: selection.selector,
395
- thinkingLevel: selection.thinkingLevel,
396
- });
397
- materializeActiveModelProfileAssignment({
398
- session: runtime.session,
399
- settings: runtime.settings,
400
- role: parsedArgs.targetId,
401
- selector: persistedSelector,
402
- });
403
- if (selection.thinkingLevel) {
404
- runtime.session.setThinkingLevel(selection.thinkingLevel);
405
- }
406
- await runtime.output(`Default model set to ${persistedSelector}.`);
407
- await runtime.notifyTitleChanged?.();
408
- } else {
469
+ const includesDefault = targetIds.includes("default");
470
+ const includesRoleAgent = targetIds.some(role => role !== "default");
471
+ if (includesRoleAgent) {
409
472
  const apiKey = await runtime.session.modelRegistry.getApiKey(
410
473
  selection.model,
411
474
  runtime.session.sessionId,
@@ -413,31 +476,60 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
413
476
  if (!apiKey) {
414
477
  throw new Error(`No API key for ${selection.model.provider}/${selection.model.id}`);
415
478
  }
416
- const overrides = runtime.settings.get("task.agentModelOverrides");
479
+ }
480
+
481
+ const overrides = runtime.settings.get("task.agentModelOverrides");
482
+ const assignments = new Map<GjcModelAssignmentTargetId, string>();
483
+ const existingDefaultThinkingLevel =
484
+ selection.thinkingLevel !== undefined
485
+ ? selection.thinkingLevel
486
+ : runtime.session.getActiveModelProfile?.()
487
+ ? undefined
488
+ : extractExplicitThinkingSelector(runtime.settings.getModelRole("default"), runtime.settings);
489
+ const persistedSelector = formatModelSelectorValue(selection.selector, existingDefaultThinkingLevel);
490
+ for (const targetId of targetIds) {
491
+ if (targetId === "default") {
492
+ assignments.set(targetId, persistedSelector);
493
+ continue;
494
+ }
417
495
  const thinkingLevel =
418
- selection.thinkingLevel ??
419
- extractExplicitThinkingSelector(overrides[parsedArgs.targetId], runtime.settings);
420
- const roleSelector = formatModelSelectorValue(selection.selector, thinkingLevel);
421
- const materializedProfile = materializeActiveModelProfileAssignment({
422
- session: runtime.session,
423
- settings: runtime.settings,
424
- role: parsedArgs.targetId,
425
- selector: roleSelector,
496
+ selection.thinkingLevel ?? extractExplicitThinkingSelector(overrides[targetId], runtime.settings);
497
+ assignments.set(targetId, formatModelSelectorValue(selection.selector, thinkingLevel));
498
+ }
499
+
500
+ if (includesDefault) {
501
+ await runtime.session.setModel(selection.model, "default", {
502
+ selector: selection.selector,
503
+ thinkingLevel: existingDefaultThinkingLevel,
426
504
  });
427
- if (!materializedProfile) {
428
- const target = GJC_MODEL_ASSIGNMENT_TARGETS[parsedArgs.targetId];
505
+ if (existingDefaultThinkingLevel) {
506
+ runtime.session.setThinkingLevel(existingDefaultThinkingLevel);
507
+ }
508
+ }
509
+
510
+ const materializedProfile = materializeActiveModelProfileAssignments({
511
+ session: runtime.session,
512
+ settings: runtime.settings,
513
+ assignments,
514
+ });
515
+ if (!materializedProfile) {
516
+ for (const [targetId, selector] of assignments) {
517
+ const target = GJC_MODEL_ASSIGNMENT_TARGETS[targetId];
429
518
  if (target.settingsPath === "modelRoles") {
430
- runtime.settings.setModelRole(parsedArgs.targetId, roleSelector);
519
+ runtime.settings.setModelRole(targetId, selector);
431
520
  } else {
432
- runtime.settings.set("task.agentModelOverrides", {
433
- ...overrides,
434
- [parsedArgs.targetId]: roleSelector,
435
- });
521
+ runtime.settings.setAgentModelOverride(targetId, selector);
436
522
  }
437
523
  }
438
- runtime.settings.getStorage()?.recordModelUsage(`${selection.model.provider}/${selection.model.id}`);
439
- await runtime.output(`${parsedArgs.targetId} agent model set to ${roleSelector}.`);
440
524
  }
525
+ runtime.settings.getStorage()?.recordModelUsage(`${selection.model.provider}/${selection.model.id}`);
526
+ await runtime.output(
527
+ formatModelAssignmentSuccess(
528
+ parsedArgs.targetId,
529
+ assignments.get(targetIds[0] ?? "default") ?? persistedSelector,
530
+ ),
531
+ );
532
+ if (includesDefault) await runtime.notifyTitleChanged?.();
441
533
  await runtime.notifyConfigChanged?.();
442
534
  return commandConsumed();
443
535
  } catch (err) {
@@ -454,7 +546,18 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
454
546
  );
455
547
  return commandConsumed();
456
548
  },
457
- handleTui: (_command, runtime) => {
549
+ handleTui: async (command, runtime) => {
550
+ if (command.args.trim()) {
551
+ const result = await BUILTIN_SLASH_COMMAND_LOOKUP.get(command.name)?.handle?.(
552
+ command,
553
+ toSlashCommandRuntime(runtime),
554
+ );
555
+ runtime.ctx.statusLine.invalidate();
556
+ runtime.ctx.updateEditorBorderColor();
557
+ runtime.ctx.editor.setText("");
558
+ runtime.ctx.ui.requestRender();
559
+ return result;
560
+ }
458
561
  runtime.ctx.showModelSelector();
459
562
  runtime.ctx.editor.setText("");
460
563
  },
@@ -1303,29 +1406,8 @@ export async function executeBuiltinSlashCommand(
1303
1406
  return true;
1304
1407
  }
1305
1408
  if (command.handle) {
1306
- // No TUI-specific override → adapt the ACP/text-mode `handle` to the
1307
- // TUI by routing `runtime.output` through `ctx.showStatus`, clearing
1308
- // the editor after the call, and reusing the active session's plugin
1309
- // reload pipeline. Spec authors get a single body usable from either
1310
- // dispatcher without forcing every TUI test to construct the full
1311
- // `SlashCommandRuntime` shape.
1312
1409
  const ctx = runtime.ctx;
1313
- const adapted: SlashCommandRuntime = {
1314
- session: ctx.session,
1315
- sessionManager: ctx.sessionManager,
1316
- settings: ctx.settings,
1317
- cwd: ctx.sessionManager.getCwd(),
1318
- output: (text: string) => {
1319
- ctx.showStatus(text);
1320
- },
1321
- refreshCommands: () => ctx.refreshSlashCommandState(),
1322
- reloadPlugins: async () => {
1323
- const projectPath = await resolveActiveProjectRegistryPath(ctx.sessionManager.getCwd());
1324
- clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
1325
- await ctx.refreshSlashCommandState();
1326
- await ctx.session.refreshSshTool({ activateIfAvailable: true });
1327
- },
1328
- };
1410
+ const adapted = toSlashCommandRuntime(runtime);
1329
1411
  const result = await command.handle(parsed, adapted);
1330
1412
  ctx.editor.setText("");
1331
1413
  if (result && typeof result === "object" && "prompt" in result) return result.prompt;
@@ -139,6 +139,8 @@ export interface ExecutorOptions {
139
139
  artifactsDir?: string;
140
140
  /** Path to parent conversation context file */
141
141
  contextFile?: string;
142
+ /** Whether the parent runtime actually exposes IRC coordination. */
143
+ ircAvailable?: boolean;
142
144
  eventBus?: EventBus;
143
145
  contextFiles?: ContextFileEntry[];
144
146
  skills?: Skill[];
@@ -623,7 +625,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
623
625
  : agent.spawns.join(",");
624
626
 
625
627
  const lspEnabled = enableLsp ?? true;
626
- const ircEnabled = subagentSettings.get("irc.enabled") === true;
628
+ const ircEnabled = options.ircAvailable === true;
627
629
  const contextFileForPrompt = ircEnabled ? undefined : options.contextFile;
628
630
  const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("eval");
629
631
 
@@ -1231,9 +1233,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1231
1233
 
1232
1234
  const { normalized: normalizedOutputSchema } = normalizeSchema(outputSchema);
1233
1235
 
1234
- const forkContextNotice = options.forkContextSeed
1235
- ? `This subagent was started with a forked snapshot of the parent conversation. Included ${options.forkContextSeed.metadata.includedMessages} message(s), skipped ${options.forkContextSeed.metadata.skippedMessages}, approximately ${options.forkContextSeed.metadata.approximateTokens} tokens. The snapshot is not live; use IRC for live coordination when enabled.`
1236
- : "";
1236
+ const forkContextNotice =
1237
+ options.forkContextSeed && options.forkContextSeed.metadata.includedMessages > 0
1238
+ ? `This subagent was started with a forked snapshot of the parent conversation. Included ${options.forkContextSeed.metadata.includedMessages} message(s), skipped ${options.forkContextSeed.metadata.skippedMessages}, approximately ${options.forkContextSeed.metadata.approximateTokens} tokens. The snapshot is not live.${ircEnabled ? " Use IRC for live coordination." : " Rely on the explicit assignment and supplied context for coordination."}`
1239
+ : "";
1237
1240
 
1238
1241
  const agentSubskillBlock = await buildAgentSubskillInjection({
1239
1242
  cwd,
package/src/task/index.ts CHANGED
@@ -178,6 +178,10 @@ export {
178
178
  /**
179
179
  * Render the tool description from a cached agent list and current settings.
180
180
  */
181
+ function hasAvailableIrcTool(session: ToolSession): boolean {
182
+ return session.settings.get("irc.enabled") === true && session.getToolByName?.("irc") !== undefined;
183
+ }
184
+
181
185
  function renderDescription(
182
186
  agents: AgentDefinition[],
183
187
  maxConcurrency: number,
@@ -381,7 +385,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
381
385
  true,
382
386
  disabledAgents,
383
387
  this.#getTaskSimpleMode(),
384
- this.session.settings.get("irc.enabled") === true,
388
+ hasAvailableIrcTool(this.session),
385
389
  this.session.getSessionSpawns() ?? "*",
386
390
  );
387
391
  }
@@ -814,7 +818,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
814
818
  ? ` Failed to schedule ${failedSchedules.length} task${failedSchedules.length === 1 ? "" : "s"}.`
815
819
  : "";
816
820
 
817
- const ircEnabled = this.session.settings.get("irc.enabled") === true;
821
+ const ircEnabled = hasAvailableIrcTool(this.session);
818
822
  const taskIdByItemId = new Map<string, string>();
819
823
  for (let i = 0; i < taskItems.length; i++) {
820
824
  taskIdByItemId.set(taskItems[i].id, uniqueIds[i]);
@@ -1171,7 +1175,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1171
1175
  // Write parent conversation context for subagents. When IRC is available,
1172
1176
  // subagents should ask live peers instead of reading a stale markdown dump.
1173
1177
  await fs.mkdir(effectiveArtifactsDir, { recursive: true });
1174
- const shouldWriteConversationContext = this.session.settings.get("irc.enabled") !== true;
1178
+ const shouldWriteConversationContext = !hasAvailableIrcTool(this.session);
1175
1179
  const compactContext = shouldWriteConversationContext ? this.session.getCompactContext?.() : undefined;
1176
1180
  let contextFilePath: string | undefined;
1177
1181
  if (compactContext) {
@@ -1298,6 +1302,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1298
1302
  persistArtifacts: !!artifactsDir,
1299
1303
  artifactsDir: effectiveArtifactsDir,
1300
1304
  contextFile: contextFilePath,
1305
+ ircAvailable: hasAvailableIrcTool(this.session),
1301
1306
  enableLsp: subagentLspEnabled,
1302
1307
  signal,
1303
1308
  eventBus: this.session.eventBus,
@@ -1360,6 +1365,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1360
1365
  persistArtifacts: !!artifactsDir,
1361
1366
  artifactsDir: effectiveArtifactsDir,
1362
1367
  contextFile: contextFilePath,
1368
+ ircAvailable: hasAvailableIrcTool(this.session),
1363
1369
  enableLsp: subagentLspEnabled,
1364
1370
  signal,
1365
1371
  eventBus: this.session.eventBus,
@@ -24,7 +24,7 @@ function escapeRegExp(value: string): string {
24
24
  */
25
25
  export class AgentOutputManager {
26
26
  #nextId = 0;
27
- #initialized = false;
27
+ #initPromise: Promise<void> | undefined;
28
28
  readonly #getArtifactsDir: () => string | null;
29
29
  readonly #parentPrefix: string | undefined;
30
30
 
@@ -33,14 +33,24 @@ export class AgentOutputManager {
33
33
  this.#parentPrefix = options?.parentPrefix;
34
34
  }
35
35
 
36
+ /**
37
+ * Memoize the in-flight scan so concurrent `allocate*`/`peekNextIndex`
38
+ * calls await the SAME `readdir` before `#nextId` is derived. Assigning the
39
+ * promise synchronously (before any `await`) closes the TOCTOU window where a
40
+ * boolean "initialized" flag, flipped ahead of the awaited scan, let a second
41
+ * caller allocate at `#nextId === 0` while the first was still scanning —
42
+ * producing duplicate indices that overwrite prior outputs on resume.
43
+ */
44
+ #ensureInitialized(): Promise<void> {
45
+ this.#initPromise ??= this.#scanExistingOutputs();
46
+ return this.#initPromise;
47
+ }
48
+
36
49
  /**
37
50
  * Scan existing agent output files to find the next available ID.
38
51
  * This ensures we don't overwrite outputs when resuming a session.
39
52
  */
40
- async #ensureInitialized(): Promise<void> {
41
- if (this.#initialized) return;
42
- this.#initialized = true;
43
-
53
+ async #scanExistingOutputs(): Promise<void> {
44
54
  const dir = this.#getArtifactsDir();
45
55
  if (!dir) return;
46
56
 
@@ -103,6 +113,6 @@ export class AgentOutputManager {
103
113
  */
104
114
  reset(): void {
105
115
  this.#nextId = 0;
106
- this.#initialized = false;
116
+ this.#initPromise = undefined;
107
117
  }
108
118
  }
@@ -77,6 +77,28 @@ function truncateText(value: string | undefined, maxChars: number): string | und
77
77
  return value.length > maxChars ? value.slice(0, maxChars) : value;
78
78
  }
79
79
 
80
+ const SAFE_REVIEW_SEVERITIES = new Set(["blocker", "critical", "error", "high", "medium", "warning", "low", "info"]);
81
+ const SAFE_REVIEW_PRIORITIES = new Set(["P0", "P1", "P2", "P3"]);
82
+
83
+ function normalizeReviewFindingSeverity(severity: unknown, priority: unknown): string | undefined {
84
+ if (typeof severity === "string") {
85
+ const normalizedPriority = severity.toUpperCase();
86
+ if (SAFE_REVIEW_PRIORITIES.has(normalizedPriority)) return normalizedPriority;
87
+ const normalizedSeverity = severity.toLowerCase();
88
+ if (SAFE_REVIEW_SEVERITIES.has(normalizedSeverity)) return normalizedSeverity;
89
+ }
90
+ if (typeof priority === "string") {
91
+ const normalizedPriority = priority.toUpperCase();
92
+ if (SAFE_REVIEW_PRIORITIES.has(normalizedPriority)) return normalizedPriority;
93
+ const normalizedSeverity = priority.toLowerCase();
94
+ if (SAFE_REVIEW_SEVERITIES.has(normalizedSeverity)) return normalizedSeverity;
95
+ }
96
+ if (typeof priority === "number" && Number.isInteger(priority) && priority >= 0 && priority <= 3) {
97
+ return `P${priority}`;
98
+ }
99
+ return undefined;
100
+ }
101
+
80
102
  function buildSafeSynopsis(raw: SingleResult, outputRef: TaskResultReceipt["outputRef"]): string {
81
103
  const status = getStatus(raw);
82
104
  if (raw.modelSubstitutionWarning) {
@@ -117,19 +139,16 @@ function buildReview(raw: SingleResult): TaskResultReceipt["review"] | undefined
117
139
  const rawFindings = Array.isArray(data.report_finding) ? data.report_finding : [];
118
140
  const findings = rawFindings.slice(0, 20).map(item => {
119
141
  const value = item && typeof item === "object" ? (item as Record<string, unknown>) : {};
120
- const severity =
121
- typeof value.severity === "string"
122
- ? value.severity
123
- : typeof value.priority === "string"
124
- ? value.priority
125
- : undefined;
142
+ const severity = normalizeReviewFindingSeverity(value.severity, value.priority);
126
143
  const summaryValue = value.summary ?? value.title ?? value.message ?? value.body ?? "finding";
127
144
  return { severity, summary: truncateText(String(summaryValue), 200) ?? "finding" };
128
145
  });
129
146
  if (!reviewYield && findings.length === 0) return undefined;
130
147
  return {
131
- overallCorrectness:
148
+ overallCorrectness: truncateText(
132
149
  typeof reviewYield?.overall_correctness === "string" ? reviewYield.overall_correctness : undefined,
150
+ 200,
151
+ ),
133
152
  findingCount: rawFindings.length,
134
153
  findings: findings.length > 0 ? findings : undefined,
135
154
  };