@gajae-code/coding-agent 0.2.5 → 0.3.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 +28 -0
- package/dist/types/async/job-manager.d.ts +91 -2
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/commands/harness.d.ts +37 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +10 -4
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/debug/crash-diagnostics.d.ts +45 -0
- package/dist/types/debug/runtime-gauges.d.ts +6 -0
- package/dist/types/deep-interview/render-middleware.d.ts +6 -0
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/eval/py/kernel.d.ts +2 -0
- package/dist/types/exec/bash-executor.d.ts +10 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
- package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
- package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
- package/dist/types/harness-control-plane/classifier.d.ts +13 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
- package/dist/types/harness-control-plane/finalize.d.ts +47 -0
- package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
- package/dist/types/harness-control-plane/operate.d.ts +35 -0
- package/dist/types/harness-control-plane/owner.d.ts +46 -0
- package/dist/types/harness-control-plane/preserve.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +88 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
- package/dist/types/harness-control-plane/seams.d.ts +21 -0
- package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
- package/dist/types/harness-control-plane/storage.d.ts +53 -0
- package/dist/types/harness-control-plane/types.d.ts +162 -0
- package/dist/types/hooks/skill-keywords.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +23 -29
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
- package/dist/types/internal-urls/types.d.ts +4 -0
- package/dist/types/lsp/index.d.ts +10 -10
- package/dist/types/modes/bridge/auth.d.ts +12 -0
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
- package/dist/types/modes/bridge/event-stream.d.ts +8 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
- package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/index.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/jobs-observer.d.ts +57 -0
- package/dist/types/modes/rpc/host-tools.d.ts +1 -16
- package/dist/types/modes/rpc/host-uris.d.ts +1 -38
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
- package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/skill-state/active-state.d.ts +2 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
- package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/id.d.ts +7 -0
- package/dist/types/task/index.d.ts +5 -0
- package/dist/types/task/receipt.d.ts +85 -0
- package/dist/types/task/spawn-gate.d.ts +38 -0
- package/dist/types/task/types.d.ts +198 -14
- package/dist/types/tools/cron.d.ts +6 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +26 -1
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +334 -6
- package/src/cli/args.ts +9 -2
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/config-cli.ts +10 -2
- package/src/cli.ts +2 -0
- package/src/commands/deep-interview.ts +1 -0
- package/src/commands/harness.ts +862 -0
- package/src/commands/launch.ts +2 -2
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +54 -39
- package/src/config/keybindings.ts +6 -0
- package/src/config/settings-schema.ts +13 -3
- package/src/config/settings.ts +5 -0
- package/src/dap/client.ts +17 -3
- package/src/debug/crash-diagnostics.ts +223 -0
- package/src/debug/runtime-gauges.ts +20 -0
- package/src/deep-interview/render-middleware.ts +372 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
- package/src/defaults/gjc/skills/team/SKILL.md +47 -21
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
- package/src/eval/py/executor.ts +21 -1
- package/src/eval/py/kernel.ts +15 -0
- package/src/exec/bash-executor.ts +41 -0
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/gjc-runtime/cli-write-receipt.ts +31 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
- package/src/gjc-runtime/goal-mode-request.ts +11 -3
- package/src/gjc-runtime/ralplan-runtime.ts +235 -43
- package/src/gjc-runtime/state-graph.ts +86 -0
- package/src/gjc-runtime/state-migrations.ts +179 -0
- package/src/gjc-runtime/state-renderer.ts +345 -0
- package/src/gjc-runtime/state-runtime.ts +1155 -46
- package/src/gjc-runtime/state-schema.ts +192 -0
- package/src/gjc-runtime/state-validation.ts +49 -0
- package/src/gjc-runtime/state-writer.ts +749 -0
- package/src/gjc-runtime/team-runtime.ts +1255 -189
- package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
- package/src/gjc-runtime/workflow-command-ref.ts +239 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
- package/src/gjc-runtime/workflow-manifest.ts +427 -0
- package/src/harness-control-plane/classifier.ts +128 -0
- package/src/harness-control-plane/control-endpoint.ts +148 -0
- package/src/harness-control-plane/finalize.ts +222 -0
- package/src/harness-control-plane/frame-mapper.ts +286 -0
- package/src/harness-control-plane/operate.ts +225 -0
- package/src/harness-control-plane/owner.ts +600 -0
- package/src/harness-control-plane/preserve.ts +102 -0
- package/src/harness-control-plane/receipts.ts +216 -0
- package/src/harness-control-plane/rpc-adapter.ts +276 -0
- package/src/harness-control-plane/seams.ts +39 -0
- package/src/harness-control-plane/session-lease.ts +388 -0
- package/src/harness-control-plane/state-machine.ts +98 -0
- package/src/harness-control-plane/storage.ts +257 -0
- package/src/harness-control-plane/types.ts +214 -0
- package/src/hooks/skill-keywords.ts +4 -2
- package/src/hooks/skill-state.ts +197 -64
- package/src/internal-urls/agent-protocol.ts +68 -21
- package/src/internal-urls/artifact-protocol.ts +12 -17
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/internal-urls/registry-helpers.ts +19 -16
- package/src/internal-urls/types.ts +4 -0
- package/src/lsp/client.ts +18 -2
- package/src/main.ts +21 -5
- package/src/modes/bridge/auth.ts +41 -0
- package/src/modes/bridge/bridge-client-bridge.ts +47 -0
- package/src/modes/bridge/bridge-mode.ts +520 -0
- package/src/modes/bridge/bridge-ui-context.ts +200 -0
- package/src/modes/bridge/event-stream.ts +70 -0
- package/src/modes/components/assistant-message.ts +5 -1
- package/src/modes/components/custom-editor.ts +101 -0
- package/src/modes/components/hook-selector.ts +133 -20
- package/src/modes/components/jobs-overlay-model.ts +109 -0
- package/src/modes/components/jobs-overlay.ts +172 -0
- package/src/modes/components/status-line/presets.ts +7 -5
- package/src/modes/components/status-line/segments.ts +25 -0
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/status-line.ts +9 -1
- package/src/modes/controllers/event-controller.ts +71 -6
- package/src/modes/controllers/extension-ui-controller.ts +43 -1
- package/src/modes/controllers/input-controller.ts +105 -9
- package/src/modes/controllers/selector-controller.ts +31 -1
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +28 -0
- package/src/modes/jobs-observer.ts +204 -0
- package/src/modes/rpc/host-tools.ts +1 -186
- package/src/modes/rpc/host-uris.ts +1 -235
- package/src/modes/rpc/rpc-client.ts +25 -10
- package/src/modes/rpc/rpc-mode.ts +12 -381
- package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
- package/src/modes/shared/agent-wire/command-validation.ts +131 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
- package/src/modes/shared/agent-wire/handshake.ts +117 -0
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
- package/src/modes/shared/agent-wire/protocol.ts +96 -0
- package/src/modes/shared/agent-wire/responses.ts +17 -0
- package/src/modes/shared/agent-wire/scopes.ts +89 -0
- package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
- package/src/modes/shared/agent-wire/ui-result.ts +48 -0
- package/src/modes/types.ts +2 -0
- package/src/prompts/agents/executor.md +13 -0
- package/src/prompts/tools/subagent.md +39 -4
- package/src/prompts/tools/task-summary.md +3 -9
- package/src/prompts/tools/task.md +5 -1
- package/src/sdk.ts +8 -0
- package/src/session/agent-session.ts +445 -71
- package/src/session/session-manager.ts +13 -1
- package/src/skill-state/active-state.ts +58 -65
- package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
- package/src/skill-state/initial-phase.ts +2 -0
- package/src/skill-state/workflow-state-contract.ts +33 -4
- package/src/skill-state/workflow-state-version.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/task/executor.ts +79 -13
- package/src/task/id.ts +33 -0
- package/src/task/index.ts +376 -74
- package/src/task/output-manager.ts +5 -4
- package/src/task/receipt.ts +297 -0
- package/src/task/render.ts +54 -134
- package/src/task/spawn-gate.ts +132 -0
- package/src/task/types.ts +104 -10
- package/src/tools/ask.ts +88 -27
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/cron.ts +48 -0
- package/src/tools/find.ts +4 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/search.ts +1 -0
- package/src/tools/skill.ts +6 -1
- package/src/tools/subagent.ts +423 -79
|
@@ -3,11 +3,34 @@ 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
|
+
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
7
|
+
|
|
6
8
|
import { applyGjcTmuxProfile } from "./launch-tmux";
|
|
9
|
+
import {
|
|
10
|
+
AlreadyExistsError,
|
|
11
|
+
appendJsonl as appendJsonlAudited,
|
|
12
|
+
appendText,
|
|
13
|
+
createJsonNoClobber,
|
|
14
|
+
deleteIfOwned,
|
|
15
|
+
removeFileAudited,
|
|
16
|
+
writeJsonAtomic,
|
|
17
|
+
writeReport,
|
|
18
|
+
writeWorkflowEnvelopeAtomic,
|
|
19
|
+
} from "./state-writer";
|
|
20
|
+
import { GJC_TMUX_PROFILE_OPTION, GJC_TMUX_PROFILE_VALUE } from "./tmux-common";
|
|
7
21
|
|
|
8
22
|
export type GjcTeamPhase = "starting" | "running" | "awaiting_integration" | "complete" | "failed" | "cancelled";
|
|
9
23
|
export type GjcTeamTaskStatus = "pending" | "blocked" | "in_progress" | "completed" | "failed";
|
|
10
24
|
export type GjcWorkerStatusState = "idle" | "working" | "blocked" | "done" | "failed" | "draining" | "unknown";
|
|
25
|
+
export type GjcTeamWorkerLifecycleState =
|
|
26
|
+
| "starting"
|
|
27
|
+
| "ready"
|
|
28
|
+
| "working"
|
|
29
|
+
| "draining"
|
|
30
|
+
| "stopped"
|
|
31
|
+
| "failed"
|
|
32
|
+
| "unknown";
|
|
33
|
+
export type GjcTeamShutdownMode = "graceful" | "force" | "abort";
|
|
11
34
|
|
|
12
35
|
export const GJC_TEAM_DEFAULT_WORKERS = 3;
|
|
13
36
|
export const GJC_TEAM_MAX_WORKERS = 20;
|
|
@@ -47,6 +70,27 @@ export interface GjcTeamTaskClaim {
|
|
|
47
70
|
token: string;
|
|
48
71
|
leased_until: string;
|
|
49
72
|
}
|
|
73
|
+
export type GjcTeamTaskCompletionEvidenceKind = "command" | "inspection" | "artifact";
|
|
74
|
+
export type GjcTeamTaskCompletionEvidenceStatus = "passed" | "failed" | "not_run" | "verified" | "rejected";
|
|
75
|
+
|
|
76
|
+
export interface GjcTeamTaskCompletionEvidenceItem {
|
|
77
|
+
kind: GjcTeamTaskCompletionEvidenceKind;
|
|
78
|
+
status: GjcTeamTaskCompletionEvidenceStatus;
|
|
79
|
+
summary: string;
|
|
80
|
+
command?: string;
|
|
81
|
+
artifact?: string;
|
|
82
|
+
location?: string;
|
|
83
|
+
output?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface GjcTeamTaskCompletionEvidence {
|
|
87
|
+
summary: string;
|
|
88
|
+
items: GjcTeamTaskCompletionEvidenceItem[];
|
|
89
|
+
files?: string[];
|
|
90
|
+
notes?: string;
|
|
91
|
+
recorded_by: string;
|
|
92
|
+
recorded_at: string;
|
|
93
|
+
}
|
|
50
94
|
|
|
51
95
|
export interface GjcTeamTask {
|
|
52
96
|
id: string;
|
|
@@ -58,9 +102,13 @@ export interface GjcTeamTask {
|
|
|
58
102
|
assignee?: string;
|
|
59
103
|
owner?: string;
|
|
60
104
|
result?: string;
|
|
105
|
+
completion_evidence?: GjcTeamTaskCompletionEvidence;
|
|
61
106
|
error?: string;
|
|
62
107
|
blocked_by?: string[];
|
|
63
108
|
depends_on?: string[];
|
|
109
|
+
lane?: string;
|
|
110
|
+
required_role?: string;
|
|
111
|
+
allowed_roles?: string[];
|
|
64
112
|
version: number;
|
|
65
113
|
claim?: GjcTeamTaskClaim;
|
|
66
114
|
created_at: string;
|
|
@@ -121,6 +169,22 @@ export interface GjcTeamMonitorSnapshot {
|
|
|
121
169
|
integration_by_worker: Record<string, GjcTeamWorkerIntegrationState>;
|
|
122
170
|
updated_at: string;
|
|
123
171
|
}
|
|
172
|
+
export interface GjcTeamWorkerLifecycle {
|
|
173
|
+
worker: string;
|
|
174
|
+
lifecycle_state: GjcTeamWorkerLifecycleState;
|
|
175
|
+
worker_status_state: GjcWorkerStatusState;
|
|
176
|
+
pane_id?: string;
|
|
177
|
+
pid?: number;
|
|
178
|
+
started_at?: string;
|
|
179
|
+
updated_at: string;
|
|
180
|
+
stopped_at?: string;
|
|
181
|
+
stop_reason?: string;
|
|
182
|
+
shutdown_request_id?: string;
|
|
183
|
+
shutdown_requested_at?: string;
|
|
184
|
+
shutdown_acknowledged_at?: string;
|
|
185
|
+
shutdown_ack_status?: string;
|
|
186
|
+
shutdown_mode?: GjcTeamShutdownMode;
|
|
187
|
+
}
|
|
124
188
|
|
|
125
189
|
export type GjcTeamNotificationDeliveryState =
|
|
126
190
|
| "pending"
|
|
@@ -167,9 +231,13 @@ export interface GjcTeamSnapshot {
|
|
|
167
231
|
task_counts: Record<GjcTeamTaskStatus, number>;
|
|
168
232
|
workers: GjcTeamWorker[];
|
|
169
233
|
integration_by_worker?: Record<string, GjcTeamWorkerIntegrationState>;
|
|
234
|
+
worker_lifecycle_by_id: Record<string, GjcTeamWorkerLifecycle>;
|
|
170
235
|
notification_summary: GjcTeamNotificationSummary;
|
|
171
236
|
updated_at: string;
|
|
172
237
|
}
|
|
238
|
+
export interface GjcTeamSnapshotOptions {
|
|
239
|
+
reconcileNotifications?: boolean;
|
|
240
|
+
}
|
|
173
241
|
|
|
174
242
|
export interface GjcTeamStartOptions {
|
|
175
243
|
workerCount: number;
|
|
@@ -189,6 +257,23 @@ export interface GjcTeamApiClaimResult {
|
|
|
189
257
|
claim_token?: string;
|
|
190
258
|
reason?: string;
|
|
191
259
|
}
|
|
260
|
+
export type GjcTeamLivenessRecoveryReason =
|
|
261
|
+
| "claim_expired"
|
|
262
|
+
| "stale_heartbeat"
|
|
263
|
+
| "missing_pane"
|
|
264
|
+
| "worker_lifecycle_failed"
|
|
265
|
+
| "worker_lifecycle_stopped";
|
|
266
|
+
|
|
267
|
+
export interface GjcTeamRecoveredClaim {
|
|
268
|
+
task_id: string;
|
|
269
|
+
worker: string;
|
|
270
|
+
reasons: GjcTeamLivenessRecoveryReason[];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface GjcTeamLivenessRecoveryResult {
|
|
274
|
+
recovered_claims: GjcTeamRecoveredClaim[];
|
|
275
|
+
stale_workers: Record<string, GjcTeamLivenessRecoveryReason[]>;
|
|
276
|
+
}
|
|
192
277
|
|
|
193
278
|
export interface GjcTeamMailboxMessage {
|
|
194
279
|
message_id: string;
|
|
@@ -201,6 +286,55 @@ export interface GjcTeamMailboxMessage {
|
|
|
201
286
|
idempotency_key?: string;
|
|
202
287
|
}
|
|
203
288
|
|
|
289
|
+
function taskReceiptFields(teamName: string, task: GjcTeamTask): Record<string, unknown> {
|
|
290
|
+
return {
|
|
291
|
+
team_name: teamName,
|
|
292
|
+
task_id: task.id,
|
|
293
|
+
status: task.status,
|
|
294
|
+
owner: task.owner,
|
|
295
|
+
worker_id: task.claim?.owner ?? task.owner ?? task.assignee,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function mailboxMessageReceiptFields(teamName: string, message: GjcTeamMailboxMessage): Record<string, unknown> {
|
|
300
|
+
return {
|
|
301
|
+
team_name: teamName,
|
|
302
|
+
message_id: message.message_id,
|
|
303
|
+
from_worker: message.from_worker,
|
|
304
|
+
to_worker: message.to_worker,
|
|
305
|
+
delivered: Boolean(message.delivered_at),
|
|
306
|
+
notified: Boolean(message.notified_at),
|
|
307
|
+
delivered_at: message.delivered_at,
|
|
308
|
+
notified_at: message.notified_at,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function notificationReceiptFields(notification: GjcTeamNotification): Record<string, unknown> {
|
|
313
|
+
return {
|
|
314
|
+
team_name: notification.team_name,
|
|
315
|
+
notification_id: notification.id,
|
|
316
|
+
recipient: notification.recipient,
|
|
317
|
+
source_type: notification.source.type,
|
|
318
|
+
source_id: notification.source.id,
|
|
319
|
+
delivery_state: notification.delivery_state,
|
|
320
|
+
pane_attempt_result: notification.pane_attempt_result,
|
|
321
|
+
pane_attempt_reason: notification.pane_attempt_reason,
|
|
322
|
+
replay_count: notification.replay_count,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function notificationSummaryReceipt(
|
|
327
|
+
teamName: string,
|
|
328
|
+
result: { notifications: GjcTeamNotification[]; summary: GjcTeamNotificationSummary },
|
|
329
|
+
): Record<string, unknown> {
|
|
330
|
+
return {
|
|
331
|
+
team_name: teamName,
|
|
332
|
+
notification_ids: result.notifications.map(notification => notification.id),
|
|
333
|
+
delivery_states: result.notifications.map(notification => notification.delivery_state),
|
|
334
|
+
summary: result.summary,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
204
338
|
interface FsError {
|
|
205
339
|
code?: string;
|
|
206
340
|
}
|
|
@@ -283,6 +417,19 @@ export interface GjcTeamEvent {
|
|
|
283
417
|
message?: string;
|
|
284
418
|
data?: Record<string, unknown>;
|
|
285
419
|
}
|
|
420
|
+
export interface GjcTeamTraceEvent {
|
|
421
|
+
schema_version: 1;
|
|
422
|
+
trace_id: string;
|
|
423
|
+
span_id: string;
|
|
424
|
+
source_event_id: string;
|
|
425
|
+
event_type: string;
|
|
426
|
+
ts: string;
|
|
427
|
+
worker?: string;
|
|
428
|
+
task_id?: string;
|
|
429
|
+
message?: string;
|
|
430
|
+
evidence_refs?: string[];
|
|
431
|
+
data?: Record<string, unknown>;
|
|
432
|
+
}
|
|
286
433
|
interface WorkerStatusFile {
|
|
287
434
|
state: GjcWorkerStatusState;
|
|
288
435
|
current_task_id?: string;
|
|
@@ -371,12 +518,15 @@ export const GJC_TEAM_API_OPERATIONS = [
|
|
|
371
518
|
"read-config",
|
|
372
519
|
"read-manifest",
|
|
373
520
|
"read-worker-status",
|
|
521
|
+
"update-worker-status",
|
|
374
522
|
"read-worker-heartbeat",
|
|
523
|
+
"recover-stale-claims",
|
|
375
524
|
"update-worker-heartbeat",
|
|
376
525
|
"write-worker-inbox",
|
|
377
526
|
"write-worker-identity",
|
|
378
527
|
"append-event",
|
|
379
528
|
"read-events",
|
|
529
|
+
"read-traces",
|
|
380
530
|
"await-event",
|
|
381
531
|
"write-shutdown-request",
|
|
382
532
|
"read-shutdown-ack",
|
|
@@ -392,9 +542,14 @@ function now(): string {
|
|
|
392
542
|
function isEnoent(error: unknown): error is FsError {
|
|
393
543
|
return typeof error === "object" && error !== null && "code" in error && (error as FsError).code === "ENOENT";
|
|
394
544
|
}
|
|
395
|
-
function
|
|
396
|
-
|
|
545
|
+
function stateWriterOptions(filePath: string, category: "state" | "ledger" | "report" | "prune", verb: string) {
|
|
546
|
+
const resolved = path.resolve(filePath);
|
|
547
|
+
const marker = `${path.sep}.gjc${path.sep}`;
|
|
548
|
+
const markerIndex = resolved.indexOf(marker);
|
|
549
|
+
const cwd = markerIndex >= 0 ? resolved.slice(0, markerIndex) : process.cwd();
|
|
550
|
+
return { cwd, audit: { category, verb, owner: "gjc-runtime" as const } };
|
|
397
551
|
}
|
|
552
|
+
|
|
398
553
|
function sanitizeName(value: string): string {
|
|
399
554
|
const sanitized = value
|
|
400
555
|
.toLowerCase()
|
|
@@ -432,9 +587,6 @@ function safePathSegment(kind: string, value: string): string {
|
|
|
432
587
|
function taskPath(dir: string, taskId: string): string {
|
|
433
588
|
return path.join(dir, "tasks", `${safePathSegment("task_id", taskId)}.json`);
|
|
434
589
|
}
|
|
435
|
-
function taskEvidencePath(dir: string, taskId: string): string {
|
|
436
|
-
return path.join(dir, "evidence", "tasks", `${safePathSegment("task_id", taskId)}.json`);
|
|
437
|
-
}
|
|
438
590
|
function mailboxPath(dir: string, worker: string): string {
|
|
439
591
|
return path.join(dir, "mailbox", `${safePathSegment("worker_id", worker)}.json`);
|
|
440
592
|
}
|
|
@@ -450,6 +602,17 @@ function notificationPath(dir: string, notificationId: string): string {
|
|
|
450
602
|
function workerDir(dir: string, worker: string): string {
|
|
451
603
|
return path.join(dir, "workers", safePathSegment("worker_id", worker));
|
|
452
604
|
}
|
|
605
|
+
function workerLifecyclePath(dir: string, worker: string): string {
|
|
606
|
+
return path.join(workerDir(dir, worker), "lifecycle.json");
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function tracePath(dir: string): string {
|
|
610
|
+
return path.join(dir, "trace.jsonl");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function traceErrorPath(dir: string): string {
|
|
614
|
+
return path.join(dir, "trace-errors.jsonl");
|
|
615
|
+
}
|
|
453
616
|
function isSafeId(value: string): boolean {
|
|
454
617
|
return (
|
|
455
618
|
/^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(value) &&
|
|
@@ -469,6 +632,12 @@ function assertKnownWorker(config: GjcTeamConfig, worker: string, allowLeader =
|
|
|
469
632
|
if (allowLeader && isLeaderRecipient(worker)) return;
|
|
470
633
|
if (!config.workers.some(candidate => candidate.id === worker)) throw new Error(`unknown_worker:${worker}`);
|
|
471
634
|
}
|
|
635
|
+
function findKnownWorker(config: GjcTeamConfig, worker: string): GjcTeamWorker {
|
|
636
|
+
assertKnownWorker(config, worker);
|
|
637
|
+
const found = config.workers.find(candidate => candidate.id === worker);
|
|
638
|
+
if (!found) throw new Error(`unknown_worker:${worker}`);
|
|
639
|
+
return found;
|
|
640
|
+
}
|
|
472
641
|
function assertKnownParticipant(config: GjcTeamConfig, worker: string): void {
|
|
473
642
|
assertKnownWorker(config, worker, true);
|
|
474
643
|
}
|
|
@@ -503,33 +672,171 @@ async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
|
503
672
|
throw error;
|
|
504
673
|
}
|
|
505
674
|
}
|
|
675
|
+
function stateCategoryForJsonPath(filePath: string): "state" | "ledger" {
|
|
676
|
+
return filePath.endsWith(".jsonl") || filePath.includes(`${path.sep}telemetry${path.sep}`) ? "ledger" : "state";
|
|
677
|
+
}
|
|
678
|
+
|
|
506
679
|
async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
|
|
507
|
-
await
|
|
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);
|
|
680
|
+
await writeJsonAtomic(filePath, value, stateWriterOptions(filePath, stateCategoryForJsonPath(filePath), "write"));
|
|
511
681
|
}
|
|
512
682
|
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
683
|
try {
|
|
516
|
-
|
|
517
|
-
|
|
684
|
+
await createJsonNoClobber(
|
|
685
|
+
filePath,
|
|
686
|
+
value,
|
|
687
|
+
stateWriterOptions(filePath, stateCategoryForJsonPath(filePath), "create"),
|
|
688
|
+
);
|
|
518
689
|
return true;
|
|
519
690
|
} catch (error) {
|
|
520
|
-
if (
|
|
691
|
+
if (error instanceof AlreadyExistsError) return false;
|
|
521
692
|
throw error;
|
|
522
|
-
} finally {
|
|
523
|
-
await handle?.close();
|
|
524
693
|
}
|
|
525
694
|
}
|
|
526
695
|
async function appendJsonl(filePath: string, value: unknown): Promise<void> {
|
|
527
|
-
await
|
|
528
|
-
|
|
696
|
+
await appendJsonlAudited(filePath, value, stateWriterOptions(filePath, "ledger", "append"));
|
|
697
|
+
}
|
|
698
|
+
function traceIdForTeam(dir: string): string {
|
|
699
|
+
return `trace-${stableHash(path.basename(dir))}`;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function evidenceRefsForEvent(event: GjcTeamEvent): string[] | undefined {
|
|
703
|
+
const refs: string[] = [];
|
|
704
|
+
if (event.task_id && event.type === "task_transitioned" && event.data && "completion_evidence" in event.data)
|
|
705
|
+
refs.push(`task:${event.task_id}:completion_evidence`);
|
|
706
|
+
if (event.task_id && event.type === "task_claim_recovered") refs.push(`task:${event.task_id}:claim_recovery`);
|
|
707
|
+
if (event.worker && event.type.startsWith("worker_")) refs.push(`worker:${event.worker}`);
|
|
708
|
+
return refs.length > 0 ? refs : undefined;
|
|
709
|
+
}
|
|
710
|
+
function pickString(value: unknown): string | undefined {
|
|
711
|
+
return typeof value === "string" ? value : undefined;
|
|
712
|
+
}
|
|
713
|
+
function pickNumber(value: unknown): number | undefined {
|
|
714
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
715
|
+
}
|
|
716
|
+
function pickBoolean(value: unknown): boolean | undefined {
|
|
717
|
+
return typeof value === "boolean" ? value : undefined;
|
|
718
|
+
}
|
|
719
|
+
function pickStringArray(value: unknown): string[] | undefined {
|
|
720
|
+
return Array.isArray(value) && value.every(item => typeof item === "string") ? value : undefined;
|
|
721
|
+
}
|
|
722
|
+
function setIfDefined(record: Record<string, unknown>, key: string, value: unknown): void {
|
|
723
|
+
if (value !== undefined) record[key] = value;
|
|
724
|
+
}
|
|
725
|
+
function messageBodyTraceProjection(body: string | undefined): Record<string, unknown> {
|
|
726
|
+
if (body === undefined) return {};
|
|
727
|
+
return {
|
|
728
|
+
body_byte_length: Buffer.byteLength(body, "utf8"),
|
|
729
|
+
body_sha256: createHash("sha256").update(body).digest("hex"),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
function traceDataForEvent(event: GjcTeamEvent): Record<string, unknown> | undefined {
|
|
733
|
+
const source = event.data ?? {};
|
|
734
|
+
const data: Record<string, unknown> = {};
|
|
735
|
+
switch (event.type) {
|
|
736
|
+
case "message_sent": {
|
|
737
|
+
setIfDefined(data, "to_worker", pickString(source.to_worker));
|
|
738
|
+
setIfDefined(data, "message_id", pickString(source.message_id));
|
|
739
|
+
Object.assign(data, messageBodyTraceProjection(pickString(event.message)));
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
case "message_acknowledged":
|
|
743
|
+
case "message_notified": {
|
|
744
|
+
setIfDefined(data, "message_id", pickString(event.message));
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
case "team_started": {
|
|
748
|
+
setIfDefined(data, "worker_count", pickNumber(source.worker_count));
|
|
749
|
+
setIfDefined(data, "agent_type", pickString(source.agent_type));
|
|
750
|
+
setIfDefined(data, "workspace_mode", pickString(source.workspace_mode));
|
|
751
|
+
setIfDefined(data, "dry_run", pickBoolean(source.dry_run));
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
case "task_claim_recovered": {
|
|
755
|
+
setIfDefined(data, "reasons", pickStringArray(source.reasons));
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
case "task_transitioned": {
|
|
759
|
+
setIfDefined(data, "status", pickString(source.status));
|
|
760
|
+
const evidence = source.completion_evidence;
|
|
761
|
+
if (typeof evidence === "object" && evidence !== null) {
|
|
762
|
+
const evidenceRecord = evidence as Record<string, unknown>;
|
|
763
|
+
data.completion_evidence = {
|
|
764
|
+
recorded_by: pickString(evidenceRecord.recorded_by),
|
|
765
|
+
item_count: pickNumber(evidenceRecord.item_count),
|
|
766
|
+
verified_item_count: pickNumber(evidenceRecord.verified_item_count),
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
case "worker_integration_attempt_requested": {
|
|
772
|
+
setIfDefined(data, "worker_name", pickString(source.worker_name));
|
|
773
|
+
setIfDefined(data, "worker_head", pickString(source.worker_head));
|
|
774
|
+
setIfDefined(data, "status", pickString(source.status));
|
|
775
|
+
if (Array.isArray(source.files)) data.file_count = source.files.length;
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
case "worker_lifecycle_nudge": {
|
|
779
|
+
setIfDefined(data, "condition", pickString(source.condition));
|
|
780
|
+
setIfDefined(data, "severity", pickString(source.severity));
|
|
781
|
+
setIfDefined(data, "fingerprint", pickString(source.fingerprint));
|
|
782
|
+
setIfDefined(data, "auto_action_taken", pickBoolean(source.auto_action_taken));
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
case "team_shutdown": {
|
|
786
|
+
setIfDefined(data, "phase", pickString(source.phase));
|
|
787
|
+
setIfDefined(data, "shutdown_request_id", pickString(source.shutdown_request_id));
|
|
788
|
+
setIfDefined(data, "graceful_shutdown_complete", pickBoolean(source.graceful_shutdown_complete));
|
|
789
|
+
if (Array.isArray(source.evidence_failures)) data.evidence_failure_count = source.evidence_failures.length;
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
case "worker_status_updated": {
|
|
793
|
+
setIfDefined(data, "status", pickString(source.status));
|
|
794
|
+
setIfDefined(data, "current_task_id", pickString(source.current_task_id));
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
case "worker_shutdown_requested": {
|
|
798
|
+
setIfDefined(data, "requested_by", pickString(source.requested_by));
|
|
799
|
+
setIfDefined(data, "request_id", pickString(source.request_id));
|
|
800
|
+
setIfDefined(data, "mode", pickString(source.mode));
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return Object.keys(data).length > 0 ? data : undefined;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async function appendTraceForEvent(dir: string, event: GjcTeamEvent): Promise<void> {
|
|
808
|
+
const evidenceRefs = evidenceRefsForEvent(event);
|
|
809
|
+
const traceData = traceDataForEvent(event);
|
|
810
|
+
const trace: GjcTeamTraceEvent = {
|
|
811
|
+
schema_version: 1,
|
|
812
|
+
trace_id: traceIdForTeam(dir),
|
|
813
|
+
span_id: `span-${stableHash(event.event_id)}`,
|
|
814
|
+
source_event_id: event.event_id,
|
|
815
|
+
event_type: event.type,
|
|
816
|
+
ts: event.ts,
|
|
817
|
+
...(event.worker ? { worker: event.worker } : {}),
|
|
818
|
+
...(event.task_id ? { task_id: event.task_id } : {}),
|
|
819
|
+
...(traceData ? { data: traceData } : {}),
|
|
820
|
+
...(evidenceRefs ? { evidence_refs: evidenceRefs } : {}),
|
|
821
|
+
};
|
|
822
|
+
try {
|
|
823
|
+
await appendJsonl(tracePath(dir), trace);
|
|
824
|
+
} catch (error) {
|
|
825
|
+
try {
|
|
826
|
+
await appendJsonl(traceErrorPath(dir), {
|
|
827
|
+
ts: now(),
|
|
828
|
+
source_event_id: event.event_id,
|
|
829
|
+
error: error instanceof Error ? error.message : String(error),
|
|
830
|
+
});
|
|
831
|
+
} catch {
|
|
832
|
+
// Trace append failure must not break legacy events.jsonl compatibility.
|
|
833
|
+
}
|
|
834
|
+
}
|
|
529
835
|
}
|
|
530
836
|
async function appendEvent(dir: string, event: Omit<GjcTeamEvent, "ts" | "event_id">): Promise<GjcTeamEvent> {
|
|
531
837
|
const full = { event_id: `evt-${Date.now()}-${Math.random().toString(16).slice(2)}`, ts: now(), ...event };
|
|
532
838
|
await appendJsonl(path.join(dir, "events.jsonl"), full);
|
|
839
|
+
await appendTraceForEvent(dir, full);
|
|
533
840
|
return full;
|
|
534
841
|
}
|
|
535
842
|
async function appendTelemetry(
|
|
@@ -562,6 +869,325 @@ async function readPhase(dir: string): Promise<GjcTeamPhase> {
|
|
|
562
869
|
async function writePhase(dir: string, phase: GjcTeamPhase): Promise<void> {
|
|
563
870
|
await writeJsonFile(path.join(dir, "phase.json"), { current_phase: phase, updated_at: now() });
|
|
564
871
|
}
|
|
872
|
+
function isGjcWorkerStatusState(value: string): value is GjcWorkerStatusState {
|
|
873
|
+
return ["idle", "working", "blocked", "done", "failed", "draining", "unknown"].includes(value);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function parseGjcWorkerStatusState(value: unknown): GjcWorkerStatusState {
|
|
877
|
+
return typeof value === "string" && isGjcWorkerStatusState(value) ? value : "unknown";
|
|
878
|
+
}
|
|
879
|
+
function parseRequiredGjcWorkerStatusState(value: unknown): GjcWorkerStatusState {
|
|
880
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
881
|
+
if (isGjcWorkerStatusState(raw)) return raw;
|
|
882
|
+
throw new Error(`invalid_worker_status:${raw}`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function lifecycleStateForWorkerStatus(status: GjcWorkerStatusState): GjcTeamWorkerLifecycleState {
|
|
886
|
+
switch (status) {
|
|
887
|
+
case "working":
|
|
888
|
+
return "working";
|
|
889
|
+
case "draining":
|
|
890
|
+
return "draining";
|
|
891
|
+
case "failed":
|
|
892
|
+
return "failed";
|
|
893
|
+
case "unknown":
|
|
894
|
+
return "unknown";
|
|
895
|
+
case "idle":
|
|
896
|
+
case "blocked":
|
|
897
|
+
case "done":
|
|
898
|
+
return "ready";
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function parseGjcTeamShutdownMode(value: unknown): GjcTeamShutdownMode {
|
|
903
|
+
const raw = typeof value === "string" ? value.trim() : "graceful";
|
|
904
|
+
if (raw === "graceful" || raw === "force" || raw === "abort") return raw;
|
|
905
|
+
throw new Error(`invalid_shutdown_mode:${raw}`);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function isGjcTeamWorkerLifecycleState(value: string): value is GjcTeamWorkerLifecycleState {
|
|
909
|
+
return ["starting", "ready", "working", "draining", "stopped", "failed", "unknown"].includes(value);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function parseGjcTeamWorkerLifecycleState(value: unknown): GjcTeamWorkerLifecycleState {
|
|
913
|
+
return typeof value === "string" && isGjcTeamWorkerLifecycleState(value) ? value : "unknown";
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
async function readWorkerStatusFile(dir: string, worker: string): Promise<WorkerStatusFile> {
|
|
917
|
+
return (
|
|
918
|
+
(await readJsonFile<WorkerStatusFile>(path.join(workerDir(dir, worker), "status.json"))) ?? {
|
|
919
|
+
state: "unknown",
|
|
920
|
+
updated_at: now(),
|
|
921
|
+
}
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async function readWorkerLifecycleRecord(dir: string, worker: GjcTeamWorker): Promise<GjcTeamWorkerLifecycle> {
|
|
926
|
+
const workerStatus = await readWorkerStatusFile(dir, worker.id);
|
|
927
|
+
const heartbeat = await readJsonFile<WorkerHeartbeatFile>(path.join(workerDir(dir, worker.id), "heartbeat.json"));
|
|
928
|
+
const rawLifecycle = await readJsonFile<Partial<GjcTeamWorkerLifecycle>>(workerLifecyclePath(dir, worker.id));
|
|
929
|
+
const shutdownAck = await readJsonFile<Record<string, unknown>>(
|
|
930
|
+
path.join(workerDir(dir, worker.id), "shutdown-ack.json"),
|
|
931
|
+
);
|
|
932
|
+
const lifecycle: GjcTeamWorkerLifecycle = {
|
|
933
|
+
worker: worker.id,
|
|
934
|
+
lifecycle_state: parseGjcTeamWorkerLifecycleState(rawLifecycle?.lifecycle_state),
|
|
935
|
+
worker_status_state: parseGjcWorkerStatusState(workerStatus.state),
|
|
936
|
+
pane_id: worker.pane_id ?? rawLifecycle?.pane_id,
|
|
937
|
+
updated_at: rawLifecycle?.updated_at ?? workerStatus.updated_at ?? now(),
|
|
938
|
+
};
|
|
939
|
+
if (typeof rawLifecycle?.pid === "number") lifecycle.pid = rawLifecycle.pid;
|
|
940
|
+
else if (typeof heartbeat?.pid === "number") lifecycle.pid = heartbeat.pid;
|
|
941
|
+
if (rawLifecycle?.started_at) lifecycle.started_at = rawLifecycle.started_at;
|
|
942
|
+
if (rawLifecycle?.stopped_at) lifecycle.stopped_at = rawLifecycle.stopped_at;
|
|
943
|
+
if (rawLifecycle?.stop_reason) lifecycle.stop_reason = rawLifecycle.stop_reason;
|
|
944
|
+
if (rawLifecycle?.shutdown_request_id) lifecycle.shutdown_request_id = rawLifecycle.shutdown_request_id;
|
|
945
|
+
if (rawLifecycle?.shutdown_requested_at) lifecycle.shutdown_requested_at = rawLifecycle.shutdown_requested_at;
|
|
946
|
+
if (
|
|
947
|
+
rawLifecycle?.shutdown_mode === "graceful" ||
|
|
948
|
+
rawLifecycle?.shutdown_mode === "force" ||
|
|
949
|
+
rawLifecycle?.shutdown_mode === "abort"
|
|
950
|
+
)
|
|
951
|
+
lifecycle.shutdown_mode = rawLifecycle.shutdown_mode;
|
|
952
|
+
if (typeof shutdownAck?.acknowledged_at === "string")
|
|
953
|
+
lifecycle.shutdown_acknowledged_at = shutdownAck.acknowledged_at;
|
|
954
|
+
if (typeof shutdownAck?.status === "string") lifecycle.shutdown_ack_status = shutdownAck.status;
|
|
955
|
+
return lifecycle;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
async function readWorkerLifecycleById(
|
|
959
|
+
dir: string,
|
|
960
|
+
config: GjcTeamConfig,
|
|
961
|
+
): Promise<Record<string, GjcTeamWorkerLifecycle>> {
|
|
962
|
+
const entries = await Promise.all(config.workers.map(worker => readWorkerLifecycleRecord(dir, worker)));
|
|
963
|
+
return Object.fromEntries(entries.map(entry => [entry.worker, entry]));
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
async function writeWorkerLifecycleRecord(
|
|
967
|
+
dir: string,
|
|
968
|
+
worker: GjcTeamWorker,
|
|
969
|
+
lifecycleState: GjcTeamWorkerLifecycleState,
|
|
970
|
+
updates: Partial<GjcTeamWorkerLifecycle> = {},
|
|
971
|
+
): Promise<GjcTeamWorkerLifecycle> {
|
|
972
|
+
const current = await readWorkerLifecycleRecord(dir, worker);
|
|
973
|
+
const next: GjcTeamWorkerLifecycle = {
|
|
974
|
+
...current,
|
|
975
|
+
...updates,
|
|
976
|
+
worker: worker.id,
|
|
977
|
+
lifecycle_state: lifecycleState,
|
|
978
|
+
worker_status_state: current.worker_status_state,
|
|
979
|
+
pane_id: updates.pane_id ?? worker.pane_id ?? current.pane_id,
|
|
980
|
+
updated_at: now(),
|
|
981
|
+
};
|
|
982
|
+
await writeJsonFile(workerLifecyclePath(dir, worker.id), next);
|
|
983
|
+
return next;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function writeWorkerLifecycleForConfig(
|
|
987
|
+
dir: string,
|
|
988
|
+
config: GjcTeamConfig,
|
|
989
|
+
lifecycleState: GjcTeamWorkerLifecycleState,
|
|
990
|
+
updatesFor: (worker: GjcTeamWorker) => Partial<GjcTeamWorkerLifecycle> = () => ({}),
|
|
991
|
+
): Promise<Record<string, GjcTeamWorkerLifecycle>> {
|
|
992
|
+
const entries = await Promise.all(
|
|
993
|
+
config.workers.map(worker => writeWorkerLifecycleRecord(dir, worker, lifecycleState, updatesFor(worker))),
|
|
994
|
+
);
|
|
995
|
+
return Object.fromEntries(entries.map(entry => [entry.worker, entry]));
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function teamModeStatePath(): string {
|
|
999
|
+
return path.join(".gjc", "state", "team-state.json");
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
export async function persistGjcTeamModeStateSummary(snapshot: GjcTeamSnapshot, cwd = process.cwd()): Promise<void> {
|
|
1003
|
+
const active = snapshot.phase !== "complete" && snapshot.phase !== "cancelled";
|
|
1004
|
+
const updatedAt = now();
|
|
1005
|
+
await writeWorkflowEnvelopeAtomic(
|
|
1006
|
+
teamModeStatePath(),
|
|
1007
|
+
{
|
|
1008
|
+
skill: "team",
|
|
1009
|
+
version: WORKFLOW_STATE_VERSION,
|
|
1010
|
+
active,
|
|
1011
|
+
current_phase: snapshot.phase,
|
|
1012
|
+
team_name: snapshot.team_name,
|
|
1013
|
+
task_counts: snapshot.task_counts,
|
|
1014
|
+
updated_at: updatedAt,
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
cwd,
|
|
1018
|
+
receipt: {
|
|
1019
|
+
cwd,
|
|
1020
|
+
skill: "team",
|
|
1021
|
+
owner: "gjc-runtime",
|
|
1022
|
+
command: "gjc team sync-team-summary",
|
|
1023
|
+
nowIso: updatedAt,
|
|
1024
|
+
},
|
|
1025
|
+
audit: { category: "state", verb: "sync-team-summary", owner: "gjc-runtime", skill: "team" },
|
|
1026
|
+
},
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function appendLivenessRecoveryReason(
|
|
1031
|
+
reasons: GjcTeamLivenessRecoveryReason[],
|
|
1032
|
+
reason: GjcTeamLivenessRecoveryReason,
|
|
1033
|
+
): void {
|
|
1034
|
+
if (!reasons.includes(reason)) reasons.push(reason);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function isPastTimestamp(value: string | undefined): boolean {
|
|
1038
|
+
if (!value) return false;
|
|
1039
|
+
const timestamp = Date.parse(value);
|
|
1040
|
+
return Number.isFinite(timestamp) && timestamp <= Date.now();
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function readClaimRecord(value: unknown): GjcTeamTaskClaim | undefined {
|
|
1044
|
+
if (!isRecord(value)) return undefined;
|
|
1045
|
+
const owner = typeof value.owner === "string" ? value.owner : "";
|
|
1046
|
+
const token = typeof value.token === "string" ? value.token : "";
|
|
1047
|
+
const leasedUntil = typeof value.leased_until === "string" ? value.leased_until : "";
|
|
1048
|
+
if (!owner || !token || !leasedUntil) return undefined;
|
|
1049
|
+
return { owner, token, leased_until: leasedUntil };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function isWorkerHeartbeatStale(
|
|
1053
|
+
worker: GjcTeamWorker,
|
|
1054
|
+
heartbeat: WorkerHeartbeatFile | null,
|
|
1055
|
+
env: NodeJS.ProcessEnv,
|
|
1056
|
+
): boolean {
|
|
1057
|
+
const thresholdMs = parseDurationEnv(env, "GJC_TEAM_HEARTBEAT_STALE_MS", 120_000);
|
|
1058
|
+
if (thresholdMs <= 0) return false;
|
|
1059
|
+
const heartbeatAt = Date.parse(heartbeat?.last_turn_at ?? worker.last_heartbeat);
|
|
1060
|
+
return Number.isFinite(heartbeatAt) && Date.now() - heartbeatAt >= thresholdMs;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
async function detectGjcTeamWorkerLivenessReasons(
|
|
1064
|
+
dir: string,
|
|
1065
|
+
config: GjcTeamConfig,
|
|
1066
|
+
worker: GjcTeamWorker,
|
|
1067
|
+
env: NodeJS.ProcessEnv,
|
|
1068
|
+
): Promise<GjcTeamLivenessRecoveryReason[]> {
|
|
1069
|
+
const reasons: GjcTeamLivenessRecoveryReason[] = [];
|
|
1070
|
+
const lifecycle = await readWorkerLifecycleRecord(dir, worker);
|
|
1071
|
+
const heartbeat = await readJsonFile<WorkerHeartbeatFile>(path.join(workerDir(dir, worker.id), "heartbeat.json"));
|
|
1072
|
+
if (lifecycle.lifecycle_state === "failed") appendLivenessRecoveryReason(reasons, "worker_lifecycle_failed");
|
|
1073
|
+
if (lifecycle.lifecycle_state === "stopped") appendLivenessRecoveryReason(reasons, "worker_lifecycle_stopped");
|
|
1074
|
+
if (isWorkerHeartbeatStale(worker, heartbeat, env)) appendLivenessRecoveryReason(reasons, "stale_heartbeat");
|
|
1075
|
+
if (!config.dry_run && (!worker.pane_id?.startsWith("%") || !paneBelongsToTeamTarget(config, worker.pane_id)))
|
|
1076
|
+
appendLivenessRecoveryReason(reasons, "missing_pane");
|
|
1077
|
+
return reasons;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function reconcileGjcTeamStaleClaims(
|
|
1081
|
+
teamName: string,
|
|
1082
|
+
dir: string,
|
|
1083
|
+
config: GjcTeamConfig,
|
|
1084
|
+
env: NodeJS.ProcessEnv,
|
|
1085
|
+
): Promise<GjcTeamLivenessRecoveryResult> {
|
|
1086
|
+
const staleWorkers: Record<string, GjcTeamLivenessRecoveryReason[]> = {};
|
|
1087
|
+
for (const worker of config.workers) {
|
|
1088
|
+
const reasons = await detectGjcTeamWorkerLivenessReasons(dir, config, worker, env);
|
|
1089
|
+
if (reasons.length === 0) continue;
|
|
1090
|
+
staleWorkers[worker.id] = reasons;
|
|
1091
|
+
if (reasons.includes("missing_pane") && reasons.includes("worker_lifecycle_stopped") === false) {
|
|
1092
|
+
await writeWorkerLifecycleRecord(dir, worker, "failed", { stop_reason: "pane_missing" });
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const recoveredClaims: GjcTeamRecoveredClaim[] = [];
|
|
1097
|
+
for (const task of await readTasks(dir)) {
|
|
1098
|
+
if (task.status === "completed" || task.status === "failed") continue;
|
|
1099
|
+
const claimPath = path.join(dir, "claims", `${task.id}.json`);
|
|
1100
|
+
const diskClaim = readClaimRecord(await readJsonFile<unknown>(claimPath));
|
|
1101
|
+
const claim = task.claim ?? diskClaim;
|
|
1102
|
+
if (!claim) continue;
|
|
1103
|
+
|
|
1104
|
+
const reasons = [...(staleWorkers[claim.owner] ?? [])];
|
|
1105
|
+
if (isPastTimestamp(claim.leased_until)) appendLivenessRecoveryReason(reasons, "claim_expired");
|
|
1106
|
+
if (reasons.length === 0) continue;
|
|
1107
|
+
|
|
1108
|
+
await fs.rm(claimPath, { force: true });
|
|
1109
|
+
recoveredClaims.push({ task_id: task.id, worker: claim.owner, reasons });
|
|
1110
|
+
if (task.status !== "in_progress") {
|
|
1111
|
+
await appendEvent(dir, {
|
|
1112
|
+
type: "task_claim_recovered",
|
|
1113
|
+
task_id: task.id,
|
|
1114
|
+
worker: claim.owner,
|
|
1115
|
+
message: "Removed stale task claim file",
|
|
1116
|
+
data: { reasons },
|
|
1117
|
+
});
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const recoveredTask = normalizeTask({
|
|
1122
|
+
...task,
|
|
1123
|
+
status: "pending",
|
|
1124
|
+
assignee: undefined,
|
|
1125
|
+
claim: undefined,
|
|
1126
|
+
version: task.version + 1,
|
|
1127
|
+
updated_at: now(),
|
|
1128
|
+
});
|
|
1129
|
+
await writeTask(dir, recoveredTask);
|
|
1130
|
+
await appendEvent(dir, {
|
|
1131
|
+
type: "task_claim_recovered",
|
|
1132
|
+
task_id: task.id,
|
|
1133
|
+
worker: claim.owner,
|
|
1134
|
+
message: "Recovered task from stale worker claim",
|
|
1135
|
+
data: { reasons },
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (recoveredClaims.length > 0)
|
|
1140
|
+
await appendTelemetry(dir, {
|
|
1141
|
+
type: "team_liveness_recovery",
|
|
1142
|
+
message: `Recovered ${recoveredClaims.length} stale team task claim(s)`,
|
|
1143
|
+
data: { team_name: teamName, recovered_claims: recoveredClaims },
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
return { recovered_claims: recoveredClaims, stale_workers: staleWorkers };
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
export async function recoverGjcTeamStaleClaims(
|
|
1150
|
+
teamName: string,
|
|
1151
|
+
cwd = process.cwd(),
|
|
1152
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
1153
|
+
): Promise<GjcTeamLivenessRecoveryResult> {
|
|
1154
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
1155
|
+
const config = await readConfig(dir);
|
|
1156
|
+
return reconcileGjcTeamStaleClaims(teamName, dir, config, env);
|
|
1157
|
+
}
|
|
1158
|
+
function normalizeOptionalTaskString(value: unknown): string | undefined {
|
|
1159
|
+
if (typeof value !== "string") return undefined;
|
|
1160
|
+
const trimmed = value.trim();
|
|
1161
|
+
return trimmed || undefined;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function normalizeOptionalTaskStringArray(value: unknown): string[] | undefined {
|
|
1165
|
+
if (!Array.isArray(value)) return undefined;
|
|
1166
|
+
const items = Array.from(
|
|
1167
|
+
new Set(value.map(item => (typeof item === "string" ? item.trim() : "")).filter(item => item.length > 0)),
|
|
1168
|
+
).sort();
|
|
1169
|
+
return items.length > 0 ? items : undefined;
|
|
1170
|
+
}
|
|
1171
|
+
type GjcTeamTaskMetadataInput = Partial<
|
|
1172
|
+
Pick<GjcTeamTask, "owner" | "lane" | "required_role" | "allowed_roles" | "depends_on" | "blocked_by">
|
|
1173
|
+
>;
|
|
1174
|
+
|
|
1175
|
+
function taskMetadataFromInput(input: Record<string, unknown>, includeOwner = false): GjcTeamTaskMetadataInput {
|
|
1176
|
+
const metadata: GjcTeamTaskMetadataInput = {};
|
|
1177
|
+
const owner = normalizeOptionalTaskString(input.owner);
|
|
1178
|
+
const lane = normalizeOptionalTaskString(input.lane);
|
|
1179
|
+
const requiredRole = normalizeOptionalTaskString(input.required_role ?? input.requiredRole);
|
|
1180
|
+
const allowedRoles = normalizeOptionalTaskStringArray(input.allowed_roles ?? input.allowedRoles);
|
|
1181
|
+
const dependsOn = normalizeOptionalTaskStringArray(input.depends_on ?? input.dependsOn);
|
|
1182
|
+
const blockedBy = normalizeOptionalTaskStringArray(input.blocked_by ?? input.blockedBy);
|
|
1183
|
+
if (includeOwner && owner) metadata.owner = owner;
|
|
1184
|
+
if (lane) metadata.lane = lane;
|
|
1185
|
+
if (requiredRole) metadata.required_role = requiredRole;
|
|
1186
|
+
if (allowedRoles) metadata.allowed_roles = allowedRoles;
|
|
1187
|
+
if (dependsOn) metadata.depends_on = dependsOn;
|
|
1188
|
+
if (blockedBy) metadata.blocked_by = blockedBy;
|
|
1189
|
+
return metadata;
|
|
1190
|
+
}
|
|
565
1191
|
|
|
566
1192
|
function normalizeTask(raw: GjcTeamTask): GjcTeamTask {
|
|
567
1193
|
const status = raw.status === ("complete" as GjcTeamTaskStatus) ? "completed" : raw.status;
|
|
@@ -573,6 +1199,9 @@ function normalizeTask(raw: GjcTeamTask): GjcTeamTask {
|
|
|
573
1199
|
title: raw.title ?? raw.subject,
|
|
574
1200
|
objective: raw.objective ?? raw.description,
|
|
575
1201
|
version: raw.version ?? 1,
|
|
1202
|
+
lane: normalizeOptionalTaskString(raw.lane),
|
|
1203
|
+
required_role: normalizeOptionalTaskString(raw.required_role),
|
|
1204
|
+
allowed_roles: normalizeOptionalTaskStringArray(raw.allowed_roles),
|
|
576
1205
|
};
|
|
577
1206
|
}
|
|
578
1207
|
|
|
@@ -616,13 +1245,189 @@ async function resolveGjcTeamSnapshotPhase(
|
|
|
616
1245
|
monitor: GjcTeamMonitorSnapshot | null,
|
|
617
1246
|
): Promise<GjcTeamPhase> {
|
|
618
1247
|
if (storedPhase !== "running") return storedPhase;
|
|
619
|
-
if (tasks.length === 0 || !tasks.every(
|
|
1248
|
+
if (tasks.length === 0 || !tasks.every(isGjcTeamTaskCompletionVerified)) return storedPhase;
|
|
620
1249
|
return (await hasPendingGjcTeamIntegration(dir, config, monitor)) ? "awaiting_integration" : storedPhase;
|
|
621
1250
|
}
|
|
622
1251
|
|
|
623
1252
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
624
1253
|
return typeof value === "object" && value != null;
|
|
625
1254
|
}
|
|
1255
|
+
const GJC_TEAM_COMPLETION_EVIDENCE_SUMMARY_MAX = 4_000;
|
|
1256
|
+
const GJC_TEAM_COMPLETION_EVIDENCE_OUTPUT_MAX = 8_000;
|
|
1257
|
+
const GJC_TEAM_COMMAND_EVIDENCE_STATUSES = new Set<GjcTeamTaskCompletionEvidenceStatus>([
|
|
1258
|
+
"passed",
|
|
1259
|
+
"failed",
|
|
1260
|
+
"not_run",
|
|
1261
|
+
]);
|
|
1262
|
+
const GJC_TEAM_VERIFICATION_EVIDENCE_STATUSES = new Set<GjcTeamTaskCompletionEvidenceStatus>(["verified", "rejected"]);
|
|
1263
|
+
|
|
1264
|
+
function completionEvidenceError(taskId: string, field: string): Error {
|
|
1265
|
+
return new Error(`invalid_completion_evidence:${taskId}:${field}`);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function trimRequiredCompletionEvidenceString(
|
|
1269
|
+
taskId: string,
|
|
1270
|
+
field: string,
|
|
1271
|
+
value: unknown,
|
|
1272
|
+
maxLength = GJC_TEAM_COMPLETION_EVIDENCE_SUMMARY_MAX,
|
|
1273
|
+
): string {
|
|
1274
|
+
if (typeof value !== "string") throw completionEvidenceError(taskId, field);
|
|
1275
|
+
const trimmed = value.trim();
|
|
1276
|
+
if (!trimmed || trimmed.length > maxLength) throw completionEvidenceError(taskId, field);
|
|
1277
|
+
return trimmed;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function trimOptionalCompletionEvidenceString(
|
|
1281
|
+
taskId: string,
|
|
1282
|
+
field: string,
|
|
1283
|
+
value: unknown,
|
|
1284
|
+
maxLength = GJC_TEAM_COMPLETION_EVIDENCE_OUTPUT_MAX,
|
|
1285
|
+
): string | undefined {
|
|
1286
|
+
if (value == null) return undefined;
|
|
1287
|
+
if (typeof value !== "string") throw completionEvidenceError(taskId, field);
|
|
1288
|
+
const trimmed = value.trim();
|
|
1289
|
+
if (!trimmed) return undefined;
|
|
1290
|
+
if (trimmed.length > maxLength) throw completionEvidenceError(taskId, field);
|
|
1291
|
+
return trimmed;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function normalizeGjcTeamCompletionEvidenceStatus(
|
|
1295
|
+
taskId: string,
|
|
1296
|
+
kind: GjcTeamTaskCompletionEvidenceKind,
|
|
1297
|
+
value: unknown,
|
|
1298
|
+
): GjcTeamTaskCompletionEvidenceStatus {
|
|
1299
|
+
const status = trimRequiredCompletionEvidenceString(taskId, "items.status", value);
|
|
1300
|
+
const allowed = kind === "command" ? GJC_TEAM_COMMAND_EVIDENCE_STATUSES : GJC_TEAM_VERIFICATION_EVIDENCE_STATUSES;
|
|
1301
|
+
if (!allowed.has(status as GjcTeamTaskCompletionEvidenceStatus))
|
|
1302
|
+
throw completionEvidenceError(taskId, "items.status");
|
|
1303
|
+
return status as GjcTeamTaskCompletionEvidenceStatus;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function normalizeGjcTeamCompletionEvidenceItem(taskId: string, value: unknown): GjcTeamTaskCompletionEvidenceItem {
|
|
1307
|
+
if (!isRecord(value) || Array.isArray(value)) throw completionEvidenceError(taskId, "items");
|
|
1308
|
+
const kind = trimRequiredCompletionEvidenceString(taskId, "items.kind", value.kind);
|
|
1309
|
+
if (kind !== "command" && kind !== "inspection" && kind !== "artifact")
|
|
1310
|
+
throw completionEvidenceError(taskId, "items.kind");
|
|
1311
|
+
const status = normalizeGjcTeamCompletionEvidenceStatus(taskId, kind, value.status);
|
|
1312
|
+
const item: GjcTeamTaskCompletionEvidenceItem = {
|
|
1313
|
+
kind,
|
|
1314
|
+
status,
|
|
1315
|
+
summary: trimRequiredCompletionEvidenceString(taskId, "items.summary", value.summary),
|
|
1316
|
+
};
|
|
1317
|
+
const command = trimOptionalCompletionEvidenceString(taskId, "items.command", value.command);
|
|
1318
|
+
const artifact = trimOptionalCompletionEvidenceString(taskId, "items.artifact", value.artifact);
|
|
1319
|
+
const location = trimOptionalCompletionEvidenceString(taskId, "items.location", value.location);
|
|
1320
|
+
const output = trimOptionalCompletionEvidenceString(taskId, "items.output", value.output);
|
|
1321
|
+
if (kind === "command" && !command) throw completionEvidenceError(taskId, "items.command");
|
|
1322
|
+
if (command) item.command = command;
|
|
1323
|
+
if (artifact) item.artifact = artifact;
|
|
1324
|
+
if (location) item.location = location;
|
|
1325
|
+
if (output) item.output = output;
|
|
1326
|
+
return item;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function normalizeGjcTeamCompletionEvidenceFiles(taskId: string, value: unknown): string[] | undefined {
|
|
1330
|
+
if (value == null) return undefined;
|
|
1331
|
+
if (!Array.isArray(value)) throw completionEvidenceError(taskId, "files");
|
|
1332
|
+
const files = new Set<string>();
|
|
1333
|
+
for (const entry of value) {
|
|
1334
|
+
if (typeof entry !== "string") throw completionEvidenceError(taskId, "files");
|
|
1335
|
+
const filePath = entry.trim().replace(/\\/g, "/");
|
|
1336
|
+
if (!filePath || filePath.includes("\0") || path.isAbsolute(filePath) || filePath.split("/").includes("..")) {
|
|
1337
|
+
throw completionEvidenceError(taskId, "files");
|
|
1338
|
+
}
|
|
1339
|
+
files.add(filePath);
|
|
1340
|
+
}
|
|
1341
|
+
return files.size > 0 ? [...files].sort() : undefined;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function isGjcTeamCompletionEvidenceItemVerified(item: GjcTeamTaskCompletionEvidenceItem): boolean {
|
|
1345
|
+
return (
|
|
1346
|
+
(item.kind === "command" && item.status === "passed") ||
|
|
1347
|
+
((item.kind === "inspection" || item.kind === "artifact") && item.status === "verified")
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function normalizeGjcTeamTaskCompletionEvidence(
|
|
1352
|
+
taskId: string,
|
|
1353
|
+
owner: string,
|
|
1354
|
+
input: unknown,
|
|
1355
|
+
recordedAt = now(),
|
|
1356
|
+
): GjcTeamTaskCompletionEvidence {
|
|
1357
|
+
if (!isRecord(input) || Array.isArray(input)) throw new Error(`completion_evidence_required:${taskId}`);
|
|
1358
|
+
const itemsValue = input.items;
|
|
1359
|
+
if (!Array.isArray(itemsValue) || itemsValue.length === 0) throw completionEvidenceError(taskId, "items");
|
|
1360
|
+
const items = itemsValue.map(item => normalizeGjcTeamCompletionEvidenceItem(taskId, item));
|
|
1361
|
+
if (!items.some(isGjcTeamCompletionEvidenceItemVerified))
|
|
1362
|
+
throw new Error(`completion_evidence_no_verified_item:${taskId}`);
|
|
1363
|
+
const evidence: GjcTeamTaskCompletionEvidence = {
|
|
1364
|
+
summary: trimRequiredCompletionEvidenceString(taskId, "summary", input.summary),
|
|
1365
|
+
items,
|
|
1366
|
+
recorded_by: owner,
|
|
1367
|
+
recorded_at: recordedAt,
|
|
1368
|
+
};
|
|
1369
|
+
const files = normalizeGjcTeamCompletionEvidenceFiles(taskId, input.files);
|
|
1370
|
+
const notes = trimOptionalCompletionEvidenceString(taskId, "notes", input.notes);
|
|
1371
|
+
if (files) evidence.files = files;
|
|
1372
|
+
if (notes) evidence.notes = notes;
|
|
1373
|
+
return evidence;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function getGjcTeamTaskCompletionEvidenceFailure(task: GjcTeamTask): string | null {
|
|
1377
|
+
if (task.status !== "completed") return `task_not_completed:${task.id}`;
|
|
1378
|
+
const evidence = task.completion_evidence;
|
|
1379
|
+
if (!isRecord(evidence) || Array.isArray(evidence)) return `completion_evidence_required:${task.id}`;
|
|
1380
|
+
if (typeof evidence.recorded_by !== "string" || evidence.recorded_by.trim().length === 0)
|
|
1381
|
+
return `invalid_completion_evidence:${task.id}:recorded_by`;
|
|
1382
|
+
if (typeof evidence.recorded_at !== "string" || evidence.recorded_at.trim().length === 0)
|
|
1383
|
+
return `invalid_completion_evidence:${task.id}:recorded_at`;
|
|
1384
|
+
try {
|
|
1385
|
+
normalizeGjcTeamTaskCompletionEvidence(task.id, evidence.recorded_by.trim(), evidence, evidence.recorded_at);
|
|
1386
|
+
return null;
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
return error instanceof Error ? error.message : `invalid_completion_evidence:${task.id}:unknown`;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function isGjcTeamTaskCompletionVerified(task: GjcTeamTask): boolean {
|
|
1393
|
+
return getGjcTeamTaskCompletionEvidenceFailure(task) == null;
|
|
1394
|
+
}
|
|
1395
|
+
function roleValuesForWorker(worker: GjcTeamWorker): Set<string> {
|
|
1396
|
+
return new Set([worker.role, worker.agent_type].map(value => value.trim()).filter(value => value.length > 0));
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function getGjcTeamTaskClaimEligibilityReason(
|
|
1400
|
+
task: GjcTeamTask,
|
|
1401
|
+
worker: GjcTeamWorker,
|
|
1402
|
+
tasks: GjcTeamTask[],
|
|
1403
|
+
): string | null {
|
|
1404
|
+
if (task.status !== "pending") return `task_not_pending:${task.id}`;
|
|
1405
|
+
if (task.owner && task.owner !== worker.id) return `task_owner_mismatch:${task.id}:${task.owner}`;
|
|
1406
|
+
if (task.assignee && task.assignee !== worker.id) return `task_assignee_mismatch:${task.id}:${task.assignee}`;
|
|
1407
|
+
|
|
1408
|
+
const workerRoles = roleValuesForWorker(worker);
|
|
1409
|
+
if (task.required_role && !workerRoles.has(task.required_role))
|
|
1410
|
+
return `task_role_mismatch:${task.id}:${task.required_role}`;
|
|
1411
|
+
if (task.allowed_roles?.length && !task.allowed_roles.some(role => workerRoles.has(role)))
|
|
1412
|
+
return `task_role_mismatch:${task.id}:${task.allowed_roles.join(",")}`;
|
|
1413
|
+
|
|
1414
|
+
if (task.blocked_by?.length) return `task_blocked:${task.id}:${task.blocked_by.join(",")}`;
|
|
1415
|
+
for (const dependencyId of task.depends_on ?? []) {
|
|
1416
|
+
const dependency = tasks.find(candidate => candidate.id === dependencyId);
|
|
1417
|
+
if (!dependency || !isGjcTeamTaskCompletionVerified(dependency))
|
|
1418
|
+
return `task_dependency_incomplete:${task.id}:${dependencyId}`;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
async function getActiveClaimReason(dir: string, task: GjcTeamTask): Promise<string | null> {
|
|
1425
|
+
const claimPath = path.join(dir, "claims", `${task.id}.json`);
|
|
1426
|
+
const diskClaim = readClaimRecord(await readJsonFile<unknown>(claimPath));
|
|
1427
|
+
const claim = task.claim ?? diskClaim;
|
|
1428
|
+
if (!claim || isPastTimestamp(claim.leased_until)) return null;
|
|
1429
|
+
return `task_already_claimed:${task.id}`;
|
|
1430
|
+
}
|
|
626
1431
|
function isGjcTeamTaskRecord(value: unknown): value is GjcTeamTask {
|
|
627
1432
|
return (
|
|
628
1433
|
isRecord(value) &&
|
|
@@ -820,17 +1625,35 @@ async function ensureWorkerWorktree(
|
|
|
820
1625
|
export function resolveGjcTmuxCommand(env: NodeJS.ProcessEnv = process.env): string {
|
|
821
1626
|
return env.GJC_TEAM_TMUX_COMMAND?.trim() || "tmux";
|
|
822
1627
|
}
|
|
1628
|
+
function buildTeamTmuxLeaderRequirementMessage(detail?: string): string {
|
|
1629
|
+
const suffix = detail?.trim() ? `:${detail.trim()}` : "";
|
|
1630
|
+
return `gjc_team_requires_tmux_leader: run \`gjc --tmux\` first, then run \`gjc team ...\` inside that tmux-backed leader session, or use \`gjc team --dry-run\` for state-only smoke tests${suffix}`;
|
|
1631
|
+
}
|
|
1632
|
+
function readGjcTmuxProfileValue(tmuxCommand: string, sessionName: string): string {
|
|
1633
|
+
const result = Bun.spawnSync(
|
|
1634
|
+
[tmuxCommand, "show-options", "-qv", "-t", `=${sessionName}`, GJC_TMUX_PROFILE_OPTION],
|
|
1635
|
+
{
|
|
1636
|
+
stdout: "pipe",
|
|
1637
|
+
stderr: "pipe",
|
|
1638
|
+
},
|
|
1639
|
+
);
|
|
1640
|
+
if (result.exitCode !== 0) return "";
|
|
1641
|
+
return result.stdout.toString().trim();
|
|
1642
|
+
}
|
|
1643
|
+
|
|
823
1644
|
function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEnv): GjcTmuxLeaderContext {
|
|
824
1645
|
const paneTarget = env.TMUX_PANE?.trim();
|
|
825
1646
|
const args = paneTarget
|
|
826
1647
|
? ["display-message", "-p", "-t", paneTarget, "#S:#I #{pane_id}"]
|
|
827
1648
|
: ["display-message", "-p", "#S:#I #{pane_id}"];
|
|
828
1649
|
const result = Bun.spawnSync([tmuxCommand, ...args], { stdout: "pipe", stderr: "pipe" });
|
|
829
|
-
if (result.exitCode !== 0) throw new Error(result.stderr.toString()
|
|
1650
|
+
if (result.exitCode !== 0) throw new Error(buildTeamTmuxLeaderRequirementMessage(result.stderr.toString()));
|
|
830
1651
|
const [sessionAndWindow = "", leaderPaneId = ""] = result.stdout.toString().trim().split(/\s+/);
|
|
831
1652
|
const [sessionName = "", windowIndex = ""] = sessionAndWindow.split(":");
|
|
832
1653
|
if (!sessionName || !windowIndex || !leaderPaneId.startsWith("%"))
|
|
833
|
-
throw new Error(`invalid_tmux_context:${result.stdout.toString().trim()}`);
|
|
1654
|
+
throw new Error(buildTeamTmuxLeaderRequirementMessage(`invalid_tmux_context:${result.stdout.toString().trim()}`));
|
|
1655
|
+
if (readGjcTmuxProfileValue(tmuxCommand, sessionName) !== GJC_TMUX_PROFILE_VALUE)
|
|
1656
|
+
throw new Error(buildTeamTmuxLeaderRequirementMessage(`unmanaged_tmux_session:${sessionName}`));
|
|
834
1657
|
return { sessionName, windowIndex, leaderPaneId, target: `${sessionName}:${windowIndex}` };
|
|
835
1658
|
}
|
|
836
1659
|
export function resolveGjcWorkerCommand(cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): string {
|
|
@@ -852,7 +1675,7 @@ function buildWorkerCommand(config: GjcTeamConfig, worker: GjcTeamWorker): strin
|
|
|
852
1675
|
workspace,
|
|
853
1676
|
`Task: ${config.task}`,
|
|
854
1677
|
`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.`,
|
|
855
|
-
`Use gjc team api claim-task/transition-task-status with this worker id
|
|
1678
|
+
`Use gjc team api update-worker-status to report task-local activity, then claim-task/transition-task-status with this worker id; record completion_evidence (summary plus a passed command or verified inspection/artifact item) before completed, and do not mutate leader-owned goal state.`,
|
|
856
1679
|
].join("\n");
|
|
857
1680
|
const env = [
|
|
858
1681
|
`GJC_TEAM_WORKER=${shellQuote(`${config.team_name}/${worker.id}`)}`,
|
|
@@ -1032,9 +1855,18 @@ async function appendIntegrationReport(
|
|
|
1032
1855
|
entry: { worker: string; operation: "merge" | "cherry-pick" | "rebase"; files: string[]; detail: string },
|
|
1033
1856
|
): Promise<void> {
|
|
1034
1857
|
const line = `- [${now()}] ${entry.worker}: ${entry.operation}; files=${entry.files.join(",") || "unknown"}; ${entry.detail}\n`;
|
|
1035
|
-
await
|
|
1036
|
-
|
|
1037
|
-
|
|
1858
|
+
if (await pathExists(integrationReportPath(dir)))
|
|
1859
|
+
await appendText(
|
|
1860
|
+
integrationReportPath(dir),
|
|
1861
|
+
line,
|
|
1862
|
+
stateWriterOptions(integrationReportPath(dir), "report", "append"),
|
|
1863
|
+
);
|
|
1864
|
+
else
|
|
1865
|
+
await writeReport(
|
|
1866
|
+
integrationReportPath(dir),
|
|
1867
|
+
`# Integration Report\n\n${line}`,
|
|
1868
|
+
stateWriterOptions(integrationReportPath(dir), "report", "write"),
|
|
1869
|
+
);
|
|
1038
1870
|
}
|
|
1039
1871
|
async function appendCommitHygieneEntries(config: GjcTeamConfig, entries: GjcTeamCommitHygieneEntry[]): Promise<void> {
|
|
1040
1872
|
if (entries.length === 0) return;
|
|
@@ -1583,13 +2415,18 @@ async function integrateGjcWorkerCommits(
|
|
|
1583
2415
|
}
|
|
1584
2416
|
|
|
1585
2417
|
async function initializeStateDirs(dir: string, workers: GjcTeamWorker[]): Promise<void> {
|
|
1586
|
-
|
|
1587
|
-
|
|
2418
|
+
// Empty mailbox directories are runtime state, so they must exist before messages arrive.
|
|
2419
|
+
await fs.mkdir(path.join(dir, "mailbox"), { recursive: true });
|
|
1588
2420
|
for (const worker of workers) {
|
|
1589
|
-
await fs.mkdir(workerDir(dir, worker.id), { recursive: true });
|
|
1590
2421
|
await fs.mkdir(mailboxDirPath(dir, worker.id), { recursive: true });
|
|
1591
2422
|
await writeJsonFile(mailboxPath(dir, worker.id), { messages: [] });
|
|
1592
2423
|
await writeJsonFile(path.join(workerDir(dir, worker.id), "status.json"), { state: "idle", updated_at: now() });
|
|
2424
|
+
await writeJsonFile(workerLifecyclePath(dir, worker.id), {
|
|
2425
|
+
worker: worker.id,
|
|
2426
|
+
lifecycle_state: "starting",
|
|
2427
|
+
worker_status_state: "idle",
|
|
2428
|
+
updated_at: now(),
|
|
2429
|
+
} satisfies GjcTeamWorkerLifecycle);
|
|
1593
2430
|
await writeJsonFile(path.join(workerDir(dir, worker.id), "heartbeat.json"), {
|
|
1594
2431
|
pid: 0,
|
|
1595
2432
|
last_turn_at: now(),
|
|
@@ -1597,6 +2434,7 @@ async function initializeStateDirs(dir: string, workers: GjcTeamWorker[]): Promi
|
|
|
1597
2434
|
alive: true,
|
|
1598
2435
|
});
|
|
1599
2436
|
}
|
|
2437
|
+
// Empty leader mailbox directory is runtime state, so it must exist before messages arrive.
|
|
1600
2438
|
await fs.mkdir(mailboxDirPath(dir, "leader-fixed"), { recursive: true });
|
|
1601
2439
|
await writeJsonFile(mailboxPath(dir, "leader-fixed"), { messages: [] });
|
|
1602
2440
|
}
|
|
@@ -1714,6 +2552,10 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
|
|
|
1714
2552
|
updated_at: now(),
|
|
1715
2553
|
};
|
|
1716
2554
|
await writeJsonFile(path.join(dir, "config.json"), runningConfig);
|
|
2555
|
+
await writeWorkerLifecycleForConfig(dir, runningConfig, "starting", worker => ({
|
|
2556
|
+
pane_id: worker.pane_id,
|
|
2557
|
+
started_at: runningConfig.created_at,
|
|
2558
|
+
}));
|
|
1717
2559
|
await writePhase(dir, "running");
|
|
1718
2560
|
return readGjcTeamSnapshot(teamName, cwd, env);
|
|
1719
2561
|
}
|
|
@@ -1722,6 +2564,7 @@ export async function readGjcTeamSnapshot(
|
|
|
1722
2564
|
teamName: string,
|
|
1723
2565
|
cwd = process.cwd(),
|
|
1724
2566
|
env: NodeJS.ProcessEnv = process.env,
|
|
2567
|
+
options: GjcTeamSnapshotOptions = {},
|
|
1725
2568
|
): Promise<GjcTeamSnapshot> {
|
|
1726
2569
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
1727
2570
|
const config = await readConfig(dir);
|
|
@@ -1736,7 +2579,11 @@ export async function readGjcTeamSnapshot(
|
|
|
1736
2579
|
};
|
|
1737
2580
|
for (const task of tasks) taskCounts[task.status] += 1;
|
|
1738
2581
|
const monitor = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
|
|
1739
|
-
const
|
|
2582
|
+
const workerLifecycleById = await readWorkerLifecycleById(dir, config);
|
|
2583
|
+
const notificationSummary =
|
|
2584
|
+
options.reconcileNotifications === true
|
|
2585
|
+
? await reconcileTeamNotifications(dir, config)
|
|
2586
|
+
: summarizeNotifications(await listNotificationRecords(dir));
|
|
1740
2587
|
const phase = await resolveGjcTeamSnapshotPhase(dir, config, storedPhase, tasks, monitor);
|
|
1741
2588
|
return {
|
|
1742
2589
|
team_name: config.team_name,
|
|
@@ -1750,10 +2597,19 @@ export async function readGjcTeamSnapshot(
|
|
|
1750
2597
|
task_counts: taskCounts,
|
|
1751
2598
|
workers: config.workers,
|
|
1752
2599
|
integration_by_worker: monitor?.integration_by_worker,
|
|
2600
|
+
worker_lifecycle_by_id: workerLifecycleById,
|
|
1753
2601
|
notification_summary: notificationSummary,
|
|
1754
2602
|
updated_at: config.updated_at,
|
|
1755
2603
|
};
|
|
1756
2604
|
}
|
|
2605
|
+
export async function monitorGjcTeamSnapshot(
|
|
2606
|
+
teamName: string,
|
|
2607
|
+
cwd = process.cwd(),
|
|
2608
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
2609
|
+
): Promise<GjcTeamSnapshot> {
|
|
2610
|
+
const snapshot = await monitorGjcTeam(teamName, cwd, env);
|
|
2611
|
+
return snapshot;
|
|
2612
|
+
}
|
|
1757
2613
|
function workerIntegrationFingerprint(head: string | null, classification: GjcWorkerCheckpointClassification): string {
|
|
1758
2614
|
return `${head ?? "no-head"}:${classification.kind}:${classification.files.join("\0")}`;
|
|
1759
2615
|
}
|
|
@@ -1864,6 +2720,7 @@ export async function monitorGjcTeam(
|
|
|
1864
2720
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
1865
2721
|
const config = await readConfig(dir);
|
|
1866
2722
|
const previous = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
|
|
2723
|
+
await reconcileGjcTeamStaleClaims(teamName, dir, config, env);
|
|
1867
2724
|
const integrationByWorker = await integrateGjcWorkerCommits(config, dir, previous, cwd, env);
|
|
1868
2725
|
await writeJsonFile(monitorSnapshotPath(dir), { integration_by_worker: integrationByWorker, updated_at: now() });
|
|
1869
2726
|
await replayGjcTeamNotifications(teamName, cwd, env);
|
|
@@ -1902,7 +2759,7 @@ async function writeGjcWorkerStartupAck(
|
|
|
1902
2759
|
): Promise<Record<string, unknown>> {
|
|
1903
2760
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
1904
2761
|
const config = await readConfig(dir);
|
|
1905
|
-
|
|
2762
|
+
const teamWorker = findKnownWorker(config, worker);
|
|
1906
2763
|
const ack = {
|
|
1907
2764
|
worker,
|
|
1908
2765
|
pid: typeof input.pid === "number" ? input.pid : undefined,
|
|
@@ -1911,6 +2768,11 @@ async function writeGjcWorkerStartupAck(
|
|
|
1911
2768
|
ack_at: now(),
|
|
1912
2769
|
};
|
|
1913
2770
|
await writeJsonFile(path.join(workerDir(dir, worker), "startup-ack.json"), ack);
|
|
2771
|
+
await writeWorkerLifecycleRecord(dir, teamWorker, "ready", {
|
|
2772
|
+
pane_id: teamWorker.pane_id,
|
|
2773
|
+
pid: typeof input.pid === "number" ? input.pid : undefined,
|
|
2774
|
+
started_at: ack.ack_at,
|
|
2775
|
+
});
|
|
1914
2776
|
await appendEvent(dir, { type: "worker_startup_ack", worker, message: `Worker ${worker} acknowledged startup` });
|
|
1915
2777
|
return ack;
|
|
1916
2778
|
}
|
|
@@ -2026,12 +2888,31 @@ export async function shutdownGjcTeam(
|
|
|
2026
2888
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2027
2889
|
const config = await readConfig(dir);
|
|
2028
2890
|
const tasks = await readTasks(dir);
|
|
2029
|
-
const
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2891
|
+
const evidenceFailures = tasks
|
|
2892
|
+
.map(task => {
|
|
2893
|
+
const reason = task.status === "completed" ? getGjcTeamTaskCompletionEvidenceFailure(task) : null;
|
|
2894
|
+
return reason ? { task_id: task.id, reason } : null;
|
|
2895
|
+
})
|
|
2896
|
+
.filter((failure): failure is { task_id: string; reason: string } => failure != null);
|
|
2897
|
+
const shutdownRequestId = `shutdown-${stableHash([config.team_name, now(), randomUUID()].join(":"))}`;
|
|
2898
|
+
const shutdownRequestedAt = now();
|
|
2899
|
+
await Promise.all(
|
|
2900
|
+
config.workers.map(worker =>
|
|
2901
|
+
writeGjcShutdownRequest(
|
|
2902
|
+
teamName,
|
|
2903
|
+
worker.id,
|
|
2904
|
+
"leader-fixed",
|
|
2905
|
+
cwd,
|
|
2906
|
+
env,
|
|
2907
|
+
shutdownRequestId,
|
|
2908
|
+
"graceful",
|
|
2909
|
+
shutdownRequestedAt,
|
|
2910
|
+
),
|
|
2911
|
+
),
|
|
2912
|
+
);
|
|
2913
|
+
const monitor = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
|
|
2914
|
+
const completionVerified = tasks.length === 0 || tasks.every(isGjcTeamTaskCompletionVerified);
|
|
2915
|
+
const pendingIntegration = completionVerified ? await hasPendingGjcTeamIntegration(dir, config, monitor) : false;
|
|
2035
2916
|
killWorkerPanes(config);
|
|
2036
2917
|
await removeCleanCreatedWorktrees(config.workers);
|
|
2037
2918
|
const stopped = {
|
|
@@ -2040,18 +2921,50 @@ export async function shutdownGjcTeam(
|
|
|
2040
2921
|
updated_at: now(),
|
|
2041
2922
|
};
|
|
2042
2923
|
await writeJsonFile(path.join(dir, "config.json"), stopped);
|
|
2924
|
+
await writeWorkerLifecycleForConfig(dir, stopped, "stopped", worker => ({
|
|
2925
|
+
pane_id: worker.pane_id,
|
|
2926
|
+
stopped_at: stopped.updated_at,
|
|
2927
|
+
stop_reason: "graceful_shutdown",
|
|
2928
|
+
shutdown_request_id: shutdownRequestId,
|
|
2929
|
+
shutdown_requested_at: shutdownRequestedAt,
|
|
2930
|
+
shutdown_mode: "graceful",
|
|
2931
|
+
}));
|
|
2932
|
+
const workerLifecycleById = await readWorkerLifecycleById(dir, stopped);
|
|
2933
|
+
const gracefulShutdownComplete = stopped.workers.every(worker => {
|
|
2934
|
+
const lifecycle = workerLifecycleById[worker.id];
|
|
2935
|
+
return (
|
|
2936
|
+
lifecycle?.lifecycle_state === "stopped" &&
|
|
2937
|
+
lifecycle.shutdown_request_id === shutdownRequestId &&
|
|
2938
|
+
lifecycle.shutdown_mode === "graceful"
|
|
2939
|
+
);
|
|
2940
|
+
});
|
|
2941
|
+
const shutdownPhase: GjcTeamPhase =
|
|
2942
|
+
completionVerified && gracefulShutdownComplete
|
|
2943
|
+
? pendingIntegration
|
|
2944
|
+
? "awaiting_integration"
|
|
2945
|
+
: "complete"
|
|
2946
|
+
: evidenceFailures.length > 0 || tasks.some(task => task.status === "failed" || task.status === "blocked")
|
|
2947
|
+
? "failed"
|
|
2948
|
+
: "cancelled";
|
|
2043
2949
|
await writePhase(dir, shutdownPhase);
|
|
2950
|
+
const shutdownData: Record<string, unknown> = {
|
|
2951
|
+
phase: shutdownPhase,
|
|
2952
|
+
shutdown_request_id: shutdownRequestId,
|
|
2953
|
+
graceful_shutdown_complete: gracefulShutdownComplete,
|
|
2954
|
+
};
|
|
2955
|
+
if (evidenceFailures.length > 0) shutdownData.evidence_failures = evidenceFailures;
|
|
2044
2956
|
await appendEvent(dir, {
|
|
2045
2957
|
type: "team_shutdown",
|
|
2046
2958
|
message:
|
|
2047
2959
|
shutdownPhase === "complete"
|
|
2048
2960
|
? "Shut down native gjc team runtime after completed tasks"
|
|
2049
2961
|
: "Shut down native gjc team runtime with incomplete tasks",
|
|
2050
|
-
data:
|
|
2962
|
+
data: shutdownData,
|
|
2051
2963
|
});
|
|
2052
2964
|
await appendTelemetry(dir, {
|
|
2053
2965
|
type: "team_shutdown",
|
|
2054
2966
|
message: `Native gjc team runtime stopped with phase ${shutdownPhase}`,
|
|
2967
|
+
data: { shutdown_request_id: shutdownRequestId, graceful_shutdown_complete: gracefulShutdownComplete },
|
|
2055
2968
|
});
|
|
2056
2969
|
return readGjcTeamSnapshot(config.team_name, cwd, env);
|
|
2057
2970
|
}
|
|
@@ -2079,9 +2992,11 @@ export async function createGjcTeamTask(
|
|
|
2079
2992
|
description: string,
|
|
2080
2993
|
cwd = process.cwd(),
|
|
2081
2994
|
env: NodeJS.ProcessEnv = process.env,
|
|
2995
|
+
taskOptions: GjcTeamTaskMetadataInput = {},
|
|
2082
2996
|
): Promise<GjcTeamTask> {
|
|
2083
2997
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2084
2998
|
const config = await readConfig(dir);
|
|
2999
|
+
if (taskOptions.owner) assertKnownWorker(config, taskOptions.owner);
|
|
2085
3000
|
const tasks = await readTasks(dir);
|
|
2086
3001
|
const next = tasks.length + 1;
|
|
2087
3002
|
const task: GjcTeamTask = {
|
|
@@ -2091,6 +3006,12 @@ export async function createGjcTeamTask(
|
|
|
2091
3006
|
title: subject,
|
|
2092
3007
|
objective: description,
|
|
2093
3008
|
status: "pending",
|
|
3009
|
+
...(taskOptions.owner ? { owner: taskOptions.owner } : {}),
|
|
3010
|
+
...(taskOptions.lane ? { lane: taskOptions.lane } : {}),
|
|
3011
|
+
...(taskOptions.required_role ? { required_role: taskOptions.required_role } : {}),
|
|
3012
|
+
...(taskOptions.allowed_roles ? { allowed_roles: taskOptions.allowed_roles } : {}),
|
|
3013
|
+
...(taskOptions.depends_on ? { depends_on: taskOptions.depends_on } : {}),
|
|
3014
|
+
...(taskOptions.blocked_by ? { blocked_by: taskOptions.blocked_by } : {}),
|
|
2094
3015
|
version: 1,
|
|
2095
3016
|
created_at: now(),
|
|
2096
3017
|
updated_at: now(),
|
|
@@ -2104,7 +3025,12 @@ export async function createGjcTeamTask(
|
|
|
2104
3025
|
export async function updateGjcTeamTask(
|
|
2105
3026
|
teamName: string,
|
|
2106
3027
|
taskId: string,
|
|
2107
|
-
updates: Partial<
|
|
3028
|
+
updates: Partial<
|
|
3029
|
+
Pick<
|
|
3030
|
+
GjcTeamTask,
|
|
3031
|
+
"subject" | "description" | "blocked_by" | "depends_on" | "lane" | "required_role" | "allowed_roles"
|
|
3032
|
+
>
|
|
3033
|
+
>,
|
|
2108
3034
|
cwd = process.cwd(),
|
|
2109
3035
|
env: NodeJS.ProcessEnv = process.env,
|
|
2110
3036
|
): Promise<GjcTeamTask> {
|
|
@@ -2131,13 +3057,20 @@ export async function claimGjcTeamTask(
|
|
|
2131
3057
|
): Promise<GjcTeamApiClaimResult> {
|
|
2132
3058
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2133
3059
|
const config = await readConfig(dir);
|
|
2134
|
-
|
|
3060
|
+
const teamWorker = findKnownWorker(config, workerId);
|
|
3061
|
+
const livenessRecovery = await reconcileGjcTeamStaleClaims(teamName, dir, config, env);
|
|
3062
|
+
const staleWorkerReasons = livenessRecovery.stale_workers[workerId];
|
|
3063
|
+
if (staleWorkerReasons?.length)
|
|
3064
|
+
return { ok: false, reason: `worker_not_live:${workerId}:${staleWorkerReasons.join(",")}` };
|
|
2135
3065
|
const tasks = await readTasks(dir);
|
|
2136
3066
|
const task = taskId
|
|
2137
3067
|
? tasks.find(candidate => candidate.id === taskId)
|
|
2138
|
-
: tasks.find(candidate =>
|
|
2139
|
-
if (!task) return { ok: false, reason: "no_pending_task" };
|
|
2140
|
-
|
|
3068
|
+
: tasks.find(candidate => getGjcTeamTaskClaimEligibilityReason(candidate, teamWorker, tasks) == null);
|
|
3069
|
+
if (!task) return { ok: false, reason: taskId ? `task_not_found:${taskId}` : "no_pending_task" };
|
|
3070
|
+
const eligibilityReason = getGjcTeamTaskClaimEligibilityReason(task, teamWorker, tasks);
|
|
3071
|
+
if (eligibilityReason) return { ok: false, reason: eligibilityReason };
|
|
3072
|
+
const activeClaimReason = await getActiveClaimReason(dir, task);
|
|
3073
|
+
if (activeClaimReason) return { ok: false, reason: activeClaimReason };
|
|
2141
3074
|
const token = randomUUID();
|
|
2142
3075
|
const claim: GjcTeamTaskClaim = {
|
|
2143
3076
|
owner: workerId,
|
|
@@ -2145,20 +3078,19 @@ export async function claimGjcTeamTask(
|
|
|
2145
3078
|
leased_until: new Date(Date.now() + 30 * 60_000).toISOString(),
|
|
2146
3079
|
};
|
|
2147
3080
|
const claimPath = path.join(dir, "claims", `${task.id}.json`);
|
|
2148
|
-
await
|
|
2149
|
-
|
|
2150
|
-
try {
|
|
2151
|
-
claimFile = await fs.open(claimPath, "wx");
|
|
2152
|
-
await claimFile.writeFile(`${JSON.stringify(claim, null, 2)}\n`, "utf-8");
|
|
2153
|
-
} catch (error) {
|
|
2154
|
-
if (isEexist(error)) return { ok: false, reason: `task_already_claimed:${task.id}` };
|
|
2155
|
-
throw error;
|
|
2156
|
-
} finally {
|
|
2157
|
-
await claimFile?.close();
|
|
2158
|
-
}
|
|
3081
|
+
const created = await writeJsonFileNoClobber(claimPath, claim);
|
|
3082
|
+
if (!created) return { ok: false, reason: `task_already_claimed:${task.id}` };
|
|
2159
3083
|
const current = await readGjcTeamTask(teamName, task.id, cwd, env);
|
|
2160
|
-
|
|
3084
|
+
const currentEligibilityReason = getGjcTeamTaskClaimEligibilityReason(current, teamWorker, await readTasks(dir));
|
|
3085
|
+
if (currentEligibilityReason) {
|
|
2161
3086
|
await fs.rm(claimPath, { force: true });
|
|
3087
|
+
return { ok: false, reason: currentEligibilityReason };
|
|
3088
|
+
}
|
|
3089
|
+
if (current.status !== "pending") {
|
|
3090
|
+
await deleteIfOwned(claimPath, {
|
|
3091
|
+
...stateWriterOptions(claimPath, "prune", "rollback"),
|
|
3092
|
+
predicate: current => (current as GjcTeamTaskClaim).token === token,
|
|
3093
|
+
});
|
|
2162
3094
|
return { ok: false, reason: `task_not_pending:${task.id}` };
|
|
2163
3095
|
}
|
|
2164
3096
|
const updated: GjcTeamTask = {
|
|
@@ -2173,7 +3105,10 @@ export async function claimGjcTeamTask(
|
|
|
2173
3105
|
try {
|
|
2174
3106
|
await writeTask(dir, updated);
|
|
2175
3107
|
} catch (error) {
|
|
2176
|
-
await
|
|
3108
|
+
await deleteIfOwned(claimPath, {
|
|
3109
|
+
...stateWriterOptions(claimPath, "prune", "rollback"),
|
|
3110
|
+
predicate: current => (current as GjcTeamTaskClaim).token === token,
|
|
3111
|
+
});
|
|
2177
3112
|
throw error;
|
|
2178
3113
|
}
|
|
2179
3114
|
await appendEvent(dir, {
|
|
@@ -2192,7 +3127,7 @@ export async function transitionGjcTeamTaskStatus(
|
|
|
2192
3127
|
env: NodeJS.ProcessEnv = process.env,
|
|
2193
3128
|
claimToken?: string,
|
|
2194
3129
|
workerId?: string,
|
|
2195
|
-
|
|
3130
|
+
completionEvidenceInput?: unknown,
|
|
2196
3131
|
): Promise<GjcTeamTask> {
|
|
2197
3132
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2198
3133
|
const config = await readConfig(dir);
|
|
@@ -2205,30 +3140,39 @@ export async function transitionGjcTeamTaskStatus(
|
|
|
2205
3140
|
if (task.claim.token !== claimToken) throw new Error(`claim_token_mismatch:${taskId}`);
|
|
2206
3141
|
if (workerId && task.claim.owner !== workerId) throw new Error(`claim_owner_mismatch:${taskId}`);
|
|
2207
3142
|
const terminal = status === "completed" || status === "failed";
|
|
2208
|
-
|
|
2209
|
-
|
|
3143
|
+
const transitionedAt = now();
|
|
3144
|
+
const completionEvidence =
|
|
3145
|
+
status === "completed"
|
|
3146
|
+
? normalizeGjcTeamTaskCompletionEvidence(taskId, task.claim.owner, completionEvidenceInput, transitionedAt)
|
|
3147
|
+
: undefined;
|
|
2210
3148
|
const updated: GjcTeamTask = {
|
|
2211
3149
|
...task,
|
|
2212
3150
|
status,
|
|
2213
3151
|
claim: terminal ? undefined : task.claim,
|
|
2214
3152
|
version: task.version + 1,
|
|
2215
|
-
updated_at:
|
|
2216
|
-
...(terminal ? { completed_at:
|
|
3153
|
+
updated_at: transitionedAt,
|
|
3154
|
+
...(terminal ? { completed_at: transitionedAt } : {}),
|
|
3155
|
+
...(completionEvidence ? { completion_evidence: completionEvidence } : {}),
|
|
2217
3156
|
};
|
|
2218
3157
|
await writeTask(dir, updated);
|
|
2219
|
-
if (terminal
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
3158
|
+
if (terminal) {
|
|
3159
|
+
const claimPath = path.join(dir, "claims", `${taskId}.json`);
|
|
3160
|
+
await removeFileAudited(claimPath, stateWriterOptions(claimPath, "prune", "terminal"));
|
|
3161
|
+
}
|
|
3162
|
+
const eventData: Record<string, unknown> = { status };
|
|
3163
|
+
if (completionEvidence) {
|
|
3164
|
+
eventData.completion_evidence = {
|
|
3165
|
+
recorded_by: completionEvidence.recorded_by,
|
|
3166
|
+
item_count: completionEvidence.items.length,
|
|
3167
|
+
verified_item_count: completionEvidence.items.filter(isGjcTeamCompletionEvidenceItemVerified).length,
|
|
3168
|
+
files_count: completionEvidence.files?.length ?? 0,
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
2227
3171
|
await appendEvent(dir, {
|
|
2228
3172
|
type: "task_transitioned",
|
|
2229
3173
|
task_id: taskId,
|
|
2230
3174
|
message: "Task status changed",
|
|
2231
|
-
data:
|
|
3175
|
+
data: eventData,
|
|
2232
3176
|
});
|
|
2233
3177
|
return updated;
|
|
2234
3178
|
}
|
|
@@ -2239,8 +3183,18 @@ export async function transitionGjcTeamTask(
|
|
|
2239
3183
|
cwd = process.cwd(),
|
|
2240
3184
|
env: NodeJS.ProcessEnv = process.env,
|
|
2241
3185
|
claimToken?: string,
|
|
3186
|
+
completionEvidenceInput?: unknown,
|
|
2242
3187
|
): Promise<GjcTeamTask> {
|
|
2243
|
-
return transitionGjcTeamTaskStatus(
|
|
3188
|
+
return transitionGjcTeamTaskStatus(
|
|
3189
|
+
teamName,
|
|
3190
|
+
taskId,
|
|
3191
|
+
parseGjcTeamTaskStatus(status, true),
|
|
3192
|
+
cwd,
|
|
3193
|
+
env,
|
|
3194
|
+
claimToken,
|
|
3195
|
+
undefined,
|
|
3196
|
+
completionEvidenceInput,
|
|
3197
|
+
);
|
|
2244
3198
|
}
|
|
2245
3199
|
export async function releaseGjcTeamTaskClaim(
|
|
2246
3200
|
teamName: string,
|
|
@@ -2263,7 +3217,11 @@ export async function releaseGjcTeamTaskClaim(
|
|
|
2263
3217
|
updated_at: now(),
|
|
2264
3218
|
};
|
|
2265
3219
|
await writeTask(dir, updated);
|
|
2266
|
-
|
|
3220
|
+
const claimPath = path.join(dir, "claims", `${taskId}.json`);
|
|
3221
|
+
await deleteIfOwned(claimPath, {
|
|
3222
|
+
...stateWriterOptions(claimPath, "prune", "release"),
|
|
3223
|
+
predicate: current => (current as GjcTeamTaskClaim).token === claimToken,
|
|
3224
|
+
});
|
|
2267
3225
|
await appendEvent(dir, {
|
|
2268
3226
|
type: "task_claim_released",
|
|
2269
3227
|
task_id: taskId,
|
|
@@ -2603,12 +3561,43 @@ export async function readGjcWorkerStatus(
|
|
|
2603
3561
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2604
3562
|
const config = await readConfig(dir);
|
|
2605
3563
|
assertKnownWorker(config, worker);
|
|
2606
|
-
return (
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
3564
|
+
return readWorkerStatusFile(dir, worker);
|
|
3565
|
+
}
|
|
3566
|
+
export async function updateGjcWorkerStatus(
|
|
3567
|
+
teamName: string,
|
|
3568
|
+
worker: string,
|
|
3569
|
+
status: GjcWorkerStatusState,
|
|
3570
|
+
cwd = process.cwd(),
|
|
3571
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
3572
|
+
currentTaskId?: string,
|
|
3573
|
+
reason?: string,
|
|
3574
|
+
): Promise<WorkerStatusFile> {
|
|
3575
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
3576
|
+
const config = await readConfig(dir);
|
|
3577
|
+
const teamWorker = findKnownWorker(config, worker);
|
|
3578
|
+
if (currentTaskId) assertSafeId("task_id", currentTaskId);
|
|
3579
|
+
const trimmedReason = reason?.trim();
|
|
3580
|
+
const value: WorkerStatusFile = {
|
|
3581
|
+
state: status,
|
|
3582
|
+
...(currentTaskId ? { current_task_id: currentTaskId } : {}),
|
|
3583
|
+
...(trimmedReason ? { reason: trimmedReason } : {}),
|
|
3584
|
+
updated_at: now(),
|
|
3585
|
+
};
|
|
3586
|
+
await writeJsonFile(path.join(workerDir(dir, worker), "status.json"), value);
|
|
3587
|
+
const currentLifecycle = await readWorkerLifecycleRecord(dir, teamWorker);
|
|
3588
|
+
const lifecycleState =
|
|
3589
|
+
currentLifecycle.lifecycle_state === "stopped" ? "stopped" : lifecycleStateForWorkerStatus(status);
|
|
3590
|
+
await writeWorkerLifecycleRecord(dir, teamWorker, lifecycleState);
|
|
3591
|
+
await appendEvent(dir, {
|
|
3592
|
+
type: "worker_status_updated",
|
|
3593
|
+
worker,
|
|
3594
|
+
message: `Worker ${worker} reported ${status}`,
|
|
3595
|
+
data: {
|
|
3596
|
+
status,
|
|
3597
|
+
current_task_id: currentTaskId,
|
|
3598
|
+
},
|
|
3599
|
+
});
|
|
3600
|
+
return value;
|
|
2612
3601
|
}
|
|
2613
3602
|
export async function readGjcWorkerHeartbeat(
|
|
2614
3603
|
teamName: string,
|
|
@@ -2646,7 +3635,7 @@ export async function writeGjcWorkerInbox(
|
|
|
2646
3635
|
const config = await readConfig(dir);
|
|
2647
3636
|
assertKnownWorker(config, worker);
|
|
2648
3637
|
const filePath = path.join(workerDir(dir, worker), "inbox.md");
|
|
2649
|
-
await
|
|
3638
|
+
await writeReport(filePath, content, stateWriterOptions(filePath, "report", "write"));
|
|
2650
3639
|
return { path: filePath };
|
|
2651
3640
|
}
|
|
2652
3641
|
export async function writeGjcWorkerIdentity(
|
|
@@ -2678,6 +3667,23 @@ export async function readGjcTeamEvents(
|
|
|
2678
3667
|
throw error;
|
|
2679
3668
|
}
|
|
2680
3669
|
}
|
|
3670
|
+
export async function readGjcTeamTraces(
|
|
3671
|
+
teamName: string,
|
|
3672
|
+
cwd = process.cwd(),
|
|
3673
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
3674
|
+
): Promise<GjcTeamTraceEvent[]> {
|
|
3675
|
+
const dir = await findTeamDir(teamName, cwd, env);
|
|
3676
|
+
try {
|
|
3677
|
+
const text = await Bun.file(tracePath(dir)).text();
|
|
3678
|
+
return text
|
|
3679
|
+
.split(/\r?\n/)
|
|
3680
|
+
.filter(Boolean)
|
|
3681
|
+
.map(line => JSON.parse(line) as GjcTeamTraceEvent);
|
|
3682
|
+
} catch (error) {
|
|
3683
|
+
if (isEnoent(error)) return [];
|
|
3684
|
+
throw error;
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
2681
3687
|
export async function appendGjcTeamEvent(
|
|
2682
3688
|
teamName: string,
|
|
2683
3689
|
type: string,
|
|
@@ -2741,13 +3747,27 @@ export async function writeGjcShutdownRequest(
|
|
|
2741
3747
|
requestedBy: string,
|
|
2742
3748
|
cwd = process.cwd(),
|
|
2743
3749
|
env: NodeJS.ProcessEnv = process.env,
|
|
3750
|
+
requestId = `shutdown-${stableHash([teamName, worker, now(), randomUUID()].join(":"))}`,
|
|
3751
|
+
mode: GjcTeamShutdownMode = "graceful",
|
|
3752
|
+
requestedAt = now(),
|
|
2744
3753
|
): Promise<Record<string, unknown>> {
|
|
2745
3754
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2746
3755
|
const config = await readConfig(dir);
|
|
2747
|
-
|
|
3756
|
+
const teamWorker = findKnownWorker(config, worker);
|
|
2748
3757
|
assertKnownParticipant(config, requestedBy);
|
|
2749
|
-
const value = { worker, requested_by: requestedBy, requested_at:
|
|
3758
|
+
const value = { worker, requested_by: requestedBy, request_id: requestId, mode, requested_at: requestedAt };
|
|
2750
3759
|
await writeJsonFile(path.join(workerDir(dir, worker), "shutdown-request.json"), value);
|
|
3760
|
+
await writeWorkerLifecycleRecord(dir, teamWorker, "draining", {
|
|
3761
|
+
shutdown_request_id: requestId,
|
|
3762
|
+
shutdown_requested_at: requestedAt,
|
|
3763
|
+
shutdown_mode: mode,
|
|
3764
|
+
});
|
|
3765
|
+
await appendEvent(dir, {
|
|
3766
|
+
type: "worker_shutdown_requested",
|
|
3767
|
+
worker,
|
|
3768
|
+
message: `Worker ${worker} shutdown requested`,
|
|
3769
|
+
data: { requested_by: requestedBy, request_id: requestId, mode },
|
|
3770
|
+
});
|
|
2751
3771
|
return value;
|
|
2752
3772
|
}
|
|
2753
3773
|
export async function readGjcShutdownAck(
|
|
@@ -2778,121 +3798,142 @@ export async function executeGjcTeamApiOperation(
|
|
|
2778
3798
|
return { tasks: await listGjcTeamTasks(teamName, cwd, env) };
|
|
2779
3799
|
case "read-task":
|
|
2780
3800
|
return { task: await readGjcTeamTask(teamName, String(input.task_id ?? input.taskId), cwd, env) };
|
|
2781
|
-
case "create-task":
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
};
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
3801
|
+
case "create-task": {
|
|
3802
|
+
const task = await createGjcTeamTask(
|
|
3803
|
+
teamName,
|
|
3804
|
+
String(input.subject ?? "Task"),
|
|
3805
|
+
String(input.description ?? ""),
|
|
3806
|
+
cwd,
|
|
3807
|
+
env,
|
|
3808
|
+
taskMetadataFromInput(input, true),
|
|
3809
|
+
);
|
|
3810
|
+
return { ok: true, ...taskReceiptFields(teamName, task) };
|
|
3811
|
+
}
|
|
3812
|
+
case "update-task": {
|
|
3813
|
+
const task = await updateGjcTeamTask(
|
|
3814
|
+
teamName,
|
|
3815
|
+
String(input.task_id ?? input.taskId),
|
|
3816
|
+
{
|
|
3817
|
+
subject: typeof input.subject === "string" ? input.subject : undefined,
|
|
3818
|
+
description: typeof input.description === "string" ? input.description : undefined,
|
|
3819
|
+
...taskMetadataFromInput(input),
|
|
3820
|
+
},
|
|
3821
|
+
cwd,
|
|
3822
|
+
env,
|
|
3823
|
+
);
|
|
3824
|
+
return { ok: true, ...taskReceiptFields(teamName, task) };
|
|
3825
|
+
}
|
|
3826
|
+
case "claim-task": {
|
|
3827
|
+
const requestedTaskId = input.task_id ?? input.taskId;
|
|
3828
|
+
const result = await claimGjcTeamTask(
|
|
2806
3829
|
teamName,
|
|
2807
3830
|
worker,
|
|
2808
3831
|
cwd,
|
|
2809
3832
|
env,
|
|
2810
|
-
typeof
|
|
3833
|
+
typeof requestedTaskId === "string" ? requestedTaskId : undefined,
|
|
2811
3834
|
);
|
|
2812
|
-
case "transition-task":
|
|
2813
|
-
case "transition-task-status":
|
|
2814
3835
|
return {
|
|
2815
|
-
ok:
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
env,
|
|
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,
|
|
2829
|
-
),
|
|
3836
|
+
ok: result.ok,
|
|
3837
|
+
reason: result.reason,
|
|
3838
|
+
team_name: teamName,
|
|
3839
|
+
worker_id: result.worker_id ?? worker,
|
|
3840
|
+
...(result.task ? taskReceiptFields(teamName, result.task) : {}),
|
|
3841
|
+
claim_token: result.claim_token,
|
|
2830
3842
|
};
|
|
2831
|
-
|
|
3843
|
+
}
|
|
3844
|
+
case "transition-task":
|
|
3845
|
+
case "transition-task-status": {
|
|
3846
|
+
const task = await transitionGjcTeamTaskStatus(
|
|
3847
|
+
teamName,
|
|
3848
|
+
String(input.task_id ?? input.taskId),
|
|
3849
|
+
parseGjcTeamTaskStatus(input.to ?? input.status),
|
|
3850
|
+
cwd,
|
|
3851
|
+
env,
|
|
3852
|
+
typeof input.claim_token === "string" ? input.claim_token : undefined,
|
|
3853
|
+
explicitWorker,
|
|
3854
|
+
input.completion_evidence ?? input.completionEvidence,
|
|
3855
|
+
);
|
|
2832
3856
|
return {
|
|
2833
3857
|
ok: true,
|
|
2834
|
-
task
|
|
2835
|
-
|
|
2836
|
-
String(input.task_id),
|
|
2837
|
-
String(input.claim_token),
|
|
2838
|
-
worker,
|
|
2839
|
-
cwd,
|
|
2840
|
-
env,
|
|
2841
|
-
),
|
|
2842
|
-
};
|
|
2843
|
-
case "send-message":
|
|
2844
|
-
return {
|
|
2845
|
-
message: await sendGjcTeamMessage(
|
|
2846
|
-
teamName,
|
|
2847
|
-
String(input.from_worker),
|
|
2848
|
-
String(input.to_worker),
|
|
2849
|
-
String(input.body),
|
|
2850
|
-
cwd,
|
|
2851
|
-
env,
|
|
2852
|
-
typeof input.idempotency_key === "string" ? input.idempotency_key : undefined,
|
|
2853
|
-
),
|
|
3858
|
+
...taskReceiptFields(teamName, task),
|
|
3859
|
+
worker_id: explicitWorker ?? task.owner ?? task.assignee,
|
|
2854
3860
|
};
|
|
2855
|
-
|
|
3861
|
+
}
|
|
3862
|
+
case "release-task-claim": {
|
|
3863
|
+
const task = await releaseGjcTeamTaskClaim(
|
|
3864
|
+
teamName,
|
|
3865
|
+
String(input.task_id),
|
|
3866
|
+
String(input.claim_token),
|
|
3867
|
+
worker,
|
|
3868
|
+
cwd,
|
|
3869
|
+
env,
|
|
3870
|
+
);
|
|
3871
|
+
return { ok: true, ...taskReceiptFields(teamName, task), worker_id: worker };
|
|
3872
|
+
}
|
|
3873
|
+
case "send-message": {
|
|
3874
|
+
const message = await sendGjcTeamMessage(
|
|
3875
|
+
teamName,
|
|
3876
|
+
String(input.from_worker),
|
|
3877
|
+
String(input.to_worker),
|
|
3878
|
+
String(input.body),
|
|
3879
|
+
cwd,
|
|
3880
|
+
env,
|
|
3881
|
+
typeof input.idempotency_key === "string" ? input.idempotency_key : undefined,
|
|
3882
|
+
);
|
|
3883
|
+
return { ok: true, ...mailboxMessageReceiptFields(teamName, message) };
|
|
3884
|
+
}
|
|
3885
|
+
case "broadcast": {
|
|
3886
|
+
const messages = await broadcastGjcTeamMessage(
|
|
3887
|
+
teamName,
|
|
3888
|
+
String(input.from_worker),
|
|
3889
|
+
String(input.body),
|
|
3890
|
+
cwd,
|
|
3891
|
+
env,
|
|
3892
|
+
typeof input.idempotency_key === "string" ? input.idempotency_key : undefined,
|
|
3893
|
+
);
|
|
2856
3894
|
return {
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
3895
|
+
ok: true,
|
|
3896
|
+
team_name: teamName,
|
|
3897
|
+
message_ids: messages.map(message => message.message_id),
|
|
3898
|
+
delivery_states: messages.map(message => ({
|
|
3899
|
+
message_id: message.message_id,
|
|
3900
|
+
to_worker: message.to_worker,
|
|
3901
|
+
delivered: Boolean(message.delivered_at),
|
|
3902
|
+
notified: Boolean(message.notified_at),
|
|
3903
|
+
})),
|
|
2865
3904
|
};
|
|
3905
|
+
}
|
|
2866
3906
|
case "mailbox-list":
|
|
2867
3907
|
return { messages: await listGjcTeamMailbox(teamName, worker, cwd, env) };
|
|
2868
|
-
case "mailbox-mark-delivered":
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
case "mailbox-mark-notified":
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
3908
|
+
case "mailbox-mark-delivered": {
|
|
3909
|
+
const message = await markGjcTeamMailboxMessage(
|
|
3910
|
+
teamName,
|
|
3911
|
+
worker,
|
|
3912
|
+
String(input.message_id),
|
|
3913
|
+
"delivered_at",
|
|
3914
|
+
cwd,
|
|
3915
|
+
env,
|
|
3916
|
+
);
|
|
3917
|
+
return { ok: true, ...mailboxMessageReceiptFields(teamName, message) };
|
|
3918
|
+
}
|
|
3919
|
+
case "mailbox-mark-notified": {
|
|
3920
|
+
const message = await markGjcTeamMailboxMessage(
|
|
3921
|
+
teamName,
|
|
3922
|
+
worker,
|
|
3923
|
+
String(input.message_id),
|
|
3924
|
+
"notified_at",
|
|
3925
|
+
cwd,
|
|
3926
|
+
env,
|
|
3927
|
+
);
|
|
3928
|
+
return { ok: true, ...mailboxMessageReceiptFields(teamName, message) };
|
|
3929
|
+
}
|
|
2890
3930
|
case "notification-list": {
|
|
2891
3931
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2892
3932
|
const config = await readConfig(dir);
|
|
2893
3933
|
await reconcileTeamNotifications(dir, config);
|
|
2894
3934
|
const notifications = await listNotificationRecords(dir);
|
|
2895
|
-
|
|
3935
|
+
const result = { notifications, summary: summarizeNotifications(notifications) };
|
|
3936
|
+
return notificationSummaryReceipt(teamName, result);
|
|
2896
3937
|
}
|
|
2897
3938
|
case "notification-read":
|
|
2898
3939
|
return {
|
|
@@ -2902,20 +3943,19 @@ export async function executeGjcTeamApiOperation(
|
|
|
2902
3943
|
),
|
|
2903
3944
|
};
|
|
2904
3945
|
case "notification-replay":
|
|
2905
|
-
return replayGjcTeamNotifications(teamName, cwd, env);
|
|
3946
|
+
return notificationSummaryReceipt(teamName, await replayGjcTeamNotifications(teamName, cwd, env));
|
|
2906
3947
|
case "notification-mark-pane-attempt": {
|
|
2907
3948
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
2908
3949
|
const notification = await readNotificationRecord(dir, String(input.notification_id));
|
|
2909
|
-
|
|
2910
|
-
notification
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
};
|
|
3950
|
+
const updated = await writeNotificationRecord(dir, {
|
|
3951
|
+
...notification,
|
|
3952
|
+
delivery_state: parsePaneAttemptResult(String(input.result ?? "failed")),
|
|
3953
|
+
pane_attempt_result: parsePaneAttemptResult(String(input.result ?? "failed")),
|
|
3954
|
+
pane_attempt_reason: String(input.reason ?? "manual_api"),
|
|
3955
|
+
pane_attempt_at: now(),
|
|
3956
|
+
updated_at: now(),
|
|
3957
|
+
});
|
|
3958
|
+
return { ok: true, ...notificationReceiptFields(updated) };
|
|
2919
3959
|
}
|
|
2920
3960
|
case "worker-startup-ack":
|
|
2921
3961
|
return writeGjcWorkerStartupAck(teamName, worker, cwd, env, input);
|
|
@@ -2925,8 +3965,22 @@ export async function executeGjcTeamApiOperation(
|
|
|
2925
3965
|
return readJsonFile(path.join(await findTeamDir(teamName, cwd, env), "manifest.v2.json"));
|
|
2926
3966
|
case "read-worker-status":
|
|
2927
3967
|
return readGjcWorkerStatus(teamName, worker, cwd, env);
|
|
3968
|
+
case "update-worker-status": {
|
|
3969
|
+
const currentTaskIdInput = input.current_task_id ?? input.currentTaskId;
|
|
3970
|
+
return updateGjcWorkerStatus(
|
|
3971
|
+
teamName,
|
|
3972
|
+
worker,
|
|
3973
|
+
parseRequiredGjcWorkerStatusState(input.status ?? input.state),
|
|
3974
|
+
cwd,
|
|
3975
|
+
env,
|
|
3976
|
+
typeof currentTaskIdInput === "string" ? currentTaskIdInput : undefined,
|
|
3977
|
+
typeof input.reason === "string" ? input.reason : undefined,
|
|
3978
|
+
);
|
|
3979
|
+
}
|
|
2928
3980
|
case "read-worker-heartbeat":
|
|
2929
3981
|
return readGjcWorkerHeartbeat(teamName, worker, cwd, env);
|
|
3982
|
+
case "recover-stale-claims":
|
|
3983
|
+
return recoverGjcTeamStaleClaims(teamName, cwd, env);
|
|
2930
3984
|
case "update-worker-heartbeat":
|
|
2931
3985
|
return updateGjcWorkerHeartbeat(
|
|
2932
3986
|
teamName,
|
|
@@ -2962,6 +4016,8 @@ export async function executeGjcTeamApiOperation(
|
|
|
2962
4016
|
return appendGjcTeamEvent(teamName, String(input.type ?? "event"), worker, cwd, env);
|
|
2963
4017
|
case "read-events":
|
|
2964
4018
|
return { events: await readGjcTeamEvents(teamName, cwd, env) };
|
|
4019
|
+
case "read-traces":
|
|
4020
|
+
return { traces: await readGjcTeamTraces(teamName, cwd, env) };
|
|
2965
4021
|
case "await-event":
|
|
2966
4022
|
return awaitGjcTeamEvent(teamName, Number(input.timeout_ms ?? 0), cwd, env);
|
|
2967
4023
|
case "write-monitor-snapshot":
|
|
@@ -2972,8 +4028,18 @@ export async function executeGjcTeamApiOperation(
|
|
|
2972
4028
|
return writeGjcTaskApproval(teamName, String(input.task_id), input, cwd, env);
|
|
2973
4029
|
case "read-task-approval":
|
|
2974
4030
|
return readGjcTaskApproval(teamName, String(input.task_id), cwd, env);
|
|
2975
|
-
case "write-shutdown-request":
|
|
2976
|
-
|
|
4031
|
+
case "write-shutdown-request": {
|
|
4032
|
+
const shutdownRequestIdInput = input.request_id ?? input.requestId;
|
|
4033
|
+
return writeGjcShutdownRequest(
|
|
4034
|
+
teamName,
|
|
4035
|
+
worker,
|
|
4036
|
+
String(input.requested_by ?? input.requestedBy ?? "leader-fixed"),
|
|
4037
|
+
cwd,
|
|
4038
|
+
env,
|
|
4039
|
+
typeof shutdownRequestIdInput === "string" ? shutdownRequestIdInput : undefined,
|
|
4040
|
+
parseGjcTeamShutdownMode(input.mode),
|
|
4041
|
+
);
|
|
4042
|
+
}
|
|
2977
4043
|
case "read-shutdown-ack":
|
|
2978
4044
|
return readGjcShutdownAck(teamName, worker, cwd, env);
|
|
2979
4045
|
default:
|