@gajae-code/coding-agent 0.2.5 → 0.3.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 (234) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/types/async/job-manager.d.ts +91 -2
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/commands/harness.d.ts +37 -0
  6. package/dist/types/config/keybindings.d.ts +5 -0
  7. package/dist/types/config/settings-schema.d.ts +10 -4
  8. package/dist/types/config/settings.d.ts +2 -0
  9. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  10. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  11. package/dist/types/deep-interview/render-middleware.d.ts +6 -0
  12. package/dist/types/eval/py/executor.d.ts +2 -0
  13. package/dist/types/eval/py/kernel.d.ts +2 -0
  14. package/dist/types/exec/bash-executor.d.ts +10 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  17. package/dist/types/extensibility/shared-events.d.ts +1 -0
  18. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  19. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  20. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  21. package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
  22. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  23. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  25. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  26. package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  28. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  29. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  30. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  31. package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
  32. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  33. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  34. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  35. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  36. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  37. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  38. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  39. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  40. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  41. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  42. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  43. package/dist/types/harness-control-plane/types.d.ts +162 -0
  44. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  45. package/dist/types/hooks/skill-state.d.ts +23 -29
  46. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  47. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  48. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  49. package/dist/types/internal-urls/types.d.ts +4 -0
  50. package/dist/types/lsp/index.d.ts +10 -10
  51. package/dist/types/modes/bridge/auth.d.ts +12 -0
  52. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  53. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  54. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  55. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  56. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  57. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  58. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  59. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  60. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  61. package/dist/types/modes/components/status-line.d.ts +2 -0
  62. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  63. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  64. package/dist/types/modes/index.d.ts +1 -0
  65. package/dist/types/modes/interactive-mode.d.ts +2 -0
  66. package/dist/types/modes/jobs-observer.d.ts +57 -0
  67. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  68. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  69. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  70. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  71. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  72. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  73. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  74. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  75. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  76. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  77. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  78. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  79. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  80. package/dist/types/modes/types.d.ts +2 -0
  81. package/dist/types/sdk.d.ts +4 -0
  82. package/dist/types/session/agent-session.d.ts +19 -1
  83. package/dist/types/skill-state/active-state.d.ts +2 -0
  84. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  85. package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
  86. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  87. package/dist/types/task/executor.d.ts +3 -0
  88. package/dist/types/task/id.d.ts +7 -0
  89. package/dist/types/task/index.d.ts +5 -0
  90. package/dist/types/task/receipt.d.ts +85 -0
  91. package/dist/types/task/spawn-gate.d.ts +38 -0
  92. package/dist/types/task/types.d.ts +198 -14
  93. package/dist/types/tools/cron.d.ts +6 -0
  94. package/dist/types/tools/index.d.ts +2 -0
  95. package/dist/types/tools/path-utils.d.ts +1 -0
  96. package/dist/types/tools/subagent.d.ts +26 -1
  97. package/package.json +7 -7
  98. package/scripts/build-binary.ts +7 -0
  99. package/src/async/job-manager.ts +334 -6
  100. package/src/cli/args.ts +9 -2
  101. package/src/cli/auth-broker-cli.ts +1 -0
  102. package/src/cli/config-cli.ts +10 -2
  103. package/src/cli.ts +2 -0
  104. package/src/commands/deep-interview.ts +1 -0
  105. package/src/commands/harness.ts +862 -0
  106. package/src/commands/launch.ts +2 -2
  107. package/src/commands/state.ts +2 -1
  108. package/src/commands/team.ts +54 -39
  109. package/src/config/keybindings.ts +6 -0
  110. package/src/config/settings-schema.ts +13 -3
  111. package/src/config/settings.ts +5 -0
  112. package/src/dap/client.ts +17 -3
  113. package/src/debug/crash-diagnostics.ts +223 -0
  114. package/src/debug/runtime-gauges.ts +20 -0
  115. package/src/deep-interview/render-middleware.ts +372 -0
  116. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  117. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  118. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  119. package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
  120. package/src/eval/py/executor.ts +21 -1
  121. package/src/eval/py/kernel.ts +15 -0
  122. package/src/exec/bash-executor.ts +41 -0
  123. package/src/extensibility/custom-tools/types.ts +1 -0
  124. package/src/extensibility/extensions/types.ts +6 -0
  125. package/src/extensibility/shared-events.ts +1 -0
  126. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  127. package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
  128. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  129. package/src/gjc-runtime/ralplan-runtime.ts +235 -43
  130. package/src/gjc-runtime/state-graph.ts +86 -0
  131. package/src/gjc-runtime/state-migrations.ts +179 -0
  132. package/src/gjc-runtime/state-renderer.ts +345 -0
  133. package/src/gjc-runtime/state-runtime.ts +1155 -46
  134. package/src/gjc-runtime/state-schema.ts +192 -0
  135. package/src/gjc-runtime/state-validation.ts +49 -0
  136. package/src/gjc-runtime/state-writer.ts +749 -0
  137. package/src/gjc-runtime/team-runtime.ts +1255 -189
  138. package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
  139. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  140. package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
  141. package/src/gjc-runtime/workflow-manifest.ts +427 -0
  142. package/src/harness-control-plane/classifier.ts +128 -0
  143. package/src/harness-control-plane/control-endpoint.ts +148 -0
  144. package/src/harness-control-plane/finalize.ts +222 -0
  145. package/src/harness-control-plane/frame-mapper.ts +286 -0
  146. package/src/harness-control-plane/operate.ts +225 -0
  147. package/src/harness-control-plane/owner.ts +600 -0
  148. package/src/harness-control-plane/preserve.ts +102 -0
  149. package/src/harness-control-plane/receipts.ts +216 -0
  150. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  151. package/src/harness-control-plane/seams.ts +39 -0
  152. package/src/harness-control-plane/session-lease.ts +388 -0
  153. package/src/harness-control-plane/state-machine.ts +98 -0
  154. package/src/harness-control-plane/storage.ts +257 -0
  155. package/src/harness-control-plane/types.ts +214 -0
  156. package/src/hooks/skill-keywords.ts +4 -2
  157. package/src/hooks/skill-state.ts +197 -64
  158. package/src/internal-urls/agent-protocol.ts +68 -21
  159. package/src/internal-urls/artifact-protocol.ts +12 -17
  160. package/src/internal-urls/docs-index.generated.ts +3 -2
  161. package/src/internal-urls/registry-helpers.ts +19 -16
  162. package/src/internal-urls/types.ts +4 -0
  163. package/src/lsp/client.ts +18 -2
  164. package/src/main.ts +21 -5
  165. package/src/modes/bridge/auth.ts +41 -0
  166. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  167. package/src/modes/bridge/bridge-mode.ts +520 -0
  168. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  169. package/src/modes/bridge/event-stream.ts +70 -0
  170. package/src/modes/components/assistant-message.ts +5 -1
  171. package/src/modes/components/custom-editor.ts +101 -0
  172. package/src/modes/components/hook-selector.ts +133 -20
  173. package/src/modes/components/jobs-overlay-model.ts +109 -0
  174. package/src/modes/components/jobs-overlay.ts +172 -0
  175. package/src/modes/components/status-line/presets.ts +7 -5
  176. package/src/modes/components/status-line/segments.ts +25 -0
  177. package/src/modes/components/status-line/types.ts +2 -0
  178. package/src/modes/components/status-line.ts +9 -1
  179. package/src/modes/controllers/event-controller.ts +71 -6
  180. package/src/modes/controllers/extension-ui-controller.ts +43 -1
  181. package/src/modes/controllers/input-controller.ts +105 -9
  182. package/src/modes/controllers/selector-controller.ts +31 -1
  183. package/src/modes/index.ts +1 -0
  184. package/src/modes/interactive-mode.ts +28 -0
  185. package/src/modes/jobs-observer.ts +204 -0
  186. package/src/modes/rpc/host-tools.ts +1 -186
  187. package/src/modes/rpc/host-uris.ts +1 -235
  188. package/src/modes/rpc/rpc-client.ts +25 -10
  189. package/src/modes/rpc/rpc-mode.ts +12 -381
  190. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  191. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  192. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  193. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  194. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  195. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  196. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  197. package/src/modes/shared/agent-wire/responses.ts +17 -0
  198. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  199. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  200. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  201. package/src/modes/types.ts +2 -0
  202. package/src/prompts/agents/executor.md +13 -0
  203. package/src/prompts/tools/subagent.md +39 -4
  204. package/src/prompts/tools/task-summary.md +3 -9
  205. package/src/prompts/tools/task.md +5 -1
  206. package/src/sdk.ts +8 -0
  207. package/src/session/agent-session.ts +445 -71
  208. package/src/session/session-manager.ts +13 -1
  209. package/src/skill-state/active-state.ts +58 -65
  210. package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
  211. package/src/skill-state/initial-phase.ts +2 -0
  212. package/src/skill-state/workflow-state-contract.ts +33 -4
  213. package/src/skill-state/workflow-state-version.ts +3 -0
  214. package/src/slash-commands/builtin-registry.ts +8 -0
  215. package/src/task/executor.ts +79 -13
  216. package/src/task/id.ts +33 -0
  217. package/src/task/index.ts +376 -74
  218. package/src/task/output-manager.ts +5 -4
  219. package/src/task/receipt.ts +297 -0
  220. package/src/task/render.ts +54 -134
  221. package/src/task/spawn-gate.ts +132 -0
  222. package/src/task/types.ts +104 -10
  223. package/src/tools/ask.ts +88 -27
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash.ts +1 -1
  227. package/src/tools/cron.ts +48 -0
  228. package/src/tools/find.ts +4 -1
  229. package/src/tools/index.ts +2 -0
  230. package/src/tools/path-utils.ts +3 -2
  231. package/src/tools/read.ts +1 -0
  232. package/src/tools/search.ts +1 -0
  233. package/src/tools/skill.ts +6 -1
  234. package/src/tools/subagent.ts +423 -79
@@ -10,7 +10,7 @@ const DEFAULT_MAX_RUNNING_JOBS = 15;
10
10
  export interface AsyncJob {
11
11
  id: string;
12
12
  type: "bash" | "task";
13
- status: "running" | "completed" | "failed" | "cancelled";
13
+ status: "running" | "completed" | "failed" | "cancelled" | "paused";
14
14
  startTime: number;
15
15
  label: string;
16
16
  abortController: AbortController;
@@ -35,6 +35,66 @@ export interface AsyncJobMetadata {
35
35
  description?: string;
36
36
  assignment?: string;
37
37
  };
38
+ /** True when this bash job was started by the `monitor` tool (vs plain async bash). */
39
+ monitor?: boolean;
40
+ }
41
+
42
+ /**
43
+ * Typed outcome a subagent task run may produce. A `paused` outcome is
44
+ * non-terminal and non-delivering: the run suspended at a safe boundary and the
45
+ * subagent can be resumed from its persisted sessionFile. `completed` always
46
+ * wins a race with a late pause because the run returns it once it has actually
47
+ * finished.
48
+ */
49
+ export type SubagentRunOutcome = { kind: "completed"; text: string } | { kind: "paused"; note?: string };
50
+
51
+ /** Canonical lifecycle of a subagent across pause/resume cycles. */
52
+ export type SubagentLifecycle = "running" | "paused" | "queued" | "completed" | "failed" | "cancelled";
53
+
54
+ /**
55
+ * Live, executor-owned control handle for a RUNNING subagent. Registered when a
56
+ * subagent run starts and removed on pause/terminal so a paused subagent retains
57
+ * no live `AgentSession` reference (leak-free).
58
+ */
59
+ export interface SubagentLiveHandle {
60
+ /** Request a cooperative safe-boundary pause (never aborts the in-flight tool). */
61
+ requestPause(): void;
62
+ /** Inject a steering message into the live session. */
63
+ injectMessage(content: string, deliverAs: "steer" | "followUp" | "nextTurn"): Promise<void>;
64
+ }
65
+
66
+ /**
67
+ * Canonical, stable-id-keyed record for a subagent. Survives `AsyncJob`
68
+ * eviction so resume stays addressable by subagent id, and is the single source
69
+ * of truth for control-plane status and identity.
70
+ */
71
+ export interface SubagentRecord {
72
+ subagentId: string;
73
+ ownerId?: string;
74
+ /** Current live/last AsyncJob id; null while queued with no active job. */
75
+ currentJobId: string | null;
76
+ historicalJobIds: string[];
77
+ status: SubagentLifecycle;
78
+ sessionFile: string | null;
79
+ /** False for ephemeral sessions (no persistent artifacts dir). */
80
+ resumable: boolean;
81
+ queued?: { ownerId?: string; seq: number; message?: string; createdAt: number };
82
+ }
83
+
84
+ /** Lightweight, manager-owned resume payload. The async layer treats `data` as opaque. */
85
+ export interface ResumeDescriptor {
86
+ subagentId: string;
87
+ ownerId?: string;
88
+ data: unknown;
89
+ }
90
+
91
+ /** A pending resume awaiting a free concurrency slot. */
92
+ interface ResumeQueueEntry {
93
+ subagentId: string;
94
+ ownerId?: string;
95
+ seq: number;
96
+ message?: string;
97
+ createdAt: number;
38
98
  }
39
99
 
40
100
  export interface AsyncJobManagerOptions {
@@ -160,6 +220,18 @@ export class AsyncJobManager {
160
220
  readonly #retentionMs: number;
161
221
  #deliveryLoop: Promise<void> | undefined;
162
222
  #disposed = false;
223
+ readonly #subagentRecords = new Map<string, SubagentRecord>();
224
+ readonly #liveHandles = new Map<string, SubagentLiveHandle>();
225
+ readonly #resumeQueue: ResumeQueueEntry[] = [];
226
+ #resumeSeq = 0;
227
+ #resumeRunner?: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined;
228
+ readonly #resumeDescriptors = new Map<string, ResumeDescriptor>();
229
+ /**
230
+ * Change listeners notified on any mutation that can alter the live job set
231
+ * (register, terminal/eviction transitions, dispose). Used by the status-line
232
+ * jobs widget / overlay to refresh event-driven without polling.
233
+ */
234
+ readonly #changeListeners = new Set<() => void>();
163
235
 
164
236
  #filterJobs(jobs: Iterable<AsyncJob>, filter?: AsyncJobFilter): AsyncJob[] {
165
237
  const ownerId = filter?.ownerId;
@@ -177,6 +249,29 @@ export class AsyncJobManager {
177
249
  this.#retentionMs = Math.max(0, Math.floor(options.retentionMs ?? DEFAULT_RETENTION_MS));
178
250
  }
179
251
 
252
+ /**
253
+ * Subscribe to live-job-set change events. Returns an unsubscribe function.
254
+ * Listener errors are isolated so one bad subscriber cannot break others.
255
+ */
256
+ onChange(cb: () => void): () => void {
257
+ this.#changeListeners.add(cb);
258
+ return () => {
259
+ this.#changeListeners.delete(cb);
260
+ };
261
+ }
262
+
263
+ #notifyChange(): void {
264
+ for (const cb of this.#changeListeners) {
265
+ try {
266
+ cb();
267
+ } catch (error) {
268
+ logger.warn("Async job change listener failed", {
269
+ error: error instanceof Error ? error.message : String(error),
270
+ });
271
+ }
272
+ }
273
+ }
274
+
180
275
  register(
181
276
  type: "bash" | "task",
182
277
  label: string,
@@ -184,7 +279,7 @@ export class AsyncJobManager {
184
279
  jobId: string;
185
280
  signal: AbortSignal;
186
281
  reportProgress: (text: string, details?: Record<string, unknown>) => Promise<void>;
187
- }) => Promise<string>,
282
+ }) => Promise<string | SubagentRunOutcome>,
188
283
  options?: AsyncJobRegisterOptions,
189
284
  ): string {
190
285
  if (this.#disposed) {
@@ -227,20 +322,38 @@ export class AsyncJobManager {
227
322
  };
228
323
  job.promise = (async () => {
229
324
  try {
230
- const text = await run({ jobId: id, signal: abortController.signal, reportProgress });
325
+ const result = await run({ jobId: id, signal: abortController.signal, reportProgress });
326
+ const outcome: SubagentRunOutcome =
327
+ typeof result === "string" ? { kind: "completed", text: result } : result;
231
328
  if (job.status === "cancelled") {
232
- job.resultText = text;
329
+ job.resultText = outcome.kind === "completed" ? outcome.text : outcome.note;
233
330
  this.#scheduleEviction(id);
331
+ this.#markRecordTerminal(id, "cancelled");
332
+ this.#drainResumeQueue();
333
+ return;
334
+ }
335
+ if (outcome.kind === "paused") {
336
+ // Sole canonical writer of the running -> paused transition. No
337
+ // delivery and no eviction scheduling: a paused subagent stays
338
+ // listed and resumable from its sessionFile.
339
+ job.status = "paused";
340
+ if (outcome.note) job.resultText = outcome.note;
341
+ this.#markRecordPaused(id);
342
+ this.#drainResumeQueue();
234
343
  return;
235
344
  }
236
345
  job.status = "completed";
237
- job.resultText = text;
238
- this.#enqueueDelivery(id, text);
346
+ job.resultText = outcome.text;
347
+ this.#enqueueDelivery(id, outcome.text);
239
348
  this.#scheduleEviction(id);
349
+ this.#markRecordTerminal(id, "completed");
350
+ this.#drainResumeQueue();
240
351
  } catch (error) {
241
352
  if (job.status === "cancelled") {
242
353
  job.errorText = error instanceof Error ? error.message : String(error);
243
354
  this.#scheduleEviction(id);
355
+ this.#markRecordTerminal(id, "cancelled");
356
+ this.#drainResumeQueue();
244
357
  return;
245
358
  }
246
359
  const errorText = error instanceof Error ? error.message : String(error);
@@ -248,10 +361,13 @@ export class AsyncJobManager {
248
361
  job.errorText = errorText;
249
362
  this.#enqueueDelivery(id, errorText);
250
363
  this.#scheduleEviction(id);
364
+ this.#markRecordTerminal(id, "failed");
365
+ this.#drainResumeQueue();
251
366
  }
252
367
  })();
253
368
 
254
369
  this.#jobs.set(id, job);
370
+ this.#notifyChange();
255
371
  return id;
256
372
  }
257
373
 
@@ -264,6 +380,15 @@ export class AsyncJobManager {
264
380
  const job = this.#jobs.get(id);
265
381
  if (!job) return false;
266
382
  if (filter?.ownerId && job.ownerId !== filter.ownerId) return false;
383
+ if (job.status === "paused") {
384
+ // Paused jobs have no running promise to abort; transition directly.
385
+ // The session file is kept, so the record stays resumable by id.
386
+ job.status = "cancelled";
387
+ this.#markRecordTerminal(id, "cancelled");
388
+ this.#scheduleEviction(id);
389
+ this.#drainResumeQueue();
390
+ return true;
391
+ }
267
392
  if (job.status !== "running") return false;
268
393
  job.status = "cancelled";
269
394
  job.abortController.abort();
@@ -271,6 +396,200 @@ export class AsyncJobManager {
271
396
  return true;
272
397
  }
273
398
 
399
+ // ── Subagent control plane (pause / resume / steer support) ──────────
400
+
401
+ /** Register or replace the canonical record for a subagent. */
402
+ registerSubagentRecord(record: SubagentRecord): void {
403
+ this.#subagentRecords.set(record.subagentId, record);
404
+ }
405
+
406
+ getSubagentRecord(subagentId: string, filter?: AsyncJobFilter): SubagentRecord | undefined {
407
+ const rec = this.#subagentRecords.get(subagentId.trim());
408
+ if (!rec) return undefined;
409
+ if (filter?.ownerId && rec.ownerId !== filter.ownerId) return undefined;
410
+ return rec;
411
+ }
412
+
413
+ getSubagentRecords(filter?: AsyncJobFilter): SubagentRecord[] {
414
+ const ownerId = filter?.ownerId;
415
+ const out: SubagentRecord[] = [];
416
+ for (const rec of this.#subagentRecords.values()) {
417
+ if (ownerId && rec.ownerId !== ownerId) continue;
418
+ out.push(rec);
419
+ }
420
+ return out;
421
+ }
422
+
423
+ registerLiveHandle(subagentId: string, handle: SubagentLiveHandle): void {
424
+ this.#liveHandles.set(subagentId, handle);
425
+ }
426
+
427
+ getLiveHandle(subagentId: string): SubagentLiveHandle | undefined {
428
+ return this.#liveHandles.get(subagentId);
429
+ }
430
+
431
+ removeLiveHandle(subagentId: string): void {
432
+ this.#liveHandles.delete(subagentId);
433
+ }
434
+
435
+ /** Install the TaskTool-owned resume runner. Returns the new job id, or undefined on failure. */
436
+ setResumeRunner(
437
+ runner: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined,
438
+ ): void {
439
+ this.#resumeRunner = runner;
440
+ }
441
+
442
+ registerResumeDescriptor(descriptor: ResumeDescriptor): void {
443
+ this.#resumeDescriptors.set(descriptor.subagentId, descriptor);
444
+ }
445
+
446
+ getResumeDescriptor(subagentId: string, filter?: AsyncJobFilter): ResumeDescriptor | undefined {
447
+ const descriptor = this.#resumeDescriptors.get(subagentId.trim());
448
+ if (!descriptor) return undefined;
449
+ if (filter?.ownerId && descriptor.ownerId !== filter.ownerId) return undefined;
450
+ return descriptor;
451
+ }
452
+
453
+ #recordByJobId(jobId: string): SubagentRecord | undefined {
454
+ for (const rec of this.#subagentRecords.values()) {
455
+ if (rec.currentJobId === jobId) return rec;
456
+ }
457
+ return undefined;
458
+ }
459
+
460
+ #markRecordPaused(jobId: string): void {
461
+ const rec = this.#recordByJobId(jobId);
462
+ if (rec) {
463
+ rec.status = "paused";
464
+ this.#liveHandles.delete(rec.subagentId);
465
+ }
466
+ }
467
+
468
+ #markRecordTerminal(jobId: string, status: "completed" | "failed" | "cancelled"): void {
469
+ const rec = this.#recordByJobId(jobId);
470
+ if (!rec) return;
471
+ rec.status = status;
472
+ this.#liveHandles.delete(rec.subagentId);
473
+ }
474
+
475
+ /** Request a graceful safe-boundary pause of a running subagent. */
476
+ pauseSubagent(
477
+ subagentId: string,
478
+ filter?: AsyncJobFilter,
479
+ ): { ok: boolean; status?: SubagentLifecycle; reason?: string } {
480
+ const rec = this.getSubagentRecord(subagentId, filter);
481
+ if (!rec) return { ok: false, reason: "not_found" };
482
+ if (rec.status !== "running") return { ok: false, status: rec.status, reason: "not_running" };
483
+ const handle = this.#liveHandles.get(rec.subagentId);
484
+ if (!handle) return { ok: false, status: rec.status, reason: "no_live_handle" };
485
+ handle.requestPause();
486
+ return { ok: true, status: rec.status };
487
+ }
488
+
489
+ /** Resume a non-running subagent from its sessionFile, optionally injecting a message first. */
490
+ resumeSubagent(
491
+ subagentId: string,
492
+ filter?: AsyncJobFilter,
493
+ message?: string,
494
+ ): { ok: boolean; status?: SubagentLifecycle; jobId?: string; queued?: boolean; reason?: string } {
495
+ const rec = this.getSubagentRecord(subagentId, filter);
496
+ if (!rec) return { ok: false, reason: "not_found" };
497
+ if (rec.status === "running") return { ok: false, status: "running", reason: "already_running" };
498
+ if (rec.status === "queued") {
499
+ if (message !== undefined && rec.queued) {
500
+ rec.queued.message = message;
501
+ const queued = this.#resumeQueue.find(entry => entry.subagentId === rec.subagentId);
502
+ if (queued) queued.message = message;
503
+ return { ok: true, queued: true, status: "queued" };
504
+ }
505
+ return { ok: false, status: "queued", reason: "already_queued" };
506
+ }
507
+ if (!rec.resumable || !rec.sessionFile) return { ok: false, reason: "context_unavailable" };
508
+ if (!this.#resumeRunner) return { ok: false, reason: "no_runner" };
509
+ if (this.getRunningJobs().length >= this.#maxRunningJobs) {
510
+ const seq = ++this.#resumeSeq;
511
+ rec.status = "queued";
512
+ rec.queued = { ownerId: rec.ownerId, seq, message, createdAt: Date.now() };
513
+ this.#resumeQueue.push({
514
+ subagentId: rec.subagentId,
515
+ ownerId: rec.ownerId,
516
+ seq,
517
+ message,
518
+ createdAt: rec.queued.createdAt,
519
+ });
520
+ return { ok: true, queued: true, status: "queued" };
521
+ }
522
+ return this.#startResume(rec, message);
523
+ }
524
+
525
+ #startResume(
526
+ rec: SubagentRecord,
527
+ message?: string,
528
+ ): { ok: boolean; status?: SubagentLifecycle; jobId?: string; reason?: string } {
529
+ const prevJobId = rec.currentJobId;
530
+ const newJobId = this.#resumeRunner?.(rec.subagentId, message, this.#resumeDescriptors.get(rec.subagentId));
531
+ if (!newJobId) return { ok: false, reason: "resume_failed" };
532
+ if (prevJobId && prevJobId !== newJobId) rec.historicalJobIds.push(prevJobId);
533
+ rec.currentJobId = newJobId;
534
+ rec.status = this.#jobs.get(newJobId)?.status ?? "running";
535
+ rec.queued = undefined;
536
+ return { ok: true, status: rec.status, jobId: newJobId };
537
+ }
538
+
539
+ /** Drain queued resumes (FIFO by seq) while concurrency slots are available. */
540
+ #drainResumeQueue(): void {
541
+ if (this.#resumeQueue.length === 0) return;
542
+ this.#resumeQueue.sort((a, b) => a.seq - b.seq);
543
+ while (this.#resumeQueue.length > 0 && this.getRunningJobs().length < this.#maxRunningJobs) {
544
+ const entry = this.#resumeQueue.shift();
545
+ if (!entry) return;
546
+ const rec = this.#subagentRecords.get(entry.subagentId);
547
+ if (rec?.status !== "queued") continue;
548
+ this.#startResume(rec, entry.message);
549
+ }
550
+ }
551
+
552
+ /** Cancel a subagent by stable id across running/paused/queued states (keeps the session file). */
553
+ cancelSubagent(subagentId: string, filter?: AsyncJobFilter): boolean {
554
+ const rec = this.getSubagentRecord(subagentId, filter);
555
+ if (!rec) return false;
556
+ if (rec.status === "running" && rec.currentJobId) return this.cancel(rec.currentJobId, filter);
557
+ if (rec.status === "paused") {
558
+ if (rec.currentJobId) {
559
+ const job = this.#jobs.get(rec.currentJobId);
560
+ if (job && job.status === "paused") {
561
+ job.status = "cancelled";
562
+ this.#scheduleEviction(rec.currentJobId);
563
+ }
564
+ }
565
+ rec.status = "cancelled";
566
+ this.#liveHandles.delete(rec.subagentId);
567
+ this.#drainResumeQueue();
568
+ return true;
569
+ }
570
+ if (rec.status === "queued") {
571
+ const idx = this.#resumeQueue.findIndex(e => e.subagentId === rec.subagentId);
572
+ if (idx !== -1) this.#resumeQueue.splice(idx, 1);
573
+ rec.status = "cancelled";
574
+ rec.queued = undefined;
575
+ return true;
576
+ }
577
+ return false;
578
+ }
579
+
580
+ #purgeOwnerSubagentState(ownerId?: string): void {
581
+ for (let i = this.#resumeQueue.length - 1; i >= 0; i--) {
582
+ if (!ownerId || this.#resumeQueue[i].ownerId === ownerId) this.#resumeQueue.splice(i, 1);
583
+ }
584
+ for (const [sid, rec] of this.#subagentRecords) {
585
+ if (!ownerId || rec.ownerId === ownerId) {
586
+ this.#liveHandles.delete(sid);
587
+ this.#resumeDescriptors.delete(sid);
588
+ this.#subagentRecords.delete(sid);
589
+ }
590
+ }
591
+ }
592
+
274
593
  getJob(id: string): AsyncJob | undefined {
275
594
  return this.#jobs.get(id);
276
595
  }
@@ -451,6 +770,7 @@ export class AsyncJobManager {
451
770
  }
452
771
  }
453
772
  }
773
+ this.#purgeOwnerSubagentState(ownerId);
454
774
  }
455
775
 
456
776
  getDeliveryState(filter?: AsyncJobFilter): AsyncJobDeliveryState {
@@ -588,6 +908,12 @@ export class AsyncJobManager {
588
908
  this.#watchedJobs.clear();
589
909
  this.#outputState.clear();
590
910
  this.#ownerCleanups.clear();
911
+ this.#subagentRecords.clear();
912
+ this.#liveHandles.clear();
913
+ this.#resumeDescriptors.clear();
914
+ this.#resumeQueue.length = 0;
915
+ this.#notifyChange();
916
+ this.#changeListeners.clear();
591
917
  return drained;
592
918
  }
593
919
 
@@ -617,6 +943,7 @@ export class AsyncJobManager {
617
943
  }
618
944
 
619
945
  #scheduleEviction(jobId: string): void {
946
+ this.#notifyChange();
620
947
  if (this.#retentionMs <= 0) {
621
948
  this.#jobs.delete(jobId);
622
949
  this.#suppressedDeliveries.delete(jobId);
@@ -634,6 +961,7 @@ export class AsyncJobManager {
634
961
  this.#suppressedDeliveries.delete(jobId);
635
962
  this.#watchedJobs.delete(jobId);
636
963
  this.#outputState.delete(jobId);
964
+ this.#notifyChange();
637
965
  }, this.#retentionMs);
638
966
  timer.unref();
639
967
  this.#evictionTimers.set(jobId, timer);
package/src/cli/args.ts CHANGED
@@ -7,7 +7,7 @@ import chalk from "chalk";
7
7
  import { parseEffort } from "../thinking";
8
8
  import { BUILTIN_TOOLS } from "../tools";
9
9
 
10
- export type Mode = "text" | "json" | "rpc" | "acp" | "rpc-ui";
10
+ export type Mode = "text" | "json" | "rpc" | "acp" | "rpc-ui" | "bridge";
11
11
 
12
12
  export interface Args {
13
13
  cwd?: string;
@@ -96,7 +96,14 @@ export function parseArgs(args: string[]): Args {
96
96
  result.allowHome = true;
97
97
  } else if (arg === "--mode" && i + 1 < args.length) {
98
98
  const mode = args[++i];
99
- if (mode === "text" || mode === "json" || mode === "rpc" || mode === "acp" || mode === "rpc-ui") {
99
+ if (
100
+ mode === "text" ||
101
+ mode === "json" ||
102
+ mode === "rpc" ||
103
+ mode === "acp" ||
104
+ mode === "rpc-ui" ||
105
+ mode === "bridge"
106
+ ) {
100
107
  result.mode = mode;
101
108
  }
102
109
  } else if (arg === "--continue" || arg === "-c") {
@@ -69,6 +69,7 @@ const CALLBACK_PORTS: Record<string, number> = {
69
69
  "google-gemini-cli": 8085,
70
70
  "google-antigravity": 51121,
71
71
  "gitlab-duo": 8080,
72
+ xai: 56121,
72
73
  };
73
74
 
74
75
  function getTokenFilePath(): string {
@@ -12,12 +12,12 @@ import {
12
12
  getEnumValues,
13
13
  getType,
14
14
  getUi,
15
+ SETTINGS_SCHEMA,
15
16
  type SettingPath,
16
17
  Settings,
17
18
  type SettingValue,
18
19
  settings,
19
20
  } from "../config/settings";
20
- import { SETTINGS_SCHEMA } from "../config/settings-schema";
21
21
  import { theme } from "../modes/theme/theme";
22
22
  import { initXdg } from "./commands/init-xdg";
23
23
 
@@ -183,10 +183,18 @@ function parseAndSetValue(path: SettingPath, rawValue: string): void {
183
183
  else throw new Error(`Invalid boolean value: ${rawValue}. Use true/false, yes/no, on/off, or 1/0`);
184
184
  break;
185
185
  }
186
- case "number":
186
+ case "number": {
187
187
  parsedValue = Number(trimmed);
188
188
  if (!Number.isFinite(parsedValue)) throw new Error(`Invalid number: ${rawValue}`);
189
+ const validate =
190
+ "validate" in SETTINGS_SCHEMA[path]
191
+ ? (SETTINGS_SCHEMA[path].validate as ((value: number) => boolean) | undefined)
192
+ : undefined;
193
+ if (validate?.(parsedValue as number) === false) {
194
+ throw new Error(`Invalid number for ${path}: ${rawValue}`);
195
+ }
189
196
  break;
197
+ }
190
198
  case "enum": {
191
199
  const valid = getEnumValues(path);
192
200
  if (valid && !valid.includes(trimmed)) {
package/src/cli.ts CHANGED
@@ -35,9 +35,11 @@ const commands: CommandEntry[] = [
35
35
  { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
36
36
  { name: "skills", load: () => import("./commands/skills").then(m => m.default) },
37
37
  { name: "session", load: () => import("./commands/session").then(m => m.default) },
38
+ { name: "harness", load: () => import("./commands/harness").then(m => m.default) },
38
39
  { name: "team", load: () => import("./commands/team").then(m => m.default) },
39
40
  { name: "ultragoal", load: () => import("./commands/ultragoal").then(m => m.default) },
40
41
  { name: "ralplan", load: () => import("./commands/ralplan").then(m => m.default) },
42
+ { name: "config", load: () => import("./commands/config").then(m => m.default) },
41
43
  {
42
44
  name: "contribute-pr",
43
45
  aliases: ["contribution-prep"],
@@ -21,6 +21,7 @@ export default class DeepInterview extends Command {
21
21
  deliberate: Flags.boolean({
22
22
  description: "Shortcut for --write handoff to ralplan in deliberate consensus mode",
23
23
  }),
24
+ force: Flags.boolean({ description: "Overwrite corrupt existing deep-interview state during --write" }),
24
25
  json: Flags.boolean({ description: "Output JSON" }),
25
26
  };
26
27
  static examples = [