@gajae-code/coding-agent 0.4.5 → 0.5.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 (185) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/types/async/job-manager.d.ts +26 -0
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/list-models.d.ts +6 -0
  5. package/dist/types/commands/gc.d.ts +26 -0
  6. package/dist/types/commands/harness.d.ts +3 -0
  7. package/dist/types/config/file-lock-gc.d.ts +5 -0
  8. package/dist/types/config/file-lock.d.ts +7 -0
  9. package/dist/types/config/model-profile-activation.d.ts +11 -2
  10. package/dist/types/config/model-profiles.d.ts +7 -0
  11. package/dist/types/config/model-registry.d.ts +3 -0
  12. package/dist/types/config/model-resolver.d.ts +2 -0
  13. package/dist/types/config/models-config-schema.d.ts +30 -0
  14. package/dist/types/config/settings-schema.d.ts +4 -3
  15. package/dist/types/coordinator/contract.d.ts +1 -1
  16. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  24. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  25. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  26. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  27. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  28. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  29. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  30. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  31. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  32. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  33. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  34. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  35. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
  38. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  39. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  40. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  41. package/dist/types/harness-control-plane/owner.d.ts +8 -1
  42. package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
  43. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  44. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  45. package/dist/types/harness-control-plane/types.d.ts +4 -0
  46. package/dist/types/hindsight/mental-models.d.ts +5 -5
  47. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  48. package/dist/types/modes/components/model-selector.d.ts +1 -12
  49. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  50. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  51. package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
  52. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  53. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  54. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  55. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  56. package/dist/types/sdk.d.ts +5 -0
  57. package/dist/types/session/agent-session.d.ts +3 -1
  58. package/dist/types/session/blob-store.d.ts +59 -4
  59. package/dist/types/session/session-manager.d.ts +24 -6
  60. package/dist/types/session/streaming-output.d.ts +3 -2
  61. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  62. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  63. package/dist/types/task/receipt.d.ts +1 -0
  64. package/dist/types/task/types.d.ts +7 -0
  65. package/dist/types/thinking-metadata.d.ts +16 -0
  66. package/dist/types/thinking.d.ts +3 -12
  67. package/dist/types/tools/ask.d.ts +15 -1
  68. package/dist/types/tools/index.d.ts +2 -0
  69. package/dist/types/tools/resolve.d.ts +0 -10
  70. package/dist/types/tools/subagent.d.ts +6 -0
  71. package/dist/types/utils/tool-choice.d.ts +14 -1
  72. package/package.json +7 -7
  73. package/src/async/job-manager.ts +52 -0
  74. package/src/cli/args.ts +3 -0
  75. package/src/cli/auth-broker-cli.ts +1 -0
  76. package/src/cli/list-models.ts +13 -1
  77. package/src/cli.ts +9 -4
  78. package/src/commands/gc.ts +22 -0
  79. package/src/commands/harness.ts +43 -5
  80. package/src/commands/launch.ts +2 -2
  81. package/src/commands/session.ts +3 -1
  82. package/src/config/file-lock-gc.ts +181 -0
  83. package/src/config/file-lock.ts +14 -0
  84. package/src/config/model-profile-activation.ts +15 -3
  85. package/src/config/model-profiles.ts +264 -56
  86. package/src/config/model-resolver.ts +9 -6
  87. package/src/config/models-config-schema.ts +1 -0
  88. package/src/config/settings-schema.ts +6 -3
  89. package/src/coordinator/contract.ts +1 -0
  90. package/src/coordinator-mcp/server.ts +513 -26
  91. package/src/cursor.ts +16 -2
  92. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  93. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  94. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  95. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  96. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  97. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  98. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  99. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  100. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  101. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  102. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  103. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  104. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  105. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  106. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  107. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  108. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  109. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  110. package/src/defaults/gjc-defaults.ts +7 -0
  111. package/src/defaults/gjc-grok-cli.ts +22 -0
  112. package/src/export/html/index.ts +13 -9
  113. package/src/extensibility/extensions/index.ts +1 -0
  114. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  115. package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
  116. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  117. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  118. package/src/gjc-runtime/gc-render.ts +70 -0
  119. package/src/gjc-runtime/gc-runtime.ts +403 -0
  120. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  121. package/src/gjc-runtime/ralplan-runtime.ts +58 -7
  122. package/src/gjc-runtime/state-renderer.ts +12 -3
  123. package/src/gjc-runtime/state-runtime.ts +46 -29
  124. package/src/gjc-runtime/team-gc.ts +49 -0
  125. package/src/gjc-runtime/team-runtime.ts +211 -8
  126. package/src/gjc-runtime/tmux-common.ts +29 -0
  127. package/src/gjc-runtime/tmux-gc.ts +176 -0
  128. package/src/gjc-runtime/tmux-sessions.ts +68 -12
  129. package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
  130. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  131. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  132. package/src/harness-control-plane/gc-adapter.ts +184 -0
  133. package/src/harness-control-plane/owner.ts +89 -27
  134. package/src/harness-control-plane/receipt-spool.ts +128 -0
  135. package/src/harness-control-plane/state-machine.ts +27 -6
  136. package/src/harness-control-plane/storage.ts +93 -0
  137. package/src/harness-control-plane/types.ts +4 -0
  138. package/src/hindsight/mental-models.ts +17 -16
  139. package/src/internal-urls/docs-index.generated.ts +14 -8
  140. package/src/main.ts +7 -2
  141. package/src/modes/components/assistant-message.ts +26 -14
  142. package/src/modes/components/diff.ts +97 -0
  143. package/src/modes/components/hook-selector.ts +19 -0
  144. package/src/modes/components/model-selector.ts +370 -181
  145. package/src/modes/components/status-line/segments.ts +1 -1
  146. package/src/modes/components/tool-execution.ts +30 -13
  147. package/src/modes/controllers/command-controller.ts +25 -6
  148. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  149. package/src/modes/controllers/selector-controller.ts +34 -42
  150. package/src/modes/rpc/rpc-client.ts +3 -2
  151. package/src/modes/rpc/rpc-mode.ts +187 -39
  152. package/src/modes/rpc/rpc-types.ts +5 -2
  153. package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
  154. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  156. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  157. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  158. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  159. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  160. package/src/sdk.ts +46 -5
  161. package/src/secrets/obfuscator.ts +102 -27
  162. package/src/session/agent-session.ts +179 -25
  163. package/src/session/blob-store.ts +148 -6
  164. package/src/session/session-manager.ts +311 -60
  165. package/src/session/streaming-output.ts +185 -122
  166. package/src/session/tool-choice-queue.ts +23 -0
  167. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  168. package/src/skill-state/workflow-hud.ts +106 -10
  169. package/src/slash-commands/builtin-registry.ts +3 -2
  170. package/src/task/executor.ts +78 -6
  171. package/src/task/receipt.ts +5 -0
  172. package/src/task/render.ts +21 -1
  173. package/src/task/types.ts +8 -0
  174. package/src/thinking-metadata.ts +51 -0
  175. package/src/thinking.ts +26 -46
  176. package/src/tools/ask.ts +56 -1
  177. package/src/tools/bash.ts +1 -1
  178. package/src/tools/index.ts +2 -0
  179. package/src/tools/job.ts +3 -2
  180. package/src/tools/monitor.ts +36 -1
  181. package/src/tools/resolve.ts +93 -18
  182. package/src/tools/subagent-render.ts +9 -0
  183. package/src/tools/subagent.ts +26 -2
  184. package/src/utils/edit-mode.ts +1 -1
  185. package/src/utils/tool-choice.ts +45 -16
@@ -168,6 +168,50 @@ interface CoordinatorSessionState {
168
168
  reason: string | null;
169
169
  }
170
170
 
171
+ type CoordinatorEventKind =
172
+ | "session.registered"
173
+ | "session.started"
174
+ | "session.state_changed"
175
+ | "turn.queued"
176
+ | "turn.delivering"
177
+ | "turn.active"
178
+ | "turn.waiting_for_answer"
179
+ | "turn.completed"
180
+ | "turn.failed"
181
+ | "turn.cancelled"
182
+ | "turn.superseded"
183
+ | "question.opened"
184
+ | "question.answered"
185
+ | "report.written"
186
+ | "tmux.delivery_succeeded"
187
+ | "tmux.delivery_failed";
188
+
189
+ interface CoordinatorEvent {
190
+ schema_version: 1;
191
+ seq: number;
192
+ id: string;
193
+ timestamp: string;
194
+ kind: CoordinatorEventKind;
195
+ session_id?: string;
196
+ turn_id?: string;
197
+ question_id?: string;
198
+ report_id?: string;
199
+ summary: string;
200
+ payload_ref?: string;
201
+ metadata?: Record<string, string | number | boolean | null>;
202
+ }
203
+
204
+ interface CoordinatorEventInput {
205
+ kind: CoordinatorEventKind;
206
+ sessionId?: string | null;
207
+ turnId?: string | null;
208
+ questionId?: string | null;
209
+ reportId?: string | null;
210
+ summary: string;
211
+ payloadRef?: string | null;
212
+ metadata?: Record<string, string | number | boolean | null>;
213
+ }
214
+
171
215
  const MISSING_FINAL_RESPONSE_ADVISORY = "completion_missing_final_response";
172
216
  const ACTIVE_TURN_STATUSES = new Set<TurnStatus>(["delivering", "active", "waiting_for_answer", "completing"]);
173
217
  const TERMINAL_TURN_STATUSES = new Set<TurnStatus>(["completed", "failed", "cancelled", "superseded"]);
@@ -351,6 +395,22 @@ function toolSchema(name: CoordinatorToolName): {
351
395
  if (name === "gjc_coordinator_read_coordination_status") {
352
396
  return { name, description: "Read coordinator coordination reports.", inputSchema: common };
353
397
  }
398
+ if (name === "gjc_coordinator_watch_events") {
399
+ return {
400
+ name,
401
+ description: "Long-poll the durable coordinator event journal for new bounded event records.",
402
+ inputSchema: {
403
+ type: "object",
404
+ properties: {
405
+ after_seq: { type: "number" },
406
+ session_id: sessionId,
407
+ event_types: { type: "array", items: { type: "string" } },
408
+ timeout_ms: { type: "number" },
409
+ limit: { type: "number" },
410
+ },
411
+ },
412
+ };
413
+ }
354
414
  return { name, description: "List known scoped GJC coordinator bridge sessions.", inputSchema: common };
355
415
  }
356
416
 
@@ -393,6 +453,218 @@ async function listJsonFiles(dir: string): Promise<unknown[]> {
393
453
  }
394
454
  }
395
455
 
456
+ const COORDINATOR_STATUS_EVENT_LIMIT = 100;
457
+
458
+ function jsonRecords(values: unknown[]): Array<Record<string, unknown>> {
459
+ return values.map(value => asRecord(value)).filter((value): value is Record<string, unknown> => value !== null);
460
+ }
461
+
462
+ function firstString(record: Record<string, unknown>, keys: string[]): string | null {
463
+ for (const key of keys) {
464
+ const value = record[key];
465
+ if (typeof value === "string" && value.length > 0) return value;
466
+ }
467
+ return null;
468
+ }
469
+
470
+ function eventTimestamp(record: Record<string, unknown>): string | null {
471
+ return firstString(record, ["updated_at", "completed_at", "answered_at", "created_at", "registered_at"]);
472
+ }
473
+
474
+ function canonicalCoordinatorEvent(
475
+ event_type: "session_state" | "turn_state" | "question_state" | "coordination_report",
476
+ record: Record<string, unknown>,
477
+ ): Record<string, unknown> {
478
+ return {
479
+ schema_version: 1,
480
+ event_type,
481
+ session_id: firstString(record, ["session_id", "sessionId"]),
482
+ turn_id: firstString(record, ["turn_id", "turnId", "current_turn_id", "last_turn_id"]),
483
+ question_id: event_type === "question_state" ? firstString(record, ["id", "question_id"]) : null,
484
+ status: firstString(record, ["status", "state"]),
485
+ source: firstString(record, ["source"]),
486
+ reason: firstString(record, ["reason"]),
487
+ updated_at: eventTimestamp(record),
488
+ };
489
+ }
490
+
491
+ function sortNewestFirst(records: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
492
+ return [...records].sort((left, right) => {
493
+ const leftTime = eventTimestamp(left) ?? "";
494
+ const rightTime = eventTimestamp(right) ?? "";
495
+ return rightTime.localeCompare(leftTime);
496
+ });
497
+ }
498
+
499
+ function buildCanonicalCoordinatorEvents(input: {
500
+ sessionStates: Array<Record<string, unknown>>;
501
+ turns: Array<Record<string, unknown>>;
502
+ questions: Array<Record<string, unknown>>;
503
+ reports: Array<Record<string, unknown>>;
504
+ }): Array<Record<string, unknown>> {
505
+ return sortNewestFirst([
506
+ ...input.sessionStates.map(record => canonicalCoordinatorEvent("session_state", record)),
507
+ ...input.turns.map(record => canonicalCoordinatorEvent("turn_state", record)),
508
+ ...input.questions.map(record => canonicalCoordinatorEvent("question_state", record)),
509
+ ...input.reports.map(record => canonicalCoordinatorEvent("coordination_report", record)),
510
+ ]).slice(0, COORDINATOR_STATUS_EVENT_LIMIT);
511
+ }
512
+
513
+ function activeSessionStates(sessionStates: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
514
+ return sessionStates.filter(record => {
515
+ const state = record.state;
516
+ return state === "booting" || state === "running" || state === "needs_user_input" || state === "stale";
517
+ });
518
+ }
519
+
520
+ function eventsDir(namespaceDir: string): string {
521
+ return path.join(namespaceDir, "events");
522
+ }
523
+
524
+ function eventJournalFile(namespaceDir: string): string {
525
+ return path.join(eventsDir(namespaceDir), "event-journal.jsonl");
526
+ }
527
+
528
+ function eventSequenceFile(namespaceDir: string): string {
529
+ return path.join(eventsDir(namespaceDir), "latest-seq.json");
530
+ }
531
+
532
+ function boundSummary(value: string): string {
533
+ const normalized = value
534
+ .replace(/[\r\n\t]+/g, " ")
535
+ .replace(/\s+/g, " ")
536
+ .trim();
537
+ return normalized.length > 240 ? `${normalized.slice(0, 237)}...` : normalized;
538
+ }
539
+
540
+ async function readLatestEventSeq(namespaceDir: string): Promise<number> {
541
+ const sequence = asRecord(await readJsonFile(eventSequenceFile(namespaceDir)));
542
+ const seq = sequence?.seq;
543
+ if (typeof seq === "number" && Number.isInteger(seq) && seq >= 0) return seq;
544
+ let latestSeq = 0;
545
+ for (const event of await readCoordinatorEvents(namespaceDir)) latestSeq = Math.max(latestSeq, event.seq);
546
+ return latestSeq;
547
+ }
548
+
549
+ const eventAppendQueues = new Map<string, Promise<unknown>>();
550
+
551
+ async function appendCoordinatorEvent(namespaceDir: string, input: CoordinatorEventInput): Promise<CoordinatorEvent> {
552
+ const previous = eventAppendQueues.get(namespaceDir) ?? Promise.resolve();
553
+ let release!: () => void;
554
+ const current = new Promise<void>(resolve => {
555
+ release = resolve;
556
+ });
557
+ const queued = previous.then(
558
+ () => current,
559
+ () => current,
560
+ );
561
+ eventAppendQueues.set(namespaceDir, queued);
562
+
563
+ await previous.catch(() => undefined);
564
+ try {
565
+ const latestSeq = await readLatestEventSeq(namespaceDir);
566
+ const seq = latestSeq + 1;
567
+ const timestamp = new Date().toISOString();
568
+ const event: CoordinatorEvent = {
569
+ schema_version: 1,
570
+ seq,
571
+ id: `event-${seq.toString().padStart(12, "0")}`,
572
+ timestamp,
573
+ kind: input.kind,
574
+ summary: boundSummary(input.summary),
575
+ ...(input.sessionId ? { session_id: input.sessionId } : {}),
576
+ ...(input.turnId ? { turn_id: input.turnId } : {}),
577
+ ...(input.questionId ? { question_id: input.questionId } : {}),
578
+ ...(input.reportId ? { report_id: input.reportId } : {}),
579
+ ...(input.payloadRef ? { payload_ref: input.payloadRef } : {}),
580
+ ...(input.metadata ? { metadata: input.metadata } : {}),
581
+ };
582
+ await ensureDir(eventsDir(namespaceDir));
583
+ await fs.appendFile(eventJournalFile(namespaceDir), `${JSON.stringify(event)}\n`);
584
+ await writeJsonFile(eventSequenceFile(namespaceDir), { seq, updated_at: timestamp });
585
+ return event;
586
+ } finally {
587
+ release();
588
+ if (eventAppendQueues.get(namespaceDir) === queued) eventAppendQueues.delete(namespaceDir);
589
+ }
590
+ }
591
+
592
+ function parseCoordinatorEvent(line: string): CoordinatorEvent | null {
593
+ try {
594
+ const event = JSON.parse(line) as CoordinatorEvent;
595
+ if (typeof event.seq !== "number" || typeof event.kind !== "string") return null;
596
+ return event;
597
+ } catch {
598
+ return null;
599
+ }
600
+ }
601
+
602
+ async function readCoordinatorEvents(namespaceDir: string): Promise<CoordinatorEvent[]> {
603
+ try {
604
+ const content = await fs.readFile(eventJournalFile(namespaceDir), "utf8");
605
+ return content
606
+ .split("\n")
607
+ .map(line => line.trim())
608
+ .filter(Boolean)
609
+ .map(parseCoordinatorEvent)
610
+ .filter((event): event is CoordinatorEvent => event !== null)
611
+ .sort((left, right) => left.seq - right.seq);
612
+ } catch (error) {
613
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
614
+ throw error;
615
+ }
616
+ }
617
+
618
+ function boundedEventLimit(value: unknown): number {
619
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
620
+ if (!Number.isFinite(parsed) || parsed <= 0) return 100;
621
+ return Math.min(parsed, 100);
622
+ }
623
+
624
+ function eventTypeFilter(value: unknown): Set<string> | null {
625
+ if (!Array.isArray(value)) return null;
626
+ const types = value.filter((item): item is string => typeof item === "string" && item.length > 0);
627
+ return types.length > 0 ? new Set(types) : null;
628
+ }
629
+
630
+ function filterCoordinatorEvents(
631
+ events: CoordinatorEvent[],
632
+ args: Record<string, unknown>,
633
+ limit: number,
634
+ ): CoordinatorEvent[] {
635
+ const afterSeq =
636
+ typeof args.after_seq === "number" ? args.after_seq : Number.parseInt(String(args.after_seq ?? "0"), 10);
637
+ const safeAfterSeq = Number.isFinite(afterSeq) && afterSeq > 0 ? afterSeq : 0;
638
+ const sessionId = args.session_id == null ? null : safeExternalId("session", args.session_id);
639
+ const eventTypes = eventTypeFilter(args.event_types);
640
+ return events
641
+ .filter(event => event.seq > safeAfterSeq)
642
+ .filter(event => !sessionId || event.session_id === sessionId)
643
+ .filter(event => !eventTypes || eventTypes.has(event.kind))
644
+ .slice(0, limit);
645
+ }
646
+
647
+ function eventSummaries(
648
+ events: CoordinatorEvent[],
649
+ ): Array<
650
+ Pick<
651
+ CoordinatorEvent,
652
+ "seq" | "id" | "timestamp" | "kind" | "session_id" | "turn_id" | "question_id" | "report_id" | "summary"
653
+ >
654
+ > {
655
+ return events.map(event => ({
656
+ seq: event.seq,
657
+ id: event.id,
658
+ timestamp: event.timestamp,
659
+ kind: event.kind,
660
+ ...(event.session_id ? { session_id: event.session_id } : {}),
661
+ ...(event.turn_id ? { turn_id: event.turn_id } : {}),
662
+ ...(event.question_id ? { question_id: event.question_id } : {}),
663
+ ...(event.report_id ? { report_id: event.report_id } : {}),
664
+ summary: event.summary,
665
+ }));
666
+ }
667
+
396
668
  function safeExternalId(kind: "session" | "question", value: unknown): string {
397
669
  if (typeof value !== "string" || !SAFE_EXTERNAL_ID_PATTERN.test(value)) throw new Error(`invalid_${kind}_id`);
398
670
  return value;
@@ -449,8 +721,36 @@ async function readTurnRecord(namespaceDir: string, turnId: unknown): Promise<Tu
449
721
  return (await readJsonFile(turnFile(namespaceDir, safeTurnId(turnId)))) as TurnRecord | null;
450
722
  }
451
723
 
724
+ function turnEventKind(status: TurnStatus): CoordinatorEventKind | null {
725
+ if (status === "queued") return "turn.queued";
726
+ if (status === "delivering") return "turn.delivering";
727
+ if (status === "active") return "turn.active";
728
+ if (status === "waiting_for_answer") return "turn.waiting_for_answer";
729
+ if (status === "completed") return "turn.completed";
730
+ if (status === "failed") return "turn.failed";
731
+ if (status === "cancelled") return "turn.cancelled";
732
+ if (status === "superseded") return "turn.superseded";
733
+ return null;
734
+ }
735
+
452
736
  async function writeTurnRecord(namespaceDir: string, turn: TurnRecord): Promise<void> {
737
+ const previous = (await readJsonFile(turnFile(namespaceDir, turn.turn_id))) as TurnRecord | null;
453
738
  await writeJsonFile(turnFile(namespaceDir, turn.turn_id), turn);
739
+ const kind = previous?.status === turn.status ? null : turnEventKind(turn.status);
740
+ if (kind) {
741
+ await appendCoordinatorEvent(namespaceDir, {
742
+ kind,
743
+ sessionId: turn.session_id,
744
+ turnId: turn.turn_id,
745
+ summary: `Turn ${turn.turn_id} is ${turn.status}`,
746
+ payloadRef: path.relative(namespaceDir, turnFile(namespaceDir, turn.turn_id)),
747
+ metadata: {
748
+ status: turn.status,
749
+ queued: turn.delivery.queued,
750
+ tmux_keys_sent: turn.delivery.tmux_keys_sent ?? null,
751
+ },
752
+ });
753
+ }
454
754
  }
455
755
 
456
756
  async function readActiveTurn(namespaceDir: string, sessionId: string): Promise<TurnRecord | null> {
@@ -505,6 +805,28 @@ async function writeSessionState(
505
805
  reason: options.reason ?? null,
506
806
  };
507
807
  await writeJsonFile(sessionStateFile(namespaceDir, sessionId), payload);
808
+ if (
809
+ !previous ||
810
+ previous.state !== payload.state ||
811
+ previous.current_turn_id !== payload.current_turn_id ||
812
+ previous.last_turn_id !== payload.last_turn_id ||
813
+ previous.live !== payload.live ||
814
+ previous.reason !== payload.reason
815
+ ) {
816
+ await appendCoordinatorEvent(namespaceDir, {
817
+ kind: "session.state_changed",
818
+ sessionId,
819
+ turnId: payload.current_turn_id ?? payload.last_turn_id,
820
+ summary: `Session ${sessionId} state changed to ${payload.state}`,
821
+ payloadRef: path.relative(namespaceDir, sessionStateFile(namespaceDir, sessionId)),
822
+ metadata: {
823
+ state: payload.state,
824
+ ready_for_input: payload.ready_for_input,
825
+ live: payload.live,
826
+ reason: payload.reason,
827
+ },
828
+ });
829
+ }
508
830
  return payload;
509
831
  }
510
832
 
@@ -742,6 +1064,7 @@ async function startTmuxSession(
742
1064
  config: CoordinatorMcpConfig,
743
1065
  input: SessionStartInput,
744
1066
  namespaceDir: string,
1067
+ runner: CommandRunner = runCommand,
745
1068
  ): Promise<Record<string, unknown>> {
746
1069
  if (!config.sessionCommand) throw new Error("coordinator_session_command_required");
747
1070
  const sessionName = `gjc-coordinator-${randomUUID().slice(0, 8)}`;
@@ -752,7 +1075,7 @@ async function startTmuxSession(
752
1075
  `${GJC_COORDINATOR_SESSION_ID_ENV}=${shellQuote(sessionName)}`,
753
1076
  config.sessionCommand,
754
1077
  ].join(" ");
755
- const started = await runCommand([
1078
+ const started = await runner([
756
1079
  "tmux",
757
1080
  "new-session",
758
1081
  "-d",
@@ -767,9 +1090,6 @@ async function startTmuxSession(
767
1090
  ]);
768
1091
  if (started.exitCode !== 0) throw new Error(`coordinator_tmux_start_failed:${started.stderr || started.stdout}`);
769
1092
  const [tmuxTarget, paneId] = started.stdout.trim().split(/\s+/, 2);
770
- const initialPromptTmuxKeysSent = input.prompt
771
- ? await sendTmuxPromptKeys(tmuxTarget || sessionName, input.prompt)
772
- : false;
773
1093
  return {
774
1094
  sessionId: sessionName,
775
1095
  tmuxSession: sessionName,
@@ -779,7 +1099,6 @@ async function startTmuxSession(
779
1099
  createdAt: new Date().toISOString(),
780
1100
  sessionCommand: config.sessionCommand,
781
1101
  runtimeStateFile,
782
- initialPromptTmuxKeysSent,
783
1102
  };
784
1103
  }
785
1104
 
@@ -883,6 +1202,31 @@ function waitForTurnStateChange(namespaceDir: string, turn: TurnRecord, timeoutM
883
1202
  return deferred.promise;
884
1203
  }
885
1204
 
1205
+ async function waitForCoordinatorEvents(namespaceDir: string, timeoutMs: number): Promise<void> {
1206
+ const deferred = Promise.withResolvers<void>();
1207
+ const watchers: nodeFs.FSWatcher[] = [];
1208
+ let settled = false;
1209
+ const finish = () => {
1210
+ if (settled) return;
1211
+ settled = true;
1212
+ for (const watcher of watchers) watcher.close();
1213
+ clearTimeout(timer);
1214
+ deferred.resolve();
1215
+ };
1216
+ const timer = setTimeout(finish, Math.max(timeoutMs, 0));
1217
+ timer.unref?.();
1218
+ await ensureDir(eventsDir(namespaceDir));
1219
+ try {
1220
+ const watcher = nodeFs.watch(eventsDir(namespaceDir), (_eventType, filename) => {
1221
+ if (filename === "event-journal.jsonl" || filename === "latest-seq.json") finish();
1222
+ });
1223
+ watchers.push(watcher);
1224
+ } catch {
1225
+ // Directory may not exist yet; the timeout remains a bounded fallback.
1226
+ }
1227
+ return deferred.promise;
1228
+ }
1229
+
886
1230
  function decodeUtf8WithinByteCap(bytes: Buffer, byteCap: number): string {
887
1231
  const decoder = new TextDecoder("utf-8", { fatal: true });
888
1232
  for (let end = Math.min(bytes.length, byteCap); end >= 0; end--) {
@@ -964,27 +1308,26 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
964
1308
  }
965
1309
 
966
1310
  async function activateTurn(session: Record<string, unknown>, turn: TurnRecord): Promise<TurnRecord> {
967
- const tmuxKeysSent = await sendTmuxPrompt(session, turn.prompt.text, commandRunner);
968
1311
  const timestamp = new Date().toISOString();
969
1312
  const target = typeof session.tmux_target === "string" ? session.tmux_target : session.tmuxTarget;
970
1313
  const live = hasTmuxIdentity(session) ? await hasTmuxSession(session, commandRunner) : null;
971
- const activeTurn: TurnRecord = {
1314
+ const pendingTurn: TurnRecord = {
972
1315
  ...turn,
973
1316
  status: "active",
974
1317
  delivery: {
975
1318
  delivered: false,
976
- queued: !tmuxKeysSent,
1319
+ queued: true,
977
1320
  target: typeof target === "string" ? target : null,
978
- tmux_keys_sent: tmuxKeysSent,
1321
+ tmux_keys_sent: false,
979
1322
  prompt_acknowledged: false,
980
- state: tmuxKeysSent ? "tmux_keys_sent" : "unavailable",
1323
+ state: "queued",
981
1324
  attempts: [
982
1325
  {
983
1326
  delivered: false,
984
- tmux_keys_sent: tmuxKeysSent,
1327
+ tmux_keys_sent: false,
985
1328
  channel: "tmux_keys",
986
1329
  created_at: timestamp,
987
- reason: tmuxKeysSent ? "awaiting_runtime_ack" : "tmux_delivery_unavailable",
1330
+ reason: "awaiting_tmux_delivery",
988
1331
  },
989
1332
  ],
990
1333
  },
@@ -992,13 +1335,60 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
992
1335
  started_at: turn.started_at ?? timestamp,
993
1336
  updated_at: timestamp,
994
1337
  };
995
- await writeActiveTurn(namespaceDir, activeTurn);
996
- await writeSessionState(namespaceDir, activeTurn.session_id, tmuxKeysSent ? "running" : "stale", {
997
- currentTurnId: activeTurn.turn_id,
1338
+ await writeTurnRecord(namespaceDir, pendingTurn);
1339
+ await writeActiveTurn(namespaceDir, pendingTurn);
1340
+ await writeSessionState(namespaceDir, pendingTurn.session_id, "running", {
1341
+ currentTurnId: pendingTurn.turn_id,
998
1342
  live,
999
- reason: tmuxKeysSent ? null : "tmux_delivery_unavailable",
1343
+ reason: null,
1000
1344
  });
1345
+
1346
+ const tmuxKeysSent = await sendTmuxPrompt(session, turn.prompt.text, commandRunner);
1347
+ const deliveredAt = new Date().toISOString();
1348
+ const activeTurn: TurnRecord = {
1349
+ ...pendingTurn,
1350
+ delivery: {
1351
+ delivered: false,
1352
+ queued: !tmuxKeysSent,
1353
+ target: typeof target === "string" ? target : null,
1354
+ tmux_keys_sent: tmuxKeysSent,
1355
+ prompt_acknowledged: false,
1356
+ state: tmuxKeysSent ? "tmux_keys_sent" : "unavailable",
1357
+ attempts: [
1358
+ {
1359
+ delivered: false,
1360
+ tmux_keys_sent: tmuxKeysSent,
1361
+ channel: "tmux_keys",
1362
+ created_at: deliveredAt,
1363
+ reason: tmuxKeysSent ? "awaiting_runtime_ack" : "tmux_delivery_unavailable",
1364
+ },
1365
+ ],
1366
+ },
1367
+ updated_at: deliveredAt,
1368
+ };
1001
1369
  await writeTurnRecord(namespaceDir, activeTurn);
1370
+ await writeActiveTurn(namespaceDir, activeTurn);
1371
+ const sessionState = await readSessionState(namespaceDir, activeTurn.session_id);
1372
+ const runtimeStateAlreadySettled =
1373
+ sessionState?.current_turn_id === activeTurn.turn_id &&
1374
+ (sessionState.state === "completed" || sessionState.state === "errored");
1375
+ if (!runtimeStateAlreadySettled) {
1376
+ await writeSessionState(namespaceDir, activeTurn.session_id, tmuxKeysSent ? "running" : "stale", {
1377
+ currentTurnId: activeTurn.turn_id,
1378
+ live,
1379
+ reason: tmuxKeysSent ? null : "tmux_delivery_unavailable",
1380
+ });
1381
+ }
1382
+ await appendCoordinatorEvent(namespaceDir, {
1383
+ kind: tmuxKeysSent ? "tmux.delivery_succeeded" : "tmux.delivery_failed",
1384
+ sessionId: activeTurn.session_id,
1385
+ turnId: activeTurn.turn_id,
1386
+ summary: tmuxKeysSent
1387
+ ? `Tmux delivery succeeded for turn ${activeTurn.turn_id}`
1388
+ : `Tmux delivery failed for turn ${activeTurn.turn_id}`,
1389
+ payloadRef: path.relative(namespaceDir, turnFile(namespaceDir, activeTurn.turn_id)),
1390
+ metadata: { target: typeof target === "string" ? target : null, live },
1391
+ });
1002
1392
  return activeTurn;
1003
1393
  }
1004
1394
 
@@ -1097,6 +1487,14 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1097
1487
  sessionFile(sessionId),
1098
1488
  commandRunner,
1099
1489
  );
1490
+ await appendCoordinatorEvent(namespaceDir, {
1491
+ kind: "session.registered",
1492
+ sessionId,
1493
+ summary: `Session ${sessionId} registered for coordinator control`,
1494
+ payloadRef: path.relative(namespaceDir, sessionFile(sessionId)),
1495
+ metadata: { source: optionalString(args.source) ?? "register_session", visible: args.visible !== false },
1496
+ });
1497
+
1100
1498
  return {
1101
1499
  ok: true,
1102
1500
  session: registered.session,
@@ -1136,8 +1534,59 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1136
1534
  if (name === "gjc_coordinator_list_artifacts") return { ok: true, roots: config.allowedRoots };
1137
1535
  if (name === "gjc_coordinator_read_artifact")
1138
1536
  return await readCoordinatorArtifact(config, { path: args.path });
1139
- if (name === "gjc_coordinator_read_coordination_status")
1140
- return { ok: true, reports: await listJsonFiles(path.join(namespaceDir, "reports")) };
1537
+ if (name === "gjc_coordinator_read_coordination_status") {
1538
+ const sessions = jsonRecords(await listSessions());
1539
+ const sessionStates = jsonRecords(await listJsonFiles(path.join(namespaceDir, "session-states")));
1540
+ const turns = jsonRecords(await listJsonFiles(turnsDir(namespaceDir)));
1541
+ const questions = jsonRecords(await listQuestions(args));
1542
+ const reports = jsonRecords(await listJsonFiles(path.join(namespaceDir, "reports")));
1543
+ const events = await readCoordinatorEvents(namespaceDir);
1544
+ return {
1545
+ ok: true,
1546
+ schema_version: 1,
1547
+ namespace: config.namespace,
1548
+ state_root: namespaceDir,
1549
+ transport: { mcp: "polling", push_subscriptions: false },
1550
+ summary: {
1551
+ sessions: sessions.length,
1552
+ active_sessions: activeSessionStates(sessionStates).length,
1553
+ turns: turns.length,
1554
+ active_turns: turns.filter(turn => ACTIVE_TURN_STATUSES.has(turn.status as TurnStatus)).length,
1555
+ queued_turns: turns.filter(turn => turn.status === "queued").length,
1556
+ terminal_turns: turns.filter(turn => TERMINAL_TURN_STATUSES.has(turn.status as TurnStatus)).length,
1557
+ open_questions: questions.filter(question => question.status === "open").length,
1558
+ reports: reports.length,
1559
+ },
1560
+ sessions,
1561
+ session_states: sessionStates,
1562
+ turns,
1563
+ questions,
1564
+ reports,
1565
+ events: buildCanonicalCoordinatorEvents({ sessionStates, turns, questions, reports }),
1566
+ latest_event_seq: await readLatestEventSeq(namespaceDir),
1567
+ recent_events: eventSummaries(events.slice(-10)),
1568
+ };
1569
+ }
1570
+ if (name === "gjc_coordinator_watch_events") {
1571
+ const limit = boundedEventLimit(args.limit);
1572
+ const timeoutMs = boundedTimeoutMs(args.timeout_ms);
1573
+ let events = await readCoordinatorEvents(namespaceDir);
1574
+ let matched = filterCoordinatorEvents(events, args, limit);
1575
+ let timedOut = false;
1576
+ if (matched.length === 0 && timeoutMs > 0) {
1577
+ await waitForCoordinatorEvents(namespaceDir, timeoutMs);
1578
+ events = await readCoordinatorEvents(namespaceDir);
1579
+ matched = filterCoordinatorEvents(events, args, limit);
1580
+ timedOut = matched.length === 0;
1581
+ }
1582
+ return {
1583
+ ok: true,
1584
+ events: matched,
1585
+ latest_seq: await readLatestEventSeq(namespaceDir),
1586
+ timed_out: timedOut,
1587
+ transport: { mcp: "long_poll", push_subscriptions: false },
1588
+ };
1589
+ }
1141
1590
  if (name === "gjc_coordinator_start_session") {
1142
1591
  requireCoordinatorMutation(config, "sessions", args);
1143
1592
  const cwd = await assertCoordinatorWorkdir(config, args.cwd);
@@ -1149,18 +1598,23 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1149
1598
  };
1150
1599
  const started = services.startSession
1151
1600
  ? await services.startSession(input)
1152
- : await startTmuxSession(config, input, namespaceDir);
1601
+ : await startTmuxSession(config, input, namespaceDir, commandRunner);
1153
1602
  const startedRecord = asRecord(started);
1154
1603
  if (!startedRecord) throw new Error("coordinator_session_command_required");
1155
1604
  const session = normalizeSession(startedRecord);
1156
1605
  await writeJsonFile(sessionFile(session.session_id), session);
1606
+ await appendCoordinatorEvent(namespaceDir, {
1607
+ kind: "session.started",
1608
+ sessionId: String(session.session_id),
1609
+ summary: `Session ${String(session.session_id)} started by coordinator`,
1610
+ payloadRef: path.relative(namespaceDir, sessionFile(session.session_id)),
1611
+ metadata: { prompted: typeof args.prompt === "string" && args.prompt.length > 0 },
1612
+ });
1157
1613
  const live = hasTmuxIdentity(session) ? await hasTmuxSession(session, commandRunner) : null;
1158
- let sessionState = await writeSessionState(
1159
- namespaceDir,
1160
- String(session.session_id),
1161
- input.prompt ? "running" : "ready_for_input",
1162
- { live, reason: null },
1163
- );
1614
+ let sessionState = await writeSessionState(namespaceDir, String(session.session_id), "ready_for_input", {
1615
+ live,
1616
+ reason: null,
1617
+ });
1164
1618
  if (typeof args.prompt === "string" && args.prompt.length > 0) {
1165
1619
  const turn = await activateTurn(
1166
1620
  session,
@@ -1304,6 +1758,25 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1304
1758
  answered_at: new Date().toISOString(),
1305
1759
  };
1306
1760
  await writeJsonFile(questionPath, answered);
1761
+ if (question.status === "open") {
1762
+ await appendCoordinatorEvent(namespaceDir, {
1763
+ kind: "question.opened",
1764
+ sessionId: typeof question.session_id === "string" ? question.session_id : null,
1765
+ turnId: typeof question.turn_id === "string" ? question.turn_id : null,
1766
+ questionId,
1767
+ summary: `Question ${questionId} opened`,
1768
+ payloadRef: path.relative(namespaceDir, questionPath),
1769
+ });
1770
+ }
1771
+ await appendCoordinatorEvent(namespaceDir, {
1772
+ kind: "question.answered",
1773
+ sessionId: typeof question.session_id === "string" ? question.session_id : null,
1774
+ turnId: typeof question.turn_id === "string" ? question.turn_id : null,
1775
+ questionId,
1776
+ summary: `Question ${questionId} answered`,
1777
+ payloadRef: path.relative(namespaceDir, questionPath),
1778
+ });
1779
+
1307
1780
  let turn: TurnRecord | null = null;
1308
1781
  if (answeredTurnId) {
1309
1782
  turn = await readTurnRecord(namespaceDir, answeredTurnId);
@@ -1402,7 +1875,21 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1402
1875
  promotedTurn = await promoteNextQueuedTurn(turn.session_id);
1403
1876
  }
1404
1877
  }
1405
- await writeJsonFile(path.join(namespaceDir, "reports", `${Date.now()}.json`), report);
1878
+ const reportId = `report-${Date.now()}`;
1879
+ const reportPath = path.join(namespaceDir, "reports", `${reportId}.json`);
1880
+ await writeJsonFile(reportPath, report);
1881
+ await appendCoordinatorEvent(namespaceDir, {
1882
+ kind: "report.written",
1883
+ sessionId,
1884
+ turnId: typeof args.turn_id === "string" ? args.turn_id : null,
1885
+ reportId,
1886
+ summary:
1887
+ typeof args.summary === "string"
1888
+ ? args.summary
1889
+ : `Report ${String(args.status ?? "unknown")} written`,
1890
+ payloadRef: path.relative(namespaceDir, reportPath),
1891
+ metadata: { status: typeof args.status === "string" ? args.status : null },
1892
+ });
1406
1893
  return {
1407
1894
  ok: true,
1408
1895
  report,