owlrunkit 0.19.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +94 -8
  3. package/docs/architecture/OWLCODA_RUNKIT_0201_RELEASE_TRUTH_AND_PROFILE_NORMALIZATION_PATCH_DECISION_20260813.md +95 -0
  4. package/docs/architecture/OWLCODA_RUNKIT_020_TEAM_DELIVERY_PHASE1_PRODUCT_DECISION_20260813.md +296 -0
  5. package/docs/architecture/OWLCODA_RUNKIT_021_TEAM_DELIVERY_PHASE2_PRODUCT_DECISION_20260813.md +249 -0
  6. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +114 -12
  7. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  8. package/package.json +1 -1
  9. package/packages/attest/README.md +7 -1
  10. package/packages/attest/consumer-surface-v3.json +275 -0
  11. package/packages/attest/distribution-manifest-v1.json +19 -0
  12. package/packages/attest/schemas/canonical-candidate-identity-v1.schema.json +112 -0
  13. package/packages/attest/schemas/foreign-workspace-filesystem-identity-v1.schema.json +32 -0
  14. package/packages/attest/schemas/quick-verification-receipt-v4.schema.json +226 -0
  15. package/packages/attest/src/candidate-identity.mjs +454 -0
  16. package/packages/attest/src/index.mjs +11 -0
  17. package/packages/attest/src/quick-receipt-contract.mjs +126 -5
  18. package/packages/attest/src/quick.mjs +48 -8
  19. package/{registry-release-policy-v1.json → registry-release-policy-v2.json} +8 -9
  20. package/sbom.spdx.json +4 -4
  21. package/scripts/runkit-contract/core-contract.mjs +21 -1
  22. package/scripts/runkit-contract/fleet-skill-plan.mjs +223 -0
  23. package/scripts/runkit-contract/formal-workflow.mjs +3 -1
  24. package/scripts/runkit-contract/inspect-presentation.mjs +56 -0
  25. package/scripts/runkit-contract/install-codex-skill.mjs +1 -0
  26. package/scripts/runkit-contract/onboarding-bootstrap.mjs +15 -0
  27. package/scripts/runkit-contract/onboarding-doctor.mjs +201 -7
  28. package/scripts/runkit-contract/profile-onboarding.mjs +6 -0
  29. package/scripts/runkit-contract/project-cli-resolver.mjs +12 -1
  30. package/scripts/runkit-contract/quick-metrics.mjs +9 -5
  31. package/scripts/runkit-contract/quick-verify.mjs +108 -24
  32. package/scripts/runkit-contract/registry-adoption-gate.mjs +12 -3
  33. package/scripts/runkit-contract/registry-adoption.mjs +10 -5
  34. package/scripts/runkit-contract/runkit-cli.mjs +367 -20
  35. package/scripts/runkit-contract/schemas/candidate-transfer-packet-v1.schema.json +98 -0
  36. package/scripts/runkit-contract/schemas/delivery-lifecycle-v1.schema.json +91 -0
  37. package/scripts/runkit-contract/schemas/external-gate-v1.schema.json +68 -0
  38. package/scripts/runkit-contract/schemas/fleet-skill-upgrade-plan-v1.schema.json +140 -0
  39. package/scripts/runkit-contract/schemas/inspect-compact-v1.schema.json +93 -0
  40. package/scripts/runkit-contract/schemas/team-acceptance-packet-v2.schema.json +109 -0
  41. package/scripts/runkit-contract/schemas/team-delivery-status-v1.schema.json +72 -0
  42. package/scripts/runkit-contract/schemas/team-delivery-status-v2.schema.json +219 -0
  43. package/scripts/runkit-contract/schemas/team-packet-validation-v1.schema.json +33 -0
  44. package/scripts/runkit-contract/schemas/team-project-event-v6.schema.json +82 -0
  45. package/scripts/runkit-contract/schemas/team-project-event-v7.schema.json +80 -0
  46. package/scripts/runkit-contract/schemas/team-project-status-v4.schema.json +208 -0
  47. package/scripts/runkit-contract/schemas/team-recommendation-v1.schema.json +121 -0
  48. package/scripts/runkit-contract/schemas/team-task-packet-v2.schema.json +228 -0
  49. package/scripts/runkit-contract/sync-release-identity.mjs +87 -0
  50. package/scripts/runkit-contract/team-delivery.mjs +1031 -0
  51. package/scripts/runkit-contract/team-project.mjs +970 -9
@@ -17,6 +17,8 @@ import {
17
17
  quickSnapshotFingerprintValid,
18
18
  validWorkspaceSnapshotShape,
19
19
  } from "../../packages/attest/src/quick-receipt-contract.mjs";
20
+ import { readFileBytesBounded } from "../../packages/attest/src/formal.mjs";
21
+ import { attestCoreBoundQuickReceiptDetailsFromBytes } from "./quick-attest.mjs";
20
22
  import {
21
23
  assertAllowedKeys,
22
24
  isRecord,
@@ -32,9 +34,12 @@ const EVENT_SCHEMA_V2 = "OwlCodaRunKitTeamProjectEventV2";
32
34
  const EVENT_SCHEMA_V3 = "OwlCodaRunKitTeamProjectEventV3";
33
35
  const EVENT_SCHEMA_V4 = "OwlCodaRunKitTeamProjectEventV4";
34
36
  const EVENT_SCHEMA_V5 = "OwlCodaRunKitTeamProjectEventV5";
37
+ const EVENT_SCHEMA_V6 = "OwlCodaRunKitTeamProjectEventV6";
38
+ const EVENT_SCHEMA_V7 = "OwlCodaRunKitTeamProjectEventV7";
35
39
  const STATUS_SCHEMA_V1 = "OwlCodaRunKitTeamProjectStatusV1";
36
40
  const STATUS_SCHEMA_V2 = "OwlCodaRunKitTeamProjectStatusV2";
37
41
  const STATUS_SCHEMA_V3 = "OwlCodaRunKitTeamProjectStatusV3";
42
+ const STATUS_SCHEMA_V4 = "OwlCodaRunKitTeamProjectStatusV4";
38
43
 
39
44
  const DEFINITION_KEYS = [
40
45
  "schemaVersion",
@@ -55,6 +60,31 @@ const WORK_ITEM_STATES = new Set([
55
60
  "failed",
56
61
  ]);
57
62
  const VERIFICATION_DISPOSITIONS = new Set(["verified", "no_longer_required"]);
63
+ const DELIVERY_STAGES = Object.freeze([
64
+ "source",
65
+ "integration",
66
+ "materialization",
67
+ "deployment",
68
+ "live_readback",
69
+ "product_acceptance",
70
+ ]);
71
+ const DELIVERY_STAGE_OUTCOMES = Object.freeze({
72
+ source: new Set(["accepted", "rejected"]),
73
+ integration: new Set(["integrated", "rejected"]),
74
+ materialization: new Set(["materialized", "failed"]),
75
+ deployment: new Set(["deployed", "failed", "rolled_back"]),
76
+ live_readback: new Set(["passed", "failed"]),
77
+ product_acceptance: new Set(["accepted", "rejected", "deferred"]),
78
+ });
79
+ const DELIVERY_POSITIVE_OUTCOME = Object.freeze({
80
+ source: "accepted",
81
+ integration: "integrated",
82
+ materialization: "materialized",
83
+ deployment: "deployed",
84
+ live_readback: "passed",
85
+ product_acceptance: "accepted",
86
+ });
87
+ const EXTERNAL_GATE_STATUSES = new Set(["pending", "passed", "blocked", "deferred"]);
58
88
  const EVENT_KEYS = Object.freeze({
59
89
  agent_assigned: [
60
90
  "schemaVersion", "eventId", "type", "occurredAt", "assignmentId",
@@ -132,6 +162,28 @@ const EVENT_KEYS = Object.freeze({
132
162
  "previousOwnedPaths", "ownedPaths", "previousMeasurable", "measurable",
133
163
  "reason", "evidenceRefs",
134
164
  ],
165
+ external_gate_reconciled: [
166
+ "schemaVersion", "eventId", "type", "occurredAt", "reconciliationId",
167
+ "supersedesReconciliationId", "gateId", "title", "requiredFor",
168
+ "gateStatus", "ownerAgentId", "sourceAdapterId", "sourceRef",
169
+ "sourceSha256", "summary", "evidenceRefs", "authorizationGranted",
170
+ ],
171
+ delivery_lifecycle_recorded: [
172
+ "schemaVersion", "eventId", "type", "occurredAt", "recordId",
173
+ "supersedesRecordId", "stage", "stageStatus", "ownerAgentId",
174
+ "summary", "evidenceRefs", "externalGateIds", "authorizationGranted",
175
+ ],
176
+ work_item_evidence_requirement_set: [
177
+ "schemaVersion", "eventId", "type", "occurredAt", "policyId",
178
+ "supersedesPolicyId", "workItemId", "ownerAgentId", "requirement",
179
+ "reason", "evidenceRefs", "authorizationGranted",
180
+ ],
181
+ checkpoint_receipt_bound: [
182
+ "schemaVersion", "eventId", "type", "occurredAt", "checkpointId",
183
+ "assignmentId", "workItemId", "state", "summary", "completedUnits",
184
+ "evidenceRefs", "blockerRefs", "decisionRefs", "nextAction",
185
+ "sourceFingerprint", "receiptBinding", "authorizationGranted",
186
+ ],
135
187
  });
136
188
 
137
189
  function canonical(value) {
@@ -672,10 +724,10 @@ function validateEventShape(value, definition) {
672
724
  throw new Error("Team project event type is invalid.");
673
725
  }
674
726
  assertAllowedKeys(value, `Team project event ${value.eventId ?? "unknown"}`, EVENT_KEYS[value.type]);
675
- if (![EVENT_SCHEMA, EVENT_SCHEMA_V2, EVENT_SCHEMA_V3, EVENT_SCHEMA_V4, EVENT_SCHEMA_V5]
727
+ if (![EVENT_SCHEMA, EVENT_SCHEMA_V2, EVENT_SCHEMA_V3, EVENT_SCHEMA_V4, EVENT_SCHEMA_V5, EVENT_SCHEMA_V6, EVENT_SCHEMA_V7]
676
728
  .includes(value.schemaVersion)) {
677
729
  throw new Error(
678
- `Team project event schemaVersion must be ${EVENT_SCHEMA}, ${EVENT_SCHEMA_V2}, ${EVENT_SCHEMA_V3}, ${EVENT_SCHEMA_V4}, or ${EVENT_SCHEMA_V5}.`,
730
+ `Team project event schemaVersion must be ${EVENT_SCHEMA}, ${EVENT_SCHEMA_V2}, ${EVENT_SCHEMA_V3}, ${EVENT_SCHEMA_V4}, ${EVENT_SCHEMA_V5}, ${EVENT_SCHEMA_V6}, or ${EVENT_SCHEMA_V7}.`,
679
731
  );
680
732
  }
681
733
  if (value.type === "rework_returned" && value.schemaVersion !== EVENT_SCHEMA_V2) {
@@ -702,6 +754,27 @@ function validateEventShape(value, definition) {
702
754
  if (value.schemaVersion === EVENT_SCHEMA_V5 && value.type !== "work_item_scope_revised") {
703
755
  throw new Error(`${EVENT_SCHEMA_V5} is reserved for work_item_scope_revised events.`);
704
756
  }
757
+ const eventV6Types = new Set(["external_gate_reconciled", "delivery_lifecycle_recorded"]);
758
+ if (eventV6Types.has(value.type) && value.schemaVersion !== EVENT_SCHEMA_V6) {
759
+ throw new Error(`${value.type} requires schemaVersion ${EVENT_SCHEMA_V6}.`);
760
+ }
761
+ if (value.schemaVersion === EVENT_SCHEMA_V6 && !eventV6Types.has(value.type)) {
762
+ throw new Error(
763
+ `${EVENT_SCHEMA_V6} is reserved for external Gate and delivery lifecycle events.`,
764
+ );
765
+ }
766
+ const eventV7Types = new Set([
767
+ "work_item_evidence_requirement_set",
768
+ "checkpoint_receipt_bound",
769
+ ]);
770
+ if (eventV7Types.has(value.type) && value.schemaVersion !== EVENT_SCHEMA_V7) {
771
+ throw new Error(`${value.type} requires schemaVersion ${EVENT_SCHEMA_V7}.`);
772
+ }
773
+ if (value.schemaVersion === EVENT_SCHEMA_V7 && !eventV7Types.has(value.type)) {
774
+ throw new Error(
775
+ `${EVENT_SCHEMA_V7} is reserved for WorkItem evidence requirements and receipt binding.`,
776
+ );
777
+ }
705
778
  safeIdentifier(value.eventId, "eventId");
706
779
  const normalizedOccurredAt = normalizeTimestamp(value.occurredAt);
707
780
  const workItemIds = new Set(definition.workItems.map(row => row.id));
@@ -720,6 +793,8 @@ function validateEventShape(value, definition) {
720
793
  "responsibility_transferred",
721
794
  "work_item_scope_revised",
722
795
  "verification_deferred",
796
+ "work_item_evidence_requirement_set",
797
+ "checkpoint_receipt_bound",
723
798
  ].includes(value.type)) {
724
799
  assertWorkItem(value.workItemId);
725
800
  }
@@ -732,6 +807,7 @@ function validateEventShape(value, definition) {
732
807
  "upstream_reopened",
733
808
  "responsibility_transferred",
734
809
  "work_item_scope_revised",
810
+ "checkpoint_receipt_bound",
735
811
  ].includes(value.type)) {
736
812
  safeIdentifier(value.assignmentId, "assignmentId");
737
813
  }
@@ -1015,6 +1091,107 @@ function validateEventShape(value, definition) {
1015
1091
  previousMeasurable,
1016
1092
  measurable,
1017
1093
  };
1094
+ } else if (value.type === "external_gate_reconciled") {
1095
+ safeIdentifier(value.reconciliationId, "reconciliationId");
1096
+ if (value.supersedesReconciliationId !== undefined) {
1097
+ safeIdentifier(value.supersedesReconciliationId, "supersedesReconciliationId");
1098
+ }
1099
+ safeIdentifier(value.gateId, "external gateId");
1100
+ nonEmptyString(value.title, "External Gate title");
1101
+ if (!DELIVERY_STAGES.includes(value.requiredFor)) {
1102
+ throw new Error("External Gate requiredFor must be a delivery stage.");
1103
+ }
1104
+ if (!EXTERNAL_GATE_STATUSES.has(value.gateStatus)) {
1105
+ throw new Error("External Gate status is invalid.");
1106
+ }
1107
+ safeIdentifier(value.ownerAgentId, "External Gate ownerAgentId");
1108
+ safeIdentifier(value.sourceAdapterId, "External Gate sourceAdapterId");
1109
+ nonEmptyString(value.sourceRef, "External Gate sourceRef");
1110
+ sha256Ref(value.sourceSha256, "External Gate sourceSha256");
1111
+ nonEmptyString(value.summary, "External Gate summary");
1112
+ stringArray(value.evidenceRefs, "External Gate evidenceRefs", { allowEmpty: false });
1113
+ if (value.authorizationGranted !== false) {
1114
+ throw new Error("External Gate authorizationGranted must remain false.");
1115
+ }
1116
+ } else if (value.type === "delivery_lifecycle_recorded") {
1117
+ safeIdentifier(value.recordId, "delivery recordId");
1118
+ if (value.supersedesRecordId !== undefined) {
1119
+ safeIdentifier(value.supersedesRecordId, "supersedesRecordId");
1120
+ }
1121
+ if (!DELIVERY_STAGES.includes(value.stage)) {
1122
+ throw new Error("Delivery lifecycle stage is invalid.");
1123
+ }
1124
+ if (!DELIVERY_STAGE_OUTCOMES[value.stage].has(value.stageStatus)) {
1125
+ throw new Error(`Delivery lifecycle outcome is invalid for ${value.stage}.`);
1126
+ }
1127
+ safeIdentifier(value.ownerAgentId, "Delivery lifecycle ownerAgentId");
1128
+ nonEmptyString(value.summary, "Delivery lifecycle summary");
1129
+ stringArray(
1130
+ value.evidenceRefs,
1131
+ "Delivery lifecycle evidenceRefs",
1132
+ { allowEmpty: false },
1133
+ );
1134
+ const gateIdsForRecord = stringArray(
1135
+ value.externalGateIds,
1136
+ "Delivery lifecycle externalGateIds",
1137
+ );
1138
+ for (const gateId of gateIdsForRecord) safeIdentifier(gateId, "externalGateId");
1139
+ if (value.authorizationGranted !== false) {
1140
+ throw new Error("Delivery lifecycle authorizationGranted must remain false.");
1141
+ }
1142
+ } else if (value.type === "work_item_evidence_requirement_set") {
1143
+ safeIdentifier(value.policyId, "evidence policyId");
1144
+ if (value.supersedesPolicyId !== undefined) {
1145
+ safeIdentifier(value.supersedesPolicyId, "supersedesPolicyId");
1146
+ }
1147
+ safeIdentifier(value.ownerAgentId, "Evidence requirement ownerAgentId");
1148
+ if (!new Set(["none", "quick"]).has(value.requirement)) {
1149
+ throw new Error("WorkItem evidence requirement is invalid.");
1150
+ }
1151
+ nonEmptyString(value.reason, "WorkItem evidence requirement reason");
1152
+ stringArray(
1153
+ value.evidenceRefs,
1154
+ "WorkItem evidence requirement evidenceRefs",
1155
+ { allowEmpty: false },
1156
+ );
1157
+ if (value.authorizationGranted !== false) {
1158
+ throw new Error("WorkItem evidence requirement authorizationGranted must remain false.");
1159
+ }
1160
+ } else if (value.type === "checkpoint_receipt_bound") {
1161
+ safeIdentifier(value.checkpointId, "checkpointId");
1162
+ if (!WORK_ITEM_STATES.has(value.state)) throw new Error("Checkpoint state is invalid.");
1163
+ nonEmptyString(value.summary, "Checkpoint summary");
1164
+ stringArray(value.evidenceRefs, "Checkpoint evidenceRefs");
1165
+ stringArray(value.blockerRefs, "Checkpoint blockerRefs");
1166
+ stringArray(value.decisionRefs, "Checkpoint decisionRefs");
1167
+ nullableString(value.nextAction, "Checkpoint nextAction");
1168
+ if (value.sourceFingerprint !== undefined
1169
+ && !/^(?:sha256:)?[a-f0-9]{64}$/u.test(value.sourceFingerprint)) {
1170
+ throw new Error("Checkpoint sourceFingerprint is invalid.");
1171
+ }
1172
+ if (value.completedUnits !== undefined
1173
+ && (!Number.isSafeInteger(value.completedUnits) || value.completedUnits < 0)) {
1174
+ throw new Error("Checkpoint completedUnits must be a non-negative integer.");
1175
+ }
1176
+ if (!isRecord(value.receiptBinding)) {
1177
+ throw new Error("Checkpoint receiptBinding must be an object.");
1178
+ }
1179
+ assertAllowedKeys(value.receiptBinding, "Checkpoint receiptBinding", [
1180
+ "kind", "receiptId", "receiptRef", "receiptSha256",
1181
+ "sourceFingerprint", "attestationDecision", "issueCodes",
1182
+ ]);
1183
+ if (value.receiptBinding.kind !== "quick"
1184
+ || value.receiptBinding.attestationDecision !== "GO") {
1185
+ throw new Error("Checkpoint receiptBinding must be one GO Quick receipt.");
1186
+ }
1187
+ safeIdentifier(value.receiptBinding.receiptId, "Quick receiptId");
1188
+ safeSourcePath(value.receiptBinding.receiptRef, "Quick receiptRef");
1189
+ sha256Ref(value.receiptBinding.receiptSha256, "Quick receiptSha256");
1190
+ sha256Ref(value.receiptBinding.sourceFingerprint, "Quick sourceFingerprint");
1191
+ stringArray(value.receiptBinding.issueCodes, "Quick attestation issueCodes");
1192
+ if (value.authorizationGranted !== false) {
1193
+ throw new Error("Receipt-bound checkpoint authorizationGranted must remain false.");
1194
+ }
1018
1195
  }
1019
1196
  return { ...structuredClone(value), occurredAt: normalizedOccurredAt };
1020
1197
  }
@@ -1034,6 +1211,10 @@ function projectEvents(definition, records) {
1034
1211
  const sourceDeliveries = [];
1035
1212
  const scopeRevisions = new Map();
1036
1213
  const scopeRevisionHistory = new Map(definition.workItems.map(row => [row.id, []]));
1214
+ const externalGates = new Map();
1215
+ const deliveryRecords = new Map();
1216
+ const evidenceRequirements = new Map();
1217
+ const receiptBindings = new Map();
1037
1218
  const seenAssignments = new Set();
1038
1219
  const seenReworks = new Set();
1039
1220
  const seenReopens = new Set();
@@ -1041,6 +1222,10 @@ function projectEvents(definition, records) {
1041
1222
  const seenDecisions = new Set();
1042
1223
  const seenDeliveries = new Set();
1043
1224
  const seenScopeRevisions = new Set();
1225
+ const seenExternalGateReconciliations = new Set();
1226
+ const seenDeliveryRecords = new Set();
1227
+ const seenEvidencePolicies = new Set();
1228
+ const seenReceiptBindings = new Set();
1044
1229
  const seenFactIds = new Set(records.map(row => row.event.eventId));
1045
1230
 
1046
1231
  for (const { event, ref } of records) {
@@ -1347,6 +1532,130 @@ function projectEvents(definition, records) {
1347
1532
  assignments.set(event.workItemId, assignment);
1348
1533
  assignmentHistory.get(event.workItemId).push(assignment);
1349
1534
  transfers.set(event.workItemId, { ...event, ref });
1535
+ } else if (event.type === "external_gate_reconciled") {
1536
+ if (seenExternalGateReconciliations.has(event.reconciliationId)) {
1537
+ throw new Error(`Duplicate external Gate reconciliation ${event.reconciliationId}.`);
1538
+ }
1539
+ const previous = externalGates.get(event.gateId) ?? null;
1540
+ if (event.supersedesReconciliationId !== previous?.reconciliationId) {
1541
+ throw new Error(
1542
+ `External Gate ${event.gateId} must supersede current reconciliation `
1543
+ + `${previous?.reconciliationId ?? "none"}.`,
1544
+ );
1545
+ }
1546
+ if (previous && previous.requiredFor !== event.requiredFor) {
1547
+ throw new Error(
1548
+ `External Gate ${event.gateId} cannot change required delivery stage.`,
1549
+ );
1550
+ }
1551
+ seenExternalGateReconciliations.add(event.reconciliationId);
1552
+ externalGates.set(event.gateId, { ...event, ref });
1553
+ } else if (event.type === "delivery_lifecycle_recorded") {
1554
+ if (seenDeliveryRecords.has(event.recordId)) {
1555
+ throw new Error(`Duplicate delivery lifecycle record ${event.recordId}.`);
1556
+ }
1557
+ const previous = deliveryRecords.get(event.stage) ?? null;
1558
+ if (event.supersedesRecordId !== previous?.recordId) {
1559
+ throw new Error(
1560
+ `Delivery stage ${event.stage} must supersede current record `
1561
+ + `${previous?.recordId ?? "none"}.`,
1562
+ );
1563
+ }
1564
+ const stageIndex = DELIVERY_STAGES.indexOf(event.stage);
1565
+ const downstream = DELIVERY_STAGES.slice(stageIndex + 1)
1566
+ .find(stage => deliveryRecords.has(stage));
1567
+ if (downstream) {
1568
+ throw new Error(
1569
+ `Delivery stage ${event.stage} cannot change after downstream delivery truth already exists.`,
1570
+ );
1571
+ }
1572
+ const decisionsResolved = [...decisions.values()].every(row => row.resolved !== null);
1573
+ const stateSnapshot = {
1574
+ checkpoints,
1575
+ reopens,
1576
+ scopeRevisions,
1577
+ };
1578
+ if (completedWorkItemIds(definition, stateSnapshot).size !== definition.workItems.length
1579
+ || gatePasses.size !== definition.integrationGates.length
1580
+ || !decisionsResolved) {
1581
+ throw new Error("Delivery lifecycle requires a completed Project Driver work ledger.");
1582
+ }
1583
+ if (stageIndex > 0) {
1584
+ const prerequisiteStage = DELIVERY_STAGES[stageIndex - 1];
1585
+ const prerequisite = deliveryRecords.get(prerequisiteStage) ?? null;
1586
+ const requiredOutcome = DELIVERY_POSITIVE_OUTCOME[prerequisiteStage];
1587
+ if (prerequisite?.stageStatus !== requiredOutcome) {
1588
+ throw new Error(
1589
+ `Delivery stage ${event.stage} requires ${prerequisiteStage}=${requiredOutcome}.`,
1590
+ );
1591
+ }
1592
+ }
1593
+ const requiredGates = [...externalGates.values()]
1594
+ .filter(gate => gate.requiredFor === event.stage)
1595
+ .sort((left, right) => compareIdentifiers(left.gateId, right.gateId));
1596
+ const unresolvedGate = requiredGates.find(gate => gate.gateStatus !== "passed");
1597
+ if (unresolvedGate) {
1598
+ throw new Error(
1599
+ `Delivery stage ${event.stage} external Gate ${unresolvedGate.gateId} is `
1600
+ + `${unresolvedGate.gateStatus}.`,
1601
+ );
1602
+ }
1603
+ const requiredGateIds = requiredGates.map(gate => gate.gateId);
1604
+ if (canonical(event.externalGateIds) !== canonical(requiredGateIds)) {
1605
+ throw new Error(
1606
+ `Delivery stage ${event.stage} must bind passed external Gates: `
1607
+ + `${requiredGateIds.join(", ") || "none"}.`,
1608
+ );
1609
+ }
1610
+ seenDeliveryRecords.add(event.recordId);
1611
+ deliveryRecords.set(event.stage, { ...event, ref });
1612
+ } else if (event.type === "work_item_evidence_requirement_set") {
1613
+ if (seenEvidencePolicies.has(event.policyId)) {
1614
+ throw new Error(`Duplicate WorkItem evidence policy ${event.policyId}.`);
1615
+ }
1616
+ const current = evidenceRequirements.get(event.workItemId) ?? null;
1617
+ if (event.supersedesPolicyId !== current?.policyId) {
1618
+ throw new Error(
1619
+ `WorkItem ${event.workItemId} evidence policy must supersede `
1620
+ + `${current?.policyId ?? "none"}.`,
1621
+ );
1622
+ }
1623
+ const assignment = assignments.get(event.workItemId) ?? null;
1624
+ if (assignment?.agentId !== event.ownerAgentId) {
1625
+ throw new Error("WorkItem evidence policy must bind the current owner.");
1626
+ }
1627
+ const checkpoint = checkpoints.get(event.workItemId) ?? null;
1628
+ if (checkpoint && ["completed", "failed"].includes(checkpoint.state)) {
1629
+ throw new Error("WorkItem evidence policy cannot change terminal WorkItem truth.");
1630
+ }
1631
+ seenEvidencePolicies.add(event.policyId);
1632
+ evidenceRequirements.set(event.workItemId, { ...event, ref });
1633
+ } else if (event.type === "checkpoint_receipt_bound") {
1634
+ if (seenReceiptBindings.has(event.checkpointId)) {
1635
+ throw new Error(`Duplicate receipt-bound checkpoint ${event.checkpointId}.`);
1636
+ }
1637
+ const current = assignments.get(event.workItemId) ?? null;
1638
+ if (current?.assignmentId !== event.assignmentId) {
1639
+ throw new Error("Receipt-bound checkpoint must bind the current assignment.");
1640
+ }
1641
+ seenReceiptBindings.add(event.checkpointId);
1642
+ const checkpoint = {
1643
+ ...event,
1644
+ type: "checkpoint_recorded",
1645
+ evidenceRefs: uniqueSorted([
1646
+ ...event.evidenceRefs,
1647
+ event.receiptBinding.receiptRef,
1648
+ ]),
1649
+ ref,
1650
+ };
1651
+ checkpoints.set(event.workItemId, checkpoint);
1652
+ receiptBindings.set(event.workItemId, {
1653
+ ...structuredClone(event.receiptBinding),
1654
+ checkpointId: event.checkpointId,
1655
+ assignmentId: event.assignmentId,
1656
+ occurredAt: event.occurredAt,
1657
+ truthRef: ref,
1658
+ });
1350
1659
  }
1351
1660
  }
1352
1661
  return {
@@ -1364,6 +1673,10 @@ function projectEvents(definition, records) {
1364
1673
  sourceDeliveries,
1365
1674
  scopeRevisions,
1366
1675
  scopeRevisionHistory,
1676
+ externalGates,
1677
+ deliveryRecords,
1678
+ evidenceRequirements,
1679
+ receiptBindings,
1367
1680
  };
1368
1681
  }
1369
1682
 
@@ -1749,6 +2062,103 @@ function verificationReceiptWarning(workItem) {
1749
2062
  };
1750
2063
  }
1751
2064
 
2065
+ function externalGateProjection(gate) {
2066
+ return {
2067
+ schemaVersion: "OwlCodaRunKitExternalGateV1",
2068
+ gateId: gate.gateId,
2069
+ title: gate.title,
2070
+ requiredFor: gate.requiredFor,
2071
+ gateStatus: gate.gateStatus,
2072
+ blocking: gate.gateStatus !== "passed",
2073
+ ownerAgentId: gate.ownerAgentId,
2074
+ source: {
2075
+ adapterId: gate.sourceAdapterId,
2076
+ ref: gate.sourceRef,
2077
+ sha256: gate.sourceSha256,
2078
+ },
2079
+ summary: gate.summary,
2080
+ evidenceRefs: [...gate.evidenceRefs],
2081
+ occurredAt: gate.occurredAt,
2082
+ truthRefs: [gate.ref],
2083
+ authorizationGranted: false,
2084
+ };
2085
+ }
2086
+
2087
+ function deliveryLifecycleProjection(overall, state) {
2088
+ const stages = DELIVERY_STAGES.map((stage) => {
2089
+ const record = state.deliveryRecords.get(stage) ?? null;
2090
+ return {
2091
+ stage,
2092
+ status: record?.stageStatus ?? "pending",
2093
+ recordId: record?.recordId ?? null,
2094
+ ownerAgentId: record?.ownerAgentId ?? null,
2095
+ summary: record?.summary ?? null,
2096
+ evidenceRefs: [...(record?.evidenceRefs ?? [])],
2097
+ externalGateIds: [...(record?.externalGateIds ?? [])],
2098
+ occurredAt: record?.occurredAt ?? null,
2099
+ truthRef: record?.ref ?? null,
2100
+ };
2101
+ });
2102
+ let currentStage = overall === "completed"
2103
+ ? "work_ledger_complete"
2104
+ : "work_ledger_incomplete";
2105
+ let nextRequiredStage = overall === "completed" ? DELIVERY_STAGES[0] : null;
2106
+ let blockingStage = null;
2107
+ if (overall === "completed") {
2108
+ const projectedNames = {
2109
+ source: "source_accepted",
2110
+ integration: "integrated",
2111
+ materialization: "materialized",
2112
+ deployment: "deployed",
2113
+ live_readback: "live_readback_passed",
2114
+ product_acceptance: "product_accepted",
2115
+ };
2116
+ for (const stage of stages) {
2117
+ if (stage.status === "pending") {
2118
+ nextRequiredStage = stage.stage;
2119
+ break;
2120
+ }
2121
+ if (stage.status !== DELIVERY_POSITIVE_OUTCOME[stage.stage]) {
2122
+ blockingStage = stage.stage;
2123
+ nextRequiredStage = stage.stage;
2124
+ break;
2125
+ }
2126
+ currentStage = projectedNames[stage.stage];
2127
+ nextRequiredStage = stage.stage === DELIVERY_STAGES.at(-1)
2128
+ ? null
2129
+ : DELIVERY_STAGES[DELIVERY_STAGES.indexOf(stage.stage) + 1];
2130
+ }
2131
+ }
2132
+ const unresolvedGates = [...state.externalGates.values()]
2133
+ .filter(gate => gate.gateStatus !== "passed")
2134
+ .sort((left, right) => (
2135
+ DELIVERY_STAGES.indexOf(left.requiredFor) - DELIVERY_STAGES.indexOf(right.requiredFor)
2136
+ || compareIdentifiers(left.gateId, right.gateId)
2137
+ ));
2138
+ const nextStageIndex = nextRequiredStage === null
2139
+ ? DELIVERY_STAGES.length
2140
+ : DELIVERY_STAGES.indexOf(nextRequiredStage);
2141
+ const blockingExternalGateIds = unresolvedGates
2142
+ .filter(gate => DELIVERY_STAGES.indexOf(gate.requiredFor) <= nextStageIndex)
2143
+ .map(gate => gate.gateId);
2144
+ const lifecycleStatus = currentStage === "product_accepted"
2145
+ && blockingExternalGateIds.length === 0
2146
+ ? "completed"
2147
+ : blockingStage !== null || blockingExternalGateIds.length > 0
2148
+ ? "blocked"
2149
+ : "in_progress";
2150
+ return {
2151
+ schemaVersion: "OwlCodaRunKitDeliveryLifecycleV1",
2152
+ lifecycleStatus,
2153
+ currentStage,
2154
+ nextRequiredStage,
2155
+ blockingStage,
2156
+ blockingExternalGateIds,
2157
+ stages,
2158
+ authorizationGranted: false,
2159
+ };
2160
+ }
2161
+
1752
2162
  function projectStatus(definition, records, workspaceRoot = null) {
1753
2163
  const projectDefinition = definition;
1754
2164
  const state = projectEvents(definition, records);
@@ -1761,6 +2171,8 @@ function projectStatus(definition, records, workspaceRoot = null) {
1761
2171
  const rework = state.reworks.get(item.id) ?? null;
1762
2172
  const sourceDeliveries = state.sourceDeliveries.filter(row => row.workItemId === item.id);
1763
2173
  const scopeRevisionHistory = state.scopeRevisionHistory.get(item.id) ?? [];
2174
+ const evidenceRequirementFact = state.evidenceRequirements.get(item.id) ?? null;
2175
+ const receiptBindingFact = state.receiptBindings.get(item.id) ?? null;
1764
2176
  const latestScopeRevision = scopeRevisionHistory.at(-1) ?? null;
1765
2177
  const latestSourceDeliveryFact = sourceDeliveries.at(-1) ?? null;
1766
2178
  const latestSourceDelivery = sourceDeliveryProjection(sourceDeliveries.at(-1) ?? null);
@@ -1857,6 +2269,7 @@ function projectStatus(definition, records, workspaceRoot = null) {
1857
2269
  ...(currentTransfer ? [currentTransfer.ref] : []),
1858
2270
  ...sourceDeliveries.map(row => row.ref),
1859
2271
  ...scopeRevisionHistory.map(row => row.ref),
2272
+ ...(evidenceRequirementFact ? [evidenceRequirementFact.ref] : []),
1860
2273
  ];
1861
2274
  const projectedRework = reworkProjection(
1862
2275
  currentRework,
@@ -1911,6 +2324,30 @@ function projectStatus(definition, records, workspaceRoot = null) {
1911
2324
  pendingHandoff,
1912
2325
  rework: projectedRework,
1913
2326
  latestSourceDelivery,
2327
+ evidenceRequirement: evidenceRequirementFact
2328
+ ? {
2329
+ policyId: evidenceRequirementFact.policyId,
2330
+ requirement: evidenceRequirementFact.requirement,
2331
+ ownerAgentId: evidenceRequirementFact.ownerAgentId,
2332
+ reason: evidenceRequirementFact.reason,
2333
+ evidenceRefs: [...evidenceRequirementFact.evidenceRefs],
2334
+ occurredAt: evidenceRequirementFact.occurredAt,
2335
+ truthRef: evidenceRequirementFact.ref,
2336
+ }
2337
+ : {
2338
+ policyId: null,
2339
+ requirement: "none",
2340
+ ownerAgentId: null,
2341
+ reason: null,
2342
+ evidenceRefs: [],
2343
+ occurredAt: null,
2344
+ truthRef: null,
2345
+ },
2346
+ boundVerificationReceipt: receiptBindingFact !== null
2347
+ && receiptBindingFact.assignmentId === assignment?.assignmentId
2348
+ && effectiveCheckpoint?.checkpointId === receiptBindingFact.checkpointId
2349
+ ? structuredClone(receiptBindingFact)
2350
+ : null,
1914
2351
  lastCheckpointAt: checkpoint?.occurredAt ?? null,
1915
2352
  evidenceRefs: projectedFact?.evidenceRefs ?? [],
1916
2353
  assignmentHistory: history,
@@ -2126,6 +2563,10 @@ function projectStatus(definition, records, workspaceRoot = null) {
2126
2563
  && openDecisions.length === 0) overall = "completed";
2127
2564
  else if (openDecisions.length > 0 || workItems.some(row => row.status === "failed")) overall = "active_with_blockers";
2128
2565
  else if (workItems.some(row => row.status !== "planned" || row.pendingHandoff !== null)) overall = "active";
2566
+ const externalGates = [...state.externalGates.values()]
2567
+ .map(externalGateProjection)
2568
+ .sort((left, right) => compareIdentifiers(left.gateId, right.gateId));
2569
+ const deliveryLifecycle = deliveryLifecycleProjection(overall, state);
2129
2570
  const status = {
2130
2571
  schemaVersion: STATUS_SCHEMA_V3,
2131
2572
  status: "team_project_status",
@@ -2147,6 +2588,8 @@ function projectStatus(definition, records, workspaceRoot = null) {
2147
2588
  dataCandidates,
2148
2589
  sourceDeliveries,
2149
2590
  warnings,
2591
+ externalGates,
2592
+ deliveryLifecycle,
2150
2593
  projectTruthHash: sha256(canonical({
2151
2594
  definition: projectDefinition,
2152
2595
  events: records.map(row => row.event),
@@ -2156,10 +2599,55 @@ function projectStatus(definition, records, workspaceRoot = null) {
2156
2599
  return { ...status, ...deriveProjectDriverSummary(status) };
2157
2600
  }
2158
2601
 
2159
- function validateEventAgainstState(event, definition, records, workspaceRoot) {
2602
+ function validateEventAgainstState(
2603
+ event,
2604
+ definition,
2605
+ records,
2606
+ workspaceRoot,
2607
+ { allowBoundReceipt = false } = {},
2608
+ ) {
2160
2609
  const normalized = validateEventShape(event, definition);
2161
2610
  const state = projectEvents(definition, records);
2162
2611
  const status = projectStatus(definition, records, workspaceRoot);
2612
+ if (normalized.schemaVersion === EVENT_SCHEMA_V6) {
2613
+ projectEvents(definition, [
2614
+ ...records,
2615
+ { event: normalized, ref: `project/events/${normalized.eventId}.json` },
2616
+ ]);
2617
+ return normalized;
2618
+ }
2619
+ if (normalized.type === "work_item_evidence_requirement_set") {
2620
+ projectEvents(definition, [
2621
+ ...records,
2622
+ { event: normalized, ref: `project/events/${normalized.eventId}.json` },
2623
+ ]);
2624
+ return normalized;
2625
+ }
2626
+ if (normalized.type === "checkpoint_receipt_bound") {
2627
+ const {
2628
+ receiptBinding: _receiptBinding,
2629
+ authorizationGranted: _authorizationGranted,
2630
+ checkpointId: _checkpointId,
2631
+ ...checkpointFields
2632
+ } = normalized;
2633
+ const checkpointEvent = {
2634
+ ...checkpointFields,
2635
+ schemaVersion: EVENT_SCHEMA,
2636
+ type: "checkpoint_recorded",
2637
+ };
2638
+ validateEventAgainstState(
2639
+ checkpointEvent,
2640
+ definition,
2641
+ records,
2642
+ workspaceRoot,
2643
+ { allowBoundReceipt: true },
2644
+ );
2645
+ projectEvents(definition, [
2646
+ ...records,
2647
+ { event: normalized, ref: `project/events/${normalized.eventId}.json` },
2648
+ ]);
2649
+ return normalized;
2650
+ }
2163
2651
  const effectiveDefinition = definitionWithScopeRevisions(definition, state);
2164
2652
  const workItem = effectiveDefinition.workItems.find(row => row.id === normalized.workItemId);
2165
2653
  if (normalized.type === "agent_assigned") {
@@ -2218,6 +2706,15 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
2218
2706
  if (normalized.state === "completed" && unresolved.length > 0) {
2219
2707
  throw new Error(`Completed checkpoint has unresolved dependencies: ${unresolved.join(", ")}.`);
2220
2708
  }
2709
+ const evidenceRequirement = state.evidenceRequirements.get(normalized.workItemId) ?? null;
2710
+ if (!allowBoundReceipt
2711
+ && ["verifying", "completed"].includes(normalized.state)
2712
+ && evidenceRequirement?.requirement === "quick") {
2713
+ throw new Error(
2714
+ `WorkItem ${normalized.workItemId} requires a bound Quick receipt. `
2715
+ + "Use project checkpoint --from-quick-receipt <receipt.json>.",
2716
+ );
2717
+ }
2221
2718
  if (normalized.state === "verifying") {
2222
2719
  const history = state.assignmentHistory.get(normalized.workItemId) ?? [];
2223
2720
  const prior = history.at(-2) ?? null;
@@ -3262,6 +3759,340 @@ export function integrateTeamProjectV1({
3262
3759
  };
3263
3760
  }
3264
3761
 
3762
+ export function reconcileExternalGateV1({
3763
+ workspaceRoot,
3764
+ reconciliationId,
3765
+ occurredAt,
3766
+ gateId,
3767
+ title,
3768
+ requiredFor,
3769
+ gateStatus,
3770
+ ownerAgentId,
3771
+ sourceAdapterId,
3772
+ sourceRef,
3773
+ sourceSha256,
3774
+ summary,
3775
+ evidenceRefs = [],
3776
+ }) {
3777
+ safeIdentifier(reconciliationId, "reconciliationId");
3778
+ const loaded = loadProject(workspaceRoot);
3779
+ const state = projectEvents(loaded.definition, loaded.events);
3780
+ const existing = loaded.events.find(row => (
3781
+ row.event.type === "external_gate_reconciled"
3782
+ && row.event.reconciliationId === reconciliationId
3783
+ ))?.event ?? null;
3784
+ let event;
3785
+ if (existing) {
3786
+ const requested = {
3787
+ occurredAt: normalizeTimestamp(occurredAt),
3788
+ gateId,
3789
+ title,
3790
+ requiredFor,
3791
+ gateStatus,
3792
+ ownerAgentId,
3793
+ sourceAdapterId,
3794
+ sourceRef,
3795
+ sourceSha256,
3796
+ summary,
3797
+ evidenceRefs,
3798
+ };
3799
+ for (const key of Object.keys(requested)) {
3800
+ if (canonical(existing[key]) !== canonical(requested[key])) {
3801
+ throw new Error(`Cannot resume external Gate reconciliation ${reconciliationId}; ${key} differs.`);
3802
+ }
3803
+ }
3804
+ event = existing;
3805
+ } else {
3806
+ const previous = state.externalGates.get(gateId) ?? null;
3807
+ event = {
3808
+ schemaVersion: EVENT_SCHEMA_V6,
3809
+ eventId: `external-gate-${reconciliationId}`,
3810
+ type: "external_gate_reconciled",
3811
+ occurredAt,
3812
+ reconciliationId,
3813
+ ...(previous ? { supersedesReconciliationId: previous.reconciliationId } : {}),
3814
+ gateId,
3815
+ title,
3816
+ requiredFor,
3817
+ gateStatus,
3818
+ ownerAgentId,
3819
+ sourceAdapterId,
3820
+ sourceRef,
3821
+ sourceSha256,
3822
+ summary,
3823
+ evidenceRefs,
3824
+ authorizationGranted: false,
3825
+ };
3826
+ }
3827
+ const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
3828
+ const status = readTeamProjectStatusV4({ workspaceRoot });
3829
+ const gate = status.externalGates.find(row => row.gateId === gateId);
3830
+ if (!gate || gate.gateStatus !== gateStatus) {
3831
+ throw new Error(`External Gate reconciliation ${reconciliationId} is not current.`);
3832
+ }
3833
+ return {
3834
+ status: "team_project_external_gate_reconciled",
3835
+ reconciliationId,
3836
+ supersedesReconciliationId: event.supersedesReconciliationId ?? null,
3837
+ gateId,
3838
+ gateStatus,
3839
+ eventId: recorded.eventId,
3840
+ eventPath: recorded.eventPath,
3841
+ eventSha256: recorded.eventSha256,
3842
+ resumed: recorded.resumed,
3843
+ projectTruthHash: status.projectTruthHash,
3844
+ dominantGap: status.dominantGap,
3845
+ nextAction: status.nextAction,
3846
+ nextActorId: status.nextActorId,
3847
+ authorizationGranted: false,
3848
+ exitCode: 0,
3849
+ };
3850
+ }
3851
+
3852
+ export function recordDeliveryLifecycleV1({
3853
+ workspaceRoot,
3854
+ recordId,
3855
+ occurredAt,
3856
+ stage,
3857
+ stageStatus,
3858
+ ownerAgentId,
3859
+ summary,
3860
+ evidenceRefs = [],
3861
+ externalGateIds = [],
3862
+ }) {
3863
+ safeIdentifier(recordId, "delivery recordId");
3864
+ const loaded = loadProject(workspaceRoot);
3865
+ const state = projectEvents(loaded.definition, loaded.events);
3866
+ const existing = loaded.events.find(row => (
3867
+ row.event.type === "delivery_lifecycle_recorded"
3868
+ && row.event.recordId === recordId
3869
+ ))?.event ?? null;
3870
+ let event;
3871
+ if (existing) {
3872
+ const requested = {
3873
+ occurredAt: normalizeTimestamp(occurredAt),
3874
+ stage,
3875
+ stageStatus,
3876
+ ownerAgentId,
3877
+ summary,
3878
+ evidenceRefs,
3879
+ externalGateIds,
3880
+ };
3881
+ for (const key of Object.keys(requested)) {
3882
+ if (canonical(existing[key]) !== canonical(requested[key])) {
3883
+ throw new Error(`Cannot resume delivery lifecycle record ${recordId}; ${key} differs.`);
3884
+ }
3885
+ }
3886
+ event = existing;
3887
+ } else {
3888
+ const previous = state.deliveryRecords.get(stage) ?? null;
3889
+ event = {
3890
+ schemaVersion: EVENT_SCHEMA_V6,
3891
+ eventId: `delivery-lifecycle-${recordId}`,
3892
+ type: "delivery_lifecycle_recorded",
3893
+ occurredAt,
3894
+ recordId,
3895
+ ...(previous ? { supersedesRecordId: previous.recordId } : {}),
3896
+ stage,
3897
+ stageStatus,
3898
+ ownerAgentId,
3899
+ summary,
3900
+ evidenceRefs,
3901
+ externalGateIds,
3902
+ authorizationGranted: false,
3903
+ };
3904
+ }
3905
+ const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
3906
+ const status = readTeamProjectStatusV4({ workspaceRoot });
3907
+ const stageProjection = status.deliveryLifecycle.stages.find(row => row.stage === stage);
3908
+ if (stageProjection?.recordId !== recordId) {
3909
+ throw new Error(`Delivery lifecycle record ${recordId} is not current.`);
3910
+ }
3911
+ return {
3912
+ status: "team_project_delivery_lifecycle_recorded",
3913
+ recordId,
3914
+ supersedesRecordId: event.supersedesRecordId ?? null,
3915
+ stage,
3916
+ stageStatus,
3917
+ eventId: recorded.eventId,
3918
+ eventPath: recorded.eventPath,
3919
+ eventSha256: recorded.eventSha256,
3920
+ resumed: recorded.resumed,
3921
+ projectTruthHash: status.projectTruthHash,
3922
+ deliveryLifecycle: status.deliveryLifecycle,
3923
+ dominantGap: status.dominantGap,
3924
+ nextAction: status.nextAction,
3925
+ nextActorId: status.nextActorId,
3926
+ authorizationGranted: false,
3927
+ exitCode: 0,
3928
+ };
3929
+ }
3930
+
3931
+ export function setWorkItemEvidenceRequirementV1({
3932
+ workspaceRoot,
3933
+ policyId,
3934
+ occurredAt,
3935
+ workItemId,
3936
+ ownerAgentId,
3937
+ requirement,
3938
+ reason,
3939
+ evidenceRefs = [],
3940
+ }) {
3941
+ safeIdentifier(policyId, "evidence policyId");
3942
+ const loaded = loadProject(workspaceRoot);
3943
+ const state = projectEvents(loaded.definition, loaded.events);
3944
+ const existing = loaded.events.find(row => (
3945
+ row.event.type === "work_item_evidence_requirement_set"
3946
+ && row.event.policyId === policyId
3947
+ ))?.event ?? null;
3948
+ let event;
3949
+ if (existing) {
3950
+ const requested = {
3951
+ occurredAt: normalizeTimestamp(occurredAt),
3952
+ workItemId,
3953
+ ownerAgentId,
3954
+ requirement,
3955
+ reason,
3956
+ evidenceRefs,
3957
+ };
3958
+ for (const key of Object.keys(requested)) {
3959
+ if (canonical(existing[key]) !== canonical(requested[key])) {
3960
+ throw new Error(`Cannot resume WorkItem evidence policy ${policyId}; ${key} differs.`);
3961
+ }
3962
+ }
3963
+ event = existing;
3964
+ } else {
3965
+ const previous = state.evidenceRequirements.get(workItemId) ?? null;
3966
+ event = {
3967
+ schemaVersion: EVENT_SCHEMA_V7,
3968
+ eventId: `work-item-evidence-${policyId}`,
3969
+ type: "work_item_evidence_requirement_set",
3970
+ occurredAt,
3971
+ policyId,
3972
+ ...(previous ? { supersedesPolicyId: previous.policyId } : {}),
3973
+ workItemId,
3974
+ ownerAgentId,
3975
+ requirement,
3976
+ reason,
3977
+ evidenceRefs,
3978
+ authorizationGranted: false,
3979
+ };
3980
+ }
3981
+ const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
3982
+ const status = readTeamProjectStatusV4({ workspaceRoot });
3983
+ const workItem = status.workItems.find(row => row.workItemId === workItemId);
3984
+ if (workItem?.evidenceRequirement.policyId !== policyId) {
3985
+ throw new Error(`WorkItem evidence policy ${policyId} is not current.`);
3986
+ }
3987
+ return {
3988
+ status: "team_project_work_item_evidence_requirement",
3989
+ policyId,
3990
+ supersedesPolicyId: event.supersedesPolicyId ?? null,
3991
+ workItemId,
3992
+ requirement,
3993
+ eventId: recorded.eventId,
3994
+ eventPath: recorded.eventPath,
3995
+ eventSha256: recorded.eventSha256,
3996
+ resumed: recorded.resumed,
3997
+ projectTruthHash: status.projectTruthHash,
3998
+ authorizationGranted: false,
3999
+ exitCode: 0,
4000
+ };
4001
+ }
4002
+
4003
+ function attestQuickReceiptBinding(workspaceRoot, receiptPath) {
4004
+ const root = realpathSync(workspaceRoot);
4005
+ const { absolutePath, bytes } = readFileBytesBounded(receiptPath);
4006
+ const result = attestCoreBoundQuickReceiptDetailsFromBytes({
4007
+ receiptPath: absolutePath,
4008
+ receiptBytes: bytes,
4009
+ workspaceRoot: root,
4010
+ });
4011
+ if (result.attestation.decision !== "GO") {
4012
+ throw new Error(
4013
+ `Quick receipt attestation must be GO; got ${result.attestation.decision}: `
4014
+ + `${result.attestation.issueCodes.join(", ") || "no issue code"}.`,
4015
+ );
4016
+ }
4017
+ return {
4018
+ kind: "quick",
4019
+ receiptId: result.attestation.subjectRef.receiptId,
4020
+ receiptRef: relativeToWorkspace(root, absolutePath),
4021
+ receiptSha256: result.attestation.subjectRef.receiptSha256,
4022
+ sourceFingerprint: result.sourceFingerprint,
4023
+ attestationDecision: "GO",
4024
+ issueCodes: [...result.attestation.issueCodes],
4025
+ };
4026
+ }
4027
+
4028
+ export function checkpointTeamProjectFromQuickReceiptV1({
4029
+ workspaceRoot,
4030
+ checkpointId,
4031
+ occurredAt,
4032
+ assignmentId,
4033
+ workItemId,
4034
+ state,
4035
+ summary,
4036
+ completedUnits,
4037
+ evidenceRefs = [],
4038
+ blockerRefs = [],
4039
+ decisionRefs = [],
4040
+ nextAction = null,
4041
+ sourceFingerprint,
4042
+ receiptPath,
4043
+ }) {
4044
+ safeIdentifier(checkpointId, "checkpointId");
4045
+ const receiptBinding = attestQuickReceiptBinding(workspaceRoot, receiptPath);
4046
+ if (sourceFingerprint !== undefined
4047
+ && sourceFingerprint !== receiptBinding.sourceFingerprint) {
4048
+ throw new Error("Checkpoint sourceFingerprint differs from the Quick receipt.");
4049
+ }
4050
+ const event = {
4051
+ schemaVersion: EVENT_SCHEMA_V7,
4052
+ eventId: `checkpoint-receipt-${checkpointId}`,
4053
+ type: "checkpoint_receipt_bound",
4054
+ occurredAt,
4055
+ checkpointId,
4056
+ assignmentId,
4057
+ workItemId,
4058
+ state,
4059
+ summary,
4060
+ ...(completedUnits === undefined ? {} : { completedUnits }),
4061
+ evidenceRefs: uniqueSorted([...evidenceRefs, receiptBinding.receiptRef]),
4062
+ blockerRefs,
4063
+ decisionRefs,
4064
+ nextAction,
4065
+ sourceFingerprint: receiptBinding.sourceFingerprint,
4066
+ receiptBinding,
4067
+ authorizationGranted: false,
4068
+ };
4069
+ const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
4070
+ const status = readTeamProjectStatusV4({ workspaceRoot });
4071
+ const workItem = status.workItems.find(row => row.workItemId === workItemId);
4072
+ if (workItem?.boundVerificationReceipt?.checkpointId !== checkpointId) {
4073
+ throw new Error(`Receipt-bound checkpoint ${checkpointId} is not current.`);
4074
+ }
4075
+ return {
4076
+ status: "team_project_checkpoint",
4077
+ checkpointId,
4078
+ assignmentId,
4079
+ workItemId,
4080
+ state,
4081
+ receiptBinding,
4082
+ eventId: recorded.eventId,
4083
+ eventPath: recorded.eventPath,
4084
+ eventSha256: recorded.eventSha256,
4085
+ resumed: recorded.resumed,
4086
+ projectTruthHash: status.projectTruthHash,
4087
+ overall: status.overall,
4088
+ warnings: status.warnings.filter(row => row.workItemId === workItemId),
4089
+ nextAction: status.nextAction,
4090
+ nextActorId: status.nextActorId,
4091
+ authorizationGranted: false,
4092
+ exitCode: 0,
4093
+ };
4094
+ }
4095
+
3265
4096
  export function checkpointTeamProjectV1({
3266
4097
  workspaceRoot,
3267
4098
  checkpointId,
@@ -3423,6 +4254,8 @@ function legacyWorkItemProjection(workItem) {
3423
4254
  ownedPaths: _ownedPaths,
3424
4255
  measurable: _measurable,
3425
4256
  scopeRevision: _scopeRevision,
4257
+ evidenceRequirement: _evidenceRequirement,
4258
+ boundVerificationReceipt: _boundVerificationReceipt,
3426
4259
  ...legacy
3427
4260
  } = workItem;
3428
4261
  return legacy;
@@ -3434,11 +4267,23 @@ function statusV2WorkItemProjection(workItem) {
3434
4267
  ownedPaths: _ownedPaths,
3435
4268
  measurable: _measurable,
3436
4269
  scopeRevision: _scopeRevision,
4270
+ evidenceRequirement: _evidenceRequirement,
4271
+ boundVerificationReceipt: _boundVerificationReceipt,
3437
4272
  ...v2
3438
4273
  } = workItem;
3439
4274
  return v2;
3440
4275
  }
3441
4276
 
4277
+ function statusV3WorkItemProjection(workItem) {
4278
+ if (workItem === null) return null;
4279
+ const {
4280
+ evidenceRequirement: _evidenceRequirement,
4281
+ boundVerificationReceipt: _boundVerificationReceipt,
4282
+ ...v3
4283
+ } = workItem;
4284
+ return v3;
4285
+ }
4286
+
3442
4287
  function statusV3CompletionProjection(status) {
3443
4288
  if (status.overall !== "completed") {
3444
4289
  return { ...status, deliveryDisposition: "work_in_progress" };
@@ -3466,11 +4311,106 @@ function statusV3CompletionProjection(status) {
3466
4311
  };
3467
4312
  }
3468
4313
 
4314
+ function statusV4DeliveryProjection(status) {
4315
+ if (status.overall !== "completed") {
4316
+ return {
4317
+ ...status,
4318
+ schemaVersion: STATUS_SCHEMA_V4,
4319
+ deliveryDisposition: status.deliveryLifecycle.currentStage,
4320
+ };
4321
+ }
4322
+ const dueGate = status.externalGates
4323
+ .filter(gate => gate.blocking)
4324
+ .sort((left, right) => (
4325
+ DELIVERY_STAGES.indexOf(left.requiredFor) - DELIVERY_STAGES.indexOf(right.requiredFor)
4326
+ || compareIdentifiers(left.gateId, right.gateId)
4327
+ ))
4328
+ .find(gate => status.deliveryLifecycle.blockingExternalGateIds.includes(gate.gateId))
4329
+ ?? null;
4330
+ if (dueGate) {
4331
+ const dominantGap = {
4332
+ kind: "external_gate",
4333
+ id: dueGate.gateId,
4334
+ title: dueGate.title,
4335
+ workItemId: null,
4336
+ agentId: dueGate.ownerAgentId,
4337
+ reason: dueGate.summary,
4338
+ truthRefs: uniqueSorted(dueGate.truthRefs),
4339
+ };
4340
+ const nextAction = `Reconcile external Gate ${dueGate.gateId}.`;
4341
+ return {
4342
+ ...status,
4343
+ schemaVersion: STATUS_SCHEMA_V4,
4344
+ deliveryDisposition: status.deliveryLifecycle.currentStage,
4345
+ headline: `Dominant gap: external Gate ${dueGate.gateId} (${dueGate.title}); `
4346
+ + `owner: ${dueGate.ownerAgentId}; blocker: ${singleLine(dueGate.summary)}; `
4347
+ + `next: ${nextAction}`,
4348
+ dominantGap,
4349
+ nextAction,
4350
+ nextActorId: dueGate.ownerAgentId,
4351
+ };
4352
+ }
4353
+ if (status.deliveryLifecycle.nextRequiredStage !== null) {
4354
+ const stage = status.deliveryLifecycle.nextRequiredStage;
4355
+ const stageProjection = status.deliveryLifecycle.stages.find(row => row.stage === stage);
4356
+ const dominantGap = {
4357
+ kind: "delivery_stage",
4358
+ id: stage,
4359
+ title: `Delivery stage ${stage}`,
4360
+ workItemId: null,
4361
+ agentId: stageProjection?.ownerAgentId ?? null,
4362
+ reason: stageProjection?.status === "pending"
4363
+ ? `Delivery stage ${stage} has no evidence-bound record.`
4364
+ : stageProjection?.summary ?? `Delivery stage ${stage} is not accepted.`,
4365
+ truthRefs: uniqueSorted([
4366
+ "project/definition.json",
4367
+ ...(stageProjection?.truthRef ? [stageProjection.truthRef] : []),
4368
+ ]),
4369
+ };
4370
+ const nextAction = `Record delivery stage ${stage}.`;
4371
+ return {
4372
+ ...status,
4373
+ schemaVersion: STATUS_SCHEMA_V4,
4374
+ deliveryDisposition: status.deliveryLifecycle.currentStage,
4375
+ headline: `Dominant gap: delivery stage ${stage}; owner: `
4376
+ + `${dominantGap.agentId ?? "unassigned"}; blocker: ${singleLine(dominantGap.reason)}; `
4377
+ + `next: ${nextAction}`,
4378
+ dominantGap,
4379
+ nextAction,
4380
+ nextActorId: dominantGap.agentId,
4381
+ };
4382
+ }
4383
+ return {
4384
+ ...status,
4385
+ schemaVersion: STATUS_SCHEMA_V4,
4386
+ deliveryDisposition: status.deliveryLifecycle.currentStage,
4387
+ headline: "Delivery lifecycle complete through product acceptance; authorizationGranted=false.",
4388
+ dominantGap: {
4389
+ kind: "none",
4390
+ id: null,
4391
+ title: null,
4392
+ workItemId: null,
4393
+ agentId: null,
4394
+ reason: null,
4395
+ truthRefs: uniqueSorted([
4396
+ "project/definition.json",
4397
+ ...status.deliveryLifecycle.stages
4398
+ .map(stage => stage.truthRef)
4399
+ .filter(Boolean),
4400
+ ]),
4401
+ },
4402
+ nextAction: null,
4403
+ nextActorId: null,
4404
+ };
4405
+ }
4406
+
3469
4407
  export function readTeamProjectStatusV1({ workspaceRoot }) {
3470
4408
  const loaded = loadProject(workspaceRoot);
3471
4409
  const {
3472
4410
  sourceDeliveries: _sourceDeliveries,
3473
4411
  warnings: _warnings,
4412
+ externalGates: _externalGates,
4413
+ deliveryLifecycle: _deliveryLifecycle,
3474
4414
  ...legacy
3475
4415
  } = projectStatus(
3476
4416
  loaded.definition,
@@ -3487,7 +4427,12 @@ export function readTeamProjectStatusV1({ workspaceRoot }) {
3487
4427
 
3488
4428
  export function readTeamProjectStatusV2({ workspaceRoot }) {
3489
4429
  const loaded = loadProject(workspaceRoot);
3490
- const { warnings: _warnings, ...v2 } = projectStatus(
4430
+ const {
4431
+ warnings: _warnings,
4432
+ externalGates: _externalGates,
4433
+ deliveryLifecycle: _deliveryLifecycle,
4434
+ ...v2
4435
+ } = projectStatus(
3491
4436
  loaded.definition,
3492
4437
  loaded.events,
3493
4438
  loaded.root,
@@ -3501,9 +4446,27 @@ export function readTeamProjectStatusV2({ workspaceRoot }) {
3501
4446
  }
3502
4447
 
3503
4448
  export function readTeamProjectStatusV3({ workspaceRoot }) {
4449
+ const loaded = loadProject(workspaceRoot);
4450
+ const {
4451
+ externalGates: _externalGates,
4452
+ deliveryLifecycle: _deliveryLifecycle,
4453
+ ...v3
4454
+ } = projectStatus(
4455
+ loaded.definition,
4456
+ loaded.events,
4457
+ loaded.root,
4458
+ );
4459
+ return {
4460
+ ...statusV3CompletionProjection(v3),
4461
+ workItems: v3.workItems.map(statusV3WorkItemProjection),
4462
+ exitCode: 0,
4463
+ };
4464
+ }
4465
+
4466
+ export function readTeamProjectStatusV4({ workspaceRoot }) {
3504
4467
  const loaded = loadProject(workspaceRoot);
3505
4468
  return {
3506
- ...statusV3CompletionProjection(projectStatus(
4469
+ ...statusV4DeliveryProjection(projectStatus(
3507
4470
  loaded.definition,
3508
4471
  loaded.events,
3509
4472
  loaded.root,
@@ -3783,9 +4746,7 @@ export function formatTeamProjectStatusHumanV2(status) {
3783
4746
  return [
3784
4747
  status.headline,
3785
4748
  `Project: ${status.projectId} — ${singleLine(status.objective)}`,
3786
- `Work ledger: ${status.overall}${status.deliveryDisposition === "code_complete_without_release_authority"
3787
- ? `; delivery: ${status.deliveryDisposition}`
3788
- : ""}`,
4749
+ `Work ledger: ${status.overall}; Delivery: ${status.deliveryDisposition}`,
3789
4750
  `Work: ${status.counts.completed}/${status.counts.total} completed; ${status.counts.active} active; ${status.counts.waiting_dependency + status.counts.waiting_decision} waiting`,
3790
4751
  `Agents: ${status.agents.map(agent => agent.agentId).join(", ") || "none assigned"}`,
3791
4752
  `Open decisions: ${status.openDecisions.map(row => row.decisionId).join(", ") || "none"}`,
@@ -3801,7 +4762,7 @@ export function formatTeamProjectStatusHumanV2(status) {
3801
4762
  ...(status.warnings?.length > 0
3802
4763
  ? [`Warnings: ${status.warnings.map(row => `${row.code}:${row.workItemId}`).join(", ")}`]
3803
4764
  : []),
3804
- `Next: ${nextAction}${status.nextActorId ? ` (owner ${status.nextActorId})` : ""}`,
4765
+ `Next: ${nextAction}${status.nextActorId ? ` (owner ${status.nextActorId})` : ""}; Authority: not granted for Git, release, deployment, production, or business actions.`,
3805
4766
  "",
3806
4767
  ].join("\n");
3807
4768
  }