@pennixrv/trellis 0.7.1-beta.9 → 0.7.2-beta.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 (231) hide show
  1. package/dist/cli/index.js +4 -4
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/commands/init.d.ts.map +1 -1
  4. package/dist/commands/init.js +39 -26
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/teamwork/adapters/acp.d.ts +20 -0
  7. package/dist/commands/teamwork/adapters/acp.d.ts.map +1 -0
  8. package/dist/commands/teamwork/adapters/acp.js +268 -0
  9. package/dist/commands/teamwork/adapters/acp.js.map +1 -0
  10. package/dist/commands/teamwork/adapters/claude.d.ts +1 -0
  11. package/dist/commands/teamwork/adapters/claude.d.ts.map +1 -1
  12. package/dist/commands/teamwork/adapters/claude.js +5 -0
  13. package/dist/commands/teamwork/adapters/claude.js.map +1 -1
  14. package/dist/commands/teamwork/adapters/codex.d.ts +3 -2
  15. package/dist/commands/teamwork/adapters/codex.d.ts.map +1 -1
  16. package/dist/commands/teamwork/adapters/codex.js +14 -15
  17. package/dist/commands/teamwork/adapters/codex.js.map +1 -1
  18. package/dist/commands/teamwork/adapters/index.d.ts +23 -1
  19. package/dist/commands/teamwork/adapters/index.d.ts.map +1 -1
  20. package/dist/commands/teamwork/adapters/index.js +163 -24
  21. package/dist/commands/teamwork/adapters/index.js.map +1 -1
  22. package/dist/commands/teamwork/agent-loader.d.ts +2 -1
  23. package/dist/commands/teamwork/agent-loader.d.ts.map +1 -1
  24. package/dist/commands/teamwork/agent-loader.js +2 -1
  25. package/dist/commands/teamwork/agent-loader.js.map +1 -1
  26. package/dist/commands/teamwork/artifact.d.ts +33 -10
  27. package/dist/commands/teamwork/artifact.d.ts.map +1 -1
  28. package/dist/commands/teamwork/artifact.js +182 -50
  29. package/dist/commands/teamwork/artifact.js.map +1 -1
  30. package/dist/commands/teamwork/batch-reservation.d.ts +16 -0
  31. package/dist/commands/teamwork/batch-reservation.d.ts.map +1 -0
  32. package/dist/commands/teamwork/batch-reservation.js +43 -0
  33. package/dist/commands/teamwork/batch-reservation.js.map +1 -0
  34. package/dist/commands/teamwork/batch.d.ts +22 -0
  35. package/dist/commands/teamwork/batch.d.ts.map +1 -0
  36. package/dist/commands/teamwork/batch.js +394 -0
  37. package/dist/commands/teamwork/batch.js.map +1 -0
  38. package/dist/commands/teamwork/context-loader.d.ts +1 -1
  39. package/dist/commands/teamwork/context-loader.d.ts.map +1 -1
  40. package/dist/commands/teamwork/context-loader.js +19 -6
  41. package/dist/commands/teamwork/context-loader.js.map +1 -1
  42. package/dist/commands/teamwork/coordination.d.ts +88 -0
  43. package/dist/commands/teamwork/coordination.d.ts.map +1 -0
  44. package/dist/commands/teamwork/coordination.js +153 -0
  45. package/dist/commands/teamwork/coordination.js.map +1 -0
  46. package/dist/commands/teamwork/coordinator.d.ts +17 -3
  47. package/dist/commands/teamwork/coordinator.d.ts.map +1 -1
  48. package/dist/commands/teamwork/coordinator.js +772 -289
  49. package/dist/commands/teamwork/coordinator.js.map +1 -1
  50. package/dist/commands/teamwork/create.d.ts +1 -0
  51. package/dist/commands/teamwork/create.d.ts.map +1 -1
  52. package/dist/commands/teamwork/create.js +2 -1
  53. package/dist/commands/teamwork/create.js.map +1 -1
  54. package/dist/commands/teamwork/dev-parse-trace.d.ts +2 -1
  55. package/dist/commands/teamwork/dev-parse-trace.d.ts.map +1 -1
  56. package/dist/commands/teamwork/dev-parse-trace.js +2 -0
  57. package/dist/commands/teamwork/dev-parse-trace.js.map +1 -1
  58. package/dist/commands/teamwork/foreground-wait.d.ts +6 -0
  59. package/dist/commands/teamwork/foreground-wait.d.ts.map +1 -0
  60. package/dist/commands/teamwork/foreground-wait.js +65 -0
  61. package/dist/commands/teamwork/foreground-wait.js.map +1 -0
  62. package/dist/commands/teamwork/guard.d.ts +2 -2
  63. package/dist/commands/teamwork/guard.d.ts.map +1 -1
  64. package/dist/commands/teamwork/guard.js +23 -5
  65. package/dist/commands/teamwork/guard.js.map +1 -1
  66. package/dist/commands/teamwork/index.d.ts.map +1 -1
  67. package/dist/commands/teamwork/index.js +139 -10
  68. package/dist/commands/teamwork/index.js.map +1 -1
  69. package/dist/commands/teamwork/kill.d.ts +2 -0
  70. package/dist/commands/teamwork/kill.d.ts.map +1 -1
  71. package/dist/commands/teamwork/kill.js +79 -38
  72. package/dist/commands/teamwork/kill.js.map +1 -1
  73. package/dist/commands/teamwork/messages.d.ts.map +1 -1
  74. package/dist/commands/teamwork/messages.js +3 -0
  75. package/dist/commands/teamwork/messages.js.map +1 -1
  76. package/dist/commands/teamwork/plan.d.ts +22 -0
  77. package/dist/commands/teamwork/plan.d.ts.map +1 -0
  78. package/dist/commands/teamwork/plan.js +288 -0
  79. package/dist/commands/teamwork/plan.js.map +1 -0
  80. package/dist/commands/teamwork/process-identity.d.ts +3 -0
  81. package/dist/commands/teamwork/process-identity.d.ts.map +1 -0
  82. package/dist/commands/teamwork/process-identity.js +24 -0
  83. package/dist/commands/teamwork/process-identity.js.map +1 -0
  84. package/dist/commands/teamwork/profiles.d.ts +43 -6
  85. package/dist/commands/teamwork/profiles.d.ts.map +1 -1
  86. package/dist/commands/teamwork/profiles.js +138 -110
  87. package/dist/commands/teamwork/profiles.js.map +1 -1
  88. package/dist/commands/teamwork/prompt.d.ts +4 -0
  89. package/dist/commands/teamwork/prompt.d.ts.map +1 -0
  90. package/dist/commands/teamwork/prompt.js +33 -0
  91. package/dist/commands/teamwork/prompt.js.map +1 -0
  92. package/dist/commands/teamwork/result.d.ts +54 -0
  93. package/dist/commands/teamwork/result.d.ts.map +1 -0
  94. package/dist/commands/teamwork/result.js +94 -0
  95. package/dist/commands/teamwork/result.js.map +1 -0
  96. package/dist/commands/teamwork/rm.d.ts.map +1 -1
  97. package/dist/commands/teamwork/rm.js +64 -64
  98. package/dist/commands/teamwork/rm.js.map +1 -1
  99. package/dist/commands/teamwork/run.d.ts +5 -0
  100. package/dist/commands/teamwork/run.d.ts.map +1 -1
  101. package/dist/commands/teamwork/run.js +120 -24
  102. package/dist/commands/teamwork/run.js.map +1 -1
  103. package/dist/commands/teamwork/runtime.d.ts +9 -0
  104. package/dist/commands/teamwork/runtime.d.ts.map +1 -0
  105. package/dist/commands/teamwork/runtime.js +372 -0
  106. package/dist/commands/teamwork/runtime.js.map +1 -0
  107. package/dist/commands/teamwork/send.d.ts +1 -0
  108. package/dist/commands/teamwork/send.d.ts.map +1 -1
  109. package/dist/commands/teamwork/send.js +2 -1
  110. package/dist/commands/teamwork/send.js.map +1 -1
  111. package/dist/commands/teamwork/spawn.d.ts +10 -3
  112. package/dist/commands/teamwork/spawn.d.ts.map +1 -1
  113. package/dist/commands/teamwork/spawn.js +41 -29
  114. package/dist/commands/teamwork/spawn.js.map +1 -1
  115. package/dist/commands/teamwork/store/lock.js +28 -4
  116. package/dist/commands/teamwork/store/lock.js.map +1 -1
  117. package/dist/commands/teamwork/supervisor/inbox.d.ts +1 -0
  118. package/dist/commands/teamwork/supervisor/inbox.d.ts.map +1 -1
  119. package/dist/commands/teamwork/supervisor/inbox.js +28 -9
  120. package/dist/commands/teamwork/supervisor/inbox.js.map +1 -1
  121. package/dist/commands/teamwork/supervisor/stdout.d.ts.map +1 -1
  122. package/dist/commands/teamwork/supervisor/stdout.js +2 -3
  123. package/dist/commands/teamwork/supervisor/stdout.js.map +1 -1
  124. package/dist/commands/teamwork/supervisor/warning.d.ts.map +1 -1
  125. package/dist/commands/teamwork/supervisor/warning.js +2 -3
  126. package/dist/commands/teamwork/supervisor/warning.js.map +1 -1
  127. package/dist/commands/teamwork/supervisor.d.ts +5 -0
  128. package/dist/commands/teamwork/supervisor.d.ts.map +1 -1
  129. package/dist/commands/teamwork/supervisor.js +21 -12
  130. package/dist/commands/teamwork/supervisor.js.map +1 -1
  131. package/dist/commands/teamwork/teammate-adapters.d.ts +0 -3
  132. package/dist/commands/teamwork/teammate-adapters.d.ts.map +1 -1
  133. package/dist/commands/teamwork/teammate-adapters.js +9 -54
  134. package/dist/commands/teamwork/teammate-adapters.js.map +1 -1
  135. package/dist/commands/teamwork/teammate-config.d.ts +5 -1
  136. package/dist/commands/teamwork/teammate-config.d.ts.map +1 -1
  137. package/dist/commands/teamwork/teammate-config.js +62 -19
  138. package/dist/commands/teamwork/teammate-config.js.map +1 -1
  139. package/dist/commands/teamwork/teammate-launch.d.ts +14 -4
  140. package/dist/commands/teamwork/teammate-launch.d.ts.map +1 -1
  141. package/dist/commands/teamwork/teammate-launch.js +129 -20
  142. package/dist/commands/teamwork/teammate-launch.js.map +1 -1
  143. package/dist/commands/teamwork/wait.d.ts +2 -0
  144. package/dist/commands/teamwork/wait.d.ts.map +1 -1
  145. package/dist/commands/teamwork/wait.js +44 -3
  146. package/dist/commands/teamwork/wait.js.map +1 -1
  147. package/dist/commands/update.d.ts.map +1 -1
  148. package/dist/commands/update.js +4 -3
  149. package/dist/commands/update.js.map +1 -1
  150. package/dist/configurators/workflow.d.ts.map +1 -1
  151. package/dist/configurators/workflow.js +14 -32
  152. package/dist/configurators/workflow.js.map +1 -1
  153. package/dist/migrations/manifests/0.7.1-beta.10.json +9 -0
  154. package/dist/migrations/manifests/0.7.1-beta.11.json +9 -0
  155. package/dist/migrations/manifests/0.7.1-beta.12.json +9 -0
  156. package/dist/migrations/manifests/0.7.1-beta.13.json +9 -0
  157. package/dist/migrations/manifests/0.7.1-beta.14.json +11 -0
  158. package/dist/migrations/manifests/0.7.1-beta.15.json +9 -0
  159. package/dist/migrations/manifests/0.7.1-beta.16.json +9 -0
  160. package/dist/migrations/manifests/0.7.1-beta.17.json +9 -0
  161. package/dist/migrations/manifests/0.7.1-beta.18.json +9 -0
  162. package/dist/migrations/manifests/0.7.1-beta.19.json +9 -0
  163. package/dist/migrations/manifests/0.7.1-beta.6.json +1 -1
  164. package/dist/migrations/manifests/0.7.2-beta.0.json +11 -0
  165. package/dist/templates/codex/hooks/session-start.py +14 -11
  166. package/dist/templates/codex/hooks.json +10 -0
  167. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-teamwork.md +14 -2
  168. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +1 -1
  169. package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +4 -0
  170. package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +6 -0
  171. package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +14 -4
  172. package/dist/templates/common/bundled-skills/trellis-teamwork/references/coordination-modes.md +31 -2
  173. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-configuration-matrix.md +37 -31
  174. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +94 -24
  175. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +112 -20
  176. package/dist/templates/copilot/hooks/session-start.py +14 -11
  177. package/dist/templates/copilot/index.d.ts +1 -6
  178. package/dist/templates/copilot/index.d.ts.map +1 -1
  179. package/dist/templates/copilot/index.js +1 -14
  180. package/dist/templates/copilot/index.js.map +1 -1
  181. package/dist/templates/markdown/spec/backend/index.md.txt +4 -35
  182. package/dist/templates/markdown/spec/frontend/index.md.txt +4 -36
  183. package/dist/templates/omp/extensions/trellis/index.ts.txt +1 -1
  184. package/dist/templates/pi/extensions/trellis/index.ts.txt +17 -1
  185. package/dist/templates/shared-hooks/guard-teamwork-wait.py +53 -0
  186. package/dist/templates/shared-hooks/index.d.ts +1 -1
  187. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  188. package/dist/templates/shared-hooks/index.js +1 -0
  189. package/dist/templates/shared-hooks/index.js.map +1 -1
  190. package/dist/templates/shared-hooks/inject-workflow-state.py +8 -3
  191. package/dist/templates/shared-hooks/session-start.py +16 -12
  192. package/dist/templates/trellis/agents/delegated-worktree-primary-agent.md +9 -0
  193. package/dist/templates/trellis/agents/profiles/architecture.md +9 -0
  194. package/dist/templates/trellis/agents/profiles/diagnosis.md +9 -0
  195. package/dist/templates/trellis/agents/profiles/exploration.md +9 -0
  196. package/dist/templates/trellis/agents/profiles/research.md +9 -0
  197. package/dist/templates/trellis/agents/profiles/review.md +9 -0
  198. package/dist/templates/trellis/agents/profiles/verification.md +9 -0
  199. package/dist/templates/trellis/agents/teammate-profiles.json +56 -4
  200. package/dist/templates/trellis/agents/teammate.md +4 -63
  201. package/dist/templates/trellis/config.yaml +2 -5
  202. package/dist/templates/trellis/index.d.ts +1 -0
  203. package/dist/templates/trellis/index.d.ts.map +1 -1
  204. package/dist/templates/trellis/index.js +16 -4
  205. package/dist/templates/trellis/index.js.map +1 -1
  206. package/dist/templates/trellis/scripts/common/task_continuity.py +54 -28
  207. package/dist/templates/trellis/scripts/common/task_store.py +7 -2
  208. package/dist/templates/trellis/scripts/common/task_workflow.py +270 -18
  209. package/dist/templates/trellis/scripts/task.py +27 -3
  210. package/dist/templates/trellis/teamwork.json +10 -0
  211. package/dist/templates/trellis/workflow.md +7 -0
  212. package/dist/utils/asset-backup.d.ts.map +1 -1
  213. package/dist/utils/asset-backup.js +10 -4
  214. package/dist/utils/asset-backup.js.map +1 -1
  215. package/dist/utils/template-fetcher.d.ts +4 -19
  216. package/dist/utils/template-fetcher.d.ts.map +1 -1
  217. package/dist/utils/template-fetcher.js +9 -45
  218. package/dist/utils/template-fetcher.js.map +1 -1
  219. package/package.json +3 -2
  220. package/dist/templates/copilot/prompts/before-dev.prompt.md +0 -39
  221. package/dist/templates/copilot/prompts/brainstorm.prompt.md +0 -13
  222. package/dist/templates/copilot/prompts/break-loop.prompt.md +0 -129
  223. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +0 -170
  224. package/dist/templates/copilot/prompts/check.prompt.md +0 -110
  225. package/dist/templates/copilot/prompts/create-command.prompt.md +0 -116
  226. package/dist/templates/copilot/prompts/finish-work.prompt.md +0 -22
  227. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +0 -223
  228. package/dist/templates/copilot/prompts/onboard.prompt.md +0 -362
  229. package/dist/templates/copilot/prompts/record-session.prompt.md +0 -58
  230. package/dist/templates/copilot/prompts/start.prompt.md +0 -65
  231. package/dist/templates/copilot/prompts/update-spec.prompt.md +0 -358
@@ -1,12 +1,13 @@
1
+ import crypto from "node:crypto";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
4
+ import { isDeepStrictEqual } from "node:util";
3
5
  import { isTaskPhaseId, parseTaskWorkflowState, } from "@pennixrv/trellis-core/task";
4
6
  import { assertCoordinationTransition, readTeamworkEvents, readCoordinationState, recordCoordinationEvent, } from "@pennixrv/trellis-core/teamwork";
5
- import { cleanupSettledTeamworkArtifacts, readTeammateReport, writeTeammateQueueFact, writeTeammateClaim, writeTeammateDisposition, validateTeamworkPlan, } from "./artifact.js";
6
- import { assertTeammateAdapterContract, } from "./teammate-adapters.js";
7
+ import { TeammateReportReadError, cleanupSettledTeamworkArtifacts, readTeammateReport, writeTeammateQueueFact, writeTeammateClaim, writeTeammateDisposition, validateTeamworkPlan, } from "./artifact.js";
8
+ import { assertTeammateAdapterContract, assertTeammateHostCapability, } from "./teammate-adapters.js";
7
9
  import { effectiveTeammateCapacity, resolveTeammateRuntimeConfig, } from "./teammate-config.js";
8
- import { resolveTeammateProfile } from "./profiles.js";
9
- import { buildTeammateLaunchSpec, } from "./teammate-launch.js";
10
+ import { readProcessIdentity } from "./process-identity.js";
10
11
  export function assertTeamworkPlanCurrent(plan, taskRoot) {
11
12
  const taskFile = path.join(taskRoot, "task.json");
12
13
  const stat = fs.lstatSync(taskFile);
@@ -37,6 +38,165 @@ export function assertTeamworkPlanCurrent(plan, taskRoot) {
37
38
  throw new Error("Teamwork plan does not match the current active task phase and revisions");
38
39
  }
39
40
  }
41
+ function terminalReceiptFromProjection(plan, projection) {
42
+ const mismatch = (reason) => {
43
+ throw new Error(`batch '${plan.batchId}' ${reason}`);
44
+ };
45
+ if (projection.issues.length > 0)
46
+ mismatch("has coordination projection issues");
47
+ const batch = projection.batches.find((entry) => entry.batchId === plan.batchId);
48
+ if (!batch)
49
+ return mismatch("does not match the frozen plan");
50
+ if (batch.planHash !== plan.planHash)
51
+ return mismatch("does not match the frozen plan");
52
+ if (batch.state !== "ALL_ACCEPTED" && batch.state !== "SETTLED_WITH_FAILURE")
53
+ mismatch("is not terminal");
54
+ const effectiveCapacity = batch.effectiveCapacity;
55
+ if (typeof effectiveCapacity !== "number" ||
56
+ !Number.isSafeInteger(effectiveCapacity) ||
57
+ effectiveCapacity < 1)
58
+ return mismatch("has no valid effective capacity");
59
+ if (projection.leases.some((lease) => lease.batchId === plan.batchId))
60
+ mismatch("still owns capacity leases");
61
+ const projectedAssignments = projection.assignments.filter((assignment) => assignment.batchId === plan.batchId);
62
+ if (projectedAssignments.length !== plan.assignments.length ||
63
+ new Set(projectedAssignments.map((assignment) => assignment.assignmentId))
64
+ .size !== plan.assignments.length)
65
+ mismatch("does not contain the exact frozen assignment set");
66
+ const assignmentById = new Map(projectedAssignments.map((assignment) => [
67
+ assignment.assignmentId,
68
+ assignment,
69
+ ]));
70
+ const planAssignmentIds = new Set(plan.assignments.map((assignment) => assignment.assignmentId));
71
+ const attempts = projection.attempts.filter((attempt) => attempt.batchId === plan.batchId);
72
+ const teammates = projection.teammates.filter((teammate) => teammate.batchId === plan.batchId);
73
+ if (attempts.length !== teammates.length)
74
+ mismatch("has unmatched provider attempts and teammates");
75
+ for (const attempt of attempts) {
76
+ if (!attempt.assignmentId ||
77
+ !planAssignmentIds.has(attempt.assignmentId) ||
78
+ !attempt.teammateId ||
79
+ attempt.terminal !== true ||
80
+ !["REPORT_SUBMITTED", "FAILED", "FINAL_FAILED"].includes(attempt.state))
81
+ mismatch("has an unresolved provider attempt");
82
+ const teammate = teammates.find((entry) => entry.attemptId === attempt.attemptId &&
83
+ entry.teammateId === attempt.teammateId);
84
+ if (teammate?.terminal !== true ||
85
+ (teammate?.state !== "REPORT_SUBMITTED" && teammate?.state !== "FAILED"))
86
+ mismatch("has an unresolved teammate terminal fact");
87
+ }
88
+ const assignments = plan.assignments.map((assignment) => {
89
+ const projected = assignmentById.get(assignment.assignmentId);
90
+ if (!projected ||
91
+ (projected.state !== "ACCEPTED" && projected.state !== "FINAL_FAILED"))
92
+ return mismatch(`assignment '${assignment.assignmentId}' is not settled`);
93
+ if (projected.state === "ACCEPTED") {
94
+ const acceptedAttempt = attempts.find((attempt) => attempt.attemptId === projected.attemptId &&
95
+ attempt.assignmentId === assignment.assignmentId);
96
+ if (acceptedAttempt?.state !== "REPORT_SUBMITTED")
97
+ return mismatch(`assignment '${assignment.assignmentId}' has no accepted attempt`);
98
+ }
99
+ return {
100
+ assignmentId: assignment.assignmentId,
101
+ status: projected.state,
102
+ };
103
+ });
104
+ const status = assignments.every((item) => item.status === "ACCEPTED")
105
+ ? "ALL_ACCEPTED"
106
+ : "SETTLED_WITH_FAILURE";
107
+ if (batch.state !== status)
108
+ mismatch("terminal state disagrees with its assignment dispositions");
109
+ const retryCounts = Object.fromEntries(plan.assignments.flatMap((assignment) => {
110
+ const count = assignmentById.get(assignment.assignmentId)?.retryCount;
111
+ if (count === undefined)
112
+ return mismatch(`assignment '${assignment.assignmentId}' is missing`);
113
+ const started = attempts.some((attempt) => attempt.assignmentId === assignment.assignmentId);
114
+ return started || count > 0 ? [[assignment.assignmentId, count]] : [];
115
+ }));
116
+ const evaluationGroups = [
117
+ ...new Set(plan.assignments
118
+ .map((assignment) => assignment.evaluationGroup?.id)
119
+ .filter((id) => id !== undefined)),
120
+ ].map((id) => ({
121
+ id,
122
+ status: plan.assignments
123
+ .filter((assignment) => assignment.evaluationGroup?.id === id)
124
+ .every((assignment) => assignmentById.get(assignment.assignmentId)?.state === "ACCEPTED")
125
+ ? "COMPLETE"
126
+ : "INCOMPLETE",
127
+ }));
128
+ return {
129
+ schemaVersion: 1,
130
+ batchId: plan.batchId,
131
+ status,
132
+ assignments,
133
+ capacity: effectiveCapacity,
134
+ retryCounts,
135
+ evaluationGroups,
136
+ };
137
+ }
138
+ function reconciliationReceipt(plan, projection, capacity) {
139
+ return {
140
+ schemaVersion: 1,
141
+ batchId: plan.batchId,
142
+ status: "RECONCILIATION_REQUIRED",
143
+ assignments: plan.assignments.map((item) => {
144
+ const current = projection.assignments.find((entry) => entry.batchId === plan.batchId &&
145
+ entry.assignmentId === item.assignmentId);
146
+ return {
147
+ assignmentId: item.assignmentId,
148
+ status: current?.state === "ACCEPTED" || current?.state === "FINAL_FAILED"
149
+ ? current.state
150
+ : "UNKNOWN",
151
+ };
152
+ }),
153
+ capacity: projection.batches.find((entry) => entry.batchId === plan.batchId)
154
+ ?.effectiveCapacity ?? capacity,
155
+ retryCounts: Object.fromEntries(projection.assignments
156
+ .filter((entry) => entry.batchId === plan.batchId)
157
+ .map((entry) => [entry.assignmentId, entry.retryCount])),
158
+ };
159
+ }
160
+ async function recordRecoveryReconciliation(input) {
161
+ const { plan, cwd, by, existingState, projection, capacity, reason } = input;
162
+ if (existingState !== "RECONCILIATION_REQUIRED" &&
163
+ existingState !== "ALL_ACCEPTED" &&
164
+ existingState !== "SETTLED_WITH_FAILURE") {
165
+ await appendTransition({
166
+ sessionId: plan.sessionId,
167
+ cwd,
168
+ by,
169
+ batchId: plan.batchId,
170
+ entity: "batch",
171
+ id: plan.batchId,
172
+ state: "RECONCILIATION_REQUIRED",
173
+ states: new Map([[plan.batchId, existingState]]),
174
+ planHash: plan.planHash,
175
+ payload: { recoveredAfterRestart: true },
176
+ });
177
+ }
178
+ const receipt = reconciliationReceipt(plan, projection, capacity);
179
+ await appendAction({
180
+ sessionId: plan.sessionId,
181
+ cwd,
182
+ by,
183
+ batchId: plan.batchId,
184
+ entity: "batch",
185
+ action: "reconciliation_required",
186
+ reason,
187
+ });
188
+ await appendAction({
189
+ sessionId: plan.sessionId,
190
+ cwd,
191
+ by,
192
+ batchId: plan.batchId,
193
+ entity: "batch",
194
+ action: "receipt_recorded",
195
+ id: `${plan.batchId}:RECONCILIATION_REQUIRED`,
196
+ payload: { ...receipt, reason },
197
+ });
198
+ return receipt;
199
+ }
40
200
  async function recoverExistingBatch(input) {
41
201
  const projection = await readCoordinationState({
42
202
  sessionId: input.plan.sessionId,
@@ -52,64 +212,55 @@ async function recoverExistingBatch(input) {
52
212
  sessionId: input.plan.sessionId,
53
213
  cwd: input.cwd,
54
214
  });
55
- const saved = events.find((event) => event.kind === "coordination" &&
56
- event.batchId === input.plan.batchId &&
57
- event.action === "receipt_recorded" &&
58
- event.payload &&
59
- (event.payload.status === "ALL_ACCEPTED" ||
60
- event.payload.status === "SETTLED_WITH_FAILURE"));
61
- if (saved?.kind === "coordination" && saved.payload) {
62
- return saved.payload;
63
- }
64
- const states = new Map([
65
- [input.plan.batchId, existing.state],
66
- ]);
67
- if (existing.state !== "RECONCILIATION_REQUIRED") {
68
- await appendTransition({
69
- sessionId: input.plan.sessionId,
70
- by: input.by,
71
- batchId: input.plan.batchId,
72
- entity: "batch",
73
- id: input.plan.batchId,
74
- state: "RECONCILIATION_REQUIRED",
75
- states,
76
- planHash: input.plan.planHash,
77
- payload: { recoveredAfterRestart: true },
78
- });
215
+ if (existing.state === "ALL_ACCEPTED" ||
216
+ existing.state === "SETTLED_WITH_FAILURE") {
217
+ let receipt;
218
+ try {
219
+ receipt = terminalReceiptFromProjection(input.plan, projection);
220
+ }
221
+ catch (error) {
222
+ return recordRecoveryReconciliation({
223
+ ...input,
224
+ existingState: existing.state,
225
+ projection,
226
+ reason: error instanceof Error ? error.message : String(error),
227
+ });
228
+ }
229
+ const receipts = events.filter((event) => event.kind === "coordination" &&
230
+ event.batchId === input.plan.batchId &&
231
+ event.action === "receipt_recorded" &&
232
+ event.payload?.status !== "RECONCILIATION_REQUIRED");
233
+ if (receipts.length > 1 ||
234
+ (receipts.length === 1 &&
235
+ !isDeepStrictEqual(receipts[0]?.payload, receipt))) {
236
+ return recordRecoveryReconciliation({
237
+ ...input,
238
+ existingState: existing.state,
239
+ projection,
240
+ reason: "saved terminal receipt disagrees with durable coordination facts",
241
+ });
242
+ }
243
+ if (receipts.length === 0) {
244
+ await appendAction({
245
+ sessionId: input.plan.sessionId,
246
+ cwd: input.cwd,
247
+ by: input.by,
248
+ batchId: input.plan.batchId,
249
+ entity: "batch",
250
+ action: "receipt_recorded",
251
+ id: `${input.plan.batchId}:${receipt.status}`,
252
+ payload: { ...receipt },
253
+ });
254
+ }
255
+ await assertTeamworkReceiptDurable(input.plan, receipt, input.cwd);
256
+ return receipt;
79
257
  }
80
- const assignments = input.plan.assignments.map((item) => {
81
- const current = projection.assignments.find((entry) => entry.batchId === input.plan.batchId &&
82
- entry.assignmentId === item.assignmentId);
83
- return {
84
- assignmentId: item.assignmentId,
85
- status: current?.state === "ACCEPTED" || current?.state === "FINAL_FAILED"
86
- ? current.state
87
- : "UNKNOWN",
88
- };
89
- });
90
- const retryCounts = Object.fromEntries(projection.assignments
91
- .filter((entry) => entry.batchId === input.plan.batchId)
92
- .map((entry) => [entry.assignmentId, entry.retryCount]));
93
- const receipt = {
94
- schemaVersion: 1,
95
- batchId: input.plan.batchId,
96
- status: "RECONCILIATION_REQUIRED",
97
- assignments,
98
- capacity: existing.effectiveCapacity ?? input.capacity,
99
- retryCounts,
100
- };
101
- await recordCoordinationEvent({
102
- sessionId: input.plan.sessionId,
103
- cwd: input.cwd,
104
- by: input.by,
105
- batchId: input.plan.batchId,
106
- entity: "batch",
107
- action: "receipt_recorded",
108
- receiptId: `${input.plan.batchId}:RECONCILIATION_REQUIRED`,
109
- payload: { ...receipt, reason: "recovered batch requires reconciliation" },
110
- idempotencyKey: `${input.plan.batchId}:RECONCILIATION_REQUIRED:receipt`,
258
+ return recordRecoveryReconciliation({
259
+ ...input,
260
+ existingState: existing.state,
261
+ projection,
262
+ reason: "recovered nonterminal batch requires reconciliation",
111
263
  });
112
- return receipt;
113
264
  }
114
265
  function transitionState(states, key, next, entity) {
115
266
  assertCoordinationTransition(entity, states.get(key), next);
@@ -119,6 +270,7 @@ async function appendTransition(input) {
119
270
  transitionState(input.states, input.id, input.state, input.entity);
120
271
  await recordCoordinationEvent({
121
272
  sessionId: input.sessionId,
273
+ ...(input.cwd ? { cwd: input.cwd } : {}),
122
274
  by: input.by,
123
275
  batchId: input.batchId,
124
276
  entity: input.entity,
@@ -141,6 +293,7 @@ async function appendTransition(input) {
141
293
  async function appendAction(input) {
142
294
  await recordCoordinationEvent({
143
295
  sessionId: input.sessionId,
296
+ ...(input.cwd ? { cwd: input.cwd } : {}),
144
297
  by: input.by,
145
298
  batchId: input.batchId,
146
299
  entity: input.entity,
@@ -164,47 +317,60 @@ async function appendAction(input) {
164
317
  idempotencyKey: `${input.batchId}:${input.action}:${input.id ?? input.leaseId ?? input.attemptId ?? "event"}`,
165
318
  });
166
319
  }
167
- function readPlanFile(planPath) {
320
+ export function readPlanFile(planPath) {
321
+ const stat = fs.lstatSync(planPath);
322
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 2 * 1024 * 1024)
323
+ throw new Error("Teamwork plan must be a regular file under 2097152 bytes");
168
324
  const raw = fs.readFileSync(planPath, "utf8");
169
325
  return JSON.parse(raw);
170
326
  }
171
327
  export function loadTeamworkPlan(planPath, taskRoot) {
172
- const plan = validateTeamworkPlan(readPlanFile(planPath), taskRoot);
328
+ const plan = validateTeamworkPlan(readPlanFile(planPath), taskRoot, undefined, { allowExpired: true });
173
329
  assertTeamworkPlanCurrent(plan, taskRoot);
174
330
  return plan;
175
331
  }
176
332
  export async function runTeamworkCoordinator(input) {
177
- const plan = validateTeamworkPlan(input.plan, input.taskRoot);
333
+ const plan = validateTeamworkPlan(input.plan, input.taskRoot, undefined, {
334
+ allowExpired: true,
335
+ });
178
336
  assertTeamworkPlanCurrent(plan, input.taskRoot);
179
337
  const cwd = input.cwd ?? input.taskRoot;
180
338
  const frozenLaunches = new Map();
181
339
  for (const assignment of plan.assignments) {
182
340
  assertTeammateAdapterContract(assignment.provider);
183
- const profile = resolveTeammateProfile({
184
- cwd,
185
- profile: assignment.profile,
186
- provider: assignment.provider,
187
- model: assignment.model,
188
- });
189
- frozenLaunches.set(assignment.assignmentId, {
190
- profile,
191
- launch: buildTeammateLaunchSpec({
192
- cwd,
193
- profile,
194
- host: input.host,
195
- contextFiles: assignment.contextFiles,
196
- contextManifests: assignment.contextManifests,
197
- protectedTargets: plan.protectedTargets,
198
- }),
199
- });
341
+ assertTeammateHostCapability(assignment.provider, input.host, assignment.execution.profile.options);
342
+ frozenLaunches.set(assignment.assignmentId, assignment.execution);
200
343
  }
201
- const config = input.config ?? resolveTeammateRuntimeConfig(cwd);
344
+ const config = input.config ??
345
+ resolveTeammateRuntimeConfig(cwd, process.env, plan.coordinationMode);
202
346
  const capacity = effectiveTeammateCapacity({
203
347
  requested: plan.requestedCapacity,
348
+ assignmentCount: plan.assignments.length,
204
349
  config,
205
350
  verifiedHostCap: input.host.verifiedHostCap,
206
351
  });
207
352
  const by = input.by ?? "main";
353
+ const existing = await readCoordinationState({
354
+ sessionId: plan.sessionId,
355
+ cwd,
356
+ });
357
+ const settled = existing.batches.find((batch) => batch.batchId === plan.batchId);
358
+ if (settled &&
359
+ ["ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(settled.state)) {
360
+ let terminal = false;
361
+ try {
362
+ terminalReceiptFromProjection(plan, existing);
363
+ terminal = true;
364
+ }
365
+ catch {
366
+ // Inconsistent terminal facts still require the protected recovery path.
367
+ }
368
+ if (terminal) {
369
+ const recovered = await recoverExistingBatch({ plan, cwd, by, capacity });
370
+ if (recovered)
371
+ return recovered;
372
+ }
373
+ }
208
374
  const batchStates = new Map();
209
375
  const assignmentStates = new Map();
210
376
  const attemptStates = new Map();
@@ -215,19 +381,8 @@ export async function runTeamworkCoordinator(input) {
215
381
  capacity,
216
382
  });
217
383
  let guardHeld = true;
218
- let recovered;
219
- try {
220
- recovered = await recoverExistingBatch({ plan, cwd, by, capacity });
221
- }
222
- catch (error) {
223
- await input.guard.release({
224
- batchId: plan.batchId,
225
- planHash: plan.planHash,
226
- capacity,
227
- });
228
- guardHeld = false;
229
- throw error;
230
- }
384
+ // Failed recovery cannot prove that an older provider process exited.
385
+ const recovered = await recoverExistingBatch({ plan, cwd, by, capacity });
231
386
  if (recovered) {
232
387
  if (recovered.status !== "RECONCILIATION_REQUIRED") {
233
388
  await input.guard.release({
@@ -239,6 +394,14 @@ export async function runTeamworkCoordinator(input) {
239
394
  }
240
395
  return recovered;
241
396
  }
397
+ if (Date.parse(plan.deadline) <= Date.now()) {
398
+ await input.guard.release({
399
+ batchId: plan.batchId,
400
+ planHash: plan.planHash,
401
+ capacity,
402
+ });
403
+ throw new Error("deadline elapsed before batch dispatch");
404
+ }
242
405
  try {
243
406
  for (const [queueIndex, assignment] of plan.assignments.entries()) {
244
407
  writeTeammateQueueFact({
@@ -328,8 +491,8 @@ export async function runTeamworkCoordinator(input) {
328
491
  const active = new Map();
329
492
  const waits = new Map();
330
493
  const final = new Map();
331
- const acceptedAttempts = new Map();
332
494
  const retryCounts = {};
495
+ let admissionClosed = false;
333
496
  async function reconcile(reason) {
334
497
  await appendAction({
335
498
  sessionId: plan.sessionId,
@@ -372,9 +535,39 @@ export async function runTeamworkCoordinator(input) {
372
535
  });
373
536
  return receipt;
374
537
  }
538
+ async function failPending(reason) {
539
+ admissionClosed = true;
540
+ while (pending.length > 0) {
541
+ const assignment = pending.shift();
542
+ if (!assignment)
543
+ continue;
544
+ await appendTransition({
545
+ sessionId: plan.sessionId,
546
+ by,
547
+ batchId: plan.batchId,
548
+ entity: "assignment",
549
+ id: assignment.assignmentId,
550
+ state: "FINAL_FAILED",
551
+ states: assignmentStates,
552
+ assignmentId: assignment.assignmentId,
553
+ retryCount: retryCounts[assignment.assignmentId] ?? 0,
554
+ payload: { reason, dispatched: false },
555
+ });
556
+ final.set(assignment.assignmentId, "FINAL_FAILED");
557
+ }
558
+ }
375
559
  async function dispatch(slot, assignment, retryCount) {
376
- const attemptId = `${assignment.assignmentId}.attempt.${retryCount}`;
377
- const teammateId = `${assignment.assignmentId}.teammate.${retryCount}`;
560
+ const boundedId = (kind) => {
561
+ const candidate = `${assignment.assignmentId}.${kind}.${retryCount}`;
562
+ return candidate.length <= 64
563
+ ? candidate
564
+ : crypto
565
+ .createHash("sha256")
566
+ .update(`${assignment.assignmentId}:${kind}:${retryCount}`)
567
+ .digest("hex");
568
+ };
569
+ const attemptId = boundedId("attempt");
570
+ const teammateId = boundedId("teammate");
378
571
  const frozen = frozenLaunches.get(assignment.assignmentId);
379
572
  if (!frozen)
380
573
  throw new Error(`missing frozen launch for '${assignment.assignmentId}'`);
@@ -388,6 +581,7 @@ export async function runTeamworkCoordinator(input) {
388
581
  profile,
389
582
  launch,
390
583
  };
584
+ input.runtime.prepare?.(context);
391
585
  await appendTransition({
392
586
  sessionId: plan.sessionId,
393
587
  by,
@@ -510,6 +704,7 @@ export async function runTeamworkCoordinator(input) {
510
704
  teammateId,
511
705
  slot,
512
706
  retryCount,
707
+ transitionKey: `attempt:${attemptId}`,
513
708
  ...(context.providerSessionId
514
709
  ? { providerSessionId: context.providerSessionId }
515
710
  : {}),
@@ -562,7 +757,63 @@ export async function runTeamworkCoordinator(input) {
562
757
  });
563
758
  guardHeld = false;
564
759
  }
760
+ async function resumeFromWait(current) {
761
+ if (assignmentStates.get(current.assignment.assignmentId) !==
762
+ "WAITING_FOR_MAIN_COMMAND")
763
+ return;
764
+ const transitionKey = `continuation:${current.continuations}`;
765
+ await appendTransition({
766
+ sessionId: plan.sessionId,
767
+ by,
768
+ batchId: plan.batchId,
769
+ entity: "attempt",
770
+ id: current.context.attemptId,
771
+ state: "RUNNING",
772
+ states: attemptStates,
773
+ assignmentId: current.assignment.assignmentId,
774
+ attemptId: current.context.attemptId,
775
+ teammateId: current.context.teammateId,
776
+ retryCount: current.retryCount,
777
+ ...(current.providerSessionId
778
+ ? { providerSessionId: current.providerSessionId }
779
+ : {}),
780
+ transitionKey,
781
+ });
782
+ await appendTransition({
783
+ sessionId: plan.sessionId,
784
+ by,
785
+ batchId: plan.batchId,
786
+ entity: "teammate",
787
+ id: current.context.teammateId,
788
+ state: "RUNNING",
789
+ states: teammateStates,
790
+ attemptId: current.context.attemptId,
791
+ teammateId: current.context.teammateId,
792
+ ...(current.providerSessionId
793
+ ? { providerSessionId: current.providerSessionId }
794
+ : {}),
795
+ transitionKey,
796
+ });
797
+ await appendTransition({
798
+ sessionId: plan.sessionId,
799
+ by,
800
+ batchId: plan.batchId,
801
+ entity: "assignment",
802
+ id: current.assignment.assignmentId,
803
+ state: "EXECUTING",
804
+ states: assignmentStates,
805
+ assignmentId: current.assignment.assignmentId,
806
+ attemptId: current.context.attemptId,
807
+ teammateId: current.context.teammateId,
808
+ retryCount: current.retryCount,
809
+ ...(current.providerSessionId
810
+ ? { providerSessionId: current.providerSessionId }
811
+ : {}),
812
+ transitionKey,
813
+ });
814
+ }
565
815
  async function cancelAndDrain(reason, terminal) {
816
+ await failPending(reason);
566
817
  if (batchStates.get(plan.batchId) === "RUNNING") {
567
818
  await appendTransition({
568
819
  sessionId: plan.sessionId,
@@ -575,52 +826,68 @@ export async function runTeamworkCoordinator(input) {
575
826
  payload: { reason },
576
827
  });
577
828
  }
578
- if (!input.runtime.cancel && active.size > 0 && !terminal) {
579
- return reconcile(`host cannot confirm cancellation: ${reason}`);
580
- }
581
- for (const current of [...active.values()]) {
582
- let result;
583
- if (terminal?.current === current) {
584
- result = terminal.result;
829
+ const cancel = input.runtime.cancel;
830
+ const outcomes = await Promise.all([...active.values()].map(async (current) => {
831
+ if (terminal?.current === current)
832
+ return { current, result: terminal.result };
833
+ if (!cancel)
834
+ return {
835
+ current,
836
+ error: new Error(`host cannot cancel active attempt: ${reason}`),
837
+ };
838
+ try {
839
+ return { current, result: await cancel(current.context) };
585
840
  }
586
- else {
587
- try {
588
- if (!input.runtime.cancel)
589
- return reconcile(`host cannot cancel active attempt: ${reason}`);
590
- result = await input.runtime.cancel(current.context);
591
- }
592
- catch (error) {
593
- return reconcile(error instanceof Error ? error.message : String(error));
594
- }
841
+ catch (error) {
842
+ return { current, error };
595
843
  }
844
+ }));
845
+ let cancellationFailure;
846
+ for (const outcome of outcomes) {
847
+ const { current } = outcome;
848
+ if ("error" in outcome) {
849
+ cancellationFailure ??=
850
+ outcome.error instanceof Error
851
+ ? outcome.error.message
852
+ : String(outcome.error);
853
+ continue;
854
+ }
855
+ const { result } = outcome;
596
856
  if (result.kind === "reconciliation" ||
597
857
  result.kind === "timeout" ||
598
858
  result.terminal !== true) {
599
- return reconcile(`attempt cancellation is not terminal: ${reason}`);
859
+ cancellationFailure ??= `attempt cancellation is not terminal: ${reason}`;
860
+ continue;
600
861
  }
601
862
  if (result.providerSessionId &&
602
863
  current.providerSessionId &&
603
864
  result.providerSessionId !== current.providerSessionId) {
604
- return reconcile("cancellation returned a different host session");
865
+ cancellationFailure ??=
866
+ "cancellation returned a different host session";
867
+ continue;
605
868
  }
606
869
  if (result.providerSessionId) {
607
870
  current.providerSessionId = result.providerSessionId;
608
871
  current.context.providerSessionId = result.providerSessionId;
609
872
  }
610
873
  try {
611
- if (result.kind === "report")
874
+ await resumeFromWait(current);
875
+ if (result.kind === "report") {
612
876
  await complete(current, false);
877
+ }
613
878
  else
614
879
  await fail(current, result.reason ?? "cancelled by coordinator", false);
615
880
  }
616
881
  catch (error) {
617
- return reconcile(error instanceof Error ? error.message : String(error));
882
+ cancellationFailure ??=
883
+ error instanceof Error ? error.message : String(error);
884
+ continue;
618
885
  }
619
886
  active.delete(current.slot);
620
887
  waits.delete(current.slot);
621
888
  }
622
- if (pending.length > 0)
623
- return reconcile(`queued assignment remains after cancellation: ${reason}`);
889
+ if (cancellationFailure)
890
+ return reconcile(cancellationFailure);
624
891
  return settle();
625
892
  }
626
893
  async function fail(activeWork, reason, allowRetry = true) {
@@ -657,7 +924,7 @@ export async function runTeamworkCoordinator(input) {
657
924
  : {}),
658
925
  terminal: true,
659
926
  });
660
- if (allowRetry && retryCount < assignment.maxRetries) {
927
+ if (allowRetry && !admissionClosed && retryCount < assignment.maxRetries) {
661
928
  await release(activeWork);
662
929
  retryCounts[assignment.assignmentId] = retryCount + 1;
663
930
  pending.unshift(assignment);
@@ -743,7 +1010,6 @@ export async function runTeamworkCoordinator(input) {
743
1010
  });
744
1011
  await release(activeWork);
745
1012
  final.set(assignment.assignmentId, "ACCEPTED");
746
- acceptedAttempts.set(assignment.assignmentId, context.attemptId);
747
1013
  }
748
1014
  async function complete(activeWork, allowRetry = true) {
749
1015
  const { assignment, context, retryCount } = activeWork;
@@ -782,9 +1048,23 @@ export async function runTeamworkCoordinator(input) {
782
1048
  });
783
1049
  }
784
1050
  catch (error) {
1051
+ if (error instanceof TeammateReportReadError && error.kind !== "missing")
1052
+ throw error;
785
1053
  await fail(activeWork, error instanceof Error ? error.message : String(error), allowRetry);
786
1054
  return;
787
1055
  }
1056
+ if (context.launch.reportVersion === 2 && report.schemaVersion !== 2) {
1057
+ await fail(activeWork, "report schema_version must match the frozen result contract (2)", allowRetry);
1058
+ return;
1059
+ }
1060
+ if (report.status === "blocked") {
1061
+ await fail(activeWork, `teammate reported blocked: ${report.evidence[0]?.summary ?? "blocker evidence missing"}`, false);
1062
+ return;
1063
+ }
1064
+ if (report.status !== "complete") {
1065
+ await fail(activeWork, `teammate reported ${report.status}`, allowRetry);
1066
+ return;
1067
+ }
788
1068
  if (input.runtime.validateReport &&
789
1069
  !(await input.runtime.validateReport(context, report))) {
790
1070
  await fail(activeWork, "report rejected by acceptance contract", allowRetry);
@@ -870,7 +1150,6 @@ export async function runTeamworkCoordinator(input) {
870
1150
  const cleanupFailures = cleanupSettledTeamworkArtifacts({
871
1151
  taskRoot: input.taskRoot,
872
1152
  assignments: plan.assignments,
873
- acceptedAttempts,
874
1153
  });
875
1154
  if (cleanupFailures.length > 0) {
876
1155
  return reconcile(`Teamwork batch cleanup failed: ${cleanupFailures.join("; ")}`);
@@ -918,20 +1197,44 @@ export async function runTeamworkCoordinator(input) {
918
1197
  id: `${plan.batchId}:${status}`,
919
1198
  payload: { ...receipt },
920
1199
  });
921
- await assertTeamworkReceiptDurable(plan.sessionId, plan.batchId, plan.planHash, cwd);
1200
+ await assertTeamworkReceiptDurable(plan, receipt, cwd);
922
1201
  await releaseGuard();
923
1202
  return receipt;
924
1203
  }
925
1204
  async function fill() {
1205
+ if (admissionClosed)
1206
+ return;
926
1207
  while (active.size < capacity && pending.length > 0) {
927
- const assignment = pending.shift();
1208
+ if (Date.parse(plan.deadline) <= Date.now()) {
1209
+ await failPending("batch deadline elapsed before dispatch");
1210
+ return;
1211
+ }
1212
+ try {
1213
+ assertTeamworkPlanCurrent(plan, input.taskRoot);
1214
+ }
1215
+ catch (error) {
1216
+ await failPending(`task phase changed before dispatch: ${error instanceof Error ? error.message : String(error)}`);
1217
+ return;
1218
+ }
1219
+ const assignment = pending[0];
928
1220
  if (!assignment)
929
1221
  return;
930
1222
  const retryCount = retryCounts[assignment.assignmentId] ?? 0;
931
1223
  let slot = 0;
932
1224
  while (active.has(slot))
933
1225
  slot += 1;
934
- await dispatch(slot, assignment, retryCount);
1226
+ try {
1227
+ await dispatch(slot, assignment, retryCount);
1228
+ pending.shift();
1229
+ }
1230
+ catch (error) {
1231
+ if (!assignmentStates.has(assignment.assignmentId) ||
1232
+ assignmentStates.get(assignment.assignmentId) === "QUEUED") {
1233
+ await failPending(`launch preflight failed before dispatch: ${error instanceof Error ? error.message : String(error)}`);
1234
+ return;
1235
+ }
1236
+ throw error;
1237
+ }
935
1238
  }
936
1239
  }
937
1240
  try {
@@ -991,162 +1294,116 @@ export async function runTeamworkCoordinator(input) {
991
1294
  result: next.result,
992
1295
  });
993
1296
  }
1297
+ try {
1298
+ await resumeFromWait(current);
1299
+ }
1300
+ catch (error) {
1301
+ return reconcile(error instanceof Error ? error.message : String(error));
1302
+ }
994
1303
  let releaseSlot = true;
995
1304
  if (next.result.kind === "timeout") {
996
1305
  current.continuations += 1;
997
- if (current.continuations > 8) {
998
- return reconcile("continuation limit exceeded");
1306
+ if (Date.now() >= Date.parse(plan.deadline))
1307
+ return cancelAndDrain("batch deadline elapsed");
1308
+ const transitionKey = `continuation:${current.continuations}`;
1309
+ await appendTransition({
1310
+ sessionId: plan.sessionId,
1311
+ by,
1312
+ batchId: plan.batchId,
1313
+ entity: "attempt",
1314
+ id: current.context.attemptId,
1315
+ state: "TIMEOUT_YIELDED",
1316
+ states: attemptStates,
1317
+ assignmentId: current.assignment.assignmentId,
1318
+ attemptId: current.context.attemptId,
1319
+ teammateId: current.context.teammateId,
1320
+ retryCount: current.retryCount,
1321
+ ...(current.providerSessionId
1322
+ ? { providerSessionId: current.providerSessionId }
1323
+ : {}),
1324
+ transitionKey,
1325
+ });
1326
+ await appendTransition({
1327
+ sessionId: plan.sessionId,
1328
+ by,
1329
+ batchId: plan.batchId,
1330
+ entity: "teammate",
1331
+ id: current.context.teammateId,
1332
+ state: "TIMEOUT_YIELDED",
1333
+ states: teammateStates,
1334
+ attemptId: current.context.attemptId,
1335
+ teammateId: current.context.teammateId,
1336
+ ...(current.providerSessionId
1337
+ ? { providerSessionId: current.providerSessionId }
1338
+ : {}),
1339
+ transitionKey,
1340
+ });
1341
+ await appendTransition({
1342
+ sessionId: plan.sessionId,
1343
+ by,
1344
+ batchId: plan.batchId,
1345
+ entity: "assignment",
1346
+ id: current.assignment.assignmentId,
1347
+ state: "WAITING_FOR_MAIN_COMMAND",
1348
+ states: assignmentStates,
1349
+ assignmentId: current.assignment.assignmentId,
1350
+ attemptId: current.context.attemptId,
1351
+ teammateId: current.context.teammateId,
1352
+ retryCount: current.retryCount,
1353
+ ...(current.providerSessionId
1354
+ ? { providerSessionId: current.providerSessionId }
1355
+ : {}),
1356
+ transitionKey,
1357
+ });
1358
+ await appendTransition({
1359
+ sessionId: plan.sessionId,
1360
+ by,
1361
+ batchId: plan.batchId,
1362
+ entity: "attempt",
1363
+ id: current.context.attemptId,
1364
+ state: "WAITING_FOR_MAIN_COMMAND",
1365
+ states: attemptStates,
1366
+ assignmentId: current.assignment.assignmentId,
1367
+ attemptId: current.context.attemptId,
1368
+ teammateId: current.context.teammateId,
1369
+ retryCount: current.retryCount,
1370
+ ...(current.providerSessionId
1371
+ ? { providerSessionId: current.providerSessionId }
1372
+ : {}),
1373
+ transitionKey,
1374
+ });
1375
+ await appendTransition({
1376
+ sessionId: plan.sessionId,
1377
+ by,
1378
+ batchId: plan.batchId,
1379
+ entity: "teammate",
1380
+ id: current.context.teammateId,
1381
+ state: "WAITING_FOR_MAIN_COMMAND",
1382
+ states: teammateStates,
1383
+ attemptId: current.context.attemptId,
1384
+ teammateId: current.context.teammateId,
1385
+ ...(current.providerSessionId
1386
+ ? { providerSessionId: current.providerSessionId }
1387
+ : {}),
1388
+ transitionKey,
1389
+ });
1390
+ let continued;
1391
+ try {
1392
+ continued = input.runtime.continue(current.context);
999
1393
  }
1000
- else {
1001
- await appendTransition({
1002
- sessionId: plan.sessionId,
1003
- by,
1004
- batchId: plan.batchId,
1005
- entity: "attempt",
1006
- id: current.context.attemptId,
1007
- state: "TIMEOUT_YIELDED",
1008
- states: attemptStates,
1009
- assignmentId: current.assignment.assignmentId,
1010
- attemptId: current.context.attemptId,
1011
- teammateId: current.context.teammateId,
1012
- retryCount: current.retryCount,
1013
- ...(current.providerSessionId
1014
- ? { providerSessionId: current.providerSessionId }
1015
- : {}),
1016
- transitionKey: `continuation:${current.continuations}`,
1017
- });
1018
- await appendTransition({
1019
- sessionId: plan.sessionId,
1020
- by,
1021
- batchId: plan.batchId,
1022
- entity: "teammate",
1023
- id: current.context.teammateId,
1024
- state: "TIMEOUT_YIELDED",
1025
- states: teammateStates,
1026
- attemptId: current.context.attemptId,
1027
- teammateId: current.context.teammateId,
1028
- ...(current.providerSessionId
1029
- ? { providerSessionId: current.providerSessionId }
1030
- : {}),
1031
- transitionKey: `continuation:${current.continuations}`,
1032
- });
1033
- await appendTransition({
1034
- sessionId: plan.sessionId,
1035
- by,
1036
- batchId: plan.batchId,
1037
- entity: "assignment",
1038
- id: current.assignment.assignmentId,
1039
- state: "WAITING_FOR_MAIN_COMMAND",
1040
- states: assignmentStates,
1041
- assignmentId: current.assignment.assignmentId,
1042
- attemptId: current.context.attemptId,
1043
- teammateId: current.context.teammateId,
1044
- retryCount: current.retryCount,
1045
- ...(current.providerSessionId
1046
- ? { providerSessionId: current.providerSessionId }
1047
- : {}),
1048
- transitionKey: `continuation:${current.continuations}`,
1049
- });
1050
- await appendTransition({
1051
- sessionId: plan.sessionId,
1052
- by,
1053
- batchId: plan.batchId,
1054
- entity: "attempt",
1055
- id: current.context.attemptId,
1056
- state: "WAITING_FOR_MAIN_COMMAND",
1057
- states: attemptStates,
1058
- assignmentId: current.assignment.assignmentId,
1059
- attemptId: current.context.attemptId,
1060
- teammateId: current.context.teammateId,
1061
- retryCount: current.retryCount,
1062
- ...(current.providerSessionId
1063
- ? { providerSessionId: current.providerSessionId }
1064
- : {}),
1065
- transitionKey: `continuation:${current.continuations}`,
1066
- });
1067
- await appendTransition({
1068
- sessionId: plan.sessionId,
1069
- by,
1070
- batchId: plan.batchId,
1071
- entity: "teammate",
1072
- id: current.context.teammateId,
1073
- state: "WAITING_FOR_MAIN_COMMAND",
1074
- states: teammateStates,
1075
- attemptId: current.context.attemptId,
1076
- teammateId: current.context.teammateId,
1077
- ...(current.providerSessionId
1078
- ? { providerSessionId: current.providerSessionId }
1079
- : {}),
1080
- transitionKey: `continuation:${current.continuations}`,
1081
- });
1082
- let continued;
1083
- try {
1084
- continued = await input.runtime.continue(current.context);
1085
- }
1086
- catch (error) {
1087
- return reconcile(error instanceof Error ? error.message : String(error));
1088
- }
1089
- if (continued.providerSessionId &&
1090
- current.providerSessionId &&
1091
- continued.providerSessionId !== current.providerSessionId) {
1092
- return reconcile("continuation returned a different host session");
1093
- }
1094
- if (continued.providerSessionId) {
1095
- current.providerSessionId = continued.providerSessionId;
1096
- current.context.providerSessionId = continued.providerSessionId;
1097
- }
1098
- await appendTransition({
1099
- sessionId: plan.sessionId,
1100
- by,
1101
- batchId: plan.batchId,
1102
- entity: "attempt",
1103
- id: current.context.attemptId,
1104
- state: "RUNNING",
1105
- states: attemptStates,
1106
- assignmentId: current.assignment.assignmentId,
1107
- attemptId: current.context.attemptId,
1108
- teammateId: current.context.teammateId,
1109
- retryCount: current.retryCount,
1110
- ...(current.providerSessionId
1111
- ? { providerSessionId: current.providerSessionId }
1112
- : {}),
1113
- transitionKey: `continuation:${current.continuations}`,
1114
- });
1115
- await appendTransition({
1116
- sessionId: plan.sessionId,
1117
- by,
1118
- batchId: plan.batchId,
1119
- entity: "teammate",
1120
- id: current.context.teammateId,
1121
- state: "RUNNING",
1122
- states: teammateStates,
1123
- attemptId: current.context.attemptId,
1124
- teammateId: current.context.teammateId,
1125
- ...(current.providerSessionId
1126
- ? { providerSessionId: current.providerSessionId }
1127
- : {}),
1128
- transitionKey: `continuation:${current.continuations}`,
1129
- });
1130
- await appendTransition({
1131
- sessionId: plan.sessionId,
1132
- by,
1133
- batchId: plan.batchId,
1134
- entity: "assignment",
1135
- id: current.assignment.assignmentId,
1136
- state: "EXECUTING",
1137
- states: assignmentStates,
1138
- assignmentId: current.assignment.assignmentId,
1139
- attemptId: current.context.attemptId,
1140
- teammateId: current.context.teammateId,
1141
- retryCount: current.retryCount,
1142
- ...(current.providerSessionId
1143
- ? { providerSessionId: current.providerSessionId }
1144
- : {}),
1145
- transitionKey: `continuation:${current.continuations}`,
1146
- });
1147
- waits.set(next.slot, Promise.resolve({ slot: next.slot, result: continued }));
1148
- releaseSlot = false;
1394
+ catch (error) {
1395
+ return reconcile(error instanceof Error ? error.message : String(error));
1149
1396
  }
1397
+ waits.set(next.slot, Promise.resolve(continued)
1398
+ .then((result) => ({ slot: next.slot, result }))
1399
+ .catch((error) => ({
1400
+ slot: next.slot,
1401
+ result: {
1402
+ kind: "reconciliation",
1403
+ reason: error instanceof Error ? error.message : String(error),
1404
+ },
1405
+ })));
1406
+ releaseSlot = false;
1150
1407
  }
1151
1408
  else if (next.result.kind === "report") {
1152
1409
  try {
@@ -1177,22 +1434,248 @@ export async function runTeamworkCoordinator(input) {
1177
1434
  }
1178
1435
  return settle();
1179
1436
  }
1180
- export async function assertTeamworkReceiptDurable(sessionId, batchId, planHash, cwd) {
1437
+ export async function assertTeamworkReceiptDurable(plan, receipt, cwd) {
1181
1438
  const [state, events] = await Promise.all([
1182
- readCoordinationState({ sessionId, ...(cwd ? { cwd } : {}) }),
1183
- readTeamworkEvents({ sessionId, ...(cwd ? { cwd } : {}) }),
1439
+ readCoordinationState({
1440
+ sessionId: plan.sessionId,
1441
+ ...(cwd ? { cwd } : {}),
1442
+ }),
1443
+ readTeamworkEvents({
1444
+ sessionId: plan.sessionId,
1445
+ ...(cwd ? { cwd } : {}),
1446
+ }),
1184
1447
  ]);
1185
- const batch = state.batches.find((entry) => entry.batchId === batchId);
1186
- if (batch?.planHash !== planHash ||
1187
- (batch.state !== "ALL_ACCEPTED" && batch.state !== "SETTLED_WITH_FAILURE")) {
1188
- throw new Error(`batch '${batchId}' has no durable terminal receipt`);
1189
- }
1190
- const receiptRecorded = events.some((event) => event.kind === "coordination" &&
1191
- event.batchId === batchId &&
1448
+ const expected = terminalReceiptFromProjection(plan, state);
1449
+ if (!isDeepStrictEqual(receipt, expected))
1450
+ throw new Error(`batch '${plan.batchId}' receipt disagrees with durable facts`);
1451
+ const receipts = events.filter((event) => event.kind === "coordination" &&
1452
+ event.batchId === plan.batchId &&
1192
1453
  event.action === "receipt_recorded" &&
1193
- event.payload?.status === batch.state);
1194
- if (!receiptRecorded) {
1195
- throw new Error(`batch '${batchId}' has no matching durable receipt event`);
1454
+ event.payload?.status !== "RECONCILIATION_REQUIRED");
1455
+ if (receipts.length !== 1 ||
1456
+ !isDeepStrictEqual(receipts[0]?.payload, expected))
1457
+ throw new Error(`batch '${plan.batchId}' has no matching durable receipt event`);
1458
+ }
1459
+ /** Retire uncertain work only from associated terminal or process-birth evidence. */
1460
+ export async function reconcileTeamworkBatch(input) {
1461
+ const { plan, cwd } = input;
1462
+ const projection = await readCoordinationState({
1463
+ sessionId: plan.sessionId,
1464
+ cwd,
1465
+ });
1466
+ const batch = projection.batches.find((entry) => entry.batchId === plan.batchId);
1467
+ if (batch?.planHash !== plan.planHash)
1468
+ throw new Error("Batch does not match the frozen plan");
1469
+ const events = await readTeamworkEvents({ sessionId: plan.sessionId, cwd });
1470
+ const resolving = events.some((event) => event.kind === "coordination" &&
1471
+ event.batchId === plan.batchId &&
1472
+ event.action === "reconciliation_resolved");
1473
+ if (["ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state)) {
1474
+ const receipt = terminalReceiptFromProjection(plan, projection);
1475
+ if (input.execute) {
1476
+ const recovered = await recoverExistingBatch({
1477
+ plan,
1478
+ cwd,
1479
+ by: "main",
1480
+ capacity: receipt.capacity,
1481
+ });
1482
+ if (!recovered)
1483
+ throw new Error("Settled batch disappeared during reconciliation");
1484
+ return recovered;
1485
+ }
1486
+ return receipt;
1487
+ }
1488
+ if (batch.state !== "RECONCILIATION_REQUIRED" &&
1489
+ !(resolving && batch.state === "SETTLING"))
1490
+ throw new Error("Recover the original batch into RECONCILIATION_REQUIRED first");
1491
+ const attempts = projection.attempts.filter((entry) => entry.batchId === plan.batchId);
1492
+ const blockers = projection.issues.filter((issue) => !issue.includes(": reconciliation required"));
1493
+ for (const attempt of attempts) {
1494
+ if (attempt.terminal)
1495
+ continue;
1496
+ const facts = events.filter((event) => event.kind === "coordination" &&
1497
+ event.batchId === plan.batchId &&
1498
+ event.attemptId === attempt.attemptId &&
1499
+ event.planHash === plan.planHash);
1500
+ if (facts.some((event) => event.kind === "coordination" && event.action === "process_exited"))
1501
+ continue;
1502
+ const started = facts
1503
+ .filter((event) => event.kind === "coordination" && event.action === "process_started")
1504
+ .pop();
1505
+ const pid = started?.kind === "coordination" ? started.payload?.pid : undefined;
1506
+ const birth = started?.kind === "coordination"
1507
+ ? started.payload?.processIdentity
1508
+ : undefined;
1509
+ let exited = false;
1510
+ if (typeof pid === "number" &&
1511
+ Number.isSafeInteger(pid) &&
1512
+ pid > 0 &&
1513
+ typeof birth === "string" &&
1514
+ birth.startsWith(`${pid}:`)) {
1515
+ const current = readProcessIdentity(pid);
1516
+ if (current)
1517
+ exited = current !== birth;
1518
+ else {
1519
+ try {
1520
+ process.kill(pid, 0);
1521
+ }
1522
+ catch (error) {
1523
+ exited = error.code === "ESRCH";
1524
+ }
1525
+ }
1526
+ }
1527
+ if (!exited)
1528
+ blockers.push(`Attempt ${attempt.attemptId} lacks confirmed process exit; preserve its lease`);
1196
1529
  }
1530
+ if (!input.execute)
1531
+ return { status: blockers.length ? "blocked" : "ready", blockers };
1532
+ if (blockers.length)
1533
+ throw new Error(blockers.join("; "));
1534
+ if (!input.reason?.trim())
1535
+ throw new Error("Reconciliation requires an explicit disposition reason");
1536
+ const by = "main";
1537
+ for (const attempt of attempts) {
1538
+ await recordCoordinationEvent({
1539
+ sessionId: plan.sessionId,
1540
+ cwd,
1541
+ by,
1542
+ batchId: plan.batchId,
1543
+ entity: "attempt",
1544
+ action: "terminal_observed",
1545
+ attemptId: attempt.attemptId,
1546
+ teammateId: attempt.teammateId,
1547
+ terminal: true,
1548
+ reason: input.reason,
1549
+ idempotencyKey: `${plan.batchId}:reconciled-terminal:${attempt.attemptId}`,
1550
+ });
1551
+ if (!["FINAL_FAILED", "REPORT_SUBMITTED"].includes(attempt.state))
1552
+ await appendTransition({
1553
+ sessionId: plan.sessionId,
1554
+ cwd,
1555
+ by,
1556
+ batchId: plan.batchId,
1557
+ entity: "attempt",
1558
+ id: attempt.attemptId,
1559
+ attemptId: attempt.attemptId,
1560
+ assignmentId: attempt.assignmentId,
1561
+ teammateId: attempt.teammateId,
1562
+ retryCount: attempt.retryCount,
1563
+ state: "FINAL_FAILED",
1564
+ terminal: true,
1565
+ states: new Map([[attempt.attemptId, attempt.state]]),
1566
+ transitionKey: "reconcile",
1567
+ });
1568
+ const teammate = projection.teammates.find((entry) => entry.batchId === plan.batchId &&
1569
+ entry.teammateId === attempt.teammateId);
1570
+ if (teammate && !["FAILED", "REPORT_SUBMITTED"].includes(teammate.state))
1571
+ await appendTransition({
1572
+ sessionId: plan.sessionId,
1573
+ cwd,
1574
+ by,
1575
+ batchId: plan.batchId,
1576
+ entity: "teammate",
1577
+ id: teammate.teammateId,
1578
+ teammateId: teammate.teammateId,
1579
+ attemptId: attempt.attemptId,
1580
+ state: "FAILED",
1581
+ terminal: true,
1582
+ states: new Map([[teammate.teammateId, teammate.state]]),
1583
+ transitionKey: "reconcile",
1584
+ });
1585
+ }
1586
+ for (const assignment of projection.assignments.filter((entry) => entry.batchId === plan.batchId)) {
1587
+ if (!["ACCEPTED", "FINAL_FAILED"].includes(assignment.state))
1588
+ await appendTransition({
1589
+ sessionId: plan.sessionId,
1590
+ cwd,
1591
+ by,
1592
+ batchId: plan.batchId,
1593
+ entity: "assignment",
1594
+ id: assignment.assignmentId,
1595
+ assignmentId: assignment.assignmentId,
1596
+ attemptId: assignment.attemptId,
1597
+ teammateId: assignment.teammateId,
1598
+ retryCount: assignment.retryCount,
1599
+ state: "FINAL_FAILED",
1600
+ states: new Map([[assignment.assignmentId, assignment.state]]),
1601
+ transitionKey: "reconcile",
1602
+ payload: { reason: input.reason },
1603
+ });
1604
+ }
1605
+ for (const lease of projection.leases.filter((entry) => entry.batchId === plan.batchId))
1606
+ await recordCoordinationEvent({
1607
+ sessionId: plan.sessionId,
1608
+ cwd,
1609
+ by,
1610
+ batchId: plan.batchId,
1611
+ entity: "attempt",
1612
+ action: "lease_released",
1613
+ leaseId: lease.leaseId,
1614
+ protected: false,
1615
+ reason: input.reason,
1616
+ idempotencyKey: `${plan.batchId}:reconciled-release:${lease.leaseId}`,
1617
+ });
1618
+ const accepted = projection.assignments
1619
+ .filter((entry) => entry.batchId === plan.batchId)
1620
+ .every((entry) => entry.state === "ACCEPTED");
1621
+ const states = new Map([
1622
+ [plan.batchId, batch.state],
1623
+ ]);
1624
+ await recordCoordinationEvent({
1625
+ sessionId: plan.sessionId,
1626
+ cwd,
1627
+ by,
1628
+ batchId: plan.batchId,
1629
+ entity: "batch",
1630
+ action: "reconciliation_resolved",
1631
+ reason: input.reason,
1632
+ idempotencyKey: `${plan.batchId}:reconciled`,
1633
+ });
1634
+ if (batch.state !== "SETTLING")
1635
+ await appendTransition({
1636
+ sessionId: plan.sessionId,
1637
+ cwd,
1638
+ by,
1639
+ batchId: plan.batchId,
1640
+ entity: "batch",
1641
+ id: plan.batchId,
1642
+ state: "SETTLING",
1643
+ states,
1644
+ transitionKey: "reconcile",
1645
+ });
1646
+ await appendTransition({
1647
+ sessionId: plan.sessionId,
1648
+ cwd,
1649
+ by,
1650
+ batchId: plan.batchId,
1651
+ entity: "batch",
1652
+ id: plan.batchId,
1653
+ state: accepted ? "ALL_ACCEPTED" : "SETTLED_WITH_FAILURE",
1654
+ states,
1655
+ transitionKey: "reconcile",
1656
+ });
1657
+ const receipt = terminalReceiptFromProjection(plan, await readCoordinationState({ sessionId: plan.sessionId, cwd }));
1658
+ await appendAction({
1659
+ sessionId: plan.sessionId,
1660
+ cwd,
1661
+ by,
1662
+ batchId: plan.batchId,
1663
+ entity: "batch",
1664
+ action: "receipt_recorded",
1665
+ id: `${plan.batchId}:${receipt.status}`,
1666
+ payload: { ...receipt },
1667
+ });
1668
+ await assertTeamworkReceiptDurable(plan, receipt, cwd);
1669
+ return receipt;
1670
+ }
1671
+ /** Read settled facts without admitting work or consulting current resource policy. */
1672
+ export async function readSettledTeamworkReceipt(plan, cwd) {
1673
+ const state = await readCoordinationState({ sessionId: plan.sessionId, cwd });
1674
+ const batch = state.batches.find((entry) => entry.batchId === plan.batchId);
1675
+ if (!batch || !["ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state))
1676
+ return undefined;
1677
+ const receipt = terminalReceiptFromProjection(plan, state);
1678
+ await assertTeamworkReceiptDurable(plan, receipt, cwd);
1679
+ return receipt;
1197
1680
  }
1198
1681
  //# sourceMappingURL=coordinator.js.map