@pennixrv/trellis 0.7.2-beta.0 → 0.7.2-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/commands/teamwork/artifact.d.ts +9 -4
  2. package/dist/commands/teamwork/artifact.d.ts.map +1 -1
  3. package/dist/commands/teamwork/artifact.js +8 -37
  4. package/dist/commands/teamwork/artifact.js.map +1 -1
  5. package/dist/commands/teamwork/batch.d.ts +1 -0
  6. package/dist/commands/teamwork/batch.d.ts.map +1 -1
  7. package/dist/commands/teamwork/batch.js +24 -3
  8. package/dist/commands/teamwork/batch.js.map +1 -1
  9. package/dist/commands/teamwork/coordinator.d.ts +16 -3
  10. package/dist/commands/teamwork/coordinator.d.ts.map +1 -1
  11. package/dist/commands/teamwork/coordinator.js +209 -86
  12. package/dist/commands/teamwork/coordinator.js.map +1 -1
  13. package/dist/commands/teamwork/guard.js +2 -2
  14. package/dist/commands/teamwork/guard.js.map +1 -1
  15. package/dist/commands/teamwork/index.d.ts.map +1 -1
  16. package/dist/commands/teamwork/index.js +25 -1
  17. package/dist/commands/teamwork/index.js.map +1 -1
  18. package/dist/commands/teamwork/result.d.ts +2 -1
  19. package/dist/commands/teamwork/result.d.ts.map +1 -1
  20. package/dist/commands/teamwork/result.js.map +1 -1
  21. package/dist/commands/teamwork/runtime.d.ts +1 -1
  22. package/dist/commands/teamwork/runtime.d.ts.map +1 -1
  23. package/dist/commands/teamwork/runtime.js +58 -29
  24. package/dist/commands/teamwork/runtime.js.map +1 -1
  25. package/dist/migrations/manifests/0.7.2-beta.1.json +11 -0
  26. package/dist/migrations/manifests/0.7.2-beta.2.json +11 -0
  27. package/dist/templates/shared-hooks/guard-teamwork-wait.py +5 -0
  28. package/dist/templates/trellis/scripts/common/config.py +5 -0
  29. package/dist/templates/trellis/scripts/common/task_continuity.py +62 -0
  30. package/dist/templates/trellis/scripts/common/task_store.py +16 -0
  31. package/dist/templates/trellis/scripts/common/task_workflow.py +57 -4
  32. package/dist/templates/trellis/scripts/task.py +6 -1
  33. package/dist/utils/asset-backup.d.ts.map +1 -1
  34. package/dist/utils/asset-backup.js +18 -6
  35. package/dist/utils/asset-backup.js.map +1 -1
  36. package/package.json +2 -2
@@ -4,10 +4,13 @@ import path from "node:path";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { isTaskPhaseId, parseTaskWorkflowState, } from "@pennixrv/trellis-core/task";
6
6
  import { assertCoordinationTransition, readTeamworkEvents, readCoordinationState, recordCoordinationEvent, } from "@pennixrv/trellis-core/teamwork";
7
- import { TeammateReportReadError, cleanupSettledTeamworkArtifacts, readTeammateReport, writeTeammateQueueFact, writeTeammateClaim, writeTeammateDisposition, validateTeamworkPlan, } from "./artifact.js";
7
+ import { TeammateReportReadError, readTeammateReport, writeTeammateQueueFact, writeTeammateClaim, writeTeammateDisposition, validateTeamworkPlan, } from "./artifact.js";
8
8
  import { assertTeammateAdapterContract, assertTeammateHostCapability, } from "./teammate-adapters.js";
9
9
  import { effectiveTeammateCapacity, resolveTeammateRuntimeConfig, } from "./teammate-config.js";
10
10
  import { readProcessIdentity } from "./process-identity.js";
11
+ import { withLock } from "./store/lock.js";
12
+ export class TeamworkPolicyChangedError extends Error {
13
+ }
11
14
  export function assertTeamworkPlanCurrent(plan, taskRoot) {
12
15
  const taskFile = path.join(taskRoot, "task.json");
13
16
  const stat = fs.lstatSync(taskFile);
@@ -49,7 +52,7 @@ function terminalReceiptFromProjection(plan, projection) {
49
52
  return mismatch("does not match the frozen plan");
50
53
  if (batch.planHash !== plan.planHash)
51
54
  return mismatch("does not match the frozen plan");
52
- if (batch.state !== "ALL_ACCEPTED" && batch.state !== "SETTLED_WITH_FAILURE")
55
+ if (!["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state))
53
56
  mismatch("is not terminal");
54
57
  const effectiveCapacity = batch.effectiveCapacity;
55
58
  if (typeof effectiveCapacity !== "number" ||
@@ -88,9 +91,10 @@ function terminalReceiptFromProjection(plan, projection) {
88
91
  const assignments = plan.assignments.map((assignment) => {
89
92
  const projected = assignmentById.get(assignment.assignmentId);
90
93
  if (!projected ||
91
- (projected.state !== "ACCEPTED" && projected.state !== "FINAL_FAILED"))
94
+ !["AWAITING_REVIEW", "ACCEPTED", "FINAL_FAILED"].includes(projected.state))
92
95
  return mismatch(`assignment '${assignment.assignmentId}' is not settled`);
93
- if (projected.state === "ACCEPTED") {
96
+ if (projected.state === "ACCEPTED" ||
97
+ projected.state === "AWAITING_REVIEW") {
94
98
  const acceptedAttempt = attempts.find((attempt) => attempt.attemptId === projected.attemptId &&
95
99
  attempt.assignmentId === assignment.assignmentId);
96
100
  if (acceptedAttempt?.state !== "REPORT_SUBMITTED")
@@ -101,9 +105,11 @@ function terminalReceiptFromProjection(plan, projection) {
101
105
  status: projected.state,
102
106
  };
103
107
  });
104
- const status = assignments.every((item) => item.status === "ACCEPTED")
105
- ? "ALL_ACCEPTED"
106
- : "SETTLED_WITH_FAILURE";
108
+ const status = assignments.some((item) => item.status === "AWAITING_REVIEW")
109
+ ? "AWAITING_REVIEW"
110
+ : assignments.every((item) => item.status === "ACCEPTED")
111
+ ? "ALL_ACCEPTED"
112
+ : "SETTLED_WITH_FAILURE";
107
113
  if (batch.state !== status)
108
114
  mismatch("terminal state disagrees with its assignment dispositions");
109
115
  const retryCounts = Object.fromEntries(plan.assignments.flatMap((assignment) => {
@@ -160,6 +166,7 @@ function reconciliationReceipt(plan, projection, capacity) {
160
166
  async function recordRecoveryReconciliation(input) {
161
167
  const { plan, cwd, by, existingState, projection, capacity, reason } = input;
162
168
  if (existingState !== "RECONCILIATION_REQUIRED" &&
169
+ existingState !== "AWAITING_REVIEW" &&
163
170
  existingState !== "ALL_ACCEPTED" &&
164
171
  existingState !== "SETTLED_WITH_FAILURE") {
165
172
  await appendTransition({
@@ -212,7 +219,8 @@ async function recoverExistingBatch(input) {
212
219
  sessionId: input.plan.sessionId,
213
220
  cwd: input.cwd,
214
221
  });
215
- if (existing.state === "ALL_ACCEPTED" ||
222
+ if (existing.state === "AWAITING_REVIEW" ||
223
+ existing.state === "ALL_ACCEPTED" ||
216
224
  existing.state === "SETTLED_WITH_FAILURE") {
217
225
  let receipt;
218
226
  try {
@@ -229,10 +237,10 @@ async function recoverExistingBatch(input) {
229
237
  const receipts = events.filter((event) => event.kind === "coordination" &&
230
238
  event.batchId === input.plan.batchId &&
231
239
  event.action === "receipt_recorded" &&
240
+ event.payload?.status === receipt.status &&
232
241
  event.payload?.status !== "RECONCILIATION_REQUIRED");
233
- if (receipts.length > 1 ||
234
- (receipts.length === 1 &&
235
- !isDeepStrictEqual(receipts[0]?.payload, receipt))) {
242
+ if (receipts.length > 0 &&
243
+ !isDeepStrictEqual(receipts.at(-1)?.payload, receipt)) {
236
244
  return recordRecoveryReconciliation({
237
245
  ...input,
238
246
  existingState: existing.state,
@@ -356,7 +364,7 @@ export async function runTeamworkCoordinator(input) {
356
364
  });
357
365
  const settled = existing.batches.find((batch) => batch.batchId === plan.batchId);
358
366
  if (settled &&
359
- ["ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(settled.state)) {
367
+ ["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(settled.state)) {
360
368
  let terminal = false;
361
369
  try {
362
370
  terminalReceiptFromProjection(plan, existing);
@@ -973,7 +981,7 @@ export async function runTeamworkCoordinator(input) {
973
981
  await release(activeWork);
974
982
  final.set(assignment.assignmentId, "FINAL_FAILED");
975
983
  }
976
- async function accept(activeWork) {
984
+ async function submit(activeWork) {
977
985
  const { assignment, context, retryCount } = activeWork;
978
986
  try {
979
987
  assertTeamworkPlanCurrent(plan, input.taskRoot);
@@ -982,26 +990,13 @@ export async function runTeamworkCoordinator(input) {
982
990
  await fail(activeWork, `task phase changed before report acceptance: ${error instanceof Error ? error.message : String(error)}`, false);
983
991
  return;
984
992
  }
985
- writeTeammateDisposition({
986
- taskRoot: input.taskRoot,
987
- briefPath: assignment.briefPath,
988
- identity: {
989
- taskId: plan.taskId,
990
- batchId: plan.batchId,
991
- assignmentId: assignment.assignmentId,
992
- attemptId: context.attemptId,
993
- teammateId: context.teammateId,
994
- },
995
- sourceDigest: assignment.sourceDigest,
996
- status: "accepted",
997
- });
998
993
  await appendTransition({
999
994
  sessionId: plan.sessionId,
1000
995
  by,
1001
996
  batchId: plan.batchId,
1002
997
  entity: "assignment",
1003
998
  id: assignment.assignmentId,
1004
- state: "ACCEPTED",
999
+ state: "AWAITING_REVIEW",
1005
1000
  states: assignmentStates,
1006
1001
  assignmentId: assignment.assignmentId,
1007
1002
  attemptId: context.attemptId,
@@ -1009,7 +1004,7 @@ export async function runTeamworkCoordinator(input) {
1009
1004
  retryCount,
1010
1005
  });
1011
1006
  await release(activeWork);
1012
- final.set(assignment.assignmentId, "ACCEPTED");
1007
+ final.set(assignment.assignmentId, "AWAITING_REVIEW");
1013
1008
  }
1014
1009
  async function complete(activeWork, allowRetry = true) {
1015
1010
  const { assignment, context, retryCount } = activeWork;
@@ -1048,28 +1043,15 @@ export async function runTeamworkCoordinator(input) {
1048
1043
  });
1049
1044
  }
1050
1045
  catch (error) {
1051
- if (error instanceof TeammateReportReadError && error.kind !== "missing")
1052
- throw error;
1053
- await fail(activeWork, error instanceof Error ? error.message : String(error), allowRetry);
1046
+ await fail(activeWork, error instanceof Error ? error.message : String(error), error instanceof TeammateReportReadError && error.kind === "missing"
1047
+ ? allowRetry
1048
+ : false);
1054
1049
  return;
1055
1050
  }
1056
1051
  if (context.launch.reportVersion === 2 && report.schemaVersion !== 2) {
1057
1052
  await fail(activeWork, "report schema_version must match the frozen result contract (2)", allowRetry);
1058
1053
  return;
1059
1054
  }
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
- }
1068
- if (input.runtime.validateReport &&
1069
- !(await input.runtime.validateReport(context, report))) {
1070
- await fail(activeWork, "report rejected by acceptance contract", allowRetry);
1071
- return;
1072
- }
1073
1055
  try {
1074
1056
  assertTeamworkPlanCurrent(plan, input.taskRoot);
1075
1057
  }
@@ -1135,7 +1117,7 @@ export async function runTeamworkCoordinator(input) {
1135
1117
  teammateId: context.teammateId,
1136
1118
  retryCount,
1137
1119
  });
1138
- await accept(activeWork);
1120
+ await submit(activeWork);
1139
1121
  }
1140
1122
  async function settle() {
1141
1123
  await appendTransition({
@@ -1147,15 +1129,8 @@ export async function runTeamworkCoordinator(input) {
1147
1129
  state: "SETTLING",
1148
1130
  states: batchStates,
1149
1131
  });
1150
- const cleanupFailures = cleanupSettledTeamworkArtifacts({
1151
- taskRoot: input.taskRoot,
1152
- assignments: plan.assignments,
1153
- });
1154
- if (cleanupFailures.length > 0) {
1155
- return reconcile(`Teamwork batch cleanup failed: ${cleanupFailures.join("; ")}`);
1156
- }
1157
- const accepted = plan.assignments.every((assignment) => final.get(assignment.assignmentId) === "ACCEPTED");
1158
- const status = accepted ? "ALL_ACCEPTED" : "SETTLED_WITH_FAILURE";
1132
+ const pendingReview = plan.assignments.some((assignment) => final.get(assignment.assignmentId) === "AWAITING_REVIEW");
1133
+ const status = pendingReview ? "AWAITING_REVIEW" : "SETTLED_WITH_FAILURE";
1159
1134
  await appendTransition({
1160
1135
  sessionId: plan.sessionId,
1161
1136
  by,
@@ -1165,29 +1140,7 @@ export async function runTeamworkCoordinator(input) {
1165
1140
  state: status,
1166
1141
  states: batchStates,
1167
1142
  });
1168
- const receipt = {
1169
- schemaVersion: 1,
1170
- batchId: plan.batchId,
1171
- status,
1172
- assignments: plan.assignments.map((assignment) => ({
1173
- assignmentId: assignment.assignmentId,
1174
- status: final.get(assignment.assignmentId) ?? "FINAL_FAILED",
1175
- })),
1176
- capacity,
1177
- retryCounts,
1178
- evaluationGroups: [
1179
- ...new Set(plan.assignments
1180
- .map((assignment) => assignment.evaluationGroup?.id)
1181
- .filter((id) => id !== undefined)),
1182
- ].map((id) => ({
1183
- id,
1184
- status: plan.assignments
1185
- .filter((assignment) => assignment.evaluationGroup?.id === id)
1186
- .every((assignment) => final.get(assignment.assignmentId) === "ACCEPTED")
1187
- ? "COMPLETE"
1188
- : "INCOMPLETE",
1189
- })),
1190
- };
1143
+ const receipt = terminalReceiptFromProjection(plan, await readCoordinationState({ sessionId: plan.sessionId, cwd }));
1191
1144
  await appendAction({
1192
1145
  sessionId: plan.sessionId,
1193
1146
  by,
@@ -1230,8 +1183,26 @@ export async function runTeamworkCoordinator(input) {
1230
1183
  catch (error) {
1231
1184
  if (!assignmentStates.has(assignment.assignmentId) ||
1232
1185
  assignmentStates.get(assignment.assignmentId) === "QUEUED") {
1233
- await failPending(`launch preflight failed before dispatch: ${error instanceof Error ? error.message : String(error)}`);
1234
- return;
1186
+ const reason = `launch preflight failed before dispatch: ${error instanceof Error ? error.message : String(error)}`;
1187
+ if (error instanceof TeamworkPolicyChangedError) {
1188
+ await failPending(reason);
1189
+ return;
1190
+ }
1191
+ pending.shift();
1192
+ await appendTransition({
1193
+ sessionId: plan.sessionId,
1194
+ by,
1195
+ batchId: plan.batchId,
1196
+ entity: "assignment",
1197
+ id: assignment.assignmentId,
1198
+ assignmentId: assignment.assignmentId,
1199
+ state: "FINAL_FAILED",
1200
+ states: assignmentStates,
1201
+ retryCount,
1202
+ payload: { reason, dispatched: false },
1203
+ });
1204
+ final.set(assignment.assignmentId, "FINAL_FAILED");
1205
+ continue;
1235
1206
  }
1236
1207
  throw error;
1237
1208
  }
@@ -1451,9 +1422,10 @@ export async function assertTeamworkReceiptDurable(plan, receipt, cwd) {
1451
1422
  const receipts = events.filter((event) => event.kind === "coordination" &&
1452
1423
  event.batchId === plan.batchId &&
1453
1424
  event.action === "receipt_recorded" &&
1425
+ event.payload?.status === receipt.status &&
1454
1426
  event.payload?.status !== "RECONCILIATION_REQUIRED");
1455
- if (receipts.length !== 1 ||
1456
- !isDeepStrictEqual(receipts[0]?.payload, expected))
1427
+ if (receipts.length === 0 ||
1428
+ !isDeepStrictEqual(receipts.at(-1)?.payload, expected))
1457
1429
  throw new Error(`batch '${plan.batchId}' has no matching durable receipt event`);
1458
1430
  }
1459
1431
  /** Retire uncertain work only from associated terminal or process-birth evidence. */
@@ -1470,7 +1442,7 @@ export async function reconcileTeamworkBatch(input) {
1470
1442
  const resolving = events.some((event) => event.kind === "coordination" &&
1471
1443
  event.batchId === plan.batchId &&
1472
1444
  event.action === "reconciliation_resolved");
1473
- if (["ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state)) {
1445
+ if (["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state)) {
1474
1446
  const receipt = terminalReceiptFromProjection(plan, projection);
1475
1447
  if (input.execute) {
1476
1448
  const recovered = await recoverExistingBatch({
@@ -1507,23 +1479,34 @@ export async function reconcileTeamworkBatch(input) {
1507
1479
  ? started.payload?.processIdentity
1508
1480
  : undefined;
1509
1481
  let exited = false;
1482
+ let identityUnknown = true;
1510
1483
  if (typeof pid === "number" &&
1511
1484
  Number.isSafeInteger(pid) &&
1512
1485
  pid > 0 &&
1513
1486
  typeof birth === "string" &&
1514
1487
  birth.startsWith(`${pid}:`)) {
1515
1488
  const current = readProcessIdentity(pid);
1516
- if (current)
1489
+ if (current) {
1517
1490
  exited = current !== birth;
1491
+ identityUnknown = false;
1492
+ }
1518
1493
  else {
1519
1494
  try {
1520
1495
  process.kill(pid, 0);
1521
1496
  }
1522
1497
  catch (error) {
1523
- exited = error.code === "ESRCH";
1498
+ const code = error.code;
1499
+ exited = code === "ESRCH";
1500
+ identityUnknown = code !== "ESRCH";
1524
1501
  }
1525
1502
  }
1526
1503
  }
1504
+ if (!exited &&
1505
+ identityUnknown &&
1506
+ input.execute &&
1507
+ input.confirmUnknownProcessExit &&
1508
+ input.reason?.trim())
1509
+ exited = true;
1527
1510
  if (!exited)
1528
1511
  blockers.push(`Attempt ${attempt.attemptId} lacks confirmed process exit; preserve its lease`);
1529
1512
  }
@@ -1629,7 +1612,13 @@ export async function reconcileTeamworkBatch(input) {
1629
1612
  entity: "batch",
1630
1613
  action: "reconciliation_resolved",
1631
1614
  reason: input.reason,
1632
- idempotencyKey: `${plan.batchId}:reconciled`,
1615
+ ...(input.confirmUnknownProcessExit
1616
+ ? { payload: { confirmedUnknownProcessExit: true } }
1617
+ : {}),
1618
+ idempotencyKey: `${plan.batchId}:reconciled:${crypto
1619
+ .createHash("sha256")
1620
+ .update(input.reason ?? "")
1621
+ .digest("hex")}`,
1633
1622
  });
1634
1623
  if (batch.state !== "SETTLING")
1635
1624
  await appendTransition({
@@ -1672,10 +1661,144 @@ export async function reconcileTeamworkBatch(input) {
1672
1661
  export async function readSettledTeamworkReceipt(plan, cwd) {
1673
1662
  const state = await readCoordinationState({ sessionId: plan.sessionId, cwd });
1674
1663
  const batch = state.batches.find((entry) => entry.batchId === plan.batchId);
1675
- if (!batch || !["ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state))
1664
+ if (!batch ||
1665
+ !["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state))
1676
1666
  return undefined;
1677
1667
  const receipt = terminalReceiptFromProjection(plan, state);
1678
1668
  await assertTeamworkReceiptDurable(plan, receipt, cwd);
1679
1669
  return receipt;
1680
1670
  }
1671
+ /** Main-agent review starts only after the CLI has drained the entire batch. */
1672
+ export async function reviewTeamworkReport(input) {
1673
+ const { plan, taskRoot, cwd } = input;
1674
+ if (!input.reason.trim())
1675
+ throw new Error("Review requires an explicit reason");
1676
+ const assignment = plan.assignments.find((item) => item.assignmentId === input.assignmentId);
1677
+ if (!assignment)
1678
+ throw new Error("Review assignment is not in the frozen plan");
1679
+ return withLock(path.join(taskRoot, "teammates", `${plan.batchId}.review.lock`), async () => {
1680
+ assertTeamworkPlanCurrent(plan, taskRoot);
1681
+ const projection = await readCoordinationState({
1682
+ sessionId: plan.sessionId,
1683
+ cwd,
1684
+ });
1685
+ const batch = projection.batches.find((item) => item.batchId === plan.batchId);
1686
+ if (!batch ||
1687
+ !["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state))
1688
+ throw new Error("Review requires the entire batch to be collected first");
1689
+ terminalReceiptFromProjection(plan, projection);
1690
+ const work = projection.assignments.find((item) => item.batchId === plan.batchId &&
1691
+ item.assignmentId === assignment.assignmentId);
1692
+ if (!work?.attemptId || !work.teammateId)
1693
+ throw new Error("Review has no matching report attempt");
1694
+ readTeammateReport({
1695
+ taskRoot,
1696
+ briefPath: assignment.briefPath,
1697
+ attemptId: work.attemptId,
1698
+ identity: {
1699
+ taskId: plan.taskId,
1700
+ batchId: plan.batchId,
1701
+ assignmentId: assignment.assignmentId,
1702
+ teammateId: work.teammateId,
1703
+ planHash: plan.planHash,
1704
+ phaseId: plan.phaseId,
1705
+ taskRouteRevision: plan.taskRouteRevision,
1706
+ inputRevision: plan.inputRevision,
1707
+ },
1708
+ sourceDigest: assignment.sourceDigest,
1709
+ });
1710
+ const reportFile = path.join(taskRoot, path.dirname(assignment.briefPath), "attempts", work.attemptId, "report.json");
1711
+ const reportSha256 = crypto
1712
+ .createHash("sha256")
1713
+ .update(fs.readFileSync(reportFile))
1714
+ .digest("hex");
1715
+ if (reportSha256 !== input.reportSha256)
1716
+ throw new Error("Review report digest does not match the inspected report");
1717
+ const state = input.decision === "accepted" ? "ACCEPTED" : "FINAL_FAILED";
1718
+ if (work.state !== "AWAITING_REVIEW" && work.state !== state)
1719
+ throw new Error("Report already has a different final disposition");
1720
+ const by = input.by ?? "main";
1721
+ writeTeammateDisposition({
1722
+ taskRoot,
1723
+ briefPath: assignment.briefPath,
1724
+ identity: {
1725
+ taskId: plan.taskId,
1726
+ batchId: plan.batchId,
1727
+ assignmentId: assignment.assignmentId,
1728
+ attemptId: work.attemptId,
1729
+ teammateId: work.teammateId,
1730
+ },
1731
+ sourceDigest: assignment.sourceDigest,
1732
+ status: input.decision,
1733
+ reason: input.reason,
1734
+ review: {
1735
+ plan_hash: plan.planHash,
1736
+ phase_id: plan.phaseId,
1737
+ task_route_revision: plan.taskRouteRevision,
1738
+ input_revision: plan.inputRevision,
1739
+ report_sha256: reportSha256,
1740
+ acceptance: assignment.acceptance,
1741
+ by,
1742
+ },
1743
+ });
1744
+ if (work.state === "AWAITING_REVIEW")
1745
+ await appendTransition({
1746
+ sessionId: plan.sessionId,
1747
+ cwd,
1748
+ by,
1749
+ batchId: plan.batchId,
1750
+ entity: "assignment",
1751
+ id: assignment.assignmentId,
1752
+ assignmentId: assignment.assignmentId,
1753
+ attemptId: work.attemptId,
1754
+ teammateId: work.teammateId,
1755
+ retryCount: work.retryCount,
1756
+ state,
1757
+ states: new Map([[assignment.assignmentId, work.state]]),
1758
+ payload: {
1759
+ reason: input.reason,
1760
+ reportSha256,
1761
+ planHash: plan.planHash,
1762
+ acceptance: assignment.acceptance,
1763
+ },
1764
+ });
1765
+ let updated = await readCoordinationState({
1766
+ sessionId: plan.sessionId,
1767
+ cwd,
1768
+ });
1769
+ const remaining = updated.assignments.filter((item) => item.batchId === plan.batchId);
1770
+ if (batch.state === "AWAITING_REVIEW" &&
1771
+ !remaining.some((item) => item.state === "AWAITING_REVIEW")) {
1772
+ await appendTransition({
1773
+ sessionId: plan.sessionId,
1774
+ cwd,
1775
+ by,
1776
+ batchId: plan.batchId,
1777
+ entity: "batch",
1778
+ id: plan.batchId,
1779
+ state: remaining.every((item) => item.state === "ACCEPTED")
1780
+ ? "ALL_ACCEPTED"
1781
+ : "SETTLED_WITH_FAILURE",
1782
+ states: new Map([[plan.batchId, batch.state]]),
1783
+ });
1784
+ updated = await readCoordinationState({
1785
+ sessionId: plan.sessionId,
1786
+ cwd,
1787
+ });
1788
+ }
1789
+ const receipt = terminalReceiptFromProjection(plan, updated);
1790
+ await appendAction({
1791
+ sessionId: plan.sessionId,
1792
+ cwd,
1793
+ by,
1794
+ batchId: plan.batchId,
1795
+ entity: "batch",
1796
+ action: "receipt_recorded",
1797
+ id: `${plan.batchId}:review:${crypto.createHash("sha256").update(JSON.stringify(receipt)).digest("hex")}`,
1798
+ payload: { ...receipt },
1799
+ });
1800
+ await assertTeamworkReceiptDurable(plan, receipt, cwd);
1801
+ return receipt;
1802
+ });
1803
+ }
1681
1804
  //# sourceMappingURL=coordinator.js.map