@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.
Files changed (234) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/types/async/job-manager.d.ts +91 -2
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/commands/harness.d.ts +37 -0
  6. package/dist/types/config/keybindings.d.ts +5 -0
  7. package/dist/types/config/settings-schema.d.ts +10 -4
  8. package/dist/types/config/settings.d.ts +2 -0
  9. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  10. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  11. package/dist/types/deep-interview/render-middleware.d.ts +6 -0
  12. package/dist/types/eval/py/executor.d.ts +2 -0
  13. package/dist/types/eval/py/kernel.d.ts +2 -0
  14. package/dist/types/exec/bash-executor.d.ts +10 -0
  15. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  17. package/dist/types/extensibility/shared-events.d.ts +1 -0
  18. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  19. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  20. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  21. package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
  22. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  23. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  25. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  26. package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  28. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  29. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  30. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  31. package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
  32. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  33. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  34. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  35. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  36. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  37. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  38. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  39. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  40. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  41. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  42. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  43. package/dist/types/harness-control-plane/types.d.ts +162 -0
  44. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  45. package/dist/types/hooks/skill-state.d.ts +23 -29
  46. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  47. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  48. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  49. package/dist/types/internal-urls/types.d.ts +4 -0
  50. package/dist/types/lsp/index.d.ts +10 -10
  51. package/dist/types/modes/bridge/auth.d.ts +12 -0
  52. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  53. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  54. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  55. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  56. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  57. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  58. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  59. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  60. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  61. package/dist/types/modes/components/status-line.d.ts +2 -0
  62. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  63. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  64. package/dist/types/modes/index.d.ts +1 -0
  65. package/dist/types/modes/interactive-mode.d.ts +2 -0
  66. package/dist/types/modes/jobs-observer.d.ts +57 -0
  67. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  68. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  69. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  70. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  71. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  72. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  73. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  74. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  75. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  76. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  77. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  78. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  79. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  80. package/dist/types/modes/types.d.ts +2 -0
  81. package/dist/types/sdk.d.ts +4 -0
  82. package/dist/types/session/agent-session.d.ts +19 -1
  83. package/dist/types/skill-state/active-state.d.ts +2 -0
  84. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  85. package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
  86. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  87. package/dist/types/task/executor.d.ts +3 -0
  88. package/dist/types/task/id.d.ts +7 -0
  89. package/dist/types/task/index.d.ts +5 -0
  90. package/dist/types/task/receipt.d.ts +85 -0
  91. package/dist/types/task/spawn-gate.d.ts +38 -0
  92. package/dist/types/task/types.d.ts +198 -14
  93. package/dist/types/tools/cron.d.ts +6 -0
  94. package/dist/types/tools/index.d.ts +2 -0
  95. package/dist/types/tools/path-utils.d.ts +1 -0
  96. package/dist/types/tools/subagent.d.ts +26 -1
  97. package/package.json +7 -7
  98. package/scripts/build-binary.ts +7 -0
  99. package/src/async/job-manager.ts +334 -6
  100. package/src/cli/args.ts +9 -2
  101. package/src/cli/auth-broker-cli.ts +1 -0
  102. package/src/cli/config-cli.ts +10 -2
  103. package/src/cli.ts +2 -0
  104. package/src/commands/deep-interview.ts +1 -0
  105. package/src/commands/harness.ts +862 -0
  106. package/src/commands/launch.ts +2 -2
  107. package/src/commands/state.ts +2 -1
  108. package/src/commands/team.ts +54 -39
  109. package/src/config/keybindings.ts +6 -0
  110. package/src/config/settings-schema.ts +13 -3
  111. package/src/config/settings.ts +5 -0
  112. package/src/dap/client.ts +17 -3
  113. package/src/debug/crash-diagnostics.ts +223 -0
  114. package/src/debug/runtime-gauges.ts +20 -0
  115. package/src/deep-interview/render-middleware.ts +372 -0
  116. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  117. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  118. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  119. package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
  120. package/src/eval/py/executor.ts +21 -1
  121. package/src/eval/py/kernel.ts +15 -0
  122. package/src/exec/bash-executor.ts +41 -0
  123. package/src/extensibility/custom-tools/types.ts +1 -0
  124. package/src/extensibility/extensions/types.ts +6 -0
  125. package/src/extensibility/shared-events.ts +1 -0
  126. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  127. package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
  128. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  129. package/src/gjc-runtime/ralplan-runtime.ts +235 -43
  130. package/src/gjc-runtime/state-graph.ts +86 -0
  131. package/src/gjc-runtime/state-migrations.ts +179 -0
  132. package/src/gjc-runtime/state-renderer.ts +345 -0
  133. package/src/gjc-runtime/state-runtime.ts +1155 -46
  134. package/src/gjc-runtime/state-schema.ts +192 -0
  135. package/src/gjc-runtime/state-validation.ts +49 -0
  136. package/src/gjc-runtime/state-writer.ts +749 -0
  137. package/src/gjc-runtime/team-runtime.ts +1255 -189
  138. package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
  139. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  140. package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
  141. package/src/gjc-runtime/workflow-manifest.ts +427 -0
  142. package/src/harness-control-plane/classifier.ts +128 -0
  143. package/src/harness-control-plane/control-endpoint.ts +148 -0
  144. package/src/harness-control-plane/finalize.ts +222 -0
  145. package/src/harness-control-plane/frame-mapper.ts +286 -0
  146. package/src/harness-control-plane/operate.ts +225 -0
  147. package/src/harness-control-plane/owner.ts +600 -0
  148. package/src/harness-control-plane/preserve.ts +102 -0
  149. package/src/harness-control-plane/receipts.ts +216 -0
  150. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  151. package/src/harness-control-plane/seams.ts +39 -0
  152. package/src/harness-control-plane/session-lease.ts +388 -0
  153. package/src/harness-control-plane/state-machine.ts +98 -0
  154. package/src/harness-control-plane/storage.ts +257 -0
  155. package/src/harness-control-plane/types.ts +214 -0
  156. package/src/hooks/skill-keywords.ts +4 -2
  157. package/src/hooks/skill-state.ts +197 -64
  158. package/src/internal-urls/agent-protocol.ts +68 -21
  159. package/src/internal-urls/artifact-protocol.ts +12 -17
  160. package/src/internal-urls/docs-index.generated.ts +3 -2
  161. package/src/internal-urls/registry-helpers.ts +19 -16
  162. package/src/internal-urls/types.ts +4 -0
  163. package/src/lsp/client.ts +18 -2
  164. package/src/main.ts +21 -5
  165. package/src/modes/bridge/auth.ts +41 -0
  166. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  167. package/src/modes/bridge/bridge-mode.ts +520 -0
  168. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  169. package/src/modes/bridge/event-stream.ts +70 -0
  170. package/src/modes/components/assistant-message.ts +5 -1
  171. package/src/modes/components/custom-editor.ts +101 -0
  172. package/src/modes/components/hook-selector.ts +133 -20
  173. package/src/modes/components/jobs-overlay-model.ts +109 -0
  174. package/src/modes/components/jobs-overlay.ts +172 -0
  175. package/src/modes/components/status-line/presets.ts +7 -5
  176. package/src/modes/components/status-line/segments.ts +25 -0
  177. package/src/modes/components/status-line/types.ts +2 -0
  178. package/src/modes/components/status-line.ts +9 -1
  179. package/src/modes/controllers/event-controller.ts +71 -6
  180. package/src/modes/controllers/extension-ui-controller.ts +43 -1
  181. package/src/modes/controllers/input-controller.ts +105 -9
  182. package/src/modes/controllers/selector-controller.ts +31 -1
  183. package/src/modes/index.ts +1 -0
  184. package/src/modes/interactive-mode.ts +28 -0
  185. package/src/modes/jobs-observer.ts +204 -0
  186. package/src/modes/rpc/host-tools.ts +1 -186
  187. package/src/modes/rpc/host-uris.ts +1 -235
  188. package/src/modes/rpc/rpc-client.ts +25 -10
  189. package/src/modes/rpc/rpc-mode.ts +12 -381
  190. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  191. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  192. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  193. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  194. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  195. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  196. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  197. package/src/modes/shared/agent-wire/responses.ts +17 -0
  198. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  199. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  200. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  201. package/src/modes/types.ts +2 -0
  202. package/src/prompts/agents/executor.md +13 -0
  203. package/src/prompts/tools/subagent.md +39 -4
  204. package/src/prompts/tools/task-summary.md +3 -9
  205. package/src/prompts/tools/task.md +5 -1
  206. package/src/sdk.ts +8 -0
  207. package/src/session/agent-session.ts +445 -71
  208. package/src/session/session-manager.ts +13 -1
  209. package/src/skill-state/active-state.ts +58 -65
  210. package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
  211. package/src/skill-state/initial-phase.ts +2 -0
  212. package/src/skill-state/workflow-state-contract.ts +33 -4
  213. package/src/skill-state/workflow-state-version.ts +3 -0
  214. package/src/slash-commands/builtin-registry.ts +8 -0
  215. package/src/task/executor.ts +79 -13
  216. package/src/task/id.ts +33 -0
  217. package/src/task/index.ts +376 -74
  218. package/src/task/output-manager.ts +5 -4
  219. package/src/task/receipt.ts +297 -0
  220. package/src/task/render.ts +54 -134
  221. package/src/task/spawn-gate.ts +132 -0
  222. package/src/task/types.ts +104 -10
  223. package/src/tools/ask.ts +88 -27
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash.ts +1 -1
  227. package/src/tools/cron.ts +48 -0
  228. package/src/tools/find.ts +4 -1
  229. package/src/tools/index.ts +2 -0
  230. package/src/tools/path-utils.ts +3 -2
  231. package/src/tools/read.ts +1 -0
  232. package/src/tools/search.ts +1 -0
  233. package/src/tools/skill.ts +6 -1
  234. 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 isEexist(error: unknown): error is FsError {
396
- return typeof error === "object" && error !== null && "code" in error && (error as FsError).code === "EEXIST";
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 fs.mkdir(path.dirname(filePath), { recursive: true });
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
- handle = await fs.open(filePath, "wx");
517
- await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf-8");
684
+ await createJsonNoClobber(
685
+ filePath,
686
+ value,
687
+ stateWriterOptions(filePath, stateCategoryForJsonPath(filePath), "create"),
688
+ );
518
689
  return true;
519
690
  } catch (error) {
520
- if (isEexist(error)) return false;
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 fs.mkdir(path.dirname(filePath), { recursive: true });
528
- await fs.appendFile(filePath, `${JSON.stringify(value)}\n`, "utf-8");
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(task => task.status === "completed")) return storedPhase;
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().trim() || "team_requires_current_tmux_context");
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, record evidence, and do not mutate leader-owned goal state.`,
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 fs.mkdir(path.dirname(integrationReportPath(dir)), { recursive: true });
1036
- if (await pathExists(integrationReportPath(dir))) await fs.appendFile(integrationReportPath(dir), line, "utf-8");
1037
- else await Bun.write(integrationReportPath(dir), `# Integration Report\n\n${line}`);
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
- for (const folder of ["tasks", "claims", "mailbox", "notifications", "dispatch", "approvals", "workers"])
1587
- await fs.mkdir(path.join(dir, folder), { recursive: true });
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 notificationSummary = await reconcileTeamNotifications(dir, config);
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
- assertKnownWorker(config, worker);
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 shutdownPhase: GjcTeamPhase =
2030
- tasks.length === 0 || tasks.every(task => task.status === "completed")
2031
- ? "complete"
2032
- : tasks.some(task => task.status === "failed" || task.status === "blocked")
2033
- ? "failed"
2034
- : "cancelled";
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: { phase: shutdownPhase },
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<Pick<GjcTeamTask, "subject" | "description" | "blocked_by" | "depends_on">>,
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
- assertKnownWorker(config, workerId);
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 => candidate.status === "pending" && (!candidate.owner || candidate.owner === workerId));
2139
- if (!task) return { ok: false, reason: "no_pending_task" };
2140
- if (task.status !== "pending") return { ok: false, reason: `task_not_pending:${task.id}` };
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 fs.mkdir(path.dirname(claimPath), { recursive: true });
2149
- let claimFile: fs.FileHandle | undefined;
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
- if (current.status !== "pending") {
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 fs.rm(claimPath, { force: true });
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
- evidence?: string,
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
- if (status === "completed" && evidence !== undefined && evidence.trim().length === 0)
2209
- throw new Error(`task_evidence_required:${taskId}`);
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: now(),
2216
- ...(terminal ? { completed_at: now() } : {}),
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 && evidence)
2220
- await writeJsonFile(taskEvidencePath(dir, taskId), {
2221
- task_id: taskId,
2222
- worker: workerId ?? task.claim.owner,
2223
- evidence,
2224
- recorded_at: now(),
2225
- });
2226
- if (terminal) await fs.rm(path.join(dir, "claims", `${taskId}.json`), { force: true });
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: { status },
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(teamName, taskId, parseGjcTeamTaskStatus(status, true), cwd, env, claimToken);
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
- await fs.rm(path.join(dir, "claims", `${taskId}.json`), { force: true });
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
- (await readJsonFile<WorkerStatusFile>(path.join(workerDir(dir, worker), "status.json"))) ?? {
2608
- state: "unknown",
2609
- updated_at: now(),
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 Bun.write(filePath, content);
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
- assertKnownWorker(config, worker);
3756
+ const teamWorker = findKnownWorker(config, worker);
2748
3757
  assertKnownParticipant(config, requestedBy);
2749
- const value = { worker, requested_by: requestedBy, requested_at: now() };
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
- return {
2783
- task: await createGjcTeamTask(
2784
- teamName,
2785
- String(input.subject ?? "Task"),
2786
- String(input.description ?? ""),
2787
- cwd,
2788
- env,
2789
- ),
2790
- };
2791
- case "update-task":
2792
- return {
2793
- task: await updateGjcTeamTask(
2794
- teamName,
2795
- String(input.task_id ?? input.taskId),
2796
- {
2797
- subject: typeof input.subject === "string" ? input.subject : undefined,
2798
- description: typeof input.description === "string" ? input.description : undefined,
2799
- },
2800
- cwd,
2801
- env,
2802
- ),
2803
- };
2804
- case "claim-task":
2805
- return claimGjcTeamTask(
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 input.task_id === "string" ? input.task_id : undefined,
3833
+ typeof requestedTaskId === "string" ? requestedTaskId : undefined,
2811
3834
  );
2812
- case "transition-task":
2813
- case "transition-task-status":
2814
3835
  return {
2815
- ok: true,
2816
- task: await transitionGjcTeamTaskStatus(
2817
- teamName,
2818
- String(input.task_id ?? input.taskId),
2819
- parseGjcTeamTaskStatus(input.to ?? input.status),
2820
- cwd,
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
- case "release-task-claim":
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: await releaseGjcTeamTaskClaim(
2835
- teamName,
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
- case "broadcast":
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
- messages: await broadcastGjcTeamMessage(
2858
- teamName,
2859
- String(input.from_worker),
2860
- String(input.body),
2861
- cwd,
2862
- env,
2863
- typeof input.idempotency_key === "string" ? input.idempotency_key : undefined,
2864
- ),
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
- return {
2870
- message: await markGjcTeamMailboxMessage(
2871
- teamName,
2872
- worker,
2873
- String(input.message_id),
2874
- "delivered_at",
2875
- cwd,
2876
- env,
2877
- ),
2878
- };
2879
- case "mailbox-mark-notified":
2880
- return {
2881
- message: await markGjcTeamMailboxMessage(
2882
- teamName,
2883
- worker,
2884
- String(input.message_id),
2885
- "notified_at",
2886
- cwd,
2887
- env,
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
- return { notifications, summary: summarizeNotifications(notifications) };
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
- return {
2910
- notification: await writeNotificationRecord(dir, {
2911
- ...notification,
2912
- delivery_state: parsePaneAttemptResult(String(input.result ?? "failed")),
2913
- pane_attempt_result: parsePaneAttemptResult(String(input.result ?? "failed")),
2914
- pane_attempt_reason: String(input.reason ?? "manual_api"),
2915
- pane_attempt_at: now(),
2916
- updated_at: now(),
2917
- }),
2918
- };
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
- return writeGjcShutdownRequest(teamName, worker, String(input.requested_by ?? "leader-fixed"), cwd, env);
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: