owlrunkit 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +105 -14
- package/docs/architecture/OWLCODA_RUNKIT_017_AGENT_NATIVE_PRODUCT_DEFINITION.md +2 -3
- package/docs/architecture/OWLCODA_RUNKIT_018_PROJECT_DRIVER_PRODUCT_DEFINITION.md +165 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +93 -14
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
- package/package.json +2 -2
- package/packages/attest/distribution-manifest-v1.json +10 -0
- package/packages/attest/src/quick.mjs +10 -0
- package/registry-release-policy-v1.json +5 -5
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/core-contract.mjs +3 -1
- package/scripts/runkit-contract/human-status.mjs +49 -0
- package/scripts/runkit-contract/inspect-presentation.mjs +58 -12
- package/scripts/runkit-contract/onboarding-doctor.mjs +34 -10
- package/scripts/runkit-contract/project-cli-resolver.mjs +6 -0
- package/scripts/runkit-contract/project-control-parser.mjs +17 -0
- package/scripts/runkit-contract/runkit-cli.mjs +568 -21
- package/scripts/runkit-contract/schemas/team-project-event-v1.schema.json +16 -1
- package/scripts/runkit-contract/schemas/team-project-status-v1.schema.json +34 -1
- package/scripts/runkit-contract/schemas/team-project-successor-receipt-v1.schema.json +204 -0
- package/scripts/runkit-contract/team-project-successor.mjs +1176 -0
- package/scripts/runkit-contract/team-project.mjs +1082 -43
|
@@ -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
|
-
|
|
295
|
-
|
|
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 ([
|
|
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
|
-
|
|
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 (
|
|
557
|
-
if (
|
|
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:
|
|
602
|
-
|
|
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:
|
|
605
|
-
nextAction:
|
|
1008
|
+
summary: latestFact?.summary ?? null,
|
|
1009
|
+
nextAction: latestFact?.nextAction ?? null,
|
|
1010
|
+
pendingHandoff,
|
|
606
1011
|
lastCheckpointAt: checkpoint?.occurredAt ?? null,
|
|
607
|
-
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
|
|
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: [
|
|
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"
|
|
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")
|
|
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
|
-
|
|
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 =>
|
|
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
|
|
976
|
-
|
|
977
|
-
)),
|
|
1992
|
+
unresolvedDecisions,
|
|
1993
|
+
deferredVerifications,
|
|
978
1994
|
dependencies: current?.unresolvedDependencies ?? [],
|
|
979
1995
|
evidenceRefs,
|
|
980
|
-
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
|
|
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
|
-
|
|
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
|
-
|
|
2010
|
+
`Agents: ${status.agents.map(agent => agent.agentId).join(", ") || "none assigned"}`,
|
|
1003
2011
|
`Open decisions: ${status.openDecisions.map(row => row.decisionId).join(", ") || "none"}`,
|
|
1004
|
-
`
|
|
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
|
}
|