owlrunkit 0.18.4 → 0.19.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 (49) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +102 -4
  3. package/docs/architecture/OWLCODA_RUNKIT_0190_CONSOLIDATED_DAILY_USE_PRODUCT_DECISION_20260812.md +97 -0
  4. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +57 -0
  5. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +28 -4
  6. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  7. package/integrations/codex/skills/owlcoda-runkit/references/contract-v0.2.md +57 -0
  8. package/package.json +1 -1
  9. package/packages/attest/README.md +9 -5
  10. package/packages/attest/consumer-surface-v2.json +263 -0
  11. package/packages/attest/distribution-manifest-v1.json +9 -1
  12. package/packages/attest/schemas/quick-verification-receipt-v3.schema.json +244 -0
  13. package/packages/attest/schemas/workspace-snapshot-v2.schema.json +114 -0
  14. package/packages/attest/src/dependency-environment.mjs +128 -0
  15. package/packages/attest/src/index.mjs +1 -0
  16. package/packages/attest/src/quick-receipt-contract.mjs +202 -9
  17. package/packages/attest/src/quick.mjs +40 -4
  18. package/packages/attest/src/workspace-snapshot.mjs +133 -6
  19. package/registry-release-policy-v1.json +5 -5
  20. package/sbom.spdx.json +5 -5
  21. package/scripts/runkit-contract/core-contract.mjs +21 -1
  22. package/scripts/runkit-contract/efficiency-metrics.mjs +559 -0
  23. package/scripts/runkit-contract/evidence-store.mjs +130 -0
  24. package/scripts/runkit-contract/onboarding-bootstrap.mjs +685 -0
  25. package/scripts/runkit-contract/onboarding-doctor.mjs +2 -1
  26. package/scripts/runkit-contract/outcome-presentation.mjs +171 -0
  27. package/scripts/runkit-contract/profile-onboarding.mjs +22 -2
  28. package/scripts/runkit-contract/project-cli-resolver.mjs +23 -5
  29. package/scripts/runkit-contract/quick-evidence-reuse.mjs +590 -0
  30. package/scripts/runkit-contract/quick-isolation.mjs +314 -0
  31. package/scripts/runkit-contract/quick-metrics.mjs +51 -3
  32. package/scripts/runkit-contract/quick-receipt.mjs +31 -1
  33. package/scripts/runkit-contract/quick-verify.mjs +216 -25
  34. package/scripts/runkit-contract/quick-workspace-snapshot.mjs +1 -0
  35. package/scripts/runkit-contract/runkit-bootstrap.mjs +1 -0
  36. package/scripts/runkit-contract/runkit-cli.mjs +330 -18
  37. package/scripts/runkit-contract/runtime-transition.mjs +478 -0
  38. package/scripts/runkit-contract/schemas/efficiency-event-v1.schema.json +56 -0
  39. package/scripts/runkit-contract/schemas/efficiency-metrics-v1.schema.json +220 -0
  40. package/scripts/runkit-contract/schemas/external-runtime-report-v1.schema.json +125 -0
  41. package/scripts/runkit-contract/schemas/onboarding-bootstrap-plan-v1.schema.json +168 -0
  42. package/scripts/runkit-contract/schemas/onboarding-bootstrap-receipt-v1.schema.json +130 -0
  43. package/scripts/runkit-contract/schemas/outcome-summary-v1.schema.json +83 -0
  44. package/scripts/runkit-contract/schemas/quick-evidence-reuse-plan-v1.schema.json +59 -0
  45. package/scripts/runkit-contract/schemas/quick-evidence-reuse-receipt-v1.schema.json +79 -0
  46. package/scripts/runkit-contract/schemas/runtime-transition-receipt-v1.schema.json +185 -0
  47. package/scripts/runkit-contract/schemas/team-project-event-v4.schema.json +142 -0
  48. package/scripts/runkit-contract/team-project.mjs +704 -16
  49. package/scripts/runkit-contract/verification-envelope.mjs +15 -18
@@ -30,6 +30,7 @@ const DEFINITION_SCHEMA = "OwlCodaRunKitTeamProjectDefinitionV1";
30
30
  const EVENT_SCHEMA = "OwlCodaRunKitTeamProjectEventV1";
31
31
  const EVENT_SCHEMA_V2 = "OwlCodaRunKitTeamProjectEventV2";
32
32
  const EVENT_SCHEMA_V3 = "OwlCodaRunKitTeamProjectEventV3";
33
+ const EVENT_SCHEMA_V4 = "OwlCodaRunKitTeamProjectEventV4";
33
34
  const STATUS_SCHEMA_V1 = "OwlCodaRunKitTeamProjectStatusV1";
34
35
  const STATUS_SCHEMA_V2 = "OwlCodaRunKitTeamProjectStatusV2";
35
36
 
@@ -110,6 +111,19 @@ const EVENT_KEYS = Object.freeze({
110
111
  "unchangedPacketPaths", "outOfScopeSourcePaths", "targetSnapshot",
111
112
  "proven", "notProven", "targetWritePerformed", "authorizationGranted",
112
113
  ],
114
+ upstream_reopened: [
115
+ "schemaVersion", "eventId", "type", "occurredAt", "reopenId",
116
+ "assignmentId", "previousAssignmentId", "previousCheckpointEventId",
117
+ "workItemId", "triggerWorkItemId", "fromAgentId", "reviewerAgentId",
118
+ "toAgentId", "reason", "evidenceRefs", "nextAction",
119
+ ],
120
+ responsibility_transferred: [
121
+ "schemaVersion", "eventId", "type", "occurredAt", "transferId",
122
+ "assignmentId", "previousAssignmentId", "checkpointEventId",
123
+ "handoffEventId", "assignmentEventId", "workItemId", "fromAgentId",
124
+ "toAgentId", "state", "summary", "completedUnits", "evidenceRefs",
125
+ "blockerRefs", "decisionRefs", "nextAction", "sourceFingerprint",
126
+ ],
113
127
  });
114
128
 
115
129
  function canonical(value) {
@@ -443,6 +457,22 @@ function latestEventFact(left, right) {
443
457
  return compareEventFacts(left, right) >= 0 ? left : right;
444
458
  }
445
459
 
460
+ function downstreamWorkItemIds(definition, workItemId, { includeRoot = false } = {}) {
461
+ const children = new Map(definition.workItems.map(row => [row.id, []]));
462
+ for (const row of definition.workItems) {
463
+ for (const dependency of row.dependencies) children.get(dependency).push(row.id);
464
+ }
465
+ const visited = new Set(includeRoot ? [workItemId] : []);
466
+ const queue = [...children.get(workItemId)];
467
+ while (queue.length > 0) {
468
+ const current = queue.shift();
469
+ if (visited.has(current)) continue;
470
+ visited.add(current);
471
+ queue.push(...children.get(current));
472
+ }
473
+ return [...visited].sort(compareIdentifiers);
474
+ }
475
+
446
476
  function pendingHandoffProjection(handoff) {
447
477
  return handoff
448
478
  ? {
@@ -585,13 +615,18 @@ function staleHandoffsForTakeover({ loaded, status, agentId, responsibilities })
585
615
  "handoff_recorded",
586
616
  "rework_returned",
587
617
  "source_delivery_imported",
618
+ "upstream_reopened",
619
+ "responsibility_transferred",
588
620
  ].includes(row.event.type)
589
621
  ));
590
622
  let reason = "superseded_by_current_truth";
591
623
  if (laterFacts.some(row => row.event.type === "handoff_recorded")) {
592
624
  reason = "redirected_by_later_handoff";
593
625
  } else if (laterFacts.some(row => (
594
- row.event.type === "agent_assigned" || row.event.type === "rework_returned"
626
+ row.event.type === "agent_assigned"
627
+ || row.event.type === "rework_returned"
628
+ || row.event.type === "upstream_reopened"
629
+ || row.event.type === "responsibility_transferred"
595
630
  ))) {
596
631
  reason = "superseded_by_assignment";
597
632
  } else if (laterFacts.length > 0) {
@@ -625,9 +660,10 @@ function validateEventShape(value, definition) {
625
660
  throw new Error("Team project event type is invalid.");
626
661
  }
627
662
  assertAllowedKeys(value, `Team project event ${value.eventId ?? "unknown"}`, EVENT_KEYS[value.type]);
628
- if (![EVENT_SCHEMA, EVENT_SCHEMA_V2, EVENT_SCHEMA_V3].includes(value.schemaVersion)) {
663
+ if (![EVENT_SCHEMA, EVENT_SCHEMA_V2, EVENT_SCHEMA_V3, EVENT_SCHEMA_V4]
664
+ .includes(value.schemaVersion)) {
629
665
  throw new Error(
630
- `Team project event schemaVersion must be ${EVENT_SCHEMA}, ${EVENT_SCHEMA_V2}, or ${EVENT_SCHEMA_V3}.`,
666
+ `Team project event schemaVersion must be ${EVENT_SCHEMA}, ${EVENT_SCHEMA_V2}, ${EVENT_SCHEMA_V3}, or ${EVENT_SCHEMA_V4}.`,
631
667
  );
632
668
  }
633
669
  if (value.type === "rework_returned" && value.schemaVersion !== EVENT_SCHEMA_V2) {
@@ -639,6 +675,15 @@ function validateEventShape(value, definition) {
639
675
  if (value.schemaVersion === EVENT_SCHEMA_V3 && value.type !== "source_delivery_imported") {
640
676
  throw new Error(`${EVENT_SCHEMA_V3} is reserved for source_delivery_imported events.`);
641
677
  }
678
+ const eventV4Types = new Set(["upstream_reopened", "responsibility_transferred"]);
679
+ if (eventV4Types.has(value.type) && value.schemaVersion !== EVENT_SCHEMA_V4) {
680
+ throw new Error(`${value.type} requires schemaVersion ${EVENT_SCHEMA_V4}.`);
681
+ }
682
+ if (value.schemaVersion === EVENT_SCHEMA_V4 && !eventV4Types.has(value.type)) {
683
+ throw new Error(
684
+ `${EVENT_SCHEMA_V4} is reserved for upstream_reopened and responsibility_transferred events.`,
685
+ );
686
+ }
642
687
  safeIdentifier(value.eventId, "eventId");
643
688
  validateTimestamp(value.occurredAt);
644
689
  const workItemIds = new Set(definition.workItems.map(row => row.id));
@@ -653,6 +698,8 @@ function validateEventShape(value, definition) {
653
698
  "handoff_recorded",
654
699
  "rework_returned",
655
700
  "source_delivery_imported",
701
+ "upstream_reopened",
702
+ "responsibility_transferred",
656
703
  "verification_deferred",
657
704
  ].includes(value.type)) {
658
705
  assertWorkItem(value.workItemId);
@@ -663,6 +710,8 @@ function validateEventShape(value, definition) {
663
710
  "handoff_recorded",
664
711
  "rework_returned",
665
712
  "source_delivery_imported",
713
+ "upstream_reopened",
714
+ "responsibility_transferred",
666
715
  ].includes(value.type)) {
667
716
  safeIdentifier(value.assignmentId, "assignmentId");
668
717
  }
@@ -864,6 +913,55 @@ function validateEventShape(value, definition) {
864
913
  if (value.targetWritePerformed !== false || value.authorizationGranted !== false) {
865
914
  throw new Error("External delivery cannot grant authority or write the target workspace.");
866
915
  }
916
+ } else if (value.type === "upstream_reopened") {
917
+ safeIdentifier(value.reopenId, "reopenId");
918
+ safeIdentifier(value.previousAssignmentId, "previousAssignmentId");
919
+ safeIdentifier(value.previousCheckpointEventId, "previousCheckpointEventId");
920
+ assertWorkItem(value.triggerWorkItemId);
921
+ safeIdentifier(value.fromAgentId, "fromAgentId");
922
+ safeIdentifier(value.reviewerAgentId, "reviewerAgentId");
923
+ safeIdentifier(value.toAgentId, "toAgentId");
924
+ nonEmptyString(value.reason, "Upstream reopen reason");
925
+ stringArray(value.evidenceRefs, "Upstream reopen evidenceRefs", { allowEmpty: false });
926
+ nonEmptyString(value.nextAction, "Upstream reopen nextAction");
927
+ } else if (value.type === "responsibility_transferred") {
928
+ safeIdentifier(value.transferId, "transferId");
929
+ safeIdentifier(value.previousAssignmentId, "previousAssignmentId");
930
+ for (const [field, label] of [
931
+ ["checkpointEventId", "checkpointEventId"],
932
+ ["handoffEventId", "handoffEventId"],
933
+ ["assignmentEventId", "assignmentEventId"],
934
+ ]) safeIdentifier(value[field], label);
935
+ if (new Set([
936
+ value.eventId,
937
+ value.checkpointEventId,
938
+ value.handoffEventId,
939
+ value.assignmentEventId,
940
+ ]).size !== 4) {
941
+ throw new Error("Responsibility transfer fact IDs must be distinct.");
942
+ }
943
+ safeIdentifier(value.fromAgentId, "fromAgentId");
944
+ safeIdentifier(value.toAgentId, "toAgentId");
945
+ if (value.fromAgentId === value.toAgentId) {
946
+ throw new Error("Responsibility transfer Agents must differ.");
947
+ }
948
+ if (!["active", "waiting_dependency", "waiting_decision", "verifying", "ready_to_integrate"]
949
+ .includes(value.state)) {
950
+ throw new Error("Responsibility transfer state must be non-terminal.");
951
+ }
952
+ nonEmptyString(value.summary, "Responsibility transfer summary");
953
+ stringArray(value.evidenceRefs, "Responsibility transfer evidenceRefs", { allowEmpty: false });
954
+ stringArray(value.blockerRefs, "Responsibility transfer blockerRefs");
955
+ stringArray(value.decisionRefs, "Responsibility transfer decisionRefs");
956
+ nonEmptyString(value.nextAction, "Responsibility transfer nextAction");
957
+ if (value.completedUnits !== undefined
958
+ && (!Number.isSafeInteger(value.completedUnits) || value.completedUnits < 0)) {
959
+ throw new Error("Responsibility transfer completedUnits must be a non-negative integer.");
960
+ }
961
+ if (value.sourceFingerprint !== undefined
962
+ && !/^(?:sha256:)?[a-f0-9]{64}$/u.test(value.sourceFingerprint)) {
963
+ throw new Error("Responsibility transfer sourceFingerprint is invalid.");
964
+ }
867
965
  }
868
966
  return structuredClone(value);
869
967
  }
@@ -874,6 +972,8 @@ function projectEvents(definition, records) {
874
972
  const checkpoints = new Map();
875
973
  const handoffs = new Map();
876
974
  const reworks = new Map();
975
+ const reopens = new Map();
976
+ const transfers = new Map();
877
977
  const decisions = new Map();
878
978
  const gatePasses = new Map();
879
979
  const verifications = new Map();
@@ -881,8 +981,11 @@ function projectEvents(definition, records) {
881
981
  const sourceDeliveries = [];
882
982
  const seenAssignments = new Set();
883
983
  const seenReworks = new Set();
984
+ const seenReopens = new Set();
985
+ const seenTransfers = new Set();
884
986
  const seenDecisions = new Set();
885
987
  const seenDeliveries = new Set();
988
+ const seenFactIds = new Set(records.map(row => row.event.eventId));
886
989
 
887
990
  for (const { event, ref } of records) {
888
991
  validateEventShape(event, definition);
@@ -995,6 +1098,166 @@ function projectEvents(definition, records) {
995
1098
  }
996
1099
  seenDeliveries.add(event.deliveryId);
997
1100
  sourceDeliveries.push({ ...event, ref });
1101
+ } else if (event.type === "upstream_reopened") {
1102
+ if (seenReopens.has(event.reopenId)) {
1103
+ throw new Error(`Duplicate upstream reopen ${event.reopenId}.`);
1104
+ }
1105
+ if (seenAssignments.has(event.assignmentId)) {
1106
+ throw new Error(`Duplicate assignmentId ${event.assignmentId}.`);
1107
+ }
1108
+ const current = assignments.get(event.workItemId) ?? null;
1109
+ const checkpoint = checkpoints.get(event.workItemId) ?? null;
1110
+ const affectedIds = downstreamWorkItemIds(
1111
+ definition,
1112
+ event.workItemId,
1113
+ { includeRoot: true },
1114
+ );
1115
+ if (!current
1116
+ || current.assignmentId !== event.previousAssignmentId
1117
+ || current.agentId !== event.fromAgentId
1118
+ || checkpoint?.eventId !== event.previousCheckpointEventId
1119
+ || checkpoint.assignmentId !== current.assignmentId
1120
+ || checkpoint.state !== "completed") {
1121
+ throw new Error(
1122
+ `Upstream reopen ${event.reopenId} does not bind the current completed WorkItem.`,
1123
+ );
1124
+ }
1125
+ if (!downstreamWorkItemIds(definition, event.workItemId)
1126
+ .includes(event.triggerWorkItemId)) {
1127
+ throw new Error(
1128
+ `Upstream reopen ${event.reopenId} trigger must be a transitive downstream WorkItem.`,
1129
+ );
1130
+ }
1131
+ const triggerAssignment = assignments.get(event.triggerWorkItemId) ?? null;
1132
+ if (triggerAssignment?.agentId !== event.reviewerAgentId) {
1133
+ throw new Error(
1134
+ `Upstream reopen ${event.reopenId} reviewer must own the trigger WorkItem.`,
1135
+ );
1136
+ }
1137
+ const invalidatedGate = definition.integrationGates.find(gate => (
1138
+ gatePasses.has(gate.id)
1139
+ && gate.requiredWorkItemIds.some(id => affectedIds.includes(id))
1140
+ ));
1141
+ if (invalidatedGate) {
1142
+ throw new Error(
1143
+ `Upstream reopen cannot invalidate passed integration gate ${invalidatedGate.id}.`,
1144
+ );
1145
+ }
1146
+ seenReopens.add(event.reopenId);
1147
+ seenAssignments.add(event.assignmentId);
1148
+ const assignment = {
1149
+ schemaVersion: EVENT_SCHEMA,
1150
+ eventId: `assignment-${event.assignmentId}`,
1151
+ type: "agent_assigned",
1152
+ occurredAt: event.occurredAt,
1153
+ assignmentId: event.assignmentId,
1154
+ supersedesAssignmentId: event.previousAssignmentId,
1155
+ workItemId: event.workItemId,
1156
+ agentId: event.toAgentId,
1157
+ ref: `${ref}#assignment`,
1158
+ };
1159
+ assignments.set(event.workItemId, assignment);
1160
+ assignmentHistory.get(event.workItemId).push(assignment);
1161
+ reopens.set(event.workItemId, { ...event, affectedWorkItemIds: affectedIds, ref });
1162
+ } else if (event.type === "responsibility_transferred") {
1163
+ if (seenTransfers.has(event.transferId)) {
1164
+ throw new Error(`Duplicate responsibility transfer ${event.transferId}.`);
1165
+ }
1166
+ if (seenAssignments.has(event.assignmentId)) {
1167
+ throw new Error(`Duplicate assignmentId ${event.assignmentId}.`);
1168
+ }
1169
+ for (const factId of [
1170
+ event.checkpointEventId,
1171
+ event.handoffEventId,
1172
+ event.assignmentEventId,
1173
+ ]) {
1174
+ if (seenFactIds.has(factId)) {
1175
+ throw new Error(`Duplicate responsibility transfer fact ID ${factId}.`);
1176
+ }
1177
+ }
1178
+ const current = assignments.get(event.workItemId) ?? null;
1179
+ const currentCheckpoint = checkpoints.get(event.workItemId) ?? null;
1180
+ const currentHandoff = handoffs.get(event.workItemId) ?? null;
1181
+ const latestCurrentFact = [
1182
+ currentCheckpoint,
1183
+ currentHandoff,
1184
+ reworks.get(event.workItemId) ?? null,
1185
+ reopens.get(event.workItemId) ?? null,
1186
+ transfers.get(event.workItemId) ?? null,
1187
+ sourceDeliveries.filter(row => row.workItemId === event.workItemId).at(-1) ?? null,
1188
+ ].reduce((latest, fact) => latestEventFact(latest, fact), null);
1189
+ if (!current
1190
+ || current.assignmentId !== event.previousAssignmentId
1191
+ || current.agentId !== event.fromAgentId) {
1192
+ throw new Error(
1193
+ `Responsibility transfer ${event.transferId} does not bind the current assignment.`,
1194
+ );
1195
+ }
1196
+ if (currentCheckpoint?.assignmentId === current.assignmentId
1197
+ && ["completed", "failed"].includes(currentCheckpoint.state)) {
1198
+ throw new Error("Responsibility transfer cannot target a terminal WorkItem.");
1199
+ }
1200
+ if (currentHandoff?.assignmentId === current.assignmentId
1201
+ && currentHandoff.fromAgentId === current.agentId
1202
+ && latestCurrentFact?.eventId === currentHandoff.eventId) {
1203
+ throw new Error("Responsibility transfer cannot replace a pending handoff.");
1204
+ }
1205
+ const checkpoint = {
1206
+ schemaVersion: EVENT_SCHEMA,
1207
+ eventId: event.checkpointEventId,
1208
+ type: "checkpoint_recorded",
1209
+ occurredAt: event.occurredAt,
1210
+ assignmentId: event.previousAssignmentId,
1211
+ workItemId: event.workItemId,
1212
+ state: event.state,
1213
+ summary: event.summary,
1214
+ ...(event.completedUnits === undefined ? {} : { completedUnits: event.completedUnits }),
1215
+ evidenceRefs: [...event.evidenceRefs],
1216
+ blockerRefs: [...event.blockerRefs],
1217
+ decisionRefs: [...event.decisionRefs],
1218
+ nextAction: event.nextAction,
1219
+ ...(event.sourceFingerprint === undefined
1220
+ ? {}
1221
+ : { sourceFingerprint: event.sourceFingerprint }),
1222
+ ref: `${ref}#checkpoint`,
1223
+ };
1224
+ const handoff = {
1225
+ schemaVersion: EVENT_SCHEMA,
1226
+ eventId: event.handoffEventId,
1227
+ type: "handoff_recorded",
1228
+ occurredAt: event.occurredAt,
1229
+ assignmentId: event.previousAssignmentId,
1230
+ workItemId: event.workItemId,
1231
+ fromAgentId: event.fromAgentId,
1232
+ toAgentId: event.toAgentId,
1233
+ summary: event.summary,
1234
+ evidenceRefs: [...event.evidenceRefs],
1235
+ nextAction: event.nextAction,
1236
+ ref: `${ref}#handoff`,
1237
+ };
1238
+ const assignment = {
1239
+ schemaVersion: EVENT_SCHEMA,
1240
+ eventId: event.assignmentEventId,
1241
+ type: "agent_assigned",
1242
+ occurredAt: event.occurredAt,
1243
+ assignmentId: event.assignmentId,
1244
+ supersedesAssignmentId: event.previousAssignmentId,
1245
+ workItemId: event.workItemId,
1246
+ agentId: event.toAgentId,
1247
+ ref: `${ref}#assignment`,
1248
+ };
1249
+ for (const factId of [
1250
+ event.checkpointEventId,
1251
+ event.handoffEventId,
1252
+ event.assignmentEventId,
1253
+ ]) seenFactIds.add(factId);
1254
+ seenTransfers.add(event.transferId);
1255
+ seenAssignments.add(event.assignmentId);
1256
+ checkpoints.set(event.workItemId, checkpoint);
1257
+ handoffs.set(event.workItemId, handoff);
1258
+ assignments.set(event.workItemId, assignment);
1259
+ assignmentHistory.get(event.workItemId).push(assignment);
1260
+ transfers.set(event.workItemId, { ...event, ref });
998
1261
  }
999
1262
  }
1000
1263
  return {
@@ -1003,6 +1266,8 @@ function projectEvents(definition, records) {
1003
1266
  checkpoints,
1004
1267
  handoffs,
1005
1268
  reworks,
1269
+ reopens,
1270
+ transfers,
1006
1271
  decisions,
1007
1272
  gatePasses,
1008
1273
  verifications,
@@ -1324,12 +1589,40 @@ function deriveProjectDriverSummary(status) {
1324
1589
  };
1325
1590
  }
1326
1591
 
1327
- function projectStatus(definition, records, workspaceRoot = null) {
1328
- const state = projectEvents(definition, records);
1592
+ function affectingReopensForWorkItem(state, workItemId) {
1593
+ return [...state.reopens.values()]
1594
+ .filter(reopen => reopen.affectedWorkItemIds.includes(workItemId))
1595
+ .sort(compareEventFacts);
1596
+ }
1597
+
1598
+ function invalidatingReopensForWorkItem(state, workItemId) {
1599
+ const checkpoint = state.checkpoints.get(workItemId) ?? null;
1600
+ return affectingReopensForWorkItem(state, workItemId).filter(reopen => (
1601
+ checkpoint?.state !== "completed" || compareEventFacts(checkpoint, reopen) <= 0
1602
+ ));
1603
+ }
1604
+
1605
+ function completedWorkItemIds(definition, state) {
1329
1606
  const completed = new Set();
1330
- for (const [id, checkpoint] of state.checkpoints) {
1331
- if (checkpoint.state === "completed") completed.add(id);
1607
+ let changed = true;
1608
+ while (changed) {
1609
+ changed = false;
1610
+ for (const item of definition.workItems) {
1611
+ if (completed.has(item.id)) continue;
1612
+ const checkpoint = state.checkpoints.get(item.id) ?? null;
1613
+ if (checkpoint?.state !== "completed"
1614
+ || invalidatingReopensForWorkItem(state, item.id).length > 0
1615
+ || item.dependencies.some(id => !completed.has(id))) continue;
1616
+ completed.add(item.id);
1617
+ changed = true;
1618
+ }
1332
1619
  }
1620
+ return completed;
1621
+ }
1622
+
1623
+ function projectStatus(definition, records, workspaceRoot = null) {
1624
+ const state = projectEvents(definition, records);
1625
+ const completed = completedWorkItemIds(definition, state);
1333
1626
  let workItems = definition.workItems.map((item) => {
1334
1627
  const assignment = state.assignments.get(item.id) ?? null;
1335
1628
  const checkpoint = state.checkpoints.get(item.id) ?? null;
@@ -1339,7 +1632,20 @@ function projectStatus(definition, records, workspaceRoot = null) {
1339
1632
  const latestSourceDeliveryFact = sourceDeliveries.at(-1) ?? null;
1340
1633
  const latestSourceDelivery = sourceDeliveryProjection(sourceDeliveries.at(-1) ?? null);
1341
1634
  const currentRework = rework?.assignmentId === assignment?.assignmentId ? rework : null;
1342
- const latestFact = latestEventFact(latestEventFact(checkpoint, handoff), currentRework);
1635
+ const reopenFacts = affectingReopensForWorkItem(state, item.id);
1636
+ const invalidatingReopens = invalidatingReopensForWorkItem(state, item.id);
1637
+ const currentReopen = state.reopens.get(item.id)?.assignmentId === assignment?.assignmentId
1638
+ ? state.reopens.get(item.id)
1639
+ : null;
1640
+ const currentTransfer = state.transfers.get(item.id)?.assignmentId === assignment?.assignmentId
1641
+ ? state.transfers.get(item.id)
1642
+ : null;
1643
+ const effectiveCheckpoint = invalidatingReopens.length === 0 ? checkpoint : null;
1644
+ let latestFact = [effectiveCheckpoint, handoff, currentRework, currentReopen, currentTransfer]
1645
+ .reduce((latest, fact) => latestEventFact(latest, fact), null);
1646
+ if (currentTransfer && latestFact?.occurredAt === currentTransfer.occurredAt) {
1647
+ latestFact = currentTransfer;
1648
+ }
1343
1649
  const activeHandoff = handoff
1344
1650
  && handoff.assignmentId === assignment?.assignmentId
1345
1651
  && handoff.fromAgentId === assignment?.agentId
@@ -1350,7 +1656,8 @@ function projectStatus(definition, records, workspaceRoot = null) {
1350
1656
  && latestFact?.eventId === activeHandoff.eventId
1351
1657
  && latestHandoffFact?.eventId !== activeHandoff.eventId;
1352
1658
  const projectedFact = handoffSupersededByDelivery
1353
- ? latestEventFact(checkpoint, currentRework)
1659
+ ? [effectiveCheckpoint, currentRework, currentReopen, currentTransfer]
1660
+ .reduce((latest, fact) => latestEventFact(latest, fact), null)
1354
1661
  : latestFact;
1355
1662
  const pendingHandoff = activeHandoff
1356
1663
  && latestFact?.eventId === activeHandoff.eventId
@@ -1362,7 +1669,11 @@ function projectStatus(definition, records, workspaceRoot = null) {
1362
1669
  let status = "planned";
1363
1670
  if (assignment) status = unresolvedDependencies.length > 0 ? "waiting_dependency" : "active";
1364
1671
  if (pendingHandoff) status = "planned";
1365
- if (projectedFact?.type === "checkpoint_recorded") status = checkpoint.state;
1672
+ if (projectedFact?.type === "checkpoint_recorded") status = effectiveCheckpoint.state;
1673
+ if (projectedFact?.type === "responsibility_transferred") status = projectedFact.state;
1674
+ if (projectedFact?.type === "upstream_reopened") status = "active";
1675
+ if (assignment && unresolvedDependencies.length > 0
1676
+ && !["completed", "failed"].includes(status)) status = "waiting_dependency";
1366
1677
  if (executionTruth && (executionTruth.status !== "bound"
1367
1678
  || (executionTruth.lifecycle === "closed"
1368
1679
  && executionTruth.closeoutDecision !== "accepted"))) {
@@ -1372,7 +1683,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
1372
1683
  ? {
1373
1684
  kind: "measured_units",
1374
1685
  unit: item.measurable.unit,
1375
- completed: checkpoint?.completedUnits ?? 0,
1686
+ completed: effectiveCheckpoint?.completedUnits ?? 0,
1376
1687
  total: item.measurable.total,
1377
1688
  }
1378
1689
  : { kind: "state_only" };
@@ -1391,6 +1702,8 @@ function projectStatus(definition, records, workspaceRoot = null) {
1391
1702
  ...(checkpoint ? [checkpoint.ref] : []),
1392
1703
  ...(handoff ? [handoff.ref] : []),
1393
1704
  ...(rework ? [rework.ref] : []),
1705
+ ...reopenFacts.map(row => row.ref),
1706
+ ...(currentTransfer ? [currentTransfer.ref] : []),
1394
1707
  ...sourceDeliveries.map(row => row.ref),
1395
1708
  ];
1396
1709
  const projectedRework = reworkProjection(
@@ -1399,9 +1712,13 @@ function projectStatus(definition, records, workspaceRoot = null) {
1399
1712
  ? assignmentEvents.findIndex(row => row.assignmentId === currentRework.assignmentId) + 1
1400
1713
  : null,
1401
1714
  );
1402
- const latestSummary = projectedFact?.type === "rework_returned"
1715
+ const latestSummary = ["rework_returned", "upstream_reopened"].includes(projectedFact?.type)
1403
1716
  ? projectedFact.reason
1404
1717
  : projectedFact?.summary ?? null;
1718
+ const checkpointLikeFact = ["checkpoint_recorded", "responsibility_transferred"]
1719
+ .includes(projectedFact?.type)
1720
+ ? projectedFact
1721
+ : null;
1405
1722
  return {
1406
1723
  workItemId: item.id,
1407
1724
  title: item.title,
@@ -1415,10 +1732,10 @@ function projectStatus(definition, records, workspaceRoot = null) {
1415
1732
  executionTruth,
1416
1733
  dependencies: [...item.dependencies],
1417
1734
  unresolvedDependencies,
1418
- blockers: projectedFact?.type === "checkpoint_recorded"
1419
- ? checkpoint.blockerRefs
1735
+ blockers: checkpointLikeFact
1736
+ ? checkpointLikeFact.blockerRefs
1420
1737
  : unresolvedDependencies.map(id => `work-item:${id}`),
1421
- decisionRefs: projectedFact?.type === "checkpoint_recorded" ? checkpoint.decisionRefs : [],
1738
+ decisionRefs: checkpointLikeFact ? checkpointLikeFact.decisionRefs : [],
1422
1739
  progress: measured,
1423
1740
  summary: latestSummary,
1424
1741
  nextAction: projectedFact?.nextAction ?? null,
@@ -1429,6 +1746,9 @@ function projectStatus(definition, records, workspaceRoot = null) {
1429
1746
  evidenceRefs: projectedFact?.evidenceRefs ?? [],
1430
1747
  assignmentHistory: history,
1431
1748
  truthRefs,
1749
+ ...(reopenFacts.length === 0
1750
+ ? {}
1751
+ : { invalidatedByReopenIds: invalidatingReopens.map(row => row.reopenId) }),
1432
1752
  };
1433
1753
  });
1434
1754
  const openDecisions = [...state.decisions.values()]
@@ -1794,6 +2114,101 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
1794
2114
  );
1795
2115
  }
1796
2116
  }
2117
+ } else if (normalized.type === "upstream_reopened") {
2118
+ if ([...state.reopens.values()].some(row => row.reopenId === normalized.reopenId)) {
2119
+ throw new Error(`Upstream reopen ${normalized.reopenId} already exists.`);
2120
+ }
2121
+ if (state.assignmentHistory.get(normalized.workItemId).some(
2122
+ row => row.assignmentId === normalized.assignmentId,
2123
+ )) {
2124
+ throw new Error(`Assignment ${normalized.assignmentId} already exists.`);
2125
+ }
2126
+ const current = state.assignments.get(normalized.workItemId) ?? null;
2127
+ const checkpoint = state.checkpoints.get(normalized.workItemId) ?? null;
2128
+ const projected = status.workItems.find(row => row.workItemId === normalized.workItemId);
2129
+ if (projected?.status !== "completed"
2130
+ || !current
2131
+ || current.assignmentId !== normalized.previousAssignmentId
2132
+ || current.agentId !== normalized.fromAgentId
2133
+ || checkpoint?.eventId !== normalized.previousCheckpointEventId
2134
+ || checkpoint.assignmentId !== current.assignmentId
2135
+ || checkpoint.state !== "completed") {
2136
+ throw new Error("reopen-upstream requires the current completed upstream WorkItem.");
2137
+ }
2138
+ const downstreamIds = downstreamWorkItemIds(definition, normalized.workItemId);
2139
+ if (!downstreamIds.includes(normalized.triggerWorkItemId)) {
2140
+ throw new Error("reopen-upstream trigger must be a transitive downstream WorkItem.");
2141
+ }
2142
+ const triggerAssignment = state.assignments.get(normalized.triggerWorkItemId) ?? null;
2143
+ if (triggerAssignment?.agentId !== normalized.reviewerAgentId) {
2144
+ throw new Error("reopen-upstream reviewer must own the trigger WorkItem.");
2145
+ }
2146
+ const affectedIds = [normalized.workItemId, ...downstreamIds];
2147
+ const passedGate = definition.integrationGates.find(gate => (
2148
+ state.gatePasses.has(gate.id)
2149
+ && gate.requiredWorkItemIds.some(id => affectedIds.includes(id))
2150
+ ));
2151
+ if (passedGate) {
2152
+ throw new Error(`reopen-upstream cannot invalidate passed integration gate ${passedGate.id}.`);
2153
+ }
2154
+ for (const otherProjection of status.workItems) {
2155
+ if (otherProjection.workItemId === normalized.workItemId
2156
+ || otherProjection.assignmentId === null
2157
+ || ["completed", "failed"].includes(otherProjection.status)) continue;
2158
+ const other = definition.workItems.find(row => row.id === otherProjection.workItemId);
2159
+ if (pathsOverlap(workItem.ownedPaths, other.ownedPaths)) {
2160
+ throw new Error(
2161
+ `Owned paths overlap active assignment ${otherProjection.assignmentId}: ${normalized.workItemId} <> ${otherProjection.workItemId}.`,
2162
+ );
2163
+ }
2164
+ }
2165
+ } else if (normalized.type === "responsibility_transferred") {
2166
+ if ([...state.transfers.values()].some(row => row.transferId === normalized.transferId)) {
2167
+ throw new Error(`Responsibility transfer ${normalized.transferId} already exists.`);
2168
+ }
2169
+ if (state.assignmentHistory.get(normalized.workItemId).some(
2170
+ row => row.assignmentId === normalized.assignmentId,
2171
+ )) {
2172
+ throw new Error(`Assignment ${normalized.assignmentId} already exists.`);
2173
+ }
2174
+ const current = state.assignments.get(normalized.workItemId) ?? null;
2175
+ const projected = status.workItems.find(row => row.workItemId === normalized.workItemId);
2176
+ if (!current
2177
+ || current.assignmentId !== normalized.previousAssignmentId
2178
+ || current.agentId !== normalized.fromAgentId) {
2179
+ throw new Error("Responsibility transfer must bind the current assignment and Agent.");
2180
+ }
2181
+ if (["completed", "failed"].includes(projected?.status)) {
2182
+ throw new Error("Responsibility transfer cannot target a terminal WorkItem.");
2183
+ }
2184
+ if (projected?.pendingHandoff !== null) {
2185
+ throw new Error("Responsibility transfer cannot replace a pending handoff.");
2186
+ }
2187
+ const existingFactIds = new Set(records.flatMap(({ event }) => [
2188
+ event.eventId,
2189
+ ...(event.type === "responsibility_transferred"
2190
+ ? [event.checkpointEventId, event.handoffEventId, event.assignmentEventId]
2191
+ : []),
2192
+ ]));
2193
+ for (const factId of [
2194
+ normalized.checkpointEventId,
2195
+ normalized.handoffEventId,
2196
+ normalized.assignmentEventId,
2197
+ ]) {
2198
+ if (existingFactIds.has(factId)) {
2199
+ throw new Error(`Responsibility transfer fact ID ${factId} already exists.`);
2200
+ }
2201
+ }
2202
+ if (workItem.measurable) {
2203
+ if (normalized.completedUnits === undefined) {
2204
+ throw new Error("Measured WorkItem transfer requires completedUnits.");
2205
+ }
2206
+ if (normalized.completedUnits > workItem.measurable.total) {
2207
+ throw new Error("Responsibility transfer completedUnits exceeds measurable total.");
2208
+ }
2209
+ } else if (normalized.completedUnits !== undefined) {
2210
+ throw new Error("Unmeasured WorkItem transfer cannot claim completedUnits.");
2211
+ }
1797
2212
  } else if (normalized.type === "decision_opened") {
1798
2213
  if (state.decisions.has(normalized.decisionId)) throw new Error(`Decision ${normalized.decisionId} already exists.`);
1799
2214
  } else if (normalized.type === "decision_resolved") {
@@ -2181,6 +2596,205 @@ export function rejectAndReturnTeamProjectV1({
2181
2596
  });
2182
2597
  }
2183
2598
 
2599
+ export function reopenUpstreamTeamProjectV1({
2600
+ workspaceRoot,
2601
+ reopenId,
2602
+ occurredAt,
2603
+ upstreamWorkItemId,
2604
+ triggerWorkItemId,
2605
+ reviewerAgentId,
2606
+ toAgentId,
2607
+ reason,
2608
+ nextAction,
2609
+ evidenceRefs,
2610
+ }) {
2611
+ safeIdentifier(reopenId, "reopenId");
2612
+ const eventId = `upstream-reopened-${reopenId}`;
2613
+ const assignmentId = `reopen-${reopenId}`;
2614
+ const loaded = loadProject(workspaceRoot);
2615
+ return withProjectLock(loaded.projectRoot, () => {
2616
+ const refreshed = loadProject(workspaceRoot);
2617
+ const existing = refreshed.events.find(row => row.event.eventId === eventId)?.event ?? null;
2618
+ let event;
2619
+ if (existing) {
2620
+ event = {
2621
+ schemaVersion: EVENT_SCHEMA_V4,
2622
+ eventId,
2623
+ type: "upstream_reopened",
2624
+ occurredAt,
2625
+ reopenId,
2626
+ assignmentId,
2627
+ previousAssignmentId: existing.previousAssignmentId,
2628
+ previousCheckpointEventId: existing.previousCheckpointEventId,
2629
+ workItemId: upstreamWorkItemId,
2630
+ triggerWorkItemId,
2631
+ fromAgentId: existing.fromAgentId,
2632
+ reviewerAgentId,
2633
+ toAgentId,
2634
+ reason,
2635
+ evidenceRefs,
2636
+ nextAction,
2637
+ };
2638
+ } else {
2639
+ const state = projectEvents(refreshed.definition, refreshed.events);
2640
+ const current = state.assignments.get(upstreamWorkItemId) ?? null;
2641
+ const checkpoint = state.checkpoints.get(upstreamWorkItemId) ?? null;
2642
+ if (!current || !checkpoint) {
2643
+ throw new Error("reopen-upstream requires an assigned, completed upstream WorkItem.");
2644
+ }
2645
+ event = {
2646
+ schemaVersion: EVENT_SCHEMA_V4,
2647
+ eventId,
2648
+ type: "upstream_reopened",
2649
+ occurredAt,
2650
+ reopenId,
2651
+ assignmentId,
2652
+ previousAssignmentId: current.assignmentId,
2653
+ previousCheckpointEventId: checkpoint.eventId,
2654
+ workItemId: upstreamWorkItemId,
2655
+ triggerWorkItemId,
2656
+ fromAgentId: current.agentId,
2657
+ reviewerAgentId,
2658
+ toAgentId,
2659
+ reason,
2660
+ evidenceRefs,
2661
+ nextAction,
2662
+ };
2663
+ }
2664
+ const recorded = appendTeamProjectEventWithinLock(refreshed, event);
2665
+ const projectedEvents = recorded.resumed
2666
+ ? refreshed.events
2667
+ : [...refreshed.events, { event, ref: `project/events/${event.eventId}.json` }];
2668
+ const status = projectStatus(refreshed.definition, projectedEvents, refreshed.root);
2669
+ const projectedState = projectEvents(refreshed.definition, projectedEvents);
2670
+ const current = status.workItems.find(row => row.workItemId === upstreamWorkItemId) ?? null;
2671
+ const reopen = projectedState.reopens.get(upstreamWorkItemId) ?? null;
2672
+ if (current?.assignmentId !== assignmentId || reopen?.reopenId !== reopenId) {
2673
+ throw new Error(
2674
+ `Cannot resume historical/superseded upstream reopen ${reopenId}; `
2675
+ + `current assignment=${current?.assignmentId ?? "none"}.`,
2676
+ );
2677
+ }
2678
+ return {
2679
+ status: "team_project_upstream_reopened",
2680
+ reopenId,
2681
+ assignmentId,
2682
+ previousAssignmentId: event.previousAssignmentId,
2683
+ previousCheckpointEventId: event.previousCheckpointEventId,
2684
+ upstreamWorkItemId,
2685
+ triggerWorkItemId,
2686
+ fromAgentId: event.fromAgentId,
2687
+ reviewerAgentId,
2688
+ toAgentId,
2689
+ invalidatedWorkItemIds: downstreamWorkItemIds(
2690
+ refreshed.definition,
2691
+ upstreamWorkItemId,
2692
+ { includeRoot: true },
2693
+ ),
2694
+ eventId: recorded.eventId,
2695
+ eventPath: recorded.eventPath,
2696
+ eventSha256: recorded.eventSha256,
2697
+ resumed: recorded.resumed,
2698
+ projectTruthHash: status.projectTruthHash,
2699
+ overall: status.overall,
2700
+ nextAction: status.nextAction,
2701
+ nextActorId: status.nextActorId,
2702
+ gitWritePerformed: false,
2703
+ releaseWritePerformed: false,
2704
+ authorizationGranted: false,
2705
+ exitCode: 0,
2706
+ };
2707
+ });
2708
+ }
2709
+
2710
+ export function transferTeamProjectV1({
2711
+ workspaceRoot,
2712
+ transferId,
2713
+ occurredAt,
2714
+ assignmentId: previousAssignmentId,
2715
+ workItemId,
2716
+ fromAgentId,
2717
+ toAgentId,
2718
+ state,
2719
+ summary,
2720
+ completedUnits,
2721
+ evidenceRefs = [],
2722
+ blockerRefs = [],
2723
+ decisionRefs = [],
2724
+ nextAction,
2725
+ sourceFingerprint,
2726
+ }) {
2727
+ safeIdentifier(transferId, "transferId");
2728
+ const eventId = `responsibility-transferred-${transferId}`;
2729
+ const assignmentId = `transfer-${transferId}`;
2730
+ const event = {
2731
+ schemaVersion: EVENT_SCHEMA_V4,
2732
+ eventId,
2733
+ type: "responsibility_transferred",
2734
+ occurredAt,
2735
+ transferId,
2736
+ assignmentId,
2737
+ previousAssignmentId,
2738
+ checkpointEventId: `transfer-checkpoint-${transferId}`,
2739
+ handoffEventId: `transfer-handoff-${transferId}`,
2740
+ assignmentEventId: `transfer-assignment-${transferId}`,
2741
+ workItemId,
2742
+ fromAgentId,
2743
+ toAgentId,
2744
+ state,
2745
+ summary,
2746
+ ...(completedUnits === undefined ? {} : { completedUnits }),
2747
+ evidenceRefs,
2748
+ blockerRefs,
2749
+ decisionRefs,
2750
+ nextAction,
2751
+ ...(sourceFingerprint === undefined ? {} : { sourceFingerprint }),
2752
+ };
2753
+ const loaded = loadProject(workspaceRoot);
2754
+ return withProjectLock(loaded.projectRoot, () => {
2755
+ const refreshed = loadProject(workspaceRoot);
2756
+ const recorded = appendTeamProjectEventWithinLock(refreshed, event);
2757
+ const projectedEvents = recorded.resumed
2758
+ ? refreshed.events
2759
+ : [...refreshed.events, { event, ref: `project/events/${event.eventId}.json` }];
2760
+ const status = projectStatus(refreshed.definition, projectedEvents, refreshed.root);
2761
+ const projectedState = projectEvents(refreshed.definition, projectedEvents);
2762
+ const current = status.workItems.find(row => row.workItemId === workItemId) ?? null;
2763
+ const transfer = projectedState.transfers.get(workItemId) ?? null;
2764
+ if (current?.assignmentId !== assignmentId || transfer?.transferId !== transferId) {
2765
+ throw new Error(
2766
+ `Cannot resume historical/superseded responsibility transfer ${transferId}; `
2767
+ + `current assignment=${current?.assignmentId ?? "none"}.`,
2768
+ );
2769
+ }
2770
+ const factRefs = ["checkpoint", "handoff", "assignment"]
2771
+ .map(fragment => `project/events/${eventId}.json#${fragment}`);
2772
+ return {
2773
+ status: "team_project_transfer",
2774
+ transferId,
2775
+ assignmentId,
2776
+ previousAssignmentId,
2777
+ workItemId,
2778
+ fromAgentId,
2779
+ toAgentId,
2780
+ state,
2781
+ factRefs,
2782
+ eventId: recorded.eventId,
2783
+ eventPath: recorded.eventPath,
2784
+ eventSha256: recorded.eventSha256,
2785
+ resumed: recorded.resumed,
2786
+ projectTruthHash: status.projectTruthHash,
2787
+ overall: status.overall,
2788
+ nextAction: status.nextAction,
2789
+ nextActorId: status.nextActorId,
2790
+ gitWritePerformed: false,
2791
+ releaseWritePerformed: false,
2792
+ authorizationGranted: false,
2793
+ exitCode: 0,
2794
+ };
2795
+ });
2796
+ }
2797
+
2184
2798
  export function openTeamProjectDecisionV1({
2185
2799
  workspaceRoot,
2186
2800
  decisionId,
@@ -2489,7 +3103,11 @@ export function checkpointTeamProjectV1({
2489
3103
 
2490
3104
  function legacyWorkItemProjection(workItem) {
2491
3105
  if (workItem === null) return null;
2492
- const { latestSourceDelivery: _latestSourceDelivery, ...legacy } = workItem;
3106
+ const {
3107
+ latestSourceDelivery: _latestSourceDelivery,
3108
+ invalidatedByReopenIds: _invalidatedByReopenIds,
3109
+ ...legacy
3110
+ } = workItem;
2493
3111
  return legacy;
2494
3112
  }
2495
3113
 
@@ -2516,6 +3134,76 @@ export function readTeamProjectStatusV2({ workspaceRoot }) {
2516
3134
  };
2517
3135
  }
2518
3136
 
3137
+ export function readTeamProjectHandoffWaitV1({ workspaceRoot }) {
3138
+ const { root, projectRoot: rootPath } = projectRoot(workspaceRoot);
3139
+ if (!existsSync(path.join(rootPath, "definition.json"))) {
3140
+ return {
3141
+ completedHandoffs: 0,
3142
+ unresolvedHandoffs: 0,
3143
+ staleHandoffs: 0,
3144
+ totalMs: 0,
3145
+ averageMs: 0,
3146
+ maxMs: 0,
3147
+ samples: [],
3148
+ inputRefs: [],
3149
+ };
3150
+ }
3151
+ const loaded = loadProject(root);
3152
+ const workFactTypes = new Set([
3153
+ "agent_assigned",
3154
+ "checkpoint_recorded",
3155
+ "handoff_recorded",
3156
+ "rework_returned",
3157
+ "source_delivery_imported",
3158
+ "upstream_reopened",
3159
+ "responsibility_transferred",
3160
+ ]);
3161
+ const samples = [];
3162
+ let unresolvedHandoffs = 0;
3163
+ let staleHandoffs = 0;
3164
+ const handoffs = loaded.events.filter(({ event }) => event.type === "handoff_recorded");
3165
+ for (const { event, ref } of handoffs) {
3166
+ const laterFacts = loaded.events.filter(({ event: later }) => (
3167
+ later.workItemId === event.workItemId
3168
+ && compareEventFacts(later, event) > 0
3169
+ && workFactTypes.has(later.type)
3170
+ ));
3171
+ const accepted = laterFacts.find(({ event: later }) => (
3172
+ later.type === "agent_assigned"
3173
+ && later.supersedesAssignmentId === event.assignmentId
3174
+ && later.agentId === event.toAgentId
3175
+ ));
3176
+ if (accepted) {
3177
+ const durationMs = Date.parse(accepted.event.occurredAt) - Date.parse(event.occurredAt);
3178
+ samples.push({
3179
+ handoffEventId: event.eventId,
3180
+ acceptedEventId: accepted.event.eventId,
3181
+ workItemId: event.workItemId,
3182
+ durationMs,
3183
+ handoffRef: ref,
3184
+ acceptedRef: accepted.ref,
3185
+ });
3186
+ } else if (laterFacts.length > 0) {
3187
+ staleHandoffs += 1;
3188
+ } else {
3189
+ unresolvedHandoffs += 1;
3190
+ }
3191
+ }
3192
+ const totalMs = samples.reduce((total, sample) => total + sample.durationMs, 0);
3193
+ return {
3194
+ completedHandoffs: samples.length,
3195
+ unresolvedHandoffs,
3196
+ staleHandoffs,
3197
+ totalMs,
3198
+ averageMs: samples.length === 0 ? 0 : Math.round(totalMs / samples.length),
3199
+ maxMs: samples.length === 0 ? 0 : Math.max(...samples.map(sample => sample.durationMs)),
3200
+ samples,
3201
+ inputRefs: loaded.events
3202
+ .filter(({ event }) => event.type === "handoff_recorded" || event.type === "agent_assigned")
3203
+ .map(({ ref }) => ref),
3204
+ };
3205
+ }
3206
+
2519
3207
  export function buildTeamProjectTakeoverV2({ workspaceRoot, agentId }) {
2520
3208
  safeIdentifier(agentId, "agentId");
2521
3209
  const loaded = loadProject(workspaceRoot);