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.
- package/CHANGELOG.md +38 -0
- package/README.md +102 -4
- package/docs/architecture/OWLCODA_RUNKIT_0190_CONSOLIDATED_DAILY_USE_PRODUCT_DECISION_20260812.md +97 -0
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +57 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +28 -4
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
- package/integrations/codex/skills/owlcoda-runkit/references/contract-v0.2.md +57 -0
- package/package.json +1 -1
- package/packages/attest/README.md +9 -5
- package/packages/attest/consumer-surface-v2.json +263 -0
- package/packages/attest/distribution-manifest-v1.json +9 -1
- package/packages/attest/schemas/quick-verification-receipt-v3.schema.json +244 -0
- package/packages/attest/schemas/workspace-snapshot-v2.schema.json +114 -0
- package/packages/attest/src/dependency-environment.mjs +128 -0
- package/packages/attest/src/index.mjs +1 -0
- package/packages/attest/src/quick-receipt-contract.mjs +202 -9
- package/packages/attest/src/quick.mjs +40 -4
- package/packages/attest/src/workspace-snapshot.mjs +133 -6
- package/registry-release-policy-v1.json +5 -5
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/core-contract.mjs +21 -1
- package/scripts/runkit-contract/efficiency-metrics.mjs +559 -0
- package/scripts/runkit-contract/evidence-store.mjs +130 -0
- package/scripts/runkit-contract/onboarding-bootstrap.mjs +685 -0
- package/scripts/runkit-contract/onboarding-doctor.mjs +2 -1
- package/scripts/runkit-contract/outcome-presentation.mjs +171 -0
- package/scripts/runkit-contract/profile-onboarding.mjs +22 -2
- package/scripts/runkit-contract/project-cli-resolver.mjs +23 -5
- package/scripts/runkit-contract/quick-evidence-reuse.mjs +590 -0
- package/scripts/runkit-contract/quick-isolation.mjs +314 -0
- package/scripts/runkit-contract/quick-metrics.mjs +51 -3
- package/scripts/runkit-contract/quick-receipt.mjs +31 -1
- package/scripts/runkit-contract/quick-verify.mjs +216 -25
- package/scripts/runkit-contract/quick-workspace-snapshot.mjs +1 -0
- package/scripts/runkit-contract/runkit-bootstrap.mjs +1 -0
- package/scripts/runkit-contract/runkit-cli.mjs +330 -18
- package/scripts/runkit-contract/runtime-transition.mjs +478 -0
- package/scripts/runkit-contract/schemas/efficiency-event-v1.schema.json +56 -0
- package/scripts/runkit-contract/schemas/efficiency-metrics-v1.schema.json +220 -0
- package/scripts/runkit-contract/schemas/external-runtime-report-v1.schema.json +125 -0
- package/scripts/runkit-contract/schemas/onboarding-bootstrap-plan-v1.schema.json +168 -0
- package/scripts/runkit-contract/schemas/onboarding-bootstrap-receipt-v1.schema.json +130 -0
- package/scripts/runkit-contract/schemas/outcome-summary-v1.schema.json +83 -0
- package/scripts/runkit-contract/schemas/quick-evidence-reuse-plan-v1.schema.json +59 -0
- package/scripts/runkit-contract/schemas/quick-evidence-reuse-receipt-v1.schema.json +79 -0
- package/scripts/runkit-contract/schemas/runtime-transition-receipt-v1.schema.json +185 -0
- package/scripts/runkit-contract/schemas/team-project-event-v4.schema.json +142 -0
- package/scripts/runkit-contract/team-project.mjs +704 -16
- 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"
|
|
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]
|
|
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 ${
|
|
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
|
|
1328
|
-
|
|
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
|
-
|
|
1331
|
-
|
|
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
|
|
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
|
-
?
|
|
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 =
|
|
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:
|
|
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
|
|
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:
|
|
1419
|
-
?
|
|
1735
|
+
blockers: checkpointLikeFact
|
|
1736
|
+
? checkpointLikeFact.blockerRefs
|
|
1420
1737
|
: unresolvedDependencies.map(id => `work-item:${id}`),
|
|
1421
|
-
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 {
|
|
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);
|