@gajae-code/coding-agent 0.7.11 → 0.8.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 (86) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/config/settings-schema.d.ts +25 -2
  5. package/dist/types/discovery/cursor.d.ts +4 -1
  6. package/dist/types/discovery/index.d.ts +0 -1
  7. package/dist/types/discovery/windsurf.d.ts +3 -1
  8. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +5 -0
  9. package/dist/types/modes/components/custom-editor.d.ts +4 -0
  10. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  11. package/dist/types/modes/components/status-line.d.ts +8 -0
  12. package/dist/types/modes/components/two-column-body.d.ts +9 -0
  13. package/dist/types/modes/components/welcome.d.ts +9 -1
  14. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  15. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  16. package/dist/types/modes/interactive-mode.d.ts +1 -0
  17. package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
  18. package/dist/types/modes/types.d.ts +1 -0
  19. package/dist/types/notifications/telegram-daemon.d.ts +3 -0
  20. package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
  21. package/dist/types/runtime-mcp/config.d.ts +2 -0
  22. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  23. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  24. package/dist/types/runtime-mcp/types.d.ts +6 -0
  25. package/dist/types/system-prompt.d.ts +7 -0
  26. package/dist/types/tools/skill.d.ts +1 -1
  27. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  28. package/package.json +10 -10
  29. package/src/capability/mcp.ts +2 -0
  30. package/src/config/mcp-schema.json +5 -1
  31. package/src/config/settings-schema.ts +18 -2
  32. package/src/coordinator-mcp/server.ts +39 -4
  33. package/src/discovery/builtin.ts +12 -0
  34. package/src/discovery/cursor.ts +5 -84
  35. package/src/discovery/gemini.ts +3 -89
  36. package/src/discovery/index.ts +4 -1
  37. package/src/discovery/mcp-json.ts +11 -0
  38. package/src/discovery/opencode.ts +5 -122
  39. package/src/discovery/windsurf.ts +4 -82
  40. package/src/edit/read-file.ts +9 -1
  41. package/src/extensibility/skills.ts +30 -2
  42. package/src/gjc-runtime/session-state-sidecar.ts +167 -24
  43. package/src/gjc-runtime/team-runtime.ts +56 -51
  44. package/src/internal-urls/docs-index.generated.ts +4 -4
  45. package/src/modes/acp/acp-agent.ts +7 -6
  46. package/src/modes/components/agent-dashboard.ts +1 -34
  47. package/src/modes/components/custom-editor.ts +18 -0
  48. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  49. package/src/modes/components/model-selector.ts +58 -2
  50. package/src/modes/components/session-selector.ts +19 -9
  51. package/src/modes/components/settings-selector.ts +13 -0
  52. package/src/modes/components/status-line.ts +215 -86
  53. package/src/modes/components/tree-selector.ts +28 -8
  54. package/src/modes/components/two-column-body.ts +36 -0
  55. package/src/modes/components/welcome.ts +171 -35
  56. package/src/modes/controllers/command-controller.ts +3 -2
  57. package/src/modes/controllers/event-controller.ts +4 -0
  58. package/src/modes/controllers/input-controller.ts +158 -38
  59. package/src/modes/controllers/selector-controller.ts +25 -28
  60. package/src/modes/interactive-mode.ts +20 -23
  61. package/src/modes/prompt-action-autocomplete.ts +17 -7
  62. package/src/modes/rpc/rpc-types.ts +1 -0
  63. package/src/modes/shared/agent-wire/deep-interview-gate.ts +9 -3
  64. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  65. package/src/modes/types.ts +1 -0
  66. package/src/notifications/index.ts +20 -1
  67. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  68. package/src/notifications/telegram-daemon.ts +14 -0
  69. package/src/prompts/system/system-prompt.md +15 -1
  70. package/src/runtime-mcp/config-writer.ts +28 -0
  71. package/src/runtime-mcp/config.ts +7 -0
  72. package/src/runtime-mcp/loader.ts +3 -0
  73. package/src/runtime-mcp/manager.ts +3 -0
  74. package/src/runtime-mcp/types.ts +6 -0
  75. package/src/sdk.ts +4 -0
  76. package/src/session/agent-session.ts +21 -1
  77. package/src/session/session-manager.ts +58 -18
  78. package/src/system-prompt.ts +8 -0
  79. package/src/task/executor.ts +5 -0
  80. package/src/tools/ast-grep.ts +10 -7
  81. package/src/tools/bash.ts +15 -6
  82. package/src/tools/index.ts +8 -1
  83. package/src/tools/skill.ts +1 -1
  84. package/src/workflow/workflow-intent-diff.ts +199 -0
  85. package/dist/types/discovery/vscode.d.ts +0 -1
  86. package/src/discovery/vscode.ts +0 -105
@@ -61,6 +61,12 @@ export interface MCPAuthConfig {
61
61
  interface MCPServerConfigBase {
62
62
  /** Whether this server is enabled (default: true) */
63
63
  enabled?: boolean;
64
+ /**
65
+ * Whether to connect this server automatically at session startup (default: true).
66
+ * When false the server stays configured-but-disconnected until connected
67
+ * explicitly (e.g. `/mcp connect <name>`).
68
+ */
69
+ autoload?: boolean;
64
70
  /** Connection timeout in milliseconds (default: 30000) */
65
71
  timeout?: number;
66
72
  /** Authentication configuration (optional) */
package/src/sdk.ts CHANGED
@@ -1816,6 +1816,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1816
1816
  intentField,
1817
1817
  mcpDiscoveryMode: false,
1818
1818
  mcpDiscoveryServerSummaries: [],
1819
+ toolDiscoveryActive: effectiveDiscoveryMode === "all",
1820
+ discoverableTools: discoverableBuiltinTools
1821
+ .map(tool => ({ name: tool.name, summary: tool.summary }))
1822
+ .sort((a, b) => a.name.localeCompare(b.name)),
1819
1823
  eagerTasks,
1820
1824
  secretsEnabled,
1821
1825
  workspaceTree: workspaceTreePromise,
@@ -192,7 +192,10 @@ import {
192
192
  modeStatePath as sessionModeStatePath,
193
193
  sessionStateDir,
194
194
  } from "../gjc-runtime/session-layout";
195
- import { persistCoordinatorRuntimeStateFromEvent } from "../gjc-runtime/session-state-sidecar";
195
+ import {
196
+ persistCoordinatorRuntimeStateFromEvent,
197
+ registerCoordinatorRuntimeStateFinalizer,
198
+ } from "../gjc-runtime/session-state-sidecar";
196
199
  import { writeArtifact } from "../gjc-runtime/state-writer";
197
200
  import { requestGjcWorkerIntegrationAttempt } from "../gjc-runtime/team-runtime";
198
201
  import { GoalRuntime } from "../goals/runtime";
@@ -260,6 +263,7 @@ import { type EditMode, resolveEditMode } from "../utils/edit-mode";
260
263
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
261
264
  import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
262
265
  import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
266
+ import { buildWorkflowIntentDiff, WORKFLOW_INTENT_DIFF_CUSTOM_TYPE } from "../workflow/workflow-intent-diff";
263
267
  import type { AuthStorage } from "./auth-storage";
264
268
  import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
265
269
  import {
@@ -1009,6 +1013,7 @@ export class AgentSession {
1009
1013
  #evalKernelOwnerId: string;
1010
1014
  /** Idempotent unregister handle for this session's resource-GC registration. */
1011
1015
  #unregisterResourceGc?: () => void;
1016
+ #unregisterRuntimeStateFinalizer?: () => void;
1012
1017
  /**
1013
1018
  * AsyncJobManager owned by this session (top-level only). Subagents leave
1014
1019
  * this undefined and **MUST NOT** dispose the global instance on teardown.
@@ -1234,6 +1239,11 @@ export class AgentSession {
1234
1239
  settings: this.settings,
1235
1240
  });
1236
1241
  }
1242
+ this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer({
1243
+ sessionId: this.sessionId,
1244
+ cwd: this.sessionManager.getCwd(),
1245
+ sessionFile: this.sessionManager.getSessionFile(),
1246
+ });
1237
1247
  // Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
1238
1248
  this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
1239
1249
  this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
@@ -3349,6 +3359,8 @@ export class AgentSession {
3349
3359
  // No-op when this session opened no tabs. Failure is logged, not thrown.
3350
3360
  this.#unregisterResourceGc?.();
3351
3361
  this.#unregisterResourceGc = undefined;
3362
+ this.#unregisterRuntimeStateFinalizer?.();
3363
+ this.#unregisterRuntimeStateFinalizer = undefined;
3352
3364
  await releaseTabsForOwner(this.sessionManager.getSessionId()).catch((error: unknown) =>
3353
3365
  logger.warn("session dispose: releaseTabsForOwner failed", { error }),
3354
3366
  );
@@ -4828,6 +4840,7 @@ export class AgentSession {
4828
4840
 
4829
4841
  // Expand file-based prompt templates if requested
4830
4842
  const expandedText = expandPromptTemplates ? expandPromptTemplate(text, [...this.#promptTemplates]) : text;
4843
+ const workflowIntentDiff = options?.synthetic ? null : buildWorkflowIntentDiff(expandedText);
4831
4844
 
4832
4845
  // If streaming, queue via steer() or followUp() based on option
4833
4846
  if (this.isStreaming) {
@@ -4839,9 +4852,16 @@ export class AgentSession {
4839
4852
  } else {
4840
4853
  await this.#queueSteer(expandedText, options?.images);
4841
4854
  }
4855
+ if (workflowIntentDiff) {
4856
+ this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
4857
+ }
4842
4858
  return;
4843
4859
  }
4844
4860
 
4861
+ if (workflowIntentDiff) {
4862
+ this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
4863
+ }
4864
+
4845
4865
  // Skip eager todo prelude when the user has already queued a directive
4846
4866
  const hasPendingUserDirective = this.#toolChoiceQueue.inspect().includes("user-force");
4847
4867
  const eagerTodoPrelude =
@@ -1461,9 +1461,25 @@ function materializeResidentEntrySync<T extends FileEntry | SessionEntry>(
1461
1461
  function materializeResidentEntriesSync<T extends FileEntry | SessionEntry>(
1462
1462
  entries: T[],
1463
1463
  stores: ResidentBlobStores,
1464
+ missingPolicy: ResidentBlobMissingPolicy = "throw",
1464
1465
  ): T[] {
1465
1466
  const cache = new Map<string, string>();
1466
- return entries.map(entry => materializeResidentEntrySync(entry, stores, cache));
1467
+ return entries.map(entry => materializeResidentEntrySync(entry, stores, cache, missingPolicy));
1468
+ }
1469
+
1470
+ function materializeResidentEntryForReadSync<T extends FileEntry | SessionEntry>(
1471
+ entry: T,
1472
+ stores: ResidentBlobStores,
1473
+ cache: Map<string, string>,
1474
+ ): T {
1475
+ return materializeResidentEntrySync(entry, stores, cache, "placeholder");
1476
+ }
1477
+
1478
+ function materializeResidentEntriesForReadSync<T extends FileEntry | SessionEntry>(
1479
+ entries: T[],
1480
+ stores: ResidentBlobStores,
1481
+ ): T[] {
1482
+ return materializeResidentEntriesSync(entries, stores, "placeholder");
1467
1483
  }
1468
1484
 
1469
1485
  function materializeResidentEntryForPersistenceSync<T extends FileEntry | SessionEntry>(
@@ -1520,12 +1536,24 @@ function cloneSessionEntry(entry: SessionEntry): SessionEntry {
1520
1536
  function materializeProviderVisibleEntrySync(entry: SessionEntry, stores: ResidentBlobStores): SessionEntry {
1521
1537
  if (entry.type === "compaction") {
1522
1538
  const cache = new Map<string, string>();
1523
- const summary = materializeResidentValueSync(entry.summary, stores, "summary", cache);
1524
- const shortSummary = materializeResidentValueSync(entry.shortSummary, stores, "shortSummary", cache);
1539
+ const summary = materializeResidentValueSync(entry.summary, stores, "summary", cache, "placeholder");
1540
+ const shortSummary = materializeResidentValueSync(
1541
+ entry.shortSummary,
1542
+ stores,
1543
+ "shortSummary",
1544
+ cache,
1545
+ "placeholder",
1546
+ );
1525
1547
  const remote = entry.preserveData?.openaiRemoteCompaction;
1526
1548
  const remoteRecord = isRecord(remote) ? remote : undefined;
1527
1549
  const replacementHistory = remoteRecord
1528
- ? materializeResidentValueSync(remoteRecord.replacementHistory, stores, "replacementHistory", cache)
1550
+ ? materializeResidentValueSync(
1551
+ remoteRecord.replacementHistory,
1552
+ stores,
1553
+ "replacementHistory",
1554
+ cache,
1555
+ "placeholder",
1556
+ )
1529
1557
  : undefined;
1530
1558
  const preserveData =
1531
1559
  remoteRecord && replacementHistory !== undefined && replacementHistory !== remoteRecord.replacementHistory
@@ -1545,7 +1573,13 @@ function materializeProviderVisibleEntrySync(entry: SessionEntry, stores: Reside
1545
1573
  };
1546
1574
  }
1547
1575
  if (entry.type === "branch_summary") {
1548
- const summary = materializeResidentValueSync(entry.summary, stores, "summary", new Map<string, string>());
1576
+ const summary = materializeResidentValueSync(
1577
+ entry.summary,
1578
+ stores,
1579
+ "summary",
1580
+ new Map<string, string>(),
1581
+ "placeholder",
1582
+ );
1549
1583
  return typeof summary === "string" ? { ...entry, summary } : { ...entry };
1550
1584
  }
1551
1585
  return cloneSessionEntry(entry);
@@ -2751,7 +2785,10 @@ export class SessionManager {
2751
2785
  }
2752
2786
 
2753
2787
  captureState(): SessionManagerStateSnapshot {
2754
- const materializedFileEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
2788
+ const materializedFileEntries = materializeResidentEntriesForReadSync(
2789
+ this.#fileEntries,
2790
+ this.#residentBlobStores(),
2791
+ );
2755
2792
  return {
2756
2793
  sessionId: this.#sessionId,
2757
2794
  sessionName: this.#sessionName,
@@ -2875,7 +2912,7 @@ export class SessionManager {
2875
2912
 
2876
2913
  const oldSessionFile = this.#sessionFile;
2877
2914
  const oldSessionId = this.#sessionId;
2878
- const materializedEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
2915
+ const materializedEntries = materializeResidentEntriesForReadSync(this.#fileEntries, this.#residentBlobStores());
2879
2916
 
2880
2917
  // Close the current writer
2881
2918
  await this.#closePersistWriter();
@@ -2947,7 +2984,10 @@ export class SessionManager {
2947
2984
  hadSessionFile = this.storage.existsSync(oldSessionFile);
2948
2985
  let movedSessionFile = false;
2949
2986
  let movedArtifactDir = false;
2950
- const materializedEntries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores());
2987
+ const materializedEntries = materializeResidentEntriesForReadSync(
2988
+ this.#fileEntries,
2989
+ this.#residentBlobStores(),
2990
+ );
2951
2991
  const restoreResidentStateAfterFailure = (): void => {
2952
2992
  this.#fileEntries = materializedEntries;
2953
2993
  this.#resetResidentTextBlobStore();
@@ -4006,7 +4046,7 @@ export class SessionManager {
4006
4046
  getLeafEntry(): SessionEntry | undefined {
4007
4047
  if (!this.#leafId) return undefined;
4008
4048
  const entry = this.#byId.get(this.#leafId);
4009
- return entry ? materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()) : undefined;
4049
+ return entry ? materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()) : undefined;
4010
4050
  }
4011
4051
 
4012
4052
  /**
@@ -4176,7 +4216,7 @@ export class SessionManager {
4176
4216
  const entry = this.#byId.get(id);
4177
4217
  return entry
4178
4218
  ? rehydrateColdSpillEntry(
4179
- materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()),
4219
+ materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()),
4180
4220
  this.#blobStore,
4181
4221
  this.#residentBlobStoresForColdRehydrate(),
4182
4222
  )
@@ -4193,7 +4233,7 @@ export class SessionManager {
4193
4233
  visited.add(current.id);
4194
4234
  path.push(
4195
4235
  rehydrateColdSpillEntry(
4196
- materializeResidentEntrySync(current, this.#residentBlobStores(), cache),
4236
+ materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache),
4197
4237
  this.#blobStore,
4198
4238
  this.#residentBlobStoresForColdRehydrate(),
4199
4239
  ),
@@ -4233,7 +4273,7 @@ export class SessionManager {
4233
4273
  .filter((entry): entry is SessionEntry => entry.type !== "session")
4234
4274
  .map(entry =>
4235
4275
  rehydrateColdSpillEntry(
4236
- materializeResidentEntrySync(entry, this.#residentBlobStores(), cache),
4276
+ materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache),
4237
4277
  this.#blobStore,
4238
4278
  this.#residentBlobStoresForColdRehydrate(),
4239
4279
  ),
@@ -4244,7 +4284,7 @@ export class SessionManager {
4244
4284
  this.#publicMaterializerCallCount++;
4245
4285
  this.#getEntryMaterializerCallCount++;
4246
4286
  const entry = this.#byId.get(id);
4247
- return entry ? materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map()) : undefined;
4287
+ return entry ? materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()) : undefined;
4248
4288
  }
4249
4289
 
4250
4290
  /**
@@ -4255,7 +4295,7 @@ export class SessionManager {
4255
4295
  const children: SessionEntry[] = [];
4256
4296
  for (const entry of this.#byId.values()) {
4257
4297
  if (entry.parentId === parentId) {
4258
- children.push(materializeResidentEntrySync(entry, this.#residentBlobStores(), cache));
4298
+ children.push(materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), cache));
4259
4299
  }
4260
4300
  }
4261
4301
  return children;
@@ -4310,7 +4350,7 @@ export class SessionManager {
4310
4350
  while (current) {
4311
4351
  if (visited.has(current.id)) break;
4312
4352
  visited.add(current.id);
4313
- path.push(materializeResidentEntrySync(current, this.#residentBlobStores(), cache));
4353
+ path.push(materializeResidentEntryForReadSync(current, this.#residentBlobStores(), cache));
4314
4354
  current = current.parentId ? this.#byId.get(current.parentId) : undefined;
4315
4355
  }
4316
4356
  path.reverse();
@@ -4375,7 +4415,7 @@ export class SessionManager {
4375
4415
  }
4376
4416
  if (entry.type !== "message" && entry.type !== "custom_message")
4377
4417
  return materializeProviderVisibleEntrySync(entry, this.#residentBlobStores());
4378
- const materialized = materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map());
4418
+ const materialized = materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map());
4379
4419
  const rehydrated = rehydrateColdSpillEntry(
4380
4420
  materialized,
4381
4421
  this.#blobStore,
@@ -4434,7 +4474,7 @@ export class SessionManager {
4434
4474
  const resolvedTextBlobCache = new Map<string, string>();
4435
4475
  const materializedEntries = this.#fileEntries
4436
4476
  .filter((e): e is SessionEntry => e.type !== "session")
4437
- .map(entry => materializeResidentEntrySync(entry, this.#residentBlobStores(), resolvedTextBlobCache));
4477
+ .map(entry => materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), resolvedTextBlobCache));
4438
4478
  this.#materializedEntriesCache = materializedEntries;
4439
4479
  this.#materializedEntriesRevision = this.#entryRevision;
4440
4480
  return materializedEntries;
@@ -4591,7 +4631,7 @@ export class SessionManager {
4591
4631
 
4592
4632
  // Filter out LabelEntry from path - we'll recreate them from the resolved map
4593
4633
  const pathWithoutLabels = branchPath.filter(e => e.type !== "label");
4594
- const materializedPathWithoutLabels = materializeResidentEntriesSync(
4634
+ const materializedPathWithoutLabels = materializeResidentEntriesForReadSync(
4595
4635
  pathWithoutLabels,
4596
4636
  this.#residentBlobStores(),
4597
4637
  );
@@ -356,6 +356,10 @@ export interface BuildSystemPromptOptions {
356
356
  mcpDiscoveryMode?: boolean;
357
357
  /** Discoverable MCP server summaries to advertise when discovery mode is active. */
358
358
  mcpDiscoveryServerSummaries?: string[];
359
+ /** Whether built-in tool discovery ("all" mode) is active; enables the tool-discovery prompt block. */
360
+ toolDiscoveryActive?: boolean;
361
+ /** Hidden discoverable tools to advertise (name + one-line summary) when tool discovery is active. */
362
+ discoverableTools?: Array<{ name: string; summary: string }>;
359
363
  /** Encourage the agent to delegate via tasks unless changes are trivial. */
360
364
  eagerTasks?: boolean;
361
365
  /** Rules with alwaysApply=true — their full content is injected into the prompt. */
@@ -394,6 +398,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
394
398
  intentField,
395
399
  mcpDiscoveryMode = false,
396
400
  mcpDiscoveryServerSummaries = [],
401
+ toolDiscoveryActive = false,
402
+ discoverableTools = [],
397
403
  eagerTasks = false,
398
404
  secretsEnabled = false,
399
405
  workspaceTree: providedWorkspaceTree,
@@ -572,6 +578,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
572
578
  mcpDiscoveryMode,
573
579
  hasMCPDiscoveryServers: mcpDiscoveryServerSummaries.length > 0,
574
580
  mcpDiscoveryServerSummaries,
581
+ toolDiscoveryActive,
582
+ discoverableTools,
575
583
  eagerTasks,
576
584
  secretsEnabled,
577
585
  };
@@ -1535,6 +1535,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1535
1535
  );
1536
1536
  await awaitAbortable(session.waitForIdle());
1537
1537
  } catch (err) {
1538
+ // An abort (parent cancel, session shutdown, timeout) rejects
1539
+ // awaitAbortable with ToolAbortError — a cancellation, not a prompt
1540
+ // failure. Mirror the outer catch's `!abortSignal.aborted` guard so
1541
+ // expected cancellations aren't logged at error level.
1542
+ if (abortSignal.aborted || err instanceof ToolAbortError) break;
1538
1543
  logger.error("Subagent prompt failed", {
1539
1544
  error: err instanceof Error ? err.message : String(err),
1540
1545
  });
@@ -223,13 +223,16 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
223
223
  const modelOut: string[] = [];
224
224
  const displayOut: string[] = [];
225
225
  const fileMatches = matchesByFile.get(relativePath) ?? [];
226
- const lineNumberWidth = fileMatches.reduce((width, match) => {
227
- const lineCount = match.text.split("\n").length;
228
- const endLine = match.startLine + lineCount - 1;
229
- return Math.max(width, String(match.startLine).length, String(endLine).length);
230
- }, 0);
231
- for (const match of fileMatches) {
232
- const matchLines = match.text.split("\n");
226
+ const preparedMatches = fileMatches.map(match => {
227
+ const lines = match.text.split("\n");
228
+ const endLine = match.startLine + lines.length - 1;
229
+ return { match, lines, endLine };
230
+ });
231
+ const lineNumberWidth = preparedMatches.reduce(
232
+ (width, entry) => Math.max(width, String(entry.match.startLine).length, String(entry.endLine).length),
233
+ 0,
234
+ );
235
+ for (const { match, lines: matchLines } of preparedMatches) {
233
236
  for (let index = 0; index < matchLines.length; index++) {
234
237
  const lineNumber = match.startLine + index;
235
238
  const isMatch = index === 0;
package/src/tools/bash.ts CHANGED
@@ -833,9 +833,22 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
833
833
  });
834
834
  }
835
835
 
836
+ // Route through the client terminal when the client advertises the terminal capability.
837
+ // Skip when pty=true (PTY needs the local terminal UI).
838
+ const clientBridge =
839
+ this.session.bashRestrictionProfile === "read-only" ? undefined : this.session.getClientBridge?.();
840
+ const clientTerminalActive = Boolean(clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty);
841
+
836
842
  const autoBgManager = AsyncJobManager.instance();
837
- if (this.#autoBackgroundEnabled && !pty && autoBgManager) {
838
- const autoBackgroundWaitMs = this.#resolveAutoBackgroundWaitMs(timeoutMs);
843
+ // Run non-PTY bash through the managed job path so Ctrl+B-twice fold-on-demand works
844
+ // even when auto-background is disabled. When a client terminal will handle the
845
+ // command, keep the existing bridge path unless auto-background is enabled.
846
+ if (!pty && autoBgManager && (this.#autoBackgroundEnabled || !clientTerminalActive)) {
847
+ // With auto-background off, wait past the command's own timeout so the job only
848
+ // leaves the foreground on an explicit Ctrl+B fold, never on an auto-background timer.
849
+ const autoBackgroundWaitMs = this.#autoBackgroundEnabled
850
+ ? this.#resolveAutoBackgroundWaitMs(timeoutMs)
851
+ : timeoutMs + 1_000;
839
852
  const startBackgrounded = autoBackgroundWaitMs === 0;
840
853
  const job = this.#startManagedBashJob({
841
854
  command,
@@ -891,10 +904,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
891
904
  });
892
905
  }
893
906
 
894
- // Route through the client terminal when the client advertises the terminal capability.
895
- // Skip when pty=true (PTY needs the local terminal UI).
896
- const clientBridge =
897
- this.session.bashRestrictionProfile === "read-only" ? undefined : this.session.getClientBridge?.();
898
907
  if (clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty) {
899
908
  const handle = await clientBridge.createTerminal({
900
909
  command,
@@ -325,7 +325,14 @@ export type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool |
325
325
  export type BuiltinToolLoadMode = "essential" | "discoverable";
326
326
 
327
327
  /** Default essential tool names when tools.essentialOverride is empty. */
328
- export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = ["read", "bash", "edit"] as const;
328
+ export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = [
329
+ "read",
330
+ "bash",
331
+ "edit",
332
+ "write",
333
+ "search",
334
+ "find",
335
+ ] as const;
329
336
 
330
337
  /**
331
338
  * Resolve the active essential built-in tool names from settings.
@@ -50,7 +50,7 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
50
50
  readonly name = "skill";
51
51
  readonly label = "Skill";
52
52
  readonly summary = "Chain into another available skill in the current turn";
53
- readonly loadMode = "discoverable";
53
+ readonly loadMode = "essential";
54
54
  readonly description: string;
55
55
  readonly parameters = skillSchema;
56
56
  readonly strict = true;
@@ -0,0 +1,199 @@
1
+ export const WORKFLOW_INTENT_DIFF_CUSTOM_TYPE = "workflow-intent-diff";
2
+
3
+ export type WorkflowIntentRoute = "direct" | "deep-interview" | "ralplan" | "ultragoal" | "team";
4
+ export type DirectTrackingMode = "custom-entry-only" | "not-direct";
5
+ export type RootCausePhaseStatus = "active" | "inactive";
6
+
7
+ export interface WorkflowIntentDiff {
8
+ readonly version: 1;
9
+ readonly route: WorkflowIntentRoute;
10
+ readonly reason: string;
11
+ readonly directTracking: DirectTrackingMode;
12
+ readonly recommendedSkill?: Exclude<WorkflowIntentRoute, "direct">;
13
+ readonly recommendedInvocation?: string;
14
+ readonly triggers: readonly string[];
15
+ readonly rootCausePhase: {
16
+ readonly status: RootCausePhaseStatus;
17
+ readonly triggers: readonly string[];
18
+ };
19
+ readonly promptPreview: string;
20
+ }
21
+
22
+ interface RouteMatch {
23
+ readonly route: WorkflowIntentRoute;
24
+ readonly reason: string;
25
+ readonly recommendedInvocation?: string;
26
+ readonly triggers: readonly string[];
27
+ }
28
+
29
+ const PROMPT_PREVIEW_LIMIT = 240;
30
+
31
+ const DURABLE_TRACKING_PATTERNS = [
32
+ /\bultragoal\b/i,
33
+ /\bdurable (?:goal|tracking|ledger|plan)\b/i,
34
+ /\b(?:goal|tracking|plan) ledger\b/i,
35
+ /\bcheckpoint(?:ed|ing)? (?:goal|plan|workflow|release|work)\b/i,
36
+ /\bcheckpoint (?:this|the) (?:goal|plan|workflow|release|work)\b/i,
37
+ ] as const;
38
+
39
+ const AMBIGUOUS_REQUIREMENT_PATTERNS = [
40
+ /\bambiguous (?:requirement|requirements|request|requests|scope)\b/i,
41
+ /\bdeep[- ]interview\b/i,
42
+ /\binterview me\b/i,
43
+ /\bdon't assume\b/i,
44
+ /\bnot sure\b/i,
45
+ /\bunclear\b/i,
46
+ /\bvague\b/i,
47
+ ] as const;
48
+
49
+ const ARCHITECTURE_SEQUENCE_PATTERNS = [
50
+ /\barchitecture\b/i,
51
+ /\barchitectural\b/i,
52
+ /\bsequence\b/i,
53
+ /\bmigrate\b/i,
54
+ /\bmigration\b/i,
55
+ /\bauth(?:entication|orization)? (?:migration|sequence|rollout|release|refactor|rewrite|transition)\b/i,
56
+ /\bsecurity\b/i,
57
+ /\bcompliance\b/i,
58
+ /\bPII\b/i,
59
+ /\bproduction (?:release|rollout|deployment|migration|incident|data|change|cutover)\b/i,
60
+ /\bdeploy(?:ing)? to production\b/i,
61
+ /\bbreaking change\b/i,
62
+ /\bdata loss\b/i,
63
+ /\bdestructive\b/i,
64
+ ] as const;
65
+
66
+ const TEAM_PATTERNS = [
67
+ /\buse (?:a )?team\b/i,
68
+ /\bcoordinated (?:persistent )?workers\b/i,
69
+ /\bpersistent workers\b/i,
70
+ /\bworker coordination\b/i,
71
+ ] as const;
72
+
73
+ const ROOT_CAUSE_TRIGGERS = [
74
+ { name: "contradiction", pattern: /\bcontradict(?:ion|s|ory)?\b/i },
75
+ { name: "regression", pattern: /\bregression\b/i },
76
+ {
77
+ name: "high-risk transition",
78
+ pattern:
79
+ /\b(?:migrate|migration|auth(?:entication|orization)? (?:migration|sequence|rollout|release|refactor|rewrite|transition)|security|breaking change|data loss|destructive|production (?:release|rollout|deployment|migration|incident|data|change|cutover)|deploy(?:ing)? to production|compliance|PII)\b/i,
80
+ },
81
+ ] as const;
82
+
83
+ function matchesAny(text: string, patterns: readonly RegExp[]): boolean {
84
+ return patterns.some(pattern => pattern.test(text));
85
+ }
86
+
87
+ function withGlobal(pattern: RegExp): RegExp {
88
+ const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
89
+ return new RegExp(pattern.source, flags);
90
+ }
91
+
92
+ function isNegatedAt(text: string, index: number): boolean {
93
+ const prefix = text.slice(Math.max(0, index - 48), index).toLowerCase();
94
+ const boundary = Math.max(
95
+ prefix.lastIndexOf("."),
96
+ prefix.lastIndexOf(","),
97
+ prefix.lastIndexOf(";"),
98
+ prefix.lastIndexOf(":"),
99
+ prefix.lastIndexOf("?"),
100
+ prefix.lastIndexOf("!"),
101
+ prefix.lastIndexOf("\n"),
102
+ prefix.lastIndexOf(" but "),
103
+ prefix.lastIndexOf(" however "),
104
+ prefix.lastIndexOf(" yet "),
105
+ prefix.lastIndexOf(" then "),
106
+ );
107
+ const localPrefix = boundary >= 0 ? prefix.slice(boundary + 1) : prefix;
108
+ return /(?:^|[\s([{])(?:no|not|without|excluding|exclude|absent|absence of|avoid)\b[^.,!?;:\n]{0,48}$/.test(
109
+ localPrefix,
110
+ );
111
+ }
112
+
113
+ function matchesAnyNonNegated(text: string, patterns: readonly RegExp[]): boolean {
114
+ return patterns.some(pattern => {
115
+ for (const match of text.matchAll(withGlobal(pattern))) {
116
+ if (!isNegatedAt(text, match.index ?? 0)) return true;
117
+ }
118
+ return false;
119
+ });
120
+ }
121
+
122
+ function collectRootCauseTriggers(text: string): readonly string[] {
123
+ return ROOT_CAUSE_TRIGGERS.filter(trigger => matchesAnyNonNegated(text, [trigger.pattern])).map(
124
+ trigger => trigger.name,
125
+ );
126
+ }
127
+
128
+ function buildPromptPreview(text: string): string {
129
+ const compact = text.replace(/\s+/g, " ").trim();
130
+ return compact.length > PROMPT_PREVIEW_LIMIT ? `${compact.slice(0, PROMPT_PREVIEW_LIMIT - 3)}...` : compact;
131
+ }
132
+
133
+ function classifyRoute(text: string): RouteMatch {
134
+ if (matchesAny(text, AMBIGUOUS_REQUIREMENT_PATTERNS)) {
135
+ return {
136
+ route: "deep-interview",
137
+ reason: "ambiguous requirements need clarification",
138
+ recommendedInvocation: "/skill:deep-interview",
139
+ triggers: ["ambiguous requirements"],
140
+ };
141
+ }
142
+
143
+ if (matchesAny(text, DURABLE_TRACKING_PATTERNS)) {
144
+ return {
145
+ route: "ultragoal",
146
+ reason: "durable tracking requested",
147
+ recommendedInvocation: "/skill:ultragoal",
148
+ triggers: ["durable tracking"],
149
+ };
150
+ }
151
+
152
+ if (matchesAnyNonNegated(text, ARCHITECTURE_SEQUENCE_PATTERNS)) {
153
+ return {
154
+ route: "ralplan",
155
+ reason: "architecture or sequence risk requires deliberate planning",
156
+ recommendedInvocation: "/skill:ralplan --deliberate",
157
+ triggers: ["architecture/sequence risk"],
158
+ };
159
+ }
160
+
161
+ if (matchesAny(text, TEAM_PATTERNS)) {
162
+ return {
163
+ route: "team",
164
+ reason: "coordinated persistent workers requested",
165
+ recommendedInvocation: "/skill:team",
166
+ triggers: ["coordinated workers"],
167
+ };
168
+ }
169
+
170
+ return {
171
+ route: "direct",
172
+ reason: "clear low-risk prompt stays on direct implementation path",
173
+ triggers: ["low-risk direct"],
174
+ };
175
+ }
176
+
177
+ export function buildWorkflowIntentDiff(text: string): WorkflowIntentDiff | null {
178
+ const promptPreview = buildPromptPreview(text);
179
+ if (!promptPreview) return null;
180
+
181
+ const route = classifyRoute(text);
182
+ const rootCauseTriggers = collectRootCauseTriggers(text);
183
+ const rootCauseActive = rootCauseTriggers.length > 0;
184
+ const direct = route.route === "direct";
185
+
186
+ return {
187
+ version: 1,
188
+ route: route.route,
189
+ reason: route.reason,
190
+ directTracking: direct ? "custom-entry-only" : "not-direct",
191
+ ...(direct ? {} : { recommendedSkill: route.route, recommendedInvocation: route.recommendedInvocation }),
192
+ triggers: route.triggers,
193
+ rootCausePhase: {
194
+ status: rootCauseActive ? "active" : "inactive",
195
+ triggers: rootCauseTriggers,
196
+ },
197
+ promptPreview,
198
+ };
199
+ }
@@ -1 +0,0 @@
1
- export {};