@gajae-code/coding-agent 0.3.1 → 0.4.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 (166) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +6 -0
  5. package/dist/types/config/model-profile-activation.d.ts +30 -0
  6. package/dist/types/config/model-profiles.d.ts +19 -0
  7. package/dist/types/config/model-registry.d.ts +25 -10
  8. package/dist/types/config/model-resolver.d.ts +1 -1
  9. package/dist/types/config/models-config-schema.d.ts +84 -0
  10. package/dist/types/config/settings-schema.d.ts +15 -0
  11. package/dist/types/edit/diff.d.ts +16 -0
  12. package/dist/types/edit/modes/replace.d.ts +7 -0
  13. package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
  14. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  15. package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
  16. package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
  17. package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
  18. package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
  19. package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
  20. package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
  21. package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
  22. package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
  23. package/dist/types/extensibility/skills.d.ts +9 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
  25. package/dist/types/harness-control-plane/storage.d.ts +7 -0
  26. package/dist/types/lsp/client.d.ts +1 -0
  27. package/dist/types/main.d.ts +10 -1
  28. package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
  29. package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
  30. package/dist/types/modes/components/model-selector.d.ts +6 -1
  31. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  32. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  33. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
  34. package/dist/types/modes/rpc/rpc-client.d.ts +9 -1
  35. package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
  36. package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
  37. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
  38. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
  39. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
  40. package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
  41. package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
  42. package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
  43. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
  44. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
  45. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
  46. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
  47. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
  48. package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
  49. package/dist/types/modes/theme/theme.d.ts +2 -1
  50. package/dist/types/modes/types.d.ts +1 -0
  51. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  52. package/dist/types/sdk.d.ts +8 -1
  53. package/dist/types/session/agent-session.d.ts +10 -0
  54. package/dist/types/session/blob-store.d.ts +17 -0
  55. package/dist/types/session/messages.d.ts +3 -0
  56. package/dist/types/session/session-storage.d.ts +6 -0
  57. package/dist/types/skill-state/active-state.d.ts +13 -0
  58. package/dist/types/task/executor.d.ts +1 -0
  59. package/dist/types/thinking.d.ts +3 -2
  60. package/dist/types/tools/hindsight-recall.d.ts +0 -2
  61. package/dist/types/tools/hindsight-reflect.d.ts +0 -2
  62. package/dist/types/tools/hindsight-retain.d.ts +0 -2
  63. package/dist/types/tools/index.d.ts +7 -4
  64. package/package.json +9 -7
  65. package/src/cli/args.ts +10 -0
  66. package/src/cli.ts +14 -0
  67. package/src/commands/harness.ts +192 -7
  68. package/src/commands/launch.ts +8 -0
  69. package/src/commands/ultragoal.ts +1 -21
  70. package/src/config/model-equivalence.ts +1 -1
  71. package/src/config/model-profile-activation.ts +157 -0
  72. package/src/config/model-profiles.ts +155 -0
  73. package/src/config/model-registry.ts +51 -5
  74. package/src/config/model-resolver.ts +3 -2
  75. package/src/config/models-config-schema.ts +42 -1
  76. package/src/config/settings-schema.ts +14 -1
  77. package/src/defaults/gjc/skills/ultragoal/SKILL.md +11 -1
  78. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
  79. package/src/defaults/gjc-defaults.ts +7 -0
  80. package/src/discovery/claude-plugins.ts +25 -5
  81. package/src/edit/diff.ts +64 -1
  82. package/src/edit/modes/replace.ts +60 -2
  83. package/src/extensibility/gjc-plugins/activation.ts +87 -0
  84. package/src/extensibility/gjc-plugins/index.ts +9 -0
  85. package/src/extensibility/gjc-plugins/injection.ts +114 -0
  86. package/src/extensibility/gjc-plugins/loader.ts +131 -0
  87. package/src/extensibility/gjc-plugins/paths.ts +66 -0
  88. package/src/extensibility/gjc-plugins/schema.ts +79 -0
  89. package/src/extensibility/gjc-plugins/state.ts +29 -0
  90. package/src/extensibility/gjc-plugins/tools.ts +47 -0
  91. package/src/extensibility/gjc-plugins/types.ts +97 -0
  92. package/src/extensibility/gjc-plugins/validation.ts +76 -0
  93. package/src/extensibility/skills.ts +39 -7
  94. package/src/gjc-runtime/state-runtime.ts +93 -2
  95. package/src/gjc-runtime/state-writer.ts +17 -1
  96. package/src/gjc-runtime/ultragoal-runtime.ts +62 -2
  97. package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
  98. package/src/gjc-runtime/workflow-manifest.ts +2 -2
  99. package/src/harness-control-plane/storage.ts +144 -2
  100. package/src/hashline/hash.ts +23 -0
  101. package/src/hooks/skill-state.ts +2 -0
  102. package/src/internal-urls/docs-index.generated.ts +8 -11
  103. package/src/lsp/client.ts +7 -0
  104. package/src/main.ts +67 -1
  105. package/src/modes/acp/acp-agent.ts +25 -2
  106. package/src/modes/bridge/bridge-mode.ts +124 -2
  107. package/src/modes/components/custom-provider-wizard.ts +318 -0
  108. package/src/modes/components/model-selector.ts +108 -18
  109. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  110. package/src/modes/controllers/input-controller.ts +14 -2
  111. package/src/modes/controllers/selector-controller.ts +57 -1
  112. package/src/modes/prompt-action-autocomplete.ts +49 -10
  113. package/src/modes/rpc/rpc-client.ts +57 -3
  114. package/src/modes/rpc/rpc-mode.ts +67 -0
  115. package/src/modes/rpc/rpc-types.ts +224 -2
  116. package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
  117. package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
  118. package/src/modes/shared/agent-wire/command-validation.ts +25 -1
  119. package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
  120. package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
  121. package/src/modes/shared/agent-wire/handshake.ts +43 -3
  122. package/src/modes/shared/agent-wire/protocol.ts +7 -0
  123. package/src/modes/shared/agent-wire/responses.ts +2 -2
  124. package/src/modes/shared/agent-wire/scopes.ts +2 -0
  125. package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
  126. package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
  127. package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
  128. package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
  129. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
  130. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
  131. package/src/modes/theme/theme.ts +6 -0
  132. package/src/modes/types.ts +1 -0
  133. package/src/prompts/memories/consolidation.md +1 -1
  134. package/src/prompts/memories/read-path.md +6 -7
  135. package/src/prompts/memories/unavailable.md +2 -2
  136. package/src/prompts/tools/bash.md +1 -1
  137. package/src/prompts/tools/irc.md +1 -1
  138. package/src/prompts/tools/read.md +2 -2
  139. package/src/prompts/tools/recall.md +1 -0
  140. package/src/prompts/tools/reflect.md +1 -0
  141. package/src/prompts/tools/retain.md +1 -0
  142. package/src/runtime-mcp/client.ts +7 -4
  143. package/src/runtime-mcp/manager.ts +45 -13
  144. package/src/runtime-mcp/transports/http.ts +40 -14
  145. package/src/runtime-mcp/transports/stdio.ts +11 -10
  146. package/src/sdk.ts +48 -1
  147. package/src/session/agent-session.ts +211 -2
  148. package/src/session/blob-store.ts +84 -0
  149. package/src/session/messages.ts +3 -0
  150. package/src/session/session-manager.ts +390 -33
  151. package/src/session/session-storage.ts +26 -0
  152. package/src/setup/provider-onboarding.ts +2 -2
  153. package/src/skill-state/active-state.ts +89 -1
  154. package/src/slash-commands/builtin-registry.ts +1 -1
  155. package/src/task/discovery.ts +7 -1
  156. package/src/task/executor.ts +18 -2
  157. package/src/task/index.ts +2 -0
  158. package/src/thinking.ts +8 -2
  159. package/src/tools/ask.ts +39 -9
  160. package/src/tools/hindsight-recall.ts +0 -2
  161. package/src/tools/hindsight-reflect.ts +0 -2
  162. package/src/tools/hindsight-retain.ts +0 -2
  163. package/src/tools/index.ts +7 -18
  164. package/src/tools/read.ts +3 -3
  165. package/src/tools/skill.ts +15 -3
  166. package/src/utils/edit-mode.ts +1 -1
@@ -147,6 +147,7 @@ export interface InteractiveModeContext {
147
147
  showStatus(message: string, options?: { dim?: boolean }): void;
148
148
  showError(message: string): void;
149
149
  showWarning(message: string): void;
150
+ notifyConfigChanged?: () => Promise<void> | void;
150
151
  showNewVersionNotification(newVersion: string): void;
151
152
  clearEditor(): void;
152
153
  updatePendingMessagesDisplay(): void;
@@ -1,5 +1,5 @@
1
1
  Memory consolidation agent.
2
- Memory root: memory://root
2
+ Memory backend: local private runtime state
3
3
  Input corpus (raw memories):
4
4
  {{raw_memories}}
5
5
  Input corpus (rollout summaries):
@@ -1,11 +1,10 @@
1
1
  # Memory Guidance
2
- Memory root: memory://root
2
+ Memory backend: local private runtime state
3
3
  Operational rules:
4
- 1) Read `memory://root/memory_summary.md` first.
5
- 2) If needed, inspect `memory://root/MEMORY.md` and `memory://root/skills/<name>/SKILL.md`.
6
- 3) Trust memory for heuristics and process context. Trust current repo files, runtime output, and user instruction for factual state and final decisions.
7
- 4) When memory changes your plan, cite the artifact path (e.g. `memory://root/skills/<name>/SKILL.md`) and pair it with current-repo evidence.
8
- 5) If memory disagrees with repo state or user instruction, prefer repo/user. Treat memory as stale. Proceed with corrected behavior, then update/regenerate memory artifacts.
9
- 6) Escalate confidence only after repository verification. Memory alone is NEVER sufficient proof.
4
+ 1) The memory summary below is already injected; do not try to call or invent a `memory` tool.
5
+ 2) Treat memory as heuristic process context. Trust current repo files, runtime output, and user instruction for factual state and final decisions.
6
+ 3) When memory changes your plan, pair it with current-repo evidence before acting.
7
+ 4) If memory disagrees with repo state or user instruction, prefer repo/user. Treat memory as stale. Proceed with corrected behavior, then update/regenerate memory artifacts through supported memory commands when available.
8
+ 5) Escalate confidence only after repository verification. Memory alone is NEVER sufficient proof.
10
9
  Memory summary:
11
10
  {{memory_summary}}
@@ -1,9 +1,9 @@
1
1
  # Memory Guidance
2
- Memory root: memory://root
2
+ Memory backend: local private runtime state
3
3
  Status: local memory is enabled, but no confirmed memory payload is available for this project yet.
4
4
 
5
5
  Operational rules:
6
6
  1) Do not claim that a user preference, fact, or instruction has been saved, remembered, or persisted unless a backend operation or a non-empty memory payload confirms it.
7
7
  2) If the user asks you to save or remember something now, explain that durable memory is not confirmed because local memory has no available payload/readback yet. You may use the instruction for the current session only.
8
- 3) If reading `memory://root` or `memory://root/memory_summary.md` fails, treat that as no confirmed memory, not as successful persistence.
8
+ 3) Do not try to call or invent a `memory` tool; local memory readback is unavailable in this session.
9
9
  4) The local backend consolidates prior session rollouts asynchronously; an empty payload is a degraded/unconfirmed state, not a successful save.
@@ -6,7 +6,7 @@ Executes bash command in shell session for terminal operations like git, bun, ca
6
6
  - Quote variable expansions like `"$NAME"` to preserve exact content
7
7
  - PTY mode is opt-in: set `pty: true` only when the command needs a real terminal (e.g. `sudo`, `ssh` requiring user input); default is `false`
8
8
  - Use `;` only when later commands should run regardless of earlier failures
9
- - Internal URIs (`agent://`, `artifact://`, `memory://`, `rule://`, `local://`) are auto-resolved to filesystem paths
9
+ - Internal URIs (`agent://`, `artifact://`, `rule://`, `local://`) are auto-resolved to filesystem paths
10
10
  {{#if asyncEnabled}}
11
11
  - Use `async: true` for long-running commands when you don't need immediate output; the call returns a background job ID and the result is delivered automatically as a follow-up.
12
12
  {{/if}}
@@ -24,7 +24,7 @@ These rules apply to both sending and replying.
24
24
  - **Do not quote the message you are replying to.** The sender already saw it; the TUI already renders it. Lead with the answer.
25
25
  - **Use IRC, not terminal tools, to learn about peers.** Do not `grep` artifacts, read other sessions' JSONL files, or shell-poke around to figure out what another agent is doing. DM them — they have the live answer and you do not.
26
26
  - **One round-trip is enough.** Replies arrive synchronously when the recipient is reachable. Do not follow up with "did you get my message?" — they did. If `delivered` is empty or the result was `failed`, the peer is unavailable; move on or report the blocker, do not retry in a loop.
27
- - **Stay terse.** A DM is a chat message, not a memo. One question per send when you can. Share file paths and artifacts via `local://` / `memory://` / `artifact://` URLs instead of pasting blobs.
27
+ - **Stay terse.** A DM is a chat message, not a memo. One question per send when you can. Share file paths and artifacts via `local://` / `artifact://` URLs instead of pasting blobs.
28
28
  - **Address peers by id.** Use the exact id from `op: "list"` (e.g. `0-AuthLoader`, `0-Main`). Do not invent friendly names.
29
29
  - **Do not IRC for things a tool would answer.** If a `read`, `grep`, or build command would resolve the question, do that first.
30
30
  - **When you receive an IRC message, answer it before continuing.** The recipient injects the question + your auto-reply into your history; address it directly, do not repeat it back to the user.
@@ -8,7 +8,7 @@ Read files, directories, archives, SQLite databases, images, documents, internal
8
8
 
9
9
  ## Parameters
10
10
 
11
- - `path` — required. Local path, internal URI (`agent://`, `artifact://`, `memory://`, `rule://`, `local://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
11
+ - `path` — required. Local path, internal URI (`agent://`, `artifact://`, `rule://`, `local://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
12
12
 
13
13
  ## Selectors
14
14
 
@@ -70,7 +70,7 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
70
70
 
71
71
  # Internal URIs
72
72
 
73
- `agent://<id>`, `artifact://<id>`, `memory://root`, `rule://<name>`, and `local://<name>.md` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.
73
+ `agent://<id>`, `artifact://<id>`, `rule://<name>`, and `local://<name>.md` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.
74
74
 
75
75
  <critical>
76
76
  - You MUST use `read` for every file, directory, archive, and URL inspection. `cat`, `head`, `tail`, `less`, `more`, `ls`, `tar`, `unzip`, `curl`, `wget` are FORBIDDEN — any such bash call is a bug, regardless of how short or convenient it looks.
@@ -1,3 +1,4 @@
1
+ Compatibility-only legacy Hindsight helper. This prompt is retained for backend/tool-call compatibility and is not part of the public gajae-code coding harness tool surface.
1
2
  Search long-term memory for relevant information. Returns raw matching entries ranked by relevance.
2
3
 
3
4
  Use proactively — before answering questions about past conversations, user preferences, project decisions, or any topic where prior context would help accuracy. When in doubt, recall first.
@@ -1,3 +1,4 @@
1
+ Compatibility-only legacy Hindsight helper. This prompt is retained for backend/tool-call compatibility and is not part of the public gajae-code coding harness tool surface.
1
2
  Generate a synthesised answer by reasoning over long-term memory. Unlike `recall`, `reflect` blends relevant memories into a coherent response.
2
3
 
3
4
  Use for open-ended questions spanning many stored facts: "What do you know about this user?", "Summarize project decisions.", "What are my preferences for X?"
@@ -1,3 +1,4 @@
1
+ Compatibility-only legacy Hindsight helper. This prompt is retained for backend/tool-call compatibility and is not part of the public gajae-code coding harness tool surface.
1
2
  Store one or more facts in long-term memory for future sessions.
2
3
 
3
4
  Use for durable, reusable knowledge: user preferences, project decisions, architectural choices, anything that improves future responses.
@@ -141,6 +141,8 @@ export async function connectToServer(
141
141
  ): Promise<MCPServerConnection> {
142
142
  const timeoutMs = config.timeout ?? CONNECTION_TIMEOUT_MS;
143
143
  let transport: MCPTransport | undefined;
144
+ const connectAbort = new AbortController();
145
+ const connectSignal = options?.signal ? AbortSignal.any([options.signal, connectAbort.signal]) : connectAbort.signal;
144
146
 
145
147
  const connect = async (): Promise<MCPServerConnection> => {
146
148
  transport = await createTransport(config);
@@ -155,7 +157,7 @@ export async function connectToServer(
155
157
 
156
158
  try {
157
159
  const initResult = await initializeConnection(transport, {
158
- signal: options?.signal,
160
+ signal: connectSignal,
159
161
  async onInitialized() {
160
162
  // Open the SSE stream before sending initialized, so server-to-client
161
163
  // requests triggered by on_initialized (e.g. roots/list) are delivered.
@@ -184,13 +186,14 @@ export async function connectToServer(
184
186
  connect(),
185
187
  timeoutMs,
186
188
  `Connection to MCP server "${name}" timed out after ${timeoutMs}ms`,
187
- options?.signal,
189
+ connectSignal,
188
190
  );
189
191
  } catch (error) {
190
192
  // If withTimeout rejected (timeout/abort) while connect() was still pending,
191
- // the transport may be alive with an open SSE listener. Close it.
193
+ // abort initialization and wait for transport cleanup before returning.
194
+ connectAbort.abort(error);
192
195
  if (transport) {
193
- void transport.close().catch(() => {});
196
+ await transport.close().catch(() => {});
194
197
  }
195
198
  throw error;
196
199
  }
@@ -152,6 +152,7 @@ export class MCPManager {
152
152
  #connections = new Map<string, MCPServerConnection>();
153
153
  #tools: CustomTool<TSchema, MCPToolDetails>[] = [];
154
154
  #pendingConnections = new Map<string, Promise<MCPServerConnection>>();
155
+ #pendingConnectionControllers = new Map<string, AbortController>();
155
156
  #pendingToolLoads = new Map<string, Promise<ToolLoadResult>>();
156
157
  #sources = new Map<string, SourceMeta>();
157
158
  #authStorage: AuthStorage | null = null;
@@ -164,6 +165,7 @@ export class MCPManager {
164
165
  #subscribedResources = new Map<string, Set<string>>();
165
166
  #pendingResourceRefresh = new Map<string, { connection: MCPServerConnection; promise: Promise<void> }>();
166
167
  #pendingReconnections = new Map<string, Promise<MCPServerConnection | null>>();
168
+ #disconnectEpochs = new Map<string, number>();
167
169
  /** Preserved configs for reconnection after connection loss. */
168
170
  #serverConfigs = new Map<string, MCPServerConfig>();
169
171
  /** Monotonic epoch incremented on disconnectAll to invalidate stale reconnections. */
@@ -348,10 +350,14 @@ export class MCPManager {
348
350
  // and falls back to cached/deferred tools.
349
351
  this.#serverConfigs.set(name, config);
350
352
 
353
+ const connectionEpoch = this.#epoch;
354
+ const connectionAbort = new AbortController();
355
+ this.#pendingConnectionControllers.set(name, connectionAbort);
351
356
  // Resolve auth config before connecting, but do so per-server in parallel.
352
357
  const connectionPromise = (async () => {
353
358
  const resolvedConfig = await this.#resolveAuthConfig(config);
354
359
  return connectToServer(name, resolvedConfig, {
360
+ signal: connectionAbort.signal,
355
361
  onNotification: (method, params) => {
356
362
  this.#handleServerNotification(name, method, params);
357
363
  },
@@ -360,18 +366,26 @@ export class MCPManager {
360
366
  },
361
367
  });
362
368
  })().then(
363
- connection => {
369
+ async connection => {
364
370
  // Store original config (without resolved tokens) to keep
365
371
  // cache keys stable and avoid leaking rotating credentials.
366
372
  connection.config = config;
367
- this.#serverConfigs.set(name, config);
368
373
  if (sources[name]) {
369
374
  connection._source = sources[name];
370
375
  }
371
- if (this.#pendingConnections.get(name) === connectionPromise) {
376
+ const stillPending = this.#pendingConnections.get(name) === connectionPromise;
377
+ const stillCurrent = this.#epoch === connectionEpoch && this.#serverConfigs.get(name) === config;
378
+ if (stillPending) {
372
379
  this.#pendingConnections.delete(name);
373
- this.#connections.set(name, connection);
380
+ this.#pendingConnectionControllers.delete(name);
374
381
  }
382
+ if (!stillPending || !stillCurrent) {
383
+ connection.transport.onClose = undefined;
384
+ await connection.transport.close().catch(() => {});
385
+ throw new Error(`Server "${name}" was disconnected during connection`);
386
+ }
387
+ this.#connections.set(name, connection);
388
+ this.#serverConfigs.set(name, config);
375
389
 
376
390
  // Wire auth refresh for HTTP transports so 401s trigger token refresh.
377
391
  if (connection.transport instanceof HttpTransport && config.auth?.type === "oauth") {
@@ -396,6 +410,7 @@ export class MCPManager {
396
410
  error => {
397
411
  if (this.#pendingConnections.get(name) === connectionPromise) {
398
412
  this.#pendingConnections.delete(name);
413
+ this.#pendingConnectionControllers.delete(name);
399
414
  }
400
415
  throw error;
401
416
  },
@@ -660,13 +675,16 @@ export class MCPManager {
660
675
  * Disconnect from a specific server.
661
676
  */
662
677
  async disconnectServer(name: string): Promise<void> {
678
+ const nextEpoch = (this.#disconnectEpochs.get(name) ?? 0) + 1;
679
+ this.#disconnectEpochs.set(name, nextEpoch);
680
+ this.#pendingConnectionControllers.get(name)?.abort(new Error(`MCP server disconnected: ${name}`));
681
+ this.#pendingConnectionControllers.delete(name);
663
682
  this.#pendingConnections.delete(name);
664
683
  this.#pendingToolLoads.delete(name);
665
684
  this.#pendingReconnections.delete(name);
666
685
  this.#sources.delete(name);
667
686
  this.#serverConfigs.delete(name);
668
687
  this.#pendingResourceRefresh.delete(name);
669
-
670
688
  const connection = this.#connections.get(name);
671
689
 
672
690
  const subscribedUris = this.#subscribedResources.get(name);
@@ -705,6 +723,10 @@ export class MCPManager {
705
723
  const promises = Array.from(this.#connections.values()).map(conn => disconnectServer(conn));
706
724
  await Promise.allSettled(promises);
707
725
 
726
+ for (const controller of this.#pendingConnectionControllers.values()) {
727
+ controller.abort(new Error("MCP manager disconnected"));
728
+ }
729
+ this.#pendingConnectionControllers.clear();
708
730
  this.#pendingConnections.clear();
709
731
  this.#pendingToolLoads.clear();
710
732
  this.#pendingReconnections.clear();
@@ -808,14 +830,24 @@ export class MCPManager {
808
830
  reconnectEpoch: number,
809
831
  ): Promise<MCPServerConnection> {
810
832
  const resolvedConfig = await this.#resolveAuthConfig(config);
811
- const connection = await connectToServer(name, resolvedConfig, {
812
- onNotification: (method, params) => {
813
- this.#handleServerNotification(name, method, params);
814
- },
815
- onRequest: (method, params) => {
816
- return this.#handleServerRequest(method, params);
817
- },
818
- });
833
+ const connectionAbort = new AbortController();
834
+ this.#pendingConnectionControllers.set(name, connectionAbort);
835
+ let connection: MCPServerConnection;
836
+ try {
837
+ connection = await connectToServer(name, resolvedConfig, {
838
+ signal: connectionAbort.signal,
839
+ onNotification: (method, params) => {
840
+ this.#handleServerNotification(name, method, params);
841
+ },
842
+ onRequest: (method, params) => {
843
+ return this.#handleServerRequest(method, params);
844
+ },
845
+ });
846
+ } finally {
847
+ if (this.#pendingConnectionControllers.get(name) === connectionAbort) {
848
+ this.#pendingConnectionControllers.delete(name);
849
+ }
850
+ }
819
851
 
820
852
  connection.config = config;
821
853
  if (source) connection._source = source;
@@ -25,6 +25,8 @@ export class HttpTransport implements MCPTransport {
25
25
  #connected = false;
26
26
  #sessionId: string | null = null;
27
27
  #sseConnection: AbortController | null = null;
28
+ #streamControllers = new Set<AbortController>();
29
+ #streamReaders = new Set<Promise<void>>();
28
30
 
29
31
  onClose?: () => void;
30
32
  onError?: (error: Error) => void;
@@ -52,6 +54,15 @@ export class HttpTransport implements MCPTransport {
52
54
  this.#connected = true;
53
55
  }
54
56
 
57
+ #trackReader(promise: Promise<void>, controller?: AbortController): void {
58
+ if (controller) this.#streamControllers.add(controller);
59
+ this.#streamReaders.add(promise);
60
+ void promise.finally(() => {
61
+ this.#streamReaders.delete(promise);
62
+ if (controller) this.#streamControllers.delete(controller);
63
+ });
64
+ }
65
+
55
66
  /**
56
67
  * Start SSE listener for server-initiated messages.
57
68
  * Resolves once the SSE connection is established (or fails/unsupported).
@@ -61,7 +72,8 @@ export class HttpTransport implements MCPTransport {
61
72
  if (!this.#connected) return;
62
73
  if (this.#sseConnection) return;
63
74
 
64
- this.#sseConnection = new AbortController();
75
+ const sseConnection = new AbortController();
76
+ this.#sseConnection = sseConnection;
65
77
  const headers: Record<string, string> = {
66
78
  Accept: "text/event-stream",
67
79
  ...this.config.headers,
@@ -76,10 +88,10 @@ export class HttpTransport implements MCPTransport {
76
88
  response = await fetch(this.config.url, {
77
89
  method: "GET",
78
90
  headers,
79
- signal: this.#sseConnection.signal,
91
+ signal: sseConnection.signal,
80
92
  });
81
93
  } catch (error) {
82
- this.#sseConnection = null;
94
+ this.#sseConnection = this.#sseConnection === sseConnection ? null : this.#sseConnection;
83
95
  if (error instanceof Error && error.name !== "AbortError") {
84
96
  this.onError?.(error);
85
97
  }
@@ -87,19 +99,20 @@ export class HttpTransport implements MCPTransport {
87
99
  }
88
100
 
89
101
  if (response.status === 405 || !response.ok || !response.body) {
90
- this.#sseConnection = null;
102
+ this.#sseConnection = this.#sseConnection === sseConnection ? null : this.#sseConnection;
91
103
  return;
92
104
  }
93
105
 
94
106
  // Connection established — read messages in background.
95
107
  // If the stream ends unexpectedly (server restart, network drop),
96
108
  // fire onClose so the manager can trigger reconnection.
97
- const signal = this.#sseConnection.signal;
98
- void this.#readSSEStream(response.body!, signal).finally(() => {
109
+ const signal = sseConnection.signal;
110
+ const reader = this.#readSSEStream(response.body!, signal).finally(() => {
99
111
  const wasConnected = this.#connected;
100
- this.#sseConnection = null;
101
- if (wasConnected) this.onClose?.();
112
+ if (this.#sseConnection === sseConnection) this.#sseConnection = null;
113
+ if (wasConnected && !signal.aborted) this.onClose?.();
102
114
  });
115
+ this.#trackReader(reader, sseConnection);
103
116
  }
104
117
  async #readSSEStream(body: ReadableStream<Uint8Array>, signal: AbortSignal): Promise<void> {
105
118
  try {
@@ -266,6 +279,8 @@ export class HttpTransport implements MCPTransport {
266
279
  // Re-reading `response.body` after `for await` breaks would lock the
267
280
  // stream a second time and surface as "ReadableStream already has a
268
281
  // controller", so we must not exit the loop early.
282
+ const drainController = abortController;
283
+ this.#streamControllers.add(drainController);
269
284
  const drain = async (): Promise<void> => {
270
285
  try {
271
286
  for await (const raw of readSseJson<JsonRpcMessage | JsonRpcMessage[]>(response.body!, operationSignal)) {
@@ -306,10 +321,11 @@ export class HttpTransport implements MCPTransport {
306
321
  }
307
322
  } finally {
308
323
  clearTimeout(timeoutId);
324
+ this.#streamControllers.delete(drainController);
309
325
  }
310
326
  };
311
327
 
312
- void drain();
328
+ this.#trackReader(drain());
313
329
  return promise;
314
330
  }
315
331
 
@@ -417,9 +433,13 @@ export class HttpTransport implements MCPTransport {
417
433
  // on the notification response (MCP Streamable HTTP spec). Read them.
418
434
  const contentType = response.headers.get("Content-Type") ?? "";
419
435
  if (contentType.includes("text/event-stream") && response.body) {
420
- // Use the SSE connection's signal if available, otherwise read until stream ends
421
- const signal = this.#sseConnection?.signal ?? AbortSignal.timeout(this.config.timeout ?? 30000);
422
- void this.#readSSEStream(response.body, signal);
436
+ const streamController = new AbortController();
437
+ const streamTimeout = AbortSignal.timeout(this.config.timeout ?? 30000);
438
+ const signals = this.#sseConnection
439
+ ? [this.#sseConnection.signal, streamController.signal, streamTimeout]
440
+ : [streamController.signal, streamTimeout];
441
+ const reader = this.#readSSEStream(response.body, AbortSignal.any(signals));
442
+ this.#trackReader(reader, streamController);
423
443
  } else {
424
444
  await response.body?.cancel();
425
445
  }
@@ -433,14 +453,20 @@ export class HttpTransport implements MCPTransport {
433
453
  }
434
454
 
435
455
  async close(): Promise<void> {
436
- if (!this.#connected) return;
456
+ const wasConnected = this.#connected;
437
457
  this.#connected = false;
438
458
 
439
- // Abort SSE listener
459
+ // Abort all SSE/background readers and wait for them to settle.
460
+ for (const controller of this.#streamControllers) {
461
+ controller.abort();
462
+ }
440
463
  if (this.#sseConnection) {
441
464
  this.#sseConnection.abort();
442
465
  this.#sseConnection = null;
443
466
  }
467
+ await Promise.allSettled(Array.from(this.#streamReaders));
468
+
469
+ if (!wasConnected && !this.#sessionId) return;
444
470
 
445
471
  // Send session termination if we have a session
446
472
  if (this.#sessionId) {
@@ -5,8 +5,7 @@
5
5
  * Messages are newline-delimited JSON.
6
6
  */
7
7
 
8
- import { getProjectDir, readJsonl, Snowflake } from "@gajae-code/utils";
9
- import { type Subprocess, spawn } from "bun";
8
+ import { getProjectDir, ptree, readJsonl, Snowflake } from "@gajae-code/utils";
10
9
  import type {
11
10
  JsonRpcError,
12
11
  JsonRpcMessage,
@@ -22,8 +21,10 @@ import { toJsonRpcError } from "../../runtime-mcp/types";
22
21
  * Stdio transport for MCP servers.
23
22
  * Spawns a subprocess and communicates via stdin/stdout.
24
23
  */
24
+ const CLOSE_WAIT_MS = 1_000;
25
+
25
26
  export class StdioTransport implements MCPTransport {
26
- #process: Subprocess<"pipe", "pipe", "pipe"> | null = null;
27
+ #process: ptree.ChildProcess<"pipe"> | null = null;
27
28
  #pendingRequests = new Map<
28
29
  string | number,
29
30
  {
@@ -57,13 +58,11 @@ export class StdioTransport implements MCPTransport {
57
58
  ...this.config.env,
58
59
  };
59
60
 
60
- this.#process = spawn({
61
- cmd: [this.config.command, ...args],
61
+ this.#process = ptree.spawn([this.config.command, ...args], {
62
62
  cwd: this.config.cwd ?? getProjectDir(),
63
63
  env,
64
64
  stdin: "pipe",
65
- stdout: "pipe",
66
- stderr: "pipe",
65
+ stderr: "full",
67
66
  });
68
67
 
69
68
  this.#connected = true;
@@ -299,9 +298,11 @@ export class StdioTransport implements MCPTransport {
299
298
  }
300
299
  this.#pendingRequests.clear();
301
300
 
302
- // Kill subprocess
303
- if (this.#process) {
304
- this.#process.kill();
301
+ // Terminate the subprocess tree and keep the handle until exit is observed.
302
+ const process = this.#process;
303
+ if (process) {
304
+ process.kill();
305
+ await Promise.race([process.exited.catch(() => {}), Bun.sleep(CLOSE_WAIT_MS)]);
305
306
  this.#process = null;
306
307
  }
307
308
 
package/src/sdk.ts CHANGED
@@ -68,6 +68,8 @@ import {
68
68
  wrapRegisteredTools,
69
69
  } from "./extensibility/extensions";
70
70
  import { ExtensionRuntime } from "./extensibility/extensions/loader";
71
+ import { resolveCurrentPhaseForParent } from "./extensibility/gjc-plugins/injection";
72
+ import { loadActiveSubskillTools } from "./extensibility/gjc-plugins/tools";
71
73
  import { loadSkills, type Skill, type SkillWarning, setActiveSkills } from "./extensibility/skills";
72
74
  import type { FileSlashCommand } from "./extensibility/slash-commands";
73
75
  import type { HindsightSessionState } from "./hindsight/state";
@@ -243,6 +245,8 @@ export interface CreateAgentSessionOptions {
243
245
 
244
246
  /** Custom tools to register (in addition to built-in tools). Accepts both CustomTool and ToolDefinition. */
245
247
  customTools?: (CustomTool | ToolDefinition)[];
248
+ /** Explicit parent/phase used to load active GJC sub-skill tools for this session. */
249
+ gjcSubskillToolContext?: { parent: string; phase: string; sessionId?: string; cwd?: string };
246
250
  /** Inline extensions (merged with discovery). */
247
251
  extensions?: ExtensionFactory[];
248
252
  /** Additional extension paths to load (merged with discovery). */
@@ -291,7 +295,7 @@ export interface CreateAgentSessionOptions {
291
295
  taskDepth?: number;
292
296
  /** Current role-agent type/name for nested task sessions. */
293
297
  currentAgentType?: string;
294
- /** Parent Hindsight state to alias for subagent memory tools. */
298
+ /** Parent Hindsight state to alias for subagent private memory backend compatibility. */
295
299
  parentHindsightSessionState?: HindsightSessionState;
296
300
  /** Pre-allocated agent identity for IRC routing. Default: "0-Main" for top-level, parentTaskPrefix-derived for sub. */
297
301
  agentId?: string;
@@ -1183,6 +1187,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1183
1187
  getActiveModelString,
1184
1188
  getPlanModeState: () => session?.getPlanModeState(),
1185
1189
  getGoalModeState: () => session?.getGoalModeState(),
1190
+ getWorkflowGateEmitter: () => session?.getWorkflowGateEmitter(),
1186
1191
  getGoalRuntime: () => session?.goalRuntime,
1187
1192
  getClientBridge: () => session?.clientBridge,
1188
1193
  getCompactContext: () => session.formatCompactContext(),
@@ -1281,6 +1286,47 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1281
1286
  customTools.push(...getSearchTools());
1282
1287
  }
1283
1288
 
1289
+ const getReservedSubskillToolNames = () => [
1290
+ ...new Set([
1291
+ ...builtinTools.map(tool => tool.name),
1292
+ ...(options.toolNames?.map(name => name.toLowerCase()) ?? []),
1293
+ ...(options.customTools?.map(tool => (isCustomTool(tool) ? tool.name : tool.name)) ?? []),
1294
+ ...customTools.map(tool => tool.name),
1295
+ ]),
1296
+ ];
1297
+
1298
+ const gjcSubskillToolContext = options.gjcSubskillToolContext;
1299
+ if (gjcSubskillToolContext?.parent.trim() && gjcSubskillToolContext.phase.trim()) {
1300
+ const pluginTools = await loadActiveSubskillTools({
1301
+ cwd: gjcSubskillToolContext.cwd ?? cwd,
1302
+ sessionId: gjcSubskillToolContext.sessionId ?? logicalSessionId,
1303
+ parent: gjcSubskillToolContext.parent,
1304
+ phase: gjcSubskillToolContext.phase,
1305
+ reservedToolNames: getReservedSubskillToolNames(),
1306
+ });
1307
+ if (pluginTools.length > 0) {
1308
+ customTools.push(...pluginTools);
1309
+ }
1310
+ } else {
1311
+ for (const skill of skills) {
1312
+ const phase = await resolveCurrentPhaseForParent({
1313
+ cwd,
1314
+ sessionId: logicalSessionId,
1315
+ parent: skill.name,
1316
+ });
1317
+ const pluginTools = await loadActiveSubskillTools({
1318
+ cwd,
1319
+ sessionId: logicalSessionId,
1320
+ parent: skill.name,
1321
+ phase,
1322
+ reservedToolNames: getReservedSubskillToolNames(),
1323
+ });
1324
+ if (pluginTools.length > 0) {
1325
+ customTools.push(...pluginTools);
1326
+ }
1327
+ }
1328
+ }
1329
+
1284
1330
  // Custom tool and extension discovery is quarantined from the public GJC utility surface.
1285
1331
  // Explicit SDK extension factories are still honored; callers use them to
1286
1332
  // register in-process tools/providers without enabling filesystem discovery.
@@ -1889,6 +1935,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1889
1935
  modelRegistry,
1890
1936
  taskDepth,
1891
1937
  toolRegistry,
1938
+ workflowGateToolSession: toolSession,
1892
1939
  transformContext,
1893
1940
  onPayload,
1894
1941
  onResponse,