owlrunkit 0.17.2 → 0.18.1

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 (33) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +117 -14
  3. package/docs/architecture/OWLCODA_RUNKIT_017_AGENT_NATIVE_PRODUCT_DEFINITION.md +2 -3
  4. package/docs/architecture/OWLCODA_RUNKIT_018_PROJECT_DRIVER_PRODUCT_DEFINITION.md +165 -0
  5. package/docs/architecture/OWLCODA_RUNKIT_USAGE_MODE_SELECTION_PRODUCT_DECISION_20260811.md +142 -0
  6. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +127 -23
  7. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  8. package/package.json +2 -2
  9. package/packages/attest/distribution-manifest-v1.json +10 -0
  10. package/packages/attest/src/quick.mjs +10 -0
  11. package/registry-release-policy-v1.json +5 -5
  12. package/sbom.spdx.json +5 -5
  13. package/scripts/runkit-contract/assurance-router.mjs +28 -20
  14. package/scripts/runkit-contract/core-contract.mjs +7 -1
  15. package/scripts/runkit-contract/inspect-presentation.mjs +38 -5
  16. package/scripts/runkit-contract/install-codex-skill.mjs +60 -1
  17. package/scripts/runkit-contract/onboarding-doctor.mjs +19 -1
  18. package/scripts/runkit-contract/profile-onboarding.mjs +597 -22
  19. package/scripts/runkit-contract/project-cli-resolver.mjs +6 -0
  20. package/scripts/runkit-contract/quick-attest.mjs +1 -0
  21. package/scripts/runkit-contract/quick-metrics.mjs +43 -5
  22. package/scripts/runkit-contract/quick-verify.mjs +15 -4
  23. package/scripts/runkit-contract/runkit-cli.mjs +747 -20
  24. package/scripts/runkit-contract/schemas/profile-reconcile-plan-v1.schema.json +105 -0
  25. package/scripts/runkit-contract/schemas/profile-reconcile-receipt-v1.schema.json +60 -0
  26. package/scripts/runkit-contract/schemas/team-project-event-v1.schema.json +16 -1
  27. package/scripts/runkit-contract/schemas/team-project-status-v1.schema.json +34 -1
  28. package/scripts/runkit-contract/schemas/team-project-successor-receipt-v1.schema.json +204 -0
  29. package/scripts/runkit-contract/schemas/usage-mode-recommendation-v1.schema.json +136 -0
  30. package/scripts/runkit-contract/team-project-successor.mjs +1176 -0
  31. package/scripts/runkit-contract/team-project.mjs +1082 -43
  32. package/scripts/runkit-contract/templates/assurance-request.json +6 -0
  33. package/scripts/runkit-contract/usage-mode.mjs +229 -0
@@ -43,6 +43,7 @@ const WORK_ITEM_STATES = new Set([
43
43
  "completed",
44
44
  "failed",
45
45
  ]);
46
+ const VERIFICATION_DISPOSITIONS = new Set(["verified", "no_longer_required"]);
46
47
  const EVENT_KEYS = Object.freeze({
47
48
  agent_assigned: [
48
49
  "schemaVersion", "eventId", "type", "occurredAt", "assignmentId",
@@ -71,6 +72,14 @@ const EVENT_KEYS = Object.freeze({
71
72
  "schemaVersion", "eventId", "type", "occurredAt", "gateId", "summary",
72
73
  "evidenceRefs",
73
74
  ],
75
+ verification_deferred: [
76
+ "schemaVersion", "eventId", "type", "occurredAt", "verificationId",
77
+ "workItemId", "ownerAgentId", "checkIds", "reason", "dueGateId",
78
+ ],
79
+ verification_closed: [
80
+ "schemaVersion", "eventId", "type", "occurredAt", "verificationId",
81
+ "disposition", "summary", "evidenceRefs", "decisionIds",
82
+ ],
74
83
  data_candidate_recorded: [
75
84
  "schemaVersion", "eventId", "type", "occurredAt", "candidateId",
76
85
  "sourceRef", "rights", "inputRef", "outputRef", "decisionRef",
@@ -109,7 +118,7 @@ function nullableString(value, label) {
109
118
  function stringArray(value, label, { allowEmpty = true } = {}) {
110
119
  if (!Array.isArray(value)
111
120
  || (!allowEmpty && value.length === 0)
112
- || value.some(item => typeof item !== "string" || item.length === 0)
121
+ || value.some(item => typeof item !== "string" || item.trim().length === 0)
113
122
  || new Set(value).size !== value.length) {
114
123
  throw new Error(`${label} must be a unique string array${allowEmpty ? "" : " with at least one entry"}.`);
115
124
  }
@@ -187,6 +196,9 @@ function validateDefinition(value) {
187
196
  ]) {
188
197
  if (!Array.isArray(value[key])) throw new Error(`${label} list must be an array.`);
189
198
  }
199
+ if (value.workItems.length === 0) {
200
+ throw new Error("Team project definition requires at least one WorkItem.");
201
+ }
190
202
  const milestoneIds = uniqueIds(value.milestones, "Milestone");
191
203
  const workstreamIds = uniqueIds(value.workstreams, "Workstream");
192
204
  const workItemIds = uniqueIds(value.workItems, "Work item");
@@ -277,6 +289,15 @@ function validateDefinition(value) {
277
289
  };
278
290
  }
279
291
 
292
+ export function normalizeTeamProjectDefinitionV1(definition) {
293
+ return validateDefinition(definition);
294
+ }
295
+
296
+ export function emptyTeamProjectTruthHashV1(definition) {
297
+ const normalized = validateDefinition(definition);
298
+ return sha256(canonical({ definition: normalized, events: [] }));
299
+ }
300
+
280
301
  function readRegularJson(filePath, label) {
281
302
  const stat = lstatSync(filePath);
282
303
  if (stat.isSymbolicLink() || !stat.isFile() || realpathSync(filePath) !== filePath) {
@@ -291,8 +312,16 @@ function loadProject(workspaceRoot) {
291
312
  if (!existsSync(definitionPath)) throw new Error("Team project is not initialized.");
292
313
  const definition = validateDefinition(readRegularJson(definitionPath, "Team project definition"));
293
314
  const eventsRoot = path.join(rootPath, "events");
294
- const events = existsSync(eventsRoot)
295
- ? readdirSync(eventsRoot, { withFileTypes: true })
315
+ if (!existsSync(eventsRoot)) {
316
+ throw new Error("Team project event directory must be a regular directory without symlinks.");
317
+ }
318
+ const eventsStat = lstatSync(eventsRoot);
319
+ if (eventsStat.isSymbolicLink()
320
+ || !eventsStat.isDirectory()
321
+ || realpathSync(eventsRoot) !== eventsRoot) {
322
+ throw new Error("Team project event directory must be a regular directory without symlinks.");
323
+ }
324
+ const events = readdirSync(eventsRoot, { withFileTypes: true })
296
325
  .filter(entry => entry.name.endsWith(".json"))
297
326
  .map((entry) => {
298
327
  if (entry.isSymbolicLink() || !entry.isFile()) {
@@ -308,8 +337,7 @@ function loadProject(workspaceRoot) {
308
337
  .sort((left, right) => (
309
338
  left.event.occurredAt.localeCompare(right.event.occurredAt)
310
339
  || left.event.eventId.localeCompare(right.event.eventId)
311
- ))
312
- : [];
340
+ ));
313
341
  return { root, projectRoot: rootPath, definitionPath, definition, events };
314
342
  }
315
343
 
@@ -335,6 +363,34 @@ function eventRef(event) {
335
363
  return `project/events/${event.eventId}.json`;
336
364
  }
337
365
 
366
+ function compareEventFacts(left, right) {
367
+ return left.occurredAt.localeCompare(right.occurredAt)
368
+ || left.eventId.localeCompare(right.eventId);
369
+ }
370
+
371
+ function latestEventFact(left, right) {
372
+ if (!left) return right;
373
+ if (!right) return left;
374
+ return compareEventFacts(left, right) >= 0 ? left : right;
375
+ }
376
+
377
+ function pendingHandoffProjection(handoff) {
378
+ return handoff
379
+ ? {
380
+ eventId: handoff.eventId,
381
+ truthRef: handoff.ref,
382
+ assignmentId: handoff.assignmentId,
383
+ workItemId: handoff.workItemId,
384
+ fromAgentId: handoff.fromAgentId,
385
+ toAgentId: handoff.toAgentId,
386
+ summary: handoff.summary,
387
+ evidenceRefs: [...handoff.evidenceRefs],
388
+ nextAction: handoff.nextAction,
389
+ occurredAt: handoff.occurredAt,
390
+ }
391
+ : null;
392
+ }
393
+
338
394
  function validateTimestamp(value) {
339
395
  if (typeof value !== "string"
340
396
  || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
@@ -360,8 +416,15 @@ function validateEventShape(value, definition) {
360
416
  safeIdentifier(id, "workItemId");
361
417
  if (!workItemIds.has(id)) throw new Error(`Team project event references unknown work item ${id}.`);
362
418
  };
363
- if (["agent_assigned", "checkpoint_recorded", "handoff_recorded"].includes(value.type)) {
419
+ if ([
420
+ "agent_assigned",
421
+ "checkpoint_recorded",
422
+ "handoff_recorded",
423
+ "verification_deferred",
424
+ ].includes(value.type)) {
364
425
  assertWorkItem(value.workItemId);
426
+ }
427
+ if (["agent_assigned", "checkpoint_recorded", "handoff_recorded"].includes(value.type)) {
365
428
  safeIdentifier(value.assignmentId, "assignmentId");
366
429
  }
367
430
  if (value.type === "agent_assigned") {
@@ -397,6 +460,9 @@ function validateEventShape(value, definition) {
397
460
  } else if (value.type === "handoff_recorded") {
398
461
  safeIdentifier(value.fromAgentId, "fromAgentId");
399
462
  safeIdentifier(value.toAgentId, "toAgentId");
463
+ if (value.fromAgentId === value.toAgentId) {
464
+ throw new Error("fromAgentId and toAgentId must differ.");
465
+ }
400
466
  nonEmptyString(value.summary, "Handoff summary");
401
467
  stringArray(value.evidenceRefs, "Handoff evidenceRefs", { allowEmpty: false });
402
468
  nonEmptyString(value.nextAction, "Handoff nextAction");
@@ -418,6 +484,44 @@ function validateEventShape(value, definition) {
418
484
  if (!gateIds.has(value.gateId)) throw new Error(`Unknown integration gate ${value.gateId}.`);
419
485
  nonEmptyString(value.summary, "Integration gate summary");
420
486
  stringArray(value.evidenceRefs, "Integration gate evidenceRefs", { allowEmpty: false });
487
+ } else if (value.type === "verification_deferred") {
488
+ safeIdentifier(value.verificationId, "verificationId");
489
+ safeIdentifier(value.ownerAgentId, "ownerAgentId");
490
+ const checkIds = stringArray(
491
+ value.checkIds,
492
+ "Deferred verification checkIds",
493
+ { allowEmpty: false },
494
+ );
495
+ for (const checkId of checkIds) safeIdentifier(checkId, "checkId");
496
+ nonEmptyString(value.reason, "Deferred verification reason");
497
+ safeIdentifier(value.dueGateId, "dueGateId");
498
+ if (!gateIds.has(value.dueGateId)) {
499
+ throw new Error(`Unknown deferred verification gate ${value.dueGateId}.`);
500
+ }
501
+ } else if (value.type === "verification_closed") {
502
+ safeIdentifier(value.verificationId, "verificationId");
503
+ if (!VERIFICATION_DISPOSITIONS.has(value.disposition)) {
504
+ throw new Error("Deferred verification disposition is invalid.");
505
+ }
506
+ nonEmptyString(value.summary, "Deferred verification close summary");
507
+ const evidenceRefs = stringArray(
508
+ value.evidenceRefs,
509
+ "Deferred verification close evidenceRefs",
510
+ );
511
+ const decisionIds = stringArray(
512
+ value.decisionIds,
513
+ "Deferred verification close decisionIds",
514
+ );
515
+ for (const decisionId of decisionIds) safeIdentifier(decisionId, "decisionId");
516
+ if (value.disposition === "verified" && evidenceRefs.length === 0) {
517
+ throw new Error("Verified deferred verification requires evidence.");
518
+ }
519
+ if (value.disposition === "verified" && decisionIds.length > 0) {
520
+ throw new Error("Verified deferred verification must close with evidence, not decision IDs.");
521
+ }
522
+ if (value.disposition === "no_longer_required" && decisionIds.length === 0) {
523
+ throw new Error("No-longer-required deferred verification requires a resolved decision.");
524
+ }
421
525
  } else if (value.type === "data_candidate_recorded") {
422
526
  safeIdentifier(value.candidateId, "candidateId");
423
527
  nonEmptyString(value.sourceRef, "Data candidate sourceRef");
@@ -439,6 +543,7 @@ function projectEvents(definition, records) {
439
543
  const handoffs = new Map();
440
544
  const decisions = new Map();
441
545
  const gatePasses = new Map();
546
+ const verifications = new Map();
442
547
  const dataCandidates = [];
443
548
  const seenAssignments = new Set();
444
549
  const seenDecisions = new Set();
@@ -470,6 +575,34 @@ function projectEvents(definition, records) {
470
575
  decision.resolved = { ...event, ref };
471
576
  } else if (event.type === "integration_gate_passed") {
472
577
  gatePasses.set(event.gateId, { ...event, ref });
578
+ } else if (event.type === "verification_deferred") {
579
+ if (verifications.has(event.verificationId)) {
580
+ throw new Error(`Duplicate deferred verification ${event.verificationId}.`);
581
+ }
582
+ if (gatePasses.has(event.dueGateId)) {
583
+ throw new Error(
584
+ `Deferred verification cannot target integration gate ${event.dueGateId}; gate already passed.`,
585
+ );
586
+ }
587
+ verifications.set(event.verificationId, {
588
+ deferred: { ...event, ref },
589
+ closed: null,
590
+ });
591
+ } else if (event.type === "verification_closed") {
592
+ const verification = verifications.get(event.verificationId);
593
+ if (!verification || verification.closed) {
594
+ throw new Error(`Deferred verification ${event.verificationId} is not open.`);
595
+ }
596
+ if (event.disposition === "no_longer_required") {
597
+ for (const decisionId of event.decisionIds) {
598
+ if (!decisions.get(decisionId)?.resolved) {
599
+ throw new Error(
600
+ `No-longer-required deferred verification requires resolved decision ${decisionId}.`,
601
+ );
602
+ }
603
+ }
604
+ }
605
+ verification.closed = { ...event, ref };
473
606
  } else if (event.type === "data_candidate_recorded") {
474
607
  if (dataCandidates.some(row => row.candidateId === event.candidateId)) {
475
608
  throw new Error(`Duplicate data candidate ${event.candidateId}.`);
@@ -484,6 +617,7 @@ function projectEvents(definition, records) {
484
617
  handoffs,
485
618
  decisions,
486
619
  gatePasses,
620
+ verifications,
487
621
  dataCandidates,
488
622
  };
489
623
  }
@@ -539,25 +673,293 @@ function executionTruthFor(workspaceRoot, assignment) {
539
673
  };
540
674
  }
541
675
 
676
+ function compareIdentifiers(left, right) {
677
+ return left < right ? -1 : left > right ? 1 : 0;
678
+ }
679
+
680
+ function uniqueSorted(values) {
681
+ return [...new Set(values)].sort(compareIdentifiers);
682
+ }
683
+
684
+ function sortWorkItemIds(ids, priority) {
685
+ return [...new Set(ids)].sort((left, right) => (
686
+ (priority.get(left) ?? Number.MAX_SAFE_INTEGER)
687
+ - (priority.get(right) ?? Number.MAX_SAFE_INTEGER)
688
+ || compareIdentifiers(left, right)
689
+ ));
690
+ }
691
+
692
+ function dominantGapCandidateOrder(status) {
693
+ const byId = new Map(status.workItems.map(row => [row.workItemId, row]));
694
+ const failed = status.workItems
695
+ .filter(row => row.status === "failed")
696
+ .map(row => row.workItemId)
697
+ .sort(compareIdentifiers);
698
+ const failedIds = new Set(failed);
699
+ const critical = status.criticalWorkItems
700
+ .filter(workItemId => (
701
+ byId.get(workItemId)?.status !== "completed"
702
+ && !failedIds.has(workItemId)
703
+ ));
704
+ const criticalIds = new Set([...failed, ...critical]);
705
+ const remaining = status.workItems
706
+ .filter(row => row.status !== "completed" && !criticalIds.has(row.workItemId))
707
+ .map(row => row.workItemId)
708
+ .sort(compareIdentifiers);
709
+ return [...failed, ...critical, ...remaining];
710
+ }
711
+
712
+ function unresolvedRootWorkItemIds(workItemId, byId, priority, visiting = new Set()) {
713
+ const row = byId.get(workItemId);
714
+ if (!row || visiting.has(workItemId)) return [workItemId];
715
+ const nextVisiting = new Set(visiting).add(workItemId);
716
+ const unresolved = row.unresolvedDependencies.filter(id => byId.has(id));
717
+ if (unresolved.length === 0) return [workItemId];
718
+ return sortWorkItemIds(
719
+ unresolved.flatMap(id => unresolvedRootWorkItemIds(id, byId, priority, nextVisiting)),
720
+ priority,
721
+ );
722
+ }
723
+
724
+ function openDecisionsForWorkItem(row, openDecisions) {
725
+ const decisionRefs = new Set([
726
+ ...row.decisionRefs,
727
+ ...row.blockers
728
+ .filter(ref => ref.startsWith("decision:"))
729
+ .map(ref => ref.slice("decision:".length)),
730
+ ]);
731
+ return openDecisions
732
+ .filter(decision => (
733
+ decisionRefs.has(decision.decisionId)
734
+ || decision.blockingWorkItemIds.includes(row.workItemId)
735
+ ))
736
+ .sort((left, right) => compareIdentifiers(left.decisionId, right.decisionId));
737
+ }
738
+
739
+ function workItemGapReason(row) {
740
+ if (row.status === "failed") {
741
+ return row.summary
742
+ ?? (row.executionTruth?.issues?.length > 0 ? "Execution truth is invalid." : null)
743
+ ?? "Work item failed.";
744
+ }
745
+ return row.blockers.length > 0 ? row.summary ?? "Work item is blocked." : null;
746
+ }
747
+
748
+ function nextActionForGap(gap, status) {
749
+ if (gap.kind === "decision") {
750
+ return { action: `Resolve ${gap.id}`, actorId: gap.agentId };
751
+ }
752
+ if (gap.kind === "deferred_verification") {
753
+ return { action: `Run deferred verification ${gap.id}`, actorId: gap.agentId };
754
+ }
755
+ if (gap.kind === "integration_gate") {
756
+ return { action: `Review integration gate ${gap.id}`, actorId: null };
757
+ }
758
+ if (gap.kind === "none") return { action: null, actorId: null };
759
+
760
+ const workItem = status.workItems.find(row => row.workItemId === gap.workItemId);
761
+ if (workItem?.pendingHandoff) {
762
+ return {
763
+ action: `Accept handoff ${gap.workItemId}`,
764
+ actorId: workItem.pendingHandoff.toAgentId,
765
+ };
766
+ }
767
+ if (!workItem?.agentId) return { action: `Assign ${gap.workItemId}`, actorId: null };
768
+ if (workItem.nextAction) {
769
+ return { action: workItem.nextAction, actorId: workItem.agentId };
770
+ }
771
+ if (workItem.status === "failed") {
772
+ return { action: `Rework ${gap.workItemId}`, actorId: workItem.agentId };
773
+ }
774
+ if (workItem.status === "verifying") {
775
+ return { action: `Verify ${gap.workItemId}`, actorId: workItem.agentId };
776
+ }
777
+ if (workItem.status === "ready_to_integrate") {
778
+ return { action: `Integrate ${gap.workItemId}`, actorId: workItem.agentId };
779
+ }
780
+ return { action: `Continue ${gap.workItemId}.`, actorId: workItem.agentId };
781
+ }
782
+
783
+ function singleLine(value) {
784
+ return String(value).replace(/\s+/gu, " ").trim();
785
+ }
786
+
787
+ function gapLabel(gap) {
788
+ if (gap.kind === "none") return "none";
789
+ if (gap.kind === "decision") return `decision ${gap.id} (${gap.title})`;
790
+ if (gap.kind === "deferred_verification") {
791
+ return `deferred verification ${gap.id}`;
792
+ }
793
+ if (gap.kind === "failed_work") return `failed work ${gap.workItemId} (${gap.title})`;
794
+ if (gap.kind === "work_item") return `work item ${gap.workItemId} (${gap.title})`;
795
+ return `integration gate ${gap.id} (${gap.title})`;
796
+ }
797
+
798
+ function buildProjectHeadline(gap, next) {
799
+ const owner = gap.agentId ?? next.actorId ?? "unassigned";
800
+ const blocker = gap.reason ?? "none";
801
+ const action = next.action ?? "none";
802
+ const normalizedAction = singleLine(action);
803
+ const actionTerminator = /[.!?。!?]$/u.test(normalizedAction) ? "" : ".";
804
+ return `Dominant gap: ${singleLine(gapLabel(gap))}; owner: ${singleLine(owner)}; `
805
+ + `blocker: ${singleLine(blocker)}; next: ${normalizedAction}${actionTerminator}`;
806
+ }
807
+
808
+ function deriveProjectDriverSummary(status) {
809
+ const byId = new Map(status.workItems.map(row => [row.workItemId, row]));
810
+ const priority = new Map(
811
+ dominantGapCandidateOrder(status).map((workItemId, index) => [workItemId, index]),
812
+ );
813
+ let gap = null;
814
+
815
+ for (const candidateId of dominantGapCandidateOrder(status)) {
816
+ const rootId = unresolvedRootWorkItemIds(candidateId, byId, priority)[0];
817
+ const root = byId.get(rootId);
818
+ if (!root) continue;
819
+ if (root.pendingHandoff) {
820
+ gap = {
821
+ kind: "work_item",
822
+ id: root.workItemId,
823
+ title: root.title,
824
+ workItemId: root.workItemId,
825
+ agentId: root.pendingHandoff.toAgentId,
826
+ reason: root.pendingHandoff.summary,
827
+ truthRefs: uniqueSorted(root.truthRefs),
828
+ };
829
+ } else if (openDecisionsForWorkItem(root, status.openDecisions).length > 0) {
830
+ const decisions = openDecisionsForWorkItem(root, status.openDecisions);
831
+ const decision = decisions[0];
832
+ gap = {
833
+ kind: "decision",
834
+ id: decision.decisionId,
835
+ title: decision.title,
836
+ workItemId: root.workItemId,
837
+ agentId: decision.ownerAgentId,
838
+ reason: decision.question,
839
+ truthRefs: uniqueSorted([...decision.truthRefs, ...root.truthRefs]),
840
+ };
841
+ } else {
842
+ gap = {
843
+ kind: root.status === "failed" ? "failed_work" : "work_item",
844
+ id: root.workItemId,
845
+ title: root.title,
846
+ workItemId: root.workItemId,
847
+ agentId: root.agentId,
848
+ reason: workItemGapReason(root),
849
+ truthRefs: uniqueSorted(root.truthRefs),
850
+ };
851
+ }
852
+ break;
853
+ }
854
+
855
+ if (!gap && status.workItems.every(row => row.status === "completed")) {
856
+ const firstUnpassedGate = status.integrationGates
857
+ .find(gate => gate.status !== "passed") ?? null;
858
+ const gateDecisions = firstUnpassedGate
859
+ ? status.openDecisions
860
+ .filter(decision => firstUnpassedGate.unresolvedDecisionIds.includes(decision.decisionId))
861
+ .sort((left, right) => compareIdentifiers(left.decisionId, right.decisionId))
862
+ : [];
863
+ if (gateDecisions.length > 0) {
864
+ const decision = gateDecisions[0];
865
+ gap = {
866
+ kind: "decision",
867
+ id: decision.decisionId,
868
+ title: decision.title,
869
+ workItemId: null,
870
+ agentId: decision.ownerAgentId,
871
+ reason: decision.question,
872
+ truthRefs: uniqueSorted([...decision.truthRefs, ...firstUnpassedGate.truthRefs]),
873
+ };
874
+ } else if (firstUnpassedGate?.unresolvedVerificationIds.length > 0) {
875
+ const verification = status.deferredVerifications.find(row => (
876
+ row.verificationId === firstUnpassedGate.unresolvedVerificationIds[0]
877
+ ));
878
+ gap = {
879
+ kind: "deferred_verification",
880
+ id: verification.verificationId,
881
+ title: `Deferred verification ${verification.verificationId}`,
882
+ workItemId: verification.workItemId,
883
+ agentId: verification.ownerAgentId,
884
+ reason: verification.reason,
885
+ truthRefs: uniqueSorted([
886
+ ...verification.truthRefs,
887
+ ...firstUnpassedGate.truthRefs,
888
+ ]),
889
+ };
890
+ } else if (firstUnpassedGate) {
891
+ gap = {
892
+ kind: "integration_gate",
893
+ id: firstUnpassedGate.gateId,
894
+ title: firstUnpassedGate.title,
895
+ workItemId: null,
896
+ agentId: null,
897
+ reason: "Integration gate has not passed.",
898
+ truthRefs: uniqueSorted(firstUnpassedGate.truthRefs),
899
+ };
900
+ } else if (status.openDecisions.length > 0) {
901
+ const decision = status.openDecisions[0];
902
+ gap = {
903
+ kind: "decision",
904
+ id: decision.decisionId,
905
+ title: decision.title,
906
+ workItemId: null,
907
+ agentId: decision.ownerAgentId,
908
+ reason: decision.question,
909
+ truthRefs: uniqueSorted(decision.truthRefs),
910
+ };
911
+ }
912
+ }
913
+
914
+ if (!gap) {
915
+ gap = {
916
+ kind: "none",
917
+ id: null,
918
+ title: null,
919
+ workItemId: null,
920
+ agentId: null,
921
+ reason: null,
922
+ truthRefs: uniqueSorted([
923
+ "project/definition.json",
924
+ ...status.integrationGates.flatMap(gate => gate.truthRefs),
925
+ ]),
926
+ };
927
+ }
928
+
929
+ const next = nextActionForGap(gap, status);
930
+ return {
931
+ headline: buildProjectHeadline(gap, next),
932
+ dominantGap: gap,
933
+ nextAction: next.action,
934
+ nextActorId: next.actorId,
935
+ };
936
+ }
937
+
542
938
  function projectStatus(definition, records, workspaceRoot = null) {
543
939
  const state = projectEvents(definition, records);
544
940
  const completed = new Set();
545
941
  for (const [id, checkpoint] of state.checkpoints) {
546
942
  if (checkpoint.state === "completed") completed.add(id);
547
943
  }
548
- const workItems = definition.workItems.map((item) => {
944
+ let workItems = definition.workItems.map((item) => {
549
945
  const assignment = state.assignments.get(item.id) ?? null;
550
946
  const checkpoint = state.checkpoints.get(item.id) ?? null;
551
947
  const handoff = state.handoffs.get(item.id) ?? null;
948
+ const latestFact = latestEventFact(checkpoint, handoff);
949
+ const activeHandoff = handoff
950
+ && handoff.assignmentId === assignment?.assignmentId
951
+ && handoff.fromAgentId === assignment?.agentId
952
+ ? handoff
953
+ : null;
954
+ const pendingHandoff = activeHandoff && latestFact?.eventId === activeHandoff.eventId
955
+ ? pendingHandoffProjection(activeHandoff)
956
+ : null;
552
957
  const executionTruth = executionTruthFor(workspaceRoot, assignment);
553
958
  const unresolvedDependencies = item.dependencies.filter(id => !completed.has(id));
554
959
  let status = "planned";
555
960
  if (assignment) status = unresolvedDependencies.length > 0 ? "waiting_dependency" : "active";
556
- if (handoff && handoff.assignmentId === assignment?.assignmentId) status = "planned";
557
- if (checkpoint) status = checkpoint.state;
558
- if (status === "active" && checkpoint?.decisionRefs.some((id) => (
559
- state.decisions.get(id)?.resolved === null
560
- ))) status = "waiting_decision";
961
+ if (pendingHandoff) status = "planned";
962
+ if (latestFact?.type === "checkpoint_recorded") status = checkpoint.state;
561
963
  if (executionTruth && (executionTruth.status !== "bound"
562
964
  || (executionTruth.lifecycle === "closed"
563
965
  && executionTruth.closeoutDecision !== "accepted"))) {
@@ -598,18 +1000,20 @@ function projectStatus(definition, records, workspaceRoot = null) {
598
1000
  executionTruth,
599
1001
  dependencies: [...item.dependencies],
600
1002
  unresolvedDependencies,
601
- blockers: checkpoint?.blockerRefs ?? unresolvedDependencies.map(id => `work-item:${id}`),
602
- decisionRefs: checkpoint?.decisionRefs ?? [],
1003
+ blockers: latestFact?.type === "checkpoint_recorded"
1004
+ ? checkpoint.blockerRefs
1005
+ : unresolvedDependencies.map(id => `work-item:${id}`),
1006
+ decisionRefs: latestFact?.type === "checkpoint_recorded" ? checkpoint.decisionRefs : [],
603
1007
  progress: measured,
604
- summary: checkpoint?.summary ?? handoff?.summary ?? null,
605
- nextAction: checkpoint ? checkpoint.nextAction : (handoff?.nextAction ?? null),
1008
+ summary: latestFact?.summary ?? null,
1009
+ nextAction: latestFact?.nextAction ?? null,
1010
+ pendingHandoff,
606
1011
  lastCheckpointAt: checkpoint?.occurredAt ?? null,
607
- evidenceRefs: checkpoint?.evidenceRefs ?? handoff?.evidenceRefs ?? [],
1012
+ evidenceRefs: latestFact?.evidenceRefs ?? [],
608
1013
  assignmentHistory: history,
609
1014
  truthRefs,
610
1015
  };
611
1016
  });
612
- const byId = new Map(workItems.map(row => [row.workItemId, row]));
613
1017
  const openDecisions = [...state.decisions.values()]
614
1018
  .filter(row => row.resolved === null)
615
1019
  .map(row => ({
@@ -622,6 +1026,45 @@ function projectStatus(definition, records, workspaceRoot = null) {
622
1026
  truthRefs: [row.opened.ref],
623
1027
  }))
624
1028
  .sort((left, right) => left.decisionId.localeCompare(right.decisionId));
1029
+ const openDecisionIds = new Set(openDecisions.map(row => row.decisionId));
1030
+ workItems = workItems.map((row) => {
1031
+ const terminal = ["completed", "failed"].includes(row.status);
1032
+ const currentDecisionIds = terminal
1033
+ ? []
1034
+ : openDecisionsForWorkItem(row, openDecisions).map(decision => decision.decisionId);
1035
+ const checkpointDecisionIds = uniqueSorted([
1036
+ ...row.decisionRefs,
1037
+ ...row.blockers
1038
+ .filter(ref => ref.startsWith("decision:"))
1039
+ .map(ref => ref.slice("decision:".length)),
1040
+ ]);
1041
+ const staleDecisionRefs = checkpointDecisionIds.filter(id => !openDecisionIds.has(id));
1042
+ const currentBlockers = row.blockers.filter(ref => (
1043
+ !ref.startsWith("decision:") || openDecisionIds.has(ref.slice("decision:".length))
1044
+ ));
1045
+ for (const decisionId of currentDecisionIds) {
1046
+ const blockerRef = `decision:${decisionId}`;
1047
+ if (!currentBlockers.includes(blockerRef)) currentBlockers.push(blockerRef);
1048
+ }
1049
+ const underlyingStatus = row.assignmentId === null
1050
+ ? "planned"
1051
+ : row.unresolvedDependencies.length > 0 ? "waiting_dependency" : "active";
1052
+ let status = row.status;
1053
+ if (!row.pendingHandoff && !terminal) {
1054
+ status = currentDecisionIds.length > 0
1055
+ ? "waiting_decision"
1056
+ : row.status === "waiting_decision" ? underlyingStatus : row.status;
1057
+ }
1058
+ return {
1059
+ ...row,
1060
+ status,
1061
+ blockers: currentBlockers,
1062
+ decisionRefs: row.decisionRefs.filter(id => openDecisionIds.has(id)),
1063
+ summary: staleDecisionRefs.length > 0 ? null : row.summary,
1064
+ nextAction: staleDecisionRefs.length > 0 ? null : row.nextAction,
1065
+ };
1066
+ });
1067
+ const byId = new Map(workItems.map(row => [row.workItemId, row]));
625
1068
  const resolvedDecisions = [...state.decisions.values()]
626
1069
  .filter(row => row.resolved !== null)
627
1070
  .map(row => ({
@@ -633,11 +1076,42 @@ function projectStatus(definition, records, workspaceRoot = null) {
633
1076
  }))
634
1077
  .sort((left, right) => left.decisionId.localeCompare(right.decisionId));
635
1078
  const resolvedDecisionIds = new Set(resolvedDecisions.map(row => row.decisionId));
1079
+ const gateOrder = new Map(
1080
+ definition.integrationGates.map((gate, index) => [gate.id, index]),
1081
+ );
1082
+ const deferredVerifications = [...state.verifications.values()]
1083
+ .map((row) => ({
1084
+ verificationId: row.deferred.verificationId,
1085
+ workItemId: row.deferred.workItemId,
1086
+ ownerAgentId: row.deferred.ownerAgentId,
1087
+ checkIds: [...row.deferred.checkIds],
1088
+ reason: row.deferred.reason,
1089
+ dueGateId: row.deferred.dueGateId,
1090
+ status: row.closed ? "closed" : "open",
1091
+ disposition: row.closed?.disposition ?? null,
1092
+ summary: row.closed?.summary ?? null,
1093
+ evidenceRefs: row.closed ? [...row.closed.evidenceRefs] : [],
1094
+ decisionIds: row.closed ? [...row.closed.decisionIds] : [],
1095
+ truthRefs: [row.deferred.ref, ...(row.closed ? [row.closed.ref] : [])],
1096
+ }))
1097
+ .sort((left, right) => (
1098
+ gateOrder.get(left.dueGateId) - gateOrder.get(right.dueGateId)
1099
+ || compareIdentifiers(left.verificationId, right.verificationId)
1100
+ ));
1101
+ const openDeferredVerifications = deferredVerifications.filter(row => row.status === "open");
1102
+ const openDeferredVerificationIds = openDeferredVerifications
1103
+ .map(row => row.verificationId);
636
1104
  const integrationGates = definition.integrationGates.map((gate) => {
637
1105
  const unresolvedWorkItemIds = gate.requiredWorkItemIds.filter(id => byId.get(id).status !== "completed");
638
1106
  const unresolvedDecisionIds = gate.requiredDecisionIds.filter(id => !resolvedDecisionIds.has(id));
1107
+ const gateVerifications = deferredVerifications.filter(row => row.dueGateId === gate.id);
1108
+ const unresolvedVerificationIds = gateVerifications
1109
+ .filter(row => row.status === "open")
1110
+ .map(row => row.verificationId);
639
1111
  const pass = state.gatePasses.get(gate.id) ?? null;
640
- let status = unresolvedWorkItemIds.length === 0 && unresolvedDecisionIds.length === 0
1112
+ let status = unresolvedWorkItemIds.length === 0
1113
+ && unresolvedDecisionIds.length === 0
1114
+ && unresolvedVerificationIds.length === 0
641
1115
  ? "ready"
642
1116
  : "blocked";
643
1117
  if (pass) {
@@ -650,8 +1124,13 @@ function projectStatus(definition, records, workspaceRoot = null) {
650
1124
  status,
651
1125
  unresolvedWorkItemIds,
652
1126
  unresolvedDecisionIds,
1127
+ unresolvedVerificationIds,
653
1128
  evidenceRefs: pass?.evidenceRefs ?? [],
654
- truthRefs: ["project/definition.json", ...(pass ? [pass.ref] : [])],
1129
+ truthRefs: [
1130
+ "project/definition.json",
1131
+ ...gateVerifications.flatMap(row => row.truthRefs),
1132
+ ...(pass ? [pass.ref] : []),
1133
+ ],
655
1134
  };
656
1135
  });
657
1136
  const stateNames = [
@@ -666,7 +1145,10 @@ function projectStatus(definition, records, workspaceRoot = null) {
666
1145
  const orderedCounts = { total: counts.total };
667
1146
  for (const name of stateNames) orderedCounts[name] = counts[name];
668
1147
  const readyQueue = workItems
669
- .filter(row => row.status === "planned" && row.unresolvedDependencies.length === 0)
1148
+ .filter(row => row.status === "planned"
1149
+ && row.assignmentId === null
1150
+ && row.pendingHandoff === null
1151
+ && row.unresolvedDependencies.length === 0)
670
1152
  .map(row => row.workItemId)
671
1153
  .sort();
672
1154
  const downstream = new Map(workItems.map(row => [row.workItemId, 0]));
@@ -732,10 +1214,11 @@ function projectStatus(definition, records, workspaceRoot = null) {
732
1214
  });
733
1215
  let overall = "planned";
734
1216
  if (workItems.every(row => row.status === "completed")
735
- && integrationGates.every(row => row.status === "passed")) overall = "completed";
1217
+ && integrationGates.every(row => row.status === "passed")
1218
+ && openDecisions.length === 0) overall = "completed";
736
1219
  else if (openDecisions.length > 0 || workItems.some(row => row.status === "failed")) overall = "active_with_blockers";
737
- else if (workItems.some(row => row.status !== "planned")) overall = "active";
738
- return {
1220
+ else if (workItems.some(row => row.status !== "planned" || row.pendingHandoff !== null)) overall = "active";
1221
+ const status = {
739
1222
  schemaVersion: STATUS_SCHEMA,
740
1223
  status: "team_project_status",
741
1224
  projectId: definition.projectId,
@@ -750,11 +1233,14 @@ function projectStatus(definition, records, workspaceRoot = null) {
750
1233
  criticalWorkItems,
751
1234
  openDecisions,
752
1235
  resolvedDecisions,
1236
+ deferredVerifications,
1237
+ openDeferredVerificationIds,
753
1238
  integrationGates,
754
1239
  dataCandidates,
755
1240
  projectTruthHash: sha256(canonical({ definition, events: records.map(row => row.event) })),
756
1241
  authorizationGranted: false,
757
1242
  };
1243
+ return { ...status, ...deriveProjectDriverSummary(status) };
758
1244
  }
759
1245
 
760
1246
  function validateEventAgainstState(event, definition, records, workspaceRoot) {
@@ -778,6 +1264,15 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
778
1264
  if (!current && normalized.supersedesAssignmentId !== undefined) {
779
1265
  throw new Error("supersedesAssignmentId has no current assignment.");
780
1266
  }
1267
+ const currentProjection = status.workItems.find(
1268
+ row => row.workItemId === normalized.workItemId,
1269
+ );
1270
+ if (currentProjection?.pendingHandoff
1271
+ && normalized.agentId !== currentProjection.pendingHandoff.toAgentId) {
1272
+ throw new Error(
1273
+ `Assignment must accept pending handoff target ${currentProjection.pendingHandoff.toAgentId}.`,
1274
+ );
1275
+ }
781
1276
  for (const projected of status.workItems) {
782
1277
  if (projected.workItemId === normalized.workItemId
783
1278
  || projected.assignmentId === null
@@ -794,6 +1289,17 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
794
1289
  if (!current || current.assignmentId !== normalized.assignmentId) {
795
1290
  throw new Error("Checkpoint must bind the current assignment.");
796
1291
  }
1292
+ if (normalized.state !== "completed") {
1293
+ const invalidatedGate = definition.integrationGates.find(gate => (
1294
+ gate.requiredWorkItemIds.includes(normalized.workItemId)
1295
+ && state.gatePasses.has(gate.id)
1296
+ ));
1297
+ if (invalidatedGate) {
1298
+ throw new Error(
1299
+ `Checkpoint cannot invalidate passed integration gate ${invalidatedGate.id}.`,
1300
+ );
1301
+ }
1302
+ }
797
1303
  const unresolved = status.workItems.find(row => row.workItemId === normalized.workItemId).unresolvedDependencies;
798
1304
  if (normalized.state === "completed" && unresolved.length > 0) {
799
1305
  throw new Error(`Completed checkpoint has unresolved dependencies: ${unresolved.join(", ")}.`);
@@ -824,6 +1330,10 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
824
1330
  }
825
1331
  } else if (normalized.type === "handoff_recorded") {
826
1332
  const current = state.assignments.get(normalized.workItemId);
1333
+ const projected = status.workItems.find(row => row.workItemId === normalized.workItemId);
1334
+ if (["completed", "failed"].includes(projected?.status)) {
1335
+ throw new Error("Handoff cannot target a completed or failed WorkItem.");
1336
+ }
827
1337
  if (!current
828
1338
  || current.assignmentId !== normalized.assignmentId
829
1339
  || current.agentId !== normalized.fromAgentId) {
@@ -834,8 +1344,35 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
834
1344
  } else if (normalized.type === "decision_resolved") {
835
1345
  const decision = state.decisions.get(normalized.decisionId);
836
1346
  if (!decision || decision.resolved) throw new Error(`Decision ${normalized.decisionId} is not open.`);
1347
+ } else if (normalized.type === "verification_deferred") {
1348
+ if (state.verifications.has(normalized.verificationId)) {
1349
+ throw new Error(`Deferred verification ${normalized.verificationId} already exists.`);
1350
+ }
1351
+ if (state.gatePasses.has(normalized.dueGateId)) {
1352
+ throw new Error(
1353
+ `Deferred verification cannot target integration gate ${normalized.dueGateId}; gate already passed.`,
1354
+ );
1355
+ }
1356
+ } else if (normalized.type === "verification_closed") {
1357
+ const verification = state.verifications.get(normalized.verificationId);
1358
+ if (!verification || verification.closed) {
1359
+ throw new Error(`Deferred verification ${normalized.verificationId} is not open.`);
1360
+ }
1361
+ if (normalized.disposition === "no_longer_required") {
1362
+ for (const decisionId of normalized.decisionIds) {
1363
+ const decision = state.decisions.get(decisionId);
1364
+ if (!decision?.resolved) {
1365
+ throw new Error(
1366
+ `No-longer-required deferred verification requires resolved decision ${decisionId}.`,
1367
+ );
1368
+ }
1369
+ }
1370
+ }
837
1371
  } else if (normalized.type === "integration_gate_passed") {
838
1372
  const gate = status.integrationGates.find(row => row.gateId === normalized.gateId);
1373
+ if (state.gatePasses.has(normalized.gateId)) {
1374
+ throw new Error(`Integration gate ${normalized.gateId} has already passed.`);
1375
+ }
839
1376
  if (gate.status !== "ready") throw new Error(`Integration gate ${normalized.gateId} is not ready.`);
840
1377
  } else if (normalized.type === "data_candidate_recorded") {
841
1378
  if (state.dataCandidates.some(row => row.candidateId === normalized.candidateId)) {
@@ -846,11 +1383,11 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
846
1383
  }
847
1384
 
848
1385
  export function initializeTeamProjectV1({ workspaceRoot, definition }) {
1386
+ const normalized = validateDefinition(definition);
849
1387
  if (!existsSync(workspaceRoot)) mkdirSync(workspaceRoot, { recursive: true });
850
1388
  const { root, projectRoot: rootPath } = projectRoot(workspaceRoot);
851
1389
  assertRealProjectTree(root, rootPath);
852
1390
  assertRealProjectTree(root, path.join(rootPath, "events"));
853
- const normalized = validateDefinition(definition);
854
1391
  const definitionPath = path.join(rootPath, "definition.json");
855
1392
  if (existsSync(definitionPath)) {
856
1393
  const existing = readRegularJson(definitionPath, "Team project definition");
@@ -927,6 +1464,432 @@ export function appendTeamProjectEventV1({ workspaceRoot, event }) {
927
1464
  });
928
1465
  }
929
1466
 
1467
+ export function assignTeamProjectV1({
1468
+ workspaceRoot,
1469
+ assignmentId,
1470
+ occurredAt,
1471
+ workItemId,
1472
+ agentId,
1473
+ supersedesAssignmentId,
1474
+ executionRunId,
1475
+ executionWorkItemId,
1476
+ }) {
1477
+ safeIdentifier(assignmentId, "assignmentId");
1478
+ const event = {
1479
+ schemaVersion: EVENT_SCHEMA,
1480
+ eventId: `assignment-${assignmentId}`,
1481
+ type: "agent_assigned",
1482
+ occurredAt,
1483
+ assignmentId,
1484
+ workItemId,
1485
+ agentId,
1486
+ };
1487
+ if (supersedesAssignmentId !== undefined) {
1488
+ event.supersedesAssignmentId = supersedesAssignmentId;
1489
+ }
1490
+ if (executionRunId !== undefined) event.executionRunId = executionRunId;
1491
+ if (executionWorkItemId !== undefined) event.executionWorkItemId = executionWorkItemId;
1492
+
1493
+ const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
1494
+ const status = readTeamProjectStatusV1({ workspaceRoot });
1495
+ if (recorded.resumed) {
1496
+ const current = status.workItems.find(row => row.workItemId === workItemId);
1497
+ if (current?.assignmentId !== assignmentId) {
1498
+ throw new Error(
1499
+ `Cannot resume historical/superseded assignment ${assignmentId}; `
1500
+ + `current assignment=${current?.assignmentId ?? "none"}, `
1501
+ + `current owner=${current?.agentId ?? "none"}.`,
1502
+ );
1503
+ }
1504
+ }
1505
+ return {
1506
+ status: "team_project_assignment",
1507
+ assignmentId,
1508
+ workItemId,
1509
+ agentId,
1510
+ eventId: recorded.eventId,
1511
+ eventPath: recorded.eventPath,
1512
+ eventSha256: recorded.eventSha256,
1513
+ resumed: recorded.resumed,
1514
+ projectTruthHash: status.projectTruthHash,
1515
+ nextAction: status.nextAction,
1516
+ nextActorId: status.nextActorId,
1517
+ authorizationGranted: false,
1518
+ exitCode: 0,
1519
+ };
1520
+ }
1521
+
1522
+ export function handoffTeamProjectV1({
1523
+ workspaceRoot,
1524
+ handoffId,
1525
+ occurredAt,
1526
+ assignmentId,
1527
+ workItemId,
1528
+ fromAgentId,
1529
+ toAgentId,
1530
+ summary,
1531
+ nextAction,
1532
+ evidenceRefs,
1533
+ }) {
1534
+ safeIdentifier(handoffId, "handoffId");
1535
+ const event = {
1536
+ schemaVersion: EVENT_SCHEMA,
1537
+ eventId: `handoff-${handoffId}`,
1538
+ type: "handoff_recorded",
1539
+ occurredAt,
1540
+ assignmentId,
1541
+ workItemId,
1542
+ fromAgentId,
1543
+ toAgentId,
1544
+ summary,
1545
+ evidenceRefs,
1546
+ nextAction,
1547
+ };
1548
+ const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
1549
+ const status = readTeamProjectStatusV1({ workspaceRoot });
1550
+ const current = status.workItems.find(row => row.workItemId === workItemId);
1551
+ const pending = current?.pendingHandoff ?? null;
1552
+ if (recorded.resumed && (
1553
+ current?.assignmentId !== assignmentId
1554
+ || current?.agentId !== fromAgentId
1555
+ || pending?.eventId !== event.eventId
1556
+ )) {
1557
+ throw new Error(
1558
+ `Cannot resume historical/superseded handoff ${handoffId}; `
1559
+ + `current assignment=${current?.assignmentId ?? "none"}, `
1560
+ + `current owner=${current?.agentId ?? "none"}, `
1561
+ + `current pending target=${pending?.toAgentId ?? "none"}.`,
1562
+ );
1563
+ }
1564
+ return {
1565
+ status: "team_project_handoff",
1566
+ handoffId,
1567
+ assignmentId,
1568
+ workItemId,
1569
+ fromAgentId,
1570
+ toAgentId,
1571
+ eventId: recorded.eventId,
1572
+ eventPath: recorded.eventPath,
1573
+ eventSha256: recorded.eventSha256,
1574
+ resumed: recorded.resumed,
1575
+ pending: true,
1576
+ targetNextAction: pending?.nextAction ?? nextAction,
1577
+ projectTruthHash: status.projectTruthHash,
1578
+ nextAction: status.nextAction,
1579
+ nextActorId: status.nextActorId,
1580
+ projectNextAction: status.nextAction,
1581
+ projectNextActorId: status.nextActorId,
1582
+ authorizationGranted: false,
1583
+ exitCode: 0,
1584
+ };
1585
+ }
1586
+
1587
+ export function openTeamProjectDecisionV1({
1588
+ workspaceRoot,
1589
+ decisionId,
1590
+ occurredAt,
1591
+ title,
1592
+ question,
1593
+ ownerAgentId,
1594
+ blockingWorkItemIds,
1595
+ options,
1596
+ }) {
1597
+ safeIdentifier(decisionId, "decisionId");
1598
+ const event = {
1599
+ schemaVersion: EVENT_SCHEMA,
1600
+ eventId: `decision-opened-${decisionId}`,
1601
+ type: "decision_opened",
1602
+ occurredAt,
1603
+ decisionId,
1604
+ title,
1605
+ question,
1606
+ ownerAgentId,
1607
+ blockingWorkItemIds,
1608
+ options,
1609
+ };
1610
+ const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
1611
+ const status = readTeamProjectStatusV1({ workspaceRoot });
1612
+ if (recorded.resumed && !status.openDecisions.some(row => row.decisionId === decisionId)) {
1613
+ const resolved = status.resolvedDecisions.some(row => row.decisionId === decisionId);
1614
+ throw new Error(
1615
+ `Cannot resume historical decision ${decisionId}; `
1616
+ + (resolved ? "decision is already resolved." : "decision is not currently open."),
1617
+ );
1618
+ }
1619
+ return {
1620
+ status: "team_project_decision",
1621
+ operation: "opened",
1622
+ decisionId,
1623
+ ownerAgentId,
1624
+ eventId: recorded.eventId,
1625
+ eventPath: recorded.eventPath,
1626
+ eventSha256: recorded.eventSha256,
1627
+ resumed: recorded.resumed,
1628
+ projectTruthHash: status.projectTruthHash,
1629
+ nextAction: status.nextAction,
1630
+ nextActorId: status.nextActorId,
1631
+ authorizationGranted: false,
1632
+ exitCode: 0,
1633
+ };
1634
+ }
1635
+
1636
+ export function resolveTeamProjectDecisionV1({
1637
+ workspaceRoot,
1638
+ decisionId,
1639
+ occurredAt,
1640
+ resolution,
1641
+ rationale,
1642
+ evidenceRefs,
1643
+ }) {
1644
+ safeIdentifier(decisionId, "decisionId");
1645
+ const event = {
1646
+ schemaVersion: EVENT_SCHEMA,
1647
+ eventId: `decision-resolved-${decisionId}`,
1648
+ type: "decision_resolved",
1649
+ occurredAt,
1650
+ decisionId,
1651
+ resolution,
1652
+ rationale,
1653
+ evidenceRefs,
1654
+ };
1655
+ const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
1656
+ const status = readTeamProjectStatusV1({ workspaceRoot });
1657
+ if (recorded.resumed && !status.resolvedDecisions.some(row => row.decisionId === decisionId)) {
1658
+ throw new Error(`Cannot resume decision ${decisionId}; decision is not resolved.`);
1659
+ }
1660
+ return {
1661
+ status: "team_project_decision",
1662
+ operation: "resolved",
1663
+ decisionId,
1664
+ eventId: recorded.eventId,
1665
+ eventPath: recorded.eventPath,
1666
+ eventSha256: recorded.eventSha256,
1667
+ resumed: recorded.resumed,
1668
+ projectTruthHash: status.projectTruthHash,
1669
+ nextAction: status.nextAction,
1670
+ nextActorId: status.nextActorId,
1671
+ authorizationGranted: false,
1672
+ exitCode: 0,
1673
+ };
1674
+ }
1675
+
1676
+ export function deferTeamProjectVerificationV1({
1677
+ workspaceRoot,
1678
+ verificationId,
1679
+ occurredAt,
1680
+ workItemId,
1681
+ ownerAgentId,
1682
+ checkIds,
1683
+ reason,
1684
+ dueGateId,
1685
+ }) {
1686
+ safeIdentifier(verificationId, "verificationId");
1687
+ const event = {
1688
+ schemaVersion: EVENT_SCHEMA,
1689
+ eventId: `verification-deferred-${verificationId}`,
1690
+ type: "verification_deferred",
1691
+ occurredAt,
1692
+ verificationId,
1693
+ workItemId,
1694
+ ownerAgentId,
1695
+ checkIds,
1696
+ reason,
1697
+ dueGateId,
1698
+ };
1699
+ const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
1700
+ const status = readTeamProjectStatusV1({ workspaceRoot });
1701
+ const verification = status.deferredVerifications.find(
1702
+ row => row.verificationId === verificationId,
1703
+ );
1704
+ if (recorded.resumed && verification?.status !== "open") {
1705
+ throw new Error(`Cannot resume closed deferred verification ${verificationId}.`);
1706
+ }
1707
+ const gate = status.integrationGates.find(row => row.gateId === dueGateId);
1708
+ return {
1709
+ status: "team_project_verification",
1710
+ operation: "deferred",
1711
+ verificationId,
1712
+ verificationStatus: verification?.status ?? null,
1713
+ dueGateId,
1714
+ gateStatus: gate?.status ?? null,
1715
+ eventId: recorded.eventId,
1716
+ eventPath: recorded.eventPath,
1717
+ eventSha256: recorded.eventSha256,
1718
+ resumed: recorded.resumed,
1719
+ overall: status.overall,
1720
+ projectTruthHash: status.projectTruthHash,
1721
+ nextAction: status.nextAction,
1722
+ nextActorId: status.nextActorId,
1723
+ verificationCommandExecuted: false,
1724
+ gitWritePerformed: false,
1725
+ releaseWritePerformed: false,
1726
+ authorizationGranted: false,
1727
+ exitCode: 0,
1728
+ };
1729
+ }
1730
+
1731
+ export function closeTeamProjectVerificationV1({
1732
+ workspaceRoot,
1733
+ verificationId,
1734
+ occurredAt,
1735
+ disposition,
1736
+ summary,
1737
+ evidenceRefs,
1738
+ decisionIds,
1739
+ }) {
1740
+ safeIdentifier(verificationId, "verificationId");
1741
+ const event = {
1742
+ schemaVersion: EVENT_SCHEMA,
1743
+ eventId: `verification-closed-${verificationId}`,
1744
+ type: "verification_closed",
1745
+ occurredAt,
1746
+ verificationId,
1747
+ disposition,
1748
+ summary,
1749
+ evidenceRefs,
1750
+ decisionIds,
1751
+ };
1752
+ const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
1753
+ const status = readTeamProjectStatusV1({ workspaceRoot });
1754
+ const verification = status.deferredVerifications.find(
1755
+ row => row.verificationId === verificationId,
1756
+ );
1757
+ if (verification?.status !== "closed" || verification.disposition !== disposition) {
1758
+ throw new Error(`Cannot resume deferred verification close ${verificationId}.`);
1759
+ }
1760
+ const gate = status.integrationGates.find(row => row.gateId === verification.dueGateId);
1761
+ return {
1762
+ status: "team_project_verification",
1763
+ operation: "closed",
1764
+ verificationId,
1765
+ verificationStatus: verification.disposition,
1766
+ dueGateId: verification.dueGateId,
1767
+ gateStatus: gate?.status ?? null,
1768
+ eventId: recorded.eventId,
1769
+ eventPath: recorded.eventPath,
1770
+ eventSha256: recorded.eventSha256,
1771
+ resumed: recorded.resumed,
1772
+ overall: status.overall,
1773
+ projectTruthHash: status.projectTruthHash,
1774
+ nextAction: status.nextAction,
1775
+ nextActorId: status.nextActorId,
1776
+ verificationCommandExecuted: false,
1777
+ gitWritePerformed: false,
1778
+ releaseWritePerformed: false,
1779
+ authorizationGranted: false,
1780
+ exitCode: 0,
1781
+ };
1782
+ }
1783
+
1784
+ export function integrateTeamProjectV1({
1785
+ workspaceRoot,
1786
+ gateId,
1787
+ occurredAt,
1788
+ summary,
1789
+ evidenceRefs,
1790
+ }) {
1791
+ safeIdentifier(gateId, "gateId");
1792
+ const event = {
1793
+ schemaVersion: EVENT_SCHEMA,
1794
+ eventId: `integration-gate-${gateId}`,
1795
+ type: "integration_gate_passed",
1796
+ occurredAt,
1797
+ gateId,
1798
+ summary,
1799
+ evidenceRefs,
1800
+ };
1801
+ const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
1802
+ const status = readTeamProjectStatusV1({ workspaceRoot });
1803
+ const gate = status.integrationGates.find(row => row.gateId === gateId);
1804
+ return {
1805
+ status: "team_project_integration",
1806
+ gateId,
1807
+ eventId: recorded.eventId,
1808
+ eventPath: recorded.eventPath,
1809
+ eventSha256: recorded.eventSha256,
1810
+ resumed: recorded.resumed,
1811
+ gateStatus: gate?.status ?? null,
1812
+ overall: status.overall,
1813
+ projectTruthHash: status.projectTruthHash,
1814
+ nextAction: status.nextAction,
1815
+ nextActorId: status.nextActorId,
1816
+ gitWritePerformed: false,
1817
+ releaseWritePerformed: false,
1818
+ authorizationGranted: false,
1819
+ exitCode: 0,
1820
+ };
1821
+ }
1822
+
1823
+ export function checkpointTeamProjectV1({
1824
+ workspaceRoot,
1825
+ checkpointId,
1826
+ occurredAt,
1827
+ assignmentId,
1828
+ workItemId,
1829
+ state,
1830
+ summary,
1831
+ completedUnits,
1832
+ evidenceRefs = [],
1833
+ blockerRefs = [],
1834
+ decisionRefs = [],
1835
+ nextAction = null,
1836
+ sourceFingerprint,
1837
+ }) {
1838
+ safeIdentifier(checkpointId, "checkpointId");
1839
+ const event = {
1840
+ schemaVersion: EVENT_SCHEMA,
1841
+ eventId: `checkpoint-${checkpointId}`,
1842
+ type: "checkpoint_recorded",
1843
+ occurredAt,
1844
+ assignmentId,
1845
+ workItemId,
1846
+ state,
1847
+ summary,
1848
+ ...(completedUnits === undefined ? {} : { completedUnits }),
1849
+ evidenceRefs,
1850
+ blockerRefs,
1851
+ decisionRefs,
1852
+ nextAction,
1853
+ ...(sourceFingerprint === undefined ? {} : { sourceFingerprint }),
1854
+ };
1855
+ const recorded = appendTeamProjectEventV1({ workspaceRoot, event });
1856
+ const status = readTeamProjectStatusV1({ workspaceRoot });
1857
+ if (recorded.resumed) {
1858
+ const loaded = loadProject(workspaceRoot);
1859
+ const stateProjection = projectEvents(loaded.definition, loaded.events);
1860
+ const currentAssignment = stateProjection.assignments.get(workItemId) ?? null;
1861
+ const latestFact = latestEventFact(
1862
+ stateProjection.checkpoints.get(workItemId) ?? null,
1863
+ stateProjection.handoffs.get(workItemId) ?? null,
1864
+ );
1865
+ if (currentAssignment?.assignmentId !== assignmentId || latestFact?.eventId !== event.eventId) {
1866
+ throw new Error(
1867
+ `Cannot resume historical/superseded checkpoint ${checkpointId}; `
1868
+ + `current assignment=${currentAssignment?.assignmentId ?? "none"}, `
1869
+ + `current owner=${currentAssignment?.agentId ?? "none"}, `
1870
+ + `latest fact=${latestFact?.eventId ?? "none"}.`,
1871
+ );
1872
+ }
1873
+ }
1874
+ return {
1875
+ status: "team_project_checkpoint",
1876
+ checkpointId,
1877
+ assignmentId,
1878
+ workItemId,
1879
+ state,
1880
+ eventId: recorded.eventId,
1881
+ eventPath: recorded.eventPath,
1882
+ eventSha256: recorded.eventSha256,
1883
+ resumed: recorded.resumed,
1884
+ projectTruthHash: status.projectTruthHash,
1885
+ overall: status.overall,
1886
+ nextAction: status.nextAction,
1887
+ nextActorId: status.nextActorId,
1888
+ authorizationGranted: false,
1889
+ exitCode: 0,
1890
+ };
1891
+ }
1892
+
930
1893
  export function readTeamProjectStatusV1({ workspaceRoot }) {
931
1894
  const loaded = loadProject(workspaceRoot);
932
1895
  return {
@@ -940,9 +1903,18 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
940
1903
  const loaded = loadProject(workspaceRoot);
941
1904
  const status = projectStatus(loaded.definition, loaded.events, loaded.root);
942
1905
  const responsibilities = status.workItems.filter(row => row.agentId === agentId);
943
- const current = responsibilities.find(row => !["completed", "failed"].includes(row.status))
1906
+ const current = responsibilities.find(row => (
1907
+ ["active", "verifying", "ready_to_integrate"].includes(row.status)
1908
+ && row.pendingHandoff === null
1909
+ ))
1910
+ ?? responsibilities.find(row => !["completed", "failed"].includes(row.status))
944
1911
  ?? responsibilities.at(-1)
945
1912
  ?? null;
1913
+ const pendingForCurrent = current?.pendingHandoff ?? null;
1914
+ const pendingForAgent = status.workItems.find(row => (
1915
+ row.pendingHandoff?.toAgentId === agentId
1916
+ ))?.pendingHandoff ?? null;
1917
+ const pendingHandoff = pendingForCurrent ?? pendingForAgent;
946
1918
  const checkpoints = loaded.events
947
1919
  .filter(row => row.event.type === "checkpoint_recorded"
948
1920
  && responsibilities.some(item => item.workItemId === row.event.workItemId))
@@ -951,16 +1923,61 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
951
1923
  const evidenceRefs = [...new Set([
952
1924
  ...(lastCheckpoint?.evidenceRefs ?? []),
953
1925
  ...(current?.evidenceRefs ?? []),
1926
+ ...(pendingHandoff?.evidenceRefs ?? []),
954
1927
  ])].sort();
1928
+ const unresolvedDecisionsById = new Map();
1929
+ for (const decision of status.openDecisions) {
1930
+ if (decision.ownerAgentId === agentId
1931
+ || (current && openDecisionsForWorkItem(current, [decision]).length > 0)) {
1932
+ unresolvedDecisionsById.set(decision.decisionId, decision);
1933
+ }
1934
+ }
1935
+ const unresolvedDecisions = [...unresolvedDecisionsById.values()]
1936
+ .sort((left, right) => compareIdentifiers(left.decisionId, right.decisionId));
1937
+ const ownedDecision = status.openDecisions.find(row => (
1938
+ row.ownerAgentId === agentId
1939
+ && status.dominantGap.kind === "decision"
1940
+ && status.dominantGap.id === row.decisionId
1941
+ )) ?? status.openDecisions.find(row => row.ownerAgentId === agentId) ?? null;
1942
+ const blockingDecision = current
1943
+ ? openDecisionsForWorkItem(current, status.openDecisions)[0] ?? null
1944
+ : null;
1945
+ const deferredVerifications = status.deferredVerifications.filter(row => (
1946
+ row.status === "open" && row.ownerAgentId === agentId
1947
+ ));
1948
+ const ownedVerification = deferredVerifications[0] ?? null;
1949
+ let nextAction = null;
1950
+ if (pendingForAgent && !current) {
1951
+ nextAction = pendingForAgent.nextAction;
1952
+ } else if (pendingForCurrent) {
1953
+ nextAction = `Waiting for ${pendingForCurrent.toAgentId} to accept ${pendingForCurrent.workItemId}.`;
1954
+ } else if (ownedDecision) {
1955
+ nextAction = `Resolve ${ownedDecision.decisionId}`;
1956
+ } else if (blockingDecision) {
1957
+ nextAction = `Waiting for ${blockingDecision.ownerAgentId} to resolve ${blockingDecision.decisionId}.`;
1958
+ } else if (ownedVerification) {
1959
+ nextAction = `Run deferred verification ${ownedVerification.verificationId}`;
1960
+ } else if (current?.nextAction) {
1961
+ nextAction = current.nextAction;
1962
+ } else {
1963
+ nextAction = current && !["completed", "failed"].includes(current.status)
1964
+ ? `Continue ${current.workItemId}.`
1965
+ : current ? null : status.readyQueue[0] ? `Take ${status.readyQueue[0]}.` : null;
1966
+ }
955
1967
  return {
956
1968
  schemaVersion: "OwlCodaRunKitTeamProjectTakeoverV1",
957
1969
  status: "team_project_takeover",
958
1970
  projectId: status.projectId,
959
1971
  objective: status.objective,
960
1972
  overall: status.overall,
1973
+ projectHeadline: status.headline,
1974
+ dominantGap: status.dominantGap,
1975
+ projectNextAction: status.nextAction,
1976
+ projectNextActorId: status.nextActorId,
961
1977
  agentId,
962
1978
  currentResponsibility: current,
963
1979
  allResponsibilities: responsibilities,
1980
+ pendingHandoff,
964
1981
  lastAcceptedCheckpoint: lastCheckpoint
965
1982
  ? {
966
1983
  workItemId: lastCheckpoint.workItemId,
@@ -972,15 +1989,11 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
972
1989
  truthRef: lastCheckpoint.truthRef,
973
1990
  }
974
1991
  : null,
975
- unresolvedDecisions: status.openDecisions.filter(row => (
976
- current && row.blockingWorkItemIds.includes(current.workItemId)
977
- )),
1992
+ unresolvedDecisions,
1993
+ deferredVerifications,
978
1994
  dependencies: current?.unresolvedDependencies ?? [],
979
1995
  evidenceRefs,
980
- nextAction: current?.nextAction
981
- ?? (current && !["completed", "failed"].includes(current.status)
982
- ? `Continue ${current.workItemId}.`
983
- : current ? null : status.readyQueue[0] ? `Take ${status.readyQueue[0]}.` : null),
1996
+ nextAction,
984
1997
  projectTruthHash: status.projectTruthHash,
985
1998
  authorizationGranted: false,
986
1999
  exitCode: 0,
@@ -988,21 +2001,47 @@ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
988
2001
  }
989
2002
 
990
2003
  export function formatTeamProjectStatusHumanV1(status) {
991
- const agentLines = status.agents.length === 0
992
- ? ["Agents: none assigned"]
993
- : status.agents.map(agent => (
994
- `Agent ${agent.agentId}: active=${agent.activeWorkItemIds.join(",") || "none"}; `
995
- + `waiting=${agent.waitingWorkItemIds.join(",") || "none"}; `
996
- + `completed=${agent.completedWorkItemIds.join(",") || "none"}`
997
- ));
2004
+ const nextAction = status.nextAction === null ? "none" : singleLine(status.nextAction);
998
2005
  return [
999
- `${status.projectId}: ${status.objective}`,
2006
+ status.headline,
2007
+ `Project: ${status.projectId} — ${singleLine(status.objective)}`,
1000
2008
  `State: ${status.overall}`,
1001
2009
  `Work: ${status.counts.completed}/${status.counts.total} completed; ${status.counts.active} active; ${status.counts.waiting_dependency + status.counts.waiting_decision} waiting`,
1002
- ...agentLines,
2010
+ `Agents: ${status.agents.map(agent => agent.agentId).join(", ") || "none assigned"}`,
1003
2011
  `Open decisions: ${status.openDecisions.map(row => row.decisionId).join(", ") || "none"}`,
1004
- `Critical work: ${status.criticalWorkItems.join(", ") || "none"}`,
2012
+ `Deferred verification: ${status.openDeferredVerificationIds.length} open${
2013
+ status.openDeferredVerificationIds.length > 0
2014
+ ? ` (${status.openDeferredVerificationIds.join(", ")})`
2015
+ : ""
2016
+ }`,
1005
2017
  `Ready queue: ${status.readyQueue.join(", ") || "none"}`,
2018
+ `Next: ${nextAction}${status.nextActorId ? ` (owner ${status.nextActorId})` : ""}`,
2019
+ "",
2020
+ ].join("\n");
2021
+ }
2022
+
2023
+ export function formatTeamProjectTakeoverHumanV1(takeover) {
2024
+ const responsibility = takeover.currentResponsibility;
2025
+ const agentNextAction = takeover.nextAction === null
2026
+ ? "none"
2027
+ : singleLine(takeover.nextAction);
2028
+ const projectNextAction = takeover.projectNextAction === null
2029
+ ? "none"
2030
+ : singleLine(takeover.projectNextAction);
2031
+ return [
2032
+ takeover.projectHeadline,
2033
+ `Project: ${takeover.projectId} — ${singleLine(takeover.objective)}`,
2034
+ `Agent: ${takeover.agentId}`,
2035
+ `Responsibility: ${responsibility?.workItemId ?? "none"}`,
2036
+ `Open decisions: ${takeover.unresolvedDecisions.map(row => row.decisionId).join(", ") || "none"}`,
2037
+ `Deferred verification: ${takeover.deferredVerifications
2038
+ .map(row => row.verificationId).join(", ") || "none"}`,
2039
+ `Pending handoff: ${takeover.pendingHandoff
2040
+ ? `${takeover.pendingHandoff.fromAgentId} -> ${takeover.pendingHandoff.toAgentId} for ${takeover.pendingHandoff.workItemId}`
2041
+ : "none"}`,
2042
+ `Agent next: ${agentNextAction}`,
2043
+ `Project next: ${projectNextAction}${takeover.projectNextActorId ? ` (owner ${takeover.projectNextActorId})` : ""}`,
2044
+ `State: ${takeover.overall}`,
1006
2045
  "",
1007
2046
  ].join("\n");
1008
2047
  }