@ccdd/project 3.3.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +9 -12
  2. package/dist/monitor-ui/assets/GraphView-6yzqBOLv.js +27 -0
  3. package/dist/monitor-ui/assets/ValidationView-BMqI2XQ5.css +1 -0
  4. package/dist/monitor-ui/assets/ValidationView-BwHWjMUI.js +1 -0
  5. package/dist/monitor-ui/assets/{elk-api-D5d3gWoP.js → elk-api-BwZQovT5.js} +1 -1
  6. package/dist/monitor-ui/assets/{elk.bundled-0eaKiuBP.js → elk.bundled-CQowIhI8.js} +1 -1
  7. package/dist/monitor-ui/assets/index-CoJ9_gC3.js +7 -0
  8. package/dist/monitor-ui/assets/{index-DDwx11D7.css → index-D0yfhUlt.css} +1 -1
  9. package/dist/monitor-ui/index.html +2 -2
  10. package/dist/scripts/prepare-demo.js +25 -44
  11. package/dist/scripts/prepare-demo.js.map +1 -1
  12. package/dist/src/artifact-scope.d.ts +6 -0
  13. package/dist/src/artifact-scope.js +27 -0
  14. package/dist/src/artifact-scope.js.map +1 -0
  15. package/dist/src/artifacts/index.d.ts +7 -155
  16. package/dist/src/artifacts/index.js +0 -354
  17. package/dist/src/artifacts/index.js.map +1 -1
  18. package/dist/src/artifacts/instruction.d.ts +6 -9
  19. package/dist/src/artifacts/instruction.js +28 -36
  20. package/dist/src/artifacts/instruction.js.map +1 -1
  21. package/dist/src/artifacts/mcp-server.js +8 -9
  22. package/dist/src/artifacts/mcp-server.js.map +1 -1
  23. package/dist/src/artifacts/scope.d.ts +12 -0
  24. package/dist/src/artifacts/scope.js +34 -0
  25. package/dist/src/artifacts/scope.js.map +1 -0
  26. package/dist/src/artifacts/tool-check.d.ts +2 -6
  27. package/dist/src/artifacts/tool-check.js +16 -20
  28. package/dist/src/artifacts/tool-check.js.map +1 -1
  29. package/dist/src/artifacts/types.d.ts +3 -37
  30. package/dist/src/artifacts/types.js +5 -89
  31. package/dist/src/artifacts/types.js.map +1 -1
  32. package/dist/src/broker/config.d.ts +5 -6
  33. package/dist/src/broker/config.js +183 -130
  34. package/dist/src/broker/config.js.map +1 -1
  35. package/dist/src/broker/graph.d.ts +14 -21
  36. package/dist/src/broker/graph.js +68 -70
  37. package/dist/src/broker/graph.js.map +1 -1
  38. package/dist/src/broker/index.d.ts +6 -28
  39. package/dist/src/broker/index.js +84 -234
  40. package/dist/src/broker/index.js.map +1 -1
  41. package/dist/src/cli.d.ts +1 -21
  42. package/dist/src/cli.js +2 -329
  43. package/dist/src/cli.js.map +1 -1
  44. package/dist/src/contracts.d.ts +13 -15
  45. package/dist/src/definitions.d.ts +33 -23
  46. package/dist/src/diagnostics-cli.d.ts +9 -0
  47. package/dist/src/diagnostics-cli.js +94 -0
  48. package/dist/src/diagnostics-cli.js.map +1 -0
  49. package/dist/src/doctor/index.d.ts +2 -4
  50. package/dist/src/doctor/index.js +17 -63
  51. package/dist/src/doctor/index.js.map +1 -1
  52. package/dist/src/executors/auth.d.ts +1 -1
  53. package/dist/src/executors/auth.js +2 -2
  54. package/dist/src/executors/auth.js.map +1 -1
  55. package/dist/src/executors/human-preparation.js +2 -10
  56. package/dist/src/executors/human-preparation.js.map +1 -1
  57. package/dist/src/executors/index.js +30 -29
  58. package/dist/src/executors/index.js.map +1 -1
  59. package/dist/src/executors/pi.js +36 -4
  60. package/dist/src/executors/pi.js.map +1 -1
  61. package/dist/src/executors/process.js +19 -11
  62. package/dist/src/executors/process.js.map +1 -1
  63. package/dist/src/executors/telemetry.d.ts +4 -0
  64. package/dist/src/executors/telemetry.js +24 -0
  65. package/dist/src/executors/telemetry.js.map +1 -0
  66. package/dist/src/monitor/actions.js +3 -3
  67. package/dist/src/monitor/actions.js.map +1 -1
  68. package/dist/src/monitor/server.js +12 -95
  69. package/dist/src/monitor/server.js.map +1 -1
  70. package/dist/src/monitor/store.js +6 -42
  71. package/dist/src/monitor/store.js.map +1 -1
  72. package/dist/src/monitor/types.d.ts +3 -13
  73. package/dist/src/monitor/ui/critic-presentation.js +1 -1
  74. package/dist/src/monitor/ui/critic-presentation.js.map +1 -1
  75. package/dist/src/monitor/ui/graph-layout.d.ts +0 -1
  76. package/dist/src/monitor/ui/graph-layout.js +1 -23
  77. package/dist/src/monitor/ui/graph-layout.js.map +1 -1
  78. package/dist/src/monitor/ui/tool-input.d.ts +1 -1
  79. package/dist/src/monitor/ui/tool-input.js +2 -3
  80. package/dist/src/monitor/ui/tool-input.js.map +1 -1
  81. package/dist/src/project/cli.js +18 -30
  82. package/dist/src/project/cli.js.map +1 -1
  83. package/dist/src/project/graph.js +2 -2
  84. package/dist/src/project/graph.js.map +1 -1
  85. package/dist/src/project/identity.js +83 -56
  86. package/dist/src/project/identity.js.map +1 -1
  87. package/dist/src/project/index.d.ts +0 -3
  88. package/dist/src/project/index.js +2 -6
  89. package/dist/src/project/index.js.map +1 -1
  90. package/dist/src/project/query.d.ts +2 -1
  91. package/dist/src/project/query.js +49 -74
  92. package/dist/src/project/query.js.map +1 -1
  93. package/dist/src/project/store.js +3 -3
  94. package/dist/src/project/store.js.map +1 -1
  95. package/dist/src/project/types.d.ts +4 -4
  96. package/dist/src/requester/index.d.ts +2 -9
  97. package/dist/src/requester/index.js +17 -96
  98. package/dist/src/requester/index.js.map +1 -1
  99. package/dist/src/sdk.d.ts +3 -8
  100. package/dist/src/sdk.js +2 -3
  101. package/dist/src/sdk.js.map +1 -1
  102. package/dist/src/tools/contracts.d.ts +44 -80
  103. package/dist/src/tools/diagnostics.js +2 -2
  104. package/dist/src/tools/diagnostics.js.map +1 -1
  105. package/dist/src/tools/environment.d.ts +1 -0
  106. package/dist/src/tools/environment.js +7 -11
  107. package/dist/src/tools/environment.js.map +1 -1
  108. package/dist/src/tools/manifest.d.ts +0 -1
  109. package/dist/src/tools/manifest.js +1 -25
  110. package/dist/src/tools/manifest.js.map +1 -1
  111. package/dist/src/tools/runner.d.ts +19 -14
  112. package/dist/src/tools/runner.js +163 -118
  113. package/dist/src/tools/runner.js.map +1 -1
  114. package/dist/src/tools/schema.js +1 -1
  115. package/dist/src/tools/schema.js.map +1 -1
  116. package/dist/src/workspaces/index.d.ts +5 -12
  117. package/dist/src/workspaces/index.js +33 -217
  118. package/dist/src/workspaces/index.js.map +1 -1
  119. package/package.json +38 -10
  120. package/dist/monitor-ui/assets/GraphView-BSZlEAs3.js +0 -27
  121. package/dist/monitor-ui/assets/ValidationView-DaEF78J-.js +0 -1
  122. package/dist/monitor-ui/assets/ValidationView-p5r8lX18.css +0 -1
  123. package/dist/monitor-ui/assets/index-BQlWbkpH.js +0 -8
  124. package/dist/src/artifacts/data.d.ts +0 -5
  125. package/dist/src/artifacts/data.js +0 -100
  126. package/dist/src/artifacts/data.js.map +0 -1
  127. package/dist/src/artifacts/groups.d.ts +0 -13
  128. package/dist/src/artifacts/groups.js +0 -93
  129. package/dist/src/artifacts/groups.js.map +0 -1
  130. package/dist/src/artifacts/human.d.ts +0 -38
  131. package/dist/src/artifacts/human.js +0 -156
  132. package/dist/src/artifacts/human.js.map +0 -1
  133. package/dist/src/artifacts/sources.d.ts +0 -11
  134. package/dist/src/artifacts/sources.js +0 -106
  135. package/dist/src/artifacts/sources.js.map +0 -1
  136. package/dist/src/review/cli.d.ts +0 -8
  137. package/dist/src/review/cli.js +0 -136
  138. package/dist/src/review/cli.js.map +0 -1
  139. package/dist/src/review/client.d.ts +0 -30
  140. package/dist/src/review/client.js +0 -193
  141. package/dist/src/review/client.js.map +0 -1
  142. package/dist/src/review/server.d.ts +0 -11
  143. package/dist/src/review/server.js +0 -249
  144. package/dist/src/review/server.js.map +0 -1
  145. package/dist/src/review/types.d.ts +0 -10
  146. package/dist/src/review/types.js +0 -17
  147. package/dist/src/review/types.js.map +0 -1
  148. package/dist/src/tools/host.d.ts +0 -7
  149. package/dist/src/tools/host.js +0 -98
  150. package/dist/src/tools/host.js.map +0 -1
  151. package/dist/src/tools/runtime-host.d.ts +0 -1
  152. package/dist/src/tools/runtime-host.js +0 -212
  153. package/dist/src/tools/runtime-host.js.map +0 -1
  154. package/dist/src/workspaces/transfer.d.ts +0 -52
  155. package/dist/src/workspaces/transfer.js +0 -372
  156. package/dist/src/workspaces/transfer.js.map +0 -1
@@ -3,19 +3,16 @@ import { randomUUID } from 'node:crypto';
3
3
  import { execFileSync } from 'node:child_process';
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
- import { isDeepStrictEqual } from 'node:util';
7
6
  import { setTimeout as delay } from 'node:timers/promises';
8
- import { prepareReviewRequests, readStoredArtifactScope } from '../requester/index.js';
7
+ import { prepareReviewRequests } from '../requester/index.js';
9
8
  import { readWorkspaceConfig } from './config.js';
10
- import { createGraphDefinition, prerequisiteCriticIds } from './graph.js';
9
+ import { createGraphDefinition } from './graph.js';
10
+ import { tokenUsage } from '../executors/telemetry.js';
11
11
  import { createReviewTools } from '../tools/runner.js';
12
- import { describeReviewTools } from '../tools/runner.js';
13
- import { validateArguments } from '../tools/schema.js';
14
12
  import { createHumanClaims, HUMAN_PREPARATION_LEASE_MS } from './human-claims.js';
15
13
  import { prepareHumanReview } from '../executors/human-preparation.js';
16
14
  import { prepareWorkspace, reopenWorkspace } from '../workspaces/index.js';
17
15
  import { createProjectSnapshot } from '../project/identity.js';
18
- import { assertGeneratedReviewInputs, prepareArtifactInputs } from '../artifacts/sources.js';
19
16
  import { includedCritics, planProject } from '../project/query.js';
20
17
  import { readEvidence } from '../project/store.js';
21
18
  const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -34,10 +31,12 @@ const fatalExecutionError = (error) => {
34
31
  const code = errorCode(error);
35
32
  return Boolean(code?.startsWith('WORKSPACE_') || ['RUN_OWNERSHIP_LOST', 'REVIEW_CANCELED', 'WORKER_STOPPED', 'WORKER_EXITED', 'REVIEW_GRAPH_INVALID'].includes(code ?? ''));
36
33
  };
37
- function storedObservation(value) {
34
+ function storedObservation(value, isError = false) {
38
35
  if (!object(value) || typeof value.artifactId !== 'string' || typeof value.operation !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(value.operation))
39
36
  return undefined;
40
37
  const observation = { artifactId: value.artifactId.slice(0, 64), operation: value.operation };
38
+ if (isError)
39
+ return observation;
41
40
  if (value.kind === 'content' || value.kind === 'empty')
42
41
  observation.kind = value.kind;
43
42
  if (typeof value.detail === 'string')
@@ -68,7 +67,9 @@ function validateResult(value) {
68
67
  if (Array.isArray(value.toolCalls))
69
68
  result.toolCalls = value.toolCalls.slice(0, 100).filter((item) => object(item) && typeof item.name === 'string').map(item => {
70
69
  const call = { name: item.name, ...(item.arguments === undefined ? {} : { arguments: copy(item.arguments) }) };
71
- const observation = storedObservation(item.observation);
70
+ const observation = storedObservation(item.observation, item.isError === true);
71
+ if (item.isError === true)
72
+ call.isError = true;
72
73
  if (observation)
73
74
  call.observation = observation;
74
75
  return call;
@@ -142,7 +143,7 @@ function prepareStateDirectory(repoPath, stateDir) {
142
143
  const canonical = canonicalFuturePath(stateDir);
143
144
  const relative = path.relative(repoPath, canonical);
144
145
  if (relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))) {
145
- throw codedError('CCDD state, logs and copied workspaces must be outside the reviewed repository.', 'WORKSPACE_UNSAFE');
146
+ throw codedError('CCDD state, logs and review outputs must be outside the reviewed repository.', 'WORKSPACE_UNSAFE');
146
147
  }
147
148
  fs.mkdirSync(canonical, { recursive: true, mode: 0o700 });
148
149
  return fs.realpathSync(canonical);
@@ -239,59 +240,17 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
239
240
  event: (request, type, message) => appendEvent(request.runId, request.id, type, message),
240
241
  assertWaiting: request => {
241
242
  ensureOpen();
243
+ if (request.configManifest?.version !== 2)
244
+ throw new Error('Historical reviews are available for result lookup only.');
242
245
  if (!request.notifiedAt)
243
246
  throw new Error('Human alarm delivery is still pending.');
244
- if (request.workspace.mode === 'lock' && !ownerAlive(ownerData(request.runId)))
245
- throw new Error('A lock review requires its monitoring worker to remain alive.');
247
+ if (!ownerAlive(ownerData(request.runId)))
248
+ throw new Error('An in-place review requires its monitoring worker to remain alive.');
246
249
  },
247
250
  });
248
- const assertRemoteClaim = (requestId, reviewerId, attemptId) => {
249
- ensureOpen();
250
- const request = required(requestData(requestId), 'request');
251
- if (request.profile.kind !== 'human' || request.status !== 'WAITING_HUMAN' || request.claimedBy !== reviewerId || request.claimAttemptId !== attemptId)
252
- throw new Error('This confirmed Claim is no longer owned by this reviewer.');
253
- if (request.workspace.mode !== 'copy')
254
- throw new Error('Remote Human review requires a copied workspace.');
255
- return request;
256
- };
257
- function prerequisites(request, run, requests) {
258
- if (run.project)
259
- return []; // Project requests are issued only after a pull readiness check.
260
- if (run.scope?.kind === 'critic')
261
- return [];
262
- if (run.graph) {
263
- const byCritic = new Map(requests.map(item => [item.criticId, item]));
264
- return prerequisiteCriticIds(request, run.graph).map(id => {
265
- const dependency = byCritic.get(id);
266
- if (!dependency || dependency.runId !== run.id)
267
- throw codedError(`Required Critic ${id} is missing from this Run.`, 'REVIEW_GRAPH_INVALID');
268
- return dependency;
269
- });
270
- }
271
- // Historical chains retain their explicit links; no target Artifact is guessed.
272
- if (request.predecessorId === null || (request.predecessorId === undefined && requests.length === 1))
273
- return [];
274
- const previous = requests.find(item => item.id === request.predecessorId);
275
- if (!previous || previous.runId !== run.id)
276
- throw codedError('This historical Run has no usable predecessor link. Submit a new review.', 'REVIEW_GRAPH_INVALID');
277
- return [previous];
278
- }
279
- function dependencyFailure(request, run, requests, seen = new Set()) {
280
- if (seen.has(request.id))
281
- return null;
282
- seen.add(request.id);
283
- for (const dependency of prerequisites(request, run, requests)) {
284
- if (dependency.status === 'RED' || dependency.status === 'ERROR')
285
- return dependency;
286
- const failure = dependencyFailure(dependency, run, requests, seen);
287
- if (failure)
288
- return failure;
289
- }
290
- return null;
291
- }
292
251
  function refreshReadinessWithin(runId) {
293
252
  const run = required(runData(runId), 'Run'), requests = runRequests(runId);
294
- if (run.project) {
253
+ if (run.project?.version === 2) {
295
254
  if (terminal.has(run.status))
296
255
  return;
297
256
  const plan = planProject(run.project.snapshot, readEvidence(db), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, attempts: requests });
@@ -308,30 +267,12 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
308
267
  }
309
268
  return;
310
269
  }
311
- for (const request of requests) {
312
- if (request.status !== 'BLOCKED')
313
- continue;
314
- const dependencies = prerequisites(request, run, requests);
315
- if (dependencies.every(item => item.status === 'GREEN')) {
316
- request.status = 'QUEUED';
317
- request.blockedReason = null;
318
- saveRequest(request);
319
- appendEvent(runId, request.id, 'request.queued', 'All required Artifact reviews are GREEN.');
320
- }
321
- else {
322
- const failed = dependencyFailure(request, run, requests);
323
- const reason = failed ? `${failed.criticId} returned ${failed.status}; dependent reviews cannot start.` : `Waiting for ${dependencies.filter(item => item.status !== 'GREEN').map(item => item.criticId).join(', ')} to become GREEN.`;
324
- if (request.blockedReason !== reason) {
325
- request.blockedReason = reason;
326
- saveRequest(request);
327
- }
328
- }
329
- }
270
+ throw new Error('Historical Runs cannot be resumed; submit a new validation request.');
330
271
  }
331
272
  const updateRunStatus = (runId) => {
332
273
  const states = runRequests(runId).map(request => request.status);
333
274
  const run = required(runData(runId), 'Run');
334
- if (run.project) {
275
+ if (run.project?.version === 2) {
335
276
  if (terminal.has(run.status))
336
277
  return;
337
278
  const plan = planProject(run.project.snapshot, readEvidence(db), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, attempts: runRequests(runId) });
@@ -348,19 +289,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
348
289
  }
349
290
  return;
350
291
  }
351
- // A branch failure is a verdict for that branch, not a stop signal for its siblings.
352
- const status = states.includes('RUNNING') ? 'RUNNING' : states.includes('QUEUED') ? 'QUEUED' :
353
- states.includes('WAITING_HUMAN') ? 'WAITING_HUMAN' : states.includes('ERROR') ? 'ERROR' : states.includes('RED') ? 'RED' :
354
- states.length > 0 && states.every(state => state === 'GREEN') ? 'GREEN' : 'ERROR';
355
- if (run.status !== status) {
356
- run.status = status;
357
- if (terminal.has(status))
358
- run.completedAt = now();
359
- else
360
- delete run.completedAt;
361
- saveRun(run);
362
- appendEvent(runId, null, 'run.status', `Run ${status}`, { status });
363
- }
292
+ throw new Error('Historical Runs are available for result lookup only.');
364
293
  };
365
294
  function finishWithin(requestId, outcome, expectedStates = ['RUNNING', 'WAITING_HUMAN']) {
366
295
  const { result, error } = outcome, hasError = Object.hasOwn(outcome, 'error');
@@ -394,7 +323,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
394
323
  saveRequest(request);
395
324
  appendEvent(runId, request.id, 'request.error', request.error, { status: request.status, ...(request.errorCode ? { code: request.errorCode } : {}) });
396
325
  }
397
- if (run.project) {
326
+ if (run.project?.version === 2) {
398
327
  const plan = planProject(run.project.snapshot, readEvidence(db), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, attempts: runRequests(runId) });
399
328
  run.project.evidenceRequestIds = [...new Set(plan.critics.flatMap(c => c.result ? [c.result.requestId] : []))];
400
329
  }
@@ -408,13 +337,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
408
337
  const owner = ownerData(runId);
409
338
  if (!owner || ownerAlive(owner))
410
339
  return;
411
- const run = runData(runId);
412
- const unfinished = runRequests(runId).filter(request => !terminal.has(request.status));
413
- const safeHumanWait = run?.workspace?.mode === 'copy' && unfinished.some(request => request.status === 'WAITING_HUMAN') &&
414
- unfinished.every(request => request.status === 'BLOCKED' || (request.status === 'WAITING_HUMAN' && request.notifiedAt));
415
- if (!safeHumanWait) {
416
- failWithin(runId, codedError('The review worker exited before completing its work. Submit a new run to retry.', 'WORKER_EXITED'));
417
- }
340
+ failWithin(runId, codedError('The review worker exited before completing its work. Submit a new run to retry.', 'WORKER_EXITED'));
418
341
  db.prepare('DELETE FROM run_owners WHERE run_id = ? AND token = ?').run(runId, owner.token);
419
342
  appendEvent(runId, null, 'worker.exited', 'The previous review worker is no longer running.');
420
343
  }
@@ -455,7 +378,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
455
378
  request.startedAt = now();
456
379
  request.blockedReason = null;
457
380
  saveRequest(request);
458
- appendEvent(runId, request.id, 'request.started', `${request.title}: reviewing ${workspace.descriptor.mode} workspace.`, { snapshotHash: workspace.descriptor.hash });
381
+ appendEvent(runId, request.id, 'request.started', `${request.title}: reviewing the supplied workspace.`, { snapshotHash: workspace.descriptor.hash });
459
382
  updateRunStatus(runId);
460
383
  });
461
384
  changed();
@@ -463,7 +386,6 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
463
386
  signal.throwIfAborted();
464
387
  await workspace.assertUnchanged();
465
388
  const request = required(requestData(requestId), 'Request');
466
- assertGeneratedReviewInputs(request);
467
389
  const runDir = path.join(stateDir, 'runs', runId, request.id);
468
390
  fs.mkdirSync(runDir, { recursive: true, mode: 0o700 });
469
391
  if (required(runData(runId), 'Run').project) {
@@ -500,21 +422,39 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
500
422
  const result = validateResult(await requireExecutors().execute(copy(request), {
501
423
  worktreePath: workspace.descriptor.path, workspacePath: workspace.descriptor.path, runDir, signal,
502
424
  onEvent(event) {
503
- if (closed || closing || signal.aborted || !event || requestData(requestId)?.status !== 'RUNNING' || !['executor.started', 'artifact.tools.ready', 'artifact.tool.called', 'executor.completed'].includes(event.type))
425
+ if (closed || closing || signal.aborted || !event || requestData(requestId)?.status !== 'RUNNING' || !['executor.started', 'artifact.tools.ready', 'artifact.tool.called', 'artifact.tool.completed', 'executor.usage', 'executor.telemetry.failed', 'executor.completed'].includes(event.type))
504
426
  return;
505
427
  const safe = {};
506
428
  for (const key of ['name', 'provider', 'model', 'kind', 'artifactId', 'path'])
507
429
  if (typeof event[key] === 'string')
508
430
  safe[key] = event[key].slice(0, 1000);
431
+ if (event.type === 'artifact.tool.completed') {
432
+ if (typeof event.startedAt === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(event.startedAt) && Number.isFinite(Date.parse(event.startedAt)))
433
+ safe.startedAt = event.startedAt;
434
+ if (typeof event.durationMs === 'number' && Number.isFinite(event.durationMs) && event.durationMs >= 0)
435
+ safe.durationMs = event.durationMs;
436
+ if (event.outcome === 'success' || event.outcome === 'error')
437
+ safe.outcome = event.outcome;
438
+ if (typeof event.operation === 'string')
439
+ safe.operation = event.operation.slice(0, 1000);
440
+ }
441
+ if (event.type === 'executor.usage') {
442
+ const usage = tokenUsage(event.usage);
443
+ if (!usage)
444
+ return;
445
+ safe.usage = usage;
446
+ }
447
+ if (event.isError === true)
448
+ safe.isError = true;
509
449
  if (object(event.observation)) {
510
450
  // Persist the same bounded metadata as final results, even if the Provider later fails.
511
- const observed = storedObservation(event.observation);
451
+ const observed = storedObservation(event.observation, event.isError === true);
512
452
  if (observed)
513
453
  safe.observation = observed;
514
454
  }
515
455
  if (Array.isArray(event.tools))
516
456
  safe.tools = event.tools.slice(0, 32).map(tool => typeof tool === 'string' ? tool : tool?.name).filter(value => typeof value === 'string');
517
- appendEvent(runId, requestId, event.type, String(event.message ?? event.type).slice(0, 2000), safe);
457
+ appendEvent(runId, requestId, event.type, event.type === 'executor.telemetry.failed' ? 'Some optional execution diagnostics could not be recorded.' : String(event.message ?? event.type).slice(0, 2000), safe);
518
458
  changed();
519
459
  },
520
460
  }));
@@ -543,6 +483,8 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
543
483
  stored = runData(runId);
544
484
  if (!stored)
545
485
  throw new Error('Unknown Run.');
486
+ if (stored.project?.version !== 2)
487
+ throw new Error('Historical Runs cannot be resumed; submit a new validation request.');
546
488
  if (terminal.has(stored.status))
547
489
  return false;
548
490
  if (!stored.workspace)
@@ -598,7 +540,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
598
540
  inFlight.set(queued.id, { kind: queued.profile.kind, promise });
599
541
  }
600
542
  if (inFlight.size) {
601
- // Human alarms and independent branches progress together; completed dependencies can unblock work immediately.
543
+ // Human alarms and independent evaluations progress together.
602
544
  await Promise.race([...inFlight.values()].map(item => item.promise).concat(delay(50, undefined, { signal: reviewSignal })));
603
545
  continue;
604
546
  }
@@ -606,24 +548,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
606
548
  if (waiting.length) {
607
549
  if (waiting.some(request => !request.notifiedAt))
608
550
  throw new Error('Human notification did not complete; submit a new Run to retry.');
609
- if (ownedRun.workspace.mode === 'copy') {
610
- await workspace.assertUnchanged();
611
- // A result can arrive during input validation. Release only after rechecking all branches atomically.
612
- const paused = transaction(() => {
613
- const pending = runRequests(runId).filter(request => !terminal.has(request.status));
614
- if (!pending.some(request => request.status === 'WAITING_HUMAN') || pending.some(request => request.status !== 'BLOCKED' && (request.status !== 'WAITING_HUMAN' || !request.notifiedAt)))
615
- return false;
616
- if (ownerData(runId)?.token !== token)
617
- throw codedError('Review ownership was lost.', 'RUN_OWNERSHIP_LOST');
618
- db.prepare('DELETE FROM run_owners WHERE run_id = ? AND token = ?').run(runId, token);
619
- appendEvent(runId, null, 'worker.paused', 'Copied Human reviews are persisted; no worker is needed while awaiting results.');
620
- return true;
621
- });
622
- if (paused)
623
- break;
624
- continue;
625
- }
626
- // The lock observer stays alive with filesystem events and metadata polls.
551
+ // The workspace observer stays alive with filesystem events and metadata polls.
627
552
  // Notification already crossed its final content boundary; idle waiting
628
553
  // must not rehash the entire workspace on every scheduling iteration.
629
554
  await delay(100, undefined, { signal: reviewSignal });
@@ -661,18 +586,17 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
661
586
  return entry.promise;
662
587
  }
663
588
  return {
664
- async submitProject({ mode = 'copy', requesterId = 'cli', selection, recursive = false, force = false }) {
589
+ async submitProject({ requesterId = 'cli', selection, recursive = false, force = false, ...removed }) {
665
590
  ensureOpen();
666
591
  requireExecutors();
592
+ if ('mode' in removed)
593
+ throw new Error('Workspace modes are no longer supported; supply an unchanged workspace.');
667
594
  if (!requesterId.trim() || requesterId.length > 200)
668
595
  throw new Error('requesterId is required (maximum 200 characters).');
669
- if (!['lock', 'copy'].includes(mode))
670
- throw new Error('mode must be lock or copy.');
671
596
  await requireExecutors().validateWorkspace?.(repoPath);
672
- const workspace = await workspaceAdapter.prepareWorkspace({ repoPath, stateDir, mode, integrity: workspaceIntegrity });
597
+ const workspace = await workspaceAdapter.prepareWorkspace({ repoPath, stateDir, integrity: workspaceIntegrity });
673
598
  try {
674
- const loaded = await readWorkspaceConfig(workspace.descriptor.path);
675
- const config = await prepareArtifactInputs(loaded.config, workspace.descriptor.path, 'review', workspace.signal);
599
+ const { config } = await readWorkspaceConfig(workspace.descriptor.path, workspace.signal);
676
600
  const snapshot = await createProjectSnapshot(config, workspace.descriptor.path, workspace.descriptor.hash, workspace.signal, workspace.descriptor.integrity);
677
601
  const ids = includedCritics(snapshot, selection, recursive);
678
602
  const templates = [];
@@ -680,7 +604,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
680
604
  templates.push(...await prepareReviewRequests({ repoPath: workspace.descriptor.path, repoId, snapshotHash: workspace.descriptor.hash, criticId: id, preparedConfig: config }));
681
605
  const id = randomUUID(), createdAt = now();
682
606
  const record = { id, repoId, snapshotHash: workspace.descriptor.hash, workspace: workspace.descriptor, requesterId,
683
- scope: { kind: 'project' }, graph: createGraphDefinition(config), project: { version: 1, snapshot, selection, recursive, force, templates }, status: 'QUEUED', createdAt };
607
+ scope: { kind: 'project' }, graph: createGraphDefinition(config), project: { version: 2, snapshot, selection, recursive, force, templates }, status: 'QUEUED', createdAt };
684
608
  await workspace.assertUnchanged();
685
609
  workspace.signal.throwIfAborted();
686
610
  transaction(() => {
@@ -696,63 +620,6 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
696
620
  await workspace.close();
697
621
  }
698
622
  },
699
- async submit({ mode = 'copy', requesterId, reviewRequests, criticId, snapshotCommit } = {}) {
700
- ensureOpen();
701
- requireExecutors();
702
- if (snapshotCommit !== undefined)
703
- throw new Error('snapshotCommit is no longer accepted; use mode copy or lock with the current workspace.');
704
- if (typeof requesterId !== 'string' || !requesterId.trim() || requesterId.length > 200)
705
- throw new Error('requesterId is required (maximum 200 characters).');
706
- if (!['lock', 'copy'].includes(mode))
707
- throw new Error('mode must be lock or copy.');
708
- await requireExecutors().validateWorkspace?.(repoPath);
709
- const workspace = await workspaceAdapter.prepareWorkspace({ repoPath, stateDir, mode, integrity: workspaceIntegrity });
710
- try {
711
- const descriptor = copy(workspace.descriptor);
712
- const { config } = await readWorkspaceConfig(descriptor.path);
713
- const graph = createGraphDefinition(config);
714
- const expected = await prepareReviewRequests({ repoPath: descriptor.path, repoId, snapshotHash: descriptor.hash, criticId, preparedConfig: config });
715
- const supplied = reviewRequests === undefined ? expected : reviewRequests;
716
- if (!isDeepStrictEqual(supplied, expected))
717
- throw new Error('Submitted review request envelopes must exactly match the Artifact definitions, payload, profiles and dependencies in the requested workspace.');
718
- const id = randomUUID();
719
- const createdAt = now();
720
- const requests = copy(supplied).map(envelope => ({
721
- ...envelope, id: randomUUID(), runId: id, workspace: descriptor, worktreePath: descriptor.path,
722
- status: 'BLOCKED', createdAt, startedAt: null, completedAt: null,
723
- result: null, error: null, claimedBy: null, claimedAt: null, notifiedAt: null,
724
- }));
725
- for (const request of requests) {
726
- const dependencies = criticId === undefined ? prerequisiteCriticIds(request, graph) : [];
727
- request.status = dependencies.length ? 'BLOCKED' : 'QUEUED';
728
- request.blockedReason = dependencies.length ? `Waiting for ${dependencies.join(', ')} to become GREEN.` : null;
729
- const capability = await requireExecutors().canExecute(copy(request));
730
- if (!capability?.ok)
731
- throw new Error(`Cannot execute ${request.criticId}: ${capability?.reason ?? 'No compatible executor.'}`);
732
- if (request.profile.kind === 'human' && typeof requireExecutors().notifyHuman !== 'function')
733
- throw new Error('Human execution requires an alarm method.');
734
- }
735
- await workspace.assertUnchanged();
736
- workspace.signal.throwIfAborted();
737
- ensureOpen();
738
- const scope = criticId === undefined ? { kind: 'graph' } : { kind: 'critic', criticId: criticId };
739
- const record = { id, repoId, snapshotHash: descriptor.hash, workspace: descriptor, requesterId, scope, graph, status: 'QUEUED', createdAt };
740
- transaction(() => {
741
- db.prepare('INSERT INTO runs(id,created_at,status,data) VALUES (?,?,?,?)').run(id, createdAt, record.status, JSON.stringify(record));
742
- requests.forEach((request, index) => db.prepare('INSERT INTO requests(id,run_id,ordinal,status,data) VALUES (?,?,?,?,?)').run(request.id, id, index, request.status, JSON.stringify(request)));
743
- appendEvent(id, null, 'run.submitted', 'Review persisted; a request-scoped worker can execute it.', { snapshotHash: descriptor.hash, mode, requesterId, scope });
744
- for (const request of requests)
745
- if (request.status === 'QUEUED')
746
- appendEvent(id, request.id, 'request.queued', 'Required Artifact reviews are satisfied; the review is ready.');
747
- appendEvent(id, null, 'workspace.ready', mode === 'copy' ? 'Content-addressed copied workspace is ready.' : 'Current workspace is fixed for this review; changes invalidate it.', { path: descriptor.path, snapshotHash: descriptor.hash });
748
- });
749
- changed();
750
- return required(getRun(id), 'Run');
751
- }
752
- finally {
753
- await workspace.close();
754
- }
755
- },
756
623
  run,
757
624
  reconcile,
758
625
  listRuns() {
@@ -779,7 +646,6 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
779
646
  },
780
647
  renewHumanTryClaim: humanClaims.renew,
781
648
  releaseHumanTryClaim: humanClaims.release,
782
- confirmHumanClaim: humanClaims.confirm,
783
649
  async claimHuman(requestId, reviewerId, { signal } = {}) {
784
650
  ensureOpen();
785
651
  if (typeof reviewerId !== 'string' || !reviewerId.trim())
@@ -788,6 +654,8 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
788
654
  if (existing)
789
655
  reconcile(existing.runId);
790
656
  const request = required(requestData(requestId), 'request');
657
+ if (request.configManifest?.version !== 2)
658
+ throw new Error('Historical reviews are available for result lookup only.');
791
659
  if (request.status === 'WAITING_HUMAN' && request.claimedBy === reviewerId)
792
660
  return request;
793
661
  if (request.claimedBy)
@@ -819,32 +687,6 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
819
687
  clearInterval(heartbeat);
820
688
  }
821
689
  },
822
- authorizeRemoteHumanTool(requestId, { reviewerId, attemptId, toolName, arguments: args = {} }) {
823
- const request = assertRemoteClaim(requestId, reviewerId, attemptId);
824
- if (!request.configManifest)
825
- throw new Error('Remote tool execution requires a TypeScript tool manifest.');
826
- const tool = describeReviewTools({ artifacts: request.artifacts, configManifest: request.configManifest, audience: 'human' }).find(value => value.name === toolName);
827
- if (!tool)
828
- throw new Error('Unknown registered Human tool.');
829
- validateArguments(tool.inputSchema, args);
830
- return tool;
831
- },
832
- recordRemoteHumanTool(requestId, { reviewerId, attemptId, toolName, arguments: args = {} }) {
833
- // Client reports only after real local execution and input validation. A receipt
834
- // never constitutes content observation or a semantic verdict.
835
- const request = assertRemoteClaim(requestId, reviewerId, attemptId);
836
- if (!request.configManifest)
837
- throw new Error('Remote tool execution requires a TypeScript tool manifest.');
838
- const tool = describeReviewTools({ artifacts: request.artifacts, configManifest: request.configManifest, audience: 'human' }).find(value => value.name === toolName);
839
- if (!tool)
840
- throw new Error('Unknown registered Human tool.');
841
- validateArguments(tool.inputSchema, args);
842
- transaction(() => {
843
- assertRemoteClaim(requestId, reviewerId, attemptId);
844
- appendEvent(request.runId, requestId, 'human.tool.executed', 'The claimed reviewer reported successful local tool execution.', { name: tool.name, artifactId: tool.artifactId, operation: tool.operation });
845
- });
846
- changed();
847
- },
848
690
  async executeHumanTool(requestId, { reviewerId, toolName, arguments: args = {}, signal }) {
849
691
  ensureOpen();
850
692
  const first = requestData(requestId);
@@ -855,10 +697,12 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
855
697
  const current = requestData(requestId);
856
698
  if (!current || current.profile.kind !== 'human' || current.status !== 'WAITING_HUMAN')
857
699
  throw new Error('Request is not waiting for a human review.');
700
+ if (current.configManifest?.version !== 2)
701
+ throw new Error('Historical reviews are available for result lookup only.');
858
702
  if (!reviewerId || current.claimedBy !== reviewerId)
859
703
  throw new Error('Only the reviewer who claimed this request can use its tools.');
860
- if (current.workspace.mode === 'lock' && !ownerAlive(ownerData(current.runId)))
861
- throw new Error('A lock review requires its monitoring worker to remain alive.');
704
+ if (!ownerAlive(ownerData(current.runId)))
705
+ throw new Error('An in-place review requires its monitoring worker to remain alive.');
862
706
  return current;
863
707
  };
864
708
  const request = assertClaim();
@@ -870,33 +714,38 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
870
714
  // remains mandatory because configuration loading and tool execution can mutate it.
871
715
  workspace.signal.throwIfAborted();
872
716
  inputsValidated = true;
873
- if (!request.configManifest) {
874
- if (Object.values(request.artifactTypes).some(type => type.custom))
875
- throw codedError('Stored TS Artifact configuration has no tool manifest.', 'WORKSPACE_ARTIFACT_MISMATCH');
876
- const expected = await readStoredArtifactScope({ repoPath: workspace.descriptor.path, criticId: request.criticId });
877
- if (!expected || expected.profile.kind !== 'human' || !isDeepStrictEqual(expected.artifactTypes, request.artifactTypes) || !isDeepStrictEqual(expected.artifacts, request.artifacts) || !isDeepStrictEqual(expected.artifactGroups ?? [], request.artifactGroups ?? [])) {
878
- throw codedError('Stored Artifact tools do not match the reviewed workspace configuration.', 'WORKSPACE_ARTIFACT_MISMATCH');
879
- }
880
- }
717
+ let timing;
881
718
  const registry = await createReviewTools({
882
- worktreePath: workspace.descriptor.path, artifacts: request.artifacts, artifactGroups: request.artifactGroups, artifactTypes: request.artifactTypes,
719
+ onExecution: diagnostic => { timing = diagnostic; },
720
+ worktreePath: workspace.descriptor.path, artifacts: request.artifacts,
883
721
  configManifest: request.configManifest, criticId: request.criticId, audience: 'human',
884
722
  runDir: path.resolve(stateDir, 'runs', request.runId, request.id, 'human-tools'), signal: workspace.signal,
885
723
  });
886
724
  try {
887
725
  registry.validateArguments(toolName, args);
888
726
  assertClaim();
889
- const result = await registry.call(toolName, args);
727
+ const attempt = await registry.call(toolName, args).then(result => ({ ok: true, result }), error => ({ ok: false, error: error }));
890
728
  await workspace.assertUnchanged();
891
729
  workspace.signal.throwIfAborted();
892
730
  assertClaim();
893
- const tool = registry.tools.find(tool => tool.name === toolName);
894
- transaction(() => {
895
- assertClaim();
896
- appendEvent(request.runId, requestId, 'human.tool.executed', 'The claimed reviewer executed a registered Artifact tool.', { name: tool.name, artifactId: tool.artifactId, operation: tool.operation });
897
- });
898
- changed();
899
- return result;
731
+ if (timing) {
732
+ const record = () => transaction(() => {
733
+ assertClaim();
734
+ appendEvent(request.runId, requestId, 'human.tool.executed', 'The claimed reviewer attempted a registered Artifact tool.', { ...timing, ...(attempt.ok && attempt.result.isError ? { isError: true } : {}) });
735
+ });
736
+ if (attempt.ok)
737
+ record();
738
+ else {
739
+ try {
740
+ record();
741
+ }
742
+ catch { /* Optional failure diagnostics never replace the original error. */ }
743
+ }
744
+ changed();
745
+ }
746
+ if (!attempt.ok)
747
+ throw attempt.error;
748
+ return attempt.result;
900
749
  }
901
750
  finally {
902
751
  await registry.close();
@@ -928,6 +777,8 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
928
777
  const request = requestData(requestId);
929
778
  if (!request || request.profile.kind !== 'human' || request.status !== 'WAITING_HUMAN')
930
779
  throw new Error('Request is not waiting for a human review.');
780
+ if (request.configManifest?.version !== 2)
781
+ throw new Error('Historical reviews are available for result lookup only.');
931
782
  if (!reviewerId || request.claimedBy !== reviewerId)
932
783
  throw new Error('Only the reviewer who claimed this request can submit its result.');
933
784
  if (!request.notifiedAt)
@@ -938,7 +789,6 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
938
789
  workspace = await workspaceAdapter.reopenWorkspace(request.workspace);
939
790
  // Reopening validates input under its integrity policy. No asynchronous work or
940
791
  // user code runs between this boundary and committing the submitted result.
941
- assertGeneratedReviewInputs(request);
942
792
  workspace.signal.throwIfAborted();
943
793
  ensureOpen();
944
794
  inputsValidated = true;
@@ -948,8 +798,8 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
948
798
  throw new Error('This human review has already completed or failed.');
949
799
  if (current.claimedBy !== reviewerId)
950
800
  throw new Error('Only the reviewer who claimed this request can submit its result.');
951
- if (current.workspace.mode === 'lock' && !ownerAlive(ownerData(current.runId)))
952
- throw new Error('A lock review requires its monitoring worker to remain alive.');
801
+ if (!ownerAlive(ownerData(current.runId)))
802
+ throw new Error('An in-place review requires its monitoring worker to remain alive.');
953
803
  required(workspace, 'Workspace').signal.throwIfAborted();
954
804
  finishWithin(requestId, { result: validated });
955
805
  });