@gajae-code/coding-agent 0.2.0 → 0.2.2
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 +38 -1
- package/dist/types/cli/skills-cli.d.ts +9 -0
- package/dist/types/commands/contribution-prep.d.ts +18 -0
- package/dist/types/commands/session.d.ts +24 -0
- package/dist/types/commands/skills.d.ts +26 -0
- package/dist/types/config/model-registry.d.ts +33 -4
- package/dist/types/config/models-config-schema.d.ts +52 -5
- package/dist/types/config/settings-schema.d.ts +1 -24
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +15 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
- package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
- package/dist/types/goals/runtime.d.ts +3 -9
- package/dist/types/goals/state.d.ts +3 -6
- package/dist/types/goals/tools/goal-tool.d.ts +1 -69
- package/dist/types/modes/components/model-selector.d.ts +21 -1
- package/dist/types/modes/components/status-line/types.d.ts +0 -3
- package/dist/types/modes/components/status-line.d.ts +0 -3
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -12
- package/dist/types/modes/theme/defaults/index.d.ts +0 -2
- package/dist/types/modes/theme/theme.d.ts +1 -2
- package/dist/types/modes/types.d.ts +1 -7
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/contribution-prep.d.ts +47 -0
- package/dist/types/skill-state/active-state.d.ts +4 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
- package/dist/types/skill-state/workflow-hud.d.ts +9 -4
- package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/args.ts +17 -2
- package/src/cli/skills-cli.ts +88 -0
- package/src/cli.ts +7 -1
- package/src/commands/contribution-prep.ts +41 -0
- package/src/commands/deep-interview.ts +6 -22
- package/src/commands/launch.ts +10 -1
- package/src/commands/ralplan.ts +10 -22
- package/src/commands/session.ts +150 -0
- package/src/commands/skills.ts +48 -0
- package/src/commands/state.ts +14 -4
- package/src/commands/team.ts +23 -3
- package/src/commit/agentic/index.ts +1 -0
- package/src/commit/pipeline.ts +1 -0
- package/src/config/model-registry.ts +269 -10
- package/src/config/models-config-schema.ts +124 -88
- package/src/config/settings-schema.ts +1 -25
- package/src/config.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -13
- package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -2
- package/src/defaults/gjc/skills/team/SKILL.md +29 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +23 -25
- package/src/eval/py/prelude.py +1 -1
- package/src/gjc-runtime/deep-interview-runtime.ts +279 -0
- package/src/gjc-runtime/goal-mode-request.ts +2 -19
- package/src/gjc-runtime/launch-tmux.ts +83 -43
- package/src/gjc-runtime/ralplan-runtime.ts +460 -0
- package/src/gjc-runtime/state-runtime.ts +562 -0
- package/src/gjc-runtime/team-runtime.ts +708 -52
- package/src/gjc-runtime/tmux-common.ts +119 -0
- package/src/gjc-runtime/tmux-sessions.ts +165 -0
- package/src/gjc-runtime/ultragoal-guard.ts +6 -3
- package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
- package/src/goals/runtime.ts +38 -144
- package/src/goals/state.ts +36 -7
- package/src/goals/tools/goal-tool.ts +15 -172
- package/src/hooks/skill-state.ts +31 -12
- package/src/internal-urls/docs-index.generated.ts +4 -3
- package/src/main.ts +10 -1
- package/src/modes/components/model-selector.ts +109 -28
- package/src/modes/components/skill-hud/render.ts +4 -0
- package/src/modes/components/status-line/segments.ts +5 -16
- package/src/modes/components/status-line/types.ts +0 -3
- package/src/modes/components/status-line.ts +0 -6
- package/src/modes/controllers/command-controller.ts +25 -1
- package/src/modes/controllers/input-controller.ts +0 -15
- package/src/modes/controllers/selector-controller.ts +42 -2
- package/src/modes/interactive-mode.ts +18 -219
- package/src/modes/theme/defaults/dark-poimandres.json +0 -1
- package/src/modes/theme/defaults/light-poimandres.json +0 -1
- package/src/modes/theme/theme.ts +0 -6
- package/src/modes/types.ts +1 -7
- package/src/prompts/goals/goal-continuation.md +1 -4
- package/src/prompts/goals/goal-mode-active.md +3 -5
- package/src/prompts/system/system-prompt.md +5 -7
- package/src/prompts/tools/goal.md +4 -4
- package/src/sdk.ts +2 -1
- package/src/session/agent-session.ts +18 -0
- package/src/session/contribution-prep.ts +320 -0
- package/src/setup/provider-onboarding.ts +2 -0
- package/src/skill-state/active-state.ts +38 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +88 -24
- package/src/skill-state/workflow-hud.ts +23 -5
- package/src/skill-state/workflow-state-contract.ts +121 -0
- package/src/slash-commands/acp-builtins.ts +11 -2
- package/src/slash-commands/builtin-registry.ts +40 -13
- package/src/task/commands.ts +1 -5
- package/src/tools/gh.ts +212 -2
- package/src/tools/index.ts +2 -5
- package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
- package/dist/types/commands/question.d.ts +0 -7
- package/dist/types/modes/loop-limit.d.ts +0 -22
- package/src/commands/gjc-runtime-bridge.ts +0 -227
- package/src/commands/question.ts +0 -12
- package/src/modes/loop-limit.ts +0 -140
- package/src/prompts/commands/orchestrate.md +0 -49
- package/src/prompts/goals/goal-budget-limit.md +0 -16
- package/src/prompts/tools/create-goal.md +0 -3
- package/src/prompts/tools/get-goal.md +0 -3
- package/src/prompts/tools/update-goal.md +0 -3
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
5
5
|
import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
|
|
6
6
|
import { applyGjcTmuxProfile } from "./launch-tmux";
|
|
7
7
|
|
|
8
|
-
export type GjcTeamPhase = "starting" | "running" | "complete" | "failed" | "cancelled";
|
|
8
|
+
export type GjcTeamPhase = "starting" | "running" | "awaiting_integration" | "complete" | "failed" | "cancelled";
|
|
9
9
|
export type GjcTeamTaskStatus = "pending" | "blocked" | "in_progress" | "completed" | "failed";
|
|
10
10
|
export type GjcWorkerStatusState = "idle" | "working" | "blocked" | "done" | "failed" | "draining" | "unknown";
|
|
11
11
|
|
|
@@ -89,6 +89,7 @@ export interface GjcTeamConfig {
|
|
|
89
89
|
tmux_session_name: string;
|
|
90
90
|
tmux_target: string;
|
|
91
91
|
workspace_mode: "direct" | "worktree";
|
|
92
|
+
dry_run: boolean;
|
|
92
93
|
leader: GjcTeamLeader;
|
|
93
94
|
leader_cwd: string;
|
|
94
95
|
team_state_root: string;
|
|
@@ -121,6 +122,39 @@ export interface GjcTeamMonitorSnapshot {
|
|
|
121
122
|
updated_at: string;
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
export type GjcTeamNotificationDeliveryState =
|
|
126
|
+
| "pending"
|
|
127
|
+
| "sent"
|
|
128
|
+
| "queued"
|
|
129
|
+
| "deferred"
|
|
130
|
+
| "failed"
|
|
131
|
+
| "delivered"
|
|
132
|
+
| "acknowledged";
|
|
133
|
+
|
|
134
|
+
export type GjcTeamPaneAttemptResult = "sent" | "queued" | "deferred" | "failed";
|
|
135
|
+
|
|
136
|
+
export interface GjcTeamNotification {
|
|
137
|
+
id: string;
|
|
138
|
+
kind: "mailbox_message" | "worker_lifecycle" | "invalid_attempt";
|
|
139
|
+
team_name: string;
|
|
140
|
+
recipient: string;
|
|
141
|
+
source: { type: "message" | "task" | "worker" | "event"; id: string };
|
|
142
|
+
idempotency_key?: string;
|
|
143
|
+
delivery_state: GjcTeamNotificationDeliveryState;
|
|
144
|
+
pane_attempt_result?: GjcTeamPaneAttemptResult;
|
|
145
|
+
pane_attempt_reason?: string;
|
|
146
|
+
pane_attempt_at?: string;
|
|
147
|
+
created_at: string;
|
|
148
|
+
updated_at: string;
|
|
149
|
+
replay_count: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface GjcTeamNotificationSummary {
|
|
153
|
+
total: number;
|
|
154
|
+
replay_eligible: number;
|
|
155
|
+
by_state: Record<GjcTeamNotificationDeliveryState, number>;
|
|
156
|
+
}
|
|
157
|
+
|
|
124
158
|
export interface GjcTeamSnapshot {
|
|
125
159
|
team_name: string;
|
|
126
160
|
display_name: string;
|
|
@@ -133,6 +167,7 @@ export interface GjcTeamSnapshot {
|
|
|
133
167
|
task_counts: Record<GjcTeamTaskStatus, number>;
|
|
134
168
|
workers: GjcTeamWorker[];
|
|
135
169
|
integration_by_worker?: Record<string, GjcTeamWorkerIntegrationState>;
|
|
170
|
+
notification_summary: GjcTeamNotificationSummary;
|
|
136
171
|
updated_at: string;
|
|
137
172
|
}
|
|
138
173
|
|
|
@@ -163,6 +198,7 @@ export interface GjcTeamMailboxMessage {
|
|
|
163
198
|
created_at: string;
|
|
164
199
|
delivered_at?: string;
|
|
165
200
|
notified_at?: string;
|
|
201
|
+
idempotency_key?: string;
|
|
166
202
|
}
|
|
167
203
|
|
|
168
204
|
interface FsError {
|
|
@@ -319,6 +355,11 @@ export const GJC_TEAM_API_OPERATIONS = [
|
|
|
319
355
|
"mailbox-list",
|
|
320
356
|
"mailbox-mark-delivered",
|
|
321
357
|
"mailbox-mark-notified",
|
|
358
|
+
"notification-list",
|
|
359
|
+
"notification-read",
|
|
360
|
+
"notification-replay",
|
|
361
|
+
"notification-mark-pane-attempt",
|
|
362
|
+
"worker-startup-ack",
|
|
322
363
|
"create-task",
|
|
323
364
|
"read-task",
|
|
324
365
|
"list-tasks",
|
|
@@ -367,6 +408,10 @@ function sanitizeName(value: string): string {
|
|
|
367
408
|
function shortHash(value: string): string {
|
|
368
409
|
return Bun.hash(value).toString(16).slice(0, 8).padStart(8, "0");
|
|
369
410
|
}
|
|
411
|
+
|
|
412
|
+
function stableHash(value: string): string {
|
|
413
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 24);
|
|
414
|
+
}
|
|
370
415
|
function makeTeamName(task: string, env: NodeJS.ProcessEnv): string {
|
|
371
416
|
const basis = [task, env.GJC_SESSION_ID, env.CODEX_SESSION_ID, env.TMUX_PANE, env.TMUX, now()]
|
|
372
417
|
.filter(Boolean)
|
|
@@ -380,14 +425,65 @@ function teamDir(stateRoot: string, teamName: string): string {
|
|
|
380
425
|
function shellQuote(value: string): string {
|
|
381
426
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
382
427
|
}
|
|
428
|
+
function safePathSegment(kind: string, value: string): string {
|
|
429
|
+
assertSafeId(kind, value);
|
|
430
|
+
return value;
|
|
431
|
+
}
|
|
383
432
|
function taskPath(dir: string, taskId: string): string {
|
|
384
|
-
return path.join(dir, "tasks", `${taskId}.json`);
|
|
433
|
+
return path.join(dir, "tasks", `${safePathSegment("task_id", taskId)}.json`);
|
|
434
|
+
}
|
|
435
|
+
function taskEvidencePath(dir: string, taskId: string): string {
|
|
436
|
+
return path.join(dir, "evidence", "tasks", `${safePathSegment("task_id", taskId)}.json`);
|
|
385
437
|
}
|
|
386
438
|
function mailboxPath(dir: string, worker: string): string {
|
|
387
|
-
return path.join(dir, "mailbox", `${worker}.json`);
|
|
439
|
+
return path.join(dir, "mailbox", `${safePathSegment("worker_id", worker)}.json`);
|
|
440
|
+
}
|
|
441
|
+
function mailboxDirPath(dir: string, worker: string): string {
|
|
442
|
+
return path.join(dir, "mailbox", safePathSegment("worker_id", worker));
|
|
443
|
+
}
|
|
444
|
+
function mailboxMessagePath(dir: string, worker: string, messageId: string): string {
|
|
445
|
+
return path.join(mailboxDirPath(dir, worker), `${safePathSegment("message_id", messageId)}.json`);
|
|
446
|
+
}
|
|
447
|
+
function notificationPath(dir: string, notificationId: string): string {
|
|
448
|
+
return path.join(dir, "notifications", `${safePathSegment("notification_id", notificationId)}.json`);
|
|
388
449
|
}
|
|
389
450
|
function workerDir(dir: string, worker: string): string {
|
|
390
|
-
return path.join(dir, "workers", worker);
|
|
451
|
+
return path.join(dir, "workers", safePathSegment("worker_id", worker));
|
|
452
|
+
}
|
|
453
|
+
function isSafeId(value: string): boolean {
|
|
454
|
+
return (
|
|
455
|
+
/^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(value) &&
|
|
456
|
+
!value.includes("..") &&
|
|
457
|
+
!value.includes("/") &&
|
|
458
|
+
!value.includes("\\")
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
function assertSafeId(kind: string, value: string): void {
|
|
462
|
+
if (!isSafeId(value)) throw new Error(`invalid_${kind}:${value}`);
|
|
463
|
+
}
|
|
464
|
+
function isLeaderRecipient(value: string): boolean {
|
|
465
|
+
return value === "leader-fixed";
|
|
466
|
+
}
|
|
467
|
+
function assertKnownWorker(config: GjcTeamConfig, worker: string, allowLeader = false): void {
|
|
468
|
+
assertSafeId("worker_id", worker);
|
|
469
|
+
if (allowLeader && isLeaderRecipient(worker)) return;
|
|
470
|
+
if (!config.workers.some(candidate => candidate.id === worker)) throw new Error(`unknown_worker:${worker}`);
|
|
471
|
+
}
|
|
472
|
+
function assertKnownParticipant(config: GjcTeamConfig, worker: string): void {
|
|
473
|
+
assertKnownWorker(config, worker, true);
|
|
474
|
+
}
|
|
475
|
+
function messageNotificationId(teamName: string, recipient: string, messageId: string): string {
|
|
476
|
+
return `ntf-${stableHash(["mailbox_message", teamName, recipient, messageId].join(":"))}`;
|
|
477
|
+
}
|
|
478
|
+
function messageIdFor(input: {
|
|
479
|
+
teamName: string;
|
|
480
|
+
fromWorker: string;
|
|
481
|
+
toWorker: string;
|
|
482
|
+
body: string;
|
|
483
|
+
idempotencyKey?: string;
|
|
484
|
+
createdKey: string;
|
|
485
|
+
}): string {
|
|
486
|
+
return `msg-${stableHash([input.teamName, input.fromWorker, input.toWorker, input.idempotencyKey ?? input.body, input.createdKey].join(":"))}`;
|
|
391
487
|
}
|
|
392
488
|
function workerIntegrationDedupePath(dir: string, worker: string): string {
|
|
393
489
|
return path.join(workerDir(dir, worker), "posttooluse-dedupe.json");
|
|
@@ -409,7 +505,23 @@ async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
|
409
505
|
}
|
|
410
506
|
async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
|
|
411
507
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
412
|
-
|
|
508
|
+
const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
509
|
+
await Bun.write(tmpPath, `${JSON.stringify(value, null, 2)}\n`);
|
|
510
|
+
await fs.rename(tmpPath, filePath);
|
|
511
|
+
}
|
|
512
|
+
async function writeJsonFileNoClobber(filePath: string, value: unknown): Promise<boolean> {
|
|
513
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
514
|
+
let handle: fs.FileHandle | undefined;
|
|
515
|
+
try {
|
|
516
|
+
handle = await fs.open(filePath, "wx");
|
|
517
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf-8");
|
|
518
|
+
return true;
|
|
519
|
+
} catch (error) {
|
|
520
|
+
if (isEexist(error)) return false;
|
|
521
|
+
throw error;
|
|
522
|
+
} finally {
|
|
523
|
+
await handle?.close();
|
|
524
|
+
}
|
|
413
525
|
}
|
|
414
526
|
async function appendJsonl(filePath: string, value: unknown): Promise<void> {
|
|
415
527
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
@@ -437,6 +549,7 @@ async function readConfig(dir: string): Promise<GjcTeamConfig> {
|
|
|
437
549
|
tmux_session: tmuxSessionName,
|
|
438
550
|
tmux_session_name: tmuxSessionName,
|
|
439
551
|
tmux_target: config.tmux_target ?? config.tmux_session ?? tmuxSessionName,
|
|
552
|
+
dry_run: config.dry_run ?? config.tmux_session_name === "dry-run",
|
|
440
553
|
leader_cwd: config.leader_cwd ?? config.leader.cwd,
|
|
441
554
|
team_state_root: config.team_state_root ?? config.state_root,
|
|
442
555
|
worker_cli_plan: config.worker_cli_plan ?? Array.from({ length: config.worker_count }, () => "gjc"),
|
|
@@ -462,16 +575,76 @@ function normalizeTask(raw: GjcTeamTask): GjcTeamTask {
|
|
|
462
575
|
version: raw.version ?? 1,
|
|
463
576
|
};
|
|
464
577
|
}
|
|
578
|
+
|
|
579
|
+
const GJC_TEAM_INTEGRATION_ATTENTION_STATUSES = new Set<GjcTeamIntegrationStatus>([
|
|
580
|
+
"integration_failed",
|
|
581
|
+
"merge_conflict",
|
|
582
|
+
"cherry_pick_conflict",
|
|
583
|
+
"rebase_conflict",
|
|
584
|
+
]);
|
|
585
|
+
const GJC_TEAM_INTEGRATION_SETTLED_STATUSES = new Set<GjcTeamIntegrationStatus>(["idle", "integrated"]);
|
|
586
|
+
|
|
587
|
+
async function hasPendingGjcTeamIntegration(
|
|
588
|
+
dir: string,
|
|
589
|
+
config: GjcTeamConfig,
|
|
590
|
+
monitor: GjcTeamMonitorSnapshot | null,
|
|
591
|
+
): Promise<boolean> {
|
|
592
|
+
for (const worker of config.workers) {
|
|
593
|
+
const integration = monitor?.integration_by_worker?.[worker.id];
|
|
594
|
+
if (integration?.status && GJC_TEAM_INTEGRATION_ATTENTION_STATUSES.has(integration.status)) return true;
|
|
595
|
+
|
|
596
|
+
const request = await readJsonFile<GjcWorkerIntegrationDedupeState>(workerIntegrationDedupePath(dir, worker.id));
|
|
597
|
+
if (!request?.last_requested_at) continue;
|
|
598
|
+
if (!integration?.status || !integration.updated_at) return true;
|
|
599
|
+
if (GJC_TEAM_INTEGRATION_ATTENTION_STATUSES.has(integration.status)) return true;
|
|
600
|
+
if (
|
|
601
|
+
GJC_TEAM_INTEGRATION_SETTLED_STATUSES.has(integration.status) &&
|
|
602
|
+
integration.updated_at >= request.last_requested_at
|
|
603
|
+
) {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function resolveGjcTeamSnapshotPhase(
|
|
612
|
+
dir: string,
|
|
613
|
+
config: GjcTeamConfig,
|
|
614
|
+
storedPhase: GjcTeamPhase,
|
|
615
|
+
tasks: GjcTeamTask[],
|
|
616
|
+
monitor: GjcTeamMonitorSnapshot | null,
|
|
617
|
+
): Promise<GjcTeamPhase> {
|
|
618
|
+
if (storedPhase !== "running") return storedPhase;
|
|
619
|
+
if (tasks.length === 0 || !tasks.every(task => task.status === "completed")) return storedPhase;
|
|
620
|
+
return (await hasPendingGjcTeamIntegration(dir, config, monitor)) ? "awaiting_integration" : storedPhase;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
624
|
+
return typeof value === "object" && value != null;
|
|
625
|
+
}
|
|
626
|
+
function isGjcTeamTaskRecord(value: unknown): value is GjcTeamTask {
|
|
627
|
+
return (
|
|
628
|
+
isRecord(value) &&
|
|
629
|
+
typeof value.id === "string" &&
|
|
630
|
+
typeof value.status === "string" &&
|
|
631
|
+
(isGjcTeamTaskStatus(value.status) || value.status === "complete") &&
|
|
632
|
+
(typeof value.subject === "string" || typeof value.title === "string") &&
|
|
633
|
+
(typeof value.description === "string" || typeof value.objective === "string")
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
function isGjcTeamTaskFile(entry: { isFile(): boolean; name: string }): boolean {
|
|
637
|
+
return entry.isFile() && entry.name.endsWith(".json") && !entry.name.endsWith(".evidence.json");
|
|
638
|
+
}
|
|
639
|
+
|
|
465
640
|
async function readTasks(dir: string): Promise<GjcTeamTask[]> {
|
|
466
641
|
try {
|
|
467
642
|
const entries = await fs.readdir(path.join(dir, "tasks"), { withFileTypes: true });
|
|
468
643
|
const tasks = await Promise.all(
|
|
469
|
-
entries
|
|
470
|
-
.filter(entry => entry.isFile() && entry.name.endsWith(".json"))
|
|
471
|
-
.map(entry => readJsonFile<GjcTeamTask>(path.join(dir, "tasks", entry.name))),
|
|
644
|
+
entries.filter(isGjcTeamTaskFile).map(entry => readJsonFile<unknown>(path.join(dir, "tasks", entry.name))),
|
|
472
645
|
);
|
|
473
646
|
return tasks
|
|
474
|
-
.filter(
|
|
647
|
+
.filter(isGjcTeamTaskRecord)
|
|
475
648
|
.map(normalizeTask)
|
|
476
649
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
477
650
|
} catch (error) {
|
|
@@ -678,6 +851,7 @@ function buildWorkerCommand(config: GjcTeamConfig, worker: GjcTeamWorker): strin
|
|
|
678
851
|
`Team state root: ${config.state_root}.`,
|
|
679
852
|
workspace,
|
|
680
853
|
`Task: ${config.task}`,
|
|
854
|
+
`Before claiming work, send startup ACK: gjc team api worker-startup-ack --input '{"team_name":"${config.team_name}","worker_id":"${worker.id}","protocol_version":"1"}' --json.`,
|
|
681
855
|
`Use gjc team api claim-task/transition-task-status with this worker id, record evidence, and do not mutate leader-owned goal state.`,
|
|
682
856
|
].join("\n");
|
|
683
857
|
const env = [
|
|
@@ -1409,10 +1583,11 @@ async function integrateGjcWorkerCommits(
|
|
|
1409
1583
|
}
|
|
1410
1584
|
|
|
1411
1585
|
async function initializeStateDirs(dir: string, workers: GjcTeamWorker[]): Promise<void> {
|
|
1412
|
-
for (const folder of ["tasks", "claims", "mailbox", "dispatch", "approvals", "workers"])
|
|
1586
|
+
for (const folder of ["tasks", "claims", "mailbox", "notifications", "dispatch", "approvals", "workers"])
|
|
1413
1587
|
await fs.mkdir(path.join(dir, folder), { recursive: true });
|
|
1414
1588
|
for (const worker of workers) {
|
|
1415
1589
|
await fs.mkdir(workerDir(dir, worker.id), { recursive: true });
|
|
1590
|
+
await fs.mkdir(mailboxDirPath(dir, worker.id), { recursive: true });
|
|
1416
1591
|
await writeJsonFile(mailboxPath(dir, worker.id), { messages: [] });
|
|
1417
1592
|
await writeJsonFile(path.join(workerDir(dir, worker.id), "status.json"), { state: "idle", updated_at: now() });
|
|
1418
1593
|
await writeJsonFile(path.join(workerDir(dir, worker.id), "heartbeat.json"), {
|
|
@@ -1422,6 +1597,7 @@ async function initializeStateDirs(dir: string, workers: GjcTeamWorker[]): Promi
|
|
|
1422
1597
|
alive: true,
|
|
1423
1598
|
});
|
|
1424
1599
|
}
|
|
1600
|
+
await fs.mkdir(mailboxDirPath(dir, "leader-fixed"), { recursive: true });
|
|
1425
1601
|
await writeJsonFile(mailboxPath(dir, "leader-fixed"), { messages: [] });
|
|
1426
1602
|
}
|
|
1427
1603
|
|
|
@@ -1466,6 +1642,7 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
1466
1642
|
tmux_session_name: tmuxContext.sessionName,
|
|
1467
1643
|
tmux_target: tmuxContext.target,
|
|
1468
1644
|
workspace_mode: worktreeMode.enabled ? "worktree" : "direct",
|
|
1645
|
+
dry_run: options.dryRun ?? false,
|
|
1469
1646
|
leader: { session_id: env.GJC_SESSION_ID ?? env.CODEX_SESSION_ID ?? "", pane_id: tmuxContext.leaderPaneId, cwd },
|
|
1470
1647
|
leader_cwd: cwd,
|
|
1471
1648
|
team_state_root: stateRoot,
|
|
@@ -1489,6 +1666,7 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
1489
1666
|
leader: config.leader,
|
|
1490
1667
|
workers: config.workers,
|
|
1491
1668
|
workspace_mode: config.workspace_mode,
|
|
1669
|
+
dry_run: config.dry_run,
|
|
1492
1670
|
created_at: createdAt,
|
|
1493
1671
|
updated_at: createdAt,
|
|
1494
1672
|
});
|
|
@@ -1496,17 +1674,25 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
1496
1674
|
for (const task of buildInitialTasks(options.task, config.workers)) await writeTask(dir, task);
|
|
1497
1675
|
await appendEvent(dir, {
|
|
1498
1676
|
type: "team_started",
|
|
1499
|
-
message:
|
|
1500
|
-
|
|
1677
|
+
message: options.dryRun
|
|
1678
|
+
? "Created native gjc team dry-run state without starting tmux workers"
|
|
1679
|
+
: "Started native gjc team runtime",
|
|
1680
|
+
data: {
|
|
1681
|
+
worker_count: options.workerCount,
|
|
1682
|
+
agent_type: options.agentType,
|
|
1683
|
+
workspace_mode: config.workspace_mode,
|
|
1684
|
+
dry_run: config.dry_run,
|
|
1685
|
+
},
|
|
1501
1686
|
});
|
|
1502
1687
|
await appendTelemetry(dir, {
|
|
1503
1688
|
type: "team_runtime",
|
|
1504
|
-
message: "Native gjc team runtime initialized",
|
|
1689
|
+
message: options.dryRun ? "Native gjc team dry-run state initialized" : "Native gjc team runtime initialized",
|
|
1505
1690
|
data: {
|
|
1506
1691
|
state_root: stateRoot,
|
|
1507
1692
|
worker_command: config.worker_command,
|
|
1508
1693
|
worker_cli_plan: workerCliPlan,
|
|
1509
1694
|
workspace_mode: config.workspace_mode,
|
|
1695
|
+
dry_run: config.dry_run,
|
|
1510
1696
|
},
|
|
1511
1697
|
});
|
|
1512
1698
|
let tmuxWorkers: GjcTeamWorker[];
|
|
@@ -1539,7 +1725,7 @@ export async function readGjcTeamSnapshot(
|
|
|
1539
1725
|
): Promise<GjcTeamSnapshot> {
|
|
1540
1726
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
1541
1727
|
const config = await readConfig(dir);
|
|
1542
|
-
const
|
|
1728
|
+
const storedPhase = await readPhase(dir);
|
|
1543
1729
|
const tasks = await readTasks(dir);
|
|
1544
1730
|
const taskCounts: Record<GjcTeamTaskStatus, number> = {
|
|
1545
1731
|
pending: 0,
|
|
@@ -1550,6 +1736,8 @@ export async function readGjcTeamSnapshot(
|
|
|
1550
1736
|
};
|
|
1551
1737
|
for (const task of tasks) taskCounts[task.status] += 1;
|
|
1552
1738
|
const monitor = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
|
|
1739
|
+
const notificationSummary = await reconcileTeamNotifications(dir, config);
|
|
1740
|
+
const phase = await resolveGjcTeamSnapshotPhase(dir, config, storedPhase, tasks, monitor);
|
|
1553
1741
|
return {
|
|
1554
1742
|
team_name: config.team_name,
|
|
1555
1743
|
display_name: config.display_name,
|
|
@@ -1562,6 +1750,7 @@ export async function readGjcTeamSnapshot(
|
|
|
1562
1750
|
task_counts: taskCounts,
|
|
1563
1751
|
workers: config.workers,
|
|
1564
1752
|
integration_by_worker: monitor?.integration_by_worker,
|
|
1753
|
+
notification_summary: notificationSummary,
|
|
1565
1754
|
updated_at: config.updated_at,
|
|
1566
1755
|
};
|
|
1567
1756
|
}
|
|
@@ -1677,6 +1866,8 @@ export async function monitorGjcTeam(
|
|
|
1677
1866
|
const previous = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
|
|
1678
1867
|
const integrationByWorker = await integrateGjcWorkerCommits(config, dir, previous, cwd, env);
|
|
1679
1868
|
await writeJsonFile(monitorSnapshotPath(dir), { integration_by_worker: integrationByWorker, updated_at: now() });
|
|
1869
|
+
await replayGjcTeamNotifications(teamName, cwd, env);
|
|
1870
|
+
await computeLifecycleNudges(config, dir, cwd, env);
|
|
1680
1871
|
return readGjcTeamSnapshot(teamName, cwd, env);
|
|
1681
1872
|
}
|
|
1682
1873
|
export async function listGjcTeams(
|
|
@@ -1697,6 +1888,136 @@ export async function listGjcTeams(
|
|
|
1697
1888
|
throw error;
|
|
1698
1889
|
}
|
|
1699
1890
|
}
|
|
1891
|
+
|
|
1892
|
+
function parsePaneAttemptResult(value: string): GjcTeamPaneAttemptResult {
|
|
1893
|
+
if (value === "sent" || value === "queued" || value === "deferred" || value === "failed") return value;
|
|
1894
|
+
throw new Error(`invalid_pane_attempt_result:${value}`);
|
|
1895
|
+
}
|
|
1896
|
+
async function writeGjcWorkerStartupAck(
|
|
1897
|
+
teamName: string,
|
|
1898
|
+
worker: string,
|
|
1899
|
+
cwd: string,
|
|
1900
|
+
env: NodeJS.ProcessEnv,
|
|
1901
|
+
input: Record<string, unknown>,
|
|
1902
|
+
): Promise<Record<string, unknown>> {
|
|
1903
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
1904
|
+
const config = await readConfig(dir);
|
|
1905
|
+
assertKnownWorker(config, worker);
|
|
1906
|
+
const ack = {
|
|
1907
|
+
worker,
|
|
1908
|
+
pid: typeof input.pid === "number" ? input.pid : undefined,
|
|
1909
|
+
session: typeof input.session === "string" ? input.session : undefined,
|
|
1910
|
+
protocol_version: String(input.protocol_version ?? "1"),
|
|
1911
|
+
ack_at: now(),
|
|
1912
|
+
};
|
|
1913
|
+
await writeJsonFile(path.join(workerDir(dir, worker), "startup-ack.json"), ack);
|
|
1914
|
+
await appendEvent(dir, { type: "worker_startup_ack", worker, message: `Worker ${worker} acknowledged startup` });
|
|
1915
|
+
return ack;
|
|
1916
|
+
}
|
|
1917
|
+
function parseDurationEnv(env: NodeJS.ProcessEnv, name: string, fallbackMs: number): number {
|
|
1918
|
+
const raw = env[name]?.trim();
|
|
1919
|
+
if (!raw) return fallbackMs;
|
|
1920
|
+
const parsed = Number(raw);
|
|
1921
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallbackMs;
|
|
1922
|
+
}
|
|
1923
|
+
async function writeLifecycleNudge(
|
|
1924
|
+
dir: string,
|
|
1925
|
+
worker: string,
|
|
1926
|
+
condition: string,
|
|
1927
|
+
severity: "warning" | "error",
|
|
1928
|
+
suggestedAction: string,
|
|
1929
|
+
env: NodeJS.ProcessEnv,
|
|
1930
|
+
): Promise<void> {
|
|
1931
|
+
const fingerprint = `nudge-${stableHash([worker, condition].join(":"))}`;
|
|
1932
|
+
const nudgePath = path.join(workerDir(dir, worker), "nudges", `${fingerprint}.json`);
|
|
1933
|
+
const existing = await readJsonFile<Record<string, unknown>>(nudgePath);
|
|
1934
|
+
const nowMs = Date.now();
|
|
1935
|
+
const cooldownMs = parseDurationEnv(env, "GJC_TEAM_NUDGE_COOLDOWN_MS", 30_000);
|
|
1936
|
+
const cooldownUntil = typeof existing?.cooldown_until === "string" ? Date.parse(existing.cooldown_until) : 0;
|
|
1937
|
+
if (existing && Number.isFinite(cooldownUntil) && cooldownUntil > nowMs) return;
|
|
1938
|
+
const firstSeen = typeof existing?.first_seen_at === "string" ? existing.first_seen_at : now();
|
|
1939
|
+
const count = typeof existing?.count === "number" ? existing.count + 1 : 1;
|
|
1940
|
+
const record = {
|
|
1941
|
+
fingerprint,
|
|
1942
|
+
worker,
|
|
1943
|
+
condition,
|
|
1944
|
+
severity,
|
|
1945
|
+
first_seen_at: firstSeen,
|
|
1946
|
+
last_seen_at: now(),
|
|
1947
|
+
cooldown_until: new Date(nowMs + cooldownMs).toISOString(),
|
|
1948
|
+
count,
|
|
1949
|
+
suggested_action: suggestedAction,
|
|
1950
|
+
auto_action_taken: false,
|
|
1951
|
+
};
|
|
1952
|
+
await writeJsonFile(nudgePath, record);
|
|
1953
|
+
await appendEvent(dir, {
|
|
1954
|
+
type: "worker_lifecycle_nudge",
|
|
1955
|
+
worker,
|
|
1956
|
+
message: suggestedAction,
|
|
1957
|
+
data: { condition, severity, fingerprint, auto_action_taken: false },
|
|
1958
|
+
});
|
|
1959
|
+
await writeNotificationRecord(dir, {
|
|
1960
|
+
id: `ntf-${stableHash(["worker_lifecycle", worker, condition].join(":"))}`,
|
|
1961
|
+
kind: "worker_lifecycle",
|
|
1962
|
+
team_name: path.basename(dir),
|
|
1963
|
+
recipient: "leader-fixed",
|
|
1964
|
+
source: { type: "worker", id: worker },
|
|
1965
|
+
delivery_state: "pending",
|
|
1966
|
+
created_at: firstSeen,
|
|
1967
|
+
updated_at: now(),
|
|
1968
|
+
replay_count: 0,
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
async function computeLifecycleNudges(
|
|
1972
|
+
config: GjcTeamConfig,
|
|
1973
|
+
dir: string,
|
|
1974
|
+
_cwd: string,
|
|
1975
|
+
env: NodeJS.ProcessEnv,
|
|
1976
|
+
): Promise<void> {
|
|
1977
|
+
const startupGraceMs = parseDurationEnv(env, "GJC_TEAM_STARTUP_GRACE_MS", 30_000);
|
|
1978
|
+
const heartbeatStaleMs = parseDurationEnv(env, "GJC_TEAM_HEARTBEAT_STALE_MS", 120_000);
|
|
1979
|
+
const createdAt = Date.parse(config.created_at);
|
|
1980
|
+
const ageMs = Date.now() - (Number.isFinite(createdAt) ? createdAt : Date.now());
|
|
1981
|
+
for (const worker of config.workers) {
|
|
1982
|
+
const ack = await readJsonFile<Record<string, unknown>>(path.join(workerDir(dir, worker.id), "startup-ack.json"));
|
|
1983
|
+
if (!ack && ageMs >= startupGraceMs) {
|
|
1984
|
+
await writeLifecycleNudge(
|
|
1985
|
+
dir,
|
|
1986
|
+
worker.id,
|
|
1987
|
+
"missing_startup_ack",
|
|
1988
|
+
"warning",
|
|
1989
|
+
`Worker ${worker.id} has not sent startup ACK; leader may inspect or relaunch manually.`,
|
|
1990
|
+
env,
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
const heartbeat = await readGjcWorkerHeartbeat(config.team_name, worker.id, config.leader.cwd, {
|
|
1994
|
+
...env,
|
|
1995
|
+
GJC_TEAM_STATE_ROOT: config.state_root,
|
|
1996
|
+
});
|
|
1997
|
+
const heartbeatAt = Date.parse(heartbeat?.last_turn_at ?? worker.last_heartbeat);
|
|
1998
|
+
if (Number.isFinite(heartbeatAt) && Date.now() - heartbeatAt >= heartbeatStaleMs) {
|
|
1999
|
+
await writeLifecycleNudge(
|
|
2000
|
+
dir,
|
|
2001
|
+
worker.id,
|
|
2002
|
+
"stale_heartbeat",
|
|
2003
|
+
"warning",
|
|
2004
|
+
`Worker ${worker.id} heartbeat is stale; leader may inspect or relaunch manually.`,
|
|
2005
|
+
env,
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
if (worker.status === "stopped") {
|
|
2009
|
+
await writeLifecycleNudge(
|
|
2010
|
+
dir,
|
|
2011
|
+
worker.id,
|
|
2012
|
+
"worker_stopped",
|
|
2013
|
+
"error",
|
|
2014
|
+
`Worker ${worker.id} is stopped before team completion; leader action is required.`,
|
|
2015
|
+
env,
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
1700
2021
|
export async function shutdownGjcTeam(
|
|
1701
2022
|
teamName: string,
|
|
1702
2023
|
cwd = process.cwd(),
|
|
@@ -1809,6 +2130,8 @@ export async function claimGjcTeamTask(
|
|
|
1809
2130
|
taskId?: string,
|
|
1810
2131
|
): Promise<GjcTeamApiClaimResult> {
|
|
1811
2132
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2133
|
+
const config = await readConfig(dir);
|
|
2134
|
+
assertKnownWorker(config, workerId);
|
|
1812
2135
|
const tasks = await readTasks(dir);
|
|
1813
2136
|
const task = taskId
|
|
1814
2137
|
? tasks.find(candidate => candidate.id === taskId)
|
|
@@ -1868,15 +2191,22 @@ export async function transitionGjcTeamTaskStatus(
|
|
|
1868
2191
|
cwd = process.cwd(),
|
|
1869
2192
|
env: NodeJS.ProcessEnv = process.env,
|
|
1870
2193
|
claimToken?: string,
|
|
2194
|
+
workerId?: string,
|
|
2195
|
+
evidence?: string,
|
|
1871
2196
|
): Promise<GjcTeamTask> {
|
|
1872
2197
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2198
|
+
const config = await readConfig(dir);
|
|
1873
2199
|
const task = await readGjcTeamTask(teamName, taskId, cwd, env);
|
|
2200
|
+
if (workerId) assertKnownWorker(config, workerId);
|
|
1874
2201
|
if (status === "pending") throw new Error(`invalid_task_transition:${taskId}:pending_requires_release`);
|
|
1875
2202
|
if (task.status === "completed" || task.status === "failed") throw new Error(`task_terminal:${taskId}`);
|
|
1876
2203
|
if (!task.claim) throw new Error(`claim_token_required:${taskId}`);
|
|
1877
2204
|
if (!claimToken) throw new Error(`claim_token_required:${taskId}`);
|
|
1878
2205
|
if (task.claim.token !== claimToken) throw new Error(`claim_token_mismatch:${taskId}`);
|
|
2206
|
+
if (workerId && task.claim.owner !== workerId) throw new Error(`claim_owner_mismatch:${taskId}`);
|
|
1879
2207
|
const terminal = status === "completed" || status === "failed";
|
|
2208
|
+
if (status === "completed" && evidence !== undefined && evidence.trim().length === 0)
|
|
2209
|
+
throw new Error(`task_evidence_required:${taskId}`);
|
|
1880
2210
|
const updated: GjcTeamTask = {
|
|
1881
2211
|
...task,
|
|
1882
2212
|
status,
|
|
@@ -1886,6 +2216,13 @@ export async function transitionGjcTeamTaskStatus(
|
|
|
1886
2216
|
...(terminal ? { completed_at: now() } : {}),
|
|
1887
2217
|
};
|
|
1888
2218
|
await writeTask(dir, updated);
|
|
2219
|
+
if (terminal && evidence)
|
|
2220
|
+
await writeJsonFile(taskEvidencePath(dir, taskId), {
|
|
2221
|
+
task_id: taskId,
|
|
2222
|
+
worker: workerId ?? task.claim.owner,
|
|
2223
|
+
evidence,
|
|
2224
|
+
recorded_at: now(),
|
|
2225
|
+
});
|
|
1889
2226
|
if (terminal) await fs.rm(path.join(dir, "claims", `${taskId}.json`), { force: true });
|
|
1890
2227
|
await appendEvent(dir, {
|
|
1891
2228
|
type: "task_transitioned",
|
|
@@ -1936,15 +2273,227 @@ export async function releaseGjcTeamTaskClaim(
|
|
|
1936
2273
|
return updated;
|
|
1937
2274
|
}
|
|
1938
2275
|
|
|
1939
|
-
|
|
2276
|
+
function emptyNotificationSummary(): GjcTeamNotificationSummary {
|
|
2277
|
+
return {
|
|
2278
|
+
total: 0,
|
|
2279
|
+
replay_eligible: 0,
|
|
2280
|
+
by_state: {
|
|
2281
|
+
pending: 0,
|
|
2282
|
+
sent: 0,
|
|
2283
|
+
queued: 0,
|
|
2284
|
+
deferred: 0,
|
|
2285
|
+
failed: 0,
|
|
2286
|
+
delivered: 0,
|
|
2287
|
+
acknowledged: 0,
|
|
2288
|
+
},
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
function isReplayEligibleNotification(state: GjcTeamNotificationDeliveryState): boolean {
|
|
2292
|
+
return state === "pending" || state === "queued" || state === "deferred" || state === "failed";
|
|
2293
|
+
}
|
|
2294
|
+
function summarizeNotifications(notifications: GjcTeamNotification[]): GjcTeamNotificationSummary {
|
|
2295
|
+
const summary = emptyNotificationSummary();
|
|
2296
|
+
for (const notification of notifications) {
|
|
2297
|
+
summary.total += 1;
|
|
2298
|
+
summary.by_state[notification.delivery_state] += 1;
|
|
2299
|
+
if (isReplayEligibleNotification(notification.delivery_state)) summary.replay_eligible += 1;
|
|
2300
|
+
}
|
|
2301
|
+
return summary;
|
|
2302
|
+
}
|
|
2303
|
+
async function listNotificationRecords(dir: string): Promise<GjcTeamNotification[]> {
|
|
2304
|
+
const notificationsDir = path.join(dir, "notifications");
|
|
2305
|
+
try {
|
|
2306
|
+
const entries = await fs.readdir(notificationsDir, { withFileTypes: true });
|
|
2307
|
+
const records = await Promise.all(
|
|
2308
|
+
entries
|
|
2309
|
+
.filter(entry => entry.isFile() && entry.name.endsWith(".json"))
|
|
2310
|
+
.map(entry => readJsonFile<GjcTeamNotification>(path.join(notificationsDir, entry.name))),
|
|
2311
|
+
);
|
|
2312
|
+
return records
|
|
2313
|
+
.filter((record): record is GjcTeamNotification => record != null)
|
|
2314
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
2315
|
+
} catch (error) {
|
|
2316
|
+
if (isEnoent(error)) return [];
|
|
2317
|
+
throw error;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
async function readNotificationRecord(dir: string, notificationId: string): Promise<GjcTeamNotification> {
|
|
2321
|
+
assertSafeId("notification_id", notificationId);
|
|
2322
|
+
const notification = await readJsonFile<GjcTeamNotification>(notificationPath(dir, notificationId));
|
|
2323
|
+
if (!notification) throw new Error(`notification_not_found:${notificationId}`);
|
|
2324
|
+
return notification;
|
|
2325
|
+
}
|
|
2326
|
+
function mergeNotificationState(
|
|
2327
|
+
current: GjcTeamNotificationDeliveryState,
|
|
2328
|
+
next: GjcTeamNotificationDeliveryState,
|
|
2329
|
+
): GjcTeamNotificationDeliveryState {
|
|
2330
|
+
const rank: Record<GjcTeamNotificationDeliveryState, number> = {
|
|
2331
|
+
pending: 0,
|
|
2332
|
+
queued: 1,
|
|
2333
|
+
deferred: 1,
|
|
2334
|
+
failed: 1,
|
|
2335
|
+
sent: 2,
|
|
2336
|
+
delivered: 3,
|
|
2337
|
+
acknowledged: 4,
|
|
2338
|
+
};
|
|
2339
|
+
return rank[next] >= rank[current] ? next : current;
|
|
2340
|
+
}
|
|
2341
|
+
async function writeNotificationRecord(dir: string, notification: GjcTeamNotification): Promise<GjcTeamNotification> {
|
|
2342
|
+
const existing = await readJsonFile<GjcTeamNotification>(notificationPath(dir, notification.id));
|
|
2343
|
+
const merged: GjcTeamNotification = existing
|
|
2344
|
+
? {
|
|
2345
|
+
...existing,
|
|
2346
|
+
...notification,
|
|
2347
|
+
delivery_state: mergeNotificationState(existing.delivery_state, notification.delivery_state),
|
|
2348
|
+
created_at: existing.created_at,
|
|
2349
|
+
replay_count: Math.max(existing.replay_count ?? 0, notification.replay_count ?? 0),
|
|
2350
|
+
updated_at: now(),
|
|
2351
|
+
}
|
|
2352
|
+
: notification;
|
|
2353
|
+
await writeJsonFile(notificationPath(dir, merged.id), merged);
|
|
2354
|
+
return merged;
|
|
2355
|
+
}
|
|
2356
|
+
async function createMessageNotification(
|
|
2357
|
+
dir: string,
|
|
2358
|
+
teamName: string,
|
|
2359
|
+
message: GjcTeamMailboxMessage,
|
|
2360
|
+
state: GjcTeamNotificationDeliveryState = "pending",
|
|
2361
|
+
): Promise<GjcTeamNotification> {
|
|
2362
|
+
const id = messageNotificationId(teamName, message.to_worker, message.message_id);
|
|
2363
|
+
return writeNotificationRecord(dir, {
|
|
2364
|
+
id,
|
|
2365
|
+
kind: "mailbox_message",
|
|
2366
|
+
team_name: teamName,
|
|
2367
|
+
recipient: message.to_worker,
|
|
2368
|
+
source: { type: "message", id: message.message_id },
|
|
2369
|
+
idempotency_key: message.idempotency_key,
|
|
2370
|
+
delivery_state: state,
|
|
2371
|
+
created_at: message.created_at,
|
|
2372
|
+
updated_at: now(),
|
|
2373
|
+
replay_count: 0,
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
async function readLegacyMailbox(dir: string, worker: string): Promise<{ messages: GjcTeamMailboxMessage[] }> {
|
|
1940
2377
|
return (await readJsonFile<{ messages: GjcTeamMailboxMessage[] }>(mailboxPath(dir, worker))) ?? { messages: [] };
|
|
1941
2378
|
}
|
|
1942
|
-
async function
|
|
2379
|
+
async function readMailbox(dir: string, worker: string): Promise<{ messages: GjcTeamMailboxMessage[] }> {
|
|
2380
|
+
assertSafeId("worker_id", worker);
|
|
2381
|
+
const byId = new Map<string, GjcTeamMailboxMessage>();
|
|
2382
|
+
for (const message of (await readLegacyMailbox(dir, worker)).messages ?? []) byId.set(message.message_id, message);
|
|
2383
|
+
try {
|
|
2384
|
+
const entries = await fs.readdir(mailboxDirPath(dir, worker), { withFileTypes: true });
|
|
2385
|
+
const records = await Promise.all(
|
|
2386
|
+
entries
|
|
2387
|
+
.filter(entry => entry.isFile() && entry.name.endsWith(".json"))
|
|
2388
|
+
.map(entry => readJsonFile<GjcTeamMailboxMessage>(path.join(mailboxDirPath(dir, worker), entry.name))),
|
|
2389
|
+
);
|
|
2390
|
+
for (const message of records) if (message) byId.set(message.message_id, message);
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
if (!isEnoent(error)) throw error;
|
|
2393
|
+
}
|
|
2394
|
+
return { messages: [...byId.values()].sort((a, b) => a.created_at.localeCompare(b.created_at)) };
|
|
2395
|
+
}
|
|
2396
|
+
async function writeLegacyMailboxView(dir: string, worker: string): Promise<void> {
|
|
2397
|
+
const current = await readMailbox(dir, worker);
|
|
2398
|
+
await writeJsonFile(mailboxPath(dir, worker), current);
|
|
2399
|
+
}
|
|
2400
|
+
async function writeMailboxMessage(
|
|
1943
2401
|
dir: string,
|
|
1944
2402
|
worker: string,
|
|
1945
|
-
|
|
1946
|
-
): Promise<
|
|
1947
|
-
|
|
2403
|
+
message: GjcTeamMailboxMessage,
|
|
2404
|
+
): Promise<GjcTeamMailboxMessage> {
|
|
2405
|
+
assertSafeId("message_id", message.message_id);
|
|
2406
|
+
const filePath = mailboxMessagePath(dir, worker, message.message_id);
|
|
2407
|
+
const existing = await readJsonFile<GjcTeamMailboxMessage>(filePath);
|
|
2408
|
+
if (existing) {
|
|
2409
|
+
if (
|
|
2410
|
+
existing.from_worker !== message.from_worker ||
|
|
2411
|
+
existing.to_worker !== message.to_worker ||
|
|
2412
|
+
existing.body !== message.body
|
|
2413
|
+
) {
|
|
2414
|
+
throw new Error(`message_id_conflict:${message.message_id}`);
|
|
2415
|
+
}
|
|
2416
|
+
const merged = {
|
|
2417
|
+
...existing,
|
|
2418
|
+
...message,
|
|
2419
|
+
notified_at: existing.notified_at ?? message.notified_at,
|
|
2420
|
+
delivered_at: existing.delivered_at ?? message.delivered_at,
|
|
2421
|
+
};
|
|
2422
|
+
await writeJsonFile(filePath, merged);
|
|
2423
|
+
await writeLegacyMailboxView(dir, worker);
|
|
2424
|
+
return merged;
|
|
2425
|
+
}
|
|
2426
|
+
const created = await writeJsonFileNoClobber(filePath, message);
|
|
2427
|
+
if (!created) return writeMailboxMessage(dir, worker, message);
|
|
2428
|
+
await writeLegacyMailboxView(dir, worker);
|
|
2429
|
+
return message;
|
|
2430
|
+
}
|
|
2431
|
+
async function reconcileTeamNotifications(dir: string, config: GjcTeamConfig): Promise<GjcTeamNotificationSummary> {
|
|
2432
|
+
for (const recipient of ["leader-fixed", ...config.workers.map(worker => worker.id)]) {
|
|
2433
|
+
const mailbox = await readMailbox(dir, recipient);
|
|
2434
|
+
for (const message of mailbox.messages) {
|
|
2435
|
+
const state = message.delivered_at ? "acknowledged" : message.notified_at ? "delivered" : "pending";
|
|
2436
|
+
await createMessageNotification(dir, config.team_name, message, state);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
return summarizeNotifications(await listNotificationRecords(dir));
|
|
2440
|
+
}
|
|
2441
|
+
async function attemptPaneNotification(
|
|
2442
|
+
dir: string,
|
|
2443
|
+
config: GjcTeamConfig,
|
|
2444
|
+
notification: GjcTeamNotification,
|
|
2445
|
+
env: NodeJS.ProcessEnv,
|
|
2446
|
+
): Promise<GjcTeamNotification> {
|
|
2447
|
+
const paneId =
|
|
2448
|
+
notification.recipient === "leader-fixed"
|
|
2449
|
+
? config.leader.pane_id
|
|
2450
|
+
: config.workers.find(worker => worker.id === notification.recipient)?.pane_id;
|
|
2451
|
+
let result: GjcTeamPaneAttemptResult = "deferred";
|
|
2452
|
+
let reason = "pane_missing";
|
|
2453
|
+
if (paneId) {
|
|
2454
|
+
if (config.tmux_session === "dry-run" || env.GJC_TEAM_FAKE_PANE_ATTEMPT === "sent") {
|
|
2455
|
+
result = "sent";
|
|
2456
|
+
reason = "dry_run_or_fake_tmux";
|
|
2457
|
+
} else {
|
|
2458
|
+
result = "queued";
|
|
2459
|
+
reason = "tmux_delivery_recorded_without_injection";
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
return writeNotificationRecord(dir, {
|
|
2463
|
+
...notification,
|
|
2464
|
+
delivery_state: result,
|
|
2465
|
+
pane_attempt_result: result,
|
|
2466
|
+
pane_attempt_reason: reason,
|
|
2467
|
+
pane_attempt_at: now(),
|
|
2468
|
+
updated_at: now(),
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
export async function replayGjcTeamNotifications(
|
|
2472
|
+
teamName: string,
|
|
2473
|
+
cwd = process.cwd(),
|
|
2474
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
2475
|
+
): Promise<{ notifications: GjcTeamNotification[]; summary: GjcTeamNotificationSummary }> {
|
|
2476
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
2477
|
+
const config = await readConfig(dir);
|
|
2478
|
+
await reconcileTeamNotifications(dir, config);
|
|
2479
|
+
const next: GjcTeamNotification[] = [];
|
|
2480
|
+
for (const notification of await listNotificationRecords(dir)) {
|
|
2481
|
+
if (!isReplayEligibleNotification(notification.delivery_state)) {
|
|
2482
|
+
next.push(notification);
|
|
2483
|
+
continue;
|
|
2484
|
+
}
|
|
2485
|
+
const attempted = await attemptPaneNotification(
|
|
2486
|
+
dir,
|
|
2487
|
+
config,
|
|
2488
|
+
{
|
|
2489
|
+
...notification,
|
|
2490
|
+
replay_count: (notification.replay_count ?? 0) + 1,
|
|
2491
|
+
},
|
|
2492
|
+
env,
|
|
2493
|
+
);
|
|
2494
|
+
next.push(attempted);
|
|
2495
|
+
}
|
|
2496
|
+
return { notifications: next, summary: summarizeNotifications(next) };
|
|
1948
2497
|
}
|
|
1949
2498
|
export async function sendGjcTeamMessage(
|
|
1950
2499
|
teamName: string,
|
|
@@ -1953,20 +2502,31 @@ export async function sendGjcTeamMessage(
|
|
|
1953
2502
|
body: string,
|
|
1954
2503
|
cwd = process.cwd(),
|
|
1955
2504
|
env: NodeJS.ProcessEnv = process.env,
|
|
2505
|
+
idempotencyKey?: string,
|
|
1956
2506
|
): Promise<GjcTeamMailboxMessage> {
|
|
1957
2507
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
1958
|
-
const
|
|
1959
|
-
|
|
2508
|
+
const config = await readConfig(dir);
|
|
2509
|
+
assertKnownParticipant(config, fromWorker);
|
|
2510
|
+
assertKnownParticipant(config, toWorker);
|
|
2511
|
+
const createdKey = idempotencyKey ?? randomUUID();
|
|
2512
|
+
const message: GjcTeamMailboxMessage = {
|
|
2513
|
+
message_id: messageIdFor({ teamName: config.team_name, fromWorker, toWorker, body, idempotencyKey, createdKey }),
|
|
1960
2514
|
from_worker: fromWorker,
|
|
1961
2515
|
to_worker: toWorker,
|
|
1962
2516
|
body,
|
|
1963
2517
|
created_at: now(),
|
|
2518
|
+
...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
|
|
1964
2519
|
};
|
|
1965
|
-
const
|
|
1966
|
-
|
|
1967
|
-
await
|
|
1968
|
-
await appendEvent(dir, {
|
|
1969
|
-
|
|
2520
|
+
const written = await writeMailboxMessage(dir, toWorker, message);
|
|
2521
|
+
const notification = await createMessageNotification(dir, config.team_name, written);
|
|
2522
|
+
await attemptPaneNotification(dir, config, notification, env);
|
|
2523
|
+
await appendEvent(dir, {
|
|
2524
|
+
type: "message_sent",
|
|
2525
|
+
worker: fromWorker,
|
|
2526
|
+
message: body,
|
|
2527
|
+
data: { to_worker: toWorker, message_id: written.message_id },
|
|
2528
|
+
});
|
|
2529
|
+
return written;
|
|
1970
2530
|
}
|
|
1971
2531
|
export async function broadcastGjcTeamMessage(
|
|
1972
2532
|
teamName: string,
|
|
@@ -1974,10 +2534,21 @@ export async function broadcastGjcTeamMessage(
|
|
|
1974
2534
|
body: string,
|
|
1975
2535
|
cwd = process.cwd(),
|
|
1976
2536
|
env: NodeJS.ProcessEnv = process.env,
|
|
2537
|
+
idempotencyKey?: string,
|
|
1977
2538
|
): Promise<GjcTeamMailboxMessage[]> {
|
|
1978
2539
|
const config = await readConfig(await findTeamDir(teamName, cwd, env));
|
|
1979
2540
|
return Promise.all(
|
|
1980
|
-
config.workers.map(worker =>
|
|
2541
|
+
config.workers.map(worker =>
|
|
2542
|
+
sendGjcTeamMessage(
|
|
2543
|
+
teamName,
|
|
2544
|
+
fromWorker,
|
|
2545
|
+
worker.id,
|
|
2546
|
+
body,
|
|
2547
|
+
cwd,
|
|
2548
|
+
env,
|
|
2549
|
+
idempotencyKey ? `${idempotencyKey}:${worker.id}` : undefined,
|
|
2550
|
+
),
|
|
2551
|
+
),
|
|
1981
2552
|
);
|
|
1982
2553
|
}
|
|
1983
2554
|
export async function listGjcTeamMailbox(
|
|
@@ -1986,7 +2557,10 @@ export async function listGjcTeamMailbox(
|
|
|
1986
2557
|
cwd = process.cwd(),
|
|
1987
2558
|
env: NodeJS.ProcessEnv = process.env,
|
|
1988
2559
|
): Promise<GjcTeamMailboxMessage[]> {
|
|
1989
|
-
|
|
2560
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
2561
|
+
const config = await readConfig(dir);
|
|
2562
|
+
assertKnownParticipant(config, worker);
|
|
2563
|
+
return (await readMailbox(dir, worker)).messages;
|
|
1990
2564
|
}
|
|
1991
2565
|
export async function markGjcTeamMailboxMessage(
|
|
1992
2566
|
teamName: string,
|
|
@@ -1996,13 +2570,29 @@ export async function markGjcTeamMailboxMessage(
|
|
|
1996
2570
|
cwd = process.cwd(),
|
|
1997
2571
|
env: NodeJS.ProcessEnv = process.env,
|
|
1998
2572
|
): Promise<GjcTeamMailboxMessage> {
|
|
2573
|
+
assertSafeId("message_id", messageId);
|
|
1999
2574
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2575
|
+
const config = await readConfig(dir);
|
|
2576
|
+
assertKnownParticipant(config, worker);
|
|
2000
2577
|
const mailbox = await readMailbox(dir, worker);
|
|
2001
|
-
const
|
|
2002
|
-
if (
|
|
2003
|
-
|
|
2004
|
-
await
|
|
2005
|
-
|
|
2578
|
+
const message = mailbox.messages.find(candidate => candidate.message_id === messageId);
|
|
2579
|
+
if (!message) throw new Error(`message_not_found:${messageId}`);
|
|
2580
|
+
const updated = { ...message, [field]: message[field] ?? now() };
|
|
2581
|
+
const written = await writeMailboxMessage(dir, worker, updated);
|
|
2582
|
+
const notificationId = messageNotificationId(config.team_name, worker, messageId);
|
|
2583
|
+
const existing =
|
|
2584
|
+
(await readJsonFile<GjcTeamNotification>(notificationPath(dir, notificationId))) ??
|
|
2585
|
+
(await createMessageNotification(dir, config.team_name, written));
|
|
2586
|
+
const nextState: GjcTeamNotificationDeliveryState = field === "delivered_at" ? "acknowledged" : "delivered";
|
|
2587
|
+
const before = existing.delivery_state;
|
|
2588
|
+
await writeNotificationRecord(dir, { ...existing, delivery_state: nextState, updated_at: now() });
|
|
2589
|
+
if (mergeNotificationState(before, nextState) !== before)
|
|
2590
|
+
await appendEvent(dir, {
|
|
2591
|
+
type: `message_${field === "delivered_at" ? "acknowledged" : "notified"}`,
|
|
2592
|
+
worker,
|
|
2593
|
+
message: messageId,
|
|
2594
|
+
});
|
|
2595
|
+
return written;
|
|
2006
2596
|
}
|
|
2007
2597
|
export async function readGjcWorkerStatus(
|
|
2008
2598
|
teamName: string,
|
|
@@ -2010,10 +2600,14 @@ export async function readGjcWorkerStatus(
|
|
|
2010
2600
|
cwd = process.cwd(),
|
|
2011
2601
|
env: NodeJS.ProcessEnv = process.env,
|
|
2012
2602
|
): Promise<WorkerStatusFile> {
|
|
2603
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
2604
|
+
const config = await readConfig(dir);
|
|
2605
|
+
assertKnownWorker(config, worker);
|
|
2013
2606
|
return (
|
|
2014
|
-
(await readJsonFile<WorkerStatusFile>(
|
|
2015
|
-
|
|
2016
|
-
|
|
2607
|
+
(await readJsonFile<WorkerStatusFile>(path.join(workerDir(dir, worker), "status.json"))) ?? {
|
|
2608
|
+
state: "unknown",
|
|
2609
|
+
updated_at: now(),
|
|
2610
|
+
}
|
|
2017
2611
|
);
|
|
2018
2612
|
}
|
|
2019
2613
|
export async function readGjcWorkerHeartbeat(
|
|
@@ -2022,9 +2616,10 @@ export async function readGjcWorkerHeartbeat(
|
|
|
2022
2616
|
cwd = process.cwd(),
|
|
2023
2617
|
env: NodeJS.ProcessEnv = process.env,
|
|
2024
2618
|
): Promise<WorkerHeartbeatFile | null> {
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
);
|
|
2619
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
2620
|
+
const config = await readConfig(dir);
|
|
2621
|
+
assertKnownWorker(config, worker);
|
|
2622
|
+
return readJsonFile<WorkerHeartbeatFile>(path.join(workerDir(dir, worker), "heartbeat.json"));
|
|
2028
2623
|
}
|
|
2029
2624
|
export async function updateGjcWorkerHeartbeat(
|
|
2030
2625
|
teamName: string,
|
|
@@ -2033,8 +2628,11 @@ export async function updateGjcWorkerHeartbeat(
|
|
|
2033
2628
|
cwd = process.cwd(),
|
|
2034
2629
|
env: NodeJS.ProcessEnv = process.env,
|
|
2035
2630
|
): Promise<WorkerHeartbeatFile> {
|
|
2631
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
2632
|
+
const config = await readConfig(dir);
|
|
2633
|
+
assertKnownWorker(config, worker);
|
|
2036
2634
|
const value = { ...heartbeat, last_turn_at: heartbeat.last_turn_at || now() };
|
|
2037
|
-
await writeJsonFile(path.join(workerDir(
|
|
2635
|
+
await writeJsonFile(path.join(workerDir(dir, worker), "heartbeat.json"), value);
|
|
2038
2636
|
return value;
|
|
2039
2637
|
}
|
|
2040
2638
|
export async function writeGjcWorkerInbox(
|
|
@@ -2044,7 +2642,10 @@ export async function writeGjcWorkerInbox(
|
|
|
2044
2642
|
cwd = process.cwd(),
|
|
2045
2643
|
env: NodeJS.ProcessEnv = process.env,
|
|
2046
2644
|
): Promise<{ path: string }> {
|
|
2047
|
-
const
|
|
2645
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
2646
|
+
const config = await readConfig(dir);
|
|
2647
|
+
assertKnownWorker(config, worker);
|
|
2648
|
+
const filePath = path.join(workerDir(dir, worker), "inbox.md");
|
|
2048
2649
|
await Bun.write(filePath, content);
|
|
2049
2650
|
return { path: filePath };
|
|
2050
2651
|
}
|
|
@@ -2054,7 +2655,10 @@ export async function writeGjcWorkerIdentity(
|
|
|
2054
2655
|
cwd = process.cwd(),
|
|
2055
2656
|
env: NodeJS.ProcessEnv = process.env,
|
|
2056
2657
|
): Promise<GjcTeamWorker> {
|
|
2057
|
-
|
|
2658
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
2659
|
+
const config = await readConfig(dir);
|
|
2660
|
+
assertKnownWorker(config, worker.id);
|
|
2661
|
+
await writeJsonFile(path.join(workerDir(dir, worker.id), "identity.json"), worker);
|
|
2058
2662
|
return worker;
|
|
2059
2663
|
}
|
|
2060
2664
|
export async function readGjcTeamEvents(
|
|
@@ -2116,6 +2720,7 @@ export async function writeGjcTaskApproval(
|
|
|
2116
2720
|
cwd = process.cwd(),
|
|
2117
2721
|
env: NodeJS.ProcessEnv = process.env,
|
|
2118
2722
|
): Promise<Record<string, unknown>> {
|
|
2723
|
+
assertSafeId("task_id", taskId);
|
|
2119
2724
|
await writeJsonFile(path.join(await findTeamDir(teamName, cwd, env), "approvals", `${taskId}.json`), approval);
|
|
2120
2725
|
return approval;
|
|
2121
2726
|
}
|
|
@@ -2125,6 +2730,7 @@ export async function readGjcTaskApproval(
|
|
|
2125
2730
|
cwd = process.cwd(),
|
|
2126
2731
|
env: NodeJS.ProcessEnv = process.env,
|
|
2127
2732
|
): Promise<Record<string, unknown> | null> {
|
|
2733
|
+
assertSafeId("task_id", taskId);
|
|
2128
2734
|
return readJsonFile<Record<string, unknown>>(
|
|
2129
2735
|
path.join(await findTeamDir(teamName, cwd, env), "approvals", `${taskId}.json`),
|
|
2130
2736
|
);
|
|
@@ -2136,11 +2742,12 @@ export async function writeGjcShutdownRequest(
|
|
|
2136
2742
|
cwd = process.cwd(),
|
|
2137
2743
|
env: NodeJS.ProcessEnv = process.env,
|
|
2138
2744
|
): Promise<Record<string, unknown>> {
|
|
2745
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
2746
|
+
const config = await readConfig(dir);
|
|
2747
|
+
assertKnownWorker(config, worker);
|
|
2748
|
+
assertKnownParticipant(config, requestedBy);
|
|
2139
2749
|
const value = { worker, requested_by: requestedBy, requested_at: now() };
|
|
2140
|
-
await writeJsonFile(
|
|
2141
|
-
path.join(workerDir(await findTeamDir(teamName, cwd, env), worker), "shutdown-request.json"),
|
|
2142
|
-
value,
|
|
2143
|
-
);
|
|
2750
|
+
await writeJsonFile(path.join(workerDir(dir, worker), "shutdown-request.json"), value);
|
|
2144
2751
|
return value;
|
|
2145
2752
|
}
|
|
2146
2753
|
export async function readGjcShutdownAck(
|
|
@@ -2149,9 +2756,10 @@ export async function readGjcShutdownAck(
|
|
|
2149
2756
|
cwd = process.cwd(),
|
|
2150
2757
|
env: NodeJS.ProcessEnv = process.env,
|
|
2151
2758
|
): Promise<Record<string, unknown> | null> {
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
);
|
|
2759
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
2760
|
+
const config = await readConfig(dir);
|
|
2761
|
+
assertKnownWorker(config, worker);
|
|
2762
|
+
return readJsonFile<Record<string, unknown>>(path.join(workerDir(dir, worker), "shutdown-ack.json"));
|
|
2155
2763
|
}
|
|
2156
2764
|
|
|
2157
2765
|
export async function executeGjcTeamApiOperation(
|
|
@@ -2162,7 +2770,9 @@ export async function executeGjcTeamApiOperation(
|
|
|
2162
2770
|
): Promise<unknown> {
|
|
2163
2771
|
const teamName = String(input.team_name ?? input.teamName ?? "").trim();
|
|
2164
2772
|
if (!teamName) throw new Error("missing_team_name");
|
|
2165
|
-
const
|
|
2773
|
+
const workerInput = input.worker ?? input.worker_id ?? input.workerId;
|
|
2774
|
+
const worker = String(workerInput ?? "worker-1");
|
|
2775
|
+
const explicitWorker = workerInput == null ? undefined : String(workerInput);
|
|
2166
2776
|
switch (operation) {
|
|
2167
2777
|
case "list-tasks":
|
|
2168
2778
|
return { tasks: await listGjcTeamTasks(teamName, cwd, env) };
|
|
@@ -2210,6 +2820,12 @@ export async function executeGjcTeamApiOperation(
|
|
|
2210
2820
|
cwd,
|
|
2211
2821
|
env,
|
|
2212
2822
|
typeof input.claim_token === "string" ? input.claim_token : undefined,
|
|
2823
|
+
explicitWorker,
|
|
2824
|
+
typeof input.evidence === "string"
|
|
2825
|
+
? input.evidence
|
|
2826
|
+
: typeof input.result === "string"
|
|
2827
|
+
? input.result
|
|
2828
|
+
: undefined,
|
|
2213
2829
|
),
|
|
2214
2830
|
};
|
|
2215
2831
|
case "release-task-claim":
|
|
@@ -2233,11 +2849,19 @@ export async function executeGjcTeamApiOperation(
|
|
|
2233
2849
|
String(input.body),
|
|
2234
2850
|
cwd,
|
|
2235
2851
|
env,
|
|
2852
|
+
typeof input.idempotency_key === "string" ? input.idempotency_key : undefined,
|
|
2236
2853
|
),
|
|
2237
2854
|
};
|
|
2238
2855
|
case "broadcast":
|
|
2239
2856
|
return {
|
|
2240
|
-
messages: await broadcastGjcTeamMessage(
|
|
2857
|
+
messages: await broadcastGjcTeamMessage(
|
|
2858
|
+
teamName,
|
|
2859
|
+
String(input.from_worker),
|
|
2860
|
+
String(input.body),
|
|
2861
|
+
cwd,
|
|
2862
|
+
env,
|
|
2863
|
+
typeof input.idempotency_key === "string" ? input.idempotency_key : undefined,
|
|
2864
|
+
),
|
|
2241
2865
|
};
|
|
2242
2866
|
case "mailbox-list":
|
|
2243
2867
|
return { messages: await listGjcTeamMailbox(teamName, worker, cwd, env) };
|
|
@@ -2263,6 +2887,38 @@ export async function executeGjcTeamApiOperation(
|
|
|
2263
2887
|
env,
|
|
2264
2888
|
),
|
|
2265
2889
|
};
|
|
2890
|
+
case "notification-list": {
|
|
2891
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
2892
|
+
const config = await readConfig(dir);
|
|
2893
|
+
await reconcileTeamNotifications(dir, config);
|
|
2894
|
+
const notifications = await listNotificationRecords(dir);
|
|
2895
|
+
return { notifications, summary: summarizeNotifications(notifications) };
|
|
2896
|
+
}
|
|
2897
|
+
case "notification-read":
|
|
2898
|
+
return {
|
|
2899
|
+
notification: await readNotificationRecord(
|
|
2900
|
+
await findTeamDir(teamName, cwd, env),
|
|
2901
|
+
String(input.notification_id),
|
|
2902
|
+
),
|
|
2903
|
+
};
|
|
2904
|
+
case "notification-replay":
|
|
2905
|
+
return replayGjcTeamNotifications(teamName, cwd, env);
|
|
2906
|
+
case "notification-mark-pane-attempt": {
|
|
2907
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
2908
|
+
const notification = await readNotificationRecord(dir, String(input.notification_id));
|
|
2909
|
+
return {
|
|
2910
|
+
notification: await writeNotificationRecord(dir, {
|
|
2911
|
+
...notification,
|
|
2912
|
+
delivery_state: parsePaneAttemptResult(String(input.result ?? "failed")),
|
|
2913
|
+
pane_attempt_result: parsePaneAttemptResult(String(input.result ?? "failed")),
|
|
2914
|
+
pane_attempt_reason: String(input.reason ?? "manual_api"),
|
|
2915
|
+
pane_attempt_at: now(),
|
|
2916
|
+
updated_at: now(),
|
|
2917
|
+
}),
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
case "worker-startup-ack":
|
|
2921
|
+
return writeGjcWorkerStartupAck(teamName, worker, cwd, env, input);
|
|
2266
2922
|
case "read-config":
|
|
2267
2923
|
return await readConfig(await findTeamDir(teamName, cwd, env));
|
|
2268
2924
|
case "read-manifest":
|