owlrunkit 0.18.3 → 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 +80 -0
- package/README.md +223 -52
- package/docs/architecture/OWLCODA_RUNKIT_0184_EXTERNAL_DELIVERY_INTAKE_PRODUCT_DECISION_20260812.md +209 -0
- package/docs/architecture/OWLCODA_RUNKIT_0190_CONSOLIDATED_DAILY_USE_PRODUCT_DECISION_20260812.md +97 -0
- package/docs/architecture/OWLCODA_RUNKIT_USAGE_MODE_SELECTION_PRODUCT_DECISION_20260811.md +8 -7
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +57 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +133 -77
- 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 +14 -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 +205 -12
- package/packages/attest/src/quick.mjs +45 -4
- package/packages/attest/src/workspace-snapshot.mjs +143 -7
- package/registry-release-policy-v1.json +5 -5
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/assurance-router.mjs +25 -6
- package/scripts/runkit-contract/core-contract.mjs +29 -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 +3 -2
- package/scripts/runkit-contract/outcome-presentation.mjs +171 -0
- package/scripts/runkit-contract/profile-onboarding.mjs +285 -17
- package/scripts/runkit-contract/project-cli-resolver.mjs +29 -5
- package/scripts/runkit-contract/project-external-delivery.mjs +707 -0
- 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 +217 -26
- package/scripts/runkit-contract/quick-workspace-snapshot.mjs +1 -0
- package/scripts/runkit-contract/runkit-bootstrap.mjs +7 -1
- package/scripts/runkit-contract/runkit-cli.mjs +602 -152
- package/scripts/runkit-contract/runtime-transition.mjs +478 -0
- package/scripts/runkit-contract/schemas/assurance-route-v2.schema.json +26 -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/profile-detection-v3.schema.json +170 -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-v3.schema.json +146 -0
- package/scripts/runkit-contract/schemas/team-project-event-v4.schema.json +142 -0
- package/scripts/runkit-contract/schemas/team-project-status-v2.schema.json +85 -0
- package/scripts/runkit-contract/schemas/usage-mode-recommendation-v2.schema.json +136 -0
- package/scripts/runkit-contract/team-project.mjs +1172 -22
- package/scripts/runkit-contract/usage-mode.mjs +28 -7
- package/scripts/runkit-contract/verification-envelope.mjs +15 -18
|
@@ -12,6 +12,11 @@ import path from "node:path";
|
|
|
12
12
|
|
|
13
13
|
import { validateLeaseOwnedPaths } from "./core-contract.mjs";
|
|
14
14
|
import { inspectProjectControlState } from "./project-control-state.mjs";
|
|
15
|
+
import { canonicalSourceFingerprint } from "./source-fingerprint.mjs";
|
|
16
|
+
import {
|
|
17
|
+
quickSnapshotFingerprintValid,
|
|
18
|
+
validWorkspaceSnapshotShape,
|
|
19
|
+
} from "../../packages/attest/src/quick-receipt-contract.mjs";
|
|
15
20
|
import {
|
|
16
21
|
assertAllowedKeys,
|
|
17
22
|
isRecord,
|
|
@@ -24,7 +29,10 @@ const PROJECT_ROOT = ".owlcoda/runkit/project";
|
|
|
24
29
|
const DEFINITION_SCHEMA = "OwlCodaRunKitTeamProjectDefinitionV1";
|
|
25
30
|
const EVENT_SCHEMA = "OwlCodaRunKitTeamProjectEventV1";
|
|
26
31
|
const EVENT_SCHEMA_V2 = "OwlCodaRunKitTeamProjectEventV2";
|
|
27
|
-
const
|
|
32
|
+
const EVENT_SCHEMA_V3 = "OwlCodaRunKitTeamProjectEventV3";
|
|
33
|
+
const EVENT_SCHEMA_V4 = "OwlCodaRunKitTeamProjectEventV4";
|
|
34
|
+
const STATUS_SCHEMA_V1 = "OwlCodaRunKitTeamProjectStatusV1";
|
|
35
|
+
const STATUS_SCHEMA_V2 = "OwlCodaRunKitTeamProjectStatusV2";
|
|
28
36
|
|
|
29
37
|
const DEFINITION_KEYS = [
|
|
30
38
|
"schemaVersion",
|
|
@@ -92,6 +100,30 @@ const EVENT_KEYS = Object.freeze({
|
|
|
92
100
|
"sourceRef", "rights", "inputRef", "outputRef", "decisionRef",
|
|
93
101
|
"verificationRefs", "outcomeRef", "version",
|
|
94
102
|
],
|
|
103
|
+
source_delivery_imported: [
|
|
104
|
+
"schemaVersion", "eventId", "type", "occurredAt", "deliveryId",
|
|
105
|
+
"projectId", "projectDefinitionSha256", "assignmentId", "workItemId",
|
|
106
|
+
"producerId", "targetWorkspaceRef",
|
|
107
|
+
"targetHead", "entrySnapshotRef", "entrySnapshotSha256",
|
|
108
|
+
"deliveryPacketSha256", "deliveryCandidateFingerprint",
|
|
109
|
+
"workspaceSnapshotFingerprint", "overlayStatus", "candidateFiles",
|
|
110
|
+
"changedPaths", "toolCachePaths", "dependencyEnvironmentPaths",
|
|
111
|
+
"unchangedPacketPaths", "outOfScopeSourcePaths", "targetSnapshot",
|
|
112
|
+
"proven", "notProven", "targetWritePerformed", "authorizationGranted",
|
|
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
|
+
],
|
|
95
127
|
});
|
|
96
128
|
|
|
97
129
|
function canonical(value) {
|
|
@@ -122,6 +154,26 @@ function nullableString(value, label) {
|
|
|
122
154
|
return nonEmptyString(value, label);
|
|
123
155
|
}
|
|
124
156
|
|
|
157
|
+
function sha256Ref(value, label) {
|
|
158
|
+
if (typeof value !== "string" || !/^sha256:[a-f0-9]{64}$/u.test(value)) {
|
|
159
|
+
throw new Error(`${label} must be a canonical SHA-256 reference.`);
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function safeSourcePath(value, label) {
|
|
165
|
+
if (typeof value !== "string"
|
|
166
|
+
|| value.length === 0
|
|
167
|
+
|| path.isAbsolute(value)
|
|
168
|
+
|| path.win32.parse(value).root
|
|
169
|
+
|| value.includes("\\")
|
|
170
|
+
|| value.includes("\0")
|
|
171
|
+
|| value.split("/").some(segment => !segment || segment === "." || segment === "..")) {
|
|
172
|
+
throw new Error(`${label} must be a safe repository-relative path.`);
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
|
|
125
177
|
function stringArray(value, label, { allowEmpty = true } = {}) {
|
|
126
178
|
if (!Array.isArray(value)
|
|
127
179
|
|| (!allowEmpty && value.length === 0)
|
|
@@ -167,6 +219,30 @@ function withProjectLock(root, operation) {
|
|
|
167
219
|
}
|
|
168
220
|
}
|
|
169
221
|
|
|
222
|
+
export function teamProjectDefinitionBindingV1(definition) {
|
|
223
|
+
const normalized = validateDefinition(definition);
|
|
224
|
+
return {
|
|
225
|
+
projectId: normalized.projectId,
|
|
226
|
+
projectDefinitionSha256: `sha256:${sha256(canonical(normalized))}`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function withTeamProjectControlLockV1({ workspaceRoot, operation }) {
|
|
231
|
+
if (typeof operation !== "function") {
|
|
232
|
+
throw new Error("Team project control operation must be a function.");
|
|
233
|
+
}
|
|
234
|
+
const loaded = loadProject(workspaceRoot);
|
|
235
|
+
return withProjectLock(loaded.projectRoot, () => {
|
|
236
|
+
const refreshed = loadProject(workspaceRoot);
|
|
237
|
+
return operation({
|
|
238
|
+
workspaceRoot: refreshed.root,
|
|
239
|
+
projectRoot: refreshed.projectRoot,
|
|
240
|
+
definition: structuredClone(refreshed.definition),
|
|
241
|
+
definitionBinding: teamProjectDefinitionBindingV1(refreshed.definition),
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
170
246
|
function uniqueIds(rows, label) {
|
|
171
247
|
const ids = rows.map(row => safeIdentifier(row.id, `${label} id`));
|
|
172
248
|
if (new Set(ids).size !== ids.length) throw new Error(`${label} ids must be unique.`);
|
|
@@ -381,6 +457,22 @@ function latestEventFact(left, right) {
|
|
|
381
457
|
return compareEventFacts(left, right) >= 0 ? left : right;
|
|
382
458
|
}
|
|
383
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
|
+
|
|
384
476
|
function pendingHandoffProjection(handoff) {
|
|
385
477
|
return handoff
|
|
386
478
|
? {
|
|
@@ -420,6 +512,140 @@ function reworkProjection(rework, attempt) {
|
|
|
420
512
|
: null;
|
|
421
513
|
}
|
|
422
514
|
|
|
515
|
+
function sourceDeliveryProjection(delivery) {
|
|
516
|
+
return delivery
|
|
517
|
+
? {
|
|
518
|
+
projectId: delivery.projectId,
|
|
519
|
+
projectDefinitionSha256: delivery.projectDefinitionSha256,
|
|
520
|
+
deliveryId: delivery.deliveryId,
|
|
521
|
+
workItemId: delivery.workItemId,
|
|
522
|
+
assignmentId: delivery.assignmentId,
|
|
523
|
+
producerId: delivery.producerId,
|
|
524
|
+
targetWorkspaceRef: delivery.targetWorkspaceRef,
|
|
525
|
+
targetHead: delivery.targetHead,
|
|
526
|
+
entrySnapshotRef: delivery.entrySnapshotRef,
|
|
527
|
+
entrySnapshotSha256: delivery.entrySnapshotSha256,
|
|
528
|
+
deliveryPacketSha256: delivery.deliveryPacketSha256,
|
|
529
|
+
deliveryCandidateFingerprint: delivery.deliveryCandidateFingerprint,
|
|
530
|
+
workspaceSnapshotFingerprint: delivery.workspaceSnapshotFingerprint,
|
|
531
|
+
overlayStatus: delivery.overlayStatus,
|
|
532
|
+
candidateFiles: delivery.candidateFiles.map(row => ({ ...row })),
|
|
533
|
+
candidatePaths: delivery.candidateFiles.map(row => row.path),
|
|
534
|
+
changedPaths: [...delivery.changedPaths],
|
|
535
|
+
toolCachePaths: [...delivery.toolCachePaths],
|
|
536
|
+
dependencyEnvironmentPaths: [...delivery.dependencyEnvironmentPaths],
|
|
537
|
+
unchangedPacketPaths: [...delivery.unchangedPacketPaths],
|
|
538
|
+
proven: [...delivery.proven],
|
|
539
|
+
notProven: [...delivery.notProven],
|
|
540
|
+
occurredAt: delivery.occurredAt,
|
|
541
|
+
truthRef: delivery.ref,
|
|
542
|
+
targetWritePerformed: false,
|
|
543
|
+
authorizationGranted: false,
|
|
544
|
+
}
|
|
545
|
+
: null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function sourceDeliveryDelta(deliveries) {
|
|
549
|
+
const current = deliveries.at(-1) ?? null;
|
|
550
|
+
if (!current) return null;
|
|
551
|
+
const previous = deliveries.at(-2) ?? null;
|
|
552
|
+
if (!previous) {
|
|
553
|
+
return {
|
|
554
|
+
status: "first_delivery",
|
|
555
|
+
currentDeliveryId: current.deliveryId,
|
|
556
|
+
previousDeliveryId: null,
|
|
557
|
+
addedPaths: current.candidateFiles.map(row => row.path),
|
|
558
|
+
changedPaths: [],
|
|
559
|
+
removedPaths: [],
|
|
560
|
+
unchangedPaths: [],
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
const currentFiles = new Map(current.candidateFiles.map(row => [row.path, row.sha256]));
|
|
564
|
+
const previousFiles = new Map(previous.candidateFiles.map(row => [row.path, row.sha256]));
|
|
565
|
+
const allPaths = uniqueSorted([...currentFiles.keys(), ...previousFiles.keys()]);
|
|
566
|
+
const addedPaths = allPaths.filter(filePath => (
|
|
567
|
+
currentFiles.has(filePath) && !previousFiles.has(filePath)
|
|
568
|
+
));
|
|
569
|
+
const removedPaths = allPaths.filter(filePath => (
|
|
570
|
+
!currentFiles.has(filePath) && previousFiles.has(filePath)
|
|
571
|
+
));
|
|
572
|
+
const changedPaths = allPaths.filter(filePath => (
|
|
573
|
+
currentFiles.has(filePath)
|
|
574
|
+
&& previousFiles.has(filePath)
|
|
575
|
+
&& currentFiles.get(filePath) !== previousFiles.get(filePath)
|
|
576
|
+
));
|
|
577
|
+
const unchangedPaths = allPaths.filter(filePath => (
|
|
578
|
+
currentFiles.has(filePath)
|
|
579
|
+
&& previousFiles.has(filePath)
|
|
580
|
+
&& currentFiles.get(filePath) === previousFiles.get(filePath)
|
|
581
|
+
));
|
|
582
|
+
return {
|
|
583
|
+
status: addedPaths.length || removedPaths.length || changedPaths.length
|
|
584
|
+
? "changed"
|
|
585
|
+
: "unchanged",
|
|
586
|
+
currentDeliveryId: current.deliveryId,
|
|
587
|
+
previousDeliveryId: previous.deliveryId,
|
|
588
|
+
addedPaths,
|
|
589
|
+
changedPaths,
|
|
590
|
+
removedPaths,
|
|
591
|
+
unchangedPaths,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function staleHandoffsForTakeover({ loaded, status, agentId, responsibilities }) {
|
|
596
|
+
const responsibilityIds = new Set(responsibilities.map(row => row.workItemId));
|
|
597
|
+
const pendingEventIds = new Set(status.workItems
|
|
598
|
+
.map(row => row.pendingHandoff?.eventId)
|
|
599
|
+
.filter(Boolean));
|
|
600
|
+
return loaded.events
|
|
601
|
+
.filter(({ event }) => (
|
|
602
|
+
event.type === "handoff_recorded"
|
|
603
|
+
&& !pendingEventIds.has(event.eventId)
|
|
604
|
+
&& (responsibilityIds.has(event.workItemId)
|
|
605
|
+
|| event.fromAgentId === agentId
|
|
606
|
+
|| event.toAgentId === agentId)
|
|
607
|
+
))
|
|
608
|
+
.map(({ event, ref }) => {
|
|
609
|
+
const laterFacts = loaded.events.filter(row => (
|
|
610
|
+
row.event.workItemId === event.workItemId
|
|
611
|
+
&& compareEventFacts(row.event, event) > 0
|
|
612
|
+
&& [
|
|
613
|
+
"agent_assigned",
|
|
614
|
+
"checkpoint_recorded",
|
|
615
|
+
"handoff_recorded",
|
|
616
|
+
"rework_returned",
|
|
617
|
+
"source_delivery_imported",
|
|
618
|
+
"upstream_reopened",
|
|
619
|
+
"responsibility_transferred",
|
|
620
|
+
].includes(row.event.type)
|
|
621
|
+
));
|
|
622
|
+
let reason = "superseded_by_current_truth";
|
|
623
|
+
if (laterFacts.some(row => row.event.type === "handoff_recorded")) {
|
|
624
|
+
reason = "redirected_by_later_handoff";
|
|
625
|
+
} else if (laterFacts.some(row => (
|
|
626
|
+
row.event.type === "agent_assigned"
|
|
627
|
+
|| row.event.type === "rework_returned"
|
|
628
|
+
|| row.event.type === "upstream_reopened"
|
|
629
|
+
|| row.event.type === "responsibility_transferred"
|
|
630
|
+
))) {
|
|
631
|
+
reason = "superseded_by_assignment";
|
|
632
|
+
} else if (laterFacts.length > 0) {
|
|
633
|
+
reason = "superseded_by_later_work_fact";
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
eventId: event.eventId,
|
|
637
|
+
truthRef: ref,
|
|
638
|
+
assignmentId: event.assignmentId,
|
|
639
|
+
workItemId: event.workItemId,
|
|
640
|
+
fromAgentId: event.fromAgentId,
|
|
641
|
+
toAgentId: event.toAgentId,
|
|
642
|
+
occurredAt: event.occurredAt,
|
|
643
|
+
reason,
|
|
644
|
+
};
|
|
645
|
+
})
|
|
646
|
+
.sort((left, right) => compareIdentifiers(left.eventId, right.eventId));
|
|
647
|
+
}
|
|
648
|
+
|
|
423
649
|
function validateTimestamp(value) {
|
|
424
650
|
if (typeof value !== "string"
|
|
425
651
|
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
|
|
@@ -434,14 +660,30 @@ function validateEventShape(value, definition) {
|
|
|
434
660
|
throw new Error("Team project event type is invalid.");
|
|
435
661
|
}
|
|
436
662
|
assertAllowedKeys(value, `Team project event ${value.eventId ?? "unknown"}`, EVENT_KEYS[value.type]);
|
|
437
|
-
if (![EVENT_SCHEMA, EVENT_SCHEMA_V2]
|
|
663
|
+
if (![EVENT_SCHEMA, EVENT_SCHEMA_V2, EVENT_SCHEMA_V3, EVENT_SCHEMA_V4]
|
|
664
|
+
.includes(value.schemaVersion)) {
|
|
438
665
|
throw new Error(
|
|
439
|
-
`Team project event schemaVersion must be ${EVENT_SCHEMA} or ${
|
|
666
|
+
`Team project event schemaVersion must be ${EVENT_SCHEMA}, ${EVENT_SCHEMA_V2}, ${EVENT_SCHEMA_V3}, or ${EVENT_SCHEMA_V4}.`,
|
|
440
667
|
);
|
|
441
668
|
}
|
|
442
669
|
if (value.type === "rework_returned" && value.schemaVersion !== EVENT_SCHEMA_V2) {
|
|
443
670
|
throw new Error(`rework_returned requires schemaVersion ${EVENT_SCHEMA_V2}.`);
|
|
444
671
|
}
|
|
672
|
+
if (value.type === "source_delivery_imported" && value.schemaVersion !== EVENT_SCHEMA_V3) {
|
|
673
|
+
throw new Error(`source_delivery_imported requires schemaVersion ${EVENT_SCHEMA_V3}.`);
|
|
674
|
+
}
|
|
675
|
+
if (value.schemaVersion === EVENT_SCHEMA_V3 && value.type !== "source_delivery_imported") {
|
|
676
|
+
throw new Error(`${EVENT_SCHEMA_V3} is reserved for source_delivery_imported events.`);
|
|
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
|
+
}
|
|
445
687
|
safeIdentifier(value.eventId, "eventId");
|
|
446
688
|
validateTimestamp(value.occurredAt);
|
|
447
689
|
const workItemIds = new Set(definition.workItems.map(row => row.id));
|
|
@@ -455,6 +697,9 @@ function validateEventShape(value, definition) {
|
|
|
455
697
|
"checkpoint_recorded",
|
|
456
698
|
"handoff_recorded",
|
|
457
699
|
"rework_returned",
|
|
700
|
+
"source_delivery_imported",
|
|
701
|
+
"upstream_reopened",
|
|
702
|
+
"responsibility_transferred",
|
|
458
703
|
"verification_deferred",
|
|
459
704
|
].includes(value.type)) {
|
|
460
705
|
assertWorkItem(value.workItemId);
|
|
@@ -464,6 +709,9 @@ function validateEventShape(value, definition) {
|
|
|
464
709
|
"checkpoint_recorded",
|
|
465
710
|
"handoff_recorded",
|
|
466
711
|
"rework_returned",
|
|
712
|
+
"source_delivery_imported",
|
|
713
|
+
"upstream_reopened",
|
|
714
|
+
"responsibility_transferred",
|
|
467
715
|
].includes(value.type)) {
|
|
468
716
|
safeIdentifier(value.assignmentId, "assignmentId");
|
|
469
717
|
}
|
|
@@ -582,6 +830,138 @@ function validateEventShape(value, definition) {
|
|
|
582
830
|
stringArray(value.verificationRefs, "Data candidate verificationRefs");
|
|
583
831
|
nullableString(value.outcomeRef, "Data candidate outcomeRef");
|
|
584
832
|
nonEmptyString(value.version, "Data candidate version");
|
|
833
|
+
} else if (value.type === "source_delivery_imported") {
|
|
834
|
+
safeIdentifier(value.projectId, "External delivery projectId");
|
|
835
|
+
sha256Ref(value.projectDefinitionSha256, "External delivery projectDefinitionSha256");
|
|
836
|
+
const definitionBinding = teamProjectDefinitionBindingV1(definition);
|
|
837
|
+
if (value.projectId !== definitionBinding.projectId
|
|
838
|
+
|| value.projectDefinitionSha256 !== definitionBinding.projectDefinitionSha256) {
|
|
839
|
+
throw new Error("External delivery project identity does not match current project truth.");
|
|
840
|
+
}
|
|
841
|
+
safeIdentifier(value.deliveryId, "deliveryId");
|
|
842
|
+
safeIdentifier(value.producerId, "producerId");
|
|
843
|
+
nonEmptyString(value.targetWorkspaceRef, "External delivery targetWorkspaceRef");
|
|
844
|
+
if (!path.isAbsolute(value.targetWorkspaceRef)) {
|
|
845
|
+
throw new Error("External delivery targetWorkspaceRef must be absolute.");
|
|
846
|
+
}
|
|
847
|
+
if (typeof value.targetHead !== "string"
|
|
848
|
+
|| !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(value.targetHead)) {
|
|
849
|
+
throw new Error("External delivery targetHead is invalid.");
|
|
850
|
+
}
|
|
851
|
+
if ((value.entrySnapshotRef === null) !== (value.entrySnapshotSha256 === null)) {
|
|
852
|
+
throw new Error("External delivery entry snapshot ref and hash must be supplied together.");
|
|
853
|
+
}
|
|
854
|
+
if (value.entrySnapshotRef !== null) {
|
|
855
|
+
nonEmptyString(value.entrySnapshotRef, "External delivery entrySnapshotRef");
|
|
856
|
+
sha256Ref(value.entrySnapshotSha256, "External delivery entrySnapshotSha256");
|
|
857
|
+
}
|
|
858
|
+
sha256Ref(value.deliveryPacketSha256, "External delivery packet hash");
|
|
859
|
+
if (typeof value.deliveryCandidateFingerprint !== "string"
|
|
860
|
+
|| !/^[a-f0-9]{64}$/u.test(value.deliveryCandidateFingerprint)) {
|
|
861
|
+
throw new Error("External delivery candidate fingerprint is invalid.");
|
|
862
|
+
}
|
|
863
|
+
sha256Ref(value.workspaceSnapshotFingerprint, "External delivery workspace snapshot fingerprint");
|
|
864
|
+
if (!["candidate_only", "attested", "attested_with_non_source_changes"].includes(value.overlayStatus)) {
|
|
865
|
+
throw new Error("External delivery overlayStatus is invalid.");
|
|
866
|
+
}
|
|
867
|
+
if (!Array.isArray(value.candidateFiles) || value.candidateFiles.length === 0) {
|
|
868
|
+
throw new Error("External delivery candidateFiles must contain at least one file.");
|
|
869
|
+
}
|
|
870
|
+
const candidateFiles = value.candidateFiles.map((row) => {
|
|
871
|
+
assertAllowedKeys(row, "External delivery candidate file", ["path", "sha256"]);
|
|
872
|
+
const filePath = safeSourcePath(row.path, "External delivery candidate file path");
|
|
873
|
+
if (typeof row.sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(row.sha256)) {
|
|
874
|
+
throw new Error(`External delivery candidate file hash is invalid: ${filePath}.`);
|
|
875
|
+
}
|
|
876
|
+
return { path: filePath, sha256: row.sha256 };
|
|
877
|
+
});
|
|
878
|
+
if (new Set(candidateFiles.map(row => row.path)).size !== candidateFiles.length
|
|
879
|
+
|| canonical([...candidateFiles].sort((left, right) => (
|
|
880
|
+
compareIdentifiers(left.path, right.path)
|
|
881
|
+
)))
|
|
882
|
+
!== canonical(candidateFiles)) {
|
|
883
|
+
throw new Error("External delivery candidateFiles must be uniquely sorted by path.");
|
|
884
|
+
}
|
|
885
|
+
if (canonicalSourceFingerprint(Object.fromEntries(
|
|
886
|
+
candidateFiles.map(row => [row.path, row.sha256]),
|
|
887
|
+
)) !== value.deliveryCandidateFingerprint) {
|
|
888
|
+
throw new Error("External delivery candidate fingerprint does not match its file map.");
|
|
889
|
+
}
|
|
890
|
+
for (const [field, label] of [
|
|
891
|
+
["changedPaths", "External delivery changedPaths"],
|
|
892
|
+
["toolCachePaths", "External delivery toolCachePaths"],
|
|
893
|
+
["dependencyEnvironmentPaths", "External delivery dependencyEnvironmentPaths"],
|
|
894
|
+
["unchangedPacketPaths", "External delivery unchangedPacketPaths"],
|
|
895
|
+
["outOfScopeSourcePaths", "External delivery outOfScopeSourcePaths"],
|
|
896
|
+
]) {
|
|
897
|
+
const paths = stringArray(value[field], label);
|
|
898
|
+
for (const filePath of paths) safeSourcePath(filePath, label);
|
|
899
|
+
if (canonical([...paths].sort()) !== canonical(paths)) {
|
|
900
|
+
throw new Error(`${label} must be sorted.`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (value.outOfScopeSourcePaths.length !== 0) {
|
|
904
|
+
throw new Error("An imported external delivery cannot contain out-of-scope source paths.");
|
|
905
|
+
}
|
|
906
|
+
stringArray(value.proven, "External delivery proven statements", { allowEmpty: false });
|
|
907
|
+
stringArray(value.notProven, "External delivery notProven statements", { allowEmpty: false });
|
|
908
|
+
if (!validWorkspaceSnapshotShape(value.targetSnapshot)
|
|
909
|
+
|| !quickSnapshotFingerprintValid(value.targetSnapshot)
|
|
910
|
+
|| value.targetSnapshot.sourceFingerprint !== value.workspaceSnapshotFingerprint) {
|
|
911
|
+
throw new Error("External delivery targetSnapshot identity is invalid.");
|
|
912
|
+
}
|
|
913
|
+
if (value.targetWritePerformed !== false || value.authorizationGranted !== false) {
|
|
914
|
+
throw new Error("External delivery cannot grant authority or write the target workspace.");
|
|
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
|
+
}
|
|
585
965
|
}
|
|
586
966
|
return structuredClone(value);
|
|
587
967
|
}
|
|
@@ -592,13 +972,20 @@ function projectEvents(definition, records) {
|
|
|
592
972
|
const checkpoints = new Map();
|
|
593
973
|
const handoffs = new Map();
|
|
594
974
|
const reworks = new Map();
|
|
975
|
+
const reopens = new Map();
|
|
976
|
+
const transfers = new Map();
|
|
595
977
|
const decisions = new Map();
|
|
596
978
|
const gatePasses = new Map();
|
|
597
979
|
const verifications = new Map();
|
|
598
980
|
const dataCandidates = [];
|
|
981
|
+
const sourceDeliveries = [];
|
|
599
982
|
const seenAssignments = new Set();
|
|
600
983
|
const seenReworks = new Set();
|
|
984
|
+
const seenReopens = new Set();
|
|
985
|
+
const seenTransfers = new Set();
|
|
601
986
|
const seenDecisions = new Set();
|
|
987
|
+
const seenDeliveries = new Set();
|
|
988
|
+
const seenFactIds = new Set(records.map(row => row.event.eventId));
|
|
602
989
|
|
|
603
990
|
for (const { event, ref } of records) {
|
|
604
991
|
validateEventShape(event, definition);
|
|
@@ -692,6 +1079,185 @@ function projectEvents(definition, records) {
|
|
|
692
1079
|
throw new Error(`Duplicate data candidate ${event.candidateId}.`);
|
|
693
1080
|
}
|
|
694
1081
|
dataCandidates.push({ ...event, ref });
|
|
1082
|
+
} else if (event.type === "source_delivery_imported") {
|
|
1083
|
+
if (seenDeliveries.has(event.deliveryId)) {
|
|
1084
|
+
throw new Error(`Duplicate external source delivery ${event.deliveryId}.`);
|
|
1085
|
+
}
|
|
1086
|
+
const current = assignments.get(event.workItemId) ?? null;
|
|
1087
|
+
if (current?.assignmentId !== event.assignmentId) {
|
|
1088
|
+
throw new Error(
|
|
1089
|
+
`External source delivery ${event.deliveryId} does not bind the current assignment.`,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
const currentCheckpoint = checkpoints.get(event.workItemId) ?? null;
|
|
1093
|
+
if (currentCheckpoint?.assignmentId === current.assignmentId
|
|
1094
|
+
&& ["completed", "failed"].includes(currentCheckpoint.state)) {
|
|
1095
|
+
throw new Error(
|
|
1096
|
+
`External source delivery ${event.deliveryId} cannot attach to terminal WorkItem truth.`,
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
seenDeliveries.add(event.deliveryId);
|
|
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 });
|
|
695
1261
|
}
|
|
696
1262
|
}
|
|
697
1263
|
return {
|
|
@@ -700,10 +1266,13 @@ function projectEvents(definition, records) {
|
|
|
700
1266
|
checkpoints,
|
|
701
1267
|
handoffs,
|
|
702
1268
|
reworks,
|
|
1269
|
+
reopens,
|
|
1270
|
+
transfers,
|
|
703
1271
|
decisions,
|
|
704
1272
|
gatePasses,
|
|
705
1273
|
verifications,
|
|
706
1274
|
dataCandidates,
|
|
1275
|
+
sourceDeliveries,
|
|
707
1276
|
};
|
|
708
1277
|
}
|
|
709
1278
|
|
|
@@ -1020,25 +1589,79 @@ function deriveProjectDriverSummary(status) {
|
|
|
1020
1589
|
};
|
|
1021
1590
|
}
|
|
1022
1591
|
|
|
1023
|
-
function
|
|
1024
|
-
|
|
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) {
|
|
1025
1606
|
const completed = new Set();
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
+
}
|
|
1028
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);
|
|
1029
1626
|
let workItems = definition.workItems.map((item) => {
|
|
1030
1627
|
const assignment = state.assignments.get(item.id) ?? null;
|
|
1031
1628
|
const checkpoint = state.checkpoints.get(item.id) ?? null;
|
|
1032
1629
|
const handoff = state.handoffs.get(item.id) ?? null;
|
|
1033
1630
|
const rework = state.reworks.get(item.id) ?? null;
|
|
1631
|
+
const sourceDeliveries = state.sourceDeliveries.filter(row => row.workItemId === item.id);
|
|
1632
|
+
const latestSourceDeliveryFact = sourceDeliveries.at(-1) ?? null;
|
|
1633
|
+
const latestSourceDelivery = sourceDeliveryProjection(sourceDeliveries.at(-1) ?? null);
|
|
1034
1634
|
const currentRework = rework?.assignmentId === assignment?.assignmentId ? rework : null;
|
|
1035
|
-
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
|
+
}
|
|
1036
1649
|
const activeHandoff = handoff
|
|
1037
1650
|
&& handoff.assignmentId === assignment?.assignmentId
|
|
1038
1651
|
&& handoff.fromAgentId === assignment?.agentId
|
|
1039
1652
|
? handoff
|
|
1040
1653
|
: null;
|
|
1041
|
-
const
|
|
1654
|
+
const latestHandoffFact = latestEventFact(activeHandoff, latestSourceDeliveryFact);
|
|
1655
|
+
const handoffSupersededByDelivery = activeHandoff
|
|
1656
|
+
&& latestFact?.eventId === activeHandoff.eventId
|
|
1657
|
+
&& latestHandoffFact?.eventId !== activeHandoff.eventId;
|
|
1658
|
+
const projectedFact = handoffSupersededByDelivery
|
|
1659
|
+
? [effectiveCheckpoint, currentRework, currentReopen, currentTransfer]
|
|
1660
|
+
.reduce((latest, fact) => latestEventFact(latest, fact), null)
|
|
1661
|
+
: latestFact;
|
|
1662
|
+
const pendingHandoff = activeHandoff
|
|
1663
|
+
&& latestFact?.eventId === activeHandoff.eventId
|
|
1664
|
+
&& latestHandoffFact?.eventId === activeHandoff.eventId
|
|
1042
1665
|
? pendingHandoffProjection(activeHandoff)
|
|
1043
1666
|
: null;
|
|
1044
1667
|
const executionTruth = executionTruthFor(workspaceRoot, assignment);
|
|
@@ -1046,7 +1669,11 @@ function projectStatus(definition, records, workspaceRoot = null) {
|
|
|
1046
1669
|
let status = "planned";
|
|
1047
1670
|
if (assignment) status = unresolvedDependencies.length > 0 ? "waiting_dependency" : "active";
|
|
1048
1671
|
if (pendingHandoff) status = "planned";
|
|
1049
|
-
if (
|
|
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";
|
|
1050
1677
|
if (executionTruth && (executionTruth.status !== "bound"
|
|
1051
1678
|
|| (executionTruth.lifecycle === "closed"
|
|
1052
1679
|
&& executionTruth.closeoutDecision !== "accepted"))) {
|
|
@@ -1056,7 +1683,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
|
|
|
1056
1683
|
? {
|
|
1057
1684
|
kind: "measured_units",
|
|
1058
1685
|
unit: item.measurable.unit,
|
|
1059
|
-
completed:
|
|
1686
|
+
completed: effectiveCheckpoint?.completedUnits ?? 0,
|
|
1060
1687
|
total: item.measurable.total,
|
|
1061
1688
|
}
|
|
1062
1689
|
: { kind: "state_only" };
|
|
@@ -1075,6 +1702,9 @@ function projectStatus(definition, records, workspaceRoot = null) {
|
|
|
1075
1702
|
...(checkpoint ? [checkpoint.ref] : []),
|
|
1076
1703
|
...(handoff ? [handoff.ref] : []),
|
|
1077
1704
|
...(rework ? [rework.ref] : []),
|
|
1705
|
+
...reopenFacts.map(row => row.ref),
|
|
1706
|
+
...(currentTransfer ? [currentTransfer.ref] : []),
|
|
1707
|
+
...sourceDeliveries.map(row => row.ref),
|
|
1078
1708
|
];
|
|
1079
1709
|
const projectedRework = reworkProjection(
|
|
1080
1710
|
currentRework,
|
|
@@ -1082,9 +1712,13 @@ function projectStatus(definition, records, workspaceRoot = null) {
|
|
|
1082
1712
|
? assignmentEvents.findIndex(row => row.assignmentId === currentRework.assignmentId) + 1
|
|
1083
1713
|
: null,
|
|
1084
1714
|
);
|
|
1085
|
-
const latestSummary =
|
|
1086
|
-
?
|
|
1087
|
-
:
|
|
1715
|
+
const latestSummary = ["rework_returned", "upstream_reopened"].includes(projectedFact?.type)
|
|
1716
|
+
? projectedFact.reason
|
|
1717
|
+
: projectedFact?.summary ?? null;
|
|
1718
|
+
const checkpointLikeFact = ["checkpoint_recorded", "responsibility_transferred"]
|
|
1719
|
+
.includes(projectedFact?.type)
|
|
1720
|
+
? projectedFact
|
|
1721
|
+
: null;
|
|
1088
1722
|
return {
|
|
1089
1723
|
workItemId: item.id,
|
|
1090
1724
|
title: item.title,
|
|
@@ -1098,19 +1732,23 @@ function projectStatus(definition, records, workspaceRoot = null) {
|
|
|
1098
1732
|
executionTruth,
|
|
1099
1733
|
dependencies: [...item.dependencies],
|
|
1100
1734
|
unresolvedDependencies,
|
|
1101
|
-
blockers:
|
|
1102
|
-
?
|
|
1735
|
+
blockers: checkpointLikeFact
|
|
1736
|
+
? checkpointLikeFact.blockerRefs
|
|
1103
1737
|
: unresolvedDependencies.map(id => `work-item:${id}`),
|
|
1104
|
-
decisionRefs:
|
|
1738
|
+
decisionRefs: checkpointLikeFact ? checkpointLikeFact.decisionRefs : [],
|
|
1105
1739
|
progress: measured,
|
|
1106
1740
|
summary: latestSummary,
|
|
1107
|
-
nextAction:
|
|
1741
|
+
nextAction: projectedFact?.nextAction ?? null,
|
|
1108
1742
|
pendingHandoff,
|
|
1109
1743
|
rework: projectedRework,
|
|
1744
|
+
latestSourceDelivery,
|
|
1110
1745
|
lastCheckpointAt: checkpoint?.occurredAt ?? null,
|
|
1111
|
-
evidenceRefs:
|
|
1746
|
+
evidenceRefs: projectedFact?.evidenceRefs ?? [],
|
|
1112
1747
|
assignmentHistory: history,
|
|
1113
1748
|
truthRefs,
|
|
1749
|
+
...(reopenFacts.length === 0
|
|
1750
|
+
? {}
|
|
1751
|
+
: { invalidatedByReopenIds: invalidatingReopens.map(row => row.reopenId) }),
|
|
1114
1752
|
};
|
|
1115
1753
|
});
|
|
1116
1754
|
const openDecisions = [...state.decisions.values()]
|
|
@@ -1311,6 +1949,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
|
|
|
1311
1949
|
truthRefs: [row.ref],
|
|
1312
1950
|
};
|
|
1313
1951
|
});
|
|
1952
|
+
const sourceDeliveries = state.sourceDeliveries.map(sourceDeliveryProjection);
|
|
1314
1953
|
let overall = "planned";
|
|
1315
1954
|
if (workItems.every(row => row.status === "completed")
|
|
1316
1955
|
&& integrationGates.every(row => row.status === "passed")
|
|
@@ -1318,7 +1957,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
|
|
|
1318
1957
|
else if (openDecisions.length > 0 || workItems.some(row => row.status === "failed")) overall = "active_with_blockers";
|
|
1319
1958
|
else if (workItems.some(row => row.status !== "planned" || row.pendingHandoff !== null)) overall = "active";
|
|
1320
1959
|
const status = {
|
|
1321
|
-
schemaVersion:
|
|
1960
|
+
schemaVersion: STATUS_SCHEMA_V2,
|
|
1322
1961
|
status: "team_project_status",
|
|
1323
1962
|
projectId: definition.projectId,
|
|
1324
1963
|
objective: definition.objective,
|
|
@@ -1336,6 +1975,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
|
|
|
1336
1975
|
openDeferredVerificationIds,
|
|
1337
1976
|
integrationGates,
|
|
1338
1977
|
dataCandidates,
|
|
1978
|
+
sourceDeliveries,
|
|
1339
1979
|
projectTruthHash: sha256(canonical({ definition, events: records.map(row => row.event) })),
|
|
1340
1980
|
authorizationGranted: false,
|
|
1341
1981
|
};
|
|
@@ -1474,6 +2114,101 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
|
|
|
1474
2114
|
);
|
|
1475
2115
|
}
|
|
1476
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
|
+
}
|
|
1477
2212
|
} else if (normalized.type === "decision_opened") {
|
|
1478
2213
|
if (state.decisions.has(normalized.decisionId)) throw new Error(`Decision ${normalized.decisionId} already exists.`);
|
|
1479
2214
|
} else if (normalized.type === "decision_resolved") {
|
|
@@ -1513,6 +2248,18 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
|
|
|
1513
2248
|
if (state.dataCandidates.some(row => row.candidateId === normalized.candidateId)) {
|
|
1514
2249
|
throw new Error(`Data candidate ${normalized.candidateId} already exists.`);
|
|
1515
2250
|
}
|
|
2251
|
+
} else if (normalized.type === "source_delivery_imported") {
|
|
2252
|
+
if (state.sourceDeliveries.some(row => row.deliveryId === normalized.deliveryId)) {
|
|
2253
|
+
throw new Error(`External source delivery ${normalized.deliveryId} already exists.`);
|
|
2254
|
+
}
|
|
2255
|
+
const current = state.assignments.get(normalized.workItemId) ?? null;
|
|
2256
|
+
if (current?.assignmentId !== normalized.assignmentId) {
|
|
2257
|
+
throw new Error("External source delivery must bind the current assignment.");
|
|
2258
|
+
}
|
|
2259
|
+
const projected = status.workItems.find(row => row.workItemId === normalized.workItemId);
|
|
2260
|
+
if (["completed", "failed"].includes(projected?.status)) {
|
|
2261
|
+
throw new Error("External source delivery requires a non-terminal WorkItem.");
|
|
2262
|
+
}
|
|
1516
2263
|
}
|
|
1517
2264
|
return normalized;
|
|
1518
2265
|
}
|
|
@@ -1849,6 +2596,205 @@ export function rejectAndReturnTeamProjectV1({
|
|
|
1849
2596
|
});
|
|
1850
2597
|
}
|
|
1851
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
|
+
|
|
1852
2798
|
export function openTeamProjectDecisionV1({
|
|
1853
2799
|
workspaceRoot,
|
|
1854
2800
|
decisionId,
|
|
@@ -2155,7 +3101,32 @@ export function checkpointTeamProjectV1({
|
|
|
2155
3101
|
};
|
|
2156
3102
|
}
|
|
2157
3103
|
|
|
3104
|
+
function legacyWorkItemProjection(workItem) {
|
|
3105
|
+
if (workItem === null) return null;
|
|
3106
|
+
const {
|
|
3107
|
+
latestSourceDelivery: _latestSourceDelivery,
|
|
3108
|
+
invalidatedByReopenIds: _invalidatedByReopenIds,
|
|
3109
|
+
...legacy
|
|
3110
|
+
} = workItem;
|
|
3111
|
+
return legacy;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
2158
3114
|
export function readTeamProjectStatusV1({ workspaceRoot }) {
|
|
3115
|
+
const loaded = loadProject(workspaceRoot);
|
|
3116
|
+
const { sourceDeliveries: _sourceDeliveries, ...legacy } = projectStatus(
|
|
3117
|
+
loaded.definition,
|
|
3118
|
+
loaded.events,
|
|
3119
|
+
loaded.root,
|
|
3120
|
+
);
|
|
3121
|
+
return {
|
|
3122
|
+
...legacy,
|
|
3123
|
+
schemaVersion: STATUS_SCHEMA_V1,
|
|
3124
|
+
workItems: legacy.workItems.map(legacyWorkItemProjection),
|
|
3125
|
+
exitCode: 0,
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
export function readTeamProjectStatusV2({ workspaceRoot }) {
|
|
2159
3130
|
const loaded = loadProject(workspaceRoot);
|
|
2160
3131
|
return {
|
|
2161
3132
|
...projectStatus(loaded.definition, loaded.events, loaded.root),
|
|
@@ -2163,7 +3134,77 @@ export function readTeamProjectStatusV1({ workspaceRoot }) {
|
|
|
2163
3134
|
};
|
|
2164
3135
|
}
|
|
2165
3136
|
|
|
2166
|
-
export function
|
|
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
|
+
|
|
3207
|
+
export function buildTeamProjectTakeoverV2({ workspaceRoot, agentId }) {
|
|
2167
3208
|
safeIdentifier(agentId, "agentId");
|
|
2168
3209
|
const loaded = loadProject(workspaceRoot);
|
|
2169
3210
|
const status = projectStatus(loaded.definition, loaded.events, loaded.root);
|
|
@@ -2180,6 +3221,29 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
|
|
|
2180
3221
|
row.pendingHandoff?.toAgentId === agentId
|
|
2181
3222
|
))?.pendingHandoff ?? null;
|
|
2182
3223
|
const pendingHandoff = pendingForCurrent ?? pendingForAgent;
|
|
3224
|
+
const staleHandoffs = staleHandoffsForTakeover({
|
|
3225
|
+
loaded,
|
|
3226
|
+
status,
|
|
3227
|
+
agentId,
|
|
3228
|
+
responsibilities,
|
|
3229
|
+
});
|
|
3230
|
+
const latestStaleHandoff = [...staleHandoffs]
|
|
3231
|
+
.sort(compareEventFacts)
|
|
3232
|
+
.at(-1) ?? null;
|
|
3233
|
+
const focusResponsibility = current
|
|
3234
|
+
?? (pendingHandoff
|
|
3235
|
+
? status.workItems.find(row => row.workItemId === pendingHandoff.workItemId) ?? null
|
|
3236
|
+
: null)
|
|
3237
|
+
?? (latestStaleHandoff
|
|
3238
|
+
? status.workItems.find(row => row.workItemId === latestStaleHandoff.workItemId) ?? null
|
|
3239
|
+
: null);
|
|
3240
|
+
const focusedDeliveries = focusResponsibility
|
|
3241
|
+
? status.sourceDeliveries.filter(row => (
|
|
3242
|
+
row.workItemId === focusResponsibility.workItemId
|
|
3243
|
+
))
|
|
3244
|
+
: [];
|
|
3245
|
+
const currentSourceDelivery = focusedDeliveries.at(-1) ?? null;
|
|
3246
|
+
const deliveryDelta = sourceDeliveryDelta(focusedDeliveries);
|
|
2183
3247
|
const checkpoints = loaded.events
|
|
2184
3248
|
.filter(row => row.event.type === "checkpoint_recorded"
|
|
2185
3249
|
&& responsibilities.some(item => item.workItemId === row.event.workItemId))
|
|
@@ -2189,6 +3253,9 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
|
|
|
2189
3253
|
...(lastCheckpoint?.evidenceRefs ?? []),
|
|
2190
3254
|
...(current?.evidenceRefs ?? []),
|
|
2191
3255
|
...(pendingHandoff?.evidenceRefs ?? []),
|
|
3256
|
+
...(currentSourceDelivery ? [currentSourceDelivery.truthRef] : []),
|
|
3257
|
+
...(deliveryDelta?.previousDeliveryId ? [focusedDeliveries.at(-2).truthRef] : []),
|
|
3258
|
+
...staleHandoffs.map(row => row.truthRef),
|
|
2192
3259
|
])].sort();
|
|
2193
3260
|
const unresolvedDecisionsById = new Map();
|
|
2194
3261
|
for (const decision of status.openDecisions) {
|
|
@@ -2230,7 +3297,7 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
|
|
|
2230
3297
|
: current ? null : status.readyQueue[0] ? `Take ${status.readyQueue[0]}.` : null;
|
|
2231
3298
|
}
|
|
2232
3299
|
return {
|
|
2233
|
-
schemaVersion: "
|
|
3300
|
+
schemaVersion: "OwlCodaRunKitTeamProjectTakeoverV2",
|
|
2234
3301
|
status: "team_project_takeover",
|
|
2235
3302
|
projectId: status.projectId,
|
|
2236
3303
|
objective: status.objective,
|
|
@@ -2243,6 +3310,9 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
|
|
|
2243
3310
|
currentResponsibility: current,
|
|
2244
3311
|
allResponsibilities: responsibilities,
|
|
2245
3312
|
pendingHandoff,
|
|
3313
|
+
staleHandoffs,
|
|
3314
|
+
currentSourceDelivery,
|
|
3315
|
+
sourceDeliveryDelta: deliveryDelta,
|
|
2246
3316
|
lastAcceptedCheckpoint: lastCheckpoint
|
|
2247
3317
|
? {
|
|
2248
3318
|
workItemId: lastCheckpoint.workItemId,
|
|
@@ -2265,6 +3335,27 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
|
|
|
2265
3335
|
};
|
|
2266
3336
|
}
|
|
2267
3337
|
|
|
3338
|
+
export function buildTeamProjectTakeoverV1(input) {
|
|
3339
|
+
const {
|
|
3340
|
+
staleHandoffs: _staleHandoffs,
|
|
3341
|
+
currentSourceDelivery: _currentSourceDelivery,
|
|
3342
|
+
sourceDeliveryDelta: _sourceDeliveryDelta,
|
|
3343
|
+
...legacy
|
|
3344
|
+
} = buildTeamProjectTakeoverV2(input);
|
|
3345
|
+
const v2OnlyEvidenceRefs = new Set([
|
|
3346
|
+
..._staleHandoffs.map(row => row.truthRef),
|
|
3347
|
+
...readTeamProjectStatusV2({ workspaceRoot: input.workspaceRoot })
|
|
3348
|
+
.sourceDeliveries.map(row => row.truthRef),
|
|
3349
|
+
]);
|
|
3350
|
+
return {
|
|
3351
|
+
...legacy,
|
|
3352
|
+
schemaVersion: "OwlCodaRunKitTeamProjectTakeoverV1",
|
|
3353
|
+
currentResponsibility: legacyWorkItemProjection(legacy.currentResponsibility),
|
|
3354
|
+
allResponsibilities: legacy.allResponsibilities.map(legacyWorkItemProjection),
|
|
3355
|
+
evidenceRefs: legacy.evidenceRefs.filter(ref => !v2OnlyEvidenceRefs.has(ref)),
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
|
|
2268
3359
|
export function formatTeamProjectStatusHumanV1(status) {
|
|
2269
3360
|
const nextAction = status.nextAction === null ? "none" : singleLine(status.nextAction);
|
|
2270
3361
|
return [
|
|
@@ -2285,6 +3376,65 @@ export function formatTeamProjectStatusHumanV1(status) {
|
|
|
2285
3376
|
].join("\n");
|
|
2286
3377
|
}
|
|
2287
3378
|
|
|
3379
|
+
export function formatTeamProjectStatusHumanV2(status) {
|
|
3380
|
+
const nextAction = status.nextAction === null ? "none" : singleLine(status.nextAction);
|
|
3381
|
+
const latestSourceDelivery = status.sourceDeliveries.at(-1) ?? null;
|
|
3382
|
+
return [
|
|
3383
|
+
status.headline,
|
|
3384
|
+
`Project: ${status.projectId} — ${singleLine(status.objective)}`,
|
|
3385
|
+
`State: ${status.overall}`,
|
|
3386
|
+
`Work: ${status.counts.completed}/${status.counts.total} completed; ${status.counts.active} active; ${status.counts.waiting_dependency + status.counts.waiting_decision} waiting`,
|
|
3387
|
+
`Agents: ${status.agents.map(agent => agent.agentId).join(", ") || "none assigned"}`,
|
|
3388
|
+
`Open decisions: ${status.openDecisions.map(row => row.decisionId).join(", ") || "none"}`,
|
|
3389
|
+
`Deferred verification: ${status.openDeferredVerificationIds.length} open${
|
|
3390
|
+
status.openDeferredVerificationIds.length > 0
|
|
3391
|
+
? ` (${status.openDeferredVerificationIds.join(", ")})`
|
|
3392
|
+
: ""
|
|
3393
|
+
}`,
|
|
3394
|
+
`Ready queue: ${status.readyQueue.join(", ") || "none"}`,
|
|
3395
|
+
`Source deliveries: ${status.sourceDeliveries.length}${latestSourceDelivery
|
|
3396
|
+
? `; latest ${latestSourceDelivery.deliveryId} (${latestSourceDelivery.overlayStatus}, ${latestSourceDelivery.deliveryCandidateFingerprint})`
|
|
3397
|
+
: ""}`,
|
|
3398
|
+
`Next: ${nextAction}${status.nextActorId ? ` (owner ${status.nextActorId})` : ""}`,
|
|
3399
|
+
"",
|
|
3400
|
+
].join("\n");
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
export function formatTeamProjectTakeoverHumanV2(takeover) {
|
|
3404
|
+
const responsibility = takeover.currentResponsibility;
|
|
3405
|
+
const agentNextAction = takeover.nextAction === null
|
|
3406
|
+
? "none"
|
|
3407
|
+
: singleLine(takeover.nextAction);
|
|
3408
|
+
const projectNextAction = takeover.projectNextAction === null
|
|
3409
|
+
? "none"
|
|
3410
|
+
: singleLine(takeover.projectNextAction);
|
|
3411
|
+
return [
|
|
3412
|
+
takeover.projectHeadline,
|
|
3413
|
+
`Project: ${takeover.projectId} — ${singleLine(takeover.objective)}`,
|
|
3414
|
+
`Agent: ${takeover.agentId}`,
|
|
3415
|
+
`Responsibility: ${responsibility?.workItemId ?? "none"}`,
|
|
3416
|
+
`Open decisions: ${takeover.unresolvedDecisions.map(row => row.decisionId).join(", ") || "none"}`,
|
|
3417
|
+
`Deferred verification: ${takeover.deferredVerifications
|
|
3418
|
+
.map(row => row.verificationId).join(", ") || "none"}`,
|
|
3419
|
+
`Handoffs: pending ${takeover.pendingHandoff
|
|
3420
|
+
? `${takeover.pendingHandoff.fromAgentId} -> ${takeover.pendingHandoff.toAgentId} for ${takeover.pendingHandoff.workItemId}`
|
|
3421
|
+
: "none"}; stale ${takeover.staleHandoffs.length > 0
|
|
3422
|
+
? takeover.staleHandoffs.map(row => `${row.eventId} (${row.reason})`).join(", ")
|
|
3423
|
+
: "none"}`,
|
|
3424
|
+
`Candidate: ${takeover.currentSourceDelivery
|
|
3425
|
+
? `${takeover.currentSourceDelivery.deliveryId} (${takeover.currentSourceDelivery.overlayStatus}, ${takeover.currentSourceDelivery.deliveryCandidateFingerprint})`
|
|
3426
|
+
: "none"}; delta ${takeover.sourceDeliveryDelta === null
|
|
3427
|
+
? "none"
|
|
3428
|
+
: takeover.sourceDeliveryDelta.previousDeliveryId === null
|
|
3429
|
+
? `first delivery; added ${takeover.sourceDeliveryDelta.addedPaths.length}`
|
|
3430
|
+
: `vs ${takeover.sourceDeliveryDelta.previousDeliveryId}; added ${takeover.sourceDeliveryDelta.addedPaths.length}, changed ${takeover.sourceDeliveryDelta.changedPaths.length}, removed ${takeover.sourceDeliveryDelta.removedPaths.length}, unchanged ${takeover.sourceDeliveryDelta.unchangedPaths.length}`}`,
|
|
3431
|
+
`Agent next: ${agentNextAction}`,
|
|
3432
|
+
`Project next: ${projectNextAction}${takeover.projectNextActorId ? ` (owner ${takeover.projectNextActorId})` : ""}`,
|
|
3433
|
+
`State: ${takeover.overall}`,
|
|
3434
|
+
"",
|
|
3435
|
+
].join("\n");
|
|
3436
|
+
}
|
|
3437
|
+
|
|
2288
3438
|
export function formatTeamProjectTakeoverHumanV1(takeover) {
|
|
2289
3439
|
const responsibility = takeover.currentResponsibility;
|
|
2290
3440
|
const agentNextAction = takeover.nextAction === null
|