@gajae-code/coding-agent 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/config/model-registry.d.ts +17 -10
  3. package/dist/types/config/models-config-schema.d.ts +37 -0
  4. package/dist/types/config/settings-schema.d.ts +5 -0
  5. package/dist/types/edit/diff.d.ts +16 -0
  6. package/dist/types/edit/modes/replace.d.ts +7 -0
  7. package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
  8. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  9. package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
  10. package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
  11. package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
  12. package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
  13. package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
  14. package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
  15. package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
  16. package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
  17. package/dist/types/extensibility/skills.d.ts +9 -1
  18. package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
  19. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -2
  20. package/dist/types/harness-control-plane/storage.d.ts +7 -0
  21. package/dist/types/lsp/client.d.ts +1 -0
  22. package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
  23. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
  24. package/dist/types/modes/rpc/rpc-client.d.ts +19 -1
  25. package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
  26. package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
  27. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
  28. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
  29. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
  30. package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
  31. package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
  32. package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
  33. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
  34. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
  35. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
  36. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
  37. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
  38. package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
  39. package/dist/types/modes/theme/theme.d.ts +2 -1
  40. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  41. package/dist/types/sdk.d.ts +7 -0
  42. package/dist/types/session/agent-session.d.ts +10 -0
  43. package/dist/types/session/blob-store.d.ts +17 -0
  44. package/dist/types/session/messages.d.ts +3 -0
  45. package/dist/types/session/session-storage.d.ts +6 -0
  46. package/dist/types/skill-state/active-state.d.ts +13 -0
  47. package/dist/types/thinking.d.ts +3 -2
  48. package/dist/types/tools/index.d.ts +3 -0
  49. package/package.json +9 -7
  50. package/src/cli.ts +14 -0
  51. package/src/commands/harness.ts +192 -7
  52. package/src/commands/ultragoal.ts +1 -21
  53. package/src/config/model-equivalence.ts +1 -1
  54. package/src/config/model-registry.ts +32 -5
  55. package/src/config/models-config-schema.ts +7 -2
  56. package/src/config/settings-schema.ts +4 -1
  57. package/src/defaults/gjc/skills/deep-interview/SKILL.md +19 -23
  58. package/src/defaults/gjc/skills/ralplan/SKILL.md +7 -7
  59. package/src/discovery/claude-plugins.ts +25 -5
  60. package/src/edit/diff.ts +64 -1
  61. package/src/edit/modes/replace.ts +60 -2
  62. package/src/extensibility/gjc-plugins/activation.ts +87 -0
  63. package/src/extensibility/gjc-plugins/index.ts +9 -0
  64. package/src/extensibility/gjc-plugins/injection.ts +114 -0
  65. package/src/extensibility/gjc-plugins/loader.ts +131 -0
  66. package/src/extensibility/gjc-plugins/paths.ts +66 -0
  67. package/src/extensibility/gjc-plugins/schema.ts +79 -0
  68. package/src/extensibility/gjc-plugins/state.ts +29 -0
  69. package/src/extensibility/gjc-plugins/tools.ts +47 -0
  70. package/src/extensibility/gjc-plugins/types.ts +97 -0
  71. package/src/extensibility/gjc-plugins/validation.ts +76 -0
  72. package/src/extensibility/skills.ts +39 -7
  73. package/src/gjc-runtime/state-runtime.ts +93 -2
  74. package/src/gjc-runtime/state-writer.ts +17 -1
  75. package/src/gjc-runtime/ultragoal-runtime.ts +76 -121
  76. package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
  77. package/src/gjc-runtime/workflow-manifest.ts +2 -2
  78. package/src/harness-control-plane/storage.ts +144 -2
  79. package/src/hashline/hash.ts +23 -0
  80. package/src/hooks/skill-state.ts +2 -0
  81. package/src/internal-urls/docs-index.generated.ts +5 -5
  82. package/src/lsp/client.ts +7 -0
  83. package/src/modes/acp/acp-agent.ts +25 -2
  84. package/src/modes/bridge/bridge-mode.ts +124 -2
  85. package/src/modes/controllers/input-controller.ts +14 -2
  86. package/src/modes/prompt-action-autocomplete.ts +49 -10
  87. package/src/modes/rpc/rpc-client.ts +79 -3
  88. package/src/modes/rpc/rpc-mode.ts +67 -0
  89. package/src/modes/rpc/rpc-types.ts +224 -2
  90. package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
  91. package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
  92. package/src/modes/shared/agent-wire/command-validation.ts +25 -1
  93. package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
  94. package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
  95. package/src/modes/shared/agent-wire/handshake.ts +43 -3
  96. package/src/modes/shared/agent-wire/protocol.ts +7 -0
  97. package/src/modes/shared/agent-wire/responses.ts +2 -2
  98. package/src/modes/shared/agent-wire/scopes.ts +2 -0
  99. package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
  100. package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
  101. package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
  102. package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
  103. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
  104. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
  105. package/src/modes/theme/theme.ts +6 -0
  106. package/src/prompts/system/system-prompt.md +9 -0
  107. package/src/runtime-mcp/client.ts +7 -4
  108. package/src/runtime-mcp/manager.ts +45 -13
  109. package/src/runtime-mcp/transports/http.ts +40 -14
  110. package/src/runtime-mcp/transports/stdio.ts +11 -10
  111. package/src/sdk.ts +47 -0
  112. package/src/session/agent-session.ts +211 -2
  113. package/src/session/blob-store.ts +84 -0
  114. package/src/session/messages.ts +3 -0
  115. package/src/session/session-manager.ts +390 -33
  116. package/src/session/session-storage.ts +26 -0
  117. package/src/setup/provider-onboarding.ts +2 -2
  118. package/src/skill-state/active-state.ts +89 -1
  119. package/src/task/discovery.ts +7 -1
  120. package/src/task/executor.ts +16 -2
  121. package/src/thinking.ts +8 -2
  122. package/src/tools/ask.ts +39 -9
  123. package/src/tools/index.ts +3 -0
  124. package/src/tools/skill.ts +15 -3
  125. package/src/utils/edit-mode.ts +1 -1
@@ -69,6 +69,13 @@ Use for read-only plan critique. It approves only when execution can proceed wit
69
69
  - Before explicit execution approval, planning workflows NEVER edit product source, run mutation-oriented shell commands, commit, push, open PRs, or delegate implementation tasks.
70
70
  </routing>
71
71
 
72
+ <skill-discipline>
73
+ - Never ignore a skill invocation or any skill text. When a skill is active, read it in full and follow its instructions exactly. Do not assume, paraphrase, reorder, or substitute steps.
74
+ - Read-only and interview-style skills (e.g. `deep-interview`, `planner`, `architect`, `critic`) MUST NOT implement, edit product source, commit, or run mutating commands. Honor each skill's read-only or pending-approval boundary even when the fix looks obvious.
75
+ - When a task fits a bundled skill, recommend invoking the corresponding `/skill:<name>`; on user approval, invoke it. Never silently bypass an applicable skill.
76
+ - When no skill is active, or the active skill explicitly permits the action, and the action is non-destructive and clearly correct, perform it directly instead of asking.
77
+ </skill-discipline>
78
+
72
79
  <runtime-state>
73
80
  - Runtime state, specs, plans, and workflow ledgers belong under `.gjc/`.
74
81
  - Default workflow skills are bundled from `packages/coding-agent/src/defaults/gjc/skills/`. Runtime user/project `.gjc` discovery remains supported, but committed repo-visible `.gjc` defaults are not the source of truth.
@@ -82,6 +89,8 @@ Use for read-only plan critique. It approves only when execution can proceed wit
82
89
  - Do not narrate progress, ceremony, timing, scope inflation, or session limits.
83
90
  - If the user's intent is clear, act without asking. Ask only when the next step is destructive or requires a missing choice that materially changes the outcome.
84
91
  - When the user proposes something wrong, say what breaks and what to do instead once; then defer to their call.
92
+ - Never use permission-begging or deferral phrasing ("if you want", "if you'd like", "shall I", "I will now", "next I plan to"). For a destructive next step, state the recommended action and stop for approval. For a non-destructive, clearly correct next step, do it directly in the same turn.
93
+ - Do not defer actionable work. Underpromise and overdeliver: report only what is done or in progress, never announce remaining work instead of doing it.
85
94
  </communication>
86
95
 
87
96
  <completion-contract>
@@ -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). */
@@ -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,