@gajae-code/coding-agent 0.5.0 → 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 (125) hide show
  1. package/CHANGELOG.md +19 -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/config/file-lock-gc.d.ts +5 -0
  7. package/dist/types/config/file-lock.d.ts +7 -0
  8. package/dist/types/coordinator/contract.d.ts +1 -1
  9. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  10. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  11. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  12. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  13. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  14. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  15. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  16. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  19. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  20. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  21. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  22. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  23. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  25. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  26. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  27. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  28. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  29. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
  30. package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
  31. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  32. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  33. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  34. package/dist/types/harness-control-plane/owner.d.ts +7 -0
  35. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  36. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  37. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  38. package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
  39. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  40. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  41. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  42. package/dist/types/session/agent-session.d.ts +1 -1
  43. package/dist/types/session/blob-store.d.ts +39 -3
  44. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  45. package/dist/types/tools/ask.d.ts +15 -1
  46. package/dist/types/tools/subagent.d.ts +6 -0
  47. package/package.json +7 -7
  48. package/src/async/job-manager.ts +52 -0
  49. package/src/cli/args.ts +3 -0
  50. package/src/cli/auth-broker-cli.ts +1 -0
  51. package/src/cli/list-models.ts +13 -1
  52. package/src/cli.ts +1 -0
  53. package/src/commands/gc.ts +22 -0
  54. package/src/commands/harness.ts +7 -3
  55. package/src/config/file-lock-gc.ts +181 -0
  56. package/src/config/file-lock.ts +14 -0
  57. package/src/config/model-profiles.ts +24 -15
  58. package/src/coordinator/contract.ts +1 -0
  59. package/src/coordinator-mcp/server.ts +459 -3
  60. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  61. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  62. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  63. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  64. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  65. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  66. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  67. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  68. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  69. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  70. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  71. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  72. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  73. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  75. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  76. package/src/defaults/gjc-defaults.ts +7 -0
  77. package/src/defaults/gjc-grok-cli.ts +22 -0
  78. package/src/extensibility/extensions/index.ts +1 -0
  79. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  80. package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
  81. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  82. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  83. package/src/gjc-runtime/gc-render.ts +70 -0
  84. package/src/gjc-runtime/gc-runtime.ts +403 -0
  85. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  86. package/src/gjc-runtime/ralplan-runtime.ts +58 -7
  87. package/src/gjc-runtime/state-renderer.ts +12 -3
  88. package/src/gjc-runtime/state-runtime.ts +46 -29
  89. package/src/gjc-runtime/team-gc.ts +49 -0
  90. package/src/gjc-runtime/team-runtime.ts +179 -2
  91. package/src/gjc-runtime/tmux-common.ts +14 -0
  92. package/src/gjc-runtime/tmux-gc.ts +176 -0
  93. package/src/gjc-runtime/tmux-sessions.ts +49 -1
  94. package/src/gjc-runtime/ultragoal-runtime.ts +12 -0
  95. package/src/harness-control-plane/gc-adapter.ts +184 -0
  96. package/src/harness-control-plane/owner.ts +11 -0
  97. package/src/harness-control-plane/storage.ts +70 -0
  98. package/src/internal-urls/docs-index.generated.ts +14 -8
  99. package/src/main.ts +7 -2
  100. package/src/modes/components/hook-selector.ts +19 -0
  101. package/src/modes/components/model-selector.ts +25 -8
  102. package/src/modes/components/status-line/segments.ts +1 -1
  103. package/src/modes/controllers/command-controller.ts +25 -6
  104. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  105. package/src/modes/controllers/selector-controller.ts +1 -0
  106. package/src/modes/rpc/rpc-mode.ts +151 -33
  107. package/src/modes/shared/agent-wire/command-dispatch.ts +278 -261
  108. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  109. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  110. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  111. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  112. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  113. package/src/sdk.ts +17 -3
  114. package/src/session/agent-session.ts +77 -8
  115. package/src/session/blob-store.ts +59 -3
  116. package/src/session/session-manager.ts +4 -4
  117. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  118. package/src/skill-state/workflow-hud.ts +106 -10
  119. package/src/slash-commands/builtin-registry.ts +3 -2
  120. package/src/task/executor.ts +9 -0
  121. package/src/tools/ask.ts +56 -1
  122. package/src/tools/job.ts +3 -2
  123. package/src/tools/monitor.ts +36 -1
  124. package/src/tools/subagent-render.ts +9 -0
  125. package/src/tools/subagent.ts +26 -2
@@ -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
 
@@ -880,6 +1202,31 @@ function waitForTurnStateChange(namespaceDir: string, turn: TurnRecord, timeoutM
880
1202
  return deferred.promise;
881
1203
  }
882
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
+
883
1230
  function decodeUtf8WithinByteCap(bytes: Buffer, byteCap: number): string {
884
1231
  const decoder = new TextDecoder("utf-8", { fatal: true });
885
1232
  for (let end = Math.min(bytes.length, byteCap); end >= 0; end--) {
@@ -1032,6 +1379,16 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1032
1379
  reason: tmuxKeysSent ? null : "tmux_delivery_unavailable",
1033
1380
  });
1034
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
+ });
1035
1392
  return activeTurn;
1036
1393
  }
1037
1394
 
@@ -1130,6 +1487,14 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1130
1487
  sessionFile(sessionId),
1131
1488
  commandRunner,
1132
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
+
1133
1498
  return {
1134
1499
  ok: true,
1135
1500
  session: registered.session,
@@ -1169,8 +1534,59 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1169
1534
  if (name === "gjc_coordinator_list_artifacts") return { ok: true, roots: config.allowedRoots };
1170
1535
  if (name === "gjc_coordinator_read_artifact")
1171
1536
  return await readCoordinatorArtifact(config, { path: args.path });
1172
- if (name === "gjc_coordinator_read_coordination_status")
1173
- 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
+ }
1174
1590
  if (name === "gjc_coordinator_start_session") {
1175
1591
  requireCoordinatorMutation(config, "sessions", args);
1176
1592
  const cwd = await assertCoordinatorWorkdir(config, args.cwd);
@@ -1187,6 +1603,13 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1187
1603
  if (!startedRecord) throw new Error("coordinator_session_command_required");
1188
1604
  const session = normalizeSession(startedRecord);
1189
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
+ });
1190
1613
  const live = hasTmuxIdentity(session) ? await hasTmuxSession(session, commandRunner) : null;
1191
1614
  let sessionState = await writeSessionState(namespaceDir, String(session.session_id), "ready_for_input", {
1192
1615
  live,
@@ -1335,6 +1758,25 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1335
1758
  answered_at: new Date().toISOString(),
1336
1759
  };
1337
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
+
1338
1780
  let turn: TurnRecord | null = null;
1339
1781
  if (answeredTurnId) {
1340
1782
  turn = await readTurnRecord(namespaceDir, answeredTurnId);
@@ -1433,7 +1875,21 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1433
1875
  promotedTurn = await promoteNextQueuedTurn(turn.session_id);
1434
1876
  }
1435
1877
  }
1436
- 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
+ });
1437
1893
  return {
1438
1894
  ok: true,
1439
1895
  report,
@@ -0,0 +1,36 @@
1
+ # Merge into ~/.gjc/agent/models.yml (or use as reference)
2
+ # Remove global x-grok-model-override when using multiple Grok Build models.
3
+
4
+ providers:
5
+ grok-build:
6
+ baseUrl: https://cli-chat-proxy.grok.com/v1
7
+ apiKeyEnv: GROK_CLI_OAUTH_TOKEN
8
+ api: grok-cli-responses
9
+ auth: oauth
10
+ headers:
11
+ x-xai-token-auth: xai-grok-cli
12
+ x-grok-client-identifier: gjc-grok-cli
13
+ x-grok-client-version: 0.2.33
14
+ models:
15
+ - id: grok-composer-2.5-fast
16
+ name: Composer 2.5 Fast
17
+ reasoning: false
18
+ input: [text, image]
19
+ contextWindow: 200000
20
+ maxTokens: 30000
21
+ cost:
22
+ input: 3
23
+ output: 15
24
+ cacheRead: 0.5
25
+ cacheWrite: 0
26
+ - id: grok-build
27
+ name: Grok Build
28
+ reasoning: true
29
+ input: [text, image]
30
+ contextWindow: 512000
31
+ maxTokens: 30000
32
+ cost:
33
+ input: 1
34
+ output: 2
35
+ cacheRead: 0.2
36
+ cacheWrite: 0.2
@@ -0,0 +1 @@
1
+ export { default } from "../grok-cli-vendor/src/index.js";
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "gjc-grok-build",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "GJC extension: Grok Build OAuth and grok-cli models"
7
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
3
+ "root": false,
4
+ "vcs": {
5
+ "enabled": true,
6
+ "clientKind": "git",
7
+ "useIgnoreFile": true
8
+ },
9
+ "files": {
10
+ "ignoreUnknown": false
11
+ },
12
+ "formatter": {
13
+ "enabled": true,
14
+ "indentStyle": "space",
15
+ "indentWidth": 2,
16
+ "lineWidth": 100
17
+ },
18
+ "linter": {
19
+ "enabled": true,
20
+ "rules": {
21
+ "recommended": true
22
+ }
23
+ },
24
+ "javascript": {
25
+ "formatter": {
26
+ "quoteStyle": "single",
27
+ "trailingCommas": "all",
28
+ "semicolons": "always"
29
+ }
30
+ },
31
+ "assist": {
32
+ "enabled": true,
33
+ "actions": {
34
+ "source": {
35
+ "organizeImports": "on"
36
+ }
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "gjc-grok-cli-vendor",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Grok CLI chat proxy provider for GJC (SuperGrok OAuth, Composer 2.5 Fast)",
7
+ "dependencies": {}
8
+ }
@@ -0,0 +1 @@
1
+ export { default } from './provider/register.js';