owlrunkit 0.18.2 → 0.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +159 -52
  3. package/docs/architecture/OWLCODA_RUNKIT_0184_EXTERNAL_DELIVERY_INTAKE_PRODUCT_DECISION_20260812.md +209 -0
  4. package/docs/architecture/OWLCODA_RUNKIT_018_PROJECT_DRIVER_PRODUCT_DEFINITION.md +16 -5
  5. package/docs/architecture/OWLCODA_RUNKIT_USAGE_MODE_SELECTION_PRODUCT_DECISION_20260811.md +54 -3
  6. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +7 -0
  7. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +125 -74
  8. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  9. package/integrations/codex/skills/owlcoda-runkit/references/contract-v0.1.md +160 -0
  10. package/integrations/codex/skills/owlcoda-runkit/references/contract-v0.2.md +545 -0
  11. package/package.json +1 -1
  12. package/packages/attest/README.md +4 -3
  13. package/packages/attest/consumer-surface-v1.json +1 -0
  14. package/packages/attest/distribution-manifest-v1.json +12 -1
  15. package/packages/attest/schemas/quick-verification-receipt-v2.schema.json +232 -0
  16. package/packages/attest/src/index.mjs +4 -1
  17. package/packages/attest/src/quick-receipt-contract.mjs +16 -6
  18. package/packages/attest/src/quick.mjs +21 -2
  19. package/packages/attest/src/workspace-snapshot.mjs +10 -1
  20. package/registry-release-policy-v1.json +5 -5
  21. package/sbom.spdx.json +5 -5
  22. package/scripts/runkit-contract/assurance-router.mjs +25 -6
  23. package/scripts/runkit-contract/core-contract.mjs +10 -1
  24. package/scripts/runkit-contract/onboarding-doctor.mjs +1 -1
  25. package/scripts/runkit-contract/profile-onboarding.mjs +263 -15
  26. package/scripts/runkit-contract/project-cli-resolver.mjs +6 -0
  27. package/scripts/runkit-contract/project-external-delivery.mjs +707 -0
  28. package/scripts/runkit-contract/quick-metrics.mjs +3 -3
  29. package/scripts/runkit-contract/quick-receipt.mjs +79 -12
  30. package/scripts/runkit-contract/quick-verify.mjs +89 -12
  31. package/scripts/runkit-contract/runkit-bootstrap.mjs +6 -1
  32. package/scripts/runkit-contract/runkit-cli.mjs +363 -143
  33. package/scripts/runkit-contract/schemas/assurance-route-v2.schema.json +26 -0
  34. package/scripts/runkit-contract/schemas/profile-detection-v3.schema.json +170 -0
  35. package/scripts/runkit-contract/schemas/team-project-event-v2.schema.json +128 -0
  36. package/scripts/runkit-contract/schemas/team-project-event-v3.schema.json +146 -0
  37. package/scripts/runkit-contract/schemas/team-project-status-v2.schema.json +85 -0
  38. package/scripts/runkit-contract/schemas/usage-mode-recommendation-v2.schema.json +136 -0
  39. package/scripts/runkit-contract/team-project.mjs +782 -55
  40. package/scripts/runkit-contract/usage-mode.mjs +28 -7
@@ -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,
@@ -23,7 +28,10 @@ import {
23
28
  const PROJECT_ROOT = ".owlcoda/runkit/project";
24
29
  const DEFINITION_SCHEMA = "OwlCodaRunKitTeamProjectDefinitionV1";
25
30
  const EVENT_SCHEMA = "OwlCodaRunKitTeamProjectEventV1";
26
- const STATUS_SCHEMA = "OwlCodaRunKitTeamProjectStatusV1";
31
+ const EVENT_SCHEMA_V2 = "OwlCodaRunKitTeamProjectEventV2";
32
+ const EVENT_SCHEMA_V3 = "OwlCodaRunKitTeamProjectEventV3";
33
+ const STATUS_SCHEMA_V1 = "OwlCodaRunKitTeamProjectStatusV1";
34
+ const STATUS_SCHEMA_V2 = "OwlCodaRunKitTeamProjectStatusV2";
27
35
 
28
36
  const DEFINITION_KEYS = [
29
37
  "schemaVersion",
@@ -60,6 +68,12 @@ const EVENT_KEYS = Object.freeze({
60
68
  "workItemId", "fromAgentId", "toAgentId", "summary", "evidenceRefs",
61
69
  "nextAction",
62
70
  ],
71
+ rework_returned: [
72
+ "schemaVersion", "eventId", "type", "occurredAt", "reworkId",
73
+ "assignmentId", "failedAssignmentId", "failedCheckpointEventId",
74
+ "workItemId", "fromAgentId", "reviewerAgentId", "toAgentId", "reason",
75
+ "evidenceRefs", "nextAction",
76
+ ],
63
77
  decision_opened: [
64
78
  "schemaVersion", "eventId", "type", "occurredAt", "decisionId",
65
79
  "title", "question", "ownerAgentId", "blockingWorkItemIds", "options",
@@ -85,6 +99,17 @@ const EVENT_KEYS = Object.freeze({
85
99
  "sourceRef", "rights", "inputRef", "outputRef", "decisionRef",
86
100
  "verificationRefs", "outcomeRef", "version",
87
101
  ],
102
+ source_delivery_imported: [
103
+ "schemaVersion", "eventId", "type", "occurredAt", "deliveryId",
104
+ "projectId", "projectDefinitionSha256", "assignmentId", "workItemId",
105
+ "producerId", "targetWorkspaceRef",
106
+ "targetHead", "entrySnapshotRef", "entrySnapshotSha256",
107
+ "deliveryPacketSha256", "deliveryCandidateFingerprint",
108
+ "workspaceSnapshotFingerprint", "overlayStatus", "candidateFiles",
109
+ "changedPaths", "toolCachePaths", "dependencyEnvironmentPaths",
110
+ "unchangedPacketPaths", "outOfScopeSourcePaths", "targetSnapshot",
111
+ "proven", "notProven", "targetWritePerformed", "authorizationGranted",
112
+ ],
88
113
  });
89
114
 
90
115
  function canonical(value) {
@@ -115,6 +140,26 @@ function nullableString(value, label) {
115
140
  return nonEmptyString(value, label);
116
141
  }
117
142
 
143
+ function sha256Ref(value, label) {
144
+ if (typeof value !== "string" || !/^sha256:[a-f0-9]{64}$/u.test(value)) {
145
+ throw new Error(`${label} must be a canonical SHA-256 reference.`);
146
+ }
147
+ return value;
148
+ }
149
+
150
+ function safeSourcePath(value, label) {
151
+ if (typeof value !== "string"
152
+ || value.length === 0
153
+ || path.isAbsolute(value)
154
+ || path.win32.parse(value).root
155
+ || value.includes("\\")
156
+ || value.includes("\0")
157
+ || value.split("/").some(segment => !segment || segment === "." || segment === "..")) {
158
+ throw new Error(`${label} must be a safe repository-relative path.`);
159
+ }
160
+ return value;
161
+ }
162
+
118
163
  function stringArray(value, label, { allowEmpty = true } = {}) {
119
164
  if (!Array.isArray(value)
120
165
  || (!allowEmpty && value.length === 0)
@@ -160,6 +205,30 @@ function withProjectLock(root, operation) {
160
205
  }
161
206
  }
162
207
 
208
+ export function teamProjectDefinitionBindingV1(definition) {
209
+ const normalized = validateDefinition(definition);
210
+ return {
211
+ projectId: normalized.projectId,
212
+ projectDefinitionSha256: `sha256:${sha256(canonical(normalized))}`,
213
+ };
214
+ }
215
+
216
+ export function withTeamProjectControlLockV1({ workspaceRoot, operation }) {
217
+ if (typeof operation !== "function") {
218
+ throw new Error("Team project control operation must be a function.");
219
+ }
220
+ const loaded = loadProject(workspaceRoot);
221
+ return withProjectLock(loaded.projectRoot, () => {
222
+ const refreshed = loadProject(workspaceRoot);
223
+ return operation({
224
+ workspaceRoot: refreshed.root,
225
+ projectRoot: refreshed.projectRoot,
226
+ definition: structuredClone(refreshed.definition),
227
+ definitionBinding: teamProjectDefinitionBindingV1(refreshed.definition),
228
+ });
229
+ });
230
+ }
231
+
163
232
  function uniqueIds(rows, label) {
164
233
  const ids = rows.map(row => safeIdentifier(row.id, `${label} id`));
165
234
  if (new Set(ids).size !== ids.length) throw new Error(`${label} ids must be unique.`);
@@ -391,6 +460,157 @@ function pendingHandoffProjection(handoff) {
391
460
  : null;
392
461
  }
393
462
 
463
+ function reworkProjection(rework, attempt) {
464
+ return rework
465
+ ? {
466
+ eventId: rework.eventId,
467
+ truthRef: rework.ref,
468
+ reworkId: rework.reworkId,
469
+ attempt,
470
+ failedCheckpointEventId: rework.failedCheckpointEventId,
471
+ failedTruthRef: `project/events/${rework.failedCheckpointEventId}.json`,
472
+ previousAssignmentId: rework.failedAssignmentId,
473
+ previousAgentId: rework.fromAgentId,
474
+ reviewerAgentId: rework.reviewerAgentId,
475
+ assignmentId: rework.assignmentId,
476
+ agentId: rework.toAgentId,
477
+ reason: rework.reason,
478
+ evidenceRefs: [...rework.evidenceRefs],
479
+ nextAction: rework.nextAction,
480
+ occurredAt: rework.occurredAt,
481
+ }
482
+ : null;
483
+ }
484
+
485
+ function sourceDeliveryProjection(delivery) {
486
+ return delivery
487
+ ? {
488
+ projectId: delivery.projectId,
489
+ projectDefinitionSha256: delivery.projectDefinitionSha256,
490
+ deliveryId: delivery.deliveryId,
491
+ workItemId: delivery.workItemId,
492
+ assignmentId: delivery.assignmentId,
493
+ producerId: delivery.producerId,
494
+ targetWorkspaceRef: delivery.targetWorkspaceRef,
495
+ targetHead: delivery.targetHead,
496
+ entrySnapshotRef: delivery.entrySnapshotRef,
497
+ entrySnapshotSha256: delivery.entrySnapshotSha256,
498
+ deliveryPacketSha256: delivery.deliveryPacketSha256,
499
+ deliveryCandidateFingerprint: delivery.deliveryCandidateFingerprint,
500
+ workspaceSnapshotFingerprint: delivery.workspaceSnapshotFingerprint,
501
+ overlayStatus: delivery.overlayStatus,
502
+ candidateFiles: delivery.candidateFiles.map(row => ({ ...row })),
503
+ candidatePaths: delivery.candidateFiles.map(row => row.path),
504
+ changedPaths: [...delivery.changedPaths],
505
+ toolCachePaths: [...delivery.toolCachePaths],
506
+ dependencyEnvironmentPaths: [...delivery.dependencyEnvironmentPaths],
507
+ unchangedPacketPaths: [...delivery.unchangedPacketPaths],
508
+ proven: [...delivery.proven],
509
+ notProven: [...delivery.notProven],
510
+ occurredAt: delivery.occurredAt,
511
+ truthRef: delivery.ref,
512
+ targetWritePerformed: false,
513
+ authorizationGranted: false,
514
+ }
515
+ : null;
516
+ }
517
+
518
+ function sourceDeliveryDelta(deliveries) {
519
+ const current = deliveries.at(-1) ?? null;
520
+ if (!current) return null;
521
+ const previous = deliveries.at(-2) ?? null;
522
+ if (!previous) {
523
+ return {
524
+ status: "first_delivery",
525
+ currentDeliveryId: current.deliveryId,
526
+ previousDeliveryId: null,
527
+ addedPaths: current.candidateFiles.map(row => row.path),
528
+ changedPaths: [],
529
+ removedPaths: [],
530
+ unchangedPaths: [],
531
+ };
532
+ }
533
+ const currentFiles = new Map(current.candidateFiles.map(row => [row.path, row.sha256]));
534
+ const previousFiles = new Map(previous.candidateFiles.map(row => [row.path, row.sha256]));
535
+ const allPaths = uniqueSorted([...currentFiles.keys(), ...previousFiles.keys()]);
536
+ const addedPaths = allPaths.filter(filePath => (
537
+ currentFiles.has(filePath) && !previousFiles.has(filePath)
538
+ ));
539
+ const removedPaths = allPaths.filter(filePath => (
540
+ !currentFiles.has(filePath) && previousFiles.has(filePath)
541
+ ));
542
+ const changedPaths = allPaths.filter(filePath => (
543
+ currentFiles.has(filePath)
544
+ && previousFiles.has(filePath)
545
+ && currentFiles.get(filePath) !== previousFiles.get(filePath)
546
+ ));
547
+ const unchangedPaths = allPaths.filter(filePath => (
548
+ currentFiles.has(filePath)
549
+ && previousFiles.has(filePath)
550
+ && currentFiles.get(filePath) === previousFiles.get(filePath)
551
+ ));
552
+ return {
553
+ status: addedPaths.length || removedPaths.length || changedPaths.length
554
+ ? "changed"
555
+ : "unchanged",
556
+ currentDeliveryId: current.deliveryId,
557
+ previousDeliveryId: previous.deliveryId,
558
+ addedPaths,
559
+ changedPaths,
560
+ removedPaths,
561
+ unchangedPaths,
562
+ };
563
+ }
564
+
565
+ function staleHandoffsForTakeover({ loaded, status, agentId, responsibilities }) {
566
+ const responsibilityIds = new Set(responsibilities.map(row => row.workItemId));
567
+ const pendingEventIds = new Set(status.workItems
568
+ .map(row => row.pendingHandoff?.eventId)
569
+ .filter(Boolean));
570
+ return loaded.events
571
+ .filter(({ event }) => (
572
+ event.type === "handoff_recorded"
573
+ && !pendingEventIds.has(event.eventId)
574
+ && (responsibilityIds.has(event.workItemId)
575
+ || event.fromAgentId === agentId
576
+ || event.toAgentId === agentId)
577
+ ))
578
+ .map(({ event, ref }) => {
579
+ const laterFacts = loaded.events.filter(row => (
580
+ row.event.workItemId === event.workItemId
581
+ && compareEventFacts(row.event, event) > 0
582
+ && [
583
+ "agent_assigned",
584
+ "checkpoint_recorded",
585
+ "handoff_recorded",
586
+ "rework_returned",
587
+ "source_delivery_imported",
588
+ ].includes(row.event.type)
589
+ ));
590
+ let reason = "superseded_by_current_truth";
591
+ if (laterFacts.some(row => row.event.type === "handoff_recorded")) {
592
+ reason = "redirected_by_later_handoff";
593
+ } else if (laterFacts.some(row => (
594
+ row.event.type === "agent_assigned" || row.event.type === "rework_returned"
595
+ ))) {
596
+ reason = "superseded_by_assignment";
597
+ } else if (laterFacts.length > 0) {
598
+ reason = "superseded_by_later_work_fact";
599
+ }
600
+ return {
601
+ eventId: event.eventId,
602
+ truthRef: ref,
603
+ assignmentId: event.assignmentId,
604
+ workItemId: event.workItemId,
605
+ fromAgentId: event.fromAgentId,
606
+ toAgentId: event.toAgentId,
607
+ occurredAt: event.occurredAt,
608
+ reason,
609
+ };
610
+ })
611
+ .sort((left, right) => compareIdentifiers(left.eventId, right.eventId));
612
+ }
613
+
394
614
  function validateTimestamp(value) {
395
615
  if (typeof value !== "string"
396
616
  || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
@@ -405,8 +625,19 @@ function validateEventShape(value, definition) {
405
625
  throw new Error("Team project event type is invalid.");
406
626
  }
407
627
  assertAllowedKeys(value, `Team project event ${value.eventId ?? "unknown"}`, EVENT_KEYS[value.type]);
408
- if (value.schemaVersion !== EVENT_SCHEMA) {
409
- throw new Error(`Team project event schemaVersion must be ${EVENT_SCHEMA}.`);
628
+ if (![EVENT_SCHEMA, EVENT_SCHEMA_V2, EVENT_SCHEMA_V3].includes(value.schemaVersion)) {
629
+ throw new Error(
630
+ `Team project event schemaVersion must be ${EVENT_SCHEMA}, ${EVENT_SCHEMA_V2}, or ${EVENT_SCHEMA_V3}.`,
631
+ );
632
+ }
633
+ if (value.type === "rework_returned" && value.schemaVersion !== EVENT_SCHEMA_V2) {
634
+ throw new Error(`rework_returned requires schemaVersion ${EVENT_SCHEMA_V2}.`);
635
+ }
636
+ if (value.type === "source_delivery_imported" && value.schemaVersion !== EVENT_SCHEMA_V3) {
637
+ throw new Error(`source_delivery_imported requires schemaVersion ${EVENT_SCHEMA_V3}.`);
638
+ }
639
+ if (value.schemaVersion === EVENT_SCHEMA_V3 && value.type !== "source_delivery_imported") {
640
+ throw new Error(`${EVENT_SCHEMA_V3} is reserved for source_delivery_imported events.`);
410
641
  }
411
642
  safeIdentifier(value.eventId, "eventId");
412
643
  validateTimestamp(value.occurredAt);
@@ -420,11 +651,19 @@ function validateEventShape(value, definition) {
420
651
  "agent_assigned",
421
652
  "checkpoint_recorded",
422
653
  "handoff_recorded",
654
+ "rework_returned",
655
+ "source_delivery_imported",
423
656
  "verification_deferred",
424
657
  ].includes(value.type)) {
425
658
  assertWorkItem(value.workItemId);
426
659
  }
427
- if (["agent_assigned", "checkpoint_recorded", "handoff_recorded"].includes(value.type)) {
660
+ if ([
661
+ "agent_assigned",
662
+ "checkpoint_recorded",
663
+ "handoff_recorded",
664
+ "rework_returned",
665
+ "source_delivery_imported",
666
+ ].includes(value.type)) {
428
667
  safeIdentifier(value.assignmentId, "assignmentId");
429
668
  }
430
669
  if (value.type === "agent_assigned") {
@@ -466,6 +705,16 @@ function validateEventShape(value, definition) {
466
705
  nonEmptyString(value.summary, "Handoff summary");
467
706
  stringArray(value.evidenceRefs, "Handoff evidenceRefs", { allowEmpty: false });
468
707
  nonEmptyString(value.nextAction, "Handoff nextAction");
708
+ } else if (value.type === "rework_returned") {
709
+ safeIdentifier(value.reworkId, "reworkId");
710
+ safeIdentifier(value.failedAssignmentId, "failedAssignmentId");
711
+ safeIdentifier(value.failedCheckpointEventId, "failedCheckpointEventId");
712
+ safeIdentifier(value.fromAgentId, "fromAgentId");
713
+ safeIdentifier(value.reviewerAgentId, "reviewerAgentId");
714
+ safeIdentifier(value.toAgentId, "toAgentId");
715
+ nonEmptyString(value.reason, "Rework reason");
716
+ stringArray(value.evidenceRefs, "Rework evidenceRefs", { allowEmpty: false });
717
+ nonEmptyString(value.nextAction, "Rework nextAction");
469
718
  } else if (value.type === "decision_opened") {
470
719
  safeIdentifier(value.decisionId, "decisionId");
471
720
  safeIdentifier(value.ownerAgentId, "ownerAgentId");
@@ -532,6 +781,89 @@ function validateEventShape(value, definition) {
532
781
  stringArray(value.verificationRefs, "Data candidate verificationRefs");
533
782
  nullableString(value.outcomeRef, "Data candidate outcomeRef");
534
783
  nonEmptyString(value.version, "Data candidate version");
784
+ } else if (value.type === "source_delivery_imported") {
785
+ safeIdentifier(value.projectId, "External delivery projectId");
786
+ sha256Ref(value.projectDefinitionSha256, "External delivery projectDefinitionSha256");
787
+ const definitionBinding = teamProjectDefinitionBindingV1(definition);
788
+ if (value.projectId !== definitionBinding.projectId
789
+ || value.projectDefinitionSha256 !== definitionBinding.projectDefinitionSha256) {
790
+ throw new Error("External delivery project identity does not match current project truth.");
791
+ }
792
+ safeIdentifier(value.deliveryId, "deliveryId");
793
+ safeIdentifier(value.producerId, "producerId");
794
+ nonEmptyString(value.targetWorkspaceRef, "External delivery targetWorkspaceRef");
795
+ if (!path.isAbsolute(value.targetWorkspaceRef)) {
796
+ throw new Error("External delivery targetWorkspaceRef must be absolute.");
797
+ }
798
+ if (typeof value.targetHead !== "string"
799
+ || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(value.targetHead)) {
800
+ throw new Error("External delivery targetHead is invalid.");
801
+ }
802
+ if ((value.entrySnapshotRef === null) !== (value.entrySnapshotSha256 === null)) {
803
+ throw new Error("External delivery entry snapshot ref and hash must be supplied together.");
804
+ }
805
+ if (value.entrySnapshotRef !== null) {
806
+ nonEmptyString(value.entrySnapshotRef, "External delivery entrySnapshotRef");
807
+ sha256Ref(value.entrySnapshotSha256, "External delivery entrySnapshotSha256");
808
+ }
809
+ sha256Ref(value.deliveryPacketSha256, "External delivery packet hash");
810
+ if (typeof value.deliveryCandidateFingerprint !== "string"
811
+ || !/^[a-f0-9]{64}$/u.test(value.deliveryCandidateFingerprint)) {
812
+ throw new Error("External delivery candidate fingerprint is invalid.");
813
+ }
814
+ sha256Ref(value.workspaceSnapshotFingerprint, "External delivery workspace snapshot fingerprint");
815
+ if (!["candidate_only", "attested", "attested_with_non_source_changes"].includes(value.overlayStatus)) {
816
+ throw new Error("External delivery overlayStatus is invalid.");
817
+ }
818
+ if (!Array.isArray(value.candidateFiles) || value.candidateFiles.length === 0) {
819
+ throw new Error("External delivery candidateFiles must contain at least one file.");
820
+ }
821
+ const candidateFiles = value.candidateFiles.map((row) => {
822
+ assertAllowedKeys(row, "External delivery candidate file", ["path", "sha256"]);
823
+ const filePath = safeSourcePath(row.path, "External delivery candidate file path");
824
+ if (typeof row.sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(row.sha256)) {
825
+ throw new Error(`External delivery candidate file hash is invalid: ${filePath}.`);
826
+ }
827
+ return { path: filePath, sha256: row.sha256 };
828
+ });
829
+ if (new Set(candidateFiles.map(row => row.path)).size !== candidateFiles.length
830
+ || canonical([...candidateFiles].sort((left, right) => (
831
+ compareIdentifiers(left.path, right.path)
832
+ )))
833
+ !== canonical(candidateFiles)) {
834
+ throw new Error("External delivery candidateFiles must be uniquely sorted by path.");
835
+ }
836
+ if (canonicalSourceFingerprint(Object.fromEntries(
837
+ candidateFiles.map(row => [row.path, row.sha256]),
838
+ )) !== value.deliveryCandidateFingerprint) {
839
+ throw new Error("External delivery candidate fingerprint does not match its file map.");
840
+ }
841
+ for (const [field, label] of [
842
+ ["changedPaths", "External delivery changedPaths"],
843
+ ["toolCachePaths", "External delivery toolCachePaths"],
844
+ ["dependencyEnvironmentPaths", "External delivery dependencyEnvironmentPaths"],
845
+ ["unchangedPacketPaths", "External delivery unchangedPacketPaths"],
846
+ ["outOfScopeSourcePaths", "External delivery outOfScopeSourcePaths"],
847
+ ]) {
848
+ const paths = stringArray(value[field], label);
849
+ for (const filePath of paths) safeSourcePath(filePath, label);
850
+ if (canonical([...paths].sort()) !== canonical(paths)) {
851
+ throw new Error(`${label} must be sorted.`);
852
+ }
853
+ }
854
+ if (value.outOfScopeSourcePaths.length !== 0) {
855
+ throw new Error("An imported external delivery cannot contain out-of-scope source paths.");
856
+ }
857
+ stringArray(value.proven, "External delivery proven statements", { allowEmpty: false });
858
+ stringArray(value.notProven, "External delivery notProven statements", { allowEmpty: false });
859
+ if (!validWorkspaceSnapshotShape(value.targetSnapshot)
860
+ || !quickSnapshotFingerprintValid(value.targetSnapshot)
861
+ || value.targetSnapshot.sourceFingerprint !== value.workspaceSnapshotFingerprint) {
862
+ throw new Error("External delivery targetSnapshot identity is invalid.");
863
+ }
864
+ if (value.targetWritePerformed !== false || value.authorizationGranted !== false) {
865
+ throw new Error("External delivery cannot grant authority or write the target workspace.");
866
+ }
535
867
  }
536
868
  return structuredClone(value);
537
869
  }
@@ -541,12 +873,16 @@ function projectEvents(definition, records) {
541
873
  const assignmentHistory = new Map(definition.workItems.map(row => [row.id, []]));
542
874
  const checkpoints = new Map();
543
875
  const handoffs = new Map();
876
+ const reworks = new Map();
544
877
  const decisions = new Map();
545
878
  const gatePasses = new Map();
546
879
  const verifications = new Map();
547
880
  const dataCandidates = [];
881
+ const sourceDeliveries = [];
548
882
  const seenAssignments = new Set();
883
+ const seenReworks = new Set();
549
884
  const seenDecisions = new Set();
885
+ const seenDeliveries = new Set();
550
886
 
551
887
  for (const { event, ref } of records) {
552
888
  validateEventShape(event, definition);
@@ -561,6 +897,38 @@ function projectEvents(definition, records) {
561
897
  checkpoints.set(event.workItemId, { ...event, ref });
562
898
  } else if (event.type === "handoff_recorded") {
563
899
  handoffs.set(event.workItemId, { ...event, ref });
900
+ } else if (event.type === "rework_returned") {
901
+ if (seenReworks.has(event.reworkId)) {
902
+ throw new Error(`Duplicate reworkId ${event.reworkId}.`);
903
+ }
904
+ if (seenAssignments.has(event.assignmentId)) {
905
+ throw new Error(`Duplicate assignmentId ${event.assignmentId}.`);
906
+ }
907
+ const current = assignments.get(event.workItemId) ?? null;
908
+ const failure = checkpoints.get(event.workItemId) ?? null;
909
+ const latestFact = latestEventFact(failure, handoffs.get(event.workItemId) ?? null);
910
+ if (!current
911
+ || current.assignmentId !== event.failedAssignmentId
912
+ || current.agentId !== event.fromAgentId
913
+ || failure?.eventId !== event.failedCheckpointEventId
914
+ || failure.assignmentId !== event.failedAssignmentId
915
+ || failure.state !== "failed"
916
+ || latestFact?.eventId !== failure.eventId) {
917
+ throw new Error(
918
+ `Rework ${event.reworkId} does not bind the current failed checkpoint and assignment.`,
919
+ );
920
+ }
921
+ seenReworks.add(event.reworkId);
922
+ seenAssignments.add(event.assignmentId);
923
+ const assignment = {
924
+ ...event,
925
+ agentId: event.toAgentId,
926
+ supersedesAssignmentId: event.failedAssignmentId,
927
+ ref,
928
+ };
929
+ assignments.set(event.workItemId, assignment);
930
+ assignmentHistory.get(event.workItemId).push(assignment);
931
+ reworks.set(event.workItemId, { ...event, ref });
564
932
  } else if (event.type === "decision_opened") {
565
933
  if (seenDecisions.has(event.decisionId)) {
566
934
  throw new Error(`Duplicate decision_opened event for ${event.decisionId}.`);
@@ -608,6 +976,25 @@ function projectEvents(definition, records) {
608
976
  throw new Error(`Duplicate data candidate ${event.candidateId}.`);
609
977
  }
610
978
  dataCandidates.push({ ...event, ref });
979
+ } else if (event.type === "source_delivery_imported") {
980
+ if (seenDeliveries.has(event.deliveryId)) {
981
+ throw new Error(`Duplicate external source delivery ${event.deliveryId}.`);
982
+ }
983
+ const current = assignments.get(event.workItemId) ?? null;
984
+ if (current?.assignmentId !== event.assignmentId) {
985
+ throw new Error(
986
+ `External source delivery ${event.deliveryId} does not bind the current assignment.`,
987
+ );
988
+ }
989
+ const currentCheckpoint = checkpoints.get(event.workItemId) ?? null;
990
+ if (currentCheckpoint?.assignmentId === current.assignmentId
991
+ && ["completed", "failed"].includes(currentCheckpoint.state)) {
992
+ throw new Error(
993
+ `External source delivery ${event.deliveryId} cannot attach to terminal WorkItem truth.`,
994
+ );
995
+ }
996
+ seenDeliveries.add(event.deliveryId);
997
+ sourceDeliveries.push({ ...event, ref });
611
998
  }
612
999
  }
613
1000
  return {
@@ -615,10 +1002,12 @@ function projectEvents(definition, records) {
615
1002
  assignmentHistory,
616
1003
  checkpoints,
617
1004
  handoffs,
1005
+ reworks,
618
1006
  decisions,
619
1007
  gatePasses,
620
1008
  verifications,
621
1009
  dataCandidates,
1010
+ sourceDeliveries,
622
1011
  };
623
1012
  }
624
1013
 
@@ -945,13 +1334,27 @@ function projectStatus(definition, records, workspaceRoot = null) {
945
1334
  const assignment = state.assignments.get(item.id) ?? null;
946
1335
  const checkpoint = state.checkpoints.get(item.id) ?? null;
947
1336
  const handoff = state.handoffs.get(item.id) ?? null;
948
- const latestFact = latestEventFact(checkpoint, handoff);
1337
+ const rework = state.reworks.get(item.id) ?? null;
1338
+ const sourceDeliveries = state.sourceDeliveries.filter(row => row.workItemId === item.id);
1339
+ const latestSourceDeliveryFact = sourceDeliveries.at(-1) ?? null;
1340
+ const latestSourceDelivery = sourceDeliveryProjection(sourceDeliveries.at(-1) ?? null);
1341
+ const currentRework = rework?.assignmentId === assignment?.assignmentId ? rework : null;
1342
+ const latestFact = latestEventFact(latestEventFact(checkpoint, handoff), currentRework);
949
1343
  const activeHandoff = handoff
950
1344
  && handoff.assignmentId === assignment?.assignmentId
951
1345
  && handoff.fromAgentId === assignment?.agentId
952
1346
  ? handoff
953
1347
  : null;
954
- const pendingHandoff = activeHandoff && latestFact?.eventId === activeHandoff.eventId
1348
+ const latestHandoffFact = latestEventFact(activeHandoff, latestSourceDeliveryFact);
1349
+ const handoffSupersededByDelivery = activeHandoff
1350
+ && latestFact?.eventId === activeHandoff.eventId
1351
+ && latestHandoffFact?.eventId !== activeHandoff.eventId;
1352
+ const projectedFact = handoffSupersededByDelivery
1353
+ ? latestEventFact(checkpoint, currentRework)
1354
+ : latestFact;
1355
+ const pendingHandoff = activeHandoff
1356
+ && latestFact?.eventId === activeHandoff.eventId
1357
+ && latestHandoffFact?.eventId === activeHandoff.eventId
955
1358
  ? pendingHandoffProjection(activeHandoff)
956
1359
  : null;
957
1360
  const executionTruth = executionTruthFor(workspaceRoot, assignment);
@@ -959,7 +1362,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
959
1362
  let status = "planned";
960
1363
  if (assignment) status = unresolvedDependencies.length > 0 ? "waiting_dependency" : "active";
961
1364
  if (pendingHandoff) status = "planned";
962
- if (latestFact?.type === "checkpoint_recorded") status = checkpoint.state;
1365
+ if (projectedFact?.type === "checkpoint_recorded") status = checkpoint.state;
963
1366
  if (executionTruth && (executionTruth.status !== "bound"
964
1367
  || (executionTruth.lifecycle === "closed"
965
1368
  && executionTruth.closeoutDecision !== "accepted"))) {
@@ -973,7 +1376,8 @@ function projectStatus(definition, records, workspaceRoot = null) {
973
1376
  total: item.measurable.total,
974
1377
  }
975
1378
  : { kind: "state_only" };
976
- const history = state.assignmentHistory.get(item.id).map(row => ({
1379
+ const assignmentEvents = state.assignmentHistory.get(item.id);
1380
+ const history = assignmentEvents.map(row => ({
977
1381
  assignmentId: row.assignmentId,
978
1382
  agentId: row.agentId,
979
1383
  occurredAt: row.occurredAt,
@@ -986,7 +1390,18 @@ function projectStatus(definition, records, workspaceRoot = null) {
986
1390
  ...history.map(row => row.truthRef),
987
1391
  ...(checkpoint ? [checkpoint.ref] : []),
988
1392
  ...(handoff ? [handoff.ref] : []),
1393
+ ...(rework ? [rework.ref] : []),
1394
+ ...sourceDeliveries.map(row => row.ref),
989
1395
  ];
1396
+ const projectedRework = reworkProjection(
1397
+ currentRework,
1398
+ currentRework
1399
+ ? assignmentEvents.findIndex(row => row.assignmentId === currentRework.assignmentId) + 1
1400
+ : null,
1401
+ );
1402
+ const latestSummary = projectedFact?.type === "rework_returned"
1403
+ ? projectedFact.reason
1404
+ : projectedFact?.summary ?? null;
990
1405
  return {
991
1406
  workItemId: item.id,
992
1407
  title: item.title,
@@ -1000,16 +1415,18 @@ function projectStatus(definition, records, workspaceRoot = null) {
1000
1415
  executionTruth,
1001
1416
  dependencies: [...item.dependencies],
1002
1417
  unresolvedDependencies,
1003
- blockers: latestFact?.type === "checkpoint_recorded"
1418
+ blockers: projectedFact?.type === "checkpoint_recorded"
1004
1419
  ? checkpoint.blockerRefs
1005
1420
  : unresolvedDependencies.map(id => `work-item:${id}`),
1006
- decisionRefs: latestFact?.type === "checkpoint_recorded" ? checkpoint.decisionRefs : [],
1421
+ decisionRefs: projectedFact?.type === "checkpoint_recorded" ? checkpoint.decisionRefs : [],
1007
1422
  progress: measured,
1008
- summary: latestFact?.summary ?? null,
1009
- nextAction: latestFact?.nextAction ?? null,
1423
+ summary: latestSummary,
1424
+ nextAction: projectedFact?.nextAction ?? null,
1010
1425
  pendingHandoff,
1426
+ rework: projectedRework,
1427
+ latestSourceDelivery,
1011
1428
  lastCheckpointAt: checkpoint?.occurredAt ?? null,
1012
- evidenceRefs: latestFact?.evidenceRefs ?? [],
1429
+ evidenceRefs: projectedFact?.evidenceRefs ?? [],
1013
1430
  assignmentHistory: history,
1014
1431
  truthRefs,
1015
1432
  };
@@ -1212,6 +1629,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
1212
1629
  truthRefs: [row.ref],
1213
1630
  };
1214
1631
  });
1632
+ const sourceDeliveries = state.sourceDeliveries.map(sourceDeliveryProjection);
1215
1633
  let overall = "planned";
1216
1634
  if (workItems.every(row => row.status === "completed")
1217
1635
  && integrationGates.every(row => row.status === "passed")
@@ -1219,7 +1637,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
1219
1637
  else if (openDecisions.length > 0 || workItems.some(row => row.status === "failed")) overall = "active_with_blockers";
1220
1638
  else if (workItems.some(row => row.status !== "planned" || row.pendingHandoff !== null)) overall = "active";
1221
1639
  const status = {
1222
- schemaVersion: STATUS_SCHEMA,
1640
+ schemaVersion: STATUS_SCHEMA_V2,
1223
1641
  status: "team_project_status",
1224
1642
  projectId: definition.projectId,
1225
1643
  objective: definition.objective,
@@ -1237,6 +1655,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
1237
1655
  openDeferredVerificationIds,
1238
1656
  integrationGates,
1239
1657
  dataCandidates,
1658
+ sourceDeliveries,
1240
1659
  projectTruthHash: sha256(canonical({ definition, events: records.map(row => row.event) })),
1241
1660
  authorizationGranted: false,
1242
1661
  };
@@ -1339,6 +1758,42 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
1339
1758
  || current.agentId !== normalized.fromAgentId) {
1340
1759
  throw new Error("Handoff must bind the current assignment and Agent.");
1341
1760
  }
1761
+ } else if (normalized.type === "rework_returned") {
1762
+ const current = state.assignments.get(normalized.workItemId) ?? null;
1763
+ const failure = state.checkpoints.get(normalized.workItemId) ?? null;
1764
+ const projected = status.workItems.find(row => row.workItemId === normalized.workItemId);
1765
+ const latestFact = latestEventFact(
1766
+ failure,
1767
+ state.handoffs.get(normalized.workItemId) ?? null,
1768
+ );
1769
+ if (projected?.status !== "failed"
1770
+ || !current
1771
+ || current.assignmentId !== normalized.failedAssignmentId
1772
+ || current.agentId !== normalized.fromAgentId
1773
+ || failure?.eventId !== normalized.failedCheckpointEventId
1774
+ || failure.assignmentId !== normalized.failedAssignmentId
1775
+ || failure.state !== "failed"
1776
+ || latestFact?.eventId !== failure.eventId) {
1777
+ throw new Error(
1778
+ "reject-and-return requires one current failed checkpoint bound to the current assignment.",
1779
+ );
1780
+ }
1781
+ if (state.assignmentHistory.get(normalized.workItemId).some(
1782
+ row => row.assignmentId === normalized.assignmentId,
1783
+ )) {
1784
+ throw new Error(`Assignment ${normalized.assignmentId} already exists.`);
1785
+ }
1786
+ for (const otherProjection of status.workItems) {
1787
+ if (otherProjection.workItemId === normalized.workItemId
1788
+ || otherProjection.assignmentId === null
1789
+ || ["completed", "failed"].includes(otherProjection.status)) continue;
1790
+ const other = definition.workItems.find(row => row.id === otherProjection.workItemId);
1791
+ if (pathsOverlap(workItem.ownedPaths, other.ownedPaths)) {
1792
+ throw new Error(
1793
+ `Owned paths overlap active assignment ${otherProjection.assignmentId}: ${normalized.workItemId} <> ${otherProjection.workItemId}.`,
1794
+ );
1795
+ }
1796
+ }
1342
1797
  } else if (normalized.type === "decision_opened") {
1343
1798
  if (state.decisions.has(normalized.decisionId)) throw new Error(`Decision ${normalized.decisionId} already exists.`);
1344
1799
  } else if (normalized.type === "decision_resolved") {
@@ -1378,6 +1833,18 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
1378
1833
  if (state.dataCandidates.some(row => row.candidateId === normalized.candidateId)) {
1379
1834
  throw new Error(`Data candidate ${normalized.candidateId} already exists.`);
1380
1835
  }
1836
+ } else if (normalized.type === "source_delivery_imported") {
1837
+ if (state.sourceDeliveries.some(row => row.deliveryId === normalized.deliveryId)) {
1838
+ throw new Error(`External source delivery ${normalized.deliveryId} already exists.`);
1839
+ }
1840
+ const current = state.assignments.get(normalized.workItemId) ?? null;
1841
+ if (current?.assignmentId !== normalized.assignmentId) {
1842
+ throw new Error("External source delivery must bind the current assignment.");
1843
+ }
1844
+ const projected = status.workItems.find(row => row.workItemId === normalized.workItemId);
1845
+ if (["completed", "failed"].includes(projected?.status)) {
1846
+ throw new Error("External source delivery requires a non-terminal WorkItem.");
1847
+ }
1381
1848
  }
1382
1849
  return normalized;
1383
1850
  }
@@ -1416,52 +1883,55 @@ export function initializeTeamProjectV1({ workspaceRoot, definition }) {
1416
1883
  };
1417
1884
  }
1418
1885
 
1419
- export function appendTeamProjectEventV1({ workspaceRoot, event }) {
1420
- const loaded = loadProject(workspaceRoot);
1421
- return withProjectLock(loaded.projectRoot, () => {
1422
- const refreshed = loadProject(workspaceRoot);
1423
- const shaped = validateEventShape(event, refreshed.definition);
1424
- const eventPath = path.join(refreshed.projectRoot, "events", `${shaped.eventId}.json`);
1425
- if (existsSync(eventPath)) {
1426
- const existing = readRegularJson(eventPath, `Team project event ${shaped.eventId}`);
1427
- if (canonical(existing) !== canonical(shaped)) {
1428
- throw new Error(`Team project immutable event differs: ${shaped.eventId}.`);
1429
- }
1430
- return {
1431
- status: "team_project_event_recorded",
1432
- exitCode: 0,
1433
- eventId: shaped.eventId,
1434
- eventPath: relativeToWorkspace(refreshed.root, eventPath),
1435
- eventSha256: sha256(canonical(shaped)),
1436
- resumed: true,
1437
- authorizationGranted: false,
1438
- };
1439
- }
1440
- const latest = refreshed.events.at(-1)?.event ?? null;
1441
- if (latest && (shaped.occurredAt < latest.occurredAt
1442
- || (shaped.occurredAt === latest.occurredAt
1443
- && shaped.eventId.localeCompare(latest.eventId) <= 0))) {
1444
- throw new Error(
1445
- `Team project event must sort after the latest event ${latest.eventId}.`,
1446
- );
1886
+ function appendTeamProjectEventWithinLock(refreshed, event) {
1887
+ const shaped = validateEventShape(event, refreshed.definition);
1888
+ const eventPath = path.join(refreshed.projectRoot, "events", `${shaped.eventId}.json`);
1889
+ if (existsSync(eventPath)) {
1890
+ const existing = readRegularJson(eventPath, `Team project event ${shaped.eventId}`);
1891
+ if (canonical(existing) !== canonical(shaped)) {
1892
+ throw new Error(`Team project immutable event differs: ${shaped.eventId}.`);
1447
1893
  }
1448
- const normalized = validateEventAgainstState(
1449
- shaped,
1450
- refreshed.definition,
1451
- refreshed.events,
1452
- refreshed.root,
1453
- );
1454
- writeJsonExclusiveAtomically(eventPath, normalized);
1455
1894
  return {
1456
1895
  status: "team_project_event_recorded",
1457
1896
  exitCode: 0,
1458
- eventId: normalized.eventId,
1897
+ eventId: shaped.eventId,
1459
1898
  eventPath: relativeToWorkspace(refreshed.root, eventPath),
1460
- eventSha256: sha256(canonical(normalized)),
1461
- resumed: false,
1899
+ eventSha256: sha256(canonical(shaped)),
1900
+ resumed: true,
1462
1901
  authorizationGranted: false,
1463
1902
  };
1464
- });
1903
+ }
1904
+ const latest = refreshed.events.at(-1)?.event ?? null;
1905
+ if (latest && (shaped.occurredAt < latest.occurredAt
1906
+ || (shaped.occurredAt === latest.occurredAt
1907
+ && shaped.eventId.localeCompare(latest.eventId) <= 0))) {
1908
+ throw new Error(
1909
+ `Team project event must sort after the latest event ${latest.eventId}.`,
1910
+ );
1911
+ }
1912
+ const normalized = validateEventAgainstState(
1913
+ shaped,
1914
+ refreshed.definition,
1915
+ refreshed.events,
1916
+ refreshed.root,
1917
+ );
1918
+ writeJsonExclusiveAtomically(eventPath, normalized);
1919
+ return {
1920
+ status: "team_project_event_recorded",
1921
+ exitCode: 0,
1922
+ eventId: normalized.eventId,
1923
+ eventPath: relativeToWorkspace(refreshed.root, eventPath),
1924
+ eventSha256: sha256(canonical(normalized)),
1925
+ resumed: false,
1926
+ authorizationGranted: false,
1927
+ };
1928
+ }
1929
+
1930
+ export function appendTeamProjectEventV1({ workspaceRoot, event }) {
1931
+ const loaded = loadProject(workspaceRoot);
1932
+ return withProjectLock(loaded.projectRoot, () => (
1933
+ appendTeamProjectEventWithinLock(loadProject(workspaceRoot), event)
1934
+ ));
1465
1935
  }
1466
1936
 
1467
1937
  export function assignTeamProjectV1({
@@ -1584,6 +2054,133 @@ export function handoffTeamProjectV1({
1584
2054
  };
1585
2055
  }
1586
2056
 
2057
+ export function rejectAndReturnTeamProjectV1({
2058
+ workspaceRoot,
2059
+ reworkId,
2060
+ occurredAt,
2061
+ workItemId,
2062
+ reviewerAgentId,
2063
+ toAgentId,
2064
+ reason,
2065
+ nextAction,
2066
+ evidenceRefs,
2067
+ }) {
2068
+ safeIdentifier(reworkId, "reworkId");
2069
+ const eventId = `rework-returned-${reworkId}`;
2070
+ const assignmentId = `rework-${reworkId}`;
2071
+ const loaded = loadProject(workspaceRoot);
2072
+ return withProjectLock(loaded.projectRoot, () => {
2073
+ const refreshed = loadProject(workspaceRoot);
2074
+ const existing = refreshed.events.find(row => row.event.eventId === eventId)?.event ?? null;
2075
+ let event;
2076
+ if (existing) {
2077
+ event = {
2078
+ schemaVersion: EVENT_SCHEMA_V2,
2079
+ eventId,
2080
+ type: "rework_returned",
2081
+ occurredAt,
2082
+ reworkId,
2083
+ assignmentId,
2084
+ failedAssignmentId: existing.failedAssignmentId,
2085
+ failedCheckpointEventId: existing.failedCheckpointEventId,
2086
+ workItemId,
2087
+ fromAgentId: existing.fromAgentId,
2088
+ reviewerAgentId,
2089
+ toAgentId,
2090
+ reason,
2091
+ evidenceRefs,
2092
+ nextAction,
2093
+ };
2094
+ } else {
2095
+ const state = projectEvents(refreshed.definition, refreshed.events);
2096
+ const status = projectStatus(refreshed.definition, refreshed.events, refreshed.root);
2097
+ const current = state.assignments.get(workItemId) ?? null;
2098
+ const failure = state.checkpoints.get(workItemId) ?? null;
2099
+ const projected = status.workItems.find(row => row.workItemId === workItemId) ?? null;
2100
+ const latestFact = latestEventFact(failure, state.handoffs.get(workItemId) ?? null);
2101
+ if (projected?.status !== "failed"
2102
+ || !current
2103
+ || failure?.state !== "failed"
2104
+ || failure.assignmentId !== current.assignmentId
2105
+ || latestFact?.eventId !== failure.eventId) {
2106
+ throw new Error(
2107
+ "reject-and-return requires one current failed checkpoint bound to the current assignment.",
2108
+ );
2109
+ }
2110
+ event = {
2111
+ schemaVersion: EVENT_SCHEMA_V2,
2112
+ eventId,
2113
+ type: "rework_returned",
2114
+ occurredAt,
2115
+ reworkId,
2116
+ assignmentId,
2117
+ failedAssignmentId: current.assignmentId,
2118
+ failedCheckpointEventId: failure.eventId,
2119
+ workItemId,
2120
+ fromAgentId: current.agentId,
2121
+ reviewerAgentId,
2122
+ toAgentId,
2123
+ reason,
2124
+ evidenceRefs,
2125
+ nextAction,
2126
+ };
2127
+ }
2128
+
2129
+ const recorded = appendTeamProjectEventWithinLock(refreshed, event);
2130
+ const projectedEvents = recorded.resumed
2131
+ ? refreshed.events
2132
+ : [
2133
+ ...refreshed.events,
2134
+ { event, ref: `project/events/${event.eventId}.json` },
2135
+ ];
2136
+ const status = projectStatus(refreshed.definition, projectedEvents, refreshed.root);
2137
+ const projectedState = projectEvents(refreshed.definition, projectedEvents);
2138
+ const current = status.workItems.find(row => row.workItemId === workItemId) ?? null;
2139
+ const latestFact = latestEventFact(
2140
+ latestEventFact(
2141
+ projectedState.checkpoints.get(workItemId) ?? null,
2142
+ projectedState.handoffs.get(workItemId) ?? null,
2143
+ ),
2144
+ projectedState.reworks.get(workItemId) ?? null,
2145
+ );
2146
+ if (current?.assignmentId !== assignmentId
2147
+ || current?.rework?.eventId !== eventId
2148
+ || latestFact?.eventId !== eventId) {
2149
+ throw new Error(
2150
+ `Cannot resume historical/superseded rework ${reworkId}; `
2151
+ + `current assignment=${current?.assignmentId ?? "none"}, `
2152
+ + `current owner=${current?.agentId ?? "none"}.`,
2153
+ );
2154
+ }
2155
+ return {
2156
+ status: "team_project_rework",
2157
+ reworkId,
2158
+ assignmentId,
2159
+ failedAssignmentId: event.failedAssignmentId,
2160
+ failedCheckpointEventId: event.failedCheckpointEventId,
2161
+ workItemId,
2162
+ fromAgentId: event.fromAgentId,
2163
+ reviewerAgentId,
2164
+ toAgentId,
2165
+ reason,
2166
+ nextAction: current.nextAction,
2167
+ eventId: recorded.eventId,
2168
+ eventPath: recorded.eventPath,
2169
+ eventSha256: recorded.eventSha256,
2170
+ resumed: recorded.resumed,
2171
+ reworkAttempt: current.rework.attempt,
2172
+ projectTruthHash: status.projectTruthHash,
2173
+ overall: status.overall,
2174
+ projectNextAction: status.nextAction,
2175
+ projectNextActorId: status.nextActorId,
2176
+ gitWritePerformed: false,
2177
+ releaseWritePerformed: false,
2178
+ authorizationGranted: false,
2179
+ exitCode: 0,
2180
+ };
2181
+ });
2182
+ }
2183
+
1587
2184
  export function openTeamProjectDecisionV1({
1588
2185
  workspaceRoot,
1589
2186
  decisionId,
@@ -1890,7 +2487,28 @@ export function checkpointTeamProjectV1({
1890
2487
  };
1891
2488
  }
1892
2489
 
2490
+ function legacyWorkItemProjection(workItem) {
2491
+ if (workItem === null) return null;
2492
+ const { latestSourceDelivery: _latestSourceDelivery, ...legacy } = workItem;
2493
+ return legacy;
2494
+ }
2495
+
1893
2496
  export function readTeamProjectStatusV1({ workspaceRoot }) {
2497
+ const loaded = loadProject(workspaceRoot);
2498
+ const { sourceDeliveries: _sourceDeliveries, ...legacy } = projectStatus(
2499
+ loaded.definition,
2500
+ loaded.events,
2501
+ loaded.root,
2502
+ );
2503
+ return {
2504
+ ...legacy,
2505
+ schemaVersion: STATUS_SCHEMA_V1,
2506
+ workItems: legacy.workItems.map(legacyWorkItemProjection),
2507
+ exitCode: 0,
2508
+ };
2509
+ }
2510
+
2511
+ export function readTeamProjectStatusV2({ workspaceRoot }) {
1894
2512
  const loaded = loadProject(workspaceRoot);
1895
2513
  return {
1896
2514
  ...projectStatus(loaded.definition, loaded.events, loaded.root),
@@ -1898,7 +2516,7 @@ export function readTeamProjectStatusV1({ workspaceRoot }) {
1898
2516
  };
1899
2517
  }
1900
2518
 
1901
- export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
2519
+ export function buildTeamProjectTakeoverV2({ workspaceRoot, agentId }) {
1902
2520
  safeIdentifier(agentId, "agentId");
1903
2521
  const loaded = loadProject(workspaceRoot);
1904
2522
  const status = projectStatus(loaded.definition, loaded.events, loaded.root);
@@ -1915,6 +2533,29 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
1915
2533
  row.pendingHandoff?.toAgentId === agentId
1916
2534
  ))?.pendingHandoff ?? null;
1917
2535
  const pendingHandoff = pendingForCurrent ?? pendingForAgent;
2536
+ const staleHandoffs = staleHandoffsForTakeover({
2537
+ loaded,
2538
+ status,
2539
+ agentId,
2540
+ responsibilities,
2541
+ });
2542
+ const latestStaleHandoff = [...staleHandoffs]
2543
+ .sort(compareEventFacts)
2544
+ .at(-1) ?? null;
2545
+ const focusResponsibility = current
2546
+ ?? (pendingHandoff
2547
+ ? status.workItems.find(row => row.workItemId === pendingHandoff.workItemId) ?? null
2548
+ : null)
2549
+ ?? (latestStaleHandoff
2550
+ ? status.workItems.find(row => row.workItemId === latestStaleHandoff.workItemId) ?? null
2551
+ : null);
2552
+ const focusedDeliveries = focusResponsibility
2553
+ ? status.sourceDeliveries.filter(row => (
2554
+ row.workItemId === focusResponsibility.workItemId
2555
+ ))
2556
+ : [];
2557
+ const currentSourceDelivery = focusedDeliveries.at(-1) ?? null;
2558
+ const deliveryDelta = sourceDeliveryDelta(focusedDeliveries);
1918
2559
  const checkpoints = loaded.events
1919
2560
  .filter(row => row.event.type === "checkpoint_recorded"
1920
2561
  && responsibilities.some(item => item.workItemId === row.event.workItemId))
@@ -1924,6 +2565,9 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
1924
2565
  ...(lastCheckpoint?.evidenceRefs ?? []),
1925
2566
  ...(current?.evidenceRefs ?? []),
1926
2567
  ...(pendingHandoff?.evidenceRefs ?? []),
2568
+ ...(currentSourceDelivery ? [currentSourceDelivery.truthRef] : []),
2569
+ ...(deliveryDelta?.previousDeliveryId ? [focusedDeliveries.at(-2).truthRef] : []),
2570
+ ...staleHandoffs.map(row => row.truthRef),
1927
2571
  ])].sort();
1928
2572
  const unresolvedDecisionsById = new Map();
1929
2573
  for (const decision of status.openDecisions) {
@@ -1965,7 +2609,7 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
1965
2609
  : current ? null : status.readyQueue[0] ? `Take ${status.readyQueue[0]}.` : null;
1966
2610
  }
1967
2611
  return {
1968
- schemaVersion: "OwlCodaRunKitTeamProjectTakeoverV1",
2612
+ schemaVersion: "OwlCodaRunKitTeamProjectTakeoverV2",
1969
2613
  status: "team_project_takeover",
1970
2614
  projectId: status.projectId,
1971
2615
  objective: status.objective,
@@ -1978,6 +2622,9 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
1978
2622
  currentResponsibility: current,
1979
2623
  allResponsibilities: responsibilities,
1980
2624
  pendingHandoff,
2625
+ staleHandoffs,
2626
+ currentSourceDelivery,
2627
+ sourceDeliveryDelta: deliveryDelta,
1981
2628
  lastAcceptedCheckpoint: lastCheckpoint
1982
2629
  ? {
1983
2630
  workItemId: lastCheckpoint.workItemId,
@@ -2000,6 +2647,27 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
2000
2647
  };
2001
2648
  }
2002
2649
 
2650
+ export function buildTeamProjectTakeoverV1(input) {
2651
+ const {
2652
+ staleHandoffs: _staleHandoffs,
2653
+ currentSourceDelivery: _currentSourceDelivery,
2654
+ sourceDeliveryDelta: _sourceDeliveryDelta,
2655
+ ...legacy
2656
+ } = buildTeamProjectTakeoverV2(input);
2657
+ const v2OnlyEvidenceRefs = new Set([
2658
+ ..._staleHandoffs.map(row => row.truthRef),
2659
+ ...readTeamProjectStatusV2({ workspaceRoot: input.workspaceRoot })
2660
+ .sourceDeliveries.map(row => row.truthRef),
2661
+ ]);
2662
+ return {
2663
+ ...legacy,
2664
+ schemaVersion: "OwlCodaRunKitTeamProjectTakeoverV1",
2665
+ currentResponsibility: legacyWorkItemProjection(legacy.currentResponsibility),
2666
+ allResponsibilities: legacy.allResponsibilities.map(legacyWorkItemProjection),
2667
+ evidenceRefs: legacy.evidenceRefs.filter(ref => !v2OnlyEvidenceRefs.has(ref)),
2668
+ };
2669
+ }
2670
+
2003
2671
  export function formatTeamProjectStatusHumanV1(status) {
2004
2672
  const nextAction = status.nextAction === null ? "none" : singleLine(status.nextAction);
2005
2673
  return [
@@ -2020,6 +2688,65 @@ export function formatTeamProjectStatusHumanV1(status) {
2020
2688
  ].join("\n");
2021
2689
  }
2022
2690
 
2691
+ export function formatTeamProjectStatusHumanV2(status) {
2692
+ const nextAction = status.nextAction === null ? "none" : singleLine(status.nextAction);
2693
+ const latestSourceDelivery = status.sourceDeliveries.at(-1) ?? null;
2694
+ return [
2695
+ status.headline,
2696
+ `Project: ${status.projectId} — ${singleLine(status.objective)}`,
2697
+ `State: ${status.overall}`,
2698
+ `Work: ${status.counts.completed}/${status.counts.total} completed; ${status.counts.active} active; ${status.counts.waiting_dependency + status.counts.waiting_decision} waiting`,
2699
+ `Agents: ${status.agents.map(agent => agent.agentId).join(", ") || "none assigned"}`,
2700
+ `Open decisions: ${status.openDecisions.map(row => row.decisionId).join(", ") || "none"}`,
2701
+ `Deferred verification: ${status.openDeferredVerificationIds.length} open${
2702
+ status.openDeferredVerificationIds.length > 0
2703
+ ? ` (${status.openDeferredVerificationIds.join(", ")})`
2704
+ : ""
2705
+ }`,
2706
+ `Ready queue: ${status.readyQueue.join(", ") || "none"}`,
2707
+ `Source deliveries: ${status.sourceDeliveries.length}${latestSourceDelivery
2708
+ ? `; latest ${latestSourceDelivery.deliveryId} (${latestSourceDelivery.overlayStatus}, ${latestSourceDelivery.deliveryCandidateFingerprint})`
2709
+ : ""}`,
2710
+ `Next: ${nextAction}${status.nextActorId ? ` (owner ${status.nextActorId})` : ""}`,
2711
+ "",
2712
+ ].join("\n");
2713
+ }
2714
+
2715
+ export function formatTeamProjectTakeoverHumanV2(takeover) {
2716
+ const responsibility = takeover.currentResponsibility;
2717
+ const agentNextAction = takeover.nextAction === null
2718
+ ? "none"
2719
+ : singleLine(takeover.nextAction);
2720
+ const projectNextAction = takeover.projectNextAction === null
2721
+ ? "none"
2722
+ : singleLine(takeover.projectNextAction);
2723
+ return [
2724
+ takeover.projectHeadline,
2725
+ `Project: ${takeover.projectId} — ${singleLine(takeover.objective)}`,
2726
+ `Agent: ${takeover.agentId}`,
2727
+ `Responsibility: ${responsibility?.workItemId ?? "none"}`,
2728
+ `Open decisions: ${takeover.unresolvedDecisions.map(row => row.decisionId).join(", ") || "none"}`,
2729
+ `Deferred verification: ${takeover.deferredVerifications
2730
+ .map(row => row.verificationId).join(", ") || "none"}`,
2731
+ `Handoffs: pending ${takeover.pendingHandoff
2732
+ ? `${takeover.pendingHandoff.fromAgentId} -> ${takeover.pendingHandoff.toAgentId} for ${takeover.pendingHandoff.workItemId}`
2733
+ : "none"}; stale ${takeover.staleHandoffs.length > 0
2734
+ ? takeover.staleHandoffs.map(row => `${row.eventId} (${row.reason})`).join(", ")
2735
+ : "none"}`,
2736
+ `Candidate: ${takeover.currentSourceDelivery
2737
+ ? `${takeover.currentSourceDelivery.deliveryId} (${takeover.currentSourceDelivery.overlayStatus}, ${takeover.currentSourceDelivery.deliveryCandidateFingerprint})`
2738
+ : "none"}; delta ${takeover.sourceDeliveryDelta === null
2739
+ ? "none"
2740
+ : takeover.sourceDeliveryDelta.previousDeliveryId === null
2741
+ ? `first delivery; added ${takeover.sourceDeliveryDelta.addedPaths.length}`
2742
+ : `vs ${takeover.sourceDeliveryDelta.previousDeliveryId}; added ${takeover.sourceDeliveryDelta.addedPaths.length}, changed ${takeover.sourceDeliveryDelta.changedPaths.length}, removed ${takeover.sourceDeliveryDelta.removedPaths.length}, unchanged ${takeover.sourceDeliveryDelta.unchangedPaths.length}`}`,
2743
+ `Agent next: ${agentNextAction}`,
2744
+ `Project next: ${projectNextAction}${takeover.projectNextActorId ? ` (owner ${takeover.projectNextActorId})` : ""}`,
2745
+ `State: ${takeover.overall}`,
2746
+ "",
2747
+ ].join("\n");
2748
+ }
2749
+
2023
2750
  export function formatTeamProjectTakeoverHumanV1(takeover) {
2024
2751
  const responsibility = takeover.currentResponsibility;
2025
2752
  const agentNextAction = takeover.nextAction === null