owlrunkit 0.18.1 → 0.18.3

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 (32) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +85 -19
  3. package/docs/architecture/OWLCODA_RUNKIT_018_PROJECT_DRIVER_PRODUCT_DEFINITION.md +16 -5
  4. package/docs/architecture/OWLCODA_RUNKIT_USAGE_MODE_SELECTION_PRODUCT_DECISION_20260811.md +78 -4
  5. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +7 -0
  6. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +28 -9
  7. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  8. package/integrations/codex/skills/owlcoda-runkit/references/contract-v0.1.md +160 -0
  9. package/integrations/codex/skills/owlcoda-runkit/references/contract-v0.2.md +545 -0
  10. package/package.json +1 -1
  11. package/packages/attest/README.md +4 -3
  12. package/packages/attest/consumer-surface-v1.json +1 -0
  13. package/packages/attest/distribution-manifest-v1.json +12 -1
  14. package/packages/attest/schemas/quick-verification-receipt-v2.schema.json +232 -0
  15. package/packages/attest/src/index.mjs +4 -1
  16. package/packages/attest/src/quick-receipt-contract.mjs +13 -3
  17. package/packages/attest/src/quick.mjs +21 -2
  18. package/registry-release-policy-v1.json +5 -5
  19. package/sbom.spdx.json +5 -5
  20. package/scripts/runkit-contract/assurance-router.mjs +3 -1
  21. package/scripts/runkit-contract/core-contract.mjs +2 -1
  22. package/scripts/runkit-contract/onboarding-doctor.mjs +46 -18
  23. package/scripts/runkit-contract/profile-onboarding.mjs +4 -0
  24. package/scripts/runkit-contract/project-cli-resolver.mjs +3 -0
  25. package/scripts/runkit-contract/quick-metrics.mjs +3 -3
  26. package/scripts/runkit-contract/quick-receipt.mjs +110 -5
  27. package/scripts/runkit-contract/quick-verify.mjs +93 -11
  28. package/scripts/runkit-contract/runkit-cli.mjs +172 -24
  29. package/scripts/runkit-contract/schemas/profile-reconcile-plan-v1.schema.json +2 -0
  30. package/scripts/runkit-contract/schemas/team-project-event-v2.schema.json +128 -0
  31. package/scripts/runkit-contract/team-project.mjs +310 -45
  32. package/scripts/runkit-contract/usage-mode.mjs +11 -13
@@ -16,6 +16,7 @@
16
16
  "detectionSha256": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/hash" }] },
17
17
  "currentProfilesSha256": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/hash" }] },
18
18
  "proposedProfilesSha256": { "$ref": "#/$defs/hash" },
19
+ "normalizationOnly": { "type": "boolean" },
19
20
  "addedProfileIds": { "$ref": "#/$defs/strings" },
20
21
  "updatedProfiles": { "type": "array", "items": { "$ref": "#/$defs/update" } },
21
22
  "proposedProfiles": { "$ref": "#/$defs/profiles" },
@@ -81,6 +82,7 @@
81
82
  "detectionSha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
82
83
  "currentProfilesSha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
83
84
  "proposedProfilesSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
85
+ "normalizationOnly": false,
84
86
  "addedProfileIds": [],
85
87
  "updatedProfiles": [{
86
88
  "profileId": "node-quality",
@@ -0,0 +1,128 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://owlcoda.com/schemas/runkit/team-project-event-v2.schema.json",
4
+ "title": "OwlCoda RunKit Team Project Event V2",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "eventId", "type", "occurredAt"],
8
+ "properties": {
9
+ "schemaVersion": { "const": "OwlCodaRunKitTeamProjectEventV2" },
10
+ "eventId": { "$ref": "#/$defs/id" },
11
+ "type": { "enum": ["agent_assigned", "checkpoint_recorded", "handoff_recorded", "rework_returned", "decision_opened", "decision_resolved", "integration_gate_passed", "verification_deferred", "verification_closed", "data_candidate_recorded"] },
12
+ "occurredAt": { "type": "string", "minLength": 24 },
13
+ "assignmentId": { "$ref": "#/$defs/id" },
14
+ "supersedesAssignmentId": { "$ref": "#/$defs/id" },
15
+ "failedAssignmentId": { "$ref": "#/$defs/id" },
16
+ "failedCheckpointEventId": { "$ref": "#/$defs/id" },
17
+ "reworkId": { "$ref": "#/$defs/id" },
18
+ "workItemId": { "$ref": "#/$defs/id" },
19
+ "agentId": { "$ref": "#/$defs/id" },
20
+ "executionRunId": { "$ref": "#/$defs/id" },
21
+ "executionWorkItemId": { "$ref": "#/$defs/id" },
22
+ "state": { "enum": ["active", "waiting_dependency", "waiting_decision", "verifying", "ready_to_integrate", "completed", "failed"] },
23
+ "summary": { "type": "string", "minLength": 1 },
24
+ "completedUnits": { "type": "integer", "minimum": 0 },
25
+ "evidenceRefs": { "$ref": "#/$defs/strings" },
26
+ "blockerRefs": { "$ref": "#/$defs/strings" },
27
+ "decisionRefs": { "$ref": "#/$defs/strings" },
28
+ "nextAction": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },
29
+ "sourceFingerprint": { "type": "string", "pattern": "^(sha256:)?[a-f0-9]{64}$" },
30
+ "fromAgentId": { "$ref": "#/$defs/id" },
31
+ "reviewerAgentId": { "$ref": "#/$defs/id" },
32
+ "toAgentId": { "$ref": "#/$defs/id" },
33
+ "reason": { "type": "string", "minLength": 1 },
34
+ "decisionId": { "$ref": "#/$defs/id" },
35
+ "title": { "type": "string", "minLength": 1 },
36
+ "question": { "type": "string", "minLength": 1 },
37
+ "ownerAgentId": { "$ref": "#/$defs/id" },
38
+ "blockingWorkItemIds": { "$ref": "#/$defs/strings" },
39
+ "options": { "$ref": "#/$defs/strings" },
40
+ "resolution": { "type": "string", "minLength": 1 },
41
+ "rationale": { "type": "string", "minLength": 1 },
42
+ "gateId": { "$ref": "#/$defs/id" },
43
+ "verificationId": { "$ref": "#/$defs/id" },
44
+ "checkIds": { "$ref": "#/$defs/ids" },
45
+ "dueGateId": { "$ref": "#/$defs/id" },
46
+ "disposition": { "enum": ["verified", "no_longer_required"] },
47
+ "decisionIds": { "$ref": "#/$defs/ids" },
48
+ "candidateId": { "$ref": "#/$defs/id" },
49
+ "sourceRef": { "type": "string", "minLength": 1 },
50
+ "rights": { "type": "string", "minLength": 1 },
51
+ "inputRef": { "type": "string", "minLength": 1 },
52
+ "outputRef": { "type": "string", "minLength": 1 },
53
+ "decisionRef": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },
54
+ "verificationRefs": { "$ref": "#/$defs/strings" },
55
+ "outcomeRef": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },
56
+ "version": { "type": "string", "minLength": 1 }
57
+ },
58
+ "allOf": [
59
+ {
60
+ "if": { "properties": { "type": { "const": "agent_assigned" } }, "required": ["type"] },
61
+ "then": {
62
+ "required": ["assignmentId", "workItemId", "agentId"],
63
+ "dependentRequired": {
64
+ "executionRunId": ["executionWorkItemId"],
65
+ "executionWorkItemId": ["executionRunId"]
66
+ }
67
+ }
68
+ },
69
+ {
70
+ "if": { "properties": { "type": { "const": "checkpoint_recorded" } }, "required": ["type"] },
71
+ "then": { "required": ["assignmentId", "workItemId", "state", "summary", "evidenceRefs", "blockerRefs", "decisionRefs", "nextAction"] }
72
+ },
73
+ {
74
+ "if": { "properties": { "type": { "const": "handoff_recorded" } }, "required": ["type"] },
75
+ "then": { "required": ["assignmentId", "workItemId", "fromAgentId", "toAgentId", "summary", "evidenceRefs", "nextAction"] }
76
+ },
77
+ {
78
+ "if": { "properties": { "type": { "const": "rework_returned" } }, "required": ["type"] },
79
+ "then": { "required": ["reworkId", "assignmentId", "failedAssignmentId", "failedCheckpointEventId", "workItemId", "fromAgentId", "reviewerAgentId", "toAgentId", "reason", "evidenceRefs", "nextAction"] }
80
+ },
81
+ {
82
+ "if": { "properties": { "type": { "const": "decision_opened" } }, "required": ["type"] },
83
+ "then": { "required": ["decisionId", "title", "question", "ownerAgentId", "blockingWorkItemIds", "options"] }
84
+ },
85
+ {
86
+ "if": { "properties": { "type": { "const": "decision_resolved" } }, "required": ["type"] },
87
+ "then": { "required": ["decisionId", "resolution", "rationale", "evidenceRefs"] }
88
+ },
89
+ {
90
+ "if": { "properties": { "type": { "const": "integration_gate_passed" } }, "required": ["type"] },
91
+ "then": { "required": ["gateId", "summary", "evidenceRefs"] }
92
+ },
93
+ {
94
+ "if": { "properties": { "type": { "const": "verification_deferred" } }, "required": ["type"] },
95
+ "then": { "required": ["verificationId", "workItemId", "ownerAgentId", "checkIds", "reason", "dueGateId"] }
96
+ },
97
+ {
98
+ "if": { "properties": { "type": { "const": "verification_closed" } }, "required": ["type"] },
99
+ "then": { "required": ["verificationId", "disposition", "summary", "evidenceRefs", "decisionIds"] }
100
+ },
101
+ {
102
+ "if": { "properties": { "type": { "const": "data_candidate_recorded" } }, "required": ["type"] },
103
+ "then": { "required": ["candidateId", "sourceRef", "rights", "inputRef", "outputRef", "decisionRef", "verificationRefs", "outcomeRef", "version"] }
104
+ }
105
+ ],
106
+ "$defs": {
107
+ "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" },
108
+ "ids": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/id" } },
109
+ "strings": { "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1 } }
110
+ },
111
+ "examples": [{
112
+ "schemaVersion": "OwlCodaRunKitTeamProjectEventV2",
113
+ "eventId": "rework-returned-review-001",
114
+ "type": "rework_returned",
115
+ "occurredAt": "2026-08-12T00:00:00.000Z",
116
+ "reworkId": "review-001",
117
+ "assignmentId": "rework-review-001",
118
+ "failedAssignmentId": "assignment-001",
119
+ "failedCheckpointEventId": "checkpoint-failed-001",
120
+ "workItemId": "engine",
121
+ "fromAgentId": "agent-kimi",
122
+ "reviewerAgentId": "agent-control",
123
+ "toAgentId": "agent-kimi",
124
+ "reason": "The candidate violates its versioned contract.",
125
+ "evidenceRefs": ["receipt:quick-failed"],
126
+ "nextAction": "Repair the contract and rerun the focused check."
127
+ }]
128
+ }
@@ -23,6 +23,7 @@ import {
23
23
  const PROJECT_ROOT = ".owlcoda/runkit/project";
24
24
  const DEFINITION_SCHEMA = "OwlCodaRunKitTeamProjectDefinitionV1";
25
25
  const EVENT_SCHEMA = "OwlCodaRunKitTeamProjectEventV1";
26
+ const EVENT_SCHEMA_V2 = "OwlCodaRunKitTeamProjectEventV2";
26
27
  const STATUS_SCHEMA = "OwlCodaRunKitTeamProjectStatusV1";
27
28
 
28
29
  const DEFINITION_KEYS = [
@@ -60,6 +61,12 @@ const EVENT_KEYS = Object.freeze({
60
61
  "workItemId", "fromAgentId", "toAgentId", "summary", "evidenceRefs",
61
62
  "nextAction",
62
63
  ],
64
+ rework_returned: [
65
+ "schemaVersion", "eventId", "type", "occurredAt", "reworkId",
66
+ "assignmentId", "failedAssignmentId", "failedCheckpointEventId",
67
+ "workItemId", "fromAgentId", "reviewerAgentId", "toAgentId", "reason",
68
+ "evidenceRefs", "nextAction",
69
+ ],
63
70
  decision_opened: [
64
71
  "schemaVersion", "eventId", "type", "occurredAt", "decisionId",
65
72
  "title", "question", "ownerAgentId", "blockingWorkItemIds", "options",
@@ -391,6 +398,28 @@ function pendingHandoffProjection(handoff) {
391
398
  : null;
392
399
  }
393
400
 
401
+ function reworkProjection(rework, attempt) {
402
+ return rework
403
+ ? {
404
+ eventId: rework.eventId,
405
+ truthRef: rework.ref,
406
+ reworkId: rework.reworkId,
407
+ attempt,
408
+ failedCheckpointEventId: rework.failedCheckpointEventId,
409
+ failedTruthRef: `project/events/${rework.failedCheckpointEventId}.json`,
410
+ previousAssignmentId: rework.failedAssignmentId,
411
+ previousAgentId: rework.fromAgentId,
412
+ reviewerAgentId: rework.reviewerAgentId,
413
+ assignmentId: rework.assignmentId,
414
+ agentId: rework.toAgentId,
415
+ reason: rework.reason,
416
+ evidenceRefs: [...rework.evidenceRefs],
417
+ nextAction: rework.nextAction,
418
+ occurredAt: rework.occurredAt,
419
+ }
420
+ : null;
421
+ }
422
+
394
423
  function validateTimestamp(value) {
395
424
  if (typeof value !== "string"
396
425
  || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
@@ -405,8 +434,13 @@ function validateEventShape(value, definition) {
405
434
  throw new Error("Team project event type is invalid.");
406
435
  }
407
436
  assertAllowedKeys(value, `Team project event ${value.eventId ?? "unknown"}`, EVENT_KEYS[value.type]);
408
- if (value.schemaVersion !== EVENT_SCHEMA) {
409
- throw new Error(`Team project event schemaVersion must be ${EVENT_SCHEMA}.`);
437
+ if (![EVENT_SCHEMA, EVENT_SCHEMA_V2].includes(value.schemaVersion)) {
438
+ throw new Error(
439
+ `Team project event schemaVersion must be ${EVENT_SCHEMA} or ${EVENT_SCHEMA_V2}.`,
440
+ );
441
+ }
442
+ if (value.type === "rework_returned" && value.schemaVersion !== EVENT_SCHEMA_V2) {
443
+ throw new Error(`rework_returned requires schemaVersion ${EVENT_SCHEMA_V2}.`);
410
444
  }
411
445
  safeIdentifier(value.eventId, "eventId");
412
446
  validateTimestamp(value.occurredAt);
@@ -420,11 +454,17 @@ function validateEventShape(value, definition) {
420
454
  "agent_assigned",
421
455
  "checkpoint_recorded",
422
456
  "handoff_recorded",
457
+ "rework_returned",
423
458
  "verification_deferred",
424
459
  ].includes(value.type)) {
425
460
  assertWorkItem(value.workItemId);
426
461
  }
427
- if (["agent_assigned", "checkpoint_recorded", "handoff_recorded"].includes(value.type)) {
462
+ if ([
463
+ "agent_assigned",
464
+ "checkpoint_recorded",
465
+ "handoff_recorded",
466
+ "rework_returned",
467
+ ].includes(value.type)) {
428
468
  safeIdentifier(value.assignmentId, "assignmentId");
429
469
  }
430
470
  if (value.type === "agent_assigned") {
@@ -466,6 +506,16 @@ function validateEventShape(value, definition) {
466
506
  nonEmptyString(value.summary, "Handoff summary");
467
507
  stringArray(value.evidenceRefs, "Handoff evidenceRefs", { allowEmpty: false });
468
508
  nonEmptyString(value.nextAction, "Handoff nextAction");
509
+ } else if (value.type === "rework_returned") {
510
+ safeIdentifier(value.reworkId, "reworkId");
511
+ safeIdentifier(value.failedAssignmentId, "failedAssignmentId");
512
+ safeIdentifier(value.failedCheckpointEventId, "failedCheckpointEventId");
513
+ safeIdentifier(value.fromAgentId, "fromAgentId");
514
+ safeIdentifier(value.reviewerAgentId, "reviewerAgentId");
515
+ safeIdentifier(value.toAgentId, "toAgentId");
516
+ nonEmptyString(value.reason, "Rework reason");
517
+ stringArray(value.evidenceRefs, "Rework evidenceRefs", { allowEmpty: false });
518
+ nonEmptyString(value.nextAction, "Rework nextAction");
469
519
  } else if (value.type === "decision_opened") {
470
520
  safeIdentifier(value.decisionId, "decisionId");
471
521
  safeIdentifier(value.ownerAgentId, "ownerAgentId");
@@ -541,11 +591,13 @@ function projectEvents(definition, records) {
541
591
  const assignmentHistory = new Map(definition.workItems.map(row => [row.id, []]));
542
592
  const checkpoints = new Map();
543
593
  const handoffs = new Map();
594
+ const reworks = new Map();
544
595
  const decisions = new Map();
545
596
  const gatePasses = new Map();
546
597
  const verifications = new Map();
547
598
  const dataCandidates = [];
548
599
  const seenAssignments = new Set();
600
+ const seenReworks = new Set();
549
601
  const seenDecisions = new Set();
550
602
 
551
603
  for (const { event, ref } of records) {
@@ -561,6 +613,38 @@ function projectEvents(definition, records) {
561
613
  checkpoints.set(event.workItemId, { ...event, ref });
562
614
  } else if (event.type === "handoff_recorded") {
563
615
  handoffs.set(event.workItemId, { ...event, ref });
616
+ } else if (event.type === "rework_returned") {
617
+ if (seenReworks.has(event.reworkId)) {
618
+ throw new Error(`Duplicate reworkId ${event.reworkId}.`);
619
+ }
620
+ if (seenAssignments.has(event.assignmentId)) {
621
+ throw new Error(`Duplicate assignmentId ${event.assignmentId}.`);
622
+ }
623
+ const current = assignments.get(event.workItemId) ?? null;
624
+ const failure = checkpoints.get(event.workItemId) ?? null;
625
+ const latestFact = latestEventFact(failure, handoffs.get(event.workItemId) ?? null);
626
+ if (!current
627
+ || current.assignmentId !== event.failedAssignmentId
628
+ || current.agentId !== event.fromAgentId
629
+ || failure?.eventId !== event.failedCheckpointEventId
630
+ || failure.assignmentId !== event.failedAssignmentId
631
+ || failure.state !== "failed"
632
+ || latestFact?.eventId !== failure.eventId) {
633
+ throw new Error(
634
+ `Rework ${event.reworkId} does not bind the current failed checkpoint and assignment.`,
635
+ );
636
+ }
637
+ seenReworks.add(event.reworkId);
638
+ seenAssignments.add(event.assignmentId);
639
+ const assignment = {
640
+ ...event,
641
+ agentId: event.toAgentId,
642
+ supersedesAssignmentId: event.failedAssignmentId,
643
+ ref,
644
+ };
645
+ assignments.set(event.workItemId, assignment);
646
+ assignmentHistory.get(event.workItemId).push(assignment);
647
+ reworks.set(event.workItemId, { ...event, ref });
564
648
  } else if (event.type === "decision_opened") {
565
649
  if (seenDecisions.has(event.decisionId)) {
566
650
  throw new Error(`Duplicate decision_opened event for ${event.decisionId}.`);
@@ -615,6 +699,7 @@ function projectEvents(definition, records) {
615
699
  assignmentHistory,
616
700
  checkpoints,
617
701
  handoffs,
702
+ reworks,
618
703
  decisions,
619
704
  gatePasses,
620
705
  verifications,
@@ -945,7 +1030,9 @@ function projectStatus(definition, records, workspaceRoot = null) {
945
1030
  const assignment = state.assignments.get(item.id) ?? null;
946
1031
  const checkpoint = state.checkpoints.get(item.id) ?? null;
947
1032
  const handoff = state.handoffs.get(item.id) ?? null;
948
- const latestFact = latestEventFact(checkpoint, handoff);
1033
+ const rework = state.reworks.get(item.id) ?? null;
1034
+ const currentRework = rework?.assignmentId === assignment?.assignmentId ? rework : null;
1035
+ const latestFact = latestEventFact(latestEventFact(checkpoint, handoff), currentRework);
949
1036
  const activeHandoff = handoff
950
1037
  && handoff.assignmentId === assignment?.assignmentId
951
1038
  && handoff.fromAgentId === assignment?.agentId
@@ -973,7 +1060,8 @@ function projectStatus(definition, records, workspaceRoot = null) {
973
1060
  total: item.measurable.total,
974
1061
  }
975
1062
  : { kind: "state_only" };
976
- const history = state.assignmentHistory.get(item.id).map(row => ({
1063
+ const assignmentEvents = state.assignmentHistory.get(item.id);
1064
+ const history = assignmentEvents.map(row => ({
977
1065
  assignmentId: row.assignmentId,
978
1066
  agentId: row.agentId,
979
1067
  occurredAt: row.occurredAt,
@@ -986,7 +1074,17 @@ function projectStatus(definition, records, workspaceRoot = null) {
986
1074
  ...history.map(row => row.truthRef),
987
1075
  ...(checkpoint ? [checkpoint.ref] : []),
988
1076
  ...(handoff ? [handoff.ref] : []),
1077
+ ...(rework ? [rework.ref] : []),
989
1078
  ];
1079
+ const projectedRework = reworkProjection(
1080
+ currentRework,
1081
+ currentRework
1082
+ ? assignmentEvents.findIndex(row => row.assignmentId === currentRework.assignmentId) + 1
1083
+ : null,
1084
+ );
1085
+ const latestSummary = latestFact?.type === "rework_returned"
1086
+ ? latestFact.reason
1087
+ : latestFact?.summary ?? null;
990
1088
  return {
991
1089
  workItemId: item.id,
992
1090
  title: item.title,
@@ -1005,9 +1103,10 @@ function projectStatus(definition, records, workspaceRoot = null) {
1005
1103
  : unresolvedDependencies.map(id => `work-item:${id}`),
1006
1104
  decisionRefs: latestFact?.type === "checkpoint_recorded" ? checkpoint.decisionRefs : [],
1007
1105
  progress: measured,
1008
- summary: latestFact?.summary ?? null,
1106
+ summary: latestSummary,
1009
1107
  nextAction: latestFact?.nextAction ?? null,
1010
1108
  pendingHandoff,
1109
+ rework: projectedRework,
1011
1110
  lastCheckpointAt: checkpoint?.occurredAt ?? null,
1012
1111
  evidenceRefs: latestFact?.evidenceRefs ?? [],
1013
1112
  assignmentHistory: history,
@@ -1339,6 +1438,42 @@ function validateEventAgainstState(event, definition, records, workspaceRoot) {
1339
1438
  || current.agentId !== normalized.fromAgentId) {
1340
1439
  throw new Error("Handoff must bind the current assignment and Agent.");
1341
1440
  }
1441
+ } else if (normalized.type === "rework_returned") {
1442
+ const current = state.assignments.get(normalized.workItemId) ?? null;
1443
+ const failure = state.checkpoints.get(normalized.workItemId) ?? null;
1444
+ const projected = status.workItems.find(row => row.workItemId === normalized.workItemId);
1445
+ const latestFact = latestEventFact(
1446
+ failure,
1447
+ state.handoffs.get(normalized.workItemId) ?? null,
1448
+ );
1449
+ if (projected?.status !== "failed"
1450
+ || !current
1451
+ || current.assignmentId !== normalized.failedAssignmentId
1452
+ || current.agentId !== normalized.fromAgentId
1453
+ || failure?.eventId !== normalized.failedCheckpointEventId
1454
+ || failure.assignmentId !== normalized.failedAssignmentId
1455
+ || failure.state !== "failed"
1456
+ || latestFact?.eventId !== failure.eventId) {
1457
+ throw new Error(
1458
+ "reject-and-return requires one current failed checkpoint bound to the current assignment.",
1459
+ );
1460
+ }
1461
+ if (state.assignmentHistory.get(normalized.workItemId).some(
1462
+ row => row.assignmentId === normalized.assignmentId,
1463
+ )) {
1464
+ throw new Error(`Assignment ${normalized.assignmentId} already exists.`);
1465
+ }
1466
+ for (const otherProjection of status.workItems) {
1467
+ if (otherProjection.workItemId === normalized.workItemId
1468
+ || otherProjection.assignmentId === null
1469
+ || ["completed", "failed"].includes(otherProjection.status)) continue;
1470
+ const other = definition.workItems.find(row => row.id === otherProjection.workItemId);
1471
+ if (pathsOverlap(workItem.ownedPaths, other.ownedPaths)) {
1472
+ throw new Error(
1473
+ `Owned paths overlap active assignment ${otherProjection.assignmentId}: ${normalized.workItemId} <> ${otherProjection.workItemId}.`,
1474
+ );
1475
+ }
1476
+ }
1342
1477
  } else if (normalized.type === "decision_opened") {
1343
1478
  if (state.decisions.has(normalized.decisionId)) throw new Error(`Decision ${normalized.decisionId} already exists.`);
1344
1479
  } else if (normalized.type === "decision_resolved") {
@@ -1416,52 +1551,55 @@ export function initializeTeamProjectV1({ workspaceRoot, definition }) {
1416
1551
  };
1417
1552
  }
1418
1553
 
1419
- export function appendTeamProjectEventV1({ workspaceRoot, event }) {
1420
- const loaded = loadProject(workspaceRoot);
1421
- return withProjectLock(loaded.projectRoot, () => {
1422
- const refreshed = loadProject(workspaceRoot);
1423
- const shaped = validateEventShape(event, refreshed.definition);
1424
- const eventPath = path.join(refreshed.projectRoot, "events", `${shaped.eventId}.json`);
1425
- if (existsSync(eventPath)) {
1426
- const existing = readRegularJson(eventPath, `Team project event ${shaped.eventId}`);
1427
- if (canonical(existing) !== canonical(shaped)) {
1428
- throw new Error(`Team project immutable event differs: ${shaped.eventId}.`);
1429
- }
1430
- return {
1431
- status: "team_project_event_recorded",
1432
- exitCode: 0,
1433
- eventId: shaped.eventId,
1434
- eventPath: relativeToWorkspace(refreshed.root, eventPath),
1435
- eventSha256: sha256(canonical(shaped)),
1436
- resumed: true,
1437
- authorizationGranted: false,
1438
- };
1439
- }
1440
- const latest = refreshed.events.at(-1)?.event ?? null;
1441
- if (latest && (shaped.occurredAt < latest.occurredAt
1442
- || (shaped.occurredAt === latest.occurredAt
1443
- && shaped.eventId.localeCompare(latest.eventId) <= 0))) {
1444
- throw new Error(
1445
- `Team project event must sort after the latest event ${latest.eventId}.`,
1446
- );
1554
+ function appendTeamProjectEventWithinLock(refreshed, event) {
1555
+ const shaped = validateEventShape(event, refreshed.definition);
1556
+ const eventPath = path.join(refreshed.projectRoot, "events", `${shaped.eventId}.json`);
1557
+ if (existsSync(eventPath)) {
1558
+ const existing = readRegularJson(eventPath, `Team project event ${shaped.eventId}`);
1559
+ if (canonical(existing) !== canonical(shaped)) {
1560
+ throw new Error(`Team project immutable event differs: ${shaped.eventId}.`);
1447
1561
  }
1448
- const normalized = validateEventAgainstState(
1449
- shaped,
1450
- refreshed.definition,
1451
- refreshed.events,
1452
- refreshed.root,
1453
- );
1454
- writeJsonExclusiveAtomically(eventPath, normalized);
1455
1562
  return {
1456
1563
  status: "team_project_event_recorded",
1457
1564
  exitCode: 0,
1458
- eventId: normalized.eventId,
1565
+ eventId: shaped.eventId,
1459
1566
  eventPath: relativeToWorkspace(refreshed.root, eventPath),
1460
- eventSha256: sha256(canonical(normalized)),
1461
- resumed: false,
1567
+ eventSha256: sha256(canonical(shaped)),
1568
+ resumed: true,
1462
1569
  authorizationGranted: false,
1463
1570
  };
1464
- });
1571
+ }
1572
+ const latest = refreshed.events.at(-1)?.event ?? null;
1573
+ if (latest && (shaped.occurredAt < latest.occurredAt
1574
+ || (shaped.occurredAt === latest.occurredAt
1575
+ && shaped.eventId.localeCompare(latest.eventId) <= 0))) {
1576
+ throw new Error(
1577
+ `Team project event must sort after the latest event ${latest.eventId}.`,
1578
+ );
1579
+ }
1580
+ const normalized = validateEventAgainstState(
1581
+ shaped,
1582
+ refreshed.definition,
1583
+ refreshed.events,
1584
+ refreshed.root,
1585
+ );
1586
+ writeJsonExclusiveAtomically(eventPath, normalized);
1587
+ return {
1588
+ status: "team_project_event_recorded",
1589
+ exitCode: 0,
1590
+ eventId: normalized.eventId,
1591
+ eventPath: relativeToWorkspace(refreshed.root, eventPath),
1592
+ eventSha256: sha256(canonical(normalized)),
1593
+ resumed: false,
1594
+ authorizationGranted: false,
1595
+ };
1596
+ }
1597
+
1598
+ export function appendTeamProjectEventV1({ workspaceRoot, event }) {
1599
+ const loaded = loadProject(workspaceRoot);
1600
+ return withProjectLock(loaded.projectRoot, () => (
1601
+ appendTeamProjectEventWithinLock(loadProject(workspaceRoot), event)
1602
+ ));
1465
1603
  }
1466
1604
 
1467
1605
  export function assignTeamProjectV1({
@@ -1584,6 +1722,133 @@ export function handoffTeamProjectV1({
1584
1722
  };
1585
1723
  }
1586
1724
 
1725
+ export function rejectAndReturnTeamProjectV1({
1726
+ workspaceRoot,
1727
+ reworkId,
1728
+ occurredAt,
1729
+ workItemId,
1730
+ reviewerAgentId,
1731
+ toAgentId,
1732
+ reason,
1733
+ nextAction,
1734
+ evidenceRefs,
1735
+ }) {
1736
+ safeIdentifier(reworkId, "reworkId");
1737
+ const eventId = `rework-returned-${reworkId}`;
1738
+ const assignmentId = `rework-${reworkId}`;
1739
+ const loaded = loadProject(workspaceRoot);
1740
+ return withProjectLock(loaded.projectRoot, () => {
1741
+ const refreshed = loadProject(workspaceRoot);
1742
+ const existing = refreshed.events.find(row => row.event.eventId === eventId)?.event ?? null;
1743
+ let event;
1744
+ if (existing) {
1745
+ event = {
1746
+ schemaVersion: EVENT_SCHEMA_V2,
1747
+ eventId,
1748
+ type: "rework_returned",
1749
+ occurredAt,
1750
+ reworkId,
1751
+ assignmentId,
1752
+ failedAssignmentId: existing.failedAssignmentId,
1753
+ failedCheckpointEventId: existing.failedCheckpointEventId,
1754
+ workItemId,
1755
+ fromAgentId: existing.fromAgentId,
1756
+ reviewerAgentId,
1757
+ toAgentId,
1758
+ reason,
1759
+ evidenceRefs,
1760
+ nextAction,
1761
+ };
1762
+ } else {
1763
+ const state = projectEvents(refreshed.definition, refreshed.events);
1764
+ const status = projectStatus(refreshed.definition, refreshed.events, refreshed.root);
1765
+ const current = state.assignments.get(workItemId) ?? null;
1766
+ const failure = state.checkpoints.get(workItemId) ?? null;
1767
+ const projected = status.workItems.find(row => row.workItemId === workItemId) ?? null;
1768
+ const latestFact = latestEventFact(failure, state.handoffs.get(workItemId) ?? null);
1769
+ if (projected?.status !== "failed"
1770
+ || !current
1771
+ || failure?.state !== "failed"
1772
+ || failure.assignmentId !== current.assignmentId
1773
+ || latestFact?.eventId !== failure.eventId) {
1774
+ throw new Error(
1775
+ "reject-and-return requires one current failed checkpoint bound to the current assignment.",
1776
+ );
1777
+ }
1778
+ event = {
1779
+ schemaVersion: EVENT_SCHEMA_V2,
1780
+ eventId,
1781
+ type: "rework_returned",
1782
+ occurredAt,
1783
+ reworkId,
1784
+ assignmentId,
1785
+ failedAssignmentId: current.assignmentId,
1786
+ failedCheckpointEventId: failure.eventId,
1787
+ workItemId,
1788
+ fromAgentId: current.agentId,
1789
+ reviewerAgentId,
1790
+ toAgentId,
1791
+ reason,
1792
+ evidenceRefs,
1793
+ nextAction,
1794
+ };
1795
+ }
1796
+
1797
+ const recorded = appendTeamProjectEventWithinLock(refreshed, event);
1798
+ const projectedEvents = recorded.resumed
1799
+ ? refreshed.events
1800
+ : [
1801
+ ...refreshed.events,
1802
+ { event, ref: `project/events/${event.eventId}.json` },
1803
+ ];
1804
+ const status = projectStatus(refreshed.definition, projectedEvents, refreshed.root);
1805
+ const projectedState = projectEvents(refreshed.definition, projectedEvents);
1806
+ const current = status.workItems.find(row => row.workItemId === workItemId) ?? null;
1807
+ const latestFact = latestEventFact(
1808
+ latestEventFact(
1809
+ projectedState.checkpoints.get(workItemId) ?? null,
1810
+ projectedState.handoffs.get(workItemId) ?? null,
1811
+ ),
1812
+ projectedState.reworks.get(workItemId) ?? null,
1813
+ );
1814
+ if (current?.assignmentId !== assignmentId
1815
+ || current?.rework?.eventId !== eventId
1816
+ || latestFact?.eventId !== eventId) {
1817
+ throw new Error(
1818
+ `Cannot resume historical/superseded rework ${reworkId}; `
1819
+ + `current assignment=${current?.assignmentId ?? "none"}, `
1820
+ + `current owner=${current?.agentId ?? "none"}.`,
1821
+ );
1822
+ }
1823
+ return {
1824
+ status: "team_project_rework",
1825
+ reworkId,
1826
+ assignmentId,
1827
+ failedAssignmentId: event.failedAssignmentId,
1828
+ failedCheckpointEventId: event.failedCheckpointEventId,
1829
+ workItemId,
1830
+ fromAgentId: event.fromAgentId,
1831
+ reviewerAgentId,
1832
+ toAgentId,
1833
+ reason,
1834
+ nextAction: current.nextAction,
1835
+ eventId: recorded.eventId,
1836
+ eventPath: recorded.eventPath,
1837
+ eventSha256: recorded.eventSha256,
1838
+ resumed: recorded.resumed,
1839
+ reworkAttempt: current.rework.attempt,
1840
+ projectTruthHash: status.projectTruthHash,
1841
+ overall: status.overall,
1842
+ projectNextAction: status.nextAction,
1843
+ projectNextActorId: status.nextActorId,
1844
+ gitWritePerformed: false,
1845
+ releaseWritePerformed: false,
1846
+ authorizationGranted: false,
1847
+ exitCode: 0,
1848
+ };
1849
+ });
1850
+ }
1851
+
1587
1852
  export function openTeamProjectDecisionV1({
1588
1853
  workspaceRoot,
1589
1854
  decisionId,
@@ -1,3 +1,5 @@
1
+ import { RUNKIT_RISK_CATEGORIES } from "./assurance-router.mjs";
2
+
1
3
  const USER_MODES = new Set(["auto", "off", "light", "managed", "formal"]);
2
4
 
3
5
  const BOOLEAN_FACTS = [
@@ -24,17 +26,7 @@ const MATERIAL_RISK_CATEGORIES = new Map([
24
26
  ["migration", "schema_or_data_migration"],
25
27
  ]);
26
28
 
27
- const KNOWN_RISK_CATEGORIES = new Set([
28
- "backtest",
29
- "deployment",
30
- "destructive",
31
- "foreign_write",
32
- "funds",
33
- "integration",
34
- "migration",
35
- "production",
36
- "release",
37
- ]);
29
+ const KNOWN_RISK_CATEGORIES = new Set(RUNKIT_RISK_CATEGORIES);
38
30
 
39
31
  const AUTHORITY_BOUNDARY = Object.freeze({
40
32
  authorizationGranted: false,
@@ -65,8 +57,14 @@ function requireFacts(input) {
65
57
  || !input.riskCategories.every((value) => typeof value === "string")) {
66
58
  throw new Error("riskCategories must be an array of strings");
67
59
  }
68
- if (input.riskCategories.some((value) => !KNOWN_RISK_CATEGORIES.has(value))) {
69
- throw new Error("riskCategories contains an unknown category");
60
+ const unknownCategories = uniqueSorted(
61
+ input.riskCategories.filter((value) => !KNOWN_RISK_CATEGORIES.has(value)),
62
+ );
63
+ if (unknownCategories.length > 0) {
64
+ throw new Error(
65
+ `riskCategories contains an unknown category: ${unknownCategories.join(", ")}. `
66
+ + `Allowed: ${RUNKIT_RISK_CATEGORIES.join(", ")}`,
67
+ );
70
68
  }
71
69
  }
72
70