@gajae-code/coding-agent 0.7.11 → 0.8.0

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 (78) hide show
  1. package/CHANGELOG.md +33 -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/runtime-mcp/config-writer.d.ts +9 -0
  20. package/dist/types/runtime-mcp/config.d.ts +2 -0
  21. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  22. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  23. package/dist/types/runtime-mcp/types.d.ts +6 -0
  24. package/dist/types/system-prompt.d.ts +7 -0
  25. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  26. package/package.json +10 -10
  27. package/src/capability/mcp.ts +2 -0
  28. package/src/config/mcp-schema.json +5 -1
  29. package/src/config/settings-schema.ts +18 -2
  30. package/src/coordinator-mcp/server.ts +6 -0
  31. package/src/discovery/builtin.ts +12 -0
  32. package/src/discovery/cursor.ts +5 -84
  33. package/src/discovery/gemini.ts +3 -89
  34. package/src/discovery/index.ts +4 -1
  35. package/src/discovery/mcp-json.ts +11 -0
  36. package/src/discovery/opencode.ts +5 -122
  37. package/src/discovery/windsurf.ts +4 -82
  38. package/src/edit/read-file.ts +9 -1
  39. package/src/gjc-runtime/session-state-sidecar.ts +167 -24
  40. package/src/gjc-runtime/team-runtime.ts +56 -51
  41. package/src/internal-urls/docs-index.generated.ts +4 -4
  42. package/src/modes/components/agent-dashboard.ts +1 -34
  43. package/src/modes/components/custom-editor.ts +18 -0
  44. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  45. package/src/modes/components/model-selector.ts +58 -2
  46. package/src/modes/components/session-selector.ts +19 -9
  47. package/src/modes/components/settings-selector.ts +13 -0
  48. package/src/modes/components/status-line.ts +215 -86
  49. package/src/modes/components/tree-selector.ts +28 -8
  50. package/src/modes/components/two-column-body.ts +36 -0
  51. package/src/modes/components/welcome.ts +171 -35
  52. package/src/modes/controllers/event-controller.ts +4 -0
  53. package/src/modes/controllers/input-controller.ts +151 -30
  54. package/src/modes/controllers/selector-controller.ts +25 -28
  55. package/src/modes/interactive-mode.ts +20 -23
  56. package/src/modes/rpc/rpc-types.ts +1 -0
  57. package/src/modes/shared/agent-wire/deep-interview-gate.ts +9 -3
  58. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  59. package/src/modes/types.ts +1 -0
  60. package/src/notifications/index.ts +20 -1
  61. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  62. package/src/prompts/system/system-prompt.md +15 -1
  63. package/src/runtime-mcp/config-writer.ts +28 -0
  64. package/src/runtime-mcp/config.ts +7 -0
  65. package/src/runtime-mcp/loader.ts +3 -0
  66. package/src/runtime-mcp/manager.ts +3 -0
  67. package/src/runtime-mcp/types.ts +6 -0
  68. package/src/sdk.ts +4 -0
  69. package/src/session/agent-session.ts +21 -1
  70. package/src/session/session-manager.ts +58 -18
  71. package/src/system-prompt.ts +8 -0
  72. package/src/task/executor.ts +5 -0
  73. package/src/tools/ast-grep.ts +10 -7
  74. package/src/tools/bash.ts +15 -6
  75. package/src/tools/index.ts +8 -1
  76. package/src/workflow/workflow-intent-diff.ts +199 -0
  77. package/dist/types/discovery/vscode.d.ts +0 -1
  78. package/src/discovery/vscode.ts +0 -105
@@ -15,7 +15,7 @@ import { formatModelProfileDisplayLabel, recommendModelProfileForProvider } from
15
15
  import { GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
16
16
  import { formatModelSelectorValue } from "../../config/model-resolver";
17
17
  import type { ModelProfileConfig } from "../../config/models-config-schema";
18
- import { settings } from "../../config/settings";
18
+ import { type Settings, settings } from "../../config/settings";
19
19
  import { DebugSelectorComponent } from "../../debug";
20
20
  import { disableProvider, enableProvider } from "../../discovery";
21
21
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
@@ -85,6 +85,7 @@ import {
85
85
  import { SessionObserverOverlayComponent } from "../components/session-observer-overlay";
86
86
  import { SessionSelectorComponent } from "../components/session-selector";
87
87
  import { SettingsSelectorComponent } from "../components/settings-selector";
88
+ import type { StatusLineSettings } from "../components/status-line";
88
89
  import { ThemeSelectorComponent } from "../components/theme-selector";
89
90
  import { ToolExecutionComponent } from "../components/tool-execution";
90
91
  import { TreeSelectorComponent } from "../components/tree-selector";
@@ -108,6 +109,24 @@ function isThemePreviewSuperseded(result: { success: boolean; error?: string }):
108
109
  return !result.success && result.error?.includes("superseded by a newer request") === true;
109
110
  }
110
111
 
112
+ /**
113
+ * Snapshot the persisted status-line settings that the status-line component
114
+ * cares about. Preview, cancel-restore, and commit paths all share this so the
115
+ * previewed row count (and every other field) can never drift out of sync.
116
+ */
117
+ export function buildStatusLineSettings(settingsInstance: Settings): StatusLineSettings {
118
+ return {
119
+ preset: settingsInstance.get("statusLine.preset"),
120
+ leftSegments: settingsInstance.get("statusLine.leftSegments"),
121
+ rightSegments: settingsInstance.get("statusLine.rightSegments"),
122
+ separator: settingsInstance.get("statusLine.separator"),
123
+ showHookStatus: settingsInstance.get("statusLine.showHookStatus"),
124
+ sessionAccent: settingsInstance.get("statusLine.sessionAccent"),
125
+ maxRows: settingsInstance.get("statusLine.maxRows"),
126
+ segmentOptions: settingsInstance.get("statusLine.segmentOptions"),
127
+ };
128
+ }
129
+
111
130
  function formatProviderOnboardingCommandGuide(): string {
112
131
  return [
113
132
  "Provider preset setup:",
@@ -442,13 +461,7 @@ export class SelectorController {
442
461
  onStatusLinePreview: previewSettings => {
443
462
  // Update status line with preview settings
444
463
  this.ctx.statusLine.updateSettings({
445
- preset: settings.get("statusLine.preset"),
446
- leftSegments: settings.get("statusLine.leftSegments"),
447
- rightSegments: settings.get("statusLine.rightSegments"),
448
- separator: settings.get("statusLine.separator"),
449
- showHookStatus: settings.get("statusLine.showHookStatus"),
450
- sessionAccent: settings.get("statusLine.sessionAccent"),
451
- segmentOptions: settings.get("statusLine.segmentOptions"),
464
+ ...buildStatusLineSettings(settings),
452
465
  ...previewSettings,
453
466
  });
454
467
  this.ctx.updateEditorTopBorder();
@@ -458,7 +471,7 @@ export class SelectorController {
458
471
  // Return the rendered status line for inline preview
459
472
  const availableWidth =
460
473
  width ?? this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
461
- return this.ctx.statusLine.getTopBorder(availableWidth).content;
474
+ return this.ctx.statusLine.getPreviewContent(availableWidth);
462
475
  },
463
476
  onPluginsChanged: () => {
464
477
  this.ctx.ui.requestRender();
@@ -466,15 +479,7 @@ export class SelectorController {
466
479
  onCancel: () => {
467
480
  done();
468
481
  // Restore status line to saved settings
469
- this.ctx.statusLine.updateSettings({
470
- preset: settings.get("statusLine.preset"),
471
- leftSegments: settings.get("statusLine.leftSegments"),
472
- rightSegments: settings.get("statusLine.rightSegments"),
473
- separator: settings.get("statusLine.separator"),
474
- showHookStatus: settings.get("statusLine.showHookStatus"),
475
- sessionAccent: settings.get("statusLine.sessionAccent"),
476
- segmentOptions: settings.get("statusLine.segmentOptions"),
477
- });
482
+ this.ctx.statusLine.updateSettings(buildStatusLineSettings(settings));
478
483
  this.ctx.updateEditorTopBorder();
479
484
  this.ctx.ui.requestRender();
480
485
  },
@@ -718,6 +723,7 @@ export class SelectorController {
718
723
  case "statusLineShowHooks":
719
724
  case "statusLine.showHookStatus":
720
725
  case "statusLine.sessionAccent":
726
+ case "statusLine.maxRows":
721
727
  case "statusLine.leftSegments":
722
728
  case "statusLine.rightSegments":
723
729
  case "statusLine.segmentOptions":
@@ -732,16 +738,7 @@ export class SelectorController {
732
738
  case "statusLineGitShowUntracked":
733
739
  case "statusLineTimeFormat":
734
740
  case "statusLineTimeShowSeconds": {
735
- const statusLineSettings = {
736
- preset: settings.get("statusLine.preset"),
737
- leftSegments: settings.get("statusLine.leftSegments"),
738
- rightSegments: settings.get("statusLine.rightSegments"),
739
- separator: settings.get("statusLine.separator"),
740
- showHookStatus: settings.get("statusLine.showHookStatus"),
741
- sessionAccent: settings.get("statusLine.sessionAccent"),
742
- segmentOptions: settings.get("statusLine.segmentOptions"),
743
- };
744
- this.ctx.statusLine.updateSettings(statusLineSettings);
741
+ this.ctx.statusLine.updateSettings(buildStatusLineSettings(settings));
745
742
  this.ctx.updateEditorTopBorder();
746
743
  this.ctx.ui.requestRender();
747
744
  break;
@@ -321,6 +321,7 @@ export class InteractiveMode implements InteractiveModeContext {
321
321
  }
322
322
  autoCompactionEscapeHandler?: () => void;
323
323
  retryEscapeHandler?: () => void;
324
+ retryEscapePrimed = false;
324
325
  retryCountdownTimer?: NodeJS.Timeout;
325
326
  unsubscribe?: () => void;
326
327
  onInputCallback?: (input: SubmittedUserInput) => void;
@@ -422,7 +423,7 @@ export class InteractiveMode implements InteractiveModeContext {
422
423
  this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
423
424
  this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
424
425
  this.editor.onAutocompleteCancel = () => {
425
- this.ui.requestRender(true);
426
+ this.ui.requestRender();
426
427
  };
427
428
  this.editor.onAutocompleteUpdate = () => {
428
429
  this.ui.requestRender();
@@ -432,7 +433,7 @@ export class InteractiveMode implements InteractiveModeContext {
432
433
  this.#syncEditorMaxHeight();
433
434
  this.updateEditorChrome();
434
435
  this.editor.invalidate();
435
- this.ui.requestRender(true, "resize");
436
+ this.ui.requestResizeRender();
436
437
  };
437
438
  process.stdout.on("resize", this.#resizeHandler);
438
439
  try {
@@ -530,6 +531,14 @@ export class InteractiveMode implements InteractiveModeContext {
530
531
  }
531
532
 
532
533
  if (!startupQuiet) {
534
+ const getWelcomeReservedBottomRows = (width: number): number =>
535
+ [
536
+ this.statusLine,
537
+ this.hookWidgetContainerAbove,
538
+ this.editorContainer,
539
+ this.hookWidgetContainerBelow,
540
+ ].reduce((rows, component) => rows + component.render(width).length, 0);
541
+
533
542
  // Add welcome header
534
543
  this.#welcomeComponent = new WelcomeComponent(
535
544
  this.#version,
@@ -538,30 +547,16 @@ export class InteractiveMode implements InteractiveModeContext {
538
547
  recentSessions,
539
548
  this.#getWelcomeLspServers(),
540
549
  welcomeLogoMode,
550
+ {
551
+ getViewportRows: () => this.ui.terminal.rows,
552
+ getReservedBottomRows: getWelcomeReservedBottomRows,
553
+ changelogMarkdown: this.#changelogMarkdown,
554
+ collapseChangelog: settings.get("collapseChangelog"),
555
+ },
541
556
  );
542
557
 
543
- // Setup UI layout
544
- this.ui.addChild(new Spacer(1));
545
558
  this.ui.addChild(this.#welcomeComponent);
546
- this.ui.addChild(new Spacer(1));
547
559
  this.#welcomeComponent.playIntro(() => this.ui.requestRender());
548
-
549
- // Add changelog if provided
550
- if (this.#changelogMarkdown) {
551
- this.ui.addChild(new DynamicBorder());
552
- if (settings.get("collapseChangelog")) {
553
- const versionMatch = this.#changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
554
- const latestVersion = versionMatch ? versionMatch[1] : this.#version;
555
- const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
556
- this.ui.addChild(new Text(condensedText, 1, 0));
557
- } else {
558
- this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
559
- this.ui.addChild(new Spacer(1));
560
- this.ui.addChild(new Markdown(this.#changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
561
- this.ui.addChild(new Spacer(1));
562
- }
563
- this.ui.addChild(new DynamicBorder());
564
- }
565
560
  }
566
561
 
567
562
  this.ui.addChild(this.chatContainer);
@@ -1963,6 +1958,8 @@ export class InteractiveMode implements InteractiveModeContext {
1963
1958
  this.loadingAnimation.stop();
1964
1959
  this.loadingAnimation = undefined;
1965
1960
  }
1961
+ this.#welcomeComponent?.dispose();
1962
+ this.#welcomeComponent = undefined;
1966
1963
  this.#cleanupMicAnimation();
1967
1964
  this.#cancelGoalContinuation();
1968
1965
  if (this.#sttController) {
@@ -2076,7 +2073,7 @@ export class InteractiveMode implements InteractiveModeContext {
2076
2073
  nextEditor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
2077
2074
  nextEditor.setAutocompleteMaxVisible(this.settings.get("autocompleteMaxVisible"));
2078
2075
  nextEditor.onAutocompleteCancel = () => {
2079
- this.ui.requestRender(true);
2076
+ this.ui.requestRender();
2080
2077
  };
2081
2078
  nextEditor.onAutocompleteUpdate = () => {
2082
2079
  this.ui.requestRender();
@@ -435,6 +435,7 @@ export interface RpcJsonSchema {
435
435
  items?: RpcJsonSchema;
436
436
  minLength?: number;
437
437
  maxLength?: number;
438
+ pattern?: string;
438
439
  minItems?: number;
439
440
  maxItems?: number;
440
441
  uniqueItems?: boolean;
@@ -131,7 +131,12 @@ function questionAnswerSchema(question: AskGateQuestion, labels: string[]): RpcJ
131
131
  properties: {
132
132
  selected: selectedBase,
133
133
  other: { type: "boolean", description: "set true to provide a free-text answer in `custom`" },
134
- custom: { type: "string", minLength: 1, description: "free-text answer; required when `other` is true" },
134
+ custom: {
135
+ type: "string",
136
+ minLength: 1,
137
+ pattern: "\\S",
138
+ description: "free-text answer; required when `other` is true",
139
+ },
135
140
  action: {
136
141
  type: "string",
137
142
  enum: ["answer", "clarify"],
@@ -140,6 +145,7 @@ function questionAnswerSchema(question: AskGateQuestion, labels: string[]): RpcJ
140
145
  question: {
141
146
  type: "string",
142
147
  minLength: 1,
148
+ pattern: "\\S",
143
149
  description: "clarification question; required when action is `clarify`",
144
150
  },
145
151
  },
@@ -156,7 +162,7 @@ function questionAnswerSchema(question: AskGateQuestion, labels: string[]): RpcJ
156
162
  properties: {
157
163
  selected: selectedWithOther,
158
164
  other: { const: true },
159
- custom: { type: "string", minLength: 1 },
165
+ custom: { type: "string", minLength: 1, pattern: "\\S" },
160
166
  action: { const: "answer" },
161
167
  },
162
168
  required: ["selected", "other", "custom"],
@@ -166,7 +172,7 @@ function questionAnswerSchema(question: AskGateQuestion, labels: string[]): RpcJ
166
172
  type: "object",
167
173
  properties: {
168
174
  action: { const: "clarify" },
169
- question: { type: "string", minLength: 1 },
175
+ question: { type: "string", minLength: 1, pattern: "\\S" },
170
176
  },
171
177
  required: ["action", "question"],
172
178
  additionalProperties: false,
@@ -25,6 +25,7 @@ const SUPPORTED_KEYWORDS = new Set<keyof RpcJsonSchema>([
25
25
  "items",
26
26
  "minLength",
27
27
  "maxLength",
28
+ "pattern",
28
29
  "minItems",
29
30
  "maxItems",
30
31
  "uniqueItems",
@@ -107,6 +108,17 @@ function walkSchema(schema: RpcJsonSchema, depth: number, path: string): void {
107
108
  throw new WorkflowGateSchemaError(`${meta} at ${path} must be a string`);
108
109
  }
109
110
  }
111
+ if (schema.pattern !== undefined) {
112
+ if (typeof schema.pattern !== "string") {
113
+ throw new WorkflowGateSchemaError(`pattern at ${path} must be a string`);
114
+ }
115
+ try {
116
+ new RegExp(schema.pattern);
117
+ } catch (error) {
118
+ const message = error instanceof Error ? error.message : String(error);
119
+ throw new WorkflowGateSchemaError(`pattern at ${path} must be a valid regular expression: ${message}`);
120
+ }
121
+ }
110
122
  for (const limit of ["minLength", "maxLength", "minItems", "maxItems"] as const) {
111
123
  const v = schema[limit];
112
124
  if (v !== undefined && (typeof v !== "number" || !Number.isInteger(v) || v < 0)) {
@@ -242,6 +254,14 @@ function validateValue(schema: RpcJsonSchema, value: unknown, path: string, erro
242
254
  expected: schema.maxLength,
243
255
  });
244
256
  }
257
+ if (schema.pattern !== undefined && !new RegExp(schema.pattern).test(value)) {
258
+ errors.push({
259
+ path,
260
+ keyword: "pattern",
261
+ message: `does not match pattern ${schema.pattern}`,
262
+ expected: schema.pattern,
263
+ });
264
+ }
245
265
  }
246
266
  if (typeof value === "number") {
247
267
  if (schema.minimum !== undefined && value < schema.minimum) {
@@ -110,6 +110,7 @@ export interface InteractiveModeContext {
110
110
  retryLoader: Loader | undefined;
111
111
  autoCompactionEscapeHandler?: () => void;
112
112
  retryEscapeHandler?: () => void;
113
+ retryEscapePrimed: boolean;
113
114
  retryCountdownTimer?: NodeJS.Timeout;
114
115
  unsubscribe?: () => void;
115
116
  onInputCallback?: (input: SubmittedUserInput) => void;
@@ -26,7 +26,7 @@ import * as path from "node:path";
26
26
  import { promisify } from "node:util";
27
27
  import type { ImageContent, TextContent } from "@gajae-code/ai";
28
28
  import { NotificationServer } from "@gajae-code/natives";
29
- import { logger } from "@gajae-code/utils";
29
+ import { logger, postmortem } from "@gajae-code/utils";
30
30
  import { Settings } from "../config/settings";
31
31
  import type { ExtensionCommandContext, ExtensionContext, ExtensionFactory } from "../extensibility/extensions";
32
32
  import { registerAskAnswerSource } from "../tools/ask-answer-registry";
@@ -348,6 +348,8 @@ interface SessionRuntime {
348
348
  /** Assistant text already flushed before an ask this turn (turn-scoped dedupe
349
349
  * so turn_end does not re-emit the pre-ask lead-in). Reset each turn. */
350
350
  preAskFlushedText?: string;
351
+ /** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
352
+ cancelPostmortemCleanup: () => void;
351
353
  }
352
354
 
353
355
  interface ResolvedSettings {
@@ -500,6 +502,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
500
502
  if (!rt) return false;
501
503
  runtimes.delete(id);
502
504
  try {
505
+ rt.cancelPostmortemCleanup();
503
506
  rt.disposeAnswerSource();
504
507
  rt.disposeFileSink();
505
508
  } catch {}
@@ -680,6 +683,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
680
683
  pendingInteractive,
681
684
  disposeAnswerSource,
682
685
  disposeFileSink,
686
+ cancelPostmortemCleanup: () => {},
683
687
  redact,
684
688
  verbosity,
685
689
  sessionTag: tag,
@@ -687,6 +691,15 @@ export const createNotificationsExtension: ExtensionFactory = api => {
687
691
  pendingInbound: new Set<number>(),
688
692
  };
689
693
  runtimes.set(id, runtime);
694
+ // A native terminal close (SIGHUP), SIGTERM, Ctrl+C exit, or fatal error
695
+ // skips AgentSession.dispose(), so the `session_shutdown` extension event
696
+ // never fires and the daemon-side topic would be orphaned. postmortem
697
+ // awaits registered cleanups on those paths, so send the graceful
698
+ // `session_closed` frame from there too. stopSession() cancels this
699
+ // registration on every other teardown path, so it never double-fires.
700
+ runtime.cancelPostmortemCleanup = postmortem.register(`notifications-session-closed:${id}`, async () => {
701
+ await stopSession(id);
702
+ });
690
703
  logger.info(`notifications: serving session ${id} at ${endpoint.url} (unattended=${unattended})`);
691
704
 
692
705
  if (settingsAvailable && settings && isGloballyConfigured(cfg)) {
@@ -848,9 +861,15 @@ export const createNotificationsExtension: ExtensionFactory = api => {
848
861
  // Re-bind the interactive ask answer source: the ask tool resolves the
849
862
  // source by the current session id, which just changed.
850
863
  try {
864
+ rt.cancelPostmortemCleanup();
851
865
  rt.disposeAnswerSource();
852
866
  rt.disposeFileSink();
853
867
  } catch {}
868
+ // Follow the id change so a later process teardown closes the re-keyed
869
+ // session (the old closure captured the retired id).
870
+ rt.cancelPostmortemCleanup = postmortem.register(`notifications-session-closed:${newId}`, async () => {
871
+ await stopSession(newId);
872
+ });
854
873
  rt.disposeAnswerSource = registerInteractiveAnswerSource(
855
874
  newId,
856
875
  rt.server,
@@ -238,6 +238,7 @@ export function daemonResumeSession(env: NodeJS.ProcessEnv = process.env, opts:
238
238
  // restart, so an unknown or ambiguous prefix fails closed instead of
239
239
  // blindly spawning `gjc --resume <prefix>` against a non-authoritative id.
240
240
  let resumeId = target.sessionIdOrPrefix;
241
+ let resumeCwd = target.path;
241
242
  if (opts.sessionsRoot) {
242
243
  const saved = listRecentSessions({ sessionsRoot: opts.sessionsRoot, limit: 1000 });
243
244
  const prefixed = saved.filter(
@@ -249,11 +250,19 @@ export function daemonResumeSession(env: NodeJS.ProcessEnv = process.env, opts:
249
250
  if (resolved.length > 1) {
250
251
  return { ambiguous: resolved.map(s => ({ sessionId: s.sessionId, path: s.path })) };
251
252
  }
252
- resumeId = resolved[0]!.sessionId;
253
+ const selected = resolved[0]!;
254
+ resumeId = selected.sessionId;
255
+ resumeCwd = selected.path;
256
+ }
257
+ const resolvedResumeCwd = resumeCwd ? path.resolve(resumeCwd) : undefined;
258
+ const resumeCwdStat = resolvedResumeCwd ? fs.statSync(resolvedResumeCwd, { throwIfNoEntry: false }) : undefined;
259
+ if (!resumeCwdStat?.isDirectory()) {
260
+ throw new Error(`gjc_lifecycle_resume_cwd_unavailable: ${resolvedResumeCwd ?? "(missing)"}`);
253
261
  }
254
262
  const tmux = resolveGjcTmuxCommand(env);
255
263
  const name = tmuxSessionNameFor(resumeId);
256
- const command = `exec env GJC_TMUX_LAUNCHED=1 GJC_NOTIFICATIONS=1 gjc --resume ${shellQuote(resumeId)}`;
264
+ const cwdPrefix = resolvedResumeCwd ? `cd ${shellQuote(resolvedResumeCwd)} && ` : "";
265
+ const command = `${cwdPrefix}exec env GJC_TMUX_LAUNCHED=1 GJC_NOTIFICATIONS=1 gjc --resume ${shellQuote(resumeId)}`;
257
266
  const r = Bun.spawnSync([tmux, "new-session", "-d", "-s", name, "sh", "-c", command], {
258
267
  stdout: "pipe",
259
268
  stderr: "pipe",
@@ -261,7 +270,7 @@ export function daemonResumeSession(env: NodeJS.ProcessEnv = process.env, opts:
261
270
  });
262
271
  if (r.exitCode !== 0) throw new Error(r.stderr.toString().trim() || "gjc_lifecycle_resume_failed");
263
272
  const tgt = buildGjcTmuxExactOptionTarget(name);
264
- for (const cmd of buildGjcTmuxProfileCommands(tgt, env, { sessionId: resumeId })) {
273
+ for (const cmd of buildGjcTmuxProfileCommands(tgt, env, { sessionId: resumeId, project: resolvedResumeCwd })) {
265
274
  Bun.spawnSync([tmux, ...cmd.args], { stdout: "pipe", stderr: "pipe", env });
266
275
  }
267
276
  return {
@@ -61,10 +61,12 @@ Use for read-only plan critique. It approves only when execution can proceed wit
61
61
  <routing>
62
62
  - Clear, low-risk implementation request → implement directly with focused verification.
63
63
  - Do not invoke `deep-interview`, `ralplan`, `ultragoal`, `team`, or role agents for simple clear implementation requests; direct tools and the default launch path are enough.
64
+ - The runtime records a `workflow-intent-diff` CustomEntry for direct-path traceability; it does not participate in LLM context and is not a reason to slow down direct execution.
64
65
  - When a task is clear, bounded, and low-risk, the default action is to make the smallest correct change and verify it, not to interview, plan, open a durable ledger, or delegate.
65
66
  - Small verification needs do not make a task a planning workflow. Escalate only for real ambiguity, non-trivial architecture/sequence risk, durable multi-goal tracking, or useful coordinated workers.
67
+ - Root-cause phase schema is active only for contradiction, regression, or high-risk transition work; otherwise keep ordinary verification and do not add root-cause ceremony.
66
68
  - Vague requirements → use `deep-interview` before planning or execution.
67
- - Clear requirements but non-trivial architecture/sequence risk → use `ralplan` and stop at pending approval.
69
+ - Clear requirements but non-trivial architecture/sequence risk → use `ralplan --deliberate` and stop at pending approval.
68
70
  - Durable goal ledger needed → use `ultragoal`; if no approved plan exists, run `ralplan` first.
69
71
  - Approved work benefits from coordinated persistent workers → use `team`.
70
72
  - Large enough implementation work → delegate bounded slices to `executor` through the task/sub-agent tool when it improves quality or throughput.
@@ -139,6 +141,18 @@ Use tools whenever they materially improve correctness, completeness, or groundi
139
141
  </inventory>
140
142
  {{/if}}
141
143
 
144
+ {{#if toolDiscoveryActive}}
145
+ <tool-discovery>
146
+ Only essential tools are loaded up front; other tools are hidden to save context and are available on demand. Call `{{toolRefs.search_tool_bm25}}` with a short natural-language query to find and activate the tool you need, then call it. Prefer discovering a purpose-built tool over forcing the task onto a resident one.
147
+ {{#if discoverableTools.length}}
148
+ Discoverable tools:
149
+ {{#each discoverableTools}}
150
+ - `{{name}}`{{#if summary}} — {{summary}}{{/if}}
151
+ {{/each}}
152
+ {{/if}}
153
+ </tool-discovery>
154
+ {{/if}}
155
+
142
156
  <inputs>
143
157
  - Keep tool inputs concise where possible.
144
158
  - For `path` or path-like fields, prefer relative paths.
@@ -237,6 +237,34 @@ export async function listMCPServers(filePath: string): Promise<string[]> {
237
237
  return Object.keys(config.mcpServers ?? {});
238
238
  }
239
239
 
240
+ /**
241
+ * Set a server's autoload flag (connect automatically at session startup).
242
+ *
243
+ * Autoload defaults to true, so turning it on removes the key to keep the
244
+ * config file free of redundant fields; turning it off writes `autoload: false`.
245
+ *
246
+ * @throws Error if the server doesn't exist
247
+ */
248
+ export async function setServerAutoload(filePath: string, name: string, autoload: boolean): Promise<void> {
249
+ const existing = await readMCPConfigFile(filePath);
250
+ const server = existing.mcpServers?.[name];
251
+ if (!server) {
252
+ throw new Error(`Server "${name}" not found in ${filePath}`);
253
+ }
254
+
255
+ const { autoload: _removed, ...rest } = server;
256
+ const updatedServer = (autoload ? rest : { ...rest, autoload: false }) as MCPServerConfig;
257
+ const updated: MCPConfigFile = {
258
+ ...existing,
259
+ mcpServers: {
260
+ ...existing.mcpServers,
261
+ [name]: updatedServer,
262
+ },
263
+ };
264
+
265
+ await writeMCPConfigFile(filePath, updated);
266
+ }
267
+
240
268
  /**
241
269
  * Read the disabled servers list from a config file.
242
270
  */
@@ -20,6 +20,8 @@ export interface LoadMCPConfigsOptions {
20
20
  filterExa?: boolean;
21
21
  /** Whether to filter out browser MCP servers when builtin browser tool is enabled (default: false) */
22
22
  filterBrowser?: boolean;
23
+ /** Only include servers eligible for startup connection, i.e. autoload !== false (default: false) */
24
+ autoloadOnly?: boolean;
23
25
  }
24
26
 
25
27
  /** Result of loading MCP configs */
@@ -40,6 +42,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
40
42
  const transport = server.transport ?? (server.command ? "stdio" : server.url ? "http" : "stdio");
41
43
  const shared = {
42
44
  enabled: server.enabled,
45
+ autoload: server.autoload,
43
46
  timeout: server.timeout,
44
47
  auth: server.auth,
45
48
  oauth: server.oauth,
@@ -96,6 +99,7 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
96
99
  const enableProjectConfig = options?.enableProjectConfig ?? true;
97
100
  const filterExa = options?.filterExa ?? true;
98
101
  const filterBrowser = options?.filterBrowser ?? false;
102
+ const autoloadOnly = options?.autoloadOnly ?? false;
99
103
 
100
104
  // Load MCP servers via capability system
101
105
  const result = await loadCapability<MCPServer>(mcpCapability.id, { cwd });
@@ -115,6 +119,9 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
115
119
  if (config.enabled === false || disabledServers.has(server.name)) {
116
120
  continue;
117
121
  }
122
+ if (autoloadOnly && config.autoload === false) {
123
+ continue;
124
+ }
118
125
  configs[server.name] = config;
119
126
  sources[server.name] = server._source;
120
127
  }
@@ -34,6 +34,8 @@ export interface MCPToolsLoadOptions {
34
34
  filterExa?: boolean;
35
35
  /** Whether to filter out browser MCP servers when builtin browser tool is enabled (default: false) */
36
36
  filterBrowser?: boolean;
37
+ /** Only connect servers with autoload !== false (default: false) */
38
+ autoloadOnly?: boolean;
37
39
  /** SQLite storage for MCP tool cache (null disables cache) */
38
40
  cacheStorage?: AgentStorage | null;
39
41
  /** Auth storage used to resolve OAuth credentials before initial MCP connect */
@@ -72,6 +74,7 @@ export async function discoverAndLoadMCPTools(cwd: string, options?: MCPToolsLoa
72
74
  enableProjectConfig: options?.enableProjectConfig,
73
75
  filterExa: options?.filterExa,
74
76
  filterBrowser: options?.filterBrowser,
77
+ autoloadOnly: options?.autoloadOnly,
75
78
  });
76
79
  } catch (error) {
77
80
  // If discovery fails entirely, return empty result
@@ -147,6 +147,8 @@ export interface MCPDiscoverOptions {
147
147
  filterExa?: boolean;
148
148
  /** Whether to filter out browser MCP servers when builtin browser tool is enabled (default: false) */
149
149
  filterBrowser?: boolean;
150
+ /** Only connect servers with autoload !== false (default: false) */
151
+ autoloadOnly?: boolean;
150
152
  /** Called when starting to connect to servers */
151
153
  onConnecting?: (serverNames: string[]) => void;
152
154
  }
@@ -325,6 +327,7 @@ export class MCPManager {
325
327
  enableProjectConfig: options?.enableProjectConfig,
326
328
  filterExa: options?.filterExa,
327
329
  filterBrowser: options?.filterBrowser,
330
+ autoloadOnly: options?.autoloadOnly,
328
331
  });
329
332
  const result = await this.connectServers(configs, sources, options?.onConnecting);
330
333
  result.exaApiKeys = exaApiKeys;
@@ -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 =