@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.
- package/CHANGELOG.md +62 -0
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/commands/harness.d.ts +3 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +7 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +30 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +8 -1
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/harness-control-plane/types.d.ts +4 -0
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +3 -1
- package/dist/types/session/blob-store.d.ts +59 -4
- package/dist/types/session/session-manager.d.ts +24 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +7 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/tools/subagent.d.ts +6 -0
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli.ts +9 -4
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +43 -5
- package/src/commands/launch.ts +2 -2
- package/src/commands/session.ts +3 -1
- package/src/config/file-lock-gc.ts +181 -0
- package/src/config/file-lock.ts +14 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +264 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +513 -26
- package/src/cursor.ts +16 -2
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/export/html/index.ts +13 -9
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +58 -7
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +46 -29
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +211 -8
- package/src/gjc-runtime/tmux-common.ts +29 -0
- package/src/gjc-runtime/tmux-gc.ts +176 -0
- package/src/gjc-runtime/tmux-sessions.ts +68 -12
- package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +89 -27
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +93 -0
- package/src/harness-control-plane/types.ts +4 -0
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +14 -8
- package/src/main.ts +7 -2
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +370 -181
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +34 -42
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +187 -39
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/sdk.ts +46 -5
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +179 -25
- package/src/session/blob-store.ts +148 -6
- package/src/session/session-manager.ts +311 -60
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +78 -6
- package/src/task/receipt.ts +5 -0
- package/src/task/render.ts +21 -1
- package/src/task/types.ts +8 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/ask.ts +56 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +9 -0
- package/src/tools/subagent.ts +26 -2
- package/src/utils/edit-mode.ts +1 -1
- 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
|
|
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
|
|
1314
|
+
const pendingTurn: TurnRecord = {
|
|
972
1315
|
...turn,
|
|
973
1316
|
status: "active",
|
|
974
1317
|
delivery: {
|
|
975
1318
|
delivered: false,
|
|
976
|
-
queued:
|
|
1319
|
+
queued: true,
|
|
977
1320
|
target: typeof target === "string" ? target : null,
|
|
978
|
-
tmux_keys_sent:
|
|
1321
|
+
tmux_keys_sent: false,
|
|
979
1322
|
prompt_acknowledged: false,
|
|
980
|
-
state:
|
|
1323
|
+
state: "queued",
|
|
981
1324
|
attempts: [
|
|
982
1325
|
{
|
|
983
1326
|
delivered: false,
|
|
984
|
-
tmux_keys_sent:
|
|
1327
|
+
tmux_keys_sent: false,
|
|
985
1328
|
channel: "tmux_keys",
|
|
986
1329
|
created_at: timestamp,
|
|
987
|
-
reason:
|
|
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
|
|
996
|
-
await
|
|
997
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
-
|
|
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,
|