owlrunkit 0.16.1 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +48 -9
  3. package/docs/architecture/OWLCODA_RUNKIT_017_AGENT_NATIVE_PRODUCT_DEFINITION.md +154 -0
  4. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +59 -14
  5. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  6. package/package.json +1 -1
  7. package/packages/attest/distribution-manifest-v1.json +10 -0
  8. package/packages/attest/src/quick.mjs +10 -0
  9. package/registry-release-policy-v1.json +5 -5
  10. package/sbom.spdx.json +5 -5
  11. package/scripts/runkit-contract/core-contract.mjs +12 -2
  12. package/scripts/runkit-contract/formal-workflow.mjs +201 -0
  13. package/scripts/runkit-contract/lifecycle-orchestration.mjs +7 -2
  14. package/scripts/runkit-contract/project-cli-resolver.mjs +3 -0
  15. package/scripts/runkit-contract/runkit-cli.mjs +99 -8
  16. package/scripts/runkit-contract/schemas/team-project-definition-v1.schema.json +108 -0
  17. package/scripts/runkit-contract/schemas/team-project-event-v1.schema.json +99 -0
  18. package/scripts/runkit-contract/schemas/team-project-status-v1.schema.json +50 -0
  19. package/scripts/runkit-contract/schemas/verification-envelope-v1.schema.json +102 -0
  20. package/scripts/runkit-contract/sync-release-identity.mjs +224 -0
  21. package/scripts/runkit-contract/team-project.mjs +1008 -0
  22. package/scripts/runkit-contract/templates/team-project-definition.json +30 -0
  23. package/scripts/runkit-contract/templates/team-project-event.json +9 -0
  24. package/scripts/runkit-contract/templates/verification-envelope.json +28 -0
  25. package/scripts/runkit-contract/verification-envelope-check.mjs +45 -0
  26. package/scripts/runkit-contract/verification-envelope.mjs +846 -0
@@ -0,0 +1,1008 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ realpathSync,
9
+ rmSync,
10
+ } from "node:fs";
11
+ import path from "node:path";
12
+
13
+ import { validateLeaseOwnedPaths } from "./core-contract.mjs";
14
+ import { inspectProjectControlState } from "./project-control-state.mjs";
15
+ import {
16
+ assertAllowedKeys,
17
+ isRecord,
18
+ relativeToWorkspace,
19
+ safeIdentifier,
20
+ writeJsonExclusiveAtomically,
21
+ } from "./provenance-common.mjs";
22
+
23
+ const PROJECT_ROOT = ".owlcoda/runkit/project";
24
+ const DEFINITION_SCHEMA = "OwlCodaRunKitTeamProjectDefinitionV1";
25
+ const EVENT_SCHEMA = "OwlCodaRunKitTeamProjectEventV1";
26
+ const STATUS_SCHEMA = "OwlCodaRunKitTeamProjectStatusV1";
27
+
28
+ const DEFINITION_KEYS = [
29
+ "schemaVersion",
30
+ "projectId",
31
+ "objective",
32
+ "milestones",
33
+ "workstreams",
34
+ "workItems",
35
+ "integrationGates",
36
+ ];
37
+ const WORK_ITEM_STATES = new Set([
38
+ "active",
39
+ "waiting_dependency",
40
+ "waiting_decision",
41
+ "verifying",
42
+ "ready_to_integrate",
43
+ "completed",
44
+ "failed",
45
+ ]);
46
+ const EVENT_KEYS = Object.freeze({
47
+ agent_assigned: [
48
+ "schemaVersion", "eventId", "type", "occurredAt", "assignmentId",
49
+ "supersedesAssignmentId", "workItemId", "agentId", "executionRunId",
50
+ "executionWorkItemId",
51
+ ],
52
+ checkpoint_recorded: [
53
+ "schemaVersion", "eventId", "type", "occurredAt", "assignmentId",
54
+ "workItemId", "state", "summary", "completedUnits", "evidenceRefs",
55
+ "blockerRefs", "decisionRefs", "nextAction", "sourceFingerprint",
56
+ ],
57
+ handoff_recorded: [
58
+ "schemaVersion", "eventId", "type", "occurredAt", "assignmentId",
59
+ "workItemId", "fromAgentId", "toAgentId", "summary", "evidenceRefs",
60
+ "nextAction",
61
+ ],
62
+ decision_opened: [
63
+ "schemaVersion", "eventId", "type", "occurredAt", "decisionId",
64
+ "title", "question", "ownerAgentId", "blockingWorkItemIds", "options",
65
+ ],
66
+ decision_resolved: [
67
+ "schemaVersion", "eventId", "type", "occurredAt", "decisionId",
68
+ "resolution", "rationale", "evidenceRefs",
69
+ ],
70
+ integration_gate_passed: [
71
+ "schemaVersion", "eventId", "type", "occurredAt", "gateId", "summary",
72
+ "evidenceRefs",
73
+ ],
74
+ data_candidate_recorded: [
75
+ "schemaVersion", "eventId", "type", "occurredAt", "candidateId",
76
+ "sourceRef", "rights", "inputRef", "outputRef", "decisionRef",
77
+ "verificationRefs", "outcomeRef", "version",
78
+ ],
79
+ });
80
+
81
+ function canonical(value) {
82
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
83
+ if (isRecord(value)) {
84
+ return `{${Object.keys(value).sort().map(key => (
85
+ `${JSON.stringify(key)}:${canonical(value[key])}`
86
+ )).join(",")}}`;
87
+ }
88
+ const encoded = JSON.stringify(value);
89
+ if (encoded === undefined) throw new Error("Project artifact is not canonical JSON.");
90
+ return encoded;
91
+ }
92
+
93
+ function sha256(value) {
94
+ return createHash("sha256").update(value).digest("hex");
95
+ }
96
+
97
+ function nonEmptyString(value, label) {
98
+ if (typeof value !== "string" || value.trim().length === 0) {
99
+ throw new Error(`${label} must be a non-empty string.`);
100
+ }
101
+ return value;
102
+ }
103
+
104
+ function nullableString(value, label) {
105
+ if (value === null) return null;
106
+ return nonEmptyString(value, label);
107
+ }
108
+
109
+ function stringArray(value, label, { allowEmpty = true } = {}) {
110
+ if (!Array.isArray(value)
111
+ || (!allowEmpty && value.length === 0)
112
+ || value.some(item => typeof item !== "string" || item.length === 0)
113
+ || new Set(value).size !== value.length) {
114
+ throw new Error(`${label} must be a unique string array${allowEmpty ? "" : " with at least one entry"}.`);
115
+ }
116
+ return [...value];
117
+ }
118
+
119
+ function projectRoot(workspaceRoot) {
120
+ const root = realpathSync(workspaceRoot);
121
+ return { root, projectRoot: path.join(root, PROJECT_ROOT) };
122
+ }
123
+
124
+ function assertRealProjectTree(root, candidate) {
125
+ const relative = path.relative(root, candidate);
126
+ let current = root;
127
+ for (const segment of relative.split(path.sep)) {
128
+ current = path.join(current, segment);
129
+ if (!existsSync(current)) mkdirSync(current);
130
+ const stat = lstatSync(current);
131
+ if (stat.isSymbolicLink() || !stat.isDirectory() || realpathSync(current) !== current) {
132
+ throw new Error("Team project control path must not contain symlinks.");
133
+ }
134
+ }
135
+ }
136
+
137
+ function withProjectLock(root, operation) {
138
+ const lockPath = path.join(root, "control.lock");
139
+ try {
140
+ mkdirSync(lockPath);
141
+ } catch (error) {
142
+ if (error?.code === "EEXIST") {
143
+ throw new Error("Another team project control transaction is active.");
144
+ }
145
+ throw error;
146
+ }
147
+ try {
148
+ return operation();
149
+ } finally {
150
+ rmSync(lockPath, { recursive: true, force: true });
151
+ }
152
+ }
153
+
154
+ function uniqueIds(rows, label) {
155
+ const ids = rows.map(row => safeIdentifier(row.id, `${label} id`));
156
+ if (new Set(ids).size !== ids.length) throw new Error(`${label} ids must be unique.`);
157
+ return new Set(ids);
158
+ }
159
+
160
+ function assertAcyclic(items) {
161
+ const byId = new Map(items.map(item => [item.id, item]));
162
+ const visiting = new Set();
163
+ const visited = new Set();
164
+ function visit(id) {
165
+ if (visiting.has(id)) throw new Error(`Work item dependency cycle includes ${id}.`);
166
+ if (visited.has(id)) return;
167
+ visiting.add(id);
168
+ for (const dependency of byId.get(id).dependencies) visit(dependency);
169
+ visiting.delete(id);
170
+ visited.add(id);
171
+ }
172
+ for (const item of items) visit(item.id);
173
+ }
174
+
175
+ function validateDefinition(value) {
176
+ assertAllowedKeys(value, "Team project definition", DEFINITION_KEYS);
177
+ if (value.schemaVersion !== DEFINITION_SCHEMA) {
178
+ throw new Error(`Team project definition schemaVersion must be ${DEFINITION_SCHEMA}.`);
179
+ }
180
+ safeIdentifier(value.projectId, "projectId");
181
+ nonEmptyString(value.objective, "objective");
182
+ for (const [key, label] of [
183
+ ["milestones", "Milestone"],
184
+ ["workstreams", "Workstream"],
185
+ ["workItems", "Work item"],
186
+ ["integrationGates", "Integration gate"],
187
+ ]) {
188
+ if (!Array.isArray(value[key])) throw new Error(`${label} list must be an array.`);
189
+ }
190
+ const milestoneIds = uniqueIds(value.milestones, "Milestone");
191
+ const workstreamIds = uniqueIds(value.workstreams, "Workstream");
192
+ const workItemIds = uniqueIds(value.workItems, "Work item");
193
+ const gateIds = uniqueIds(value.integrationGates, "Integration gate");
194
+
195
+ const milestones = value.milestones.map((row) => {
196
+ assertAllowedKeys(row, `Milestone ${row.id ?? "unknown"}`, ["id", "title"]);
197
+ nonEmptyString(row.title, `Milestone ${row.id} title`);
198
+ return { id: row.id, title: row.title };
199
+ });
200
+ const workstreams = value.workstreams.map((row) => {
201
+ assertAllowedKeys(row, `Workstream ${row.id ?? "unknown"}`, ["id", "title", "milestoneId"]);
202
+ nonEmptyString(row.title, `Workstream ${row.id} title`);
203
+ if (!milestoneIds.has(row.milestoneId)) {
204
+ throw new Error(`Workstream ${row.id} references unknown milestone ${row.milestoneId}.`);
205
+ }
206
+ return { id: row.id, title: row.title, milestoneId: row.milestoneId };
207
+ });
208
+ const workItems = value.workItems.map((row) => {
209
+ assertAllowedKeys(row, `Work item ${row.id ?? "unknown"}`, [
210
+ "id", "title", "milestoneId", "workstreamId", "dependencies",
211
+ "ownedPaths", "measurable",
212
+ ]);
213
+ nonEmptyString(row.title, `Work item ${row.id} title`);
214
+ if (!milestoneIds.has(row.milestoneId)) {
215
+ throw new Error(`Work item ${row.id} references unknown milestone ${row.milestoneId}.`);
216
+ }
217
+ if (!workstreamIds.has(row.workstreamId)) {
218
+ throw new Error(`Work item ${row.id} references unknown workstream ${row.workstreamId}.`);
219
+ }
220
+ const dependencies = stringArray(row.dependencies, `Work item ${row.id} dependencies`);
221
+ for (const dependency of dependencies) {
222
+ if (!workItemIds.has(dependency) || dependency === row.id) {
223
+ throw new Error(`Work item ${row.id} has invalid dependency ${dependency}.`);
224
+ }
225
+ }
226
+ const ownedPaths = validateLeaseOwnedPaths(row.ownedPaths);
227
+ let measurable = null;
228
+ if (row.measurable !== undefined) {
229
+ assertAllowedKeys(row.measurable, `Work item ${row.id} measurable`, ["unit", "total"]);
230
+ nonEmptyString(row.measurable.unit, `Work item ${row.id} measurable unit`);
231
+ if (!Number.isSafeInteger(row.measurable.total) || row.measurable.total < 1) {
232
+ throw new Error(`Work item ${row.id} measurable total must be a positive integer.`);
233
+ }
234
+ measurable = { unit: row.measurable.unit, total: row.measurable.total };
235
+ }
236
+ return {
237
+ id: row.id,
238
+ title: row.title,
239
+ milestoneId: row.milestoneId,
240
+ workstreamId: row.workstreamId,
241
+ dependencies,
242
+ ownedPaths,
243
+ ...(measurable ? { measurable } : {}),
244
+ };
245
+ });
246
+ assertAcyclic(workItems);
247
+ const integrationGates = value.integrationGates.map((row) => {
248
+ assertAllowedKeys(row, `Integration gate ${row.id ?? "unknown"}`, [
249
+ "id", "title", "requiredWorkItemIds", "requiredDecisionIds",
250
+ ]);
251
+ nonEmptyString(row.title, `Integration gate ${row.id} title`);
252
+ const requiredWorkItemIds = stringArray(
253
+ row.requiredWorkItemIds,
254
+ `Integration gate ${row.id} requiredWorkItemIds`,
255
+ { allowEmpty: false },
256
+ );
257
+ for (const workItemId of requiredWorkItemIds) {
258
+ if (!workItemIds.has(workItemId)) {
259
+ throw new Error(`Integration gate ${row.id} references unknown work item ${workItemId}.`);
260
+ }
261
+ }
262
+ const requiredDecisionIds = stringArray(
263
+ row.requiredDecisionIds,
264
+ `Integration gate ${row.id} requiredDecisionIds`,
265
+ );
266
+ return { id: row.id, title: row.title, requiredWorkItemIds, requiredDecisionIds };
267
+ });
268
+ if (gateIds.size !== integrationGates.length) throw new Error("Integration gate ids must be unique.");
269
+ return {
270
+ schemaVersion: DEFINITION_SCHEMA,
271
+ projectId: value.projectId,
272
+ objective: value.objective,
273
+ milestones,
274
+ workstreams,
275
+ workItems,
276
+ integrationGates,
277
+ };
278
+ }
279
+
280
+ function readRegularJson(filePath, label) {
281
+ const stat = lstatSync(filePath);
282
+ if (stat.isSymbolicLink() || !stat.isFile() || realpathSync(filePath) !== filePath) {
283
+ throw new Error(`${label} must be a regular file without symlinks.`);
284
+ }
285
+ return JSON.parse(readFileSync(filePath, "utf8"));
286
+ }
287
+
288
+ function loadProject(workspaceRoot) {
289
+ const { root, projectRoot: rootPath } = projectRoot(workspaceRoot);
290
+ const definitionPath = path.join(rootPath, "definition.json");
291
+ if (!existsSync(definitionPath)) throw new Error("Team project is not initialized.");
292
+ const definition = validateDefinition(readRegularJson(definitionPath, "Team project definition"));
293
+ const eventsRoot = path.join(rootPath, "events");
294
+ const events = existsSync(eventsRoot)
295
+ ? readdirSync(eventsRoot, { withFileTypes: true })
296
+ .filter(entry => entry.name.endsWith(".json"))
297
+ .map((entry) => {
298
+ if (entry.isSymbolicLink() || !entry.isFile()) {
299
+ throw new Error(`Team project event must be a regular file: ${entry.name}`);
300
+ }
301
+ const eventPath = path.join(eventsRoot, entry.name);
302
+ const event = readRegularJson(eventPath, `Team project event ${entry.name}`);
303
+ if (`${event.eventId}.json` !== entry.name) {
304
+ throw new Error(`Team project event filename is not bound to eventId: ${entry.name}`);
305
+ }
306
+ return { event, ref: `project/events/${entry.name}` };
307
+ })
308
+ .sort((left, right) => (
309
+ left.event.occurredAt.localeCompare(right.event.occurredAt)
310
+ || left.event.eventId.localeCompare(right.event.eventId)
311
+ ))
312
+ : [];
313
+ return { root, projectRoot: rootPath, definitionPath, definition, events };
314
+ }
315
+
316
+ function pathPrefix(value) {
317
+ return value.endsWith("/**") ? value.slice(0, -3) : null;
318
+ }
319
+
320
+ function covers(owner, candidate) {
321
+ if (owner === candidate) return true;
322
+ const prefix = pathPrefix(owner);
323
+ if (prefix === null) return false;
324
+ const candidatePrefix = pathPrefix(candidate) ?? candidate;
325
+ return candidatePrefix === prefix || candidatePrefix.startsWith(`${prefix}/`);
326
+ }
327
+
328
+ function pathsOverlap(left, right) {
329
+ return left.some(leftPath => right.some(rightPath => (
330
+ covers(leftPath, rightPath) || covers(rightPath, leftPath)
331
+ )));
332
+ }
333
+
334
+ function eventRef(event) {
335
+ return `project/events/${event.eventId}.json`;
336
+ }
337
+
338
+ function validateTimestamp(value) {
339
+ if (typeof value !== "string"
340
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
341
+ || Number.isNaN(Date.parse(value))) {
342
+ throw new Error("Team project event occurredAt must be an ISO-8601 UTC timestamp.");
343
+ }
344
+ return value;
345
+ }
346
+
347
+ function validateEventShape(value, definition) {
348
+ if (!isRecord(value) || typeof value.type !== "string" || !(value.type in EVENT_KEYS)) {
349
+ throw new Error("Team project event type is invalid.");
350
+ }
351
+ assertAllowedKeys(value, `Team project event ${value.eventId ?? "unknown"}`, EVENT_KEYS[value.type]);
352
+ if (value.schemaVersion !== EVENT_SCHEMA) {
353
+ throw new Error(`Team project event schemaVersion must be ${EVENT_SCHEMA}.`);
354
+ }
355
+ safeIdentifier(value.eventId, "eventId");
356
+ validateTimestamp(value.occurredAt);
357
+ const workItemIds = new Set(definition.workItems.map(row => row.id));
358
+ const gateIds = new Set(definition.integrationGates.map(row => row.id));
359
+ const assertWorkItem = (id) => {
360
+ safeIdentifier(id, "workItemId");
361
+ if (!workItemIds.has(id)) throw new Error(`Team project event references unknown work item ${id}.`);
362
+ };
363
+ if (["agent_assigned", "checkpoint_recorded", "handoff_recorded"].includes(value.type)) {
364
+ assertWorkItem(value.workItemId);
365
+ safeIdentifier(value.assignmentId, "assignmentId");
366
+ }
367
+ if (value.type === "agent_assigned") {
368
+ safeIdentifier(value.agentId, "agentId");
369
+ if (value.supersedesAssignmentId !== undefined) {
370
+ safeIdentifier(value.supersedesAssignmentId, "supersedesAssignmentId");
371
+ }
372
+ if ((value.executionRunId === undefined) !== (value.executionWorkItemId === undefined)) {
373
+ throw new Error("executionRunId and executionWorkItemId must be supplied together.");
374
+ }
375
+ if (value.executionRunId !== undefined) {
376
+ safeIdentifier(value.executionRunId, "executionRunId");
377
+ safeIdentifier(value.executionWorkItemId, "executionWorkItemId");
378
+ }
379
+ } else if (value.type === "checkpoint_recorded") {
380
+ if (!WORK_ITEM_STATES.has(value.state)) throw new Error("Checkpoint state is invalid.");
381
+ nonEmptyString(value.summary, "Checkpoint summary");
382
+ stringArray(value.evidenceRefs, "Checkpoint evidenceRefs");
383
+ stringArray(value.blockerRefs, "Checkpoint blockerRefs");
384
+ stringArray(value.decisionRefs, "Checkpoint decisionRefs");
385
+ nullableString(value.nextAction, "Checkpoint nextAction");
386
+ if (value.sourceFingerprint !== undefined
387
+ && !/^(?:sha256:)?[a-f0-9]{64}$/u.test(value.sourceFingerprint)) {
388
+ throw new Error("Checkpoint sourceFingerprint is invalid.");
389
+ }
390
+ if (value.completedUnits !== undefined
391
+ && (!Number.isSafeInteger(value.completedUnits) || value.completedUnits < 0)) {
392
+ throw new Error("Checkpoint completedUnits must be a non-negative integer.");
393
+ }
394
+ if (value.state === "completed" && value.evidenceRefs.length === 0) {
395
+ throw new Error("Completed checkpoint requires evidence.");
396
+ }
397
+ } else if (value.type === "handoff_recorded") {
398
+ safeIdentifier(value.fromAgentId, "fromAgentId");
399
+ safeIdentifier(value.toAgentId, "toAgentId");
400
+ nonEmptyString(value.summary, "Handoff summary");
401
+ stringArray(value.evidenceRefs, "Handoff evidenceRefs", { allowEmpty: false });
402
+ nonEmptyString(value.nextAction, "Handoff nextAction");
403
+ } else if (value.type === "decision_opened") {
404
+ safeIdentifier(value.decisionId, "decisionId");
405
+ safeIdentifier(value.ownerAgentId, "ownerAgentId");
406
+ nonEmptyString(value.title, "Decision title");
407
+ nonEmptyString(value.question, "Decision question");
408
+ const blocking = stringArray(value.blockingWorkItemIds, "Decision blockingWorkItemIds");
409
+ for (const id of blocking) assertWorkItem(id);
410
+ stringArray(value.options, "Decision options", { allowEmpty: false });
411
+ } else if (value.type === "decision_resolved") {
412
+ safeIdentifier(value.decisionId, "decisionId");
413
+ nonEmptyString(value.resolution, "Decision resolution");
414
+ nonEmptyString(value.rationale, "Decision rationale");
415
+ stringArray(value.evidenceRefs, "Decision evidenceRefs", { allowEmpty: false });
416
+ } else if (value.type === "integration_gate_passed") {
417
+ safeIdentifier(value.gateId, "gateId");
418
+ if (!gateIds.has(value.gateId)) throw new Error(`Unknown integration gate ${value.gateId}.`);
419
+ nonEmptyString(value.summary, "Integration gate summary");
420
+ stringArray(value.evidenceRefs, "Integration gate evidenceRefs", { allowEmpty: false });
421
+ } else if (value.type === "data_candidate_recorded") {
422
+ safeIdentifier(value.candidateId, "candidateId");
423
+ nonEmptyString(value.sourceRef, "Data candidate sourceRef");
424
+ nonEmptyString(value.rights, "Data candidate rights");
425
+ nonEmptyString(value.inputRef, "Data candidate inputRef");
426
+ nonEmptyString(value.outputRef, "Data candidate outputRef");
427
+ nullableString(value.decisionRef, "Data candidate decisionRef");
428
+ stringArray(value.verificationRefs, "Data candidate verificationRefs");
429
+ nullableString(value.outcomeRef, "Data candidate outcomeRef");
430
+ nonEmptyString(value.version, "Data candidate version");
431
+ }
432
+ return structuredClone(value);
433
+ }
434
+
435
+ function projectEvents(definition, records) {
436
+ const assignments = new Map();
437
+ const assignmentHistory = new Map(definition.workItems.map(row => [row.id, []]));
438
+ const checkpoints = new Map();
439
+ const handoffs = new Map();
440
+ const decisions = new Map();
441
+ const gatePasses = new Map();
442
+ const dataCandidates = [];
443
+ const seenAssignments = new Set();
444
+ const seenDecisions = new Set();
445
+
446
+ for (const { event, ref } of records) {
447
+ validateEventShape(event, definition);
448
+ if (event.type === "agent_assigned") {
449
+ if (seenAssignments.has(event.assignmentId)) {
450
+ throw new Error(`Duplicate assignmentId ${event.assignmentId}.`);
451
+ }
452
+ seenAssignments.add(event.assignmentId);
453
+ assignments.set(event.workItemId, { ...event, ref });
454
+ assignmentHistory.get(event.workItemId).push({ ...event, ref });
455
+ } else if (event.type === "checkpoint_recorded") {
456
+ checkpoints.set(event.workItemId, { ...event, ref });
457
+ } else if (event.type === "handoff_recorded") {
458
+ handoffs.set(event.workItemId, { ...event, ref });
459
+ } else if (event.type === "decision_opened") {
460
+ if (seenDecisions.has(event.decisionId)) {
461
+ throw new Error(`Duplicate decision_opened event for ${event.decisionId}.`);
462
+ }
463
+ seenDecisions.add(event.decisionId);
464
+ decisions.set(event.decisionId, { opened: { ...event, ref }, resolved: null });
465
+ } else if (event.type === "decision_resolved") {
466
+ const decision = decisions.get(event.decisionId);
467
+ if (!decision || decision.resolved) {
468
+ throw new Error(`Decision ${event.decisionId} is not open.`);
469
+ }
470
+ decision.resolved = { ...event, ref };
471
+ } else if (event.type === "integration_gate_passed") {
472
+ gatePasses.set(event.gateId, { ...event, ref });
473
+ } else if (event.type === "data_candidate_recorded") {
474
+ if (dataCandidates.some(row => row.candidateId === event.candidateId)) {
475
+ throw new Error(`Duplicate data candidate ${event.candidateId}.`);
476
+ }
477
+ dataCandidates.push({ ...event, ref });
478
+ }
479
+ }
480
+ return {
481
+ assignments,
482
+ assignmentHistory,
483
+ checkpoints,
484
+ handoffs,
485
+ decisions,
486
+ gatePasses,
487
+ dataCandidates,
488
+ };
489
+ }
490
+
491
+ function executionTruthFor(workspaceRoot, assignment) {
492
+ if (!assignment?.executionRunId) return null;
493
+ if (!workspaceRoot
494
+ || !existsSync(path.join(workspaceRoot, ".owlcoda/runkit/executions"))) {
495
+ return {
496
+ status: "missing",
497
+ runId: assignment.executionRunId,
498
+ lifecycle: null,
499
+ closeoutDecision: null,
500
+ closeoutTrusted: false,
501
+ leaseState: "missing",
502
+ leaseWorkItemId: assignment.executionWorkItemId,
503
+ issues: ["execution_not_found"],
504
+ };
505
+ }
506
+ const control = inspectProjectControlState({ workspaceRoot });
507
+ const execution = control.executions.find(row => row.runId === assignment.executionRunId);
508
+ if (!execution) {
509
+ return {
510
+ status: "missing",
511
+ runId: assignment.executionRunId,
512
+ lifecycle: null,
513
+ closeoutDecision: null,
514
+ closeoutTrusted: false,
515
+ leaseState: "missing",
516
+ leaseWorkItemId: assignment.executionWorkItemId,
517
+ issues: ["execution_not_found"],
518
+ };
519
+ }
520
+ const lease = execution.lease;
521
+ let leaseState = "missing";
522
+ if (lease.activeWorkItemIds.includes(assignment.executionWorkItemId)) leaseState = "active";
523
+ else if (lease.releasedWorkItemIds.includes(assignment.executionWorkItemId)) leaseState = "released";
524
+ else if (lease.preservedInactiveWorkItemIds.includes(assignment.executionWorkItemId)) leaseState = "preserved_inactive";
525
+ const issues = [
526
+ ...execution.issues,
527
+ ...lease.issues,
528
+ ...(leaseState === "missing" ? ["execution_lease_work_item_missing"] : []),
529
+ ];
530
+ return {
531
+ status: issues.length === 0 ? "bound" : "invalid",
532
+ runId: assignment.executionRunId,
533
+ lifecycle: execution.lifecycle,
534
+ closeoutDecision: execution.closeout?.decision ?? null,
535
+ closeoutTrusted: execution.closeout?.trusted === true,
536
+ leaseState,
537
+ leaseWorkItemId: assignment.executionWorkItemId,
538
+ issues,
539
+ };
540
+ }
541
+
542
+ function projectStatus(definition, records, workspaceRoot = null) {
543
+ const state = projectEvents(definition, records);
544
+ const completed = new Set();
545
+ for (const [id, checkpoint] of state.checkpoints) {
546
+ if (checkpoint.state === "completed") completed.add(id);
547
+ }
548
+ const workItems = definition.workItems.map((item) => {
549
+ const assignment = state.assignments.get(item.id) ?? null;
550
+ const checkpoint = state.checkpoints.get(item.id) ?? null;
551
+ const handoff = state.handoffs.get(item.id) ?? null;
552
+ const executionTruth = executionTruthFor(workspaceRoot, assignment);
553
+ const unresolvedDependencies = item.dependencies.filter(id => !completed.has(id));
554
+ let status = "planned";
555
+ if (assignment) status = unresolvedDependencies.length > 0 ? "waiting_dependency" : "active";
556
+ if (handoff && handoff.assignmentId === assignment?.assignmentId) status = "planned";
557
+ if (checkpoint) status = checkpoint.state;
558
+ if (status === "active" && checkpoint?.decisionRefs.some((id) => (
559
+ state.decisions.get(id)?.resolved === null
560
+ ))) status = "waiting_decision";
561
+ if (executionTruth && (executionTruth.status !== "bound"
562
+ || (executionTruth.lifecycle === "closed"
563
+ && executionTruth.closeoutDecision !== "accepted"))) {
564
+ status = "failed";
565
+ }
566
+ const measured = item.measurable
567
+ ? {
568
+ kind: "measured_units",
569
+ unit: item.measurable.unit,
570
+ completed: checkpoint?.completedUnits ?? 0,
571
+ total: item.measurable.total,
572
+ }
573
+ : { kind: "state_only" };
574
+ const history = state.assignmentHistory.get(item.id).map(row => ({
575
+ assignmentId: row.assignmentId,
576
+ agentId: row.agentId,
577
+ occurredAt: row.occurredAt,
578
+ supersedesAssignmentId: row.supersedesAssignmentId ?? null,
579
+ executionRunId: row.executionRunId ?? null,
580
+ truthRef: row.ref,
581
+ }));
582
+ const truthRefs = [
583
+ "project/definition.json",
584
+ ...history.map(row => row.truthRef),
585
+ ...(checkpoint ? [checkpoint.ref] : []),
586
+ ...(handoff ? [handoff.ref] : []),
587
+ ];
588
+ return {
589
+ workItemId: item.id,
590
+ title: item.title,
591
+ milestoneId: item.milestoneId,
592
+ workstreamId: item.workstreamId,
593
+ status,
594
+ agentId: assignment?.agentId ?? null,
595
+ assignmentId: assignment?.assignmentId ?? null,
596
+ executionRunId: assignment?.executionRunId ?? null,
597
+ executionWorkItemId: assignment?.executionWorkItemId ?? null,
598
+ executionTruth,
599
+ dependencies: [...item.dependencies],
600
+ unresolvedDependencies,
601
+ blockers: checkpoint?.blockerRefs ?? unresolvedDependencies.map(id => `work-item:${id}`),
602
+ decisionRefs: checkpoint?.decisionRefs ?? [],
603
+ progress: measured,
604
+ summary: checkpoint?.summary ?? handoff?.summary ?? null,
605
+ nextAction: checkpoint ? checkpoint.nextAction : (handoff?.nextAction ?? null),
606
+ lastCheckpointAt: checkpoint?.occurredAt ?? null,
607
+ evidenceRefs: checkpoint?.evidenceRefs ?? handoff?.evidenceRefs ?? [],
608
+ assignmentHistory: history,
609
+ truthRefs,
610
+ };
611
+ });
612
+ const byId = new Map(workItems.map(row => [row.workItemId, row]));
613
+ const openDecisions = [...state.decisions.values()]
614
+ .filter(row => row.resolved === null)
615
+ .map(row => ({
616
+ decisionId: row.opened.decisionId,
617
+ title: row.opened.title,
618
+ question: row.opened.question,
619
+ ownerAgentId: row.opened.ownerAgentId,
620
+ blockingWorkItemIds: [...row.opened.blockingWorkItemIds],
621
+ options: [...row.opened.options],
622
+ truthRefs: [row.opened.ref],
623
+ }))
624
+ .sort((left, right) => left.decisionId.localeCompare(right.decisionId));
625
+ const resolvedDecisions = [...state.decisions.values()]
626
+ .filter(row => row.resolved !== null)
627
+ .map(row => ({
628
+ decisionId: row.opened.decisionId,
629
+ resolution: row.resolved.resolution,
630
+ rationale: row.resolved.rationale,
631
+ evidenceRefs: [...row.resolved.evidenceRefs],
632
+ truthRefs: [row.opened.ref, row.resolved.ref],
633
+ }))
634
+ .sort((left, right) => left.decisionId.localeCompare(right.decisionId));
635
+ const resolvedDecisionIds = new Set(resolvedDecisions.map(row => row.decisionId));
636
+ const integrationGates = definition.integrationGates.map((gate) => {
637
+ const unresolvedWorkItemIds = gate.requiredWorkItemIds.filter(id => byId.get(id).status !== "completed");
638
+ const unresolvedDecisionIds = gate.requiredDecisionIds.filter(id => !resolvedDecisionIds.has(id));
639
+ const pass = state.gatePasses.get(gate.id) ?? null;
640
+ let status = unresolvedWorkItemIds.length === 0 && unresolvedDecisionIds.length === 0
641
+ ? "ready"
642
+ : "blocked";
643
+ if (pass) {
644
+ if (status !== "ready") throw new Error(`Integration gate ${gate.id} was passed before prerequisites.`);
645
+ status = "passed";
646
+ }
647
+ return {
648
+ gateId: gate.id,
649
+ title: gate.title,
650
+ status,
651
+ unresolvedWorkItemIds,
652
+ unresolvedDecisionIds,
653
+ evidenceRefs: pass?.evidenceRefs ?? [],
654
+ truthRefs: ["project/definition.json", ...(pass ? [pass.ref] : [])],
655
+ };
656
+ });
657
+ const stateNames = [
658
+ "planned", "active", "waiting_dependency", "waiting_decision", "verifying",
659
+ "ready_to_integrate", "completed", "failed",
660
+ ];
661
+ const counts = Object.fromEntries(stateNames.map(name => [
662
+ name,
663
+ workItems.filter(row => row.status === name).length,
664
+ ]));
665
+ counts.total = workItems.length;
666
+ const orderedCounts = { total: counts.total };
667
+ for (const name of stateNames) orderedCounts[name] = counts[name];
668
+ const readyQueue = workItems
669
+ .filter(row => row.status === "planned" && row.unresolvedDependencies.length === 0)
670
+ .map(row => row.workItemId)
671
+ .sort();
672
+ const downstream = new Map(workItems.map(row => [row.workItemId, 0]));
673
+ for (const item of workItems) {
674
+ for (const dependency of item.dependencies) {
675
+ downstream.set(dependency, downstream.get(dependency) + 1);
676
+ }
677
+ }
678
+ const decisionBlocked = new Set(openDecisions.flatMap(row => row.blockingWorkItemIds));
679
+ const criticalWorkItems = workItems
680
+ .filter(row => row.status !== "completed" && (downstream.get(row.workItemId) > 0 || decisionBlocked.has(row.workItemId)))
681
+ .sort((left, right) => (
682
+ downstream.get(right.workItemId) - downstream.get(left.workItemId)
683
+ || left.workItemId.localeCompare(right.workItemId)
684
+ ))
685
+ .map(row => row.workItemId);
686
+ const agents = [...new Set(workItems.map(row => row.agentId).filter(Boolean))]
687
+ .sort()
688
+ .map(agentId => {
689
+ const assigned = workItems.filter(row => row.agentId === agentId);
690
+ const nextActions = assigned.map(row => row.nextAction).filter(Boolean);
691
+ return {
692
+ agentId,
693
+ activeWorkItemIds: assigned.filter(row => ["active", "verifying", "ready_to_integrate"].includes(row.status)).map(row => row.workItemId).sort(),
694
+ waitingWorkItemIds: assigned.filter(row => ["waiting_dependency", "waiting_decision"].includes(row.status)).map(row => row.workItemId).sort(),
695
+ completedWorkItemIds: assigned.filter(row => row.status === "completed").map(row => row.workItemId).sort(),
696
+ failedWorkItemIds: assigned.filter(row => row.status === "failed").map(row => row.workItemId).sort(),
697
+ nextActions,
698
+ lastCheckpointAt: assigned.map(row => row.lastCheckpointAt).filter(Boolean).sort().at(-1) ?? null,
699
+ };
700
+ });
701
+ const aggregateGroup = (definitions, foreignKey, idKey) => definitions.map(group => {
702
+ const items = workItems.filter(row => row[foreignKey] === group.id);
703
+ return {
704
+ [idKey]: group.id,
705
+ title: group.title,
706
+ totalWorkItems: items.length,
707
+ completedWorkItems: items.filter(row => row.status === "completed").length,
708
+ blockedWorkItems: items.filter(row => ["waiting_dependency", "waiting_decision", "failed"].includes(row.status)).map(row => row.workItemId).sort(),
709
+ };
710
+ });
711
+ const dataCandidates = state.dataCandidates.map((row) => {
712
+ const missingFields = [
713
+ ...(row.decisionRef === null ? ["decisionRef"] : []),
714
+ ...(row.outcomeRef === null ? ["outcomeRef"] : []),
715
+ ...(row.rights === "unknown" ? ["rights"] : []),
716
+ ...(row.verificationRefs.length === 0 ? ["verificationRefs"] : []),
717
+ ].sort();
718
+ return {
719
+ candidateId: row.candidateId,
720
+ sourceRef: row.sourceRef,
721
+ rights: row.rights,
722
+ inputRef: row.inputRef,
723
+ outputRef: row.outputRef,
724
+ decisionRef: row.decisionRef,
725
+ verificationRefs: [...row.verificationRefs],
726
+ outcomeRef: row.outcomeRef,
727
+ version: row.version,
728
+ admissionStatus: missingFields.length === 0 ? "eligible_candidate" : "incomplete",
729
+ missingFields,
730
+ truthRefs: [row.ref],
731
+ };
732
+ });
733
+ let overall = "planned";
734
+ if (workItems.every(row => row.status === "completed")
735
+ && integrationGates.every(row => row.status === "passed")) overall = "completed";
736
+ else if (openDecisions.length > 0 || workItems.some(row => row.status === "failed")) overall = "active_with_blockers";
737
+ else if (workItems.some(row => row.status !== "planned")) overall = "active";
738
+ return {
739
+ schemaVersion: STATUS_SCHEMA,
740
+ status: "team_project_status",
741
+ projectId: definition.projectId,
742
+ objective: definition.objective,
743
+ overall,
744
+ counts: orderedCounts,
745
+ milestones: aggregateGroup(definition.milestones, "milestoneId", "milestoneId"),
746
+ workstreams: aggregateGroup(definition.workstreams, "workstreamId", "workstreamId"),
747
+ workItems,
748
+ agents,
749
+ readyQueue,
750
+ criticalWorkItems,
751
+ openDecisions,
752
+ resolvedDecisions,
753
+ integrationGates,
754
+ dataCandidates,
755
+ projectTruthHash: sha256(canonical({ definition, events: records.map(row => row.event) })),
756
+ authorizationGranted: false,
757
+ };
758
+ }
759
+
760
+ function validateEventAgainstState(event, definition, records, workspaceRoot) {
761
+ const normalized = validateEventShape(event, definition);
762
+ const state = projectEvents(definition, records);
763
+ const status = projectStatus(definition, records, workspaceRoot);
764
+ const workItem = definition.workItems.find(row => row.id === normalized.workItemId);
765
+ if (normalized.type === "agent_assigned") {
766
+ if (normalized.executionRunId !== undefined) {
767
+ const executionTruth = executionTruthFor(workspaceRoot, normalized);
768
+ if (executionTruth.status !== "bound") {
769
+ throw new Error(
770
+ `Agent assignment execution binding is not valid: ${executionTruth.issues.join(", ")}.`,
771
+ );
772
+ }
773
+ }
774
+ const current = state.assignments.get(normalized.workItemId) ?? null;
775
+ if (current && normalized.supersedesAssignmentId !== current.assignmentId) {
776
+ throw new Error(`Assignment must supersede current assignment ${current.assignmentId}.`);
777
+ }
778
+ if (!current && normalized.supersedesAssignmentId !== undefined) {
779
+ throw new Error("supersedesAssignmentId has no current assignment.");
780
+ }
781
+ for (const projected of status.workItems) {
782
+ if (projected.workItemId === normalized.workItemId
783
+ || projected.assignmentId === null
784
+ || ["completed", "failed"].includes(projected.status)) continue;
785
+ const other = definition.workItems.find(row => row.id === projected.workItemId);
786
+ if (pathsOverlap(workItem.ownedPaths, other.ownedPaths)) {
787
+ throw new Error(
788
+ `Owned paths overlap active assignment ${projected.assignmentId}: ${normalized.workItemId} <> ${projected.workItemId}.`,
789
+ );
790
+ }
791
+ }
792
+ } else if (normalized.type === "checkpoint_recorded") {
793
+ const current = state.assignments.get(normalized.workItemId);
794
+ if (!current || current.assignmentId !== normalized.assignmentId) {
795
+ throw new Error("Checkpoint must bind the current assignment.");
796
+ }
797
+ const unresolved = status.workItems.find(row => row.workItemId === normalized.workItemId).unresolvedDependencies;
798
+ if (normalized.state === "completed" && unresolved.length > 0) {
799
+ throw new Error(`Completed checkpoint has unresolved dependencies: ${unresolved.join(", ")}.`);
800
+ }
801
+ const executionTruth = status.workItems.find(
802
+ row => row.workItemId === normalized.workItemId,
803
+ ).executionTruth;
804
+ if (normalized.state === "completed" && executionTruth
805
+ && !(executionTruth.status === "bound"
806
+ && executionTruth.lifecycle === "closed"
807
+ && executionTruth.closeoutTrusted
808
+ && executionTruth.closeoutDecision === "accepted"
809
+ && new Set(["released", "preserved_inactive"]).has(executionTruth.leaseState))) {
810
+ throw new Error("Completed checkpoint bound execution is not closed and accepted with a released lease.");
811
+ }
812
+ if (workItem.measurable) {
813
+ if (normalized.completedUnits === undefined) {
814
+ throw new Error("Measured work item checkpoint requires completedUnits.");
815
+ }
816
+ if (normalized.completedUnits > workItem.measurable.total) {
817
+ throw new Error("Checkpoint completedUnits exceeds measurable total.");
818
+ }
819
+ if (normalized.state === "completed" && normalized.completedUnits !== workItem.measurable.total) {
820
+ throw new Error("Completed measured work item must reach its total units.");
821
+ }
822
+ } else if (normalized.completedUnits !== undefined) {
823
+ throw new Error("Unmeasured work item cannot claim completedUnits.");
824
+ }
825
+ } else if (normalized.type === "handoff_recorded") {
826
+ const current = state.assignments.get(normalized.workItemId);
827
+ if (!current
828
+ || current.assignmentId !== normalized.assignmentId
829
+ || current.agentId !== normalized.fromAgentId) {
830
+ throw new Error("Handoff must bind the current assignment and Agent.");
831
+ }
832
+ } else if (normalized.type === "decision_opened") {
833
+ if (state.decisions.has(normalized.decisionId)) throw new Error(`Decision ${normalized.decisionId} already exists.`);
834
+ } else if (normalized.type === "decision_resolved") {
835
+ const decision = state.decisions.get(normalized.decisionId);
836
+ if (!decision || decision.resolved) throw new Error(`Decision ${normalized.decisionId} is not open.`);
837
+ } else if (normalized.type === "integration_gate_passed") {
838
+ const gate = status.integrationGates.find(row => row.gateId === normalized.gateId);
839
+ if (gate.status !== "ready") throw new Error(`Integration gate ${normalized.gateId} is not ready.`);
840
+ } else if (normalized.type === "data_candidate_recorded") {
841
+ if (state.dataCandidates.some(row => row.candidateId === normalized.candidateId)) {
842
+ throw new Error(`Data candidate ${normalized.candidateId} already exists.`);
843
+ }
844
+ }
845
+ return normalized;
846
+ }
847
+
848
+ export function initializeTeamProjectV1({ workspaceRoot, definition }) {
849
+ if (!existsSync(workspaceRoot)) mkdirSync(workspaceRoot, { recursive: true });
850
+ const { root, projectRoot: rootPath } = projectRoot(workspaceRoot);
851
+ assertRealProjectTree(root, rootPath);
852
+ assertRealProjectTree(root, path.join(rootPath, "events"));
853
+ const normalized = validateDefinition(definition);
854
+ const definitionPath = path.join(rootPath, "definition.json");
855
+ if (existsSync(definitionPath)) {
856
+ const existing = readRegularJson(definitionPath, "Team project definition");
857
+ if (canonical(existing) !== canonical(normalized)) {
858
+ throw new Error("Team project definition is immutable and differs.");
859
+ }
860
+ return {
861
+ status: "team_project_initialized",
862
+ exitCode: 0,
863
+ projectId: normalized.projectId,
864
+ definitionPath: relativeToWorkspace(root, definitionPath),
865
+ definitionSha256: sha256(canonical(normalized)),
866
+ resumed: true,
867
+ authorizationGranted: false,
868
+ };
869
+ }
870
+ writeJsonExclusiveAtomically(definitionPath, normalized);
871
+ return {
872
+ status: "team_project_initialized",
873
+ exitCode: 0,
874
+ projectId: normalized.projectId,
875
+ definitionPath: relativeToWorkspace(root, definitionPath),
876
+ definitionSha256: sha256(canonical(normalized)),
877
+ resumed: false,
878
+ authorizationGranted: false,
879
+ };
880
+ }
881
+
882
+ export function appendTeamProjectEventV1({ workspaceRoot, event }) {
883
+ const loaded = loadProject(workspaceRoot);
884
+ return withProjectLock(loaded.projectRoot, () => {
885
+ const refreshed = loadProject(workspaceRoot);
886
+ const shaped = validateEventShape(event, refreshed.definition);
887
+ const eventPath = path.join(refreshed.projectRoot, "events", `${shaped.eventId}.json`);
888
+ if (existsSync(eventPath)) {
889
+ const existing = readRegularJson(eventPath, `Team project event ${shaped.eventId}`);
890
+ if (canonical(existing) !== canonical(shaped)) {
891
+ throw new Error(`Team project immutable event differs: ${shaped.eventId}.`);
892
+ }
893
+ return {
894
+ status: "team_project_event_recorded",
895
+ exitCode: 0,
896
+ eventId: shaped.eventId,
897
+ eventPath: relativeToWorkspace(refreshed.root, eventPath),
898
+ eventSha256: sha256(canonical(shaped)),
899
+ resumed: true,
900
+ authorizationGranted: false,
901
+ };
902
+ }
903
+ const latest = refreshed.events.at(-1)?.event ?? null;
904
+ if (latest && (shaped.occurredAt < latest.occurredAt
905
+ || (shaped.occurredAt === latest.occurredAt
906
+ && shaped.eventId.localeCompare(latest.eventId) <= 0))) {
907
+ throw new Error(
908
+ `Team project event must sort after the latest event ${latest.eventId}.`,
909
+ );
910
+ }
911
+ const normalized = validateEventAgainstState(
912
+ shaped,
913
+ refreshed.definition,
914
+ refreshed.events,
915
+ refreshed.root,
916
+ );
917
+ writeJsonExclusiveAtomically(eventPath, normalized);
918
+ return {
919
+ status: "team_project_event_recorded",
920
+ exitCode: 0,
921
+ eventId: normalized.eventId,
922
+ eventPath: relativeToWorkspace(refreshed.root, eventPath),
923
+ eventSha256: sha256(canonical(normalized)),
924
+ resumed: false,
925
+ authorizationGranted: false,
926
+ };
927
+ });
928
+ }
929
+
930
+ export function readTeamProjectStatusV1({ workspaceRoot }) {
931
+ const loaded = loadProject(workspaceRoot);
932
+ return {
933
+ ...projectStatus(loaded.definition, loaded.events, loaded.root),
934
+ exitCode: 0,
935
+ };
936
+ }
937
+
938
+ export function buildTeamProjectTakeoverV1({ workspaceRoot, agentId }) {
939
+ safeIdentifier(agentId, "agentId");
940
+ const loaded = loadProject(workspaceRoot);
941
+ const status = projectStatus(loaded.definition, loaded.events, loaded.root);
942
+ const responsibilities = status.workItems.filter(row => row.agentId === agentId);
943
+ const current = responsibilities.find(row => !["completed", "failed"].includes(row.status))
944
+ ?? responsibilities.at(-1)
945
+ ?? null;
946
+ const checkpoints = loaded.events
947
+ .filter(row => row.event.type === "checkpoint_recorded"
948
+ && responsibilities.some(item => item.workItemId === row.event.workItemId))
949
+ .map(row => ({ ...row.event, truthRef: row.ref }));
950
+ const lastCheckpoint = checkpoints.at(-1) ?? null;
951
+ const evidenceRefs = [...new Set([
952
+ ...(lastCheckpoint?.evidenceRefs ?? []),
953
+ ...(current?.evidenceRefs ?? []),
954
+ ])].sort();
955
+ return {
956
+ schemaVersion: "OwlCodaRunKitTeamProjectTakeoverV1",
957
+ status: "team_project_takeover",
958
+ projectId: status.projectId,
959
+ objective: status.objective,
960
+ overall: status.overall,
961
+ agentId,
962
+ currentResponsibility: current,
963
+ allResponsibilities: responsibilities,
964
+ lastAcceptedCheckpoint: lastCheckpoint
965
+ ? {
966
+ workItemId: lastCheckpoint.workItemId,
967
+ assignmentId: lastCheckpoint.assignmentId,
968
+ state: lastCheckpoint.state,
969
+ summary: lastCheckpoint.summary,
970
+ evidenceRefs: [...lastCheckpoint.evidenceRefs],
971
+ occurredAt: lastCheckpoint.occurredAt,
972
+ truthRef: lastCheckpoint.truthRef,
973
+ }
974
+ : null,
975
+ unresolvedDecisions: status.openDecisions.filter(row => (
976
+ current && row.blockingWorkItemIds.includes(current.workItemId)
977
+ )),
978
+ dependencies: current?.unresolvedDependencies ?? [],
979
+ evidenceRefs,
980
+ nextAction: current?.nextAction
981
+ ?? (current && !["completed", "failed"].includes(current.status)
982
+ ? `Continue ${current.workItemId}.`
983
+ : current ? null : status.readyQueue[0] ? `Take ${status.readyQueue[0]}.` : null),
984
+ projectTruthHash: status.projectTruthHash,
985
+ authorizationGranted: false,
986
+ exitCode: 0,
987
+ };
988
+ }
989
+
990
+ export function formatTeamProjectStatusHumanV1(status) {
991
+ const agentLines = status.agents.length === 0
992
+ ? ["Agents: none assigned"]
993
+ : status.agents.map(agent => (
994
+ `Agent ${agent.agentId}: active=${agent.activeWorkItemIds.join(",") || "none"}; `
995
+ + `waiting=${agent.waitingWorkItemIds.join(",") || "none"}; `
996
+ + `completed=${agent.completedWorkItemIds.join(",") || "none"}`
997
+ ));
998
+ return [
999
+ `${status.projectId}: ${status.objective}`,
1000
+ `State: ${status.overall}`,
1001
+ `Work: ${status.counts.completed}/${status.counts.total} completed; ${status.counts.active} active; ${status.counts.waiting_dependency + status.counts.waiting_decision} waiting`,
1002
+ ...agentLines,
1003
+ `Open decisions: ${status.openDecisions.map(row => row.decisionId).join(", ") || "none"}`,
1004
+ `Critical work: ${status.criticalWorkItems.join(", ") || "none"}`,
1005
+ `Ready queue: ${status.readyQueue.join(", ") || "none"}`,
1006
+ "",
1007
+ ].join("\n");
1008
+ }