@gajae-code/coding-agent 0.2.5 → 0.3.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 (112) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/types/async/job-manager.d.ts +84 -2
  3. package/dist/types/commands/harness.d.ts +37 -0
  4. package/dist/types/config/settings-schema.d.ts +6 -0
  5. package/dist/types/config/settings.d.ts +2 -0
  6. package/dist/types/deep-interview/render-middleware.d.ts +5 -0
  7. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  9. package/dist/types/extensibility/shared-events.d.ts +1 -0
  10. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  11. package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
  12. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  14. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
  16. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  17. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  18. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
  20. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  21. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  22. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  23. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  24. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  25. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  26. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  27. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  28. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  29. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  30. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  31. package/dist/types/harness-control-plane/types.d.ts +162 -0
  32. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  33. package/dist/types/hooks/skill-state.d.ts +2 -29
  34. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  35. package/dist/types/modes/interactive-mode.d.ts +1 -0
  36. package/dist/types/modes/types.d.ts +1 -0
  37. package/dist/types/sdk.d.ts +2 -0
  38. package/dist/types/session/agent-session.d.ts +8 -0
  39. package/dist/types/skill-state/active-state.d.ts +2 -0
  40. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  41. package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
  42. package/dist/types/task/executor.d.ts +3 -0
  43. package/dist/types/task/types.d.ts +55 -3
  44. package/dist/types/tools/subagent.d.ts +11 -1
  45. package/package.json +7 -7
  46. package/src/async/job-manager.ts +298 -6
  47. package/src/cli/auth-broker-cli.ts +1 -0
  48. package/src/cli/config-cli.ts +10 -2
  49. package/src/cli.ts +2 -0
  50. package/src/commands/harness.ts +592 -0
  51. package/src/commands/team.ts +36 -39
  52. package/src/config/settings-schema.ts +7 -0
  53. package/src/config/settings.ts +5 -0
  54. package/src/deep-interview/render-middleware.ts +366 -0
  55. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  56. package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
  57. package/src/extensibility/custom-tools/types.ts +1 -0
  58. package/src/extensibility/extensions/types.ts +6 -0
  59. package/src/extensibility/shared-events.ts +1 -0
  60. package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
  61. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  62. package/src/gjc-runtime/ralplan-runtime.ts +25 -10
  63. package/src/gjc-runtime/state-graph.ts +86 -0
  64. package/src/gjc-runtime/state-migrations.ts +132 -0
  65. package/src/gjc-runtime/state-renderer.ts +345 -0
  66. package/src/gjc-runtime/state-runtime.ts +733 -21
  67. package/src/gjc-runtime/state-validation.ts +49 -0
  68. package/src/gjc-runtime/state-writer.ts +718 -0
  69. package/src/gjc-runtime/team-runtime.ts +1083 -89
  70. package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
  71. package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
  72. package/src/gjc-runtime/workflow-manifest.ts +425 -0
  73. package/src/harness-control-plane/classifier.ts +128 -0
  74. package/src/harness-control-plane/control-endpoint.ts +137 -0
  75. package/src/harness-control-plane/finalize.ts +222 -0
  76. package/src/harness-control-plane/frame-mapper.ts +286 -0
  77. package/src/harness-control-plane/operate.ts +225 -0
  78. package/src/harness-control-plane/owner.ts +553 -0
  79. package/src/harness-control-plane/preserve.ts +102 -0
  80. package/src/harness-control-plane/receipts.ts +216 -0
  81. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  82. package/src/harness-control-plane/seams.ts +39 -0
  83. package/src/harness-control-plane/session-lease.ts +388 -0
  84. package/src/harness-control-plane/state-machine.ts +97 -0
  85. package/src/harness-control-plane/storage.ts +257 -0
  86. package/src/harness-control-plane/types.ts +214 -0
  87. package/src/hooks/skill-keywords.ts +4 -2
  88. package/src/hooks/skill-state.ts +24 -41
  89. package/src/internal-urls/docs-index.generated.ts +1 -1
  90. package/src/modes/components/assistant-message.ts +5 -1
  91. package/src/modes/components/hook-selector.ts +72 -2
  92. package/src/modes/controllers/event-controller.ts +71 -6
  93. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  94. package/src/modes/controllers/input-controller.ts +9 -1
  95. package/src/modes/controllers/selector-controller.ts +2 -1
  96. package/src/modes/interactive-mode.ts +1 -0
  97. package/src/modes/types.ts +1 -0
  98. package/src/prompts/agents/executor.md +13 -0
  99. package/src/prompts/tools/subagent.md +33 -3
  100. package/src/sdk.ts +4 -0
  101. package/src/session/agent-session.ts +231 -33
  102. package/src/session/session-manager.ts +13 -1
  103. package/src/skill-state/active-state.ts +58 -65
  104. package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
  105. package/src/skill-state/initial-phase.ts +2 -0
  106. package/src/skill-state/workflow-state-contract.ts +26 -0
  107. package/src/task/executor.ts +50 -8
  108. package/src/task/index.ts +120 -8
  109. package/src/task/render.ts +6 -3
  110. package/src/task/types.ts +56 -3
  111. package/src/tools/ask.ts +28 -7
  112. package/src/tools/subagent.ts +255 -64
@@ -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;
@@ -37,6 +37,64 @@ export interface AsyncJobMetadata {
37
37
  };
38
38
  }
39
39
 
40
+ /**
41
+ * Typed outcome a subagent task run may produce. A `paused` outcome is
42
+ * non-terminal and non-delivering: the run suspended at a safe boundary and the
43
+ * subagent can be resumed from its persisted sessionFile. `completed` always
44
+ * wins a race with a late pause because the run returns it once it has actually
45
+ * finished.
46
+ */
47
+ export type SubagentRunOutcome = { kind: "completed"; text: string } | { kind: "paused"; note?: string };
48
+
49
+ /** Canonical lifecycle of a subagent across pause/resume cycles. */
50
+ export type SubagentLifecycle = "running" | "paused" | "queued" | "completed" | "failed" | "cancelled";
51
+
52
+ /**
53
+ * Live, executor-owned control handle for a RUNNING subagent. Registered when a
54
+ * subagent run starts and removed on pause/terminal so a paused subagent retains
55
+ * no live `AgentSession` reference (leak-free).
56
+ */
57
+ export interface SubagentLiveHandle {
58
+ /** Request a cooperative safe-boundary pause (never aborts the in-flight tool). */
59
+ requestPause(): void;
60
+ /** Inject a steering message into the live session. */
61
+ injectMessage(content: string, deliverAs: "steer" | "followUp" | "nextTurn"): Promise<void>;
62
+ }
63
+
64
+ /**
65
+ * Canonical, stable-id-keyed record for a subagent. Survives `AsyncJob`
66
+ * eviction so resume stays addressable by subagent id, and is the single source
67
+ * of truth for control-plane status and identity.
68
+ */
69
+ export interface SubagentRecord {
70
+ subagentId: string;
71
+ ownerId?: string;
72
+ /** Current live/last AsyncJob id; null while queued with no active job. */
73
+ currentJobId: string | null;
74
+ historicalJobIds: string[];
75
+ status: SubagentLifecycle;
76
+ sessionFile: string | null;
77
+ /** False for ephemeral sessions (no persistent artifacts dir). */
78
+ resumable: boolean;
79
+ queued?: { ownerId?: string; seq: number; message?: string; createdAt: number };
80
+ }
81
+
82
+ /** Lightweight, manager-owned resume payload. The async layer treats `data` as opaque. */
83
+ export interface ResumeDescriptor {
84
+ subagentId: string;
85
+ ownerId?: string;
86
+ data: unknown;
87
+ }
88
+
89
+ /** A pending resume awaiting a free concurrency slot. */
90
+ interface ResumeQueueEntry {
91
+ subagentId: string;
92
+ ownerId?: string;
93
+ seq: number;
94
+ message?: string;
95
+ createdAt: number;
96
+ }
97
+
40
98
  export interface AsyncJobManagerOptions {
41
99
  onJobComplete: (jobId: string, text: string, job?: AsyncJob) => void | Promise<void>;
42
100
  maxRunningJobs?: number;
@@ -160,6 +218,12 @@ export class AsyncJobManager {
160
218
  readonly #retentionMs: number;
161
219
  #deliveryLoop: Promise<void> | undefined;
162
220
  #disposed = false;
221
+ readonly #subagentRecords = new Map<string, SubagentRecord>();
222
+ readonly #liveHandles = new Map<string, SubagentLiveHandle>();
223
+ readonly #resumeQueue: ResumeQueueEntry[] = [];
224
+ #resumeSeq = 0;
225
+ #resumeRunner?: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined;
226
+ readonly #resumeDescriptors = new Map<string, ResumeDescriptor>();
163
227
 
164
228
  #filterJobs(jobs: Iterable<AsyncJob>, filter?: AsyncJobFilter): AsyncJob[] {
165
229
  const ownerId = filter?.ownerId;
@@ -184,7 +248,7 @@ export class AsyncJobManager {
184
248
  jobId: string;
185
249
  signal: AbortSignal;
186
250
  reportProgress: (text: string, details?: Record<string, unknown>) => Promise<void>;
187
- }) => Promise<string>,
251
+ }) => Promise<string | SubagentRunOutcome>,
188
252
  options?: AsyncJobRegisterOptions,
189
253
  ): string {
190
254
  if (this.#disposed) {
@@ -227,20 +291,38 @@ export class AsyncJobManager {
227
291
  };
228
292
  job.promise = (async () => {
229
293
  try {
230
- const text = await run({ jobId: id, signal: abortController.signal, reportProgress });
294
+ const result = await run({ jobId: id, signal: abortController.signal, reportProgress });
295
+ const outcome: SubagentRunOutcome =
296
+ typeof result === "string" ? { kind: "completed", text: result } : result;
231
297
  if (job.status === "cancelled") {
232
- job.resultText = text;
298
+ job.resultText = outcome.kind === "completed" ? outcome.text : outcome.note;
233
299
  this.#scheduleEviction(id);
300
+ this.#markRecordTerminal(id, "cancelled");
301
+ this.#drainResumeQueue();
302
+ return;
303
+ }
304
+ if (outcome.kind === "paused") {
305
+ // Sole canonical writer of the running -> paused transition. No
306
+ // delivery and no eviction scheduling: a paused subagent stays
307
+ // listed and resumable from its sessionFile.
308
+ job.status = "paused";
309
+ if (outcome.note) job.resultText = outcome.note;
310
+ this.#markRecordPaused(id);
311
+ this.#drainResumeQueue();
234
312
  return;
235
313
  }
236
314
  job.status = "completed";
237
- job.resultText = text;
238
- this.#enqueueDelivery(id, text);
315
+ job.resultText = outcome.text;
316
+ this.#enqueueDelivery(id, outcome.text);
239
317
  this.#scheduleEviction(id);
318
+ this.#markRecordTerminal(id, "completed");
319
+ this.#drainResumeQueue();
240
320
  } catch (error) {
241
321
  if (job.status === "cancelled") {
242
322
  job.errorText = error instanceof Error ? error.message : String(error);
243
323
  this.#scheduleEviction(id);
324
+ this.#markRecordTerminal(id, "cancelled");
325
+ this.#drainResumeQueue();
244
326
  return;
245
327
  }
246
328
  const errorText = error instanceof Error ? error.message : String(error);
@@ -248,6 +330,8 @@ export class AsyncJobManager {
248
330
  job.errorText = errorText;
249
331
  this.#enqueueDelivery(id, errorText);
250
332
  this.#scheduleEviction(id);
333
+ this.#markRecordTerminal(id, "failed");
334
+ this.#drainResumeQueue();
251
335
  }
252
336
  })();
253
337
 
@@ -264,6 +348,15 @@ export class AsyncJobManager {
264
348
  const job = this.#jobs.get(id);
265
349
  if (!job) return false;
266
350
  if (filter?.ownerId && job.ownerId !== filter.ownerId) return false;
351
+ if (job.status === "paused") {
352
+ // Paused jobs have no running promise to abort; transition directly.
353
+ // The session file is kept, so the record stays resumable by id.
354
+ job.status = "cancelled";
355
+ this.#markRecordTerminal(id, "cancelled");
356
+ this.#scheduleEviction(id);
357
+ this.#drainResumeQueue();
358
+ return true;
359
+ }
267
360
  if (job.status !== "running") return false;
268
361
  job.status = "cancelled";
269
362
  job.abortController.abort();
@@ -271,6 +364,200 @@ export class AsyncJobManager {
271
364
  return true;
272
365
  }
273
366
 
367
+ // ── Subagent control plane (pause / resume / steer support) ──────────
368
+
369
+ /** Register or replace the canonical record for a subagent. */
370
+ registerSubagentRecord(record: SubagentRecord): void {
371
+ this.#subagentRecords.set(record.subagentId, record);
372
+ }
373
+
374
+ getSubagentRecord(subagentId: string, filter?: AsyncJobFilter): SubagentRecord | undefined {
375
+ const rec = this.#subagentRecords.get(subagentId.trim());
376
+ if (!rec) return undefined;
377
+ if (filter?.ownerId && rec.ownerId !== filter.ownerId) return undefined;
378
+ return rec;
379
+ }
380
+
381
+ getSubagentRecords(filter?: AsyncJobFilter): SubagentRecord[] {
382
+ const ownerId = filter?.ownerId;
383
+ const out: SubagentRecord[] = [];
384
+ for (const rec of this.#subagentRecords.values()) {
385
+ if (ownerId && rec.ownerId !== ownerId) continue;
386
+ out.push(rec);
387
+ }
388
+ return out;
389
+ }
390
+
391
+ registerLiveHandle(subagentId: string, handle: SubagentLiveHandle): void {
392
+ this.#liveHandles.set(subagentId, handle);
393
+ }
394
+
395
+ getLiveHandle(subagentId: string): SubagentLiveHandle | undefined {
396
+ return this.#liveHandles.get(subagentId);
397
+ }
398
+
399
+ removeLiveHandle(subagentId: string): void {
400
+ this.#liveHandles.delete(subagentId);
401
+ }
402
+
403
+ /** Install the TaskTool-owned resume runner. Returns the new job id, or undefined on failure. */
404
+ setResumeRunner(
405
+ runner: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined,
406
+ ): void {
407
+ this.#resumeRunner = runner;
408
+ }
409
+
410
+ registerResumeDescriptor(descriptor: ResumeDescriptor): void {
411
+ this.#resumeDescriptors.set(descriptor.subagentId, descriptor);
412
+ }
413
+
414
+ getResumeDescriptor(subagentId: string, filter?: AsyncJobFilter): ResumeDescriptor | undefined {
415
+ const descriptor = this.#resumeDescriptors.get(subagentId.trim());
416
+ if (!descriptor) return undefined;
417
+ if (filter?.ownerId && descriptor.ownerId !== filter.ownerId) return undefined;
418
+ return descriptor;
419
+ }
420
+
421
+ #recordByJobId(jobId: string): SubagentRecord | undefined {
422
+ for (const rec of this.#subagentRecords.values()) {
423
+ if (rec.currentJobId === jobId) return rec;
424
+ }
425
+ return undefined;
426
+ }
427
+
428
+ #markRecordPaused(jobId: string): void {
429
+ const rec = this.#recordByJobId(jobId);
430
+ if (rec) {
431
+ rec.status = "paused";
432
+ this.#liveHandles.delete(rec.subagentId);
433
+ }
434
+ }
435
+
436
+ #markRecordTerminal(jobId: string, status: "completed" | "failed" | "cancelled"): void {
437
+ const rec = this.#recordByJobId(jobId);
438
+ if (!rec) return;
439
+ rec.status = status;
440
+ this.#liveHandles.delete(rec.subagentId);
441
+ }
442
+
443
+ /** Request a graceful safe-boundary pause of a running subagent. */
444
+ pauseSubagent(
445
+ subagentId: string,
446
+ filter?: AsyncJobFilter,
447
+ ): { ok: boolean; status?: SubagentLifecycle; reason?: string } {
448
+ const rec = this.getSubagentRecord(subagentId, filter);
449
+ if (!rec) return { ok: false, reason: "not_found" };
450
+ if (rec.status !== "running") return { ok: false, status: rec.status, reason: "not_running" };
451
+ const handle = this.#liveHandles.get(rec.subagentId);
452
+ if (!handle) return { ok: false, status: rec.status, reason: "no_live_handle" };
453
+ handle.requestPause();
454
+ return { ok: true, status: rec.status };
455
+ }
456
+
457
+ /** Resume a non-running subagent from its sessionFile, optionally injecting a message first. */
458
+ resumeSubagent(
459
+ subagentId: string,
460
+ filter?: AsyncJobFilter,
461
+ message?: string,
462
+ ): { ok: boolean; status?: SubagentLifecycle; jobId?: string; queued?: boolean; reason?: string } {
463
+ const rec = this.getSubagentRecord(subagentId, filter);
464
+ if (!rec) return { ok: false, reason: "not_found" };
465
+ if (rec.status === "running") return { ok: false, status: "running", reason: "already_running" };
466
+ if (rec.status === "queued") {
467
+ if (message !== undefined && rec.queued) {
468
+ rec.queued.message = message;
469
+ const queued = this.#resumeQueue.find(entry => entry.subagentId === rec.subagentId);
470
+ if (queued) queued.message = message;
471
+ return { ok: true, queued: true, status: "queued" };
472
+ }
473
+ return { ok: false, status: "queued", reason: "already_queued" };
474
+ }
475
+ if (!rec.resumable || !rec.sessionFile) return { ok: false, reason: "context_unavailable" };
476
+ if (!this.#resumeRunner) return { ok: false, reason: "no_runner" };
477
+ if (this.getRunningJobs().length >= this.#maxRunningJobs) {
478
+ const seq = ++this.#resumeSeq;
479
+ rec.status = "queued";
480
+ rec.queued = { ownerId: rec.ownerId, seq, message, createdAt: Date.now() };
481
+ this.#resumeQueue.push({
482
+ subagentId: rec.subagentId,
483
+ ownerId: rec.ownerId,
484
+ seq,
485
+ message,
486
+ createdAt: rec.queued.createdAt,
487
+ });
488
+ return { ok: true, queued: true, status: "queued" };
489
+ }
490
+ return this.#startResume(rec, message);
491
+ }
492
+
493
+ #startResume(
494
+ rec: SubagentRecord,
495
+ message?: string,
496
+ ): { ok: boolean; status?: SubagentLifecycle; jobId?: string; reason?: string } {
497
+ const prevJobId = rec.currentJobId;
498
+ const newJobId = this.#resumeRunner?.(rec.subagentId, message, this.#resumeDescriptors.get(rec.subagentId));
499
+ if (!newJobId) return { ok: false, reason: "resume_failed" };
500
+ if (prevJobId && prevJobId !== newJobId) rec.historicalJobIds.push(prevJobId);
501
+ rec.currentJobId = newJobId;
502
+ rec.status = this.#jobs.get(newJobId)?.status ?? "running";
503
+ rec.queued = undefined;
504
+ return { ok: true, status: rec.status, jobId: newJobId };
505
+ }
506
+
507
+ /** Drain queued resumes (FIFO by seq) while concurrency slots are available. */
508
+ #drainResumeQueue(): void {
509
+ if (this.#resumeQueue.length === 0) return;
510
+ this.#resumeQueue.sort((a, b) => a.seq - b.seq);
511
+ while (this.#resumeQueue.length > 0 && this.getRunningJobs().length < this.#maxRunningJobs) {
512
+ const entry = this.#resumeQueue.shift();
513
+ if (!entry) return;
514
+ const rec = this.#subagentRecords.get(entry.subagentId);
515
+ if (rec?.status !== "queued") continue;
516
+ this.#startResume(rec, entry.message);
517
+ }
518
+ }
519
+
520
+ /** Cancel a subagent by stable id across running/paused/queued states (keeps the session file). */
521
+ cancelSubagent(subagentId: string, filter?: AsyncJobFilter): boolean {
522
+ const rec = this.getSubagentRecord(subagentId, filter);
523
+ if (!rec) return false;
524
+ if (rec.status === "running" && rec.currentJobId) return this.cancel(rec.currentJobId, filter);
525
+ if (rec.status === "paused") {
526
+ if (rec.currentJobId) {
527
+ const job = this.#jobs.get(rec.currentJobId);
528
+ if (job && job.status === "paused") {
529
+ job.status = "cancelled";
530
+ this.#scheduleEviction(rec.currentJobId);
531
+ }
532
+ }
533
+ rec.status = "cancelled";
534
+ this.#liveHandles.delete(rec.subagentId);
535
+ this.#drainResumeQueue();
536
+ return true;
537
+ }
538
+ if (rec.status === "queued") {
539
+ const idx = this.#resumeQueue.findIndex(e => e.subagentId === rec.subagentId);
540
+ if (idx !== -1) this.#resumeQueue.splice(idx, 1);
541
+ rec.status = "cancelled";
542
+ rec.queued = undefined;
543
+ return true;
544
+ }
545
+ return false;
546
+ }
547
+
548
+ #purgeOwnerSubagentState(ownerId?: string): void {
549
+ for (let i = this.#resumeQueue.length - 1; i >= 0; i--) {
550
+ if (!ownerId || this.#resumeQueue[i].ownerId === ownerId) this.#resumeQueue.splice(i, 1);
551
+ }
552
+ for (const [sid, rec] of this.#subagentRecords) {
553
+ if (!ownerId || rec.ownerId === ownerId) {
554
+ this.#liveHandles.delete(sid);
555
+ this.#resumeDescriptors.delete(sid);
556
+ this.#subagentRecords.delete(sid);
557
+ }
558
+ }
559
+ }
560
+
274
561
  getJob(id: string): AsyncJob | undefined {
275
562
  return this.#jobs.get(id);
276
563
  }
@@ -451,6 +738,7 @@ export class AsyncJobManager {
451
738
  }
452
739
  }
453
740
  }
741
+ this.#purgeOwnerSubagentState(ownerId);
454
742
  }
455
743
 
456
744
  getDeliveryState(filter?: AsyncJobFilter): AsyncJobDeliveryState {
@@ -588,6 +876,10 @@ export class AsyncJobManager {
588
876
  this.#watchedJobs.clear();
589
877
  this.#outputState.clear();
590
878
  this.#ownerCleanups.clear();
879
+ this.#subagentRecords.clear();
880
+ this.#liveHandles.clear();
881
+ this.#resumeDescriptors.clear();
882
+ this.#resumeQueue.length = 0;
591
883
  return drained;
592
884
  }
593
885
 
@@ -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"],