@gajae-code/coding-agent 0.7.10 → 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 (148) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +2 -2
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/config/keybindings.d.ts +11 -5
  5. package/dist/types/config/settings-schema.d.ts +25 -2
  6. package/dist/types/defaults/gjc-defaults.d.ts +7 -0
  7. package/dist/types/discovery/cursor.d.ts +4 -1
  8. package/dist/types/discovery/index.d.ts +0 -1
  9. package/dist/types/discovery/windsurf.d.ts +3 -1
  10. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  11. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +3 -2
  12. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +1 -0
  13. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +5 -0
  14. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  15. package/dist/types/modes/components/custom-editor.d.ts +6 -1
  16. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  17. package/dist/types/modes/components/skill-message.d.ts +1 -0
  18. package/dist/types/modes/components/status-line.d.ts +8 -0
  19. package/dist/types/modes/components/two-column-body.d.ts +9 -0
  20. package/dist/types/modes/components/welcome.d.ts +9 -1
  21. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  22. package/dist/types/modes/controllers/input-controller.d.ts +5 -1
  23. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  24. package/dist/types/modes/interactive-mode.d.ts +2 -0
  25. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -0
  26. package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
  27. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  28. package/dist/types/modes/types.d.ts +2 -0
  29. package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
  30. package/dist/types/runtime-mcp/config.d.ts +2 -0
  31. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  32. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  33. package/dist/types/runtime-mcp/types.d.ts +6 -0
  34. package/dist/types/sdk.d.ts +1 -1
  35. package/dist/types/session/agent-session.d.ts +8 -5
  36. package/dist/types/slash-commands/types.d.ts +2 -0
  37. package/dist/types/system-prompt.d.ts +7 -0
  38. package/dist/types/utils/cmux-workspace.d.ts +30 -1
  39. package/dist/types/web/search/index.d.ts +3 -1
  40. package/dist/types/web/search/provider.d.ts +9 -0
  41. package/dist/types/web/search/providers/utils.d.ts +42 -13
  42. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  43. package/package.json +10 -10
  44. package/src/capability/mcp.ts +2 -0
  45. package/src/cli/update-cli.ts +24 -0
  46. package/src/cli/web-search-cli.ts +2 -5
  47. package/src/cli.ts +1 -1
  48. package/src/commands/launch.ts +1 -1
  49. package/src/commands/ralplan.ts +1 -0
  50. package/src/config/keybindings.ts +14 -5
  51. package/src/config/mcp-schema.json +5 -1
  52. package/src/config/model-profiles.ts +16 -0
  53. package/src/config/settings-schema.ts +18 -2
  54. package/src/coordinator-mcp/server.ts +15 -3
  55. package/src/defaults/gjc/skills/ralplan/SKILL.md +16 -14
  56. package/src/defaults/gjc-defaults.ts +16 -0
  57. package/src/discovery/builtin.ts +12 -0
  58. package/src/discovery/cursor.ts +5 -84
  59. package/src/discovery/gemini.ts +3 -89
  60. package/src/discovery/index.ts +4 -1
  61. package/src/discovery/mcp-json.ts +11 -0
  62. package/src/discovery/opencode.ts +5 -122
  63. package/src/discovery/windsurf.ts +4 -82
  64. package/src/edit/read-file.ts +9 -1
  65. package/src/extensibility/extensions/runner.ts +2 -1
  66. package/src/gjc-runtime/launch-tmux.ts +164 -42
  67. package/src/gjc-runtime/ralplan-runtime.ts +22 -6
  68. package/src/gjc-runtime/restricted-role-agent-bash.ts +1 -0
  69. package/src/gjc-runtime/session-state-sidecar.ts +167 -24
  70. package/src/gjc-runtime/state-runtime.ts +8 -5
  71. package/src/gjc-runtime/state-writer.ts +5 -5
  72. package/src/gjc-runtime/team-runtime.ts +56 -51
  73. package/src/internal-urls/docs-index.generated.ts +8 -7
  74. package/src/main.ts +2 -2
  75. package/src/migrate/executor.ts +23 -6
  76. package/src/modes/components/agent-dashboard.ts +1 -34
  77. package/src/modes/components/custom-editor.ts +26 -0
  78. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  79. package/src/modes/components/model-selector.ts +64 -4
  80. package/src/modes/components/session-selector.ts +19 -9
  81. package/src/modes/components/settings-selector.ts +13 -2
  82. package/src/modes/components/skill-message.ts +18 -10
  83. package/src/modes/components/status-line.ts +215 -86
  84. package/src/modes/components/tree-selector.ts +28 -8
  85. package/src/modes/components/two-column-body.ts +36 -0
  86. package/src/modes/components/welcome.ts +171 -35
  87. package/src/modes/controllers/command-controller.ts +23 -0
  88. package/src/modes/controllers/event-controller.ts +4 -0
  89. package/src/modes/controllers/input-controller.ts +192 -39
  90. package/src/modes/controllers/selector-controller.ts +25 -28
  91. package/src/modes/interactive-mode.ts +36 -30
  92. package/src/modes/prompt-action-autocomplete.ts +23 -1
  93. package/src/modes/rpc/rpc-types.ts +1 -0
  94. package/src/modes/shared/agent-wire/deep-interview-gate.ts +9 -3
  95. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  96. package/src/modes/slash-command-visibility.ts +9 -0
  97. package/src/modes/types.ts +2 -0
  98. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  99. package/src/modes/utils/ui-helpers.ts +76 -3
  100. package/src/notifications/index.ts +20 -1
  101. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  102. package/src/notifications/telegram-daemon.ts +4 -4
  103. package/src/prompts/agents/architect.md +7 -4
  104. package/src/prompts/agents/critic.md +15 -6
  105. package/src/prompts/agents/planner.md +8 -3
  106. package/src/prompts/system/system-prompt.md +15 -1
  107. package/src/prompts/tools/bash.md +1 -1
  108. package/src/runtime-mcp/config-writer.ts +28 -0
  109. package/src/runtime-mcp/config.ts +7 -0
  110. package/src/runtime-mcp/loader.ts +3 -0
  111. package/src/runtime-mcp/manager.ts +3 -0
  112. package/src/runtime-mcp/types.ts +6 -0
  113. package/src/sdk.ts +7 -6
  114. package/src/session/agent-session.ts +81 -18
  115. package/src/session/session-manager.ts +58 -18
  116. package/src/slash-commands/builtin-registry.ts +30 -10
  117. package/src/slash-commands/types.ts +2 -0
  118. package/src/system-prompt.ts +8 -0
  119. package/src/task/executor.ts +5 -0
  120. package/src/tools/ast-grep.ts +10 -7
  121. package/src/tools/bash.ts +39 -14
  122. package/src/tools/index.ts +8 -1
  123. package/src/utils/cmux-workspace.ts +134 -7
  124. package/src/utils/title-generator.ts +1 -1
  125. package/src/web/kagi.ts +1 -1
  126. package/src/web/parallel.ts +2 -2
  127. package/src/web/search/index.ts +118 -45
  128. package/src/web/search/provider.ts +65 -0
  129. package/src/web/search/providers/anthropic.ts +1 -1
  130. package/src/web/search/providers/brave.ts +1 -1
  131. package/src/web/search/providers/codex.ts +1 -1
  132. package/src/web/search/providers/duckduckgo.ts +1 -1
  133. package/src/web/search/providers/exa.ts +1 -1
  134. package/src/web/search/providers/gemini.ts +7 -3
  135. package/src/web/search/providers/jina.ts +1 -1
  136. package/src/web/search/providers/kimi.ts +6 -1
  137. package/src/web/search/providers/openai-compatible.ts +1 -1
  138. package/src/web/search/providers/parallel.ts +1 -1
  139. package/src/web/search/providers/perplexity.ts +2 -2
  140. package/src/web/search/providers/searxng.ts +1 -1
  141. package/src/web/search/providers/synthetic.ts +1 -1
  142. package/src/web/search/providers/tavily.ts +1 -1
  143. package/src/web/search/providers/utils.ts +67 -17
  144. package/src/web/search/providers/xai.ts +1 -1
  145. package/src/web/search/providers/zai.ts +1 -1
  146. package/src/workflow/workflow-intent-diff.ts +199 -0
  147. package/dist/types/discovery/vscode.d.ts +0 -1
  148. package/src/discovery/vscode.ts +0 -105
@@ -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) {
@@ -0,0 +1,9 @@
1
+ const BEGINNER_HIDDEN_EXTENSION_COMMANDS = new Set(["grok-build-usage"]);
2
+
3
+ export function shouldShowExtensionCommand(name: string, activeProvider: string | undefined): boolean {
4
+ if (!BEGINNER_HIDDEN_EXTENSION_COMMANDS.has(name)) return true;
5
+ if (name === "grok-build-usage") {
6
+ return activeProvider === "grok-build" || Boolean(process.env.GROK_CLI_OAUTH_TOKEN);
7
+ }
8
+ return true;
9
+ }
@@ -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;
@@ -209,6 +210,7 @@ export interface InteractiveModeContext {
209
210
  handleUsageCommand(reports?: UsageReport[] | null): Promise<void>;
210
211
  handleChangelogCommand(showFull?: boolean): Promise<void>;
211
212
  handleHotkeysCommand(): void;
213
+ handleHelpCommand(): void;
212
214
  handleToolsCommand(): void;
213
215
  handleContextCommand(): void;
214
216
  handleDumpCommand(): void;
@@ -38,6 +38,8 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
38
38
  `| \`${appKey(bindings, "app.clear")}\` | Clear editor (first) / exit (second) |`,
39
39
  `| \`${appKey(bindings, "app.exit")}\` | Exit (when editor is empty) |`,
40
40
  `| \`${appKey(bindings, "app.suspend")}\` | Suspend to background |`,
41
+ `| \`${appKey(bindings, "app.commandPalette.open")}\` | Open command palette |`,
42
+ `| \`${appKey(bindings, "app.session.new")}\` | Start a fresh session |`,
41
43
  `| \`${appKey(bindings, "app.thinking.cycle")}\` | Cycle thinking level |`,
42
44
  `| \`${appKey(bindings, "app.model.cycleForward")}\` | Cycle configured model roles |`,
43
45
  `| \`${appKey(bindings, "app.model.cycleBackward")}\` | Cycle configured model roles temporarily |`,
@@ -51,8 +53,9 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
51
53
  `| \`${appKey(bindings, "app.editor.external")}\` | Edit message in external editor |`,
52
54
  `| \`${appKey(bindings, "app.clipboard.pasteImage")}\` | Paste image from clipboard |`,
53
55
  `| \`${appKey(bindings, "app.stt.toggle")}\` | Toggle speech-to-text recording |`,
54
- "| `#` | Open prompt actions |",
55
- "| `/` | Slash commands |",
56
+ "| `?` | Show help / shortcuts |",
57
+ "| `#` | Prompt actions (command-palette style actions) |",
58
+ "| `/` | Slash commands (try `/help` or `/new`) |",
56
59
  "| `!` | Run bash command |",
57
60
  "| `!!` | Run bash command (excluded from context) |",
58
61
  "| `$` | Run Python in shared kernel |",
@@ -2,6 +2,8 @@ import type { AgentMessage } from "@gajae-code/agent-core";
2
2
  import type { AssistantMessage, ImageContent, Message } from "@gajae-code/ai";
3
3
  import { type Component, Spacer, Text, TruncatedText } from "@gajae-code/tui";
4
4
  import { settings } from "../../config/settings";
5
+ import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
6
+ import { buildSkillPromptMessage, parseSkillInvocations } from "../../extensibility/skills";
5
7
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
6
8
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
7
9
  import { BranchSummaryMessageComponent } from "../../modes/components/branch-summary-message";
@@ -610,9 +612,80 @@ export class UiHelpers {
610
612
  this.ctx.updatePendingMessagesDisplay();
611
613
  this.ctx.showStatus("Queued message for after compaction");
612
614
  }
615
+ #hasSkillInvocations(text: string): boolean {
616
+ return parseSkillInvocations(text, this.ctx.skillCommands ?? new Map()).length > 0;
617
+ }
618
+
619
+ #isCompactionCommandMessage(text: string): boolean {
620
+ return this.#hasSkillInvocations(text) || this.isKnownSlashCommand(text);
621
+ }
622
+
623
+ async #deliverQueuedSkillMessage(message: CompactionQueuedMessage): Promise<boolean> {
624
+ const invocations = parseSkillInvocations(message.text, this.ctx.skillCommands ?? new Map());
625
+ if (invocations.length === 0) {
626
+ return false;
627
+ }
628
+
629
+ for (let index = 0; index < invocations.length; index += 1) {
630
+ const invocation = invocations[index];
631
+ if (!invocation) continue;
632
+
633
+ const activationResult = await resolveSubskillActivationForSkillInvocation({
634
+ cwd: this.ctx.sessionManager.getCwd(),
635
+ sessionId: this.ctx.session.sessionId,
636
+ skillName: invocation.skill.name,
637
+ args: invocation.args,
638
+ });
639
+ const built = await buildSkillPromptMessage(invocation.skill, activationResult.cleanedArgs, {
640
+ subskillActivation: activationResult.activation,
641
+ subskillActivationSet: activationResult.activeSubskillsToPersist,
642
+ cwd: this.ctx.sessionManager.getCwd(),
643
+ sessionId: this.ctx.session.sessionId,
644
+ });
645
+ const details: SkillPromptDetails = built.details;
646
+ const displayText = `/${invocation.commandName}${activationResult.cleanedArgs ? ` ${activationResult.cleanedArgs}` : ""}`;
647
+
648
+ if (this.ctx.session.isStreaming) {
649
+ const tag = this.ctx.session.enqueueCustomMessageDisplay(displayText, message.mode);
650
+ details.__pendingDisplayTag = tag;
651
+ }
652
+
653
+ const isLast = index === invocations.length - 1;
654
+ if (!this.ctx.session.isStreaming && !isLast) {
655
+ await this.ctx.session.sendCustomMessage({
656
+ customType: SKILL_PROMPT_MESSAGE_TYPE,
657
+ content: built.message,
658
+ display: true,
659
+ details,
660
+ attribution: "user",
661
+ });
662
+ continue;
663
+ }
613
664
 
665
+ await this.ctx.session.promptCustomMessage(
666
+ {
667
+ customType: SKILL_PROMPT_MESSAGE_TYPE,
668
+ content: built.message,
669
+ display: true,
670
+ details,
671
+ attribution: "user",
672
+ },
673
+ { streamingBehavior: message.mode },
674
+ );
675
+ }
676
+
677
+ if (this.ctx.session.isStreaming) {
678
+ this.ctx.updatePendingMessagesDisplay();
679
+ this.ctx.ui.requestRender();
680
+ }
681
+
682
+ return true;
683
+ }
614
684
  async #deliverQueuedMessage(message: CompactionQueuedMessage): Promise<void> {
615
- if (this.ctx.isKnownSlashCommand(message.text)) {
685
+ if (await this.#deliverQueuedSkillMessage(message)) {
686
+ return;
687
+ }
688
+ if (this.isKnownSlashCommand(message.text)) {
616
689
  await this.ctx.session.prompt(message.text);
617
690
  return;
618
691
  }
@@ -671,14 +744,14 @@ export class UiHelpers {
671
744
 
672
745
  let firstPromptIndex = -1;
673
746
  for (let i = 0; i < queuedMessages.length; i++) {
674
- if (!this.ctx.isKnownSlashCommand(queuedMessages[i].text)) {
747
+ if (!this.#isCompactionCommandMessage(queuedMessages[i].text)) {
675
748
  firstPromptIndex = i;
676
749
  break;
677
750
  }
678
751
  }
679
752
  if (firstPromptIndex === -1) {
680
753
  for (const message of queuedMessages) {
681
- await this.ctx.session.prompt(message.text);
754
+ await this.#deliverQueuedMessage(message);
682
755
  }
683
756
  return;
684
757
  }
@@ -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 {
@@ -13,7 +13,7 @@ import { parseInThreadConfigCommand } from "./config-commands";
13
13
  import { daemonPaths } from "./daemon-paths";
14
14
  import {
15
15
  buildCompactChoiceGrid,
16
- pre,
16
+ code,
17
17
  splitTelegramHtml,
18
18
  TELEGRAM_MESSAGE_LIMIT,
19
19
  TELEGRAM_PARSE_MODE,
@@ -1117,10 +1117,10 @@ export class TelegramNotificationDaemon {
1117
1117
  limit: 10,
1118
1118
  includeInternal: false,
1119
1119
  });
1120
- const lines = recent.length
1121
- ? recent.map(e => `• ${e.sessionId}${e.path ? ` (${e.path})` : ""}`).join("\n")
1120
+ const body = recent.length
1121
+ ? recent.map(e => `• ${code(e.sessionId)}${e.path ? ` (${code(e.path)})` : ""}`).join("\n")
1122
1122
  : "No recent sessions.";
1123
- await replyHtml(pre(lines));
1123
+ await replyHtml(body);
1124
1124
  return true;
1125
1125
  }
1126
1126
 
@@ -25,7 +25,7 @@ You may receive a forked parent-conversation snapshot as background. Your read-o
25
25
 
26
26
  <constraints>
27
27
  - Read-only: never write, edit, format, commit, push, or mutate files.
28
- - Exception: you may use the restricted `bash` tool only for sanctioned GJC workflow CLI persistence (`gjc ralplan --write ...`) and GJC workflow state read/write/contract commands (`gjc state ...`). For `gjc ralplan --write`, pass the verdict markdown inline in `--artifact`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
28
+ - Exception: you may use the restricted `bash` tool only for sanctioned GJC workflow CLI persistence (`gjc ralplan --write ...`) and GJC workflow state read/write/contract commands (`gjc state ...`). For `gjc ralplan --write`, pass the verdict markdown through the `GJC_RALPLAN_ARTIFACT` env override and `--artifact-env GJC_RALPLAN_ARTIFACT`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
29
29
  - Never approve code or plans you have not grounded in inspected files.
30
30
  - Never give generic advice detached from this codebase.
31
31
  - Never approve CRITICAL or HIGH severity issues.
@@ -63,6 +63,9 @@ A narrow compatibility fallback can be acceptable only when it is scoped to a kn
63
63
  ## Summary
64
64
  2-3 sentences with result and main recommendation.
65
65
 
66
+ ## Claims
67
+ Evidence-backed claims being reviewed or introduced.
68
+
66
69
  ## Analysis
67
70
  Evidence-backed findings.
68
71
 
@@ -81,12 +84,12 @@ Prioritized concrete actions.
81
84
  ## Code Review Recommendation
82
85
  `APPROVE` / `COMMENT` / `REQUEST CHANGES`
83
86
 
84
- ## Trade-offs
87
+ ## Tradeoffs
85
88
  Table or bullets comparing viable options when relevant.
86
89
 
87
- Persist this full review as the durable artifact via the restricted bash CLI, passing the markdown inline (never a file path, never `/tmp`):
90
+ Persist this full review as the durable artifact via the restricted bash CLI, passing the markdown through the `GJC_RALPLAN_ARTIFACT` env override (never a file path, never `/tmp`):
88
91
 
89
- gjc ralplan --write --stage architect --stage_n <N> --artifact "<full review markdown>" --json
92
+ gjc ralplan --write --stage architect --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json
90
93
 
91
94
  Then return to the caller ONLY the write receipt (`run_id`, `path`, `sha256`, `stage`, `stage_n`) plus the compact verdict (Architectural Status + Code Review Recommendation). Never paste the full review body back into your response — the caller reads the persisted artifact when it needs the full text.
92
95
  </output_contract>
@@ -17,7 +17,7 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
17
17
 
18
18
  <constraints>
19
19
  - Read-only: do not write, edit, format, commit, push, or mutate files.
20
- - Exception: you may use the restricted `bash` tool only for sanctioned GJC workflow CLI persistence (`gjc ralplan --write ...`) and GJC workflow state read/write/contract commands (`gjc state ...`). For `gjc ralplan --write`, pass the evaluation markdown inline in `--artifact`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
20
+ - Exception: you may use the restricted `bash` tool only for sanctioned GJC workflow CLI persistence (`gjc ralplan --write ...`) and GJC workflow state read/write/contract commands (`gjc state ...`). For `gjc ralplan --write`, pass the evaluation markdown through the `GJC_RALPLAN_ARTIFACT` env override and `--artifact-env GJC_RALPLAN_ARTIFACT`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
21
21
  - A lone file path is valid input; read and evaluate it.
22
22
  - Reject YAML-only plans as invalid plan format when a human-readable plan is required.
23
23
  - Do not invent problems; report no issues found when the plan passes.
@@ -42,11 +42,19 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
42
42
  </success_criteria>
43
43
 
44
44
  <output_contract>
45
+ ## Verdict
45
46
  **[OKAY / ITERATE / REJECT]**
46
47
 
47
- **Justification**: concise evidence-backed explanation.
48
+ ## Claim Checks
49
+ Concise evidence-backed explanation of verified claims.
48
50
 
49
- **Summary**:
51
+ ## Missing Evidence
52
+ Definitely missing or unverified evidence, or `None`.
53
+
54
+ ## Approval Boundary
55
+ What execution may proceed with, and what remains outside approval.
56
+
57
+ ## Summary
50
58
  - Clarity
51
59
  - Verifiability
52
60
  - Completeness
@@ -55,11 +63,12 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
55
63
  - Alternatives Depth
56
64
  - Risk/Verification Rigor
57
65
 
58
- If not OKAY, list concrete required fixes.
66
+ ## Required Changes
67
+ If not OKAY, list concrete required fixes; otherwise write `None`.
59
68
 
60
- Persist this full evaluation as the durable artifact via the restricted bash CLI, passing the markdown inline (never a file path, never `/tmp`):
69
+ Persist this full evaluation as the durable artifact via the restricted bash CLI, passing the markdown through the `GJC_RALPLAN_ARTIFACT` env override (never a file path, never `/tmp`):
61
70
 
62
- gjc ralplan --write --stage critic --stage_n <N> --artifact "<full evaluation markdown>" --json
71
+ gjc ralplan --write --stage critic --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json
63
72
 
64
73
  Then return to the caller ONLY the write receipt (`run_id`, `path`, `sha256`, `stage`, `stage_n`) plus the compact verdict (OKAY / ITERATE / REJECT). Never paste the full evaluation body back into your response — the caller reads the persisted artifact when it needs the full text.
65
74
  </output_contract>
@@ -17,7 +17,7 @@ Leave execution with a right-sized, evidence-grounded plan: scope, steps, accept
17
17
 
18
18
  <constraints>
19
19
  - Read-only: never write, edit, format, commit, push, or mutate files.
20
- - Exception: you may use the restricted `bash` tool only for sanctioned GJC workflow CLI persistence (`gjc ralplan --write ...`) and GJC workflow state read/write/contract commands (`gjc state ...`). For `gjc ralplan --write`, pass the plan markdown inline in `--artifact`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
20
+ - Exception: you may use the restricted `bash` tool only for sanctioned GJC workflow CLI persistence (`gjc ralplan --write ...`) and GJC workflow state read/write/contract commands (`gjc state ...`). For `gjc ralplan --write`, pass the plan markdown through the `GJC_RALPLAN_ARTIFACT` env override and `--artifact-env GJC_RALPLAN_ARTIFACT`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
21
21
  - Persist durable plans only through `gjc ralplan --write`. Never write plan files to `/tmp`, the repository, or any other path, and never rely on a file the caller must read back. The CLI is your only persistence channel.
22
22
  - Inspect the repository before asking about code facts.
23
23
  - Ask only about priorities, tradeoffs, scope decisions, timelines, or preferences that repository inspection cannot resolve.
@@ -45,16 +45,21 @@ Leave execution with a right-sized, evidence-grounded plan: scope, steps, accept
45
45
  <output_contract>
46
46
  Build the full plan as a single markdown document containing:
47
47
  - Summary
48
+ - Intent Diff
49
+ - Decision Drivers
50
+ - Options
48
51
  - In scope / out of scope
49
52
  - File-level changes
50
53
  - Sequencing and dependencies
51
54
  - Acceptance criteria
52
55
  - Verification
56
+ - Escalation/Risk Gate
57
+ - Verification Plan
53
58
  - Risks and mitigations
54
59
 
55
- Persist that markdown as the durable artifact via the restricted bash CLI, passing the plan inline (never a file path, never `/tmp`):
60
+ Persist that markdown as the durable artifact via the restricted bash CLI, passing the plan through the `GJC_RALPLAN_ARTIFACT` env override (never a file path, never `/tmp`):
56
61
 
57
- gjc ralplan --write --stage planner --stage_n <N> --artifact "<full plan markdown>" --json
62
+ gjc ralplan --write --stage planner --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json
58
63
 
59
64
  Then return to the caller ONLY the write receipt (`run_id`, `path`, `sha256`, `stage`, `stage_n`) plus a compact plan summary (<=10 lines). Never paste the full plan body back into your response — the caller reads the persisted artifact when it needs the full text.
60
65
  </output_contract>
@@ -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.
@@ -33,7 +33,7 @@ This session's bash tool is restricted. It only accepts commands beginning with:
33
33
  {{#each restrictedAllowedPrefixes}}
34
34
  - `{{this}}`
35
35
  {{/each}}
36
- Use it only for sanctioned GJC workflow CLI persistence or state read/write/contract operations; per-command env overrides and all other shell command shapes are blocked before execution.
36
+ Use it only for sanctioned GJC workflow CLI persistence or state read/write/contract operations; the only per-command env override allowed is `GJC_RALPLAN_ARTIFACT` when paired with `gjc ralplan --write ... --artifact-env GJC_RALPLAN_ARTIFACT`, and all other shell command shapes are blocked before execution.
37
37
  {{/when}}
38
38
  </restricted-bash-mode>
39
39
  {{/if}}
@@ -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) */