@pennixrv/trellis 0.7.2-beta.0 → 0.7.2-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/teamwork/artifact.d.ts +9 -4
- package/dist/commands/teamwork/artifact.d.ts.map +1 -1
- package/dist/commands/teamwork/artifact.js +8 -37
- package/dist/commands/teamwork/artifact.js.map +1 -1
- package/dist/commands/teamwork/batch.d.ts +1 -0
- package/dist/commands/teamwork/batch.d.ts.map +1 -1
- package/dist/commands/teamwork/batch.js +24 -3
- package/dist/commands/teamwork/batch.js.map +1 -1
- package/dist/commands/teamwork/coordinator.d.ts +16 -3
- package/dist/commands/teamwork/coordinator.d.ts.map +1 -1
- package/dist/commands/teamwork/coordinator.js +209 -86
- package/dist/commands/teamwork/coordinator.js.map +1 -1
- package/dist/commands/teamwork/guard.js +2 -2
- package/dist/commands/teamwork/guard.js.map +1 -1
- package/dist/commands/teamwork/index.d.ts.map +1 -1
- package/dist/commands/teamwork/index.js +25 -1
- package/dist/commands/teamwork/index.js.map +1 -1
- package/dist/commands/teamwork/result.d.ts +2 -1
- package/dist/commands/teamwork/result.d.ts.map +1 -1
- package/dist/commands/teamwork/result.js.map +1 -1
- package/dist/commands/teamwork/runtime.d.ts +1 -1
- package/dist/commands/teamwork/runtime.d.ts.map +1 -1
- package/dist/commands/teamwork/runtime.js +58 -29
- package/dist/commands/teamwork/runtime.js.map +1 -1
- package/dist/migrations/manifests/0.7.2-beta.1.json +11 -0
- package/dist/migrations/manifests/0.7.2-beta.2.json +11 -0
- package/dist/migrations/manifests/0.7.2-beta.3.json +9 -0
- package/dist/templates/common/skills/request-intake.md +3 -3
- package/dist/templates/kiro/agents/trellis.json +1 -1
- package/dist/templates/shared-hooks/guard-teamwork-wait.py +5 -0
- package/dist/templates/trellis/scripts/common/config.py +5 -0
- package/dist/templates/trellis/scripts/common/task_continuity.py +62 -0
- package/dist/templates/trellis/scripts/common/task_store.py +16 -0
- package/dist/templates/trellis/scripts/common/task_workflow.py +57 -4
- package/dist/templates/trellis/scripts/task.py +6 -1
- package/dist/templates/trellis/workflow.md +3 -3
- package/dist/utils/asset-backup.d.ts.map +1 -1
- package/dist/utils/asset-backup.js +18 -6
- package/dist/utils/asset-backup.js.map +1 -1
- 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,
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
105
|
-
? "
|
|
106
|
-
: "
|
|
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 === "
|
|
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 >
|
|
234
|
-
(receipts.
|
|
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
|
|
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: "
|
|
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, "
|
|
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
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
|
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
|
|
1151
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
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
|
|
1456
|
-
!isDeepStrictEqual(receipts
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|