owlrunkit 0.19.0 → 0.19.2

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 (36) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +81 -3
  3. package/docs/architecture/OWLCODA_RUNKIT_0190_CONSOLIDATED_DAILY_USE_PRODUCT_DECISION_20260812.md +12 -9
  4. package/docs/architecture/OWLCODA_RUNKIT_0191_PROFILE_RECONCILE_PATCH_PRODUCT_DECISION_20260813.md +36 -0
  5. package/docs/architecture/OWLCODA_RUNKIT_0192_FORMAL_USABILITY_PRODUCT_DECISION_20260813.md +141 -0
  6. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +52 -9
  7. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  8. package/package.json +1 -1
  9. package/packages/attest/distribution-manifest-v1.json +10 -0
  10. package/packages/attest/src/quick.mjs +42 -4
  11. package/registry-release-policy-v1.json +5 -5
  12. package/sbom.spdx.json +5 -5
  13. package/scripts/runkit-contract/core-contract.mjs +35 -2
  14. package/scripts/runkit-contract/efficiency-metrics.mjs +9 -7
  15. package/scripts/runkit-contract/formal-preflight.mjs +352 -0
  16. package/scripts/runkit-contract/formal-supplemental-evidence.mjs +713 -0
  17. package/scripts/runkit-contract/formal-workflow.mjs +529 -59
  18. package/scripts/runkit-contract/onboarding-doctor.mjs +32 -13
  19. package/scripts/runkit-contract/profile-onboarding.mjs +52 -33
  20. package/scripts/runkit-contract/project-cli-resolver.mjs +33 -3
  21. package/scripts/runkit-contract/project-external-delivery.mjs +16 -9
  22. package/scripts/runkit-contract/quick-attest.mjs +24 -0
  23. package/scripts/runkit-contract/runkit-cli.mjs +365 -49
  24. package/scripts/runkit-contract/schemas/formal-check-v2.schema.json +231 -0
  25. package/scripts/runkit-contract/schemas/formal-envelope-preflight-v1.schema.json +212 -0
  26. package/scripts/runkit-contract/schemas/formal-evidence-summary-v1.schema.json +114 -0
  27. package/scripts/runkit-contract/schemas/formal-supplemental-evidence-v1.schema.json +103 -0
  28. package/scripts/runkit-contract/schemas/team-project-event-v5.schema.json +89 -0
  29. package/scripts/runkit-contract/schemas/team-project-status-v3.schema.json +173 -0
  30. package/scripts/runkit-contract/snapshot.mjs +125 -5
  31. package/scripts/runkit-contract/source-candidate.mjs +271 -69
  32. package/scripts/runkit-contract/sync-release-identity.mjs +39 -4
  33. package/scripts/runkit-contract/team-project-successor.mjs +7 -4
  34. package/scripts/runkit-contract/team-project.mjs +440 -28
  35. package/scripts/runkit-contract/usage-mode.mjs +9 -3
  36. package/scripts/runkit-contract/verification-envelope.mjs +425 -37
@@ -31,8 +31,10 @@ const EVENT_SCHEMA = "OwlCodaRunKitTeamProjectEventV1";
31
31
  const EVENT_SCHEMA_V2 = "OwlCodaRunKitTeamProjectEventV2";
32
32
  const EVENT_SCHEMA_V3 = "OwlCodaRunKitTeamProjectEventV3";
33
33
  const EVENT_SCHEMA_V4 = "OwlCodaRunKitTeamProjectEventV4";
34
+ const EVENT_SCHEMA_V5 = "OwlCodaRunKitTeamProjectEventV5";
34
35
  const STATUS_SCHEMA_V1 = "OwlCodaRunKitTeamProjectStatusV1";
35
36
  const STATUS_SCHEMA_V2 = "OwlCodaRunKitTeamProjectStatusV2";
37
+ const STATUS_SCHEMA_V3 = "OwlCodaRunKitTeamProjectStatusV3";
36
38
 
37
39
  const DEFINITION_KEYS = [
38
40
  "schemaVersion",
@@ -124,6 +126,12 @@ const EVENT_KEYS = Object.freeze({
124
126
  "toAgentId", "state", "summary", "completedUnits", "evidenceRefs",
125
127
  "blockerRefs", "decisionRefs", "nextAction", "sourceFingerprint",
126
128
  ],
129
+ work_item_scope_revised: [
130
+ "schemaVersion", "eventId", "type", "occurredAt", "revisionId",
131
+ "supersedesRevisionId", "assignmentId", "workItemId", "ownerAgentId",
132
+ "previousOwnedPaths", "ownedPaths", "previousMeasurable", "measurable",
133
+ "reason", "evidenceRefs",
134
+ ],
127
135
  });
128
136
 
129
137
  function canonical(value) {
@@ -617,6 +625,7 @@ function staleHandoffsForTakeover({ loaded, status, agentId, responsibilities })
617
625
  "source_delivery_imported",
618
626
  "upstream_reopened",
619
627
  "responsibility_transferred",
628
+ "work_item_scope_revised",
620
629
  ].includes(row.event.type)
621
630
  ));
622
631
  let reason = "superseded_by_current_truth";
@@ -646,13 +655,16 @@ function staleHandoffsForTakeover({ loaded, status, agentId, responsibilities })
646
655
  .sort((left, right) => compareIdentifiers(left.eventId, right.eventId));
647
656
  }
648
657
 
649
- function validateTimestamp(value) {
658
+ function normalizeTimestamp(value) {
650
659
  if (typeof value !== "string"
651
- || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
660
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u.test(value)
652
661
  || Number.isNaN(Date.parse(value))) {
653
- throw new Error("Team project event occurredAt must be an ISO-8601 UTC timestamp.");
662
+ throw new Error(
663
+ "Team project event occurredAt must be an ISO-8601 UTC timestamp "
664
+ + "(YYYY-MM-DDTHH:mm:ssZ or YYYY-MM-DDTHH:mm:ss.sssZ).",
665
+ );
654
666
  }
655
- return value;
667
+ return new Date(value).toISOString();
656
668
  }
657
669
 
658
670
  function validateEventShape(value, definition) {
@@ -660,10 +672,10 @@ function validateEventShape(value, definition) {
660
672
  throw new Error("Team project event type is invalid.");
661
673
  }
662
674
  assertAllowedKeys(value, `Team project event ${value.eventId ?? "unknown"}`, EVENT_KEYS[value.type]);
663
- if (![EVENT_SCHEMA, EVENT_SCHEMA_V2, EVENT_SCHEMA_V3, EVENT_SCHEMA_V4]
675
+ if (![EVENT_SCHEMA, EVENT_SCHEMA_V2, EVENT_SCHEMA_V3, EVENT_SCHEMA_V4, EVENT_SCHEMA_V5]
664
676
  .includes(value.schemaVersion)) {
665
677
  throw new Error(
666
- `Team project event schemaVersion must be ${EVENT_SCHEMA}, ${EVENT_SCHEMA_V2}, ${EVENT_SCHEMA_V3}, or ${EVENT_SCHEMA_V4}.`,
678
+ `Team project event schemaVersion must be ${EVENT_SCHEMA}, ${EVENT_SCHEMA_V2}, ${EVENT_SCHEMA_V3}, ${EVENT_SCHEMA_V4}, or ${EVENT_SCHEMA_V5}.`,
667
679
  );
668
680
  }
669
681
  if (value.type === "rework_returned" && value.schemaVersion !== EVENT_SCHEMA_V2) {
@@ -684,8 +696,14 @@ function validateEventShape(value, definition) {
684
696
  `${EVENT_SCHEMA_V4} is reserved for upstream_reopened and responsibility_transferred events.`,
685
697
  );
686
698
  }
699
+ if (value.type === "work_item_scope_revised" && value.schemaVersion !== EVENT_SCHEMA_V5) {
700
+ throw new Error(`work_item_scope_revised requires schemaVersion ${EVENT_SCHEMA_V5}.`);
701
+ }
702
+ if (value.schemaVersion === EVENT_SCHEMA_V5 && value.type !== "work_item_scope_revised") {
703
+ throw new Error(`${EVENT_SCHEMA_V5} is reserved for work_item_scope_revised events.`);
704
+ }
687
705
  safeIdentifier(value.eventId, "eventId");
688
- validateTimestamp(value.occurredAt);
706
+ const normalizedOccurredAt = normalizeTimestamp(value.occurredAt);
689
707
  const workItemIds = new Set(definition.workItems.map(row => row.id));
690
708
  const gateIds = new Set(definition.integrationGates.map(row => row.id));
691
709
  const assertWorkItem = (id) => {
@@ -700,6 +718,7 @@ function validateEventShape(value, definition) {
700
718
  "source_delivery_imported",
701
719
  "upstream_reopened",
702
720
  "responsibility_transferred",
721
+ "work_item_scope_revised",
703
722
  "verification_deferred",
704
723
  ].includes(value.type)) {
705
724
  assertWorkItem(value.workItemId);
@@ -712,6 +731,7 @@ function validateEventShape(value, definition) {
712
731
  "source_delivery_imported",
713
732
  "upstream_reopened",
714
733
  "responsibility_transferred",
734
+ "work_item_scope_revised",
715
735
  ].includes(value.type)) {
716
736
  safeIdentifier(value.assignmentId, "assignmentId");
717
737
  }
@@ -962,8 +982,41 @@ function validateEventShape(value, definition) {
962
982
  && !/^(?:sha256:)?[a-f0-9]{64}$/u.test(value.sourceFingerprint)) {
963
983
  throw new Error("Responsibility transfer sourceFingerprint is invalid.");
964
984
  }
985
+ } else if (value.type === "work_item_scope_revised") {
986
+ safeIdentifier(value.revisionId, "revisionId");
987
+ if (value.supersedesRevisionId !== undefined) {
988
+ safeIdentifier(value.supersedesRevisionId, "supersedesRevisionId");
989
+ }
990
+ safeIdentifier(value.ownerAgentId, "ownerAgentId");
991
+ const previousOwnedPaths = validateLeaseOwnedPaths(value.previousOwnedPaths);
992
+ const ownedPaths = validateLeaseOwnedPaths(value.ownedPaths);
993
+ const validateMeasurable = (measurable, label) => {
994
+ if (measurable === null) return null;
995
+ if (!isRecord(measurable)) throw new Error(`${label} must be an object or null.`);
996
+ assertAllowedKeys(measurable, label, ["unit", "total"]);
997
+ nonEmptyString(measurable.unit, `${label} unit`);
998
+ if (!Number.isSafeInteger(measurable.total) || measurable.total < 1) {
999
+ throw new Error(`${label} total must be a positive integer.`);
1000
+ }
1001
+ return { unit: measurable.unit, total: measurable.total };
1002
+ };
1003
+ const previousMeasurable = validateMeasurable(
1004
+ value.previousMeasurable,
1005
+ "Previous WorkItem measurable",
1006
+ );
1007
+ const measurable = validateMeasurable(value.measurable, "Revised WorkItem measurable");
1008
+ nonEmptyString(value.reason, "WorkItem scope revision reason");
1009
+ stringArray(value.evidenceRefs, "WorkItem scope revision evidenceRefs");
1010
+ return {
1011
+ ...structuredClone(value),
1012
+ occurredAt: normalizedOccurredAt,
1013
+ previousOwnedPaths,
1014
+ ownedPaths,
1015
+ previousMeasurable,
1016
+ measurable,
1017
+ };
965
1018
  }
966
- return structuredClone(value);
1019
+ return { ...structuredClone(value), occurredAt: normalizedOccurredAt };
967
1020
  }
968
1021
 
969
1022
  function projectEvents(definition, records) {
@@ -979,12 +1032,15 @@ function projectEvents(definition, records) {
979
1032
  const verifications = new Map();
980
1033
  const dataCandidates = [];
981
1034
  const sourceDeliveries = [];
1035
+ const scopeRevisions = new Map();
1036
+ const scopeRevisionHistory = new Map(definition.workItems.map(row => [row.id, []]));
982
1037
  const seenAssignments = new Set();
983
1038
  const seenReworks = new Set();
984
1039
  const seenReopens = new Set();
985
1040
  const seenTransfers = new Set();
986
1041
  const seenDecisions = new Set();
987
1042
  const seenDeliveries = new Set();
1043
+ const seenScopeRevisions = new Set();
988
1044
  const seenFactIds = new Set(records.map(row => row.event.eventId));
989
1045
 
990
1046
  for (const { event, ref } of records) {
@@ -1052,7 +1108,8 @@ function projectEvents(definition, records) {
1052
1108
  }
1053
1109
  if (gatePasses.has(event.dueGateId)) {
1054
1110
  throw new Error(
1055
- `Deferred verification cannot target integration gate ${event.dueGateId}; gate already passed.`,
1111
+ `Deferred verification cannot target integration gate ${event.dueGateId}; gate already passed. `
1112
+ + "Create a successor project with project successor, or define a new unpassed gate before deferring.",
1056
1113
  );
1057
1114
  }
1058
1115
  verifications.set(event.verificationId, {
@@ -1098,6 +1155,38 @@ function projectEvents(definition, records) {
1098
1155
  }
1099
1156
  seenDeliveries.add(event.deliveryId);
1100
1157
  sourceDeliveries.push({ ...event, ref });
1158
+ } else if (event.type === "work_item_scope_revised") {
1159
+ if (seenScopeRevisions.has(event.revisionId)) {
1160
+ throw new Error(`Duplicate WorkItem scope revision ${event.revisionId}.`);
1161
+ }
1162
+ const baseWorkItem = definition.workItems.find(row => row.id === event.workItemId);
1163
+ const previous = scopeRevisions.get(event.workItemId) ?? null;
1164
+ const currentAssignment = assignments.get(event.workItemId) ?? null;
1165
+ const currentOwnedPaths = previous?.ownedPaths ?? baseWorkItem.ownedPaths;
1166
+ const currentMeasurable = previous
1167
+ ? previous.measurable
1168
+ : baseWorkItem.measurable ?? null;
1169
+ if (event.supersedesRevisionId !== previous?.revisionId) {
1170
+ throw new Error(
1171
+ `WorkItem scope revision ${event.revisionId} must supersede current revision ${previous?.revisionId ?? "none"}.`,
1172
+ );
1173
+ }
1174
+ if (canonical(event.previousOwnedPaths) !== canonical(currentOwnedPaths)
1175
+ || canonical(event.previousMeasurable) !== canonical(currentMeasurable)) {
1176
+ throw new Error(
1177
+ `WorkItem scope revision ${event.revisionId} does not bind the current scope.`,
1178
+ );
1179
+ }
1180
+ if (currentAssignment?.assignmentId !== event.assignmentId
1181
+ || currentAssignment.agentId !== event.ownerAgentId) {
1182
+ throw new Error(
1183
+ `WorkItem scope revision ${event.revisionId} does not bind the current assignment and owner.`,
1184
+ );
1185
+ }
1186
+ seenScopeRevisions.add(event.revisionId);
1187
+ const revision = { ...event, ref };
1188
+ scopeRevisions.set(event.workItemId, revision);
1189
+ scopeRevisionHistory.get(event.workItemId).push(revision);
1101
1190
  } else if (event.type === "upstream_reopened") {
1102
1191
  if (seenReopens.has(event.reopenId)) {
1103
1192
  throw new Error(`Duplicate upstream reopen ${event.reopenId}.`);
@@ -1273,6 +1362,26 @@ function projectEvents(definition, records) {
1273
1362
  verifications,
1274
1363
  dataCandidates,
1275
1364
  sourceDeliveries,
1365
+ scopeRevisions,
1366
+ scopeRevisionHistory,
1367
+ };
1368
+ }
1369
+
1370
+ function definitionWithScopeRevisions(definition, state) {
1371
+ return {
1372
+ ...definition,
1373
+ workItems: definition.workItems.map((item) => {
1374
+ const revision = state.scopeRevisions.get(item.id) ?? null;
1375
+ if (!revision) return item;
1376
+ const { measurable: _baseMeasurable, ...base } = item;
1377
+ return {
1378
+ ...base,
1379
+ ownedPaths: [...revision.ownedPaths],
1380
+ ...(revision.measurable === null
1381
+ ? {}
1382
+ : { measurable: structuredClone(revision.measurable) }),
1383
+ };
1384
+ }),
1276
1385
  };
1277
1386
  }
1278
1387
 
@@ -1610,7 +1719,9 @@ function completedWorkItemIds(definition, state) {
1610
1719
  for (const item of definition.workItems) {
1611
1720
  if (completed.has(item.id)) continue;
1612
1721
  const checkpoint = state.checkpoints.get(item.id) ?? null;
1722
+ const scopeRevision = state.scopeRevisions.get(item.id) ?? null;
1613
1723
  if (checkpoint?.state !== "completed"
1724
+ || (scopeRevision && compareEventFacts(checkpoint, scopeRevision) <= 0)
1614
1725
  || invalidatingReopensForWorkItem(state, item.id).length > 0
1615
1726
  || item.dependencies.some(id => !completed.has(id))) continue;
1616
1727
  completed.add(item.id);
@@ -1620,8 +1731,28 @@ function completedWorkItemIds(definition, state) {
1620
1731
  return completed;
1621
1732
  }
1622
1733
 
1734
+ function hasBoundVerificationReceipt(evidenceRefs) {
1735
+ return evidenceRefs.some(ref => (
1736
+ /(?:quick[^\s]*receipt|receipt[^\s]*quick|formal[^\s]*(?:receipt|check|closeout)|(?:receipt|check|closeout)[^\s]*formal|closeout-receipt)/iu
1737
+ .test(ref)
1738
+ ));
1739
+ }
1740
+
1741
+ function verificationReceiptWarning(workItem) {
1742
+ if (!["verifying", "completed"].includes(workItem.status)
1743
+ || hasBoundVerificationReceipt(workItem.evidenceRefs)) return null;
1744
+ return {
1745
+ code: "no_bound_verification_receipt",
1746
+ workItemId: workItem.workItemId,
1747
+ message: "No Quick or Formal receipt reference is bound to the current verification state.",
1748
+ truthRefs: uniqueSorted(workItem.truthRefs),
1749
+ };
1750
+ }
1751
+
1623
1752
  function projectStatus(definition, records, workspaceRoot = null) {
1753
+ const projectDefinition = definition;
1624
1754
  const state = projectEvents(definition, records);
1755
+ definition = definitionWithScopeRevisions(definition, state);
1625
1756
  const completed = completedWorkItemIds(definition, state);
1626
1757
  let workItems = definition.workItems.map((item) => {
1627
1758
  const assignment = state.assignments.get(item.id) ?? null;
@@ -1629,6 +1760,8 @@ function projectStatus(definition, records, workspaceRoot = null) {
1629
1760
  const handoff = state.handoffs.get(item.id) ?? null;
1630
1761
  const rework = state.reworks.get(item.id) ?? null;
1631
1762
  const sourceDeliveries = state.sourceDeliveries.filter(row => row.workItemId === item.id);
1763
+ const scopeRevisionHistory = state.scopeRevisionHistory.get(item.id) ?? [];
1764
+ const latestScopeRevision = scopeRevisionHistory.at(-1) ?? null;
1632
1765
  const latestSourceDeliveryFact = sourceDeliveries.at(-1) ?? null;
1633
1766
  const latestSourceDelivery = sourceDeliveryProjection(sourceDeliveries.at(-1) ?? null);
1634
1767
  const currentRework = rework?.assignmentId === assignment?.assignmentId ? rework : null;
@@ -1640,8 +1773,19 @@ function projectStatus(definition, records, workspaceRoot = null) {
1640
1773
  const currentTransfer = state.transfers.get(item.id)?.assignmentId === assignment?.assignmentId
1641
1774
  ? state.transfers.get(item.id)
1642
1775
  : null;
1643
- const effectiveCheckpoint = invalidatingReopens.length === 0 ? checkpoint : null;
1644
- let latestFact = [effectiveCheckpoint, handoff, currentRework, currentReopen, currentTransfer]
1776
+ const scopeInvalidatesCheckpoint = checkpoint && latestScopeRevision
1777
+ && compareEventFacts(checkpoint, latestScopeRevision) <= 0;
1778
+ const effectiveCheckpoint = invalidatingReopens.length === 0 && !scopeInvalidatesCheckpoint
1779
+ ? checkpoint
1780
+ : null;
1781
+ let latestFact = [
1782
+ effectiveCheckpoint,
1783
+ handoff,
1784
+ currentRework,
1785
+ currentReopen,
1786
+ currentTransfer,
1787
+ latestScopeRevision,
1788
+ ]
1645
1789
  .reduce((latest, fact) => latestEventFact(latest, fact), null);
1646
1790
  if (currentTransfer && latestFact?.occurredAt === currentTransfer.occurredAt) {
1647
1791
  latestFact = currentTransfer;
@@ -1656,7 +1800,13 @@ function projectStatus(definition, records, workspaceRoot = null) {
1656
1800
  && latestFact?.eventId === activeHandoff.eventId
1657
1801
  && latestHandoffFact?.eventId !== activeHandoff.eventId;
1658
1802
  const projectedFact = handoffSupersededByDelivery
1659
- ? [effectiveCheckpoint, currentRework, currentReopen, currentTransfer]
1803
+ ? [
1804
+ effectiveCheckpoint,
1805
+ currentRework,
1806
+ currentReopen,
1807
+ currentTransfer,
1808
+ latestScopeRevision,
1809
+ ]
1660
1810
  .reduce((latest, fact) => latestEventFact(latest, fact), null)
1661
1811
  : latestFact;
1662
1812
  const pendingHandoff = activeHandoff
@@ -1672,6 +1822,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
1672
1822
  if (projectedFact?.type === "checkpoint_recorded") status = effectiveCheckpoint.state;
1673
1823
  if (projectedFact?.type === "responsibility_transferred") status = projectedFact.state;
1674
1824
  if (projectedFact?.type === "upstream_reopened") status = "active";
1825
+ if (projectedFact?.type === "work_item_scope_revised") status = "active";
1675
1826
  if (assignment && unresolvedDependencies.length > 0
1676
1827
  && !["completed", "failed"].includes(status)) status = "waiting_dependency";
1677
1828
  if (executionTruth && (executionTruth.status !== "bound"
@@ -1705,6 +1856,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
1705
1856
  ...reopenFacts.map(row => row.ref),
1706
1857
  ...(currentTransfer ? [currentTransfer.ref] : []),
1707
1858
  ...sourceDeliveries.map(row => row.ref),
1859
+ ...scopeRevisionHistory.map(row => row.ref),
1708
1860
  ];
1709
1861
  const projectedRework = reworkProjection(
1710
1862
  currentRework,
@@ -1712,7 +1864,11 @@ function projectStatus(definition, records, workspaceRoot = null) {
1712
1864
  ? assignmentEvents.findIndex(row => row.assignmentId === currentRework.assignmentId) + 1
1713
1865
  : null,
1714
1866
  );
1715
- const latestSummary = ["rework_returned", "upstream_reopened"].includes(projectedFact?.type)
1867
+ const latestSummary = [
1868
+ "rework_returned",
1869
+ "upstream_reopened",
1870
+ "work_item_scope_revised",
1871
+ ].includes(projectedFact?.type)
1716
1872
  ? projectedFact.reason
1717
1873
  : projectedFact?.summary ?? null;
1718
1874
  const checkpointLikeFact = ["checkpoint_recorded", "responsibility_transferred"]
@@ -1737,6 +1893,19 @@ function projectStatus(definition, records, workspaceRoot = null) {
1737
1893
  : unresolvedDependencies.map(id => `work-item:${id}`),
1738
1894
  decisionRefs: checkpointLikeFact ? checkpointLikeFact.decisionRefs : [],
1739
1895
  progress: measured,
1896
+ ownedPaths: [...item.ownedPaths],
1897
+ measurable: item.measurable ? structuredClone(item.measurable) : null,
1898
+ scopeRevision: latestScopeRevision
1899
+ ? {
1900
+ revisionId: latestScopeRevision.revisionId,
1901
+ supersedesRevisionId: latestScopeRevision.supersedesRevisionId ?? null,
1902
+ occurredAt: latestScopeRevision.occurredAt,
1903
+ ownerAgentId: latestScopeRevision.ownerAgentId,
1904
+ reason: latestScopeRevision.reason,
1905
+ evidenceRefs: [...latestScopeRevision.evidenceRefs],
1906
+ truthRef: latestScopeRevision.ref,
1907
+ }
1908
+ : null,
1740
1909
  summary: latestSummary,
1741
1910
  nextAction: projectedFact?.nextAction ?? null,
1742
1911
  pendingHandoff,
@@ -1950,6 +2119,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
1950
2119
  };
1951
2120
  });
1952
2121
  const sourceDeliveries = state.sourceDeliveries.map(sourceDeliveryProjection);
2122
+ const warnings = workItems.map(verificationReceiptWarning).filter(Boolean);
1953
2123
  let overall = "planned";
1954
2124
  if (workItems.every(row => row.status === "completed")
1955
2125
  && integrationGates.every(row => row.status === "passed")
@@ -1957,7 +2127,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
1957
2127
  else if (openDecisions.length > 0 || workItems.some(row => row.status === "failed")) overall = "active_with_blockers";
1958
2128
  else if (workItems.some(row => row.status !== "planned" || row.pendingHandoff !== null)) overall = "active";
1959
2129
  const status = {
1960
- schemaVersion: STATUS_SCHEMA_V2,
2130
+ schemaVersion: STATUS_SCHEMA_V3,
1961
2131
  status: "team_project_status",
1962
2132
  projectId: definition.projectId,
1963
2133
  objective: definition.objective,
@@ -1976,7 +2146,11 @@ function projectStatus(definition, records, workspaceRoot = null) {
1976
2146
  integrationGates,
1977
2147
  dataCandidates,
1978
2148
  sourceDeliveries,
1979
- projectTruthHash: sha256(canonical({ definition, events: records.map(row => row.event) })),
2149
+ warnings,
2150
+ projectTruthHash: sha256(canonical({
2151
+ definition: projectDefinition,
2152
+ events: records.map(row => row.event),
2153
+ })),
1980
2154
  authorizationGranted: false,
1981
2155
  };
1982
2156
  return { ...status, ...deriveProjectDriverSummary(status) };
@@ -1986,7 +2160,8 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
1986
2160
  const normalized = validateEventShape(event, definition);
1987
2161
  const state = projectEvents(definition, records);
1988
2162
  const status = projectStatus(definition, records, workspaceRoot);
1989
- const workItem = definition.workItems.find(row => row.id === normalized.workItemId);
2163
+ const effectiveDefinition = definitionWithScopeRevisions(definition, state);
2164
+ const workItem = effectiveDefinition.workItems.find(row => row.id === normalized.workItemId);
1990
2165
  if (normalized.type === "agent_assigned") {
1991
2166
  if (normalized.executionRunId !== undefined) {
1992
2167
  const executionTruth = executionTruthFor(workspaceRoot, normalized);
@@ -2016,7 +2191,7 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
2016
2191
  if (projected.workItemId === normalized.workItemId
2017
2192
  || projected.assignmentId === null
2018
2193
  || ["completed", "failed"].includes(projected.status)) continue;
2019
- const other = definition.workItems.find(row => row.id === projected.workItemId);
2194
+ const other = effectiveDefinition.workItems.find(row => row.id === projected.workItemId);
2020
2195
  if (pathsOverlap(workItem.ownedPaths, other.ownedPaths)) {
2021
2196
  throw new Error(
2022
2197
  `Owned paths overlap active assignment ${projected.assignmentId}: ${normalized.workItemId} <> ${projected.workItemId}.`,
@@ -2043,6 +2218,16 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
2043
2218
  if (normalized.state === "completed" && unresolved.length > 0) {
2044
2219
  throw new Error(`Completed checkpoint has unresolved dependencies: ${unresolved.join(", ")}.`);
2045
2220
  }
2221
+ if (normalized.state === "verifying") {
2222
+ const history = state.assignmentHistory.get(normalized.workItemId) ?? [];
2223
+ const prior = history.at(-2) ?? null;
2224
+ if (!prior || prior.agentId === current.agentId) {
2225
+ throw new Error(
2226
+ "Independent verification requires a different current owner from the implementation owner. "
2227
+ + `Record a handoff to the verifier, then run: npx --no-install owlrunkit project assign --workspace ${JSON.stringify(workspaceRoot)} --assignment-id <verifier-assignment-id> --at <ISO-UTC> --work-item ${normalized.workItemId} --agent <verifier-agent> --supersedes ${current.assignmentId}`,
2228
+ );
2229
+ }
2230
+ }
2046
2231
  const executionTruth = status.workItems.find(
2047
2232
  row => row.workItemId === normalized.workItemId,
2048
2233
  ).executionTruth;
@@ -2107,7 +2292,7 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
2107
2292
  if (otherProjection.workItemId === normalized.workItemId
2108
2293
  || otherProjection.assignmentId === null
2109
2294
  || ["completed", "failed"].includes(otherProjection.status)) continue;
2110
- const other = definition.workItems.find(row => row.id === otherProjection.workItemId);
2295
+ const other = effectiveDefinition.workItems.find(row => row.id === otherProjection.workItemId);
2111
2296
  if (pathsOverlap(workItem.ownedPaths, other.ownedPaths)) {
2112
2297
  throw new Error(
2113
2298
  `Owned paths overlap active assignment ${otherProjection.assignmentId}: ${normalized.workItemId} <> ${otherProjection.workItemId}.`,
@@ -2155,7 +2340,7 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
2155
2340
  if (otherProjection.workItemId === normalized.workItemId
2156
2341
  || otherProjection.assignmentId === null
2157
2342
  || ["completed", "failed"].includes(otherProjection.status)) continue;
2158
- const other = definition.workItems.find(row => row.id === otherProjection.workItemId);
2343
+ const other = effectiveDefinition.workItems.find(row => row.id === otherProjection.workItemId);
2159
2344
  if (pathsOverlap(workItem.ownedPaths, other.ownedPaths)) {
2160
2345
  throw new Error(
2161
2346
  `Owned paths overlap active assignment ${otherProjection.assignmentId}: ${normalized.workItemId} <> ${otherProjection.workItemId}.`,
@@ -2220,9 +2405,54 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
2220
2405
  }
2221
2406
  if (state.gatePasses.has(normalized.dueGateId)) {
2222
2407
  throw new Error(
2223
- `Deferred verification cannot target integration gate ${normalized.dueGateId}; gate already passed.`,
2408
+ `Deferred verification cannot target integration gate ${normalized.dueGateId}; gate already passed. `
2409
+ + "Create a successor project with: npx --no-install owlrunkit project successor --workspace <directory> --transition-id <id> --at <ISO-UTC> --definition <project.json> --reason <text>; or define a new unpassed gate before deferring.",
2410
+ );
2411
+ }
2412
+ } else if (normalized.type === "work_item_scope_revised") {
2413
+ const current = state.assignments.get(normalized.workItemId) ?? null;
2414
+ const currentRevision = state.scopeRevisions.get(normalized.workItemId) ?? null;
2415
+ const currentScope = currentRevision?.ownedPaths ?? workItem.ownedPaths;
2416
+ const currentMeasurable = currentRevision
2417
+ ? currentRevision.measurable
2418
+ : workItem.measurable ?? null;
2419
+ const projected = status.workItems.find(row => row.workItemId === normalized.workItemId);
2420
+ if (!current
2421
+ || current.assignmentId !== normalized.assignmentId
2422
+ || current.agentId !== normalized.ownerAgentId) {
2423
+ throw new Error("WorkItem scope revision must bind the current assignment and owner.");
2424
+ }
2425
+ if (["completed", "failed"].includes(projected?.status)) {
2426
+ throw new Error("WorkItem scope revision requires a non-terminal WorkItem.");
2427
+ }
2428
+ if (normalized.supersedesRevisionId !== currentRevision?.revisionId) {
2429
+ throw new Error(
2430
+ `WorkItem scope revision must supersede current revision ${currentRevision?.revisionId ?? "none"}.`,
2224
2431
  );
2225
2432
  }
2433
+ if (canonical(normalized.previousOwnedPaths) !== canonical(currentScope)
2434
+ || canonical(normalized.previousMeasurable) !== canonical(currentMeasurable)) {
2435
+ throw new Error("WorkItem scope revision does not bind the current scope.");
2436
+ }
2437
+ if (canonical(normalized.ownedPaths) === canonical(currentScope)
2438
+ && canonical(normalized.measurable) === canonical(currentMeasurable)) {
2439
+ throw new Error("WorkItem scope revision must change ownedPaths or measurable truth.");
2440
+ }
2441
+ for (const otherProjection of status.workItems) {
2442
+ if (otherProjection.workItemId === normalized.workItemId
2443
+ || otherProjection.assignmentId === null
2444
+ || ["completed", "failed"].includes(otherProjection.status)) continue;
2445
+ const otherRevision = state.scopeRevisions.get(otherProjection.workItemId) ?? null;
2446
+ const otherDefinition = effectiveDefinition.workItems.find(
2447
+ row => row.id === otherProjection.workItemId,
2448
+ );
2449
+ const otherOwnedPaths = otherRevision?.ownedPaths ?? otherDefinition.ownedPaths;
2450
+ if (pathsOverlap(normalized.ownedPaths, otherOwnedPaths)) {
2451
+ throw new Error(
2452
+ `Revised owned paths overlap active assignment ${otherProjection.assignmentId}: ${normalized.workItemId} <> ${otherProjection.workItemId}.`,
2453
+ );
2454
+ }
2455
+ }
2226
2456
  } else if (normalized.type === "verification_closed") {
2227
2457
  const verification = state.verifications.get(normalized.verificationId);
2228
2458
  if (!verification || verification.closed) {
@@ -2376,7 +2606,7 @@ export function assignTeamProjectV1({
2376
2606
  if (executionWorkItemId !== undefined) event.executionWorkItemId = executionWorkItemId;
2377
2607
 
2378
2608
  const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
2379
- const status = readTeamProjectStatusV1({ workspaceRoot });
2609
+ const status = readTeamProjectStatusV3({ workspaceRoot });
2380
2610
  if (recorded.resumed) {
2381
2611
  const current = status.workItems.find(row => row.workItemId === workItemId);
2382
2612
  if (current?.assignmentId !== assignmentId) {
@@ -3010,7 +3240,7 @@ export function integrateTeamProjectV1({
3010
3240
  evidenceRefs,
3011
3241
  };
3012
3242
  const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
3013
- const status = readTeamProjectStatusV1({ workspaceRoot });
3243
+ const status = readTeamProjectStatusV3({ workspaceRoot });
3014
3244
  const gate = status.integrationGates.find(row => row.gateId === gateId);
3015
3245
  return {
3016
3246
  status: "team_project_integration",
@@ -3021,6 +3251,7 @@ export function integrateTeamProjectV1({
3021
3251
  resumed: recorded.resumed,
3022
3252
  gateStatus: gate?.status ?? null,
3023
3253
  overall: status.overall,
3254
+ deliveryDisposition: status.deliveryDisposition,
3024
3255
  projectTruthHash: status.projectTruthHash,
3025
3256
  nextAction: status.nextAction,
3026
3257
  nextActorId: status.nextActorId,
@@ -3064,7 +3295,7 @@ export function checkpointTeamProjectV1({
3064
3295
  ...(sourceFingerprint === undefined ? {} : { sourceFingerprint }),
3065
3296
  };
3066
3297
  const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
3067
- const status = readTeamProjectStatusV1({ workspaceRoot });
3298
+ const status = readTeamProjectStatusV3({ workspaceRoot });
3068
3299
  if (recorded.resumed) {
3069
3300
  const loaded = loadProject(workspaceRoot);
3070
3301
  const stateProjection = projectEvents(loaded.definition, loaded.events);
@@ -3094,6 +3325,89 @@ export function checkpointTeamProjectV1({
3094
3325
  resumed: recorded.resumed,
3095
3326
  projectTruthHash: status.projectTruthHash,
3096
3327
  overall: status.overall,
3328
+ warnings: status.warnings.filter(row => row.workItemId === workItemId),
3329
+ nextAction: status.nextAction,
3330
+ nextActorId: status.nextActorId,
3331
+ authorizationGranted: false,
3332
+ exitCode: 0,
3333
+ };
3334
+ }
3335
+
3336
+ export function reviseTeamProjectWorkItemScopeV1({
3337
+ workspaceRoot,
3338
+ revisionId,
3339
+ occurredAt,
3340
+ assignmentId,
3341
+ workItemId,
3342
+ ownerAgentId,
3343
+ ownedPaths,
3344
+ measurable = null,
3345
+ reason,
3346
+ evidenceRefs = [],
3347
+ }) {
3348
+ safeIdentifier(revisionId, "revisionId");
3349
+ const loaded = loadProject(workspaceRoot);
3350
+ const state = projectEvents(loaded.definition, loaded.events);
3351
+ const baseWorkItem = loaded.definition.workItems.find(row => row.id === workItemId);
3352
+ if (!baseWorkItem) throw new Error(`Unknown team project WorkItem: ${workItemId}.`);
3353
+ const existing = loaded.events.find(row => (
3354
+ row.event.type === "work_item_scope_revised"
3355
+ && row.event.revisionId === revisionId
3356
+ ))?.event ?? null;
3357
+ let event;
3358
+ if (existing) {
3359
+ const requested = {
3360
+ occurredAt: normalizeTimestamp(occurredAt),
3361
+ assignmentId,
3362
+ workItemId,
3363
+ ownerAgentId,
3364
+ ownedPaths: validateLeaseOwnedPaths(ownedPaths),
3365
+ measurable,
3366
+ reason,
3367
+ evidenceRefs,
3368
+ };
3369
+ for (const key of Object.keys(requested)) {
3370
+ if (canonical(existing[key]) !== canonical(requested[key])) {
3371
+ throw new Error(`Cannot resume WorkItem scope revision ${revisionId}; ${key} differs.`);
3372
+ }
3373
+ }
3374
+ event = existing;
3375
+ } else {
3376
+ const previous = state.scopeRevisions.get(workItemId) ?? null;
3377
+ event = {
3378
+ schemaVersion: EVENT_SCHEMA_V5,
3379
+ eventId: `work-item-scope-revised-${revisionId}`,
3380
+ type: "work_item_scope_revised",
3381
+ occurredAt,
3382
+ revisionId,
3383
+ ...(previous ? { supersedesRevisionId: previous.revisionId } : {}),
3384
+ assignmentId,
3385
+ workItemId,
3386
+ ownerAgentId,
3387
+ previousOwnedPaths: [...(previous?.ownedPaths ?? baseWorkItem.ownedPaths)],
3388
+ ownedPaths,
3389
+ previousMeasurable: structuredClone(
3390
+ previous ? previous.measurable : baseWorkItem.measurable ?? null,
3391
+ ),
3392
+ measurable,
3393
+ reason,
3394
+ evidenceRefs,
3395
+ };
3396
+ }
3397
+ const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
3398
+ const status = readTeamProjectStatusV3({ workspaceRoot });
3399
+ const workItem = status.workItems.find(row => row.workItemId === workItemId);
3400
+ return {
3401
+ status: "team_project_work_item_scope_revised",
3402
+ revisionId,
3403
+ workItemId,
3404
+ ownedPaths: [...workItem.ownedPaths],
3405
+ measurable: structuredClone(workItem.measurable),
3406
+ eventId: recorded.eventId,
3407
+ eventPath: recorded.eventPath,
3408
+ eventSha256: recorded.eventSha256,
3409
+ resumed: recorded.resumed,
3410
+ projectTruthHash: status.projectTruthHash,
3097
3411
  nextAction: status.nextAction,
3098
3412
  nextActorId: status.nextActorId,
3099
3413
  authorizationGranted: false,
@@ -3106,14 +3420,59 @@ function legacyWorkItemProjection(workItem) {
3106
3420
  const {
3107
3421
  latestSourceDelivery: _latestSourceDelivery,
3108
3422
  invalidatedByReopenIds: _invalidatedByReopenIds,
3423
+ ownedPaths: _ownedPaths,
3424
+ measurable: _measurable,
3425
+ scopeRevision: _scopeRevision,
3109
3426
  ...legacy
3110
3427
  } = workItem;
3111
3428
  return legacy;
3112
3429
  }
3113
3430
 
3431
+ function statusV2WorkItemProjection(workItem) {
3432
+ if (workItem === null) return null;
3433
+ const {
3434
+ ownedPaths: _ownedPaths,
3435
+ measurable: _measurable,
3436
+ scopeRevision: _scopeRevision,
3437
+ ...v2
3438
+ } = workItem;
3439
+ return v2;
3440
+ }
3441
+
3442
+ function statusV3CompletionProjection(status) {
3443
+ if (status.overall !== "completed") {
3444
+ return { ...status, deliveryDisposition: "work_in_progress" };
3445
+ }
3446
+ const dominantGap = {
3447
+ kind: "release_authority",
3448
+ id: "release-authority",
3449
+ title: "Separate release authority",
3450
+ workItemId: null,
3451
+ agentId: null,
3452
+ reason: "Project work ledger is complete, but Git, release, deployment, production, and business authority remain outside Project Driver.",
3453
+ truthRefs: uniqueSorted([
3454
+ "project/definition.json",
3455
+ ...status.integrationGates.flatMap(gate => gate.truthRefs),
3456
+ ]),
3457
+ };
3458
+ const nextAction = "Obtain separate Git/release/deployment authority if activation is intended; otherwise stop.";
3459
+ return {
3460
+ ...status,
3461
+ deliveryDisposition: "code_complete_without_release_authority",
3462
+ headline: `code_complete_without_release_authority: work ledger completed; authorizationGranted=false; next: ${nextAction}`,
3463
+ dominantGap,
3464
+ nextAction,
3465
+ nextActorId: null,
3466
+ };
3467
+ }
3468
+
3114
3469
  export function readTeamProjectStatusV1({ workspaceRoot }) {
3115
3470
  const loaded = loadProject(workspaceRoot);
3116
- const { sourceDeliveries: _sourceDeliveries, ...legacy } = projectStatus(
3471
+ const {
3472
+ sourceDeliveries: _sourceDeliveries,
3473
+ warnings: _warnings,
3474
+ ...legacy
3475
+ } = projectStatus(
3117
3476
  loaded.definition,
3118
3477
  loaded.events,
3119
3478
  loaded.root,
@@ -3128,8 +3487,27 @@ export function readTeamProjectStatusV1({ workspaceRoot }) {
3128
3487
 
3129
3488
  export function readTeamProjectStatusV2({ workspaceRoot }) {
3130
3489
  const loaded = loadProject(workspaceRoot);
3490
+ const { warnings: _warnings, ...v2 } = projectStatus(
3491
+ loaded.definition,
3492
+ loaded.events,
3493
+ loaded.root,
3494
+ );
3131
3495
  return {
3132
- ...projectStatus(loaded.definition, loaded.events, loaded.root),
3496
+ ...v2,
3497
+ schemaVersion: STATUS_SCHEMA_V2,
3498
+ workItems: v2.workItems.map(statusV2WorkItemProjection),
3499
+ exitCode: 0,
3500
+ };
3501
+ }
3502
+
3503
+ export function readTeamProjectStatusV3({ workspaceRoot }) {
3504
+ const loaded = loadProject(workspaceRoot);
3505
+ return {
3506
+ ...statusV3CompletionProjection(projectStatus(
3507
+ loaded.definition,
3508
+ loaded.events,
3509
+ loaded.root,
3510
+ )),
3133
3511
  exitCode: 0,
3134
3512
  };
3135
3513
  }
@@ -3307,8 +3685,8 @@ export function buildTeamProjectTakeoverV2({ workspaceRoot, agentId }) {
3307
3685
  projectNextAction: status.nextAction,
3308
3686
  projectNextActorId: status.nextActorId,
3309
3687
  agentId,
3310
- currentResponsibility: current,
3311
- allResponsibilities: responsibilities,
3688
+ currentResponsibility: statusV2WorkItemProjection(current),
3689
+ allResponsibilities: responsibilities.map(statusV2WorkItemProjection),
3312
3690
  pendingHandoff,
3313
3691
  staleHandoffs,
3314
3692
  currentSourceDelivery,
@@ -3356,6 +3734,29 @@ export function buildTeamProjectTakeoverV1(input) {
3356
3734
  };
3357
3735
  }
3358
3736
 
3737
+ export function buildTeamProjectTakeoverV3(input) {
3738
+ const legacy = buildTeamProjectTakeoverV2(input);
3739
+ const status = readTeamProjectStatusV3({ workspaceRoot: input.workspaceRoot });
3740
+ const byId = new Map(status.workItems.map(row => [row.workItemId, row]));
3741
+ return {
3742
+ ...legacy,
3743
+ schemaVersion: "OwlCodaRunKitTeamProjectTakeoverV3",
3744
+ projectHeadline: status.headline,
3745
+ dominantGap: status.dominantGap,
3746
+ projectNextAction: status.nextAction,
3747
+ projectNextActorId: status.nextActorId,
3748
+ currentResponsibility: legacy.currentResponsibility
3749
+ ? byId.get(legacy.currentResponsibility.workItemId) ?? null
3750
+ : null,
3751
+ allResponsibilities: legacy.allResponsibilities
3752
+ .map(row => byId.get(row.workItemId))
3753
+ .filter(Boolean),
3754
+ deliveryDisposition: status.deliveryDisposition,
3755
+ warnings: status.warnings,
3756
+ authorizationGranted: false,
3757
+ };
3758
+ }
3759
+
3359
3760
  export function formatTeamProjectStatusHumanV1(status) {
3360
3761
  const nextAction = status.nextAction === null ? "none" : singleLine(status.nextAction);
3361
3762
  return [
@@ -3382,7 +3783,9 @@ export function formatTeamProjectStatusHumanV2(status) {
3382
3783
  return [
3383
3784
  status.headline,
3384
3785
  `Project: ${status.projectId} — ${singleLine(status.objective)}`,
3385
- `State: ${status.overall}`,
3786
+ `Work ledger: ${status.overall}${status.deliveryDisposition === "code_complete_without_release_authority"
3787
+ ? `; delivery: ${status.deliveryDisposition}`
3788
+ : ""}`,
3386
3789
  `Work: ${status.counts.completed}/${status.counts.total} completed; ${status.counts.active} active; ${status.counts.waiting_dependency + status.counts.waiting_decision} waiting`,
3387
3790
  `Agents: ${status.agents.map(agent => agent.agentId).join(", ") || "none assigned"}`,
3388
3791
  `Open decisions: ${status.openDecisions.map(row => row.decisionId).join(", ") || "none"}`,
@@ -3395,6 +3798,9 @@ export function formatTeamProjectStatusHumanV2(status) {
3395
3798
  `Source deliveries: ${status.sourceDeliveries.length}${latestSourceDelivery
3396
3799
  ? `; latest ${latestSourceDelivery.deliveryId} (${latestSourceDelivery.overlayStatus}, ${latestSourceDelivery.deliveryCandidateFingerprint})`
3397
3800
  : ""}`,
3801
+ ...(status.warnings?.length > 0
3802
+ ? [`Warnings: ${status.warnings.map(row => `${row.code}:${row.workItemId}`).join(", ")}`]
3803
+ : []),
3398
3804
  `Next: ${nextAction}${status.nextActorId ? ` (owner ${status.nextActorId})` : ""}`,
3399
3805
  "",
3400
3806
  ].join("\n");
@@ -3411,6 +3817,9 @@ export function formatTeamProjectTakeoverHumanV2(takeover) {
3411
3817
  return [
3412
3818
  takeover.projectHeadline,
3413
3819
  `Project: ${takeover.projectId} — ${singleLine(takeover.objective)}`,
3820
+ ...(takeover.deliveryDisposition
3821
+ ? [`Delivery disposition: ${takeover.deliveryDisposition}`]
3822
+ : []),
3414
3823
  `Agent: ${takeover.agentId}`,
3415
3824
  `Responsibility: ${responsibility?.workItemId ?? "none"}`,
3416
3825
  `Open decisions: ${takeover.unresolvedDecisions.map(row => row.decisionId).join(", ") || "none"}`,
@@ -3431,6 +3840,9 @@ export function formatTeamProjectTakeoverHumanV2(takeover) {
3431
3840
  `Agent next: ${agentNextAction}`,
3432
3841
  `Project next: ${projectNextAction}${takeover.projectNextActorId ? ` (owner ${takeover.projectNextActorId})` : ""}`,
3433
3842
  `State: ${takeover.overall}`,
3843
+ ...(takeover.warnings?.length > 0
3844
+ ? [`Warnings: ${takeover.warnings.map(row => `${row.code}:${row.workItemId}`).join(", ")}`]
3845
+ : []),
3434
3846
  "",
3435
3847
  ].join("\n");
3436
3848
  }