@gajae-code/coding-agent 0.4.2 → 0.4.4

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 (115) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/types/async/job-manager.d.ts +44 -1
  3. package/dist/types/cli/setup-cli.d.ts +14 -1
  4. package/dist/types/commands/coordinator.d.ts +19 -0
  5. package/dist/types/commands/mcp-serve.d.ts +24 -0
  6. package/dist/types/commands/setup.d.ts +41 -0
  7. package/dist/types/commit/model-selection.d.ts +1 -1
  8. package/dist/types/config/model-registry.d.ts +3 -1
  9. package/dist/types/config/model-resolver.d.ts +1 -19
  10. package/dist/types/config/models-config-schema.d.ts +12 -0
  11. package/dist/types/config/settings-schema.d.ts +15 -1
  12. package/dist/types/coordinator/contract.d.ts +4 -0
  13. package/dist/types/coordinator-mcp/policy.d.ts +24 -0
  14. package/dist/types/coordinator-mcp/safety.d.ts +26 -0
  15. package/dist/types/coordinator-mcp/server.d.ts +52 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +13 -0
  17. package/dist/types/gjc-runtime/goal-mode-request.d.ts +8 -1
  18. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  19. package/dist/types/harness-control-plane/types.d.ts +7 -2
  20. package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
  21. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  22. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  23. package/dist/types/modes/shared/agent-wire/command-contract.d.ts +18 -0
  24. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +84 -0
  25. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +14 -7
  26. package/dist/types/modes/shared/agent-wire/event-observation.d.ts +37 -0
  27. package/dist/types/modes/shared/agent-wire/protocol.d.ts +13 -34
  28. package/dist/types/session/agent-session.d.ts +12 -1
  29. package/dist/types/session/session-manager.d.ts +1 -1
  30. package/dist/types/setup/hermes-setup.d.ts +71 -0
  31. package/dist/types/task/render.d.ts +7 -1
  32. package/dist/types/tools/bash.d.ts +2 -0
  33. package/dist/types/tools/browser/actions.d.ts +54 -0
  34. package/dist/types/tools/browser.d.ts +80 -0
  35. package/dist/types/tools/image-gen.d.ts +1 -0
  36. package/dist/types/tools/index.d.ts +3 -1
  37. package/dist/types/tools/job.d.ts +1 -1
  38. package/dist/types/tools/subagent-render.d.ts +25 -0
  39. package/dist/types/tools/subagent.d.ts +5 -1
  40. package/package.json +7 -7
  41. package/src/async/job-manager.ts +163 -2
  42. package/src/cli/setup-cli.ts +86 -2
  43. package/src/cli.ts +2 -0
  44. package/src/commands/coordinator.ts +70 -0
  45. package/src/commands/mcp-serve.ts +62 -0
  46. package/src/commands/setup.ts +30 -1
  47. package/src/commands/ultragoal.ts +7 -1
  48. package/src/commit/agentic/index.ts +2 -2
  49. package/src/commit/model-selection.ts +7 -22
  50. package/src/commit/pipeline.ts +2 -2
  51. package/src/config/model-registry.ts +17 -9
  52. package/src/config/model-resolver.ts +14 -84
  53. package/src/config/models-config-schema.ts +2 -0
  54. package/src/config/settings-schema.ts +14 -1
  55. package/src/coordinator/contract.ts +20 -0
  56. package/src/coordinator-mcp/policy.ts +160 -0
  57. package/src/coordinator-mcp/safety.ts +80 -0
  58. package/src/coordinator-mcp/server.ts +1316 -0
  59. package/src/extensibility/extensions/types.ts +13 -0
  60. package/src/gjc-runtime/goal-mode-request.ts +21 -1
  61. package/src/gjc-runtime/session-state-sidecar.ts +79 -0
  62. package/src/harness-control-plane/owner.ts +3 -3
  63. package/src/harness-control-plane/rpc-adapter.ts +7 -1
  64. package/src/harness-control-plane/types.ts +8 -11
  65. package/src/internal-urls/docs-index.generated.ts +6 -5
  66. package/src/memories/index.ts +1 -1
  67. package/src/modes/acp/acp-agent.ts +17 -9
  68. package/src/modes/acp/acp-event-mapper.ts +33 -1
  69. package/src/modes/components/custom-editor.ts +19 -3
  70. package/src/modes/components/hook-selector.ts +109 -5
  71. package/src/modes/controllers/extension-ui-controller.ts +16 -1
  72. package/src/modes/controllers/input-controller.ts +27 -7
  73. package/src/modes/controllers/selector-controller.ts +7 -1
  74. package/src/modes/interactive-mode.ts +3 -1
  75. package/src/modes/rpc/rpc-client.ts +16 -3
  76. package/src/modes/rpc/rpc-mode.ts +5 -2
  77. package/src/modes/shared/agent-wire/command-contract.ts +18 -0
  78. package/src/modes/shared/agent-wire/event-contract.ts +147 -0
  79. package/src/modes/shared/agent-wire/event-envelope.ts +35 -16
  80. package/src/modes/shared/agent-wire/event-observation.ts +397 -0
  81. package/src/modes/shared/agent-wire/protocol.ts +24 -81
  82. package/src/modes/utils/context-usage.ts +2 -2
  83. package/src/prompts/agents/architect.md +6 -0
  84. package/src/prompts/agents/critic.md +6 -0
  85. package/src/prompts/agents/explore.md +1 -1
  86. package/src/prompts/agents/plan.md +1 -1
  87. package/src/prompts/agents/planner.md +8 -1
  88. package/src/prompts/agents/reviewer.md +1 -1
  89. package/src/prompts/tools/browser.md +3 -2
  90. package/src/runtime-mcp/manager.ts +15 -2
  91. package/src/sdk.ts +3 -1
  92. package/src/session/agent-session.ts +66 -4
  93. package/src/session/session-manager.ts +1 -1
  94. package/src/setup/hermes/templates/operator-instructions.v1.md +29 -0
  95. package/src/setup/hermes-setup.ts +429 -0
  96. package/src/task/agents.ts +1 -1
  97. package/src/task/index.ts +2 -0
  98. package/src/task/render.ts +14 -0
  99. package/src/tools/ask.ts +30 -10
  100. package/src/tools/bash.ts +6 -1
  101. package/src/tools/browser/actions.ts +189 -0
  102. package/src/tools/browser.ts +91 -1
  103. package/src/tools/image-gen.ts +42 -15
  104. package/src/tools/index.ts +7 -1
  105. package/src/tools/inspect-image.ts +10 -8
  106. package/src/tools/job.ts +12 -2
  107. package/src/tools/monitor.ts +98 -17
  108. package/src/tools/renderers.ts +2 -0
  109. package/src/tools/subagent-render.ts +160 -0
  110. package/src/tools/subagent.ts +49 -7
  111. package/src/utils/commit-message-generator.ts +6 -13
  112. package/src/utils/title-generator.ts +1 -1
  113. package/dist/types/harness-control-plane/frame-mapper.d.ts +0 -29
  114. package/src/harness-control-plane/frame-mapper.ts +0 -286
  115. package/src/priority.json +0 -37
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.4.2",
4
+ "version": "0.4.4",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -50,12 +50,12 @@
50
50
  "@agentclientprotocol/sdk": "0.21.0",
51
51
  "@babel/parser": "^7.29.3",
52
52
  "@mozilla/readability": "^0.6.0",
53
- "@gajae-code/stats": "0.4.2",
54
- "@gajae-code/agent-core": "0.4.2",
55
- "@gajae-code/ai": "0.4.2",
56
- "@gajae-code/natives": "0.4.2",
57
- "@gajae-code/tui": "0.4.2",
58
- "@gajae-code/utils": "0.4.2",
53
+ "@gajae-code/stats": "0.4.4",
54
+ "@gajae-code/agent-core": "0.4.4",
55
+ "@gajae-code/ai": "0.4.4",
56
+ "@gajae-code/natives": "0.4.4",
57
+ "@gajae-code/tui": "0.4.4",
58
+ "@gajae-code/utils": "0.4.4",
59
59
  "@puppeteer/browsers": "^2.13.0",
60
60
  "@types/turndown": "5.0.6",
61
61
  "@xterm/headless": "^6.0.0",
@@ -1,11 +1,12 @@
1
1
  import { logger } from "@gajae-code/utils";
2
- import type { AgentSource } from "../task/types";
2
+ import type { AgentProgress, AgentSource } from "../task/types";
3
3
 
4
4
  const DELIVERY_RETRY_BASE_MS = 500;
5
5
  const DELIVERY_RETRY_MAX_MS = 30_000;
6
6
  const DELIVERY_RETRY_JITTER_MS = 200;
7
7
  const DEFAULT_RETENTION_MS = 5 * 60 * 1000;
8
8
  const DEFAULT_MAX_RUNNING_JOBS = 15;
9
+ const MONITOR_TOMBSTONE_TTL_MS = 5 * 60_000;
9
10
 
10
11
  export interface AsyncJob {
11
12
  id: string;
@@ -120,6 +121,27 @@ export interface AsyncJobDeliveryState {
120
121
  pendingJobIds: string[];
121
122
  }
122
123
 
124
+ export interface AsyncJobLifecycleCleanup {
125
+ onCancel?: (job: AsyncJob) => void;
126
+ onTerminal?: (job: AsyncJob) => void;
127
+ onEvict?: (job: AsyncJob) => void;
128
+ /**
129
+ * Idempotent residual cleanup invoked by a post-eviction tombstone purge
130
+ * (e.g. a late `job cancel` after the job left the registry). Kept distinct
131
+ * from the at-most-once lifecycle phases so a tombstone purge never has to
132
+ * re-invoke a phase hook. Must be safe to call repeatedly.
133
+ */
134
+ onTombstonePurge?: (job: AsyncJob) => void;
135
+ }
136
+
137
+ export interface MonitorTombstone {
138
+ jobId: string;
139
+ ownerId?: string;
140
+ status: AsyncJob["status"];
141
+ expiresAt: number;
142
+ purge: () => unknown;
143
+ }
144
+
123
145
  export interface AsyncJobRegisterOptions {
124
146
  id?: string;
125
147
  /** Registry id of the agent that owns this job; used to scope cancelAll. */
@@ -127,6 +149,7 @@ export interface AsyncJobRegisterOptions {
127
149
  /** Structured metadata for tool-specific control surfaces. */
128
150
  metadata?: AsyncJobMetadata;
129
151
  onProgress?: (text: string, details?: Record<string, unknown>) => void | Promise<void>;
152
+ lifecycle?: AsyncJobLifecycleCleanup;
130
153
  }
131
154
 
132
155
  /**
@@ -214,6 +237,9 @@ export class AsyncJobManager {
214
237
  readonly #evictionTimers = new Map<string, NodeJS.Timeout>();
215
238
  readonly #outputState = new Map<string, AsyncJobOutputState>();
216
239
  readonly #ownerCleanups = new Map<string, Set<() => void>>();
240
+ readonly #lifecycles = new Map<string, AsyncJobLifecycleCleanup>();
241
+ readonly #lifecyclePhases = new Map<string, Set<"cancel" | "terminal" | "evict">>();
242
+ readonly #monitorTombstones = new Map<string, MonitorTombstone>();
217
243
  readonly #outputRetentionBytes = DEFAULT_JOB_OUTPUT_RETENTION_BYTES;
218
244
  readonly #onJobComplete: AsyncJobManagerOptions["onJobComplete"];
219
245
  readonly #maxRunningJobs: number;
@@ -222,6 +248,7 @@ export class AsyncJobManager {
222
248
  #disposed = false;
223
249
  readonly #subagentRecords = new Map<string, SubagentRecord>();
224
250
  readonly #liveHandles = new Map<string, SubagentLiveHandle>();
251
+ readonly #subagentProgress = new Map<string, AgentProgress>();
225
252
  readonly #resumeQueue: ResumeQueueEntry[] = [];
226
253
  #resumeSeq = 0;
227
254
  #resumeRunner?: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined;
@@ -292,6 +319,7 @@ export class AsyncJobManager {
292
319
  );
293
320
  }
294
321
 
322
+ this.#expireMonitorTombstones();
295
323
  const id = this.#resolveJobId(options?.id);
296
324
  this.#suppressedDeliveries.delete(id);
297
325
  const abortController = new AbortController();
@@ -308,6 +336,7 @@ export class AsyncJobManager {
308
336
  ownerId: options?.ownerId,
309
337
  metadata: options?.metadata,
310
338
  };
339
+ if (options?.lifecycle) this.#lifecycles.set(id, options.lifecycle);
311
340
 
312
341
  const reportProgress = async (text: string, details?: Record<string, unknown>): Promise<void> => {
313
342
  if (!options?.onProgress) return;
@@ -325,8 +354,10 @@ export class AsyncJobManager {
325
354
  const result = await run({ jobId: id, signal: abortController.signal, reportProgress });
326
355
  const outcome: SubagentRunOutcome =
327
356
  typeof result === "string" ? { kind: "completed", text: result } : result;
357
+
328
358
  if (job.status === "cancelled") {
329
359
  job.resultText = outcome.kind === "completed" ? outcome.text : outcome.note;
360
+ this.#runLifecycle(id, "terminal");
330
361
  this.#scheduleEviction(id);
331
362
  this.#markRecordTerminal(id, "cancelled");
332
363
  this.#drainResumeQueue();
@@ -342,20 +373,24 @@ export class AsyncJobManager {
342
373
  this.#drainResumeQueue();
343
374
  return;
344
375
  }
376
+
345
377
  job.status = "completed";
346
378
  job.resultText = outcome.text;
347
379
  this.#enqueueDelivery(id, outcome.text);
380
+ this.#runLifecycle(id, "terminal");
348
381
  this.#scheduleEviction(id);
349
382
  this.#markRecordTerminal(id, "completed");
350
383
  this.#drainResumeQueue();
351
384
  } catch (error) {
352
385
  if (job.status === "cancelled") {
353
386
  job.errorText = error instanceof Error ? error.message : String(error);
387
+ this.#runLifecycle(id, "terminal");
354
388
  this.#scheduleEviction(id);
355
389
  this.#markRecordTerminal(id, "cancelled");
356
390
  this.#drainResumeQueue();
357
391
  return;
358
392
  }
393
+ this.#runLifecycle(id, "terminal");
359
394
  const errorText = error instanceof Error ? error.message : String(error);
360
395
  job.status = "failed";
361
396
  job.errorText = errorText;
@@ -381,6 +416,7 @@ export class AsyncJobManager {
381
416
  if (!job) return false;
382
417
  if (filter?.ownerId && job.ownerId !== filter.ownerId) return false;
383
418
  if (job.status === "paused") {
419
+ this.#runLifecycle(id, "cancel");
384
420
  // Paused jobs have no running promise to abort; transition directly.
385
421
  // The session file is kept, so the record stays resumable by id.
386
422
  job.status = "cancelled";
@@ -390,12 +426,76 @@ export class AsyncJobManager {
390
426
  return true;
391
427
  }
392
428
  if (job.status !== "running") return false;
429
+ this.#runLifecycle(id, "cancel");
393
430
  job.status = "cancelled";
394
431
  job.abortController.abort();
395
- this.#scheduleEviction(id);
396
432
  return true;
397
433
  }
398
434
 
435
+ #runLifecycle(jobId: string, phase: "cancel" | "terminal" | "evict"): void {
436
+ const fired = this.#lifecyclePhases.get(jobId) ?? new Set<"cancel" | "terminal" | "evict">();
437
+ if (fired.has(phase)) return;
438
+ fired.add(phase);
439
+ this.#lifecyclePhases.set(jobId, fired);
440
+ const lifecycle = this.#lifecycles.get(jobId);
441
+ const job = this.#jobs.get(jobId);
442
+ if (!lifecycle || !job) return;
443
+ try {
444
+ if (phase === "cancel") lifecycle.onCancel?.(job);
445
+ else if (phase === "terminal") lifecycle.onTerminal?.(job);
446
+ else lifecycle.onEvict?.(job);
447
+ } catch (error) {
448
+ logger.warn("Async job lifecycle cleanup failed", {
449
+ jobId,
450
+ phase,
451
+ error: error instanceof Error ? error.message : String(error),
452
+ });
453
+ }
454
+ }
455
+
456
+ #expireMonitorTombstones(): void {
457
+ const now = Date.now();
458
+ for (const [jobId, tombstone] of this.#monitorTombstones) {
459
+ if (tombstone.expiresAt <= now) this.#monitorTombstones.delete(jobId);
460
+ }
461
+ }
462
+
463
+ #recordMonitorTombstone(jobId: string): void {
464
+ const job = this.#jobs.get(jobId);
465
+ if (!job?.metadata?.monitor) return;
466
+ const lifecycle = this.#lifecycles.get(jobId);
467
+ this.#monitorTombstones.set(jobId, {
468
+ jobId,
469
+ ownerId: job.ownerId,
470
+ status: job.status,
471
+ expiresAt: Date.now() + MONITOR_TOMBSTONE_TTL_MS,
472
+ purge: () => (lifecycle?.onTombstonePurge ?? lifecycle?.onEvict)?.(job),
473
+ });
474
+ }
475
+
476
+ getMonitorTombstone(jobId: string, filter?: AsyncJobFilter): MonitorTombstone | undefined {
477
+ this.#expireMonitorTombstones();
478
+ const tombstone = this.#monitorTombstones.get(jobId);
479
+ if (!tombstone) return undefined;
480
+ if (filter?.ownerId && tombstone.ownerId !== filter.ownerId) return undefined;
481
+ return tombstone;
482
+ }
483
+
484
+ purgeMonitorTombstone(jobId: string, filter?: AsyncJobFilter): { found: boolean; status?: AsyncJob["status"] } {
485
+ const tombstone = this.getMonitorTombstone(jobId, filter);
486
+ if (!tombstone) return { found: false };
487
+ this.#monitorTombstones.delete(jobId);
488
+ try {
489
+ tombstone.purge();
490
+ } catch (error) {
491
+ logger.warn("Monitor tombstone purge failed", {
492
+ jobId,
493
+ error: error instanceof Error ? error.message : String(error),
494
+ });
495
+ }
496
+ return { found: true, status: tombstone.status };
497
+ }
498
+
399
499
  // ── Subagent control plane (pause / resume / steer support) ──────────
400
500
 
401
501
  /** Register or replace the canonical record for a subagent. */
@@ -432,6 +532,38 @@ export class AsyncJobManager {
432
532
  this.#liveHandles.delete(subagentId);
433
533
  }
434
534
 
535
+ /**
536
+ * Retain the latest live `AgentProgress` for a subagent (deep-cloned so later
537
+ * mutation of the live object cannot corrupt retained state). Read by the
538
+ * `subagent` await panel; cleared on terminal/cancel/purge/dispose.
539
+ *
540
+ * Ignored for ids without a canonical `SubagentRecord` (e.g. foreground/inline
541
+ * task runs that share the executor path) so the map only holds detached
542
+ * subagent progress and never accumulates untracked foreground task state.
543
+ */
544
+ recordSubagentProgress(subagentId: string, progress: AgentProgress): void {
545
+ if (!this.#subagentRecords.has(subagentId)) return;
546
+ this.#subagentProgress.set(subagentId, structuredClone(progress));
547
+ }
548
+
549
+ getSubagentProgress(subagentId: string): AgentProgress | undefined {
550
+ return this.#subagentProgress.get(subagentId);
551
+ }
552
+
553
+ /**
554
+ * True only when a live, in-session progress producer exists for this id: a
555
+ * canonical registered record with a live handle or an in-memory running job.
556
+ * False for `SubagentTool` backward-compat job synthesis and resumed-from-disk
557
+ * records, which have no live producer to stream from.
558
+ */
559
+ hasLiveSubagent(subagentId: string, filter?: AsyncJobFilter): boolean {
560
+ const rec = this.getSubagentRecord(subagentId, filter);
561
+ if (!rec) return false;
562
+ if (this.#liveHandles.has(rec.subagentId)) return true;
563
+ const job = rec.currentJobId ? this.#jobs.get(rec.currentJobId) : undefined;
564
+ return job?.status === "running";
565
+ }
566
+
435
567
  /** Install the TaskTool-owned resume runner. Returns the new job id, or undefined on failure. */
436
568
  setResumeRunner(
437
569
  runner: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined,
@@ -462,6 +594,7 @@ export class AsyncJobManager {
462
594
  if (rec) {
463
595
  rec.status = "paused";
464
596
  this.#liveHandles.delete(rec.subagentId);
597
+ this.#subagentProgress.delete(rec.subagentId);
465
598
  }
466
599
  }
467
600
 
@@ -470,6 +603,7 @@ export class AsyncJobManager {
470
603
  if (!rec) return;
471
604
  rec.status = status;
472
605
  this.#liveHandles.delete(rec.subagentId);
606
+ this.#subagentProgress.delete(rec.subagentId);
473
607
  }
474
608
 
475
609
  /** Request a graceful safe-boundary pause of a running subagent. */
@@ -527,6 +661,9 @@ export class AsyncJobManager {
527
661
  message?: string,
528
662
  ): { ok: boolean; status?: SubagentLifecycle; jobId?: string; reason?: string } {
529
663
  const prevJobId = rec.currentJobId;
664
+ // Clear any retained progress from the previous run so a resumed subagent
665
+ // never renders the prior run's tool/output as live before it emits again.
666
+ this.#subagentProgress.delete(rec.subagentId);
530
667
  const newJobId = this.#resumeRunner?.(rec.subagentId, message, this.#resumeDescriptors.get(rec.subagentId));
531
668
  if (!newJobId) return { ok: false, reason: "resume_failed" };
532
669
  if (prevJobId && prevJobId !== newJobId) rec.historicalJobIds.push(prevJobId);
@@ -564,6 +701,7 @@ export class AsyncJobManager {
564
701
  }
565
702
  rec.status = "cancelled";
566
703
  this.#liveHandles.delete(rec.subagentId);
704
+ this.#subagentProgress.delete(rec.subagentId);
567
705
  this.#drainResumeQueue();
568
706
  return true;
569
707
  }
@@ -572,6 +710,7 @@ export class AsyncJobManager {
572
710
  if (idx !== -1) this.#resumeQueue.splice(idx, 1);
573
711
  rec.status = "cancelled";
574
712
  rec.queued = undefined;
713
+ this.#subagentProgress.delete(rec.subagentId);
575
714
  return true;
576
715
  }
577
716
  return false;
@@ -586,6 +725,7 @@ export class AsyncJobManager {
586
725
  this.#liveHandles.delete(sid);
587
726
  this.#resumeDescriptors.delete(sid);
588
727
  this.#subagentRecords.delete(sid);
728
+ this.#subagentProgress.delete(sid);
589
729
  }
590
730
  }
591
731
  }
@@ -836,6 +976,7 @@ export class AsyncJobManager {
836
976
  */
837
977
  cancelAll(filter?: AsyncJobFilter): void {
838
978
  for (const job of this.getRunningJobs(filter)) {
979
+ this.#runLifecycle(job.id, "cancel");
839
980
  job.status = "cancelled";
840
981
  job.abortController.abort();
841
982
  this.#scheduleEviction(job.id);
@@ -898,6 +1039,17 @@ export class AsyncJobManager {
898
1039
  // manager. Errors in cleanup callbacks are logged but never escalated.
899
1040
  this.runOwnerCleanups();
900
1041
  this.cancelAll();
1042
+ for (const tombstone of this.#monitorTombstones.values()) {
1043
+ try {
1044
+ tombstone.purge();
1045
+ } catch (error) {
1046
+ logger.warn("Monitor tombstone purge failed during dispose", {
1047
+ jobId: tombstone.jobId,
1048
+ error: error instanceof Error ? error.message : String(error),
1049
+ });
1050
+ }
1051
+ }
1052
+ this.#monitorTombstones.clear();
901
1053
  await this.waitForAll();
902
1054
  const drained = await this.drainDeliveries({ timeoutMs: options?.timeoutMs ?? 3_000 });
903
1055
  this.#clearEvictionTimers();
@@ -910,6 +1062,7 @@ export class AsyncJobManager {
910
1062
  this.#ownerCleanups.clear();
911
1063
  this.#subagentRecords.clear();
912
1064
  this.#liveHandles.clear();
1065
+ this.#subagentProgress.clear();
913
1066
  this.#resumeDescriptors.clear();
914
1067
  this.#resumeQueue.length = 0;
915
1068
  this.#notifyChange();
@@ -945,7 +1098,11 @@ export class AsyncJobManager {
945
1098
  #scheduleEviction(jobId: string): void {
946
1099
  this.#notifyChange();
947
1100
  if (this.#retentionMs <= 0) {
1101
+ this.#recordMonitorTombstone(jobId);
1102
+ this.#runLifecycle(jobId, "evict");
948
1103
  this.#jobs.delete(jobId);
1104
+ this.#lifecycles.delete(jobId);
1105
+ this.#lifecyclePhases.delete(jobId);
949
1106
  this.#suppressedDeliveries.delete(jobId);
950
1107
  this.#watchedJobs.delete(jobId);
951
1108
  this.#outputState.delete(jobId);
@@ -957,7 +1114,11 @@ export class AsyncJobManager {
957
1114
  }
958
1115
  const timer = setTimeout(() => {
959
1116
  this.#evictionTimers.delete(jobId);
1117
+ this.#recordMonitorTombstone(jobId);
1118
+ this.#runLifecycle(jobId, "evict");
960
1119
  this.#jobs.delete(jobId);
1120
+ this.#lifecycles.delete(jobId);
1121
+ this.#lifecyclePhases.delete(jobId);
961
1122
  this.#suppressedDeliveries.delete(jobId);
962
1123
  this.#watchedJobs.delete(jobId);
963
1124
  this.#outputState.delete(jobId);
@@ -14,6 +14,12 @@ import {
14
14
  readGjcManagedCodexHooksStatus,
15
15
  } from "../hooks/codex-native-hooks-config";
16
16
  import { theme } from "../modes/theme/theme";
17
+ import {
18
+ formatHermesSetupResult,
19
+ type HermesSetupFlags,
20
+ hermesSetupExitCode,
21
+ runHermesSetup,
22
+ } from "../setup/hermes-setup";
17
23
  import {
18
24
  addApiCompatibleProvider,
19
25
  formatProviderPresetList,
@@ -21,7 +27,7 @@ import {
21
27
  parseProviderCompatibility,
22
28
  } from "../setup/provider-onboarding";
23
29
 
24
- export type SetupComponent = "defaults" | "hooks" | "provider" | "python" | "stt";
30
+ export type SetupComponent = "defaults" | "hermes" | "hooks" | "provider" | "python" | "stt";
25
31
 
26
32
  export interface SetupCommandArgs {
27
33
  component: SetupComponent;
@@ -36,10 +42,23 @@ export interface SetupCommandArgs {
36
42
  apiKeyEnv?: string;
37
43
  model?: string[];
38
44
  modelsPath?: string;
45
+ smoke?: boolean;
46
+ install?: boolean;
47
+ root?: string[];
48
+ repo?: string;
49
+ profile?: string;
50
+ sessionCommand?: string;
51
+ stateRoot?: string;
52
+ mutation?: string[];
53
+ artifactByteCap?: string;
54
+ serverKey?: string;
55
+ gjcCommand?: string;
56
+ target?: string;
57
+ profileDir?: string;
39
58
  };
40
59
  }
41
60
 
42
- const VALID_COMPONENTS: SetupComponent[] = ["defaults", "hooks", "provider", "python", "stt"];
61
+ const VALID_COMPONENTS: SetupComponent[] = ["defaults", "hermes", "hooks", "provider", "python", "stt"];
43
62
 
44
63
  function hasProviderSetupFlags(flags: SetupCommandArgs["flags"]): boolean {
45
64
  return (
@@ -88,6 +107,32 @@ export function parseSetupArgs(args: string[]): SetupCommandArgs | undefined {
88
107
  flags.check = true;
89
108
  } else if (arg === "--force" || arg === "-f") {
90
109
  flags.force = true;
110
+ } else if (arg === "--smoke") {
111
+ flags.smoke = true;
112
+ } else if (arg === "--install") {
113
+ flags.install = true;
114
+ } else if (arg === "--root") {
115
+ flags.root = [...(flags.root ?? []), args[++i] ?? ""];
116
+ } else if (arg === "--repo") {
117
+ flags.repo = args[++i];
118
+ } else if (arg === "--profile") {
119
+ flags.profile = args[++i];
120
+ } else if (arg === "--session-command") {
121
+ flags.sessionCommand = args[++i];
122
+ } else if (arg === "--state-root") {
123
+ flags.stateRoot = args[++i];
124
+ } else if (arg === "--mutation") {
125
+ flags.mutation = [...(flags.mutation ?? []), args[++i] ?? ""];
126
+ } else if (arg === "--artifact-byte-cap") {
127
+ flags.artifactByteCap = args[++i];
128
+ } else if (arg === "--server-key") {
129
+ flags.serverKey = args[++i];
130
+ } else if (arg === "--gjc-command") {
131
+ flags.gjcCommand = args[++i];
132
+ } else if (arg === "--target") {
133
+ flags.target = args[++i];
134
+ } else if (arg === "--profile-dir") {
135
+ flags.profileDir = args[++i];
91
136
  } else if (arg === "--compat") {
92
137
  flags.compat = args[++i];
93
138
  } else if (arg === "--preset") {
@@ -177,6 +222,9 @@ export async function runSetupCommand(cmd: SetupCommandArgs): Promise<void> {
177
222
  case "defaults":
178
223
  await handleDefaultsSetup(cmd.flags);
179
224
  break;
225
+ case "hermes":
226
+ await handleHermesSetup(cmd.flags);
227
+ break;
180
228
  case "hooks":
181
229
  await handleHooksSetup(cmd.flags);
182
230
  break;
@@ -192,6 +240,26 @@ export async function runSetupCommand(cmd: SetupCommandArgs): Promise<void> {
192
240
  }
193
241
  }
194
242
 
243
+ async function handleHermesSetup(flags: HermesSetupFlags): Promise<void> {
244
+ try {
245
+ const result = await runHermesSetup(flags);
246
+ if (flags.json) {
247
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
248
+ return;
249
+ }
250
+ process.stdout.write(`${chalk.green(`${theme.status.success} Hermes MCP setup ready`)}\n`);
251
+ process.stdout.write(`${chalk.dim(formatHermesSetupResult(result))}\n`);
252
+ } catch (error) {
253
+ const message = error instanceof Error ? error.message : String(error);
254
+ if (flags.json) {
255
+ process.stdout.write(`${JSON.stringify({ ok: false, error: message }, null, 2)}\n`);
256
+ } else {
257
+ process.stderr.write(`${chalk.red(`${theme.status.error} Hermes MCP setup failed`)}\n`);
258
+ process.stderr.write(`${chalk.dim(message)}\n`);
259
+ }
260
+ process.exit(hermesSetupExitCode(error));
261
+ }
262
+ }
195
263
  async function handleProviderSetup(flags: {
196
264
  json?: boolean;
197
265
  force?: boolean;
@@ -410,6 +478,7 @@ ${chalk.bold("Usage:")}
410
478
 
411
479
  ${chalk.bold("Components:")}
412
480
  defaults Install bundled GJC default workflow skills (default)
481
+ hermes Optional: render/install a Hermes MCP bridge setup package
413
482
  hooks Optional: install GJC native Codex UserPromptSubmit/Stop skill-state hooks
414
483
  provider Optional: add a preset, OpenAI-compatible, or Anthropic-compatible API provider
415
484
  python Optional: verify a Python 3 interpreter is reachable for code execution
@@ -421,6 +490,11 @@ ${chalk.bold("Provider example:")}
421
490
  ${APP_NAME} setup provider --preset glm
422
491
  MY_PROVIDER_KEY=sk-... ${APP_NAME} setup provider --compat openai --provider my-oai --base-url https://api.example.com/v1 --api-key-env MY_PROVIDER_KEY --model gpt-example
423
492
 
493
+ ${chalk.bold("Hermes example:")}
494
+ ${APP_NAME} setup hermes --root /path/to/repo
495
+ ${APP_NAME} setup hermes --root /path/to/repo --profile my-bot --repo gajae-code --profile-dir /path/to/hermes/profile --install
496
+ ${APP_NAME} setup hermes --root /path/to/repo --session-command "gjc --model <provider/model>"
497
+
424
498
  ${chalk.bold("Options:")}
425
499
  -c, --check Check if dependencies are installed without installing
426
500
  -f, --force Overwrite existing default workflow skill files
@@ -432,6 +506,15 @@ ${chalk.bold("Options:")}
432
506
  --api-key-env Read provider API key from this environment variable
433
507
  --model, --models Model id to add (repeat or comma-separate)
434
508
  --models-path Override models config path
509
+ --smoke Run Hermes MCP setup smoke checks
510
+ --install Install generated Hermes setup files
511
+ --root Allowed Hermes MCP workdir/artifact root (repeatable)
512
+ --profile Hermes MCP profile namespace
513
+ --repo Hermes MCP repo namespace
514
+ --session-command Explicit GJC session command; omitted by default
515
+ --mutation Hermes MCP mutation classes: sessions,questions,reports,all
516
+ --target Hermes config file target for config-only install
517
+ --profile-dir Hermes profile directory for full setup install
435
518
 
436
519
  ${chalk.bold("Examples:")}
437
520
  ${APP_NAME} setup Install bundled GJC default workflow skills
@@ -439,6 +522,7 @@ ${chalk.bold("Examples:")}
439
522
  ${APP_NAME} setup defaults --check Check bundled GJC default workflow skills are installed
440
523
  ${APP_NAME} setup hooks Install native Codex skill-state hooks
441
524
  ${APP_NAME} setup hooks --check Check native Codex skill-state hooks
525
+ ${APP_NAME} setup hermes Render a model-agnostic Hermes MCP setup preview
442
526
  ${APP_NAME} setup python Install Python execution dependencies
443
527
  ${APP_NAME} setup stt Install speech-to-text dependencies
444
528
  ${APP_NAME} setup stt --check Check if STT dependencies are available
package/src/cli.ts CHANGED
@@ -36,10 +36,12 @@ const commands: CommandEntry[] = [
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
38
  { name: "harness", load: () => import("./commands/harness").then(m => m.default) },
39
+ { name: "coordinator", load: () => import("./commands/coordinator").then(m => m.default) },
39
40
  { name: "team", load: () => import("./commands/team").then(m => m.default) },
40
41
  { name: "ultragoal", load: () => import("./commands/ultragoal").then(m => m.default) },
41
42
  { name: "ralplan", load: () => import("./commands/ralplan").then(m => m.default) },
42
43
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
44
+ { name: "mcp-serve", load: () => import("./commands/mcp-serve").then(m => m.default) },
43
45
  {
44
46
  name: "contribute-pr",
45
47
  aliases: ["contribution-prep"],
@@ -0,0 +1,70 @@
1
+ import { Args, Command, Flags } from "@gajae-code/utils/cli";
2
+ import {
3
+ COORDINATOR_MCP_PROTOCOL_VERSION,
4
+ COORDINATOR_MCP_SERVER_NAME,
5
+ COORDINATOR_MCP_TOOL_NAMES,
6
+ } from "../coordinator/contract";
7
+
8
+ function writeJson(value: unknown): void {
9
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
10
+ `);
11
+ }
12
+
13
+ function coordinatorContractPayload(): {
14
+ ok: true;
15
+ server: { name: string; protocolVersion: string };
16
+ readOnly: true;
17
+ tools: string[];
18
+ } {
19
+ return {
20
+ ok: true,
21
+ server: { name: COORDINATOR_MCP_SERVER_NAME, protocolVersion: COORDINATOR_MCP_PROTOCOL_VERSION },
22
+ readOnly: true,
23
+ tools: [...COORDINATOR_MCP_TOOL_NAMES],
24
+ };
25
+ }
26
+
27
+ export default class Coordinator extends Command {
28
+ static description = "Inspect GJC coordinator MCP bridge contracts";
29
+ static strict = false;
30
+
31
+ static args = {
32
+ action: Args.string({ description: "Action to run (check or tools)", required: false }),
33
+ };
34
+
35
+ static flags = {
36
+ json: Flags.boolean({ char: "j", description: "Emit machine-readable JSON", default: false }),
37
+ };
38
+
39
+ async run(): Promise<void> {
40
+ const { args, flags } = await this.parse(Coordinator);
41
+ const action = args.action ?? "check";
42
+ if (action !== "check" && action !== "tools") {
43
+ const payload = { ok: false, reason: "unknown_coordinator_subcommand", subcommand: action };
44
+ if (flags.json) writeJson(payload);
45
+ else
46
+ process.stderr.write(`unknown_coordinator_subcommand:${action}
47
+ `);
48
+ process.exit(1);
49
+ }
50
+
51
+ const payload = coordinatorContractPayload();
52
+ if (flags.json) {
53
+ writeJson(action === "tools" ? { ok: true, tools: payload.tools } : payload);
54
+ return;
55
+ }
56
+ if (action === "tools") {
57
+ for (const tool of payload.tools)
58
+ process.stdout.write(`${tool}
59
+ `);
60
+ return;
61
+ }
62
+ process.stdout.write(
63
+ `server: ${payload.server.name}
64
+ protocol: ${payload.server.protocolVersion}
65
+ readOnly: true
66
+ tools: ${payload.tools.length}
67
+ `,
68
+ );
69
+ }
70
+ }
@@ -0,0 +1,62 @@
1
+ import { Args, Command, Flags } from "@gajae-code/utils/cli";
2
+ import {
3
+ COORDINATOR_MCP_PROTOCOL_VERSION,
4
+ COORDINATOR_MCP_SERVER_NAME,
5
+ COORDINATOR_MCP_TOOL_NAMES,
6
+ } from "../coordinator/contract";
7
+ import { runCoordinatorMcpStdio } from "../coordinator-mcp/server";
8
+
9
+ function writeJson(value: unknown): void {
10
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
11
+ }
12
+
13
+ export function validateMcpServeSubcommandForTest(server: string | undefined): void {
14
+ if (server !== "coordinator") throw new Error(`unknown_mcp_serve_subcommand:${server ?? ""}`);
15
+ }
16
+
17
+ export default class McpServe extends Command {
18
+ static description = "Serve GJC MCP compatibility bridges";
19
+ static strict = false;
20
+
21
+ static args = {
22
+ server: Args.string({ description: "MCP server to run (coordinator)", required: false }),
23
+ };
24
+
25
+ static flags = {
26
+ json: Flags.boolean({ char: "j", description: "Emit machine-readable JSON", default: false }),
27
+ check: Flags.boolean({ description: "Validate server configuration and print a smoke summary", default: false }),
28
+ };
29
+
30
+ async run(): Promise<void> {
31
+ const { args, flags } = await this.parse(McpServe);
32
+ const server = args.server ?? "";
33
+ try {
34
+ validateMcpServeSubcommandForTest(server);
35
+ } catch (error) {
36
+ const subcommand = server;
37
+ if (flags.json) {
38
+ writeJson({ ok: false, reason: "unknown_mcp_serve_subcommand", subcommand });
39
+ } else {
40
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
41
+ }
42
+ return;
43
+ }
44
+
45
+ if (flags.check) {
46
+ const payload = {
47
+ ok: true,
48
+ server: { name: COORDINATOR_MCP_SERVER_NAME, protocolVersion: COORDINATOR_MCP_PROTOCOL_VERSION },
49
+ readOnly: true,
50
+ tools: [...COORDINATOR_MCP_TOOL_NAMES],
51
+ };
52
+ if (flags.json) writeJson(payload);
53
+ else
54
+ process.stdout.write(
55
+ `server: ${payload.server.name}\nprotocol: ${payload.server.protocolVersion}\ntools: ${payload.tools.length}\n`,
56
+ );
57
+ return;
58
+ }
59
+
60
+ await runCoordinatorMcpStdio();
61
+ }
62
+ }