@gajae-code/coding-agent 0.2.4 → 0.3.0

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 (266) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +145 -2
  4. package/dist/types/commands/harness.d.ts +37 -0
  5. package/dist/types/config/settings-schema.d.ts +13 -3
  6. package/dist/types/config/settings.d.ts +3 -1
  7. package/dist/types/deep-interview/render-middleware.d.ts +5 -0
  8. package/dist/types/discovery/helpers.d.ts +1 -0
  9. package/dist/types/exec/bash-executor.d.ts +8 -1
  10. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  12. package/dist/types/extensibility/shared-events.d.ts +1 -0
  13. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +2 -0
  14. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
  16. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  17. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  18. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  19. package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
  20. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  21. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  22. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  23. package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
  24. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  25. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  26. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  27. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  28. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  29. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  30. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  31. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  32. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  33. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  34. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  35. package/dist/types/harness-control-plane/types.d.ts +162 -0
  36. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  37. package/dist/types/hooks/skill-state.d.ts +2 -29
  38. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  39. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  40. package/dist/types/modes/components/skill-hud/render.d.ts +1 -1
  41. package/dist/types/modes/interactive-mode.d.ts +2 -0
  42. package/dist/types/modes/theme/defaults/index.d.ts +45 -9477
  43. package/dist/types/modes/theme/theme.d.ts +1 -5
  44. package/dist/types/modes/types.d.ts +2 -0
  45. package/dist/types/sdk.d.ts +4 -0
  46. package/dist/types/session/agent-session.d.ts +8 -0
  47. package/dist/types/session/streaming-output.d.ts +11 -0
  48. package/dist/types/skill-state/active-state.d.ts +3 -0
  49. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  50. package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
  51. package/dist/types/task/executor.d.ts +3 -0
  52. package/dist/types/task/types.d.ts +56 -3
  53. package/dist/types/tools/bash-allowed-prefixes.d.ts +5 -0
  54. package/dist/types/tools/bash.d.ts +24 -0
  55. package/dist/types/tools/cron.d.ts +110 -0
  56. package/dist/types/tools/index.d.ts +4 -0
  57. package/dist/types/tools/monitor.d.ts +54 -0
  58. package/dist/types/tools/subagent.d.ts +11 -1
  59. package/dist/types/web/search/index.d.ts +1 -0
  60. package/dist/types/web/search/provider.d.ts +11 -4
  61. package/dist/types/web/search/providers/duckduckgo.d.ts +57 -0
  62. package/dist/types/web/search/types.d.ts +1 -1
  63. package/package.json +7 -7
  64. package/src/async/job-manager.ts +522 -6
  65. package/src/cli/agents-cli.ts +3 -0
  66. package/src/cli/auth-broker-cli.ts +1 -0
  67. package/src/cli/config-cli.ts +10 -2
  68. package/src/cli.ts +2 -0
  69. package/src/commands/harness.ts +592 -0
  70. package/src/commands/team.ts +36 -39
  71. package/src/config/settings-schema.ts +15 -2
  72. package/src/config/settings.ts +49 -7
  73. package/src/deep-interview/render-middleware.ts +366 -0
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +9 -2
  75. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  76. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  77. package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
  78. package/src/discovery/helpers.ts +5 -0
  79. package/src/eval/js/shared/rewrite-imports.ts +1 -2
  80. package/src/exec/bash-executor.ts +20 -9
  81. package/src/extensibility/custom-tools/types.ts +1 -0
  82. package/src/extensibility/extensions/types.ts +6 -0
  83. package/src/extensibility/shared-events.ts +1 -0
  84. package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
  85. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  86. package/src/gjc-runtime/ralplan-runtime.ts +27 -10
  87. package/src/gjc-runtime/restricted-role-agent-bash.ts +5 -0
  88. package/src/gjc-runtime/state-graph.ts +86 -0
  89. package/src/gjc-runtime/state-migrations.ts +132 -0
  90. package/src/gjc-runtime/state-renderer.ts +345 -0
  91. package/src/gjc-runtime/state-runtime.ts +733 -21
  92. package/src/gjc-runtime/state-validation.ts +49 -0
  93. package/src/gjc-runtime/state-writer.ts +718 -0
  94. package/src/gjc-runtime/team-runtime.ts +1083 -89
  95. package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
  96. package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
  97. package/src/gjc-runtime/workflow-manifest.ts +425 -0
  98. package/src/harness-control-plane/classifier.ts +128 -0
  99. package/src/harness-control-plane/control-endpoint.ts +137 -0
  100. package/src/harness-control-plane/finalize.ts +222 -0
  101. package/src/harness-control-plane/frame-mapper.ts +286 -0
  102. package/src/harness-control-plane/operate.ts +225 -0
  103. package/src/harness-control-plane/owner.ts +553 -0
  104. package/src/harness-control-plane/preserve.ts +102 -0
  105. package/src/harness-control-plane/receipts.ts +216 -0
  106. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  107. package/src/harness-control-plane/seams.ts +39 -0
  108. package/src/harness-control-plane/session-lease.ts +388 -0
  109. package/src/harness-control-plane/state-machine.ts +97 -0
  110. package/src/harness-control-plane/storage.ts +257 -0
  111. package/src/harness-control-plane/types.ts +214 -0
  112. package/src/hooks/skill-keywords.ts +4 -2
  113. package/src/hooks/skill-state.ts +25 -42
  114. package/src/internal-urls/docs-index.generated.ts +6 -4
  115. package/src/lsp/render.ts +1 -1
  116. package/src/modes/acp/acp-agent.ts +1 -1
  117. package/src/modes/acp/acp-client-bridge.ts +1 -1
  118. package/src/modes/components/agent-dashboard.ts +1 -1
  119. package/src/modes/components/assistant-message.ts +5 -1
  120. package/src/modes/components/diff.ts +2 -2
  121. package/src/modes/components/hook-selector.ts +72 -2
  122. package/src/modes/components/skill-hud/render.ts +7 -2
  123. package/src/modes/controllers/event-controller.ts +71 -6
  124. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  125. package/src/modes/controllers/input-controller.ts +19 -3
  126. package/src/modes/controllers/selector-controller.ts +3 -2
  127. package/src/modes/interactive-mode.ts +21 -2
  128. package/src/modes/theme/defaults/index.ts +0 -196
  129. package/src/modes/theme/theme.ts +35 -35
  130. package/src/modes/types.ts +2 -0
  131. package/src/prompts/agents/architect.md +5 -1
  132. package/src/prompts/agents/critic.md +5 -1
  133. package/src/prompts/agents/executor.md +13 -0
  134. package/src/prompts/agents/frontmatter.md +1 -0
  135. package/src/prompts/agents/planner.md +5 -1
  136. package/src/prompts/tools/bash.md +9 -0
  137. package/src/prompts/tools/cron.md +25 -0
  138. package/src/prompts/tools/monitor.md +30 -0
  139. package/src/prompts/tools/subagent.md +33 -3
  140. package/src/runtime-mcp/oauth-flow.ts +4 -2
  141. package/src/sdk.ts +7 -0
  142. package/src/session/agent-session.ts +247 -38
  143. package/src/session/session-manager.ts +13 -1
  144. package/src/session/streaming-output.ts +21 -0
  145. package/src/skill-state/active-state.ts +222 -78
  146. package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
  147. package/src/skill-state/initial-phase.ts +2 -0
  148. package/src/skill-state/workflow-state-contract.ts +26 -0
  149. package/src/task/agents.ts +1 -0
  150. package/src/task/executor.ts +51 -8
  151. package/src/task/index.ts +120 -8
  152. package/src/task/render.ts +6 -3
  153. package/src/task/types.ts +57 -3
  154. package/src/tools/ask.ts +28 -7
  155. package/src/tools/bash-allowed-prefixes.ts +169 -0
  156. package/src/tools/bash.ts +190 -29
  157. package/src/tools/browser/tab-worker.ts +1 -1
  158. package/src/tools/cron.ts +665 -0
  159. package/src/tools/index.ts +20 -2
  160. package/src/tools/monitor.ts +136 -0
  161. package/src/tools/subagent.ts +255 -64
  162. package/src/vim/engine.ts +3 -3
  163. package/src/web/search/index.ts +31 -18
  164. package/src/web/search/provider.ts +57 -12
  165. package/src/web/search/providers/duckduckgo.ts +279 -0
  166. package/src/web/search/types.ts +2 -0
  167. package/src/modes/theme/dark.json +0 -95
  168. package/src/modes/theme/defaults/alabaster.json +0 -93
  169. package/src/modes/theme/defaults/amethyst.json +0 -96
  170. package/src/modes/theme/defaults/anthracite.json +0 -93
  171. package/src/modes/theme/defaults/basalt.json +0 -91
  172. package/src/modes/theme/defaults/birch.json +0 -95
  173. package/src/modes/theme/defaults/dark-abyss.json +0 -91
  174. package/src/modes/theme/defaults/dark-arctic.json +0 -104
  175. package/src/modes/theme/defaults/dark-aurora.json +0 -95
  176. package/src/modes/theme/defaults/dark-catppuccin.json +0 -107
  177. package/src/modes/theme/defaults/dark-cavern.json +0 -91
  178. package/src/modes/theme/defaults/dark-copper.json +0 -95
  179. package/src/modes/theme/defaults/dark-cosmos.json +0 -90
  180. package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
  181. package/src/modes/theme/defaults/dark-dracula.json +0 -98
  182. package/src/modes/theme/defaults/dark-eclipse.json +0 -91
  183. package/src/modes/theme/defaults/dark-ember.json +0 -95
  184. package/src/modes/theme/defaults/dark-equinox.json +0 -90
  185. package/src/modes/theme/defaults/dark-forest.json +0 -96
  186. package/src/modes/theme/defaults/dark-github.json +0 -105
  187. package/src/modes/theme/defaults/dark-gruvbox.json +0 -112
  188. package/src/modes/theme/defaults/dark-lavender.json +0 -95
  189. package/src/modes/theme/defaults/dark-lunar.json +0 -89
  190. package/src/modes/theme/defaults/dark-midnight.json +0 -95
  191. package/src/modes/theme/defaults/dark-monochrome.json +0 -94
  192. package/src/modes/theme/defaults/dark-monokai.json +0 -98
  193. package/src/modes/theme/defaults/dark-nebula.json +0 -90
  194. package/src/modes/theme/defaults/dark-nord.json +0 -97
  195. package/src/modes/theme/defaults/dark-ocean.json +0 -101
  196. package/src/modes/theme/defaults/dark-one.json +0 -100
  197. package/src/modes/theme/defaults/dark-poimandres.json +0 -141
  198. package/src/modes/theme/defaults/dark-rainforest.json +0 -91
  199. package/src/modes/theme/defaults/dark-reef.json +0 -91
  200. package/src/modes/theme/defaults/dark-retro.json +0 -92
  201. package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
  202. package/src/modes/theme/defaults/dark-sakura.json +0 -95
  203. package/src/modes/theme/defaults/dark-slate.json +0 -95
  204. package/src/modes/theme/defaults/dark-solarized.json +0 -97
  205. package/src/modes/theme/defaults/dark-solstice.json +0 -90
  206. package/src/modes/theme/defaults/dark-starfall.json +0 -91
  207. package/src/modes/theme/defaults/dark-sunset.json +0 -99
  208. package/src/modes/theme/defaults/dark-swamp.json +0 -90
  209. package/src/modes/theme/defaults/dark-synthwave.json +0 -103
  210. package/src/modes/theme/defaults/dark-taiga.json +0 -91
  211. package/src/modes/theme/defaults/dark-terminal.json +0 -95
  212. package/src/modes/theme/defaults/dark-tokyo-night.json +0 -101
  213. package/src/modes/theme/defaults/dark-tundra.json +0 -91
  214. package/src/modes/theme/defaults/dark-twilight.json +0 -91
  215. package/src/modes/theme/defaults/dark-volcanic.json +0 -91
  216. package/src/modes/theme/defaults/graphite.json +0 -92
  217. package/src/modes/theme/defaults/light-arctic.json +0 -107
  218. package/src/modes/theme/defaults/light-aurora-day.json +0 -91
  219. package/src/modes/theme/defaults/light-canyon.json +0 -91
  220. package/src/modes/theme/defaults/light-catppuccin.json +0 -106
  221. package/src/modes/theme/defaults/light-cirrus.json +0 -90
  222. package/src/modes/theme/defaults/light-coral.json +0 -95
  223. package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
  224. package/src/modes/theme/defaults/light-dawn.json +0 -90
  225. package/src/modes/theme/defaults/light-dunes.json +0 -91
  226. package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
  227. package/src/modes/theme/defaults/light-forest.json +0 -100
  228. package/src/modes/theme/defaults/light-frost.json +0 -95
  229. package/src/modes/theme/defaults/light-github.json +0 -115
  230. package/src/modes/theme/defaults/light-glacier.json +0 -91
  231. package/src/modes/theme/defaults/light-gruvbox.json +0 -108
  232. package/src/modes/theme/defaults/light-haze.json +0 -90
  233. package/src/modes/theme/defaults/light-honeycomb.json +0 -95
  234. package/src/modes/theme/defaults/light-lagoon.json +0 -91
  235. package/src/modes/theme/defaults/light-lavender.json +0 -95
  236. package/src/modes/theme/defaults/light-meadow.json +0 -91
  237. package/src/modes/theme/defaults/light-mint.json +0 -95
  238. package/src/modes/theme/defaults/light-monochrome.json +0 -101
  239. package/src/modes/theme/defaults/light-ocean.json +0 -99
  240. package/src/modes/theme/defaults/light-one.json +0 -99
  241. package/src/modes/theme/defaults/light-opal.json +0 -91
  242. package/src/modes/theme/defaults/light-orchard.json +0 -91
  243. package/src/modes/theme/defaults/light-paper.json +0 -95
  244. package/src/modes/theme/defaults/light-poimandres.json +0 -141
  245. package/src/modes/theme/defaults/light-prism.json +0 -90
  246. package/src/modes/theme/defaults/light-retro.json +0 -98
  247. package/src/modes/theme/defaults/light-sand.json +0 -95
  248. package/src/modes/theme/defaults/light-savanna.json +0 -91
  249. package/src/modes/theme/defaults/light-solarized.json +0 -102
  250. package/src/modes/theme/defaults/light-soleil.json +0 -90
  251. package/src/modes/theme/defaults/light-sunset.json +0 -99
  252. package/src/modes/theme/defaults/light-synthwave.json +0 -98
  253. package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
  254. package/src/modes/theme/defaults/light-wetland.json +0 -91
  255. package/src/modes/theme/defaults/light-zenith.json +0 -89
  256. package/src/modes/theme/defaults/limestone.json +0 -94
  257. package/src/modes/theme/defaults/mahogany.json +0 -97
  258. package/src/modes/theme/defaults/marble.json +0 -93
  259. package/src/modes/theme/defaults/obsidian.json +0 -91
  260. package/src/modes/theme/defaults/onyx.json +0 -91
  261. package/src/modes/theme/defaults/pearl.json +0 -93
  262. package/src/modes/theme/defaults/porcelain.json +0 -91
  263. package/src/modes/theme/defaults/quartz.json +0 -96
  264. package/src/modes/theme/defaults/sandstone.json +0 -95
  265. package/src/modes/theme/defaults/titanium.json +0 -90
  266. package/src/modes/theme/light.json +0 -93
@@ -4,10 +4,30 @@ import * as path from "node:path";
4
4
  import type { WorkflowHudSummary } from "../skill-state/active-state";
5
5
  import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
6
6
  import { applyGjcTmuxProfile } from "./launch-tmux";
7
+ import {
8
+ AlreadyExistsError,
9
+ appendJsonl as appendJsonlAudited,
10
+ appendText,
11
+ createJsonNoClobber,
12
+ deleteIfOwned,
13
+ removeFileAudited,
14
+ writeJsonAtomic,
15
+ writeReport,
16
+ } from "./state-writer";
17
+ import { GJC_TMUX_PROFILE_OPTION, GJC_TMUX_PROFILE_VALUE } from "./tmux-common";
7
18
 
8
19
  export type GjcTeamPhase = "starting" | "running" | "awaiting_integration" | "complete" | "failed" | "cancelled";
9
20
  export type GjcTeamTaskStatus = "pending" | "blocked" | "in_progress" | "completed" | "failed";
10
21
  export type GjcWorkerStatusState = "idle" | "working" | "blocked" | "done" | "failed" | "draining" | "unknown";
22
+ export type GjcTeamWorkerLifecycleState =
23
+ | "starting"
24
+ | "ready"
25
+ | "working"
26
+ | "draining"
27
+ | "stopped"
28
+ | "failed"
29
+ | "unknown";
30
+ export type GjcTeamShutdownMode = "graceful" | "force" | "abort";
11
31
 
12
32
  export const GJC_TEAM_DEFAULT_WORKERS = 3;
13
33
  export const GJC_TEAM_MAX_WORKERS = 20;
@@ -47,6 +67,27 @@ export interface GjcTeamTaskClaim {
47
67
  token: string;
48
68
  leased_until: string;
49
69
  }
70
+ export type GjcTeamTaskCompletionEvidenceKind = "command" | "inspection" | "artifact";
71
+ export type GjcTeamTaskCompletionEvidenceStatus = "passed" | "failed" | "not_run" | "verified" | "rejected";
72
+
73
+ export interface GjcTeamTaskCompletionEvidenceItem {
74
+ kind: GjcTeamTaskCompletionEvidenceKind;
75
+ status: GjcTeamTaskCompletionEvidenceStatus;
76
+ summary: string;
77
+ command?: string;
78
+ artifact?: string;
79
+ location?: string;
80
+ output?: string;
81
+ }
82
+
83
+ export interface GjcTeamTaskCompletionEvidence {
84
+ summary: string;
85
+ items: GjcTeamTaskCompletionEvidenceItem[];
86
+ files?: string[];
87
+ notes?: string;
88
+ recorded_by: string;
89
+ recorded_at: string;
90
+ }
50
91
 
51
92
  export interface GjcTeamTask {
52
93
  id: string;
@@ -58,9 +99,13 @@ export interface GjcTeamTask {
58
99
  assignee?: string;
59
100
  owner?: string;
60
101
  result?: string;
102
+ completion_evidence?: GjcTeamTaskCompletionEvidence;
61
103
  error?: string;
62
104
  blocked_by?: string[];
63
105
  depends_on?: string[];
106
+ lane?: string;
107
+ required_role?: string;
108
+ allowed_roles?: string[];
64
109
  version: number;
65
110
  claim?: GjcTeamTaskClaim;
66
111
  created_at: string;
@@ -121,6 +166,22 @@ export interface GjcTeamMonitorSnapshot {
121
166
  integration_by_worker: Record<string, GjcTeamWorkerIntegrationState>;
122
167
  updated_at: string;
123
168
  }
169
+ export interface GjcTeamWorkerLifecycle {
170
+ worker: string;
171
+ lifecycle_state: GjcTeamWorkerLifecycleState;
172
+ worker_status_state: GjcWorkerStatusState;
173
+ pane_id?: string;
174
+ pid?: number;
175
+ started_at?: string;
176
+ updated_at: string;
177
+ stopped_at?: string;
178
+ stop_reason?: string;
179
+ shutdown_request_id?: string;
180
+ shutdown_requested_at?: string;
181
+ shutdown_acknowledged_at?: string;
182
+ shutdown_ack_status?: string;
183
+ shutdown_mode?: GjcTeamShutdownMode;
184
+ }
124
185
 
125
186
  export type GjcTeamNotificationDeliveryState =
126
187
  | "pending"
@@ -167,9 +228,13 @@ export interface GjcTeamSnapshot {
167
228
  task_counts: Record<GjcTeamTaskStatus, number>;
168
229
  workers: GjcTeamWorker[];
169
230
  integration_by_worker?: Record<string, GjcTeamWorkerIntegrationState>;
231
+ worker_lifecycle_by_id: Record<string, GjcTeamWorkerLifecycle>;
170
232
  notification_summary: GjcTeamNotificationSummary;
171
233
  updated_at: string;
172
234
  }
235
+ export interface GjcTeamSnapshotOptions {
236
+ reconcileNotifications?: boolean;
237
+ }
173
238
 
174
239
  export interface GjcTeamStartOptions {
175
240
  workerCount: number;
@@ -189,6 +254,23 @@ export interface GjcTeamApiClaimResult {
189
254
  claim_token?: string;
190
255
  reason?: string;
191
256
  }
257
+ export type GjcTeamLivenessRecoveryReason =
258
+ | "claim_expired"
259
+ | "stale_heartbeat"
260
+ | "missing_pane"
261
+ | "worker_lifecycle_failed"
262
+ | "worker_lifecycle_stopped";
263
+
264
+ export interface GjcTeamRecoveredClaim {
265
+ task_id: string;
266
+ worker: string;
267
+ reasons: GjcTeamLivenessRecoveryReason[];
268
+ }
269
+
270
+ export interface GjcTeamLivenessRecoveryResult {
271
+ recovered_claims: GjcTeamRecoveredClaim[];
272
+ stale_workers: Record<string, GjcTeamLivenessRecoveryReason[]>;
273
+ }
192
274
 
193
275
  export interface GjcTeamMailboxMessage {
194
276
  message_id: string;
@@ -283,6 +365,19 @@ export interface GjcTeamEvent {
283
365
  message?: string;
284
366
  data?: Record<string, unknown>;
285
367
  }
368
+ export interface GjcTeamTraceEvent {
369
+ schema_version: 1;
370
+ trace_id: string;
371
+ span_id: string;
372
+ source_event_id: string;
373
+ event_type: string;
374
+ ts: string;
375
+ worker?: string;
376
+ task_id?: string;
377
+ message?: string;
378
+ evidence_refs?: string[];
379
+ data?: Record<string, unknown>;
380
+ }
286
381
  interface WorkerStatusFile {
287
382
  state: GjcWorkerStatusState;
288
383
  current_task_id?: string;
@@ -371,12 +466,15 @@ export const GJC_TEAM_API_OPERATIONS = [
371
466
  "read-config",
372
467
  "read-manifest",
373
468
  "read-worker-status",
469
+ "update-worker-status",
374
470
  "read-worker-heartbeat",
471
+ "recover-stale-claims",
375
472
  "update-worker-heartbeat",
376
473
  "write-worker-inbox",
377
474
  "write-worker-identity",
378
475
  "append-event",
379
476
  "read-events",
477
+ "read-traces",
380
478
  "await-event",
381
479
  "write-shutdown-request",
382
480
  "read-shutdown-ack",
@@ -392,9 +490,14 @@ function now(): string {
392
490
  function isEnoent(error: unknown): error is FsError {
393
491
  return typeof error === "object" && error !== null && "code" in error && (error as FsError).code === "ENOENT";
394
492
  }
395
- function isEexist(error: unknown): error is FsError {
396
- return typeof error === "object" && error !== null && "code" in error && (error as FsError).code === "EEXIST";
493
+ function stateWriterOptions(filePath: string, category: "state" | "ledger" | "report" | "prune", verb: string) {
494
+ const resolved = path.resolve(filePath);
495
+ const marker = `${path.sep}.gjc${path.sep}`;
496
+ const markerIndex = resolved.indexOf(marker);
497
+ const cwd = markerIndex >= 0 ? resolved.slice(0, markerIndex) : process.cwd();
498
+ return { cwd, audit: { category, verb, owner: "gjc-runtime" as const } };
397
499
  }
500
+
398
501
  function sanitizeName(value: string): string {
399
502
  const sanitized = value
400
503
  .toLowerCase()
@@ -432,9 +535,6 @@ function safePathSegment(kind: string, value: string): string {
432
535
  function taskPath(dir: string, taskId: string): string {
433
536
  return path.join(dir, "tasks", `${safePathSegment("task_id", taskId)}.json`);
434
537
  }
435
- function taskEvidencePath(dir: string, taskId: string): string {
436
- return path.join(dir, "evidence", "tasks", `${safePathSegment("task_id", taskId)}.json`);
437
- }
438
538
  function mailboxPath(dir: string, worker: string): string {
439
539
  return path.join(dir, "mailbox", `${safePathSegment("worker_id", worker)}.json`);
440
540
  }
@@ -450,6 +550,17 @@ function notificationPath(dir: string, notificationId: string): string {
450
550
  function workerDir(dir: string, worker: string): string {
451
551
  return path.join(dir, "workers", safePathSegment("worker_id", worker));
452
552
  }
553
+ function workerLifecyclePath(dir: string, worker: string): string {
554
+ return path.join(workerDir(dir, worker), "lifecycle.json");
555
+ }
556
+
557
+ function tracePath(dir: string): string {
558
+ return path.join(dir, "trace.jsonl");
559
+ }
560
+
561
+ function traceErrorPath(dir: string): string {
562
+ return path.join(dir, "trace-errors.jsonl");
563
+ }
453
564
  function isSafeId(value: string): boolean {
454
565
  return (
455
566
  /^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(value) &&
@@ -469,6 +580,12 @@ function assertKnownWorker(config: GjcTeamConfig, worker: string, allowLeader =
469
580
  if (allowLeader && isLeaderRecipient(worker)) return;
470
581
  if (!config.workers.some(candidate => candidate.id === worker)) throw new Error(`unknown_worker:${worker}`);
471
582
  }
583
+ function findKnownWorker(config: GjcTeamConfig, worker: string): GjcTeamWorker {
584
+ assertKnownWorker(config, worker);
585
+ const found = config.workers.find(candidate => candidate.id === worker);
586
+ if (!found) throw new Error(`unknown_worker:${worker}`);
587
+ return found;
588
+ }
472
589
  function assertKnownParticipant(config: GjcTeamConfig, worker: string): void {
473
590
  assertKnownWorker(config, worker, true);
474
591
  }
@@ -503,33 +620,171 @@ async function readJsonFile<T>(filePath: string): Promise<T | null> {
503
620
  throw error;
504
621
  }
505
622
  }
623
+ function stateCategoryForJsonPath(filePath: string): "state" | "ledger" {
624
+ return filePath.endsWith(".jsonl") || filePath.includes(`${path.sep}telemetry${path.sep}`) ? "ledger" : "state";
625
+ }
626
+
506
627
  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);
628
+ await writeJsonAtomic(filePath, value, stateWriterOptions(filePath, stateCategoryForJsonPath(filePath), "write"));
511
629
  }
512
630
  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
631
  try {
516
- handle = await fs.open(filePath, "wx");
517
- await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf-8");
632
+ await createJsonNoClobber(
633
+ filePath,
634
+ value,
635
+ stateWriterOptions(filePath, stateCategoryForJsonPath(filePath), "create"),
636
+ );
518
637
  return true;
519
638
  } catch (error) {
520
- if (isEexist(error)) return false;
639
+ if (error instanceof AlreadyExistsError) return false;
521
640
  throw error;
522
- } finally {
523
- await handle?.close();
524
641
  }
525
642
  }
526
643
  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");
644
+ await appendJsonlAudited(filePath, value, stateWriterOptions(filePath, "ledger", "append"));
645
+ }
646
+ function traceIdForTeam(dir: string): string {
647
+ return `trace-${stableHash(path.basename(dir))}`;
648
+ }
649
+
650
+ function evidenceRefsForEvent(event: GjcTeamEvent): string[] | undefined {
651
+ const refs: string[] = [];
652
+ if (event.task_id && event.type === "task_transitioned" && event.data && "completion_evidence" in event.data)
653
+ refs.push(`task:${event.task_id}:completion_evidence`);
654
+ if (event.task_id && event.type === "task_claim_recovered") refs.push(`task:${event.task_id}:claim_recovery`);
655
+ if (event.worker && event.type.startsWith("worker_")) refs.push(`worker:${event.worker}`);
656
+ return refs.length > 0 ? refs : undefined;
657
+ }
658
+ function pickString(value: unknown): string | undefined {
659
+ return typeof value === "string" ? value : undefined;
660
+ }
661
+ function pickNumber(value: unknown): number | undefined {
662
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
663
+ }
664
+ function pickBoolean(value: unknown): boolean | undefined {
665
+ return typeof value === "boolean" ? value : undefined;
666
+ }
667
+ function pickStringArray(value: unknown): string[] | undefined {
668
+ return Array.isArray(value) && value.every(item => typeof item === "string") ? value : undefined;
669
+ }
670
+ function setIfDefined(record: Record<string, unknown>, key: string, value: unknown): void {
671
+ if (value !== undefined) record[key] = value;
672
+ }
673
+ function messageBodyTraceProjection(body: string | undefined): Record<string, unknown> {
674
+ if (body === undefined) return {};
675
+ return {
676
+ body_byte_length: Buffer.byteLength(body, "utf8"),
677
+ body_sha256: createHash("sha256").update(body).digest("hex"),
678
+ };
679
+ }
680
+ function traceDataForEvent(event: GjcTeamEvent): Record<string, unknown> | undefined {
681
+ const source = event.data ?? {};
682
+ const data: Record<string, unknown> = {};
683
+ switch (event.type) {
684
+ case "message_sent": {
685
+ setIfDefined(data, "to_worker", pickString(source.to_worker));
686
+ setIfDefined(data, "message_id", pickString(source.message_id));
687
+ Object.assign(data, messageBodyTraceProjection(pickString(event.message)));
688
+ break;
689
+ }
690
+ case "message_acknowledged":
691
+ case "message_notified": {
692
+ setIfDefined(data, "message_id", pickString(event.message));
693
+ break;
694
+ }
695
+ case "team_started": {
696
+ setIfDefined(data, "worker_count", pickNumber(source.worker_count));
697
+ setIfDefined(data, "agent_type", pickString(source.agent_type));
698
+ setIfDefined(data, "workspace_mode", pickString(source.workspace_mode));
699
+ setIfDefined(data, "dry_run", pickBoolean(source.dry_run));
700
+ break;
701
+ }
702
+ case "task_claim_recovered": {
703
+ setIfDefined(data, "reasons", pickStringArray(source.reasons));
704
+ break;
705
+ }
706
+ case "task_transitioned": {
707
+ setIfDefined(data, "status", pickString(source.status));
708
+ const evidence = source.completion_evidence;
709
+ if (typeof evidence === "object" && evidence !== null) {
710
+ const evidenceRecord = evidence as Record<string, unknown>;
711
+ data.completion_evidence = {
712
+ recorded_by: pickString(evidenceRecord.recorded_by),
713
+ item_count: pickNumber(evidenceRecord.item_count),
714
+ verified_item_count: pickNumber(evidenceRecord.verified_item_count),
715
+ };
716
+ }
717
+ break;
718
+ }
719
+ case "worker_integration_attempt_requested": {
720
+ setIfDefined(data, "worker_name", pickString(source.worker_name));
721
+ setIfDefined(data, "worker_head", pickString(source.worker_head));
722
+ setIfDefined(data, "status", pickString(source.status));
723
+ if (Array.isArray(source.files)) data.file_count = source.files.length;
724
+ break;
725
+ }
726
+ case "worker_lifecycle_nudge": {
727
+ setIfDefined(data, "condition", pickString(source.condition));
728
+ setIfDefined(data, "severity", pickString(source.severity));
729
+ setIfDefined(data, "fingerprint", pickString(source.fingerprint));
730
+ setIfDefined(data, "auto_action_taken", pickBoolean(source.auto_action_taken));
731
+ break;
732
+ }
733
+ case "team_shutdown": {
734
+ setIfDefined(data, "phase", pickString(source.phase));
735
+ setIfDefined(data, "shutdown_request_id", pickString(source.shutdown_request_id));
736
+ setIfDefined(data, "graceful_shutdown_complete", pickBoolean(source.graceful_shutdown_complete));
737
+ if (Array.isArray(source.evidence_failures)) data.evidence_failure_count = source.evidence_failures.length;
738
+ break;
739
+ }
740
+ case "worker_status_updated": {
741
+ setIfDefined(data, "status", pickString(source.status));
742
+ setIfDefined(data, "current_task_id", pickString(source.current_task_id));
743
+ break;
744
+ }
745
+ case "worker_shutdown_requested": {
746
+ setIfDefined(data, "requested_by", pickString(source.requested_by));
747
+ setIfDefined(data, "request_id", pickString(source.request_id));
748
+ setIfDefined(data, "mode", pickString(source.mode));
749
+ break;
750
+ }
751
+ }
752
+ return Object.keys(data).length > 0 ? data : undefined;
753
+ }
754
+
755
+ async function appendTraceForEvent(dir: string, event: GjcTeamEvent): Promise<void> {
756
+ const evidenceRefs = evidenceRefsForEvent(event);
757
+ const traceData = traceDataForEvent(event);
758
+ const trace: GjcTeamTraceEvent = {
759
+ schema_version: 1,
760
+ trace_id: traceIdForTeam(dir),
761
+ span_id: `span-${stableHash(event.event_id)}`,
762
+ source_event_id: event.event_id,
763
+ event_type: event.type,
764
+ ts: event.ts,
765
+ ...(event.worker ? { worker: event.worker } : {}),
766
+ ...(event.task_id ? { task_id: event.task_id } : {}),
767
+ ...(traceData ? { data: traceData } : {}),
768
+ ...(evidenceRefs ? { evidence_refs: evidenceRefs } : {}),
769
+ };
770
+ try {
771
+ await appendJsonl(tracePath(dir), trace);
772
+ } catch (error) {
773
+ try {
774
+ await appendJsonl(traceErrorPath(dir), {
775
+ ts: now(),
776
+ source_event_id: event.event_id,
777
+ error: error instanceof Error ? error.message : String(error),
778
+ });
779
+ } catch {
780
+ // Trace append failure must not break legacy events.jsonl compatibility.
781
+ }
782
+ }
529
783
  }
530
784
  async function appendEvent(dir: string, event: Omit<GjcTeamEvent, "ts" | "event_id">): Promise<GjcTeamEvent> {
531
785
  const full = { event_id: `evt-${Date.now()}-${Math.random().toString(16).slice(2)}`, ts: now(), ...event };
532
786
  await appendJsonl(path.join(dir, "events.jsonl"), full);
787
+ await appendTraceForEvent(dir, full);
533
788
  return full;
534
789
  }
535
790
  async function appendTelemetry(
@@ -562,6 +817,325 @@ async function readPhase(dir: string): Promise<GjcTeamPhase> {
562
817
  async function writePhase(dir: string, phase: GjcTeamPhase): Promise<void> {
563
818
  await writeJsonFile(path.join(dir, "phase.json"), { current_phase: phase, updated_at: now() });
564
819
  }
820
+ function isGjcWorkerStatusState(value: string): value is GjcWorkerStatusState {
821
+ return ["idle", "working", "blocked", "done", "failed", "draining", "unknown"].includes(value);
822
+ }
823
+
824
+ function parseGjcWorkerStatusState(value: unknown): GjcWorkerStatusState {
825
+ return typeof value === "string" && isGjcWorkerStatusState(value) ? value : "unknown";
826
+ }
827
+ function parseRequiredGjcWorkerStatusState(value: unknown): GjcWorkerStatusState {
828
+ const raw = typeof value === "string" ? value.trim() : "";
829
+ if (isGjcWorkerStatusState(raw)) return raw;
830
+ throw new Error(`invalid_worker_status:${raw}`);
831
+ }
832
+
833
+ function lifecycleStateForWorkerStatus(status: GjcWorkerStatusState): GjcTeamWorkerLifecycleState {
834
+ switch (status) {
835
+ case "working":
836
+ return "working";
837
+ case "draining":
838
+ return "draining";
839
+ case "failed":
840
+ return "failed";
841
+ case "unknown":
842
+ return "unknown";
843
+ case "idle":
844
+ case "blocked":
845
+ case "done":
846
+ return "ready";
847
+ }
848
+ }
849
+
850
+ function parseGjcTeamShutdownMode(value: unknown): GjcTeamShutdownMode {
851
+ const raw = typeof value === "string" ? value.trim() : "graceful";
852
+ if (raw === "graceful" || raw === "force" || raw === "abort") return raw;
853
+ throw new Error(`invalid_shutdown_mode:${raw}`);
854
+ }
855
+
856
+ function isGjcTeamWorkerLifecycleState(value: string): value is GjcTeamWorkerLifecycleState {
857
+ return ["starting", "ready", "working", "draining", "stopped", "failed", "unknown"].includes(value);
858
+ }
859
+
860
+ function parseGjcTeamWorkerLifecycleState(value: unknown): GjcTeamWorkerLifecycleState {
861
+ return typeof value === "string" && isGjcTeamWorkerLifecycleState(value) ? value : "unknown";
862
+ }
863
+
864
+ async function readWorkerStatusFile(dir: string, worker: string): Promise<WorkerStatusFile> {
865
+ return (
866
+ (await readJsonFile<WorkerStatusFile>(path.join(workerDir(dir, worker), "status.json"))) ?? {
867
+ state: "unknown",
868
+ updated_at: now(),
869
+ }
870
+ );
871
+ }
872
+
873
+ async function readWorkerLifecycleRecord(dir: string, worker: GjcTeamWorker): Promise<GjcTeamWorkerLifecycle> {
874
+ const workerStatus = await readWorkerStatusFile(dir, worker.id);
875
+ const heartbeat = await readJsonFile<WorkerHeartbeatFile>(path.join(workerDir(dir, worker.id), "heartbeat.json"));
876
+ const rawLifecycle = await readJsonFile<Partial<GjcTeamWorkerLifecycle>>(workerLifecyclePath(dir, worker.id));
877
+ const shutdownAck = await readJsonFile<Record<string, unknown>>(
878
+ path.join(workerDir(dir, worker.id), "shutdown-ack.json"),
879
+ );
880
+ const lifecycle: GjcTeamWorkerLifecycle = {
881
+ worker: worker.id,
882
+ lifecycle_state: parseGjcTeamWorkerLifecycleState(rawLifecycle?.lifecycle_state),
883
+ worker_status_state: parseGjcWorkerStatusState(workerStatus.state),
884
+ pane_id: worker.pane_id ?? rawLifecycle?.pane_id,
885
+ updated_at: rawLifecycle?.updated_at ?? workerStatus.updated_at ?? now(),
886
+ };
887
+ if (typeof rawLifecycle?.pid === "number") lifecycle.pid = rawLifecycle.pid;
888
+ else if (typeof heartbeat?.pid === "number") lifecycle.pid = heartbeat.pid;
889
+ if (rawLifecycle?.started_at) lifecycle.started_at = rawLifecycle.started_at;
890
+ if (rawLifecycle?.stopped_at) lifecycle.stopped_at = rawLifecycle.stopped_at;
891
+ if (rawLifecycle?.stop_reason) lifecycle.stop_reason = rawLifecycle.stop_reason;
892
+ if (rawLifecycle?.shutdown_request_id) lifecycle.shutdown_request_id = rawLifecycle.shutdown_request_id;
893
+ if (rawLifecycle?.shutdown_requested_at) lifecycle.shutdown_requested_at = rawLifecycle.shutdown_requested_at;
894
+ if (
895
+ rawLifecycle?.shutdown_mode === "graceful" ||
896
+ rawLifecycle?.shutdown_mode === "force" ||
897
+ rawLifecycle?.shutdown_mode === "abort"
898
+ )
899
+ lifecycle.shutdown_mode = rawLifecycle.shutdown_mode;
900
+ if (typeof shutdownAck?.acknowledged_at === "string")
901
+ lifecycle.shutdown_acknowledged_at = shutdownAck.acknowledged_at;
902
+ if (typeof shutdownAck?.status === "string") lifecycle.shutdown_ack_status = shutdownAck.status;
903
+ return lifecycle;
904
+ }
905
+
906
+ async function readWorkerLifecycleById(
907
+ dir: string,
908
+ config: GjcTeamConfig,
909
+ ): Promise<Record<string, GjcTeamWorkerLifecycle>> {
910
+ const entries = await Promise.all(config.workers.map(worker => readWorkerLifecycleRecord(dir, worker)));
911
+ return Object.fromEntries(entries.map(entry => [entry.worker, entry]));
912
+ }
913
+
914
+ async function writeWorkerLifecycleRecord(
915
+ dir: string,
916
+ worker: GjcTeamWorker,
917
+ lifecycleState: GjcTeamWorkerLifecycleState,
918
+ updates: Partial<GjcTeamWorkerLifecycle> = {},
919
+ ): Promise<GjcTeamWorkerLifecycle> {
920
+ const current = await readWorkerLifecycleRecord(dir, worker);
921
+ const next: GjcTeamWorkerLifecycle = {
922
+ ...current,
923
+ ...updates,
924
+ worker: worker.id,
925
+ lifecycle_state: lifecycleState,
926
+ worker_status_state: current.worker_status_state,
927
+ pane_id: updates.pane_id ?? worker.pane_id ?? current.pane_id,
928
+ updated_at: now(),
929
+ };
930
+ await writeJsonFile(workerLifecyclePath(dir, worker.id), next);
931
+ return next;
932
+ }
933
+
934
+ async function writeWorkerLifecycleForConfig(
935
+ dir: string,
936
+ config: GjcTeamConfig,
937
+ lifecycleState: GjcTeamWorkerLifecycleState,
938
+ updatesFor: (worker: GjcTeamWorker) => Partial<GjcTeamWorkerLifecycle> = () => ({}),
939
+ ): Promise<Record<string, GjcTeamWorkerLifecycle>> {
940
+ const entries = await Promise.all(
941
+ config.workers.map(worker => writeWorkerLifecycleRecord(dir, worker, lifecycleState, updatesFor(worker))),
942
+ );
943
+ return Object.fromEntries(entries.map(entry => [entry.worker, entry]));
944
+ }
945
+
946
+ function teamModeStatePath(): string {
947
+ return path.join(".gjc", "state", "team-state.json");
948
+ }
949
+
950
+ export async function persistGjcTeamModeStateSummary(snapshot: GjcTeamSnapshot, cwd = process.cwd()): Promise<void> {
951
+ const active = snapshot.phase !== "complete" && snapshot.phase !== "cancelled";
952
+ const updatedAt = now();
953
+ await writeJsonAtomic(
954
+ teamModeStatePath(),
955
+ {
956
+ skill: "team",
957
+ version: 1,
958
+ active,
959
+ current_phase: snapshot.phase,
960
+ team_name: snapshot.team_name,
961
+ task_counts: snapshot.task_counts,
962
+ updated_at: updatedAt,
963
+ },
964
+ {
965
+ cwd,
966
+ receipt: {
967
+ cwd,
968
+ skill: "team",
969
+ owner: "gjc-runtime",
970
+ command: "gjc team sync-team-summary",
971
+ nowIso: updatedAt,
972
+ },
973
+ audit: { category: "state", verb: "sync-team-summary", owner: "gjc-runtime", skill: "team" },
974
+ },
975
+ );
976
+ }
977
+
978
+ function appendLivenessRecoveryReason(
979
+ reasons: GjcTeamLivenessRecoveryReason[],
980
+ reason: GjcTeamLivenessRecoveryReason,
981
+ ): void {
982
+ if (!reasons.includes(reason)) reasons.push(reason);
983
+ }
984
+
985
+ function isPastTimestamp(value: string | undefined): boolean {
986
+ if (!value) return false;
987
+ const timestamp = Date.parse(value);
988
+ return Number.isFinite(timestamp) && timestamp <= Date.now();
989
+ }
990
+
991
+ function readClaimRecord(value: unknown): GjcTeamTaskClaim | undefined {
992
+ if (!isRecord(value)) return undefined;
993
+ const owner = typeof value.owner === "string" ? value.owner : "";
994
+ const token = typeof value.token === "string" ? value.token : "";
995
+ const leasedUntil = typeof value.leased_until === "string" ? value.leased_until : "";
996
+ if (!owner || !token || !leasedUntil) return undefined;
997
+ return { owner, token, leased_until: leasedUntil };
998
+ }
999
+
1000
+ function isWorkerHeartbeatStale(
1001
+ worker: GjcTeamWorker,
1002
+ heartbeat: WorkerHeartbeatFile | null,
1003
+ env: NodeJS.ProcessEnv,
1004
+ ): boolean {
1005
+ const thresholdMs = parseDurationEnv(env, "GJC_TEAM_HEARTBEAT_STALE_MS", 120_000);
1006
+ if (thresholdMs <= 0) return false;
1007
+ const heartbeatAt = Date.parse(heartbeat?.last_turn_at ?? worker.last_heartbeat);
1008
+ return Number.isFinite(heartbeatAt) && Date.now() - heartbeatAt >= thresholdMs;
1009
+ }
1010
+
1011
+ async function detectGjcTeamWorkerLivenessReasons(
1012
+ dir: string,
1013
+ config: GjcTeamConfig,
1014
+ worker: GjcTeamWorker,
1015
+ env: NodeJS.ProcessEnv,
1016
+ ): Promise<GjcTeamLivenessRecoveryReason[]> {
1017
+ const reasons: GjcTeamLivenessRecoveryReason[] = [];
1018
+ const lifecycle = await readWorkerLifecycleRecord(dir, worker);
1019
+ const heartbeat = await readJsonFile<WorkerHeartbeatFile>(path.join(workerDir(dir, worker.id), "heartbeat.json"));
1020
+ if (lifecycle.lifecycle_state === "failed") appendLivenessRecoveryReason(reasons, "worker_lifecycle_failed");
1021
+ if (lifecycle.lifecycle_state === "stopped") appendLivenessRecoveryReason(reasons, "worker_lifecycle_stopped");
1022
+ if (isWorkerHeartbeatStale(worker, heartbeat, env)) appendLivenessRecoveryReason(reasons, "stale_heartbeat");
1023
+ if (!config.dry_run && (!worker.pane_id?.startsWith("%") || !paneBelongsToTeamTarget(config, worker.pane_id)))
1024
+ appendLivenessRecoveryReason(reasons, "missing_pane");
1025
+ return reasons;
1026
+ }
1027
+
1028
+ async function reconcileGjcTeamStaleClaims(
1029
+ teamName: string,
1030
+ dir: string,
1031
+ config: GjcTeamConfig,
1032
+ env: NodeJS.ProcessEnv,
1033
+ ): Promise<GjcTeamLivenessRecoveryResult> {
1034
+ const staleWorkers: Record<string, GjcTeamLivenessRecoveryReason[]> = {};
1035
+ for (const worker of config.workers) {
1036
+ const reasons = await detectGjcTeamWorkerLivenessReasons(dir, config, worker, env);
1037
+ if (reasons.length === 0) continue;
1038
+ staleWorkers[worker.id] = reasons;
1039
+ if (reasons.includes("missing_pane") && reasons.includes("worker_lifecycle_stopped") === false) {
1040
+ await writeWorkerLifecycleRecord(dir, worker, "failed", { stop_reason: "pane_missing" });
1041
+ }
1042
+ }
1043
+
1044
+ const recoveredClaims: GjcTeamRecoveredClaim[] = [];
1045
+ for (const task of await readTasks(dir)) {
1046
+ if (task.status === "completed" || task.status === "failed") continue;
1047
+ const claimPath = path.join(dir, "claims", `${task.id}.json`);
1048
+ const diskClaim = readClaimRecord(await readJsonFile<unknown>(claimPath));
1049
+ const claim = task.claim ?? diskClaim;
1050
+ if (!claim) continue;
1051
+
1052
+ const reasons = [...(staleWorkers[claim.owner] ?? [])];
1053
+ if (isPastTimestamp(claim.leased_until)) appendLivenessRecoveryReason(reasons, "claim_expired");
1054
+ if (reasons.length === 0) continue;
1055
+
1056
+ await fs.rm(claimPath, { force: true });
1057
+ recoveredClaims.push({ task_id: task.id, worker: claim.owner, reasons });
1058
+ if (task.status !== "in_progress") {
1059
+ await appendEvent(dir, {
1060
+ type: "task_claim_recovered",
1061
+ task_id: task.id,
1062
+ worker: claim.owner,
1063
+ message: "Removed stale task claim file",
1064
+ data: { reasons },
1065
+ });
1066
+ continue;
1067
+ }
1068
+
1069
+ const recoveredTask = normalizeTask({
1070
+ ...task,
1071
+ status: "pending",
1072
+ assignee: undefined,
1073
+ claim: undefined,
1074
+ version: task.version + 1,
1075
+ updated_at: now(),
1076
+ });
1077
+ await writeTask(dir, recoveredTask);
1078
+ await appendEvent(dir, {
1079
+ type: "task_claim_recovered",
1080
+ task_id: task.id,
1081
+ worker: claim.owner,
1082
+ message: "Recovered task from stale worker claim",
1083
+ data: { reasons },
1084
+ });
1085
+ }
1086
+
1087
+ if (recoveredClaims.length > 0)
1088
+ await appendTelemetry(dir, {
1089
+ type: "team_liveness_recovery",
1090
+ message: `Recovered ${recoveredClaims.length} stale team task claim(s)`,
1091
+ data: { team_name: teamName, recovered_claims: recoveredClaims },
1092
+ });
1093
+
1094
+ return { recovered_claims: recoveredClaims, stale_workers: staleWorkers };
1095
+ }
1096
+
1097
+ export async function recoverGjcTeamStaleClaims(
1098
+ teamName: string,
1099
+ cwd = process.cwd(),
1100
+ env: NodeJS.ProcessEnv = process.env,
1101
+ ): Promise<GjcTeamLivenessRecoveryResult> {
1102
+ const dir = await findTeamDir(teamName, cwd, env);
1103
+ const config = await readConfig(dir);
1104
+ return reconcileGjcTeamStaleClaims(teamName, dir, config, env);
1105
+ }
1106
+ function normalizeOptionalTaskString(value: unknown): string | undefined {
1107
+ if (typeof value !== "string") return undefined;
1108
+ const trimmed = value.trim();
1109
+ return trimmed || undefined;
1110
+ }
1111
+
1112
+ function normalizeOptionalTaskStringArray(value: unknown): string[] | undefined {
1113
+ if (!Array.isArray(value)) return undefined;
1114
+ const items = Array.from(
1115
+ new Set(value.map(item => (typeof item === "string" ? item.trim() : "")).filter(item => item.length > 0)),
1116
+ ).sort();
1117
+ return items.length > 0 ? items : undefined;
1118
+ }
1119
+ type GjcTeamTaskMetadataInput = Partial<
1120
+ Pick<GjcTeamTask, "owner" | "lane" | "required_role" | "allowed_roles" | "depends_on" | "blocked_by">
1121
+ >;
1122
+
1123
+ function taskMetadataFromInput(input: Record<string, unknown>, includeOwner = false): GjcTeamTaskMetadataInput {
1124
+ const metadata: GjcTeamTaskMetadataInput = {};
1125
+ const owner = normalizeOptionalTaskString(input.owner);
1126
+ const lane = normalizeOptionalTaskString(input.lane);
1127
+ const requiredRole = normalizeOptionalTaskString(input.required_role ?? input.requiredRole);
1128
+ const allowedRoles = normalizeOptionalTaskStringArray(input.allowed_roles ?? input.allowedRoles);
1129
+ const dependsOn = normalizeOptionalTaskStringArray(input.depends_on ?? input.dependsOn);
1130
+ const blockedBy = normalizeOptionalTaskStringArray(input.blocked_by ?? input.blockedBy);
1131
+ if (includeOwner && owner) metadata.owner = owner;
1132
+ if (lane) metadata.lane = lane;
1133
+ if (requiredRole) metadata.required_role = requiredRole;
1134
+ if (allowedRoles) metadata.allowed_roles = allowedRoles;
1135
+ if (dependsOn) metadata.depends_on = dependsOn;
1136
+ if (blockedBy) metadata.blocked_by = blockedBy;
1137
+ return metadata;
1138
+ }
565
1139
 
566
1140
  function normalizeTask(raw: GjcTeamTask): GjcTeamTask {
567
1141
  const status = raw.status === ("complete" as GjcTeamTaskStatus) ? "completed" : raw.status;
@@ -573,6 +1147,9 @@ function normalizeTask(raw: GjcTeamTask): GjcTeamTask {
573
1147
  title: raw.title ?? raw.subject,
574
1148
  objective: raw.objective ?? raw.description,
575
1149
  version: raw.version ?? 1,
1150
+ lane: normalizeOptionalTaskString(raw.lane),
1151
+ required_role: normalizeOptionalTaskString(raw.required_role),
1152
+ allowed_roles: normalizeOptionalTaskStringArray(raw.allowed_roles),
576
1153
  };
577
1154
  }
578
1155
 
@@ -616,13 +1193,189 @@ async function resolveGjcTeamSnapshotPhase(
616
1193
  monitor: GjcTeamMonitorSnapshot | null,
617
1194
  ): Promise<GjcTeamPhase> {
618
1195
  if (storedPhase !== "running") return storedPhase;
619
- if (tasks.length === 0 || !tasks.every(task => task.status === "completed")) return storedPhase;
1196
+ if (tasks.length === 0 || !tasks.every(isGjcTeamTaskCompletionVerified)) return storedPhase;
620
1197
  return (await hasPendingGjcTeamIntegration(dir, config, monitor)) ? "awaiting_integration" : storedPhase;
621
1198
  }
622
1199
 
623
1200
  function isRecord(value: unknown): value is Record<string, unknown> {
624
1201
  return typeof value === "object" && value != null;
625
1202
  }
1203
+ const GJC_TEAM_COMPLETION_EVIDENCE_SUMMARY_MAX = 4_000;
1204
+ const GJC_TEAM_COMPLETION_EVIDENCE_OUTPUT_MAX = 8_000;
1205
+ const GJC_TEAM_COMMAND_EVIDENCE_STATUSES = new Set<GjcTeamTaskCompletionEvidenceStatus>([
1206
+ "passed",
1207
+ "failed",
1208
+ "not_run",
1209
+ ]);
1210
+ const GJC_TEAM_VERIFICATION_EVIDENCE_STATUSES = new Set<GjcTeamTaskCompletionEvidenceStatus>(["verified", "rejected"]);
1211
+
1212
+ function completionEvidenceError(taskId: string, field: string): Error {
1213
+ return new Error(`invalid_completion_evidence:${taskId}:${field}`);
1214
+ }
1215
+
1216
+ function trimRequiredCompletionEvidenceString(
1217
+ taskId: string,
1218
+ field: string,
1219
+ value: unknown,
1220
+ maxLength = GJC_TEAM_COMPLETION_EVIDENCE_SUMMARY_MAX,
1221
+ ): string {
1222
+ if (typeof value !== "string") throw completionEvidenceError(taskId, field);
1223
+ const trimmed = value.trim();
1224
+ if (!trimmed || trimmed.length > maxLength) throw completionEvidenceError(taskId, field);
1225
+ return trimmed;
1226
+ }
1227
+
1228
+ function trimOptionalCompletionEvidenceString(
1229
+ taskId: string,
1230
+ field: string,
1231
+ value: unknown,
1232
+ maxLength = GJC_TEAM_COMPLETION_EVIDENCE_OUTPUT_MAX,
1233
+ ): string | undefined {
1234
+ if (value == null) return undefined;
1235
+ if (typeof value !== "string") throw completionEvidenceError(taskId, field);
1236
+ const trimmed = value.trim();
1237
+ if (!trimmed) return undefined;
1238
+ if (trimmed.length > maxLength) throw completionEvidenceError(taskId, field);
1239
+ return trimmed;
1240
+ }
1241
+
1242
+ function normalizeGjcTeamCompletionEvidenceStatus(
1243
+ taskId: string,
1244
+ kind: GjcTeamTaskCompletionEvidenceKind,
1245
+ value: unknown,
1246
+ ): GjcTeamTaskCompletionEvidenceStatus {
1247
+ const status = trimRequiredCompletionEvidenceString(taskId, "items.status", value);
1248
+ const allowed = kind === "command" ? GJC_TEAM_COMMAND_EVIDENCE_STATUSES : GJC_TEAM_VERIFICATION_EVIDENCE_STATUSES;
1249
+ if (!allowed.has(status as GjcTeamTaskCompletionEvidenceStatus))
1250
+ throw completionEvidenceError(taskId, "items.status");
1251
+ return status as GjcTeamTaskCompletionEvidenceStatus;
1252
+ }
1253
+
1254
+ function normalizeGjcTeamCompletionEvidenceItem(taskId: string, value: unknown): GjcTeamTaskCompletionEvidenceItem {
1255
+ if (!isRecord(value) || Array.isArray(value)) throw completionEvidenceError(taskId, "items");
1256
+ const kind = trimRequiredCompletionEvidenceString(taskId, "items.kind", value.kind);
1257
+ if (kind !== "command" && kind !== "inspection" && kind !== "artifact")
1258
+ throw completionEvidenceError(taskId, "items.kind");
1259
+ const status = normalizeGjcTeamCompletionEvidenceStatus(taskId, kind, value.status);
1260
+ const item: GjcTeamTaskCompletionEvidenceItem = {
1261
+ kind,
1262
+ status,
1263
+ summary: trimRequiredCompletionEvidenceString(taskId, "items.summary", value.summary),
1264
+ };
1265
+ const command = trimOptionalCompletionEvidenceString(taskId, "items.command", value.command);
1266
+ const artifact = trimOptionalCompletionEvidenceString(taskId, "items.artifact", value.artifact);
1267
+ const location = trimOptionalCompletionEvidenceString(taskId, "items.location", value.location);
1268
+ const output = trimOptionalCompletionEvidenceString(taskId, "items.output", value.output);
1269
+ if (kind === "command" && !command) throw completionEvidenceError(taskId, "items.command");
1270
+ if (command) item.command = command;
1271
+ if (artifact) item.artifact = artifact;
1272
+ if (location) item.location = location;
1273
+ if (output) item.output = output;
1274
+ return item;
1275
+ }
1276
+
1277
+ function normalizeGjcTeamCompletionEvidenceFiles(taskId: string, value: unknown): string[] | undefined {
1278
+ if (value == null) return undefined;
1279
+ if (!Array.isArray(value)) throw completionEvidenceError(taskId, "files");
1280
+ const files = new Set<string>();
1281
+ for (const entry of value) {
1282
+ if (typeof entry !== "string") throw completionEvidenceError(taskId, "files");
1283
+ const filePath = entry.trim().replace(/\\/g, "/");
1284
+ if (!filePath || filePath.includes("\0") || path.isAbsolute(filePath) || filePath.split("/").includes("..")) {
1285
+ throw completionEvidenceError(taskId, "files");
1286
+ }
1287
+ files.add(filePath);
1288
+ }
1289
+ return files.size > 0 ? [...files].sort() : undefined;
1290
+ }
1291
+
1292
+ function isGjcTeamCompletionEvidenceItemVerified(item: GjcTeamTaskCompletionEvidenceItem): boolean {
1293
+ return (
1294
+ (item.kind === "command" && item.status === "passed") ||
1295
+ ((item.kind === "inspection" || item.kind === "artifact") && item.status === "verified")
1296
+ );
1297
+ }
1298
+
1299
+ function normalizeGjcTeamTaskCompletionEvidence(
1300
+ taskId: string,
1301
+ owner: string,
1302
+ input: unknown,
1303
+ recordedAt = now(),
1304
+ ): GjcTeamTaskCompletionEvidence {
1305
+ if (!isRecord(input) || Array.isArray(input)) throw new Error(`completion_evidence_required:${taskId}`);
1306
+ const itemsValue = input.items;
1307
+ if (!Array.isArray(itemsValue) || itemsValue.length === 0) throw completionEvidenceError(taskId, "items");
1308
+ const items = itemsValue.map(item => normalizeGjcTeamCompletionEvidenceItem(taskId, item));
1309
+ if (!items.some(isGjcTeamCompletionEvidenceItemVerified))
1310
+ throw new Error(`completion_evidence_no_verified_item:${taskId}`);
1311
+ const evidence: GjcTeamTaskCompletionEvidence = {
1312
+ summary: trimRequiredCompletionEvidenceString(taskId, "summary", input.summary),
1313
+ items,
1314
+ recorded_by: owner,
1315
+ recorded_at: recordedAt,
1316
+ };
1317
+ const files = normalizeGjcTeamCompletionEvidenceFiles(taskId, input.files);
1318
+ const notes = trimOptionalCompletionEvidenceString(taskId, "notes", input.notes);
1319
+ if (files) evidence.files = files;
1320
+ if (notes) evidence.notes = notes;
1321
+ return evidence;
1322
+ }
1323
+
1324
+ function getGjcTeamTaskCompletionEvidenceFailure(task: GjcTeamTask): string | null {
1325
+ if (task.status !== "completed") return `task_not_completed:${task.id}`;
1326
+ const evidence = task.completion_evidence;
1327
+ if (!isRecord(evidence) || Array.isArray(evidence)) return `completion_evidence_required:${task.id}`;
1328
+ if (typeof evidence.recorded_by !== "string" || evidence.recorded_by.trim().length === 0)
1329
+ return `invalid_completion_evidence:${task.id}:recorded_by`;
1330
+ if (typeof evidence.recorded_at !== "string" || evidence.recorded_at.trim().length === 0)
1331
+ return `invalid_completion_evidence:${task.id}:recorded_at`;
1332
+ try {
1333
+ normalizeGjcTeamTaskCompletionEvidence(task.id, evidence.recorded_by.trim(), evidence, evidence.recorded_at);
1334
+ return null;
1335
+ } catch (error) {
1336
+ return error instanceof Error ? error.message : `invalid_completion_evidence:${task.id}:unknown`;
1337
+ }
1338
+ }
1339
+
1340
+ function isGjcTeamTaskCompletionVerified(task: GjcTeamTask): boolean {
1341
+ return getGjcTeamTaskCompletionEvidenceFailure(task) == null;
1342
+ }
1343
+ function roleValuesForWorker(worker: GjcTeamWorker): Set<string> {
1344
+ return new Set([worker.role, worker.agent_type].map(value => value.trim()).filter(value => value.length > 0));
1345
+ }
1346
+
1347
+ function getGjcTeamTaskClaimEligibilityReason(
1348
+ task: GjcTeamTask,
1349
+ worker: GjcTeamWorker,
1350
+ tasks: GjcTeamTask[],
1351
+ ): string | null {
1352
+ if (task.status !== "pending") return `task_not_pending:${task.id}`;
1353
+ if (task.owner && task.owner !== worker.id) return `task_owner_mismatch:${task.id}:${task.owner}`;
1354
+ if (task.assignee && task.assignee !== worker.id) return `task_assignee_mismatch:${task.id}:${task.assignee}`;
1355
+
1356
+ const workerRoles = roleValuesForWorker(worker);
1357
+ if (task.required_role && !workerRoles.has(task.required_role))
1358
+ return `task_role_mismatch:${task.id}:${task.required_role}`;
1359
+ if (task.allowed_roles?.length && !task.allowed_roles.some(role => workerRoles.has(role)))
1360
+ return `task_role_mismatch:${task.id}:${task.allowed_roles.join(",")}`;
1361
+
1362
+ if (task.blocked_by?.length) return `task_blocked:${task.id}:${task.blocked_by.join(",")}`;
1363
+ for (const dependencyId of task.depends_on ?? []) {
1364
+ const dependency = tasks.find(candidate => candidate.id === dependencyId);
1365
+ if (!dependency || !isGjcTeamTaskCompletionVerified(dependency))
1366
+ return `task_dependency_incomplete:${task.id}:${dependencyId}`;
1367
+ }
1368
+
1369
+ return null;
1370
+ }
1371
+
1372
+ async function getActiveClaimReason(dir: string, task: GjcTeamTask): Promise<string | null> {
1373
+ const claimPath = path.join(dir, "claims", `${task.id}.json`);
1374
+ const diskClaim = readClaimRecord(await readJsonFile<unknown>(claimPath));
1375
+ const claim = task.claim ?? diskClaim;
1376
+ if (!claim || isPastTimestamp(claim.leased_until)) return null;
1377
+ return `task_already_claimed:${task.id}`;
1378
+ }
626
1379
  function isGjcTeamTaskRecord(value: unknown): value is GjcTeamTask {
627
1380
  return (
628
1381
  isRecord(value) &&
@@ -820,17 +1573,35 @@ async function ensureWorkerWorktree(
820
1573
  export function resolveGjcTmuxCommand(env: NodeJS.ProcessEnv = process.env): string {
821
1574
  return env.GJC_TEAM_TMUX_COMMAND?.trim() || "tmux";
822
1575
  }
1576
+ function buildTeamTmuxLeaderRequirementMessage(detail?: string): string {
1577
+ const suffix = detail?.trim() ? `:${detail.trim()}` : "";
1578
+ 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}`;
1579
+ }
1580
+ function readGjcTmuxProfileValue(tmuxCommand: string, sessionName: string): string {
1581
+ const result = Bun.spawnSync(
1582
+ [tmuxCommand, "show-options", "-qv", "-t", `=${sessionName}`, GJC_TMUX_PROFILE_OPTION],
1583
+ {
1584
+ stdout: "pipe",
1585
+ stderr: "pipe",
1586
+ },
1587
+ );
1588
+ if (result.exitCode !== 0) return "";
1589
+ return result.stdout.toString().trim();
1590
+ }
1591
+
823
1592
  function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEnv): GjcTmuxLeaderContext {
824
1593
  const paneTarget = env.TMUX_PANE?.trim();
825
1594
  const args = paneTarget
826
1595
  ? ["display-message", "-p", "-t", paneTarget, "#S:#I #{pane_id}"]
827
1596
  : ["display-message", "-p", "#S:#I #{pane_id}"];
828
1597
  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");
1598
+ if (result.exitCode !== 0) throw new Error(buildTeamTmuxLeaderRequirementMessage(result.stderr.toString()));
830
1599
  const [sessionAndWindow = "", leaderPaneId = ""] = result.stdout.toString().trim().split(/\s+/);
831
1600
  const [sessionName = "", windowIndex = ""] = sessionAndWindow.split(":");
832
1601
  if (!sessionName || !windowIndex || !leaderPaneId.startsWith("%"))
833
- throw new Error(`invalid_tmux_context:${result.stdout.toString().trim()}`);
1602
+ throw new Error(buildTeamTmuxLeaderRequirementMessage(`invalid_tmux_context:${result.stdout.toString().trim()}`));
1603
+ if (readGjcTmuxProfileValue(tmuxCommand, sessionName) !== GJC_TMUX_PROFILE_VALUE)
1604
+ throw new Error(buildTeamTmuxLeaderRequirementMessage(`unmanaged_tmux_session:${sessionName}`));
834
1605
  return { sessionName, windowIndex, leaderPaneId, target: `${sessionName}:${windowIndex}` };
835
1606
  }
836
1607
  export function resolveGjcWorkerCommand(cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): string {
@@ -852,7 +1623,7 @@ function buildWorkerCommand(config: GjcTeamConfig, worker: GjcTeamWorker): strin
852
1623
  workspace,
853
1624
  `Task: ${config.task}`,
854
1625
  `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.`,
1626
+ `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
1627
  ].join("\n");
857
1628
  const env = [
858
1629
  `GJC_TEAM_WORKER=${shellQuote(`${config.team_name}/${worker.id}`)}`,
@@ -1032,9 +1803,18 @@ async function appendIntegrationReport(
1032
1803
  entry: { worker: string; operation: "merge" | "cherry-pick" | "rebase"; files: string[]; detail: string },
1033
1804
  ): Promise<void> {
1034
1805
  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}`);
1806
+ if (await pathExists(integrationReportPath(dir)))
1807
+ await appendText(
1808
+ integrationReportPath(dir),
1809
+ line,
1810
+ stateWriterOptions(integrationReportPath(dir), "report", "append"),
1811
+ );
1812
+ else
1813
+ await writeReport(
1814
+ integrationReportPath(dir),
1815
+ `# Integration Report\n\n${line}`,
1816
+ stateWriterOptions(integrationReportPath(dir), "report", "write"),
1817
+ );
1038
1818
  }
1039
1819
  async function appendCommitHygieneEntries(config: GjcTeamConfig, entries: GjcTeamCommitHygieneEntry[]): Promise<void> {
1040
1820
  if (entries.length === 0) return;
@@ -1583,13 +2363,18 @@ async function integrateGjcWorkerCommits(
1583
2363
  }
1584
2364
 
1585
2365
  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 });
2366
+ // Empty mailbox directories are runtime state, so they must exist before messages arrive.
2367
+ await fs.mkdir(path.join(dir, "mailbox"), { recursive: true });
1588
2368
  for (const worker of workers) {
1589
- await fs.mkdir(workerDir(dir, worker.id), { recursive: true });
1590
2369
  await fs.mkdir(mailboxDirPath(dir, worker.id), { recursive: true });
1591
2370
  await writeJsonFile(mailboxPath(dir, worker.id), { messages: [] });
1592
2371
  await writeJsonFile(path.join(workerDir(dir, worker.id), "status.json"), { state: "idle", updated_at: now() });
2372
+ await writeJsonFile(workerLifecyclePath(dir, worker.id), {
2373
+ worker: worker.id,
2374
+ lifecycle_state: "starting",
2375
+ worker_status_state: "idle",
2376
+ updated_at: now(),
2377
+ } satisfies GjcTeamWorkerLifecycle);
1593
2378
  await writeJsonFile(path.join(workerDir(dir, worker.id), "heartbeat.json"), {
1594
2379
  pid: 0,
1595
2380
  last_turn_at: now(),
@@ -1597,6 +2382,7 @@ async function initializeStateDirs(dir: string, workers: GjcTeamWorker[]): Promi
1597
2382
  alive: true,
1598
2383
  });
1599
2384
  }
2385
+ // Empty leader mailbox directory is runtime state, so it must exist before messages arrive.
1600
2386
  await fs.mkdir(mailboxDirPath(dir, "leader-fixed"), { recursive: true });
1601
2387
  await writeJsonFile(mailboxPath(dir, "leader-fixed"), { messages: [] });
1602
2388
  }
@@ -1714,6 +2500,10 @@ export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTea
1714
2500
  updated_at: now(),
1715
2501
  };
1716
2502
  await writeJsonFile(path.join(dir, "config.json"), runningConfig);
2503
+ await writeWorkerLifecycleForConfig(dir, runningConfig, "starting", worker => ({
2504
+ pane_id: worker.pane_id,
2505
+ started_at: runningConfig.created_at,
2506
+ }));
1717
2507
  await writePhase(dir, "running");
1718
2508
  return readGjcTeamSnapshot(teamName, cwd, env);
1719
2509
  }
@@ -1722,6 +2512,7 @@ export async function readGjcTeamSnapshot(
1722
2512
  teamName: string,
1723
2513
  cwd = process.cwd(),
1724
2514
  env: NodeJS.ProcessEnv = process.env,
2515
+ options: GjcTeamSnapshotOptions = {},
1725
2516
  ): Promise<GjcTeamSnapshot> {
1726
2517
  const dir = await findTeamDir(teamName, cwd, env);
1727
2518
  const config = await readConfig(dir);
@@ -1736,7 +2527,11 @@ export async function readGjcTeamSnapshot(
1736
2527
  };
1737
2528
  for (const task of tasks) taskCounts[task.status] += 1;
1738
2529
  const monitor = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
1739
- const notificationSummary = await reconcileTeamNotifications(dir, config);
2530
+ const workerLifecycleById = await readWorkerLifecycleById(dir, config);
2531
+ const notificationSummary =
2532
+ options.reconcileNotifications === true
2533
+ ? await reconcileTeamNotifications(dir, config)
2534
+ : summarizeNotifications(await listNotificationRecords(dir));
1740
2535
  const phase = await resolveGjcTeamSnapshotPhase(dir, config, storedPhase, tasks, monitor);
1741
2536
  return {
1742
2537
  team_name: config.team_name,
@@ -1750,10 +2545,19 @@ export async function readGjcTeamSnapshot(
1750
2545
  task_counts: taskCounts,
1751
2546
  workers: config.workers,
1752
2547
  integration_by_worker: monitor?.integration_by_worker,
2548
+ worker_lifecycle_by_id: workerLifecycleById,
1753
2549
  notification_summary: notificationSummary,
1754
2550
  updated_at: config.updated_at,
1755
2551
  };
1756
2552
  }
2553
+ export async function monitorGjcTeamSnapshot(
2554
+ teamName: string,
2555
+ cwd = process.cwd(),
2556
+ env: NodeJS.ProcessEnv = process.env,
2557
+ ): Promise<GjcTeamSnapshot> {
2558
+ const snapshot = await monitorGjcTeam(teamName, cwd, env);
2559
+ return snapshot;
2560
+ }
1757
2561
  function workerIntegrationFingerprint(head: string | null, classification: GjcWorkerCheckpointClassification): string {
1758
2562
  return `${head ?? "no-head"}:${classification.kind}:${classification.files.join("\0")}`;
1759
2563
  }
@@ -1864,6 +2668,7 @@ export async function monitorGjcTeam(
1864
2668
  const dir = await findTeamDir(teamName, cwd, env);
1865
2669
  const config = await readConfig(dir);
1866
2670
  const previous = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
2671
+ await reconcileGjcTeamStaleClaims(teamName, dir, config, env);
1867
2672
  const integrationByWorker = await integrateGjcWorkerCommits(config, dir, previous, cwd, env);
1868
2673
  await writeJsonFile(monitorSnapshotPath(dir), { integration_by_worker: integrationByWorker, updated_at: now() });
1869
2674
  await replayGjcTeamNotifications(teamName, cwd, env);
@@ -1902,7 +2707,7 @@ async function writeGjcWorkerStartupAck(
1902
2707
  ): Promise<Record<string, unknown>> {
1903
2708
  const dir = await findTeamDir(teamName, cwd, env);
1904
2709
  const config = await readConfig(dir);
1905
- assertKnownWorker(config, worker);
2710
+ const teamWorker = findKnownWorker(config, worker);
1906
2711
  const ack = {
1907
2712
  worker,
1908
2713
  pid: typeof input.pid === "number" ? input.pid : undefined,
@@ -1911,6 +2716,11 @@ async function writeGjcWorkerStartupAck(
1911
2716
  ack_at: now(),
1912
2717
  };
1913
2718
  await writeJsonFile(path.join(workerDir(dir, worker), "startup-ack.json"), ack);
2719
+ await writeWorkerLifecycleRecord(dir, teamWorker, "ready", {
2720
+ pane_id: teamWorker.pane_id,
2721
+ pid: typeof input.pid === "number" ? input.pid : undefined,
2722
+ started_at: ack.ack_at,
2723
+ });
1914
2724
  await appendEvent(dir, { type: "worker_startup_ack", worker, message: `Worker ${worker} acknowledged startup` });
1915
2725
  return ack;
1916
2726
  }
@@ -2026,12 +2836,31 @@ export async function shutdownGjcTeam(
2026
2836
  const dir = await findTeamDir(teamName, cwd, env);
2027
2837
  const config = await readConfig(dir);
2028
2838
  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";
2839
+ const evidenceFailures = tasks
2840
+ .map(task => {
2841
+ const reason = task.status === "completed" ? getGjcTeamTaskCompletionEvidenceFailure(task) : null;
2842
+ return reason ? { task_id: task.id, reason } : null;
2843
+ })
2844
+ .filter((failure): failure is { task_id: string; reason: string } => failure != null);
2845
+ const shutdownRequestId = `shutdown-${stableHash([config.team_name, now(), randomUUID()].join(":"))}`;
2846
+ const shutdownRequestedAt = now();
2847
+ await Promise.all(
2848
+ config.workers.map(worker =>
2849
+ writeGjcShutdownRequest(
2850
+ teamName,
2851
+ worker.id,
2852
+ "leader-fixed",
2853
+ cwd,
2854
+ env,
2855
+ shutdownRequestId,
2856
+ "graceful",
2857
+ shutdownRequestedAt,
2858
+ ),
2859
+ ),
2860
+ );
2861
+ const monitor = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
2862
+ const completionVerified = tasks.length === 0 || tasks.every(isGjcTeamTaskCompletionVerified);
2863
+ const pendingIntegration = completionVerified ? await hasPendingGjcTeamIntegration(dir, config, monitor) : false;
2035
2864
  killWorkerPanes(config);
2036
2865
  await removeCleanCreatedWorktrees(config.workers);
2037
2866
  const stopped = {
@@ -2040,18 +2869,50 @@ export async function shutdownGjcTeam(
2040
2869
  updated_at: now(),
2041
2870
  };
2042
2871
  await writeJsonFile(path.join(dir, "config.json"), stopped);
2872
+ await writeWorkerLifecycleForConfig(dir, stopped, "stopped", worker => ({
2873
+ pane_id: worker.pane_id,
2874
+ stopped_at: stopped.updated_at,
2875
+ stop_reason: "graceful_shutdown",
2876
+ shutdown_request_id: shutdownRequestId,
2877
+ shutdown_requested_at: shutdownRequestedAt,
2878
+ shutdown_mode: "graceful",
2879
+ }));
2880
+ const workerLifecycleById = await readWorkerLifecycleById(dir, stopped);
2881
+ const gracefulShutdownComplete = stopped.workers.every(worker => {
2882
+ const lifecycle = workerLifecycleById[worker.id];
2883
+ return (
2884
+ lifecycle?.lifecycle_state === "stopped" &&
2885
+ lifecycle.shutdown_request_id === shutdownRequestId &&
2886
+ lifecycle.shutdown_mode === "graceful"
2887
+ );
2888
+ });
2889
+ const shutdownPhase: GjcTeamPhase =
2890
+ completionVerified && gracefulShutdownComplete
2891
+ ? pendingIntegration
2892
+ ? "awaiting_integration"
2893
+ : "complete"
2894
+ : evidenceFailures.length > 0 || tasks.some(task => task.status === "failed" || task.status === "blocked")
2895
+ ? "failed"
2896
+ : "cancelled";
2043
2897
  await writePhase(dir, shutdownPhase);
2898
+ const shutdownData: Record<string, unknown> = {
2899
+ phase: shutdownPhase,
2900
+ shutdown_request_id: shutdownRequestId,
2901
+ graceful_shutdown_complete: gracefulShutdownComplete,
2902
+ };
2903
+ if (evidenceFailures.length > 0) shutdownData.evidence_failures = evidenceFailures;
2044
2904
  await appendEvent(dir, {
2045
2905
  type: "team_shutdown",
2046
2906
  message:
2047
2907
  shutdownPhase === "complete"
2048
2908
  ? "Shut down native gjc team runtime after completed tasks"
2049
2909
  : "Shut down native gjc team runtime with incomplete tasks",
2050
- data: { phase: shutdownPhase },
2910
+ data: shutdownData,
2051
2911
  });
2052
2912
  await appendTelemetry(dir, {
2053
2913
  type: "team_shutdown",
2054
2914
  message: `Native gjc team runtime stopped with phase ${shutdownPhase}`,
2915
+ data: { shutdown_request_id: shutdownRequestId, graceful_shutdown_complete: gracefulShutdownComplete },
2055
2916
  });
2056
2917
  return readGjcTeamSnapshot(config.team_name, cwd, env);
2057
2918
  }
@@ -2079,9 +2940,11 @@ export async function createGjcTeamTask(
2079
2940
  description: string,
2080
2941
  cwd = process.cwd(),
2081
2942
  env: NodeJS.ProcessEnv = process.env,
2943
+ taskOptions: GjcTeamTaskMetadataInput = {},
2082
2944
  ): Promise<GjcTeamTask> {
2083
2945
  const dir = await findTeamDir(teamName, cwd, env);
2084
2946
  const config = await readConfig(dir);
2947
+ if (taskOptions.owner) assertKnownWorker(config, taskOptions.owner);
2085
2948
  const tasks = await readTasks(dir);
2086
2949
  const next = tasks.length + 1;
2087
2950
  const task: GjcTeamTask = {
@@ -2091,6 +2954,12 @@ export async function createGjcTeamTask(
2091
2954
  title: subject,
2092
2955
  objective: description,
2093
2956
  status: "pending",
2957
+ ...(taskOptions.owner ? { owner: taskOptions.owner } : {}),
2958
+ ...(taskOptions.lane ? { lane: taskOptions.lane } : {}),
2959
+ ...(taskOptions.required_role ? { required_role: taskOptions.required_role } : {}),
2960
+ ...(taskOptions.allowed_roles ? { allowed_roles: taskOptions.allowed_roles } : {}),
2961
+ ...(taskOptions.depends_on ? { depends_on: taskOptions.depends_on } : {}),
2962
+ ...(taskOptions.blocked_by ? { blocked_by: taskOptions.blocked_by } : {}),
2094
2963
  version: 1,
2095
2964
  created_at: now(),
2096
2965
  updated_at: now(),
@@ -2104,7 +2973,12 @@ export async function createGjcTeamTask(
2104
2973
  export async function updateGjcTeamTask(
2105
2974
  teamName: string,
2106
2975
  taskId: string,
2107
- updates: Partial<Pick<GjcTeamTask, "subject" | "description" | "blocked_by" | "depends_on">>,
2976
+ updates: Partial<
2977
+ Pick<
2978
+ GjcTeamTask,
2979
+ "subject" | "description" | "blocked_by" | "depends_on" | "lane" | "required_role" | "allowed_roles"
2980
+ >
2981
+ >,
2108
2982
  cwd = process.cwd(),
2109
2983
  env: NodeJS.ProcessEnv = process.env,
2110
2984
  ): Promise<GjcTeamTask> {
@@ -2131,13 +3005,20 @@ export async function claimGjcTeamTask(
2131
3005
  ): Promise<GjcTeamApiClaimResult> {
2132
3006
  const dir = await findTeamDir(teamName, cwd, env);
2133
3007
  const config = await readConfig(dir);
2134
- assertKnownWorker(config, workerId);
3008
+ const teamWorker = findKnownWorker(config, workerId);
3009
+ const livenessRecovery = await reconcileGjcTeamStaleClaims(teamName, dir, config, env);
3010
+ const staleWorkerReasons = livenessRecovery.stale_workers[workerId];
3011
+ if (staleWorkerReasons?.length)
3012
+ return { ok: false, reason: `worker_not_live:${workerId}:${staleWorkerReasons.join(",")}` };
2135
3013
  const tasks = await readTasks(dir);
2136
3014
  const task = taskId
2137
3015
  ? 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}` };
3016
+ : tasks.find(candidate => getGjcTeamTaskClaimEligibilityReason(candidate, teamWorker, tasks) == null);
3017
+ if (!task) return { ok: false, reason: taskId ? `task_not_found:${taskId}` : "no_pending_task" };
3018
+ const eligibilityReason = getGjcTeamTaskClaimEligibilityReason(task, teamWorker, tasks);
3019
+ if (eligibilityReason) return { ok: false, reason: eligibilityReason };
3020
+ const activeClaimReason = await getActiveClaimReason(dir, task);
3021
+ if (activeClaimReason) return { ok: false, reason: activeClaimReason };
2141
3022
  const token = randomUUID();
2142
3023
  const claim: GjcTeamTaskClaim = {
2143
3024
  owner: workerId,
@@ -2145,20 +3026,19 @@ export async function claimGjcTeamTask(
2145
3026
  leased_until: new Date(Date.now() + 30 * 60_000).toISOString(),
2146
3027
  };
2147
3028
  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
- }
3029
+ const created = await writeJsonFileNoClobber(claimPath, claim);
3030
+ if (!created) return { ok: false, reason: `task_already_claimed:${task.id}` };
2159
3031
  const current = await readGjcTeamTask(teamName, task.id, cwd, env);
2160
- if (current.status !== "pending") {
3032
+ const currentEligibilityReason = getGjcTeamTaskClaimEligibilityReason(current, teamWorker, await readTasks(dir));
3033
+ if (currentEligibilityReason) {
2161
3034
  await fs.rm(claimPath, { force: true });
3035
+ return { ok: false, reason: currentEligibilityReason };
3036
+ }
3037
+ if (current.status !== "pending") {
3038
+ await deleteIfOwned(claimPath, {
3039
+ ...stateWriterOptions(claimPath, "prune", "rollback"),
3040
+ predicate: current => (current as GjcTeamTaskClaim).token === token,
3041
+ });
2162
3042
  return { ok: false, reason: `task_not_pending:${task.id}` };
2163
3043
  }
2164
3044
  const updated: GjcTeamTask = {
@@ -2173,7 +3053,10 @@ export async function claimGjcTeamTask(
2173
3053
  try {
2174
3054
  await writeTask(dir, updated);
2175
3055
  } catch (error) {
2176
- await fs.rm(claimPath, { force: true });
3056
+ await deleteIfOwned(claimPath, {
3057
+ ...stateWriterOptions(claimPath, "prune", "rollback"),
3058
+ predicate: current => (current as GjcTeamTaskClaim).token === token,
3059
+ });
2177
3060
  throw error;
2178
3061
  }
2179
3062
  await appendEvent(dir, {
@@ -2192,7 +3075,7 @@ export async function transitionGjcTeamTaskStatus(
2192
3075
  env: NodeJS.ProcessEnv = process.env,
2193
3076
  claimToken?: string,
2194
3077
  workerId?: string,
2195
- evidence?: string,
3078
+ completionEvidenceInput?: unknown,
2196
3079
  ): Promise<GjcTeamTask> {
2197
3080
  const dir = await findTeamDir(teamName, cwd, env);
2198
3081
  const config = await readConfig(dir);
@@ -2205,30 +3088,39 @@ export async function transitionGjcTeamTaskStatus(
2205
3088
  if (task.claim.token !== claimToken) throw new Error(`claim_token_mismatch:${taskId}`);
2206
3089
  if (workerId && task.claim.owner !== workerId) throw new Error(`claim_owner_mismatch:${taskId}`);
2207
3090
  const terminal = status === "completed" || status === "failed";
2208
- if (status === "completed" && evidence !== undefined && evidence.trim().length === 0)
2209
- throw new Error(`task_evidence_required:${taskId}`);
3091
+ const transitionedAt = now();
3092
+ const completionEvidence =
3093
+ status === "completed"
3094
+ ? normalizeGjcTeamTaskCompletionEvidence(taskId, task.claim.owner, completionEvidenceInput, transitionedAt)
3095
+ : undefined;
2210
3096
  const updated: GjcTeamTask = {
2211
3097
  ...task,
2212
3098
  status,
2213
3099
  claim: terminal ? undefined : task.claim,
2214
3100
  version: task.version + 1,
2215
- updated_at: now(),
2216
- ...(terminal ? { completed_at: now() } : {}),
3101
+ updated_at: transitionedAt,
3102
+ ...(terminal ? { completed_at: transitionedAt } : {}),
3103
+ ...(completionEvidence ? { completion_evidence: completionEvidence } : {}),
2217
3104
  };
2218
3105
  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 });
3106
+ if (terminal) {
3107
+ const claimPath = path.join(dir, "claims", `${taskId}.json`);
3108
+ await removeFileAudited(claimPath, stateWriterOptions(claimPath, "prune", "terminal"));
3109
+ }
3110
+ const eventData: Record<string, unknown> = { status };
3111
+ if (completionEvidence) {
3112
+ eventData.completion_evidence = {
3113
+ recorded_by: completionEvidence.recorded_by,
3114
+ item_count: completionEvidence.items.length,
3115
+ verified_item_count: completionEvidence.items.filter(isGjcTeamCompletionEvidenceItemVerified).length,
3116
+ files_count: completionEvidence.files?.length ?? 0,
3117
+ };
3118
+ }
2227
3119
  await appendEvent(dir, {
2228
3120
  type: "task_transitioned",
2229
3121
  task_id: taskId,
2230
3122
  message: "Task status changed",
2231
- data: { status },
3123
+ data: eventData,
2232
3124
  });
2233
3125
  return updated;
2234
3126
  }
@@ -2239,8 +3131,18 @@ export async function transitionGjcTeamTask(
2239
3131
  cwd = process.cwd(),
2240
3132
  env: NodeJS.ProcessEnv = process.env,
2241
3133
  claimToken?: string,
3134
+ completionEvidenceInput?: unknown,
2242
3135
  ): Promise<GjcTeamTask> {
2243
- return transitionGjcTeamTaskStatus(teamName, taskId, parseGjcTeamTaskStatus(status, true), cwd, env, claimToken);
3136
+ return transitionGjcTeamTaskStatus(
3137
+ teamName,
3138
+ taskId,
3139
+ parseGjcTeamTaskStatus(status, true),
3140
+ cwd,
3141
+ env,
3142
+ claimToken,
3143
+ undefined,
3144
+ completionEvidenceInput,
3145
+ );
2244
3146
  }
2245
3147
  export async function releaseGjcTeamTaskClaim(
2246
3148
  teamName: string,
@@ -2263,7 +3165,11 @@ export async function releaseGjcTeamTaskClaim(
2263
3165
  updated_at: now(),
2264
3166
  };
2265
3167
  await writeTask(dir, updated);
2266
- await fs.rm(path.join(dir, "claims", `${taskId}.json`), { force: true });
3168
+ const claimPath = path.join(dir, "claims", `${taskId}.json`);
3169
+ await deleteIfOwned(claimPath, {
3170
+ ...stateWriterOptions(claimPath, "prune", "release"),
3171
+ predicate: current => (current as GjcTeamTaskClaim).token === claimToken,
3172
+ });
2267
3173
  await appendEvent(dir, {
2268
3174
  type: "task_claim_released",
2269
3175
  task_id: taskId,
@@ -2603,12 +3509,43 @@ export async function readGjcWorkerStatus(
2603
3509
  const dir = await findTeamDir(teamName, cwd, env);
2604
3510
  const config = await readConfig(dir);
2605
3511
  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
- );
3512
+ return readWorkerStatusFile(dir, worker);
3513
+ }
3514
+ export async function updateGjcWorkerStatus(
3515
+ teamName: string,
3516
+ worker: string,
3517
+ status: GjcWorkerStatusState,
3518
+ cwd = process.cwd(),
3519
+ env: NodeJS.ProcessEnv = process.env,
3520
+ currentTaskId?: string,
3521
+ reason?: string,
3522
+ ): Promise<WorkerStatusFile> {
3523
+ const dir = await findTeamDir(teamName, cwd, env);
3524
+ const config = await readConfig(dir);
3525
+ const teamWorker = findKnownWorker(config, worker);
3526
+ if (currentTaskId) assertSafeId("task_id", currentTaskId);
3527
+ const trimmedReason = reason?.trim();
3528
+ const value: WorkerStatusFile = {
3529
+ state: status,
3530
+ ...(currentTaskId ? { current_task_id: currentTaskId } : {}),
3531
+ ...(trimmedReason ? { reason: trimmedReason } : {}),
3532
+ updated_at: now(),
3533
+ };
3534
+ await writeJsonFile(path.join(workerDir(dir, worker), "status.json"), value);
3535
+ const currentLifecycle = await readWorkerLifecycleRecord(dir, teamWorker);
3536
+ const lifecycleState =
3537
+ currentLifecycle.lifecycle_state === "stopped" ? "stopped" : lifecycleStateForWorkerStatus(status);
3538
+ await writeWorkerLifecycleRecord(dir, teamWorker, lifecycleState);
3539
+ await appendEvent(dir, {
3540
+ type: "worker_status_updated",
3541
+ worker,
3542
+ message: `Worker ${worker} reported ${status}`,
3543
+ data: {
3544
+ status,
3545
+ current_task_id: currentTaskId,
3546
+ },
3547
+ });
3548
+ return value;
2612
3549
  }
2613
3550
  export async function readGjcWorkerHeartbeat(
2614
3551
  teamName: string,
@@ -2646,7 +3583,7 @@ export async function writeGjcWorkerInbox(
2646
3583
  const config = await readConfig(dir);
2647
3584
  assertKnownWorker(config, worker);
2648
3585
  const filePath = path.join(workerDir(dir, worker), "inbox.md");
2649
- await Bun.write(filePath, content);
3586
+ await writeReport(filePath, content, stateWriterOptions(filePath, "report", "write"));
2650
3587
  return { path: filePath };
2651
3588
  }
2652
3589
  export async function writeGjcWorkerIdentity(
@@ -2678,6 +3615,23 @@ export async function readGjcTeamEvents(
2678
3615
  throw error;
2679
3616
  }
2680
3617
  }
3618
+ export async function readGjcTeamTraces(
3619
+ teamName: string,
3620
+ cwd = process.cwd(),
3621
+ env: NodeJS.ProcessEnv = process.env,
3622
+ ): Promise<GjcTeamTraceEvent[]> {
3623
+ const dir = await findTeamDir(teamName, cwd, env);
3624
+ try {
3625
+ const text = await Bun.file(tracePath(dir)).text();
3626
+ return text
3627
+ .split(/\r?\n/)
3628
+ .filter(Boolean)
3629
+ .map(line => JSON.parse(line) as GjcTeamTraceEvent);
3630
+ } catch (error) {
3631
+ if (isEnoent(error)) return [];
3632
+ throw error;
3633
+ }
3634
+ }
2681
3635
  export async function appendGjcTeamEvent(
2682
3636
  teamName: string,
2683
3637
  type: string,
@@ -2741,13 +3695,27 @@ export async function writeGjcShutdownRequest(
2741
3695
  requestedBy: string,
2742
3696
  cwd = process.cwd(),
2743
3697
  env: NodeJS.ProcessEnv = process.env,
3698
+ requestId = `shutdown-${stableHash([teamName, worker, now(), randomUUID()].join(":"))}`,
3699
+ mode: GjcTeamShutdownMode = "graceful",
3700
+ requestedAt = now(),
2744
3701
  ): Promise<Record<string, unknown>> {
2745
3702
  const dir = await findTeamDir(teamName, cwd, env);
2746
3703
  const config = await readConfig(dir);
2747
- assertKnownWorker(config, worker);
3704
+ const teamWorker = findKnownWorker(config, worker);
2748
3705
  assertKnownParticipant(config, requestedBy);
2749
- const value = { worker, requested_by: requestedBy, requested_at: now() };
3706
+ const value = { worker, requested_by: requestedBy, request_id: requestId, mode, requested_at: requestedAt };
2750
3707
  await writeJsonFile(path.join(workerDir(dir, worker), "shutdown-request.json"), value);
3708
+ await writeWorkerLifecycleRecord(dir, teamWorker, "draining", {
3709
+ shutdown_request_id: requestId,
3710
+ shutdown_requested_at: requestedAt,
3711
+ shutdown_mode: mode,
3712
+ });
3713
+ await appendEvent(dir, {
3714
+ type: "worker_shutdown_requested",
3715
+ worker,
3716
+ message: `Worker ${worker} shutdown requested`,
3717
+ data: { requested_by: requestedBy, request_id: requestId, mode },
3718
+ });
2751
3719
  return value;
2752
3720
  }
2753
3721
  export async function readGjcShutdownAck(
@@ -2786,6 +3754,7 @@ export async function executeGjcTeamApiOperation(
2786
3754
  String(input.description ?? ""),
2787
3755
  cwd,
2788
3756
  env,
3757
+ taskMetadataFromInput(input, true),
2789
3758
  ),
2790
3759
  };
2791
3760
  case "update-task":
@@ -2796,19 +3765,22 @@ export async function executeGjcTeamApiOperation(
2796
3765
  {
2797
3766
  subject: typeof input.subject === "string" ? input.subject : undefined,
2798
3767
  description: typeof input.description === "string" ? input.description : undefined,
3768
+ ...taskMetadataFromInput(input),
2799
3769
  },
2800
3770
  cwd,
2801
3771
  env,
2802
3772
  ),
2803
3773
  };
2804
- case "claim-task":
3774
+ case "claim-task": {
3775
+ const requestedTaskId = input.task_id ?? input.taskId;
2805
3776
  return claimGjcTeamTask(
2806
3777
  teamName,
2807
3778
  worker,
2808
3779
  cwd,
2809
3780
  env,
2810
- typeof input.task_id === "string" ? input.task_id : undefined,
3781
+ typeof requestedTaskId === "string" ? requestedTaskId : undefined,
2811
3782
  );
3783
+ }
2812
3784
  case "transition-task":
2813
3785
  case "transition-task-status":
2814
3786
  return {
@@ -2821,11 +3793,7 @@ export async function executeGjcTeamApiOperation(
2821
3793
  env,
2822
3794
  typeof input.claim_token === "string" ? input.claim_token : undefined,
2823
3795
  explicitWorker,
2824
- typeof input.evidence === "string"
2825
- ? input.evidence
2826
- : typeof input.result === "string"
2827
- ? input.result
2828
- : undefined,
3796
+ input.completion_evidence ?? input.completionEvidence,
2829
3797
  ),
2830
3798
  };
2831
3799
  case "release-task-claim":
@@ -2925,8 +3893,22 @@ export async function executeGjcTeamApiOperation(
2925
3893
  return readJsonFile(path.join(await findTeamDir(teamName, cwd, env), "manifest.v2.json"));
2926
3894
  case "read-worker-status":
2927
3895
  return readGjcWorkerStatus(teamName, worker, cwd, env);
3896
+ case "update-worker-status": {
3897
+ const currentTaskIdInput = input.current_task_id ?? input.currentTaskId;
3898
+ return updateGjcWorkerStatus(
3899
+ teamName,
3900
+ worker,
3901
+ parseRequiredGjcWorkerStatusState(input.status ?? input.state),
3902
+ cwd,
3903
+ env,
3904
+ typeof currentTaskIdInput === "string" ? currentTaskIdInput : undefined,
3905
+ typeof input.reason === "string" ? input.reason : undefined,
3906
+ );
3907
+ }
2928
3908
  case "read-worker-heartbeat":
2929
3909
  return readGjcWorkerHeartbeat(teamName, worker, cwd, env);
3910
+ case "recover-stale-claims":
3911
+ return recoverGjcTeamStaleClaims(teamName, cwd, env);
2930
3912
  case "update-worker-heartbeat":
2931
3913
  return updateGjcWorkerHeartbeat(
2932
3914
  teamName,
@@ -2962,6 +3944,8 @@ export async function executeGjcTeamApiOperation(
2962
3944
  return appendGjcTeamEvent(teamName, String(input.type ?? "event"), worker, cwd, env);
2963
3945
  case "read-events":
2964
3946
  return { events: await readGjcTeamEvents(teamName, cwd, env) };
3947
+ case "read-traces":
3948
+ return { traces: await readGjcTeamTraces(teamName, cwd, env) };
2965
3949
  case "await-event":
2966
3950
  return awaitGjcTeamEvent(teamName, Number(input.timeout_ms ?? 0), cwd, env);
2967
3951
  case "write-monitor-snapshot":
@@ -2972,8 +3956,18 @@ export async function executeGjcTeamApiOperation(
2972
3956
  return writeGjcTaskApproval(teamName, String(input.task_id), input, cwd, env);
2973
3957
  case "read-task-approval":
2974
3958
  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);
3959
+ case "write-shutdown-request": {
3960
+ const shutdownRequestIdInput = input.request_id ?? input.requestId;
3961
+ return writeGjcShutdownRequest(
3962
+ teamName,
3963
+ worker,
3964
+ String(input.requested_by ?? input.requestedBy ?? "leader-fixed"),
3965
+ cwd,
3966
+ env,
3967
+ typeof shutdownRequestIdInput === "string" ? shutdownRequestIdInput : undefined,
3968
+ parseGjcTeamShutdownMode(input.mode),
3969
+ );
3970
+ }
2977
3971
  case "read-shutdown-ack":
2978
3972
  return readGjcShutdownAck(teamName, worker, cwd, env);
2979
3973
  default: