onto-mcp 0.3.0 → 0.3.2

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 (61) hide show
  1. package/.onto/authority/core-lexicon.yaml +12 -0
  2. package/.onto/domains/software-engineering/competency_qs.md +192 -63
  3. package/.onto/domains/software-engineering/concepts.md +67 -5
  4. package/.onto/domains/software-engineering/conciseness_rules.md +22 -2
  5. package/.onto/domains/software-engineering/dependency_rules.md +78 -8
  6. package/.onto/domains/software-engineering/domain_scope.md +181 -150
  7. package/.onto/domains/software-engineering/extension_cases.md +318 -542
  8. package/.onto/domains/software-engineering/logic_rules.md +75 -3
  9. package/.onto/domains/software-engineering/problem_framing_profile.md +29 -2
  10. package/.onto/domains/software-engineering/prompt_interface.md +122 -0
  11. package/.onto/domains/software-engineering/structure_spec.md +53 -4
  12. package/.onto/principles/llm-native-development-guideline.md +20 -0
  13. package/.onto/principles/productization-charter.md +6 -0
  14. package/.onto/processes/evolve/material-kind-adapter-contract.md +6 -0
  15. package/.onto/processes/reconstruct/reconstruct-boundary-contract.md +468 -81
  16. package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +177 -0
  17. package/.onto/processes/reconstruct/source-profile-contract.md +39 -6
  18. package/.onto/processes/reconstruct/top-level-concept-discovery-contract.md +387 -0
  19. package/.onto/processes/review/binding-contract.md +8 -0
  20. package/.onto/processes/review/lens-registry.md +16 -0
  21. package/.onto/processes/review/pre-dispatch-contracts.md +34 -13
  22. package/.onto/processes/review/productized-live-path.md +3 -1
  23. package/.onto/processes/shared/pipeline-execution-ledger-contract.md +185 -0
  24. package/.onto/processes/shared/target-material-kind-contract.md +24 -2
  25. package/.onto/roles/axiology.md +7 -2
  26. package/AGENTS.md +4 -2
  27. package/README.md +52 -29
  28. package/dist/core-api/reconstruct-api.js +92 -5
  29. package/dist/core-api/review-api.js +1744 -371
  30. package/dist/core-runtime/cli/mock-review-unit-executor.js +17 -0
  31. package/dist/core-runtime/cli/render-review-final-output.js +9 -0
  32. package/dist/core-runtime/cli/review-invoke.js +387 -55
  33. package/dist/core-runtime/cli/run-review-prompt-execution.js +361 -90
  34. package/dist/core-runtime/path-boundary.js +58 -0
  35. package/dist/core-runtime/pipeline-execution-ledger.js +100 -0
  36. package/dist/core-runtime/reconstruct/artifact-types.js +33 -1
  37. package/dist/core-runtime/reconstruct/materialize-preparation.js +54 -4
  38. package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +342 -0
  39. package/dist/core-runtime/reconstruct/post-seed-validation.js +630 -0
  40. package/dist/core-runtime/reconstruct/record.js +105 -1
  41. package/dist/core-runtime/reconstruct/run.js +1594 -38
  42. package/dist/core-runtime/reconstruct/seed-candidate-validation.js +29 -0
  43. package/dist/core-runtime/review/continuation-plan.js +160 -0
  44. package/dist/core-runtime/review/execution-plan-boundary.js +123 -0
  45. package/dist/core-runtime/review/materializers.js +8 -3
  46. package/dist/core-runtime/review/pipeline-execution-ledger.js +250 -0
  47. package/dist/core-runtime/review/review-artifact-utils.js +15 -2
  48. package/dist/core-runtime/review/review-invocation-runner.js +604 -0
  49. package/dist/core-runtime/target-material-kind.js +43 -5
  50. package/dist/mcp/server.js +289 -59
  51. package/dist/mcp/tool-schemas.js +28 -2
  52. package/package.json +4 -2
  53. package/.onto/domains/llm-native-development/competency_qs.md +0 -430
  54. package/.onto/domains/llm-native-development/concepts.md +0 -242
  55. package/.onto/domains/llm-native-development/conciseness_rules.md +0 -163
  56. package/.onto/domains/llm-native-development/dependency_rules.md +0 -216
  57. package/.onto/domains/llm-native-development/domain_scope.md +0 -197
  58. package/.onto/domains/llm-native-development/extension_cases.md +0 -474
  59. package/.onto/domains/llm-native-development/logic_rules.md +0 -123
  60. package/.onto/domains/llm-native-development/prompt_interface.md +0 -49
  61. package/.onto/domains/llm-native-development/structure_spec.md +0 -245
@@ -1,14 +1,40 @@
1
+ import crypto from "node:crypto";
1
2
  import fs from "node:fs/promises";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { resolveOntoHome } from "../core-runtime/discovery/onto-home.js";
5
6
  import { loadCoreLensRegistry } from "../core-runtime/discovery/lens-registry.js";
6
- import { fileExists, isoFromTimestamp, isoNow, readYamlDocument, } from "../core-runtime/review/review-artifact-utils.js";
7
+ import { fileExists, isDeprecatedDomainAlias, isoFromTimestamp, isoNow, normalizeDomainValue, readYamlDocument, writeYamlDocument, } from "../core-runtime/review/review-artifact-utils.js";
8
+ import { assertPathInsideRoot, realpathIfExists, } from "../core-runtime/path-boundary.js";
9
+ import { assertReviewExecutionPlanSessionBoundary, } from "../core-runtime/review/execution-plan-boundary.js";
7
10
  import { buildReviewRouteVisibilityFromSession, } from "../core-runtime/review/route-visibility.js";
8
11
  import { readValidatedReviewRecord } from "../core-runtime/review/review-record-validation.js";
9
12
  import { readReviewResultClassification } from "../core-runtime/review/review-result-classification.js";
10
13
  import { REVIEW_EXECUTION_STEP_IDS, REVIEW_PROGRESS_STEPS, REVIEW_PROGRESS_TOTAL_STEPS, reviewProgressStepById, reviewProgressStepIdFromHalt, } from "../core-runtime/review/review-progress-contract.js";
11
- import { reviewPrepareOnly, runReviewInvokeCli } from "../core-runtime/cli/review-invoke.js";
14
+ import { collectReviewInvocationArtifactRefs, prepareReviewInvocationRequest, runReviewInvocation, } from "../core-runtime/review/review-invocation-runner.js";
15
+ import { completeReviewSession } from "../core-runtime/cli/complete-review-session.js";
16
+ import { buildExecutorConfigFromRealization, } from "../core-runtime/cli/review-invoke.js";
17
+ import { executeReviewPromptExecution, } from "../core-runtime/cli/run-review-prompt-execution.js";
18
+ import { buildReviewPipelineExecutionLedger, } from "../core-runtime/review/pipeline-execution-ledger.js";
19
+ import { buildReviewContinuationPlan, } from "../core-runtime/review/continuation-plan.js";
20
+ export class ReviewContinuationError extends Error {
21
+ failureContent;
22
+ originalError;
23
+ constructor(args) {
24
+ super(args.message);
25
+ this.name = "ReviewContinuationError";
26
+ this.originalError = args.originalError;
27
+ this.failureContent = args.failureContent;
28
+ }
29
+ }
30
+ export class ReviewDomainResolutionError extends Error {
31
+ domainResolution;
32
+ constructor(args) {
33
+ super(args.message);
34
+ this.name = "ReviewDomainResolutionError";
35
+ this.domainResolution = args.domainResolution;
36
+ }
37
+ }
12
38
  function stringifyConsoleArgs(args) {
13
39
  return args
14
40
  .map((arg) => {
@@ -74,53 +100,6 @@ async function withCapturedConsole(action, observer) {
74
100
  function resolveRequiredOntoHome(explicit) {
75
101
  return resolveOntoHome(explicit);
76
102
  }
77
- function appendCommonReviewArgs(args, request, ontoHome) {
78
- const result = [
79
- ...args,
80
- request.target,
81
- request.intent,
82
- "--project-root",
83
- path.resolve(request.projectRoot),
84
- ];
85
- result.push("--onto-home", ontoHome);
86
- if (request.domain && request.noDomain) {
87
- throw new Error("Use either domain or noDomain, not both.");
88
- }
89
- if (request.domain) {
90
- result.push("--domain", request.domain);
91
- }
92
- if (request.noDomain) {
93
- result.push("--no-domain");
94
- }
95
- if (request.reviewMode) {
96
- result.push("--review-mode", request.reviewMode);
97
- }
98
- if (request.targetScopeKind) {
99
- result.push("--target-scope-kind", request.targetScopeKind);
100
- }
101
- if (request.primaryRef) {
102
- result.push("--primary-ref", request.primaryRef);
103
- }
104
- for (const memberRef of request.memberRefs ?? []) {
105
- result.push("--member-ref", memberRef);
106
- }
107
- if (request.bundleKind) {
108
- result.push("--bundle-kind", request.bundleKind);
109
- }
110
- if (request.diffRange) {
111
- result.push("--diff-range", request.diffRange);
112
- }
113
- if (request.executorRealization) {
114
- result.push("--executor-realization", request.executorRealization);
115
- }
116
- for (const lensId of request.lensIds ?? []) {
117
- result.push("--lens-id", lensId);
118
- }
119
- if (request.confirmValueAlignment) {
120
- result.push("--confirm-value-alignment");
121
- }
122
- return result;
123
- }
124
103
  function basenameSessionId(sessionRoot) {
125
104
  return path.basename(path.resolve(sessionRoot));
126
105
  }
@@ -132,19 +111,63 @@ async function readOptionalYaml(filePath) {
132
111
  async function readOptionalReviewRecord(filePath) {
133
112
  if (!(await fileExists(filePath)))
134
113
  return null;
135
- return readValidatedReviewRecord(filePath);
114
+ try {
115
+ return await readValidatedReviewRecord(filePath);
116
+ }
117
+ catch {
118
+ return null;
119
+ }
136
120
  }
137
121
  async function readOptionalText(filePath) {
138
122
  if (!(await fileExists(filePath)))
139
123
  return undefined;
140
124
  return fs.readFile(filePath, "utf8");
141
125
  }
126
+ async function resolveReviewRecordFinalOutputPath(args) {
127
+ const sessionRoot = path.resolve(args.sessionRoot);
128
+ const rawRef = args.finalOutputRef ?? path.join(sessionRoot, "final-output.md");
129
+ const candidates = path.isAbsolute(rawRef)
130
+ ? [path.resolve(rawRef)]
131
+ : [
132
+ path.resolve(sessionRoot, rawRef),
133
+ ...(args.projectRoot ? [path.resolve(args.projectRoot, rawRef)] : []),
134
+ ];
135
+ for (const candidate of candidates) {
136
+ if (!(await fileExists(candidate)))
137
+ continue;
138
+ await assertPathInsideRoot({
139
+ root: sessionRoot,
140
+ candidate,
141
+ label: "ReviewRecord.final_output_ref",
142
+ });
143
+ return candidate;
144
+ }
145
+ const fallback = candidates[0] ?? path.join(sessionRoot, "final-output.md");
146
+ await assertPathInsideRoot({
147
+ root: sessionRoot,
148
+ candidate: fallback,
149
+ label: "ReviewRecord.final_output_ref",
150
+ });
151
+ return fallback;
152
+ }
153
+ async function assertSamePath(args) {
154
+ const expected = path.resolve(args.expected);
155
+ const actual = path.resolve(args.actual);
156
+ if (expected === actual)
157
+ return;
158
+ const realExpected = await realpathIfExists(expected);
159
+ const realActual = await realpathIfExists(actual);
160
+ if (realExpected && realActual && path.resolve(realExpected) === path.resolve(realActual)) {
161
+ return;
162
+ }
163
+ throw new Error(`${args.label} mismatch: expected ${expected}, received ${actual}`);
164
+ }
142
165
  function buildOpeningBriefPresentation(input) {
143
166
  return {
144
167
  prompt: [
145
168
  "Explain this onto review opening brief to the user before execution.",
146
169
  "Use only the provided input facts. Do not infer or invent target scope, boundary, domain, lens set, model, provider, or execution mode.",
147
- "Cover: what is being reviewed, why, filesystem boundary, selected domain, review mode and lens set, execution path, model/provider settings, and where the user can change configuration.",
170
+ "Cover: what is being reviewed, why, filesystem boundary, selected domain and domain selection reason, review mode and lens set, execution path, model/provider settings, and where the user can change configuration.",
148
171
  "Keep it structured and concise. Use the user's conversation language.",
149
172
  ].join("\n"),
150
173
  input,
@@ -180,6 +203,399 @@ const OPENING_PRESENTATION_SOURCE_REF_KEYS = [
180
203
  "review_target_profile",
181
204
  "review_context_manifest",
182
205
  ];
206
+ const ACTIVE_REVIEW_ATTEMPT_FILENAME = "active-review-attempt.yaml";
207
+ const ENVIRONMENT_WARNINGS_FILENAME = "environment-warnings.yaml";
208
+ const REVIEW_CANCEL_REQUEST_FILENAME = "review-cancel-request.yaml";
209
+ const DEFAULT_ACTIVE_ATTEMPT_STALE_AFTER_SECONDS = 1_200;
210
+ const REVIEW_RUNNER_WARNING_PREFIX = "[review runner warning]";
211
+ function activeAttemptPath(sessionRoot) {
212
+ return path.join(sessionRoot, ACTIVE_REVIEW_ATTEMPT_FILENAME);
213
+ }
214
+ function environmentWarningsPath(sessionRoot) {
215
+ return path.join(sessionRoot, ENVIRONMENT_WARNINGS_FILENAME);
216
+ }
217
+ function reviewCancelRequestPath(sessionRoot) {
218
+ return path.join(sessionRoot, REVIEW_CANCEL_REQUEST_FILENAME);
219
+ }
220
+ function activeAttemptStaleAfterSeconds() {
221
+ const parsed = Number(process.env.ONTO_REVIEW_ACTIVE_ATTEMPT_STALE_AFTER_SECONDS);
222
+ return Number.isFinite(parsed) && parsed > 0
223
+ ? Math.floor(parsed)
224
+ : DEFAULT_ACTIVE_ATTEMPT_STALE_AFTER_SECONDS;
225
+ }
226
+ function stripDomainTokenValue(domainValue) {
227
+ const trimmed = domainValue.trim();
228
+ return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
229
+ }
230
+ function stableJson(value) {
231
+ if (Array.isArray(value)) {
232
+ return `[${value.map((item) => stableJson(item)).join(",")}]`;
233
+ }
234
+ if (value && typeof value === "object") {
235
+ const record = value;
236
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
237
+ }
238
+ return JSON.stringify(value);
239
+ }
240
+ function normalizeRefForHash(ref) {
241
+ if (!ref)
242
+ return null;
243
+ return path.normalize(ref).split(path.sep).join(path.posix.sep);
244
+ }
245
+ function canonicalReviewRequestIdentity(input) {
246
+ if (!input.target || !input.intent)
247
+ return null;
248
+ return {
249
+ schema: "review-request-identity-v2",
250
+ target: normalizeRefForHash(input.target),
251
+ intent: input.intent,
252
+ domain: input.domain ? normalizeDomainValue(input.domain) : null,
253
+ targetScopeKind: input.targetScopeKind,
254
+ primaryRef: normalizeRefForHash(input.primaryRef ?? undefined),
255
+ memberRefs: input.memberRefs.map((ref) => normalizeRefForHash(ref) ?? ref).sort(),
256
+ bundleKind: input.bundleKind,
257
+ reviewMode: input.reviewMode,
258
+ lensIds: [...input.lensIds].sort(),
259
+ };
260
+ }
261
+ function hashReviewRequestIdentity(identity) {
262
+ if (!identity)
263
+ return null;
264
+ return crypto.createHash("sha256").update(stableJson(identity)).digest("hex");
265
+ }
266
+ function requestHashForReviewInput(args) {
267
+ return hashReviewRequestIdentity(canonicalReviewRequestIdentity({
268
+ target: args.target,
269
+ intent: args.intent,
270
+ domain: args.noDomain ? "none" : args.domain ?? null,
271
+ targetScopeKind: args.targetScopeKind ?? null,
272
+ primaryRef: args.primaryRef ?? null,
273
+ memberRefs: args.memberRefs ?? [],
274
+ bundleKind: args.bundleKind ?? null,
275
+ reviewMode: args.reviewMode ?? null,
276
+ lensIds: args.lensIds ?? [],
277
+ }));
278
+ }
279
+ function requestHashFromArtifacts(args) {
280
+ const scope = args.interpretation?.target_scope_candidate;
281
+ return hashReviewRequestIdentity(canonicalReviewRequestIdentity({
282
+ target: args.metadata?.requested_target ?? null,
283
+ intent: args.interpretation?.intent_summary ?? null,
284
+ domain: args.binding?.resolved_session_domain ??
285
+ args.metadata?.requested_domain_token ??
286
+ null,
287
+ targetScopeKind: scope?.kind ?? null,
288
+ primaryRef: scope?.primary_ref ?? null,
289
+ memberRefs: scope?.member_refs ?? [],
290
+ bundleKind: scope?.bundle_kind ?? null,
291
+ reviewMode: args.binding?.resolved_review_mode ??
292
+ args.interpretation?.review_mode_recommendation ??
293
+ null,
294
+ lensIds: args.binding?.resolved_lens_set ??
295
+ args.interpretation?.lens_selection_plan?.recommended_lenses ??
296
+ [],
297
+ }));
298
+ }
299
+ function domainTokenResolution(args) {
300
+ const requestedToken = args.requestedToken ?? "";
301
+ const stripped = stripDomainTokenValue(requestedToken);
302
+ const normalized = args.normalizedDomain ?? null;
303
+ const suggestionIds = args.suggestionIds ?? [];
304
+ if (normalized === "none") {
305
+ return {
306
+ requestedToken,
307
+ normalizedDomain: null,
308
+ resolution: "no_domain",
309
+ suggestionIds,
310
+ };
311
+ }
312
+ if (!normalized) {
313
+ return {
314
+ requestedToken,
315
+ normalizedDomain: null,
316
+ resolution: suggestionIds.length > 0 ? "suggestion" : "unknown",
317
+ suggestionIds,
318
+ };
319
+ }
320
+ return {
321
+ requestedToken,
322
+ normalizedDomain: normalized,
323
+ resolution: stripped.length > 0 && normalizeDomainValue(stripped) !== stripped
324
+ ? "alias"
325
+ : "exact",
326
+ suggestionIds,
327
+ };
328
+ }
329
+ async function availableDomainIds(projectRoot, ontoHome) {
330
+ const roots = [
331
+ path.join(path.resolve(projectRoot), ".onto", "domains"),
332
+ path.join(os.homedir(), ".onto", "domains"),
333
+ path.join(ontoHome, ".onto", "domains"),
334
+ ];
335
+ const ids = new Set();
336
+ for (const root of roots) {
337
+ for (const id of await listDomainDirs(root)) {
338
+ ids.add(id);
339
+ }
340
+ }
341
+ return [...ids].sort();
342
+ }
343
+ function domainSimilarityScore(requested, candidate) {
344
+ const requestedTokens = new Set(requested.toLowerCase().split(/[-_\s]+/).filter(Boolean));
345
+ const candidateTokens = new Set(candidate.toLowerCase().split(/[-_\s]+/).filter(Boolean));
346
+ let overlap = 0;
347
+ for (const token of requestedTokens) {
348
+ if (candidateTokens.has(token))
349
+ overlap += 1;
350
+ }
351
+ if (candidate.toLowerCase().includes(requested.toLowerCase()) ||
352
+ requested.toLowerCase().includes(candidate.toLowerCase())) {
353
+ overlap += 2;
354
+ }
355
+ return overlap;
356
+ }
357
+ function suggestDomainIds(requestedToken, availableIds) {
358
+ const stripped = stripDomainTokenValue(requestedToken).toLowerCase();
359
+ if (!stripped)
360
+ return [];
361
+ return availableIds
362
+ .map((id) => ({ id, score: domainSimilarityScore(stripped, id) }))
363
+ .filter((entry) => entry.score > 0)
364
+ .sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
365
+ .slice(0, 5)
366
+ .map((entry) => entry.id);
367
+ }
368
+ async function validateRequestedDomainForDispatch(request, ontoHome) {
369
+ if (!request.domain || request.noDomain)
370
+ return;
371
+ const normalizedDomain = normalizeDomainValue(request.domain);
372
+ if (normalizedDomain === "none")
373
+ return;
374
+ const domains = await availableDomainIds(request.projectRoot, ontoHome);
375
+ if (domains.includes(normalizedDomain))
376
+ return;
377
+ const suggestionIds = suggestDomainIds(request.domain, domains);
378
+ throw new ReviewDomainResolutionError({
379
+ message: suggestionIds.length > 0
380
+ ? `Unknown review domain ${request.domain}. Did you mean: ${suggestionIds.join(", ")}?`
381
+ : `Unknown review domain ${request.domain}; no safe domain suggestion is available.`,
382
+ domainResolution: domainTokenResolution({
383
+ requestedToken: request.domain,
384
+ normalizedDomain: null,
385
+ suggestionIds,
386
+ }),
387
+ });
388
+ }
389
+ function reviewTerminalStatus(status) {
390
+ return (status === "completed" ||
391
+ status === "completed_with_degradation" ||
392
+ status === "halted_partial" ||
393
+ status === "failed");
394
+ }
395
+ async function readTargetMaterialSupport(sessionRoot, executionPlan) {
396
+ const targetProfilePath = executionPlan?.review_target_profile_path ??
397
+ path.join(sessionRoot, "execution-preparation", "review-target-profile.yaml");
398
+ const targetProfile = await readOptionalYaml(targetProfilePath);
399
+ const profile = targetProfile?.material_profile;
400
+ if (!profile)
401
+ return null;
402
+ return {
403
+ targetMaterialKind: profile.target_material_kind,
404
+ supportStatus: profile.support_status,
405
+ unsupportedReason: profile.unsupported_reason,
406
+ detectionConfidence: profile.detection.confidence,
407
+ detectionConfidenceBasis: profile.detection.confidence_basis,
408
+ };
409
+ }
410
+ async function readEnvironmentWarnings(sessionRoot) {
411
+ const artifact = await readOptionalYaml(environmentWarningsPath(sessionRoot));
412
+ return artifact?.warnings ?? [];
413
+ }
414
+ async function writeEnvironmentWarningsFromStderr(args) {
415
+ const warningLines = args.stderr
416
+ .map((line) => line.trim())
417
+ .filter((line) => line.startsWith(REVIEW_RUNNER_WARNING_PREFIX))
418
+ .map((line) => line.slice(REVIEW_RUNNER_WARNING_PREFIX.length).trim())
419
+ .filter((line) => line.length > 0);
420
+ if (warningLines.length === 0)
421
+ return [];
422
+ const existing = await readEnvironmentWarnings(args.sessionRoot);
423
+ const seenMessages = new Set(existing.map((warning) => warning.message));
424
+ const observedAt = isoNow();
425
+ const additions = [];
426
+ for (const [index, message] of warningLines.entries()) {
427
+ if (seenMessages.has(message))
428
+ continue;
429
+ const digest = crypto
430
+ .createHash("sha256")
431
+ .update(`${observedAt}\n${index}\n${message}`)
432
+ .digest("hex")
433
+ .slice(0, 12);
434
+ additions.push({
435
+ warningId: `environment-warning-${digest}`,
436
+ source: "review_runner_warning",
437
+ message,
438
+ fatality: "non_fatal",
439
+ affectedCapability: "review_execution_observability",
440
+ outputTrustImpact: "unknown",
441
+ observedAt,
442
+ });
443
+ }
444
+ const warnings = [...existing, ...additions];
445
+ if (warnings.length > 0) {
446
+ await writeYamlDocument(environmentWarningsPath(args.sessionRoot), {
447
+ schema_version: "1",
448
+ session_id: basenameSessionId(args.sessionRoot),
449
+ created_at: observedAt,
450
+ warnings,
451
+ });
452
+ }
453
+ return warnings;
454
+ }
455
+ function activeUnitsForInitialReview(executionPlan) {
456
+ const lensIds = (executionPlan?.lens_execution_seats ?? []).map((seat) => seat.lens_id);
457
+ return lensIds.length > 0
458
+ ? lensIds.map((lensId) => `lens:${lensId}`)
459
+ : ["review_execution"];
460
+ }
461
+ function requestedUnitsMatchActive(activeUnits, targetUnits) {
462
+ if (activeUnits.length === 0)
463
+ return false;
464
+ if (!targetUnits || targetUnits.length === 0)
465
+ return true;
466
+ const normalizedActive = new Set(activeUnits.flatMap((unit) => {
467
+ const suffix = unit.includes(":") ? unit.split(":").at(-1) ?? unit : unit;
468
+ return [unit, suffix];
469
+ }));
470
+ return targetUnits.some((unit) => normalizedActive.has(unit));
471
+ }
472
+ async function writeActiveAttemptStarted(args) {
473
+ const sessionMetadata = await readOptionalYaml(path.join(args.sessionRoot, "session-metadata.yaml"));
474
+ const artifactRefs = await collectArtifactRefs(args.sessionRoot);
475
+ const observed = await artifactObservation(artifactRefs);
476
+ const now = isoNow();
477
+ const artifact = {
478
+ schema_version: "1",
479
+ attempt_id: args.attemptId,
480
+ attempt_kind: args.attemptKind,
481
+ session_id: sessionMetadata?.session_id ?? basenameSessionId(args.sessionRoot),
482
+ session_root: args.sessionRoot,
483
+ project_root: sessionMetadata?.project_root ?? null,
484
+ created_at: now,
485
+ updated_at: now,
486
+ status: "started",
487
+ active_units: args.activeUnits,
488
+ requested_frontier_units: args.requestedFrontierUnits ?? [],
489
+ run_control: {
490
+ stale_after_seconds: activeAttemptStaleAfterSeconds(),
491
+ source_tool: args.sourceTool,
492
+ request_hash: args.requestHash,
493
+ },
494
+ latest_observed_artifact_ref: observed.ref,
495
+ };
496
+ await writeYamlDocument(activeAttemptPath(args.sessionRoot), artifact);
497
+ }
498
+ async function updateActiveAttemptTerminal(args) {
499
+ const attemptPath = activeAttemptPath(args.sessionRoot);
500
+ const existing = await readOptionalYaml(attemptPath);
501
+ if (!existing)
502
+ return;
503
+ const artifactRefs = await collectArtifactRefs(args.sessionRoot);
504
+ const observed = await artifactObservation(artifactRefs);
505
+ await writeYamlDocument(attemptPath, {
506
+ ...existing,
507
+ updated_at: isoNow(),
508
+ status: args.status,
509
+ latest_observed_artifact_ref: observed.ref,
510
+ ...(args.errorMessage !== undefined ? { error_message: args.errorMessage } : {}),
511
+ });
512
+ }
513
+ async function activeAttemptProjection(sessionRoot) {
514
+ const attemptPath = activeAttemptPath(sessionRoot);
515
+ const artifact = await readOptionalYaml(attemptPath);
516
+ if (!artifact)
517
+ return null;
518
+ const updatedMs = parseTimestampMs(artifact.updated_at);
519
+ const secondsSinceUpdated = secondsBetween(Date.now(), updatedMs);
520
+ const staleAfterSeconds = artifact.run_control?.stale_after_seconds ?? DEFAULT_ACTIVE_ATTEMPT_STALE_AFTER_SECONDS;
521
+ const isStale = artifact.status === "started" &&
522
+ secondsSinceUpdated !== null &&
523
+ secondsSinceUpdated > staleAfterSeconds;
524
+ return {
525
+ attemptId: artifact.attempt_id,
526
+ attemptKind: artifact.attempt_kind,
527
+ status: artifact.status,
528
+ sessionId: artifact.session_id,
529
+ sessionRoot: artifact.session_root,
530
+ startedAt: artifact.created_at,
531
+ updatedAt: artifact.updated_at,
532
+ activeUnits: artifact.active_units ?? [],
533
+ requestedFrontierUnits: artifact.requested_frontier_units ?? [],
534
+ latestObservedArtifactRef: artifact.latest_observed_artifact_ref ?? null,
535
+ staleAfterSeconds,
536
+ secondsSinceUpdated,
537
+ isStale,
538
+ attemptManifestRef: attemptPath,
539
+ };
540
+ }
541
+ async function buildRunControl(sessionRoot, status) {
542
+ const activeAttempt = await activeAttemptProjection(sessionRoot);
543
+ const cancellationRequestRef = await fileExists(reviewCancelRequestPath(sessionRoot))
544
+ ? reviewCancelRequestPath(sessionRoot)
545
+ : null;
546
+ const cancellationRequested = cancellationRequestRef !== null;
547
+ const alreadyRunning = status === "running" &&
548
+ activeAttempt?.status === "started" &&
549
+ !activeAttempt.isStale;
550
+ const lifecycleState = status === "completed" || status === "completed_with_degradation"
551
+ ? "completed"
552
+ : status === "halted_partial"
553
+ ? "halted"
554
+ : activeAttempt?.status === "failed"
555
+ ? "failed_attempt"
556
+ : activeAttempt?.status === "started" && activeAttempt.isStale
557
+ ? "stale_active"
558
+ : cancellationRequested
559
+ ? "cancellation_requested"
560
+ : alreadyRunning
561
+ ? "active"
562
+ : status === "prepared"
563
+ ? "prepared"
564
+ : "unknown";
565
+ const continuationAvailable = status === "prepared" ||
566
+ status === "halted_partial" ||
567
+ lifecycleState === "failed_attempt" ||
568
+ lifecycleState === "stale_active";
569
+ const cancellationAvailable = alreadyRunning && !cancellationRequested;
570
+ const statusReason = lifecycleState === "active"
571
+ ? "review attempt is actively running and can be cancelled"
572
+ : lifecycleState === "cancellation_requested"
573
+ ? "cancellation has already been requested and will be observed at a runtime checkpoint"
574
+ : lifecycleState === "stale_active"
575
+ ? "active attempt is stale; use review_status evidence before continuing"
576
+ : lifecycleState === "failed_attempt"
577
+ ? "active attempt failed before a stronger terminal execution artifact was written"
578
+ : lifecycleState === "prepared"
579
+ ? "review is prepared but no worker attempt is active"
580
+ : lifecycleState === "halted"
581
+ ? "review has halted through execution artifacts"
582
+ : lifecycleState === "completed"
583
+ ? "review is terminally completed"
584
+ : "no actionable run-control state is available";
585
+ return {
586
+ activeAttempt,
587
+ lifecycleState,
588
+ alreadyRunning,
589
+ cancellationAvailable,
590
+ cancellationRequested,
591
+ cancellationRequestRef,
592
+ continuationAvailable,
593
+ retryAvailable: continuationAvailable,
594
+ retrySemantics: "use_review_continue",
595
+ hostTimeoutSemantics: "review_continues_under_session",
596
+ statusReason,
597
+ };
598
+ }
183
599
  function compactSeverityCounts(summary) {
184
600
  return [
185
601
  `blocker=${summary.severity_counts.blocker}`,
@@ -213,140 +629,6 @@ function buildHaltPresentation(input) {
213
629
  input,
214
630
  };
215
631
  }
216
- function progressEvent(args) {
217
- return {
218
- presentation_contract_version: REVIEW_PRESENTATION_CONTRACT_VERSION,
219
- event_kind: "mcp_progress",
220
- sequence: args.sequence,
221
- generated_at: isoNow(),
222
- source: args.source,
223
- stage: args.stage,
224
- session_root: args.sessionRoot,
225
- message: args.message,
226
- progress: {
227
- current: args.current,
228
- total: args.total ?? 100,
229
- ...(args.exactStep !== undefined ? { exact_step: args.exactStep } : {}),
230
- ...(args.exactTotal !== undefined ? { exact_total: args.exactTotal } : {}),
231
- ...(args.label !== undefined ? { label: args.label } : {}),
232
- },
233
- };
234
- }
235
- function progressUnitsForInvokeStep(step) {
236
- switch (step) {
237
- case 1:
238
- return 5;
239
- case 2:
240
- return 10;
241
- case 3:
242
- return 90;
243
- default:
244
- return 0;
245
- }
246
- }
247
- function progressUnitsForRuntimeStep(step, total) {
248
- if (total <= 0)
249
- return 10;
250
- return Math.min(89, 10 + Math.round((step / total) * 75));
251
- }
252
- function parseSessionRootLine(projectRoot, line) {
253
- const match = /^\s*session_root:\s+(.+?)\s*$/.exec(line);
254
- if (!match?.[1])
255
- return null;
256
- const rawSessionRoot = match[1];
257
- return path.isAbsolute(rawSessionRoot)
258
- ? rawSessionRoot
259
- : path.resolve(projectRoot, rawSessionRoot);
260
- }
261
- function consoleLineProgressEvent(args) {
262
- const plannedSessionRoot = parseSessionRootLine(args.projectRoot, args.line);
263
- if (plannedSessionRoot) {
264
- return {
265
- sessionRoot: plannedSessionRoot,
266
- event: progressEvent({
267
- sequence: args.sequence,
268
- source: "review_invoke_console",
269
- stage: "session_planned",
270
- sessionRoot: plannedSessionRoot,
271
- message: `Review session planned at ${plannedSessionRoot}.`,
272
- current: 1,
273
- label: "session planned",
274
- }),
275
- };
276
- }
277
- if (args.line.trim() === "[review start]") {
278
- return {
279
- sessionRoot: args.sessionRoot,
280
- event: progressEvent({
281
- sequence: args.sequence,
282
- source: "review_invoke_console",
283
- stage: "start_preview",
284
- sessionRoot: args.sessionRoot,
285
- message: "Review start preview generated.",
286
- current: 0,
287
- label: "start preview",
288
- }),
289
- };
290
- }
291
- const invokeStepMatch = /^\[review invoke\] step (\d+)\/3\s+(.+?)\s*$/.exec(args.line);
292
- if (invokeStepMatch?.[1] && invokeStepMatch[2]) {
293
- const step = Number.parseInt(invokeStepMatch[1], 10);
294
- const label = invokeStepMatch[2];
295
- return {
296
- sessionRoot: args.sessionRoot,
297
- event: progressEvent({
298
- sequence: args.sequence,
299
- source: "review_invoke_console",
300
- stage: "invoke_step",
301
- sessionRoot: args.sessionRoot,
302
- message: label,
303
- current: progressUnitsForInvokeStep(step),
304
- exactStep: step,
305
- exactTotal: 3,
306
- label,
307
- }),
308
- };
309
- }
310
- const runtimeStepMatch = /^\[review progress\]\s+(\d+)\/(\d+)\s+(.+?)\s*$/.exec(args.line);
311
- if (runtimeStepMatch?.[1] && runtimeStepMatch[2] && runtimeStepMatch[3]) {
312
- const step = Number.parseInt(runtimeStepMatch[1], 10);
313
- const total = Number.parseInt(runtimeStepMatch[2], 10);
314
- const label = runtimeStepMatch[3];
315
- return {
316
- sessionRoot: args.sessionRoot,
317
- event: progressEvent({
318
- sequence: args.sequence,
319
- source: "review_invoke_console",
320
- stage: "runtime_step",
321
- sessionRoot: args.sessionRoot,
322
- message: label,
323
- current: progressUnitsForRuntimeStep(step, total),
324
- exactStep: step,
325
- exactTotal: total,
326
- label,
327
- }),
328
- };
329
- }
330
- const completedMatch = /^\[review invoke\] completed 3\/3\s+(.+?)\s*$/.exec(args.line);
331
- if (completedMatch?.[1]) {
332
- const label = completedMatch[1];
333
- return {
334
- sessionRoot: args.sessionRoot,
335
- event: progressEvent({
336
- sequence: args.sequence,
337
- source: "review_invoke_console",
338
- stage: "completed",
339
- sessionRoot: args.sessionRoot,
340
- message: label,
341
- current: 98,
342
- exactStep: 3,
343
- exactTotal: 3,
344
- label,
345
- }),
346
- };
347
- }
348
- return null;
349
- }
350
632
  function generatedFromArtifactRefs(artifactRefs, keys = PRESENTATION_SOURCE_REF_KEYS) {
351
633
  const refs = {};
352
634
  for (const key of keys) {
@@ -462,6 +744,241 @@ function livenessSummary(args) {
462
744
  return `Review is still active at ${args.currentLabel ?? "unknown step"}, but no artifact change has been observed for the stale threshold.${active}${lastArtifact}${since}`;
463
745
  }
464
746
  }
747
+ const RUNTIME_UNIT_STALE_AFTER_SECONDS = 300;
748
+ async function observeFile(filePath) {
749
+ if (!filePath)
750
+ return { exists: false, size: 0, mtimeMs: null };
751
+ try {
752
+ const stat = await fs.stat(filePath);
753
+ return {
754
+ exists: true,
755
+ size: stat.size,
756
+ mtimeMs: stat.mtimeMs,
757
+ };
758
+ }
759
+ catch {
760
+ return { exists: false, size: 0, mtimeMs: null };
761
+ }
762
+ }
763
+ function allReviewUnitResults(executionResult) {
764
+ if (!executionResult)
765
+ return [];
766
+ return [
767
+ ...executionResult.lens_execution_results,
768
+ ...(executionResult.issue_artifact_execution_results ?? []),
769
+ ...(executionResult.deliberation_execution_results ?? []),
770
+ ...(executionResult.synthesize_execution_result
771
+ ? [executionResult.synthesize_execution_result]
772
+ : []),
773
+ ];
774
+ }
775
+ function parseRuntimeLogHeading(rawHeading) {
776
+ const match = /^##\s+(.+?)\s+\|\s+(.+?)\s*$/.exec(rawHeading.trim());
777
+ if (!match?.[2])
778
+ return null;
779
+ return {
780
+ at: match[1] ?? null,
781
+ title: match[2],
782
+ };
783
+ }
784
+ function bodyScalar(body, key) {
785
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
786
+ const match = new RegExp(`^${escaped}:\\s*(.+?)\\s*$`, "m").exec(body);
787
+ return match?.[1] ?? null;
788
+ }
789
+ function parseAttempt(body) {
790
+ const raw = bodyScalar(body, "attempt");
791
+ if (!raw)
792
+ return null;
793
+ const match = /^(\d+)/.exec(raw.trim());
794
+ if (!match?.[1])
795
+ return null;
796
+ const parsed = Number.parseInt(match[1], 10);
797
+ return Number.isFinite(parsed) ? parsed : null;
798
+ }
799
+ function runtimeSignalFromEntry(entry) {
800
+ const [headingLine, ...bodyLines] = entry.split(/\r?\n/);
801
+ if (!headingLine)
802
+ return null;
803
+ const heading = parseRuntimeLogHeading(headingLine);
804
+ if (!heading)
805
+ return null;
806
+ const body = bodyLines.join("\n");
807
+ const title = heading.title;
808
+ const titlePatterns = [
809
+ {
810
+ kind: "started",
811
+ pattern: /^runner dispatch started: (.+?)\s*$/,
812
+ summary: "runner dispatch started",
813
+ },
814
+ {
815
+ kind: "retry",
816
+ pattern: /^runner dispatch retry: (.+?)\s*$/,
817
+ summary: "runner dispatch retry",
818
+ },
819
+ {
820
+ kind: "completed",
821
+ pattern: /^runner (?:nested )?dispatch completed: (.+?)\s*$/,
822
+ summary: "runner dispatch completed",
823
+ },
824
+ {
825
+ kind: "failed",
826
+ pattern: /^(?:lens|deliberation|issue_artifact|synthesize) failure: (.+?)\s*$/,
827
+ summary: "runner dispatch failed",
828
+ },
829
+ ];
830
+ for (const candidate of titlePatterns) {
831
+ const match = candidate.pattern.exec(title);
832
+ if (!match?.[1])
833
+ continue;
834
+ const atMs = parseTimestampMs(heading.at);
835
+ return {
836
+ unitId: match[1],
837
+ kind: candidate.kind,
838
+ summary: candidate.summary,
839
+ at: heading.at,
840
+ atMs,
841
+ attempt: candidate.kind === "retry" ? parseAttempt(body) : null,
842
+ failureMessage: candidate.kind === "failed" ? bodyScalar(body, "message") : null,
843
+ };
844
+ }
845
+ return null;
846
+ }
847
+ async function runtimeLogSignalsByUnit(errorLogPath) {
848
+ const text = await readOptionalText(errorLogPath);
849
+ const signals = new Map();
850
+ if (!text)
851
+ return signals;
852
+ for (const rawEntry of text.split(/\n(?=## )/)) {
853
+ const signal = runtimeSignalFromEntry(rawEntry.trimEnd());
854
+ if (!signal)
855
+ continue;
856
+ signals.set(signal.unitId, [...(signals.get(signal.unitId) ?? []), signal]);
857
+ }
858
+ return signals;
859
+ }
860
+ function latestRuntimeSignal(signals) {
861
+ if (signals.length === 0)
862
+ return null;
863
+ return signals.reduce((latest, signal) => {
864
+ const latestMs = latest.atMs ?? -1;
865
+ const signalMs = signal.atMs ?? -1;
866
+ return signalMs >= latestMs ? signal : latest;
867
+ });
868
+ }
869
+ function runtimeSignalCount(signals, kind) {
870
+ return signals.filter((signal) => signal.kind === kind).length;
871
+ }
872
+ function runtimeUnitAlias(unitKind, unitId) {
873
+ if (unitKind === "lens")
874
+ return `lens:${unitId}`;
875
+ if (unitKind === "deliberation" && unitId.startsWith("deliberation-")) {
876
+ return `deliberation:${unitId.replace(/^deliberation-/, "")}`;
877
+ }
878
+ return unitId;
879
+ }
880
+ function runtimeUnitStepId(unitKind) {
881
+ if (unitKind === "lens")
882
+ return "lens_dispatch";
883
+ if (unitKind === "deliberation")
884
+ return "lens_deliberation_responses";
885
+ return null;
886
+ }
887
+ async function deriveRuntimeUnitProgress(args) {
888
+ const executionPlan = args.executionPlan;
889
+ if (!executionPlan)
890
+ return [];
891
+ const signalsByUnit = await runtimeLogSignalsByUnit(executionPlan.error_log_path);
892
+ const terminalResultsByUnit = new Map(allReviewUnitResults(args.executionResult).map((result) => [
893
+ result.unit_id,
894
+ result,
895
+ ]));
896
+ const lensUnits = executionPlan.lens_execution_seats.map((seat) => {
897
+ const packetPath = executionPlan.lens_prompt_packet_seats.find((packetSeat) => packetSeat.lens_id === seat.lens_id)?.packet_path ?? null;
898
+ return {
899
+ unitId: seat.lens_id,
900
+ unitKind: "lens",
901
+ packetPath,
902
+ outputPath: seat.output_path,
903
+ runningLogRef: path.join(path.dirname(seat.output_path), `.${seat.lens_id}.running.log`),
904
+ };
905
+ });
906
+ const projections = [];
907
+ for (const unit of lensUnits) {
908
+ const output = await observeFile(unit.outputPath);
909
+ const runningLog = await observeFile(unit.runningLogRef);
910
+ const signals = signalsByUnit.get(unit.unitId) ?? [];
911
+ const latestSignal = latestRuntimeSignal(signals);
912
+ const terminalResult = terminalResultsByUnit.get(unit.unitId);
913
+ const retryCount = runtimeSignalCount(signals, "retry");
914
+ const hasStarted = signals.some((signal) => signal.kind === "started") ||
915
+ runningLog.exists;
916
+ const attemptCount = Math.max(terminalResult ? 1 : 0, hasStarted ? retryCount + 1 : 0);
917
+ let latestSignalName = latestSignal?.summary ?? null;
918
+ let latestSignalAt = latestSignal?.at ?? null;
919
+ let latestSignalMs = latestSignal?.atMs ?? null;
920
+ if (runningLog.exists &&
921
+ runningLog.mtimeMs !== null &&
922
+ (latestSignalMs === null || runningLog.mtimeMs > latestSignalMs)) {
923
+ latestSignalName = "running log updated";
924
+ latestSignalAt = isoFromTimestamp(runningLog.mtimeMs);
925
+ latestSignalMs = runningLog.mtimeMs;
926
+ }
927
+ let status = "pending";
928
+ let failureMessage = latestSignal?.failureMessage ?? null;
929
+ if (terminalResult?.status === "failed") {
930
+ status = "failed";
931
+ latestSignalName = "terminal execution result failed";
932
+ latestSignalAt = terminalResult.completed_at;
933
+ latestSignalMs = parseTimestampMs(terminalResult.completed_at);
934
+ failureMessage = terminalResult.failure_message ?? failureMessage;
935
+ }
936
+ else if (terminalResult?.status === "completed" ||
937
+ latestSignal?.kind === "completed" ||
938
+ (output.exists && output.size > 0)) {
939
+ status = "completed";
940
+ if (!latestSignalName || (output.mtimeMs !== null && output.mtimeMs > (latestSignalMs ?? -1))) {
941
+ latestSignalName = "output file present";
942
+ latestSignalAt = output.mtimeMs === null ? null : isoFromTimestamp(output.mtimeMs);
943
+ latestSignalMs = output.mtimeMs;
944
+ }
945
+ }
946
+ else if (latestSignal?.kind === "failed") {
947
+ status = "failed";
948
+ failureMessage = latestSignal.failureMessage;
949
+ }
950
+ else if (latestSignal?.kind === "retry") {
951
+ const seconds = secondsBetween(args.nowMs, latestSignalMs);
952
+ status =
953
+ seconds !== null && seconds > RUNTIME_UNIT_STALE_AFTER_SECONDS
954
+ ? "running_stale"
955
+ : "retrying";
956
+ }
957
+ else if (hasStarted) {
958
+ const seconds = secondsBetween(args.nowMs, latestSignalMs);
959
+ status =
960
+ seconds !== null && seconds > RUNTIME_UNIT_STALE_AFTER_SECONDS
961
+ ? "running_stale"
962
+ : "running";
963
+ }
964
+ projections.push({
965
+ unitId: unit.unitId,
966
+ publicAlias: runtimeUnitAlias(unit.unitKind, unit.unitId),
967
+ unitKind: unit.unitKind,
968
+ progressStepId: runtimeUnitStepId(unit.unitKind),
969
+ status,
970
+ packetPath: unit.packetPath,
971
+ outputPath: unit.outputPath,
972
+ runningLogRef: runningLog.exists ? unit.runningLogRef : null,
973
+ latestSignal: latestSignalName,
974
+ latestSignalAt,
975
+ secondsSinceLatestSignal: secondsBetween(args.nowMs, latestSignalMs),
976
+ attemptCount,
977
+ failureMessage,
978
+ });
979
+ }
980
+ return projections;
981
+ }
465
982
  async function existingSeatIds(seats) {
466
983
  const existing = [];
467
984
  for (const seat of seats ?? []) {
@@ -477,7 +994,7 @@ async function completedProgressStepIds(params) {
477
994
  const completed = [];
478
995
  if (params.executionPlan)
479
996
  completed.push("manifest_validation");
480
- const plannedLensIds = params.executionPlan?.lens_execution_seats.map((seat) => seat.lens_id) ?? [];
997
+ const plannedLensIds = (params.executionPlan?.lens_execution_seats ?? []).map((seat) => seat.lens_id);
481
998
  const completedLensIds = await existingSeatIds(params.executionPlan?.lens_execution_seats);
482
999
  const allPlannedLensesCompleted = plannedLensIds.length > 0 && completedLensIds.length >= plannedLensIds.length;
483
1000
  if (allPlannedLensesCompleted || params.artifactRefs.lens_completion_barrier) {
@@ -496,8 +1013,7 @@ async function completedProgressStepIds(params) {
496
1013
  if (params.artifactRefs.deliberation_plan)
497
1014
  completed.push("deliberation_plan");
498
1015
  const deliberationIds = await existingSeatIds(params.executionPlan?.lens_deliberation_prompt_packet_seats);
499
- const plannedDeliberationIds = params.executionPlan?.lens_deliberation_prompt_packet_seats.map((seat) => seat.lens_id) ??
500
- [];
1016
+ const plannedDeliberationIds = (params.executionPlan?.lens_deliberation_prompt_packet_seats ?? []).map((seat) => seat.lens_id);
501
1017
  if ((plannedDeliberationIds.length > 0 &&
502
1018
  deliberationIds.length >= plannedDeliberationIds.length) ||
503
1019
  params.artifactRefs.deliberation_output) {
@@ -531,14 +1047,13 @@ async function activeUnits(params) {
531
1047
  return typeof unitId === "string" && unitId.length > 0 ? [unitId] : [];
532
1048
  }
533
1049
  if (params.currentStepId === "lens_dispatch") {
534
- const planned = params.executionPlan?.lens_execution_seats.map((seat) => seat.lens_id) ?? [];
1050
+ const planned = (params.executionPlan?.lens_execution_seats ?? []).map((seat) => seat.lens_id);
535
1051
  const completed = new Set(await existingSeatIds(params.executionPlan?.lens_execution_seats));
536
1052
  const pending = planned.filter((lensId) => !completed.has(lensId));
537
1053
  return (pending.length > 0 ? pending : planned).map((lensId) => `lens:${lensId}`);
538
1054
  }
539
1055
  if (params.currentStepId === "lens_deliberation_responses") {
540
- const planned = params.executionPlan?.lens_deliberation_prompt_packet_seats.map((seat) => seat.lens_id) ??
541
- [];
1056
+ const planned = (params.executionPlan?.lens_deliberation_prompt_packet_seats ?? []).map((seat) => seat.lens_id);
542
1057
  const completed = new Set(await existingSeatIds(params.executionPlan?.lens_deliberation_prompt_packet_seats));
543
1058
  return planned
544
1059
  .filter((lensId) => !completed.has(lensId))
@@ -665,6 +1180,24 @@ async function buildReviewStatusPresentationInput(params) {
665
1180
  const completedStatus = params.status === "completed" || params.status === "completed_with_degradation";
666
1181
  const sessionStartMs = parseTimestampMs(params.executionResult?.execution_started_at) ??
667
1182
  parseTimestampMs(sessionMetadata?.created_at);
1183
+ const unitProgress = await deriveRuntimeUnitProgress({
1184
+ executionPlan: params.executionPlan,
1185
+ executionResult: params.executionResult,
1186
+ nowMs,
1187
+ });
1188
+ const runtimeActiveUnits = unitProgress
1189
+ .filter((unit) => unit.status === "running" ||
1190
+ unit.status === "retrying" ||
1191
+ unit.status === "running_stale")
1192
+ .map((unit) => unit.publicAlias);
1193
+ const progressActiveUnits = currentStepId === "lens_dispatch" && unitProgress.length > 0
1194
+ ? runtimeActiveUnits
1195
+ : await activeUnits({
1196
+ status: params.status,
1197
+ currentStepId,
1198
+ executionPlan: params.executionPlan,
1199
+ executionResult: params.executionResult,
1200
+ });
668
1201
  const progress = {
669
1202
  current_step: completedStatus
670
1203
  ? totalSteps
@@ -688,12 +1221,7 @@ async function buildReviewStatusPresentationInput(params) {
688
1221
  params.artifactRefs.execution_plan ? "execution_plan" : null,
689
1222
  ].filter((step) => step !== null)
690
1223
  : completedSteps,
691
- active_units: await activeUnits({
692
- status: params.status,
693
- currentStepId,
694
- executionPlan: params.executionPlan,
695
- executionResult: params.executionResult,
696
- }),
1224
+ active_units: progressActiveUnits,
697
1225
  pending_units: completedStatus
698
1226
  ? []
699
1227
  : params.status === "prepared"
@@ -712,6 +1240,7 @@ async function buildReviewStatusPresentationInput(params) {
712
1240
  : currentStepId
713
1241
  ? `next ${stepById(currentStepId).label} artifact or timeout`
714
1242
  : null,
1243
+ unit_progress: unitProgress,
715
1244
  };
716
1245
  const completedLensIds = await existingSeatIds(params.executionPlan?.lens_execution_seats);
717
1246
  const halt = haltPresentation({
@@ -744,6 +1273,8 @@ async function buildReviewStatusPresentationInput(params) {
744
1273
  secondsSinceLastArtifact,
745
1274
  }),
746
1275
  };
1276
+ const runControl = await buildRunControl(params.sessionRoot, params.status);
1277
+ const targetMaterialSupport = await readTargetMaterialSupport(params.sessionRoot, params.executionPlan);
747
1278
  return {
748
1279
  presentation_contract_version: REVIEW_PRESENTATION_CONTRACT_VERSION,
749
1280
  presentation_kind: "progress",
@@ -762,6 +1293,9 @@ async function buildReviewStatusPresentationInput(params) {
762
1293
  }),
763
1294
  result_classification_summary: resultClassificationSummary,
764
1295
  halt,
1296
+ run_control: runControl,
1297
+ target_material_support: targetMaterialSupport,
1298
+ environment_warnings: await readEnvironmentWarnings(params.sessionRoot),
765
1299
  };
766
1300
  }
767
1301
  async function buildPreparedOpeningBriefInput(sessionRoot, executionPlan) {
@@ -798,6 +1332,7 @@ async function buildPreparedOpeningBriefInput(sessionRoot, executionPlan) {
798
1332
  resolved_host_runtime: binding.resolved_host_runtime,
799
1333
  boundary_policy: binding.boundary_policy,
800
1334
  effective_boundary_state: binding.effective_boundary_state,
1335
+ binding_notes: binding.binding_notes ?? [],
801
1336
  }
802
1337
  : null,
803
1338
  review_target_profile: reviewTargetProfile
@@ -825,60 +1360,328 @@ async function buildPreparedOpeningBriefInput(sessionRoot, executionPlan) {
825
1360
  },
826
1361
  };
827
1362
  }
828
- function parseReviewInvokeOutput(stdout) {
829
- for (const line of [...stdout].reverse()) {
830
- const trimmed = line.trim();
831
- if (!trimmed.startsWith("{"))
1363
+ async function buildReviewRunHandle(args) {
1364
+ const artifactRefs = await collectArtifactRefs(args.sessionRoot);
1365
+ const executionPlan = await readOptionalYaml(path.join(args.sessionRoot, "execution-plan.yaml"));
1366
+ const metadata = await readOptionalYaml(path.join(args.sessionRoot, "session-metadata.yaml"));
1367
+ const interpretation = await readOptionalYaml(path.join(args.sessionRoot, "interpretation.yaml"));
1368
+ const binding = await readOptionalYaml(path.join(args.sessionRoot, "binding.yaml"));
1369
+ const targetProfile = await readOptionalYaml(executionPlan?.review_target_profile_path ??
1370
+ path.join(args.sessionRoot, "execution-preparation", "review-target-profile.yaml"));
1371
+ const requestHash = args.requestHash ??
1372
+ requestHashFromArtifacts({ metadata, interpretation, binding });
1373
+ const normalizedDomain = binding?.resolved_session_domain ??
1374
+ targetProfile?.domain ??
1375
+ (metadata?.requested_domain_token
1376
+ ? normalizeDomainValue(metadata.requested_domain_token)
1377
+ : null);
1378
+ return {
1379
+ schemaVersion: "1",
1380
+ sessionId: metadata?.session_id ?? executionPlan?.session_id ?? basenameSessionId(args.sessionRoot),
1381
+ sessionRoot: args.sessionRoot,
1382
+ invocationId: args.invocationId,
1383
+ status: args.status,
1384
+ projectRoot: metadata?.project_root ?? null,
1385
+ target: {
1386
+ requestedTarget: metadata?.requested_target ?? targetProfile?.requested_target ?? null,
1387
+ targetScopeKind: targetProfile?.target_scope_kind ?? null,
1388
+ targetMaterialKind: targetProfile?.target_material_kind ?? null,
1389
+ },
1390
+ domain: domainTokenResolution({
1391
+ requestedToken: metadata?.requested_domain_token ?? null,
1392
+ normalizedDomain,
1393
+ }),
1394
+ artifactRefs: {
1395
+ sessionMetadata: artifactRefs.session_metadata ?? null,
1396
+ executionPlan: artifactRefs.execution_plan ?? null,
1397
+ reviewRunManifest: artifactRefs.review_run_manifest ?? null,
1398
+ executionResult: artifactRefs.execution_result ?? null,
1399
+ finalOutput: artifactRefs.final_output ?? null,
1400
+ reviewRecord: artifactRefs.review_record ?? null,
1401
+ },
1402
+ requestHash,
1403
+ pollAfterSeconds: reviewTerminalStatus(args.status) ? null : 5,
1404
+ };
1405
+ }
1406
+ async function buildRunningReviewRunResult(args) {
1407
+ const sessionRoot = path.resolve(args.sessionRoot);
1408
+ const artifactRefs = await collectArtifactRefs(sessionRoot);
1409
+ const failures = await collectStructuredFailures(sessionRoot);
1410
+ const executionPlan = await readOptionalYaml(path.join(sessionRoot, "execution-plan.yaml"));
1411
+ const executionResult = await readOptionalYaml(path.join(sessionRoot, "execution-result.yaml"));
1412
+ const reviewRecord = await readOptionalReviewRecord(path.join(sessionRoot, "review-record.yaml"));
1413
+ const progressInput = await buildReviewStatusPresentationInput({
1414
+ sessionRoot,
1415
+ status: "running",
1416
+ artifactRefs,
1417
+ executionPlan,
1418
+ executionResult,
1419
+ reviewRecord,
1420
+ });
1421
+ const runHandle = await buildReviewRunHandle({
1422
+ sessionRoot,
1423
+ status: "running",
1424
+ invocationId: args.invocationId,
1425
+ ...(args.requestHash !== undefined ? { requestHash: args.requestHash } : {}),
1426
+ });
1427
+ const llmPresentation = {
1428
+ progress: buildProgressPresentation(progressInput),
1429
+ };
1430
+ if (executionPlan) {
1431
+ llmPresentation.openingBrief = buildOpeningBriefPresentation(await buildPreparedOpeningBriefInput(sessionRoot, executionPlan));
1432
+ }
1433
+ return {
1434
+ sessionId: runHandle.sessionId,
1435
+ sessionRoot,
1436
+ status: "running",
1437
+ finalOutputPath: executionPlan?.final_output_path ?? path.join(sessionRoot, "final-output.md"),
1438
+ reviewRecordPath: executionPlan?.review_record_path ?? path.join(sessionRoot, "review-record.yaml"),
1439
+ executionResultPath: executionPlan?.execution_result_path ?? path.join(sessionRoot, "execution-result.yaml"),
1440
+ reviewRunManifestPath: path.join(sessionRoot, "review-run-manifest.yaml"),
1441
+ deliberationStatus: null,
1442
+ participatingLensIds: [],
1443
+ degradedLensIds: [],
1444
+ artifactRefs,
1445
+ ...failures,
1446
+ routeVisibility: await buildReviewRouteVisibilityFromSession(sessionRoot),
1447
+ llmPresentation,
1448
+ runHandle,
1449
+ runControl: progressInput.run_control,
1450
+ targetMaterialSupport: progressInput.target_material_support,
1451
+ environmentWarnings: progressInput.environment_warnings,
1452
+ };
1453
+ }
1454
+ async function collectArtifactRefs(sessionRoot) {
1455
+ return collectReviewInvocationArtifactRefs(sessionRoot);
1456
+ }
1457
+ async function buildPipelineExecutionLedgerIfPossible(args) {
1458
+ if (!args.executionPlan)
1459
+ return undefined;
1460
+ const reviewRunManifest = await readOptionalYaml(path.join(args.sessionRoot, "review-run-manifest.yaml"));
1461
+ const lensCompletionBarrier = await readOptionalYaml(path.join(args.sessionRoot, "lens-completion-barrier.yaml"));
1462
+ try {
1463
+ return await buildReviewPipelineExecutionLedger({
1464
+ sessionRoot: args.sessionRoot,
1465
+ artifactRefs: args.artifactRefs,
1466
+ executionPlan: args.executionPlan,
1467
+ executionResult: args.executionResult,
1468
+ reviewRunManifest,
1469
+ lensCompletionBarrier,
1470
+ });
1471
+ }
1472
+ catch {
1473
+ return undefined;
1474
+ }
1475
+ }
1476
+ function workerExecutorToRealization(workerExecutor) {
1477
+ if (workerExecutor === "mock")
1478
+ return "mock";
1479
+ if (workerExecutor === "codex")
1480
+ return "codex";
1481
+ if (workerExecutor === "direct_call")
1482
+ return "ts_inline_http";
1483
+ return null;
1484
+ }
1485
+ function workerExecutorFromRealization(realization) {
1486
+ if (realization === "mock")
1487
+ return "mock";
1488
+ if (realization === "codex")
1489
+ return "codex";
1490
+ return "direct_call";
1491
+ }
1492
+ function reviewExecutionHostFromRuntime(hostRuntime, workerExecutor) {
1493
+ if (workerExecutor === "mock")
1494
+ return "standalone";
1495
+ if (workerExecutor === "codex")
1496
+ return "codex";
1497
+ if (hostRuntime === "openai" ||
1498
+ hostRuntime === "anthropic" ||
1499
+ hostRuntime === "grok" ||
1500
+ hostRuntime === "lmstudio") {
1501
+ return hostRuntime;
1502
+ }
1503
+ return "openai";
1504
+ }
1505
+ function reviewExecutionProfileFromManifest(manifest) {
1506
+ const profile = manifest?.review_execution_profile;
1507
+ const route = profile?.runtime_route;
1508
+ const workerExecutor = route?.worker_executor;
1509
+ if (workerExecutor !== "mock" &&
1510
+ workerExecutor !== "codex" &&
1511
+ workerExecutor !== "direct_call") {
1512
+ return undefined;
1513
+ }
1514
+ if (profile?.mode !== "main-workers" &&
1515
+ profile?.mode !== "nested-workers") {
1516
+ return undefined;
1517
+ }
1518
+ if (profile.teamlead === null ||
1519
+ typeof profile.teamlead !== "object" ||
1520
+ profile.lens === null ||
1521
+ typeof profile.lens !== "object" ||
1522
+ profile.synthesize === null ||
1523
+ typeof profile.synthesize !== "object" ||
1524
+ typeof profile.deliberation !== "string") {
1525
+ return undefined;
1526
+ }
1527
+ const host = reviewExecutionHostFromRuntime(route?.host_runtime, workerExecutor);
1528
+ const runtimeProvider = typeof route?.runtime_provider === "string" &&
1529
+ route.runtime_provider !== "mock" &&
1530
+ route.runtime_provider !== "codex"
1531
+ ? route.runtime_provider
1532
+ : undefined;
1533
+ const authMode = route?.auth_mode === "api_key" ||
1534
+ route?.auth_mode === "oauth" ||
1535
+ route?.auth_mode === "local"
1536
+ ? route.auth_mode
1537
+ : undefined;
1538
+ const reconstructed = {
1539
+ mode: profile.mode,
1540
+ teamlead: profile.teamlead,
1541
+ lens: profile.lens,
1542
+ synthesize: profile.synthesize,
1543
+ deliberation: profile.deliberation,
1544
+ worker_executor: workerExecutor,
1545
+ host,
1546
+ trace: Array.isArray(profile.trace)
1547
+ ? profile.trace.filter((item) => typeof item === "string")
1548
+ : [],
1549
+ };
1550
+ if (runtimeProvider) {
1551
+ reconstructed.provider =
1552
+ runtimeProvider;
1553
+ }
1554
+ if (authMode) {
1555
+ reconstructed.auth =
1556
+ authMode;
1557
+ }
1558
+ if (typeof profile.model === "string")
1559
+ reconstructed.model = profile.model;
1560
+ if (typeof profile.effort === "string")
1561
+ reconstructed.effort = profile.effort;
1562
+ if (typeof profile.service_tier === "string") {
1563
+ reconstructed.service_tier = profile.service_tier;
1564
+ }
1565
+ if (typeof profile.base_url === "string") {
1566
+ reconstructed.base_url = profile.base_url;
1567
+ }
1568
+ return reconstructed;
1569
+ }
1570
+ function reviewExecutionProfileFromActorProfiles(args) {
1571
+ const profiles = args.actorProfiles?.profiles ?? [];
1572
+ const teamlead = profiles.find((profile) => profile.actor_kind === "teamlead");
1573
+ const lens = profiles.find((profile) => profile.actor_kind === "lens");
1574
+ const synthesize = profiles.find((profile) => profile.actor_kind === "synthesize");
1575
+ if (!teamlead || !lens || !synthesize)
1576
+ return undefined;
1577
+ const workerExecutor = workerExecutorFromRealization(args.executorRealization);
1578
+ const host = reviewExecutionHostFromRuntime(teamlead.host_runtime, workerExecutor);
1579
+ const runtimeProvider = teamlead.runtime_provider &&
1580
+ teamlead.runtime_provider !== "mock" &&
1581
+ teamlead.runtime_provider !== "codex"
1582
+ ? teamlead.runtime_provider
1583
+ : undefined;
1584
+ const reconstructed = {
1585
+ mode: "main-workers",
1586
+ teamlead: { seat: teamlead.seat, llm: "inherit" },
1587
+ lens: { seat: lens.seat, llm: "inherit" },
1588
+ synthesize: { seat: synthesize.seat, llm: "inherit" },
1589
+ deliberation: "controlled-lens-deliberation",
1590
+ worker_executor: workerExecutor,
1591
+ host,
1592
+ trace: ["reconstructed_from_actor_invocation_profiles_for_continuation"],
1593
+ };
1594
+ if (runtimeProvider) {
1595
+ reconstructed.provider =
1596
+ runtimeProvider;
1597
+ }
1598
+ if (teamlead.auth_mode === "api_key" ||
1599
+ teamlead.auth_mode === "oauth" ||
1600
+ teamlead.auth_mode === "local") {
1601
+ reconstructed.auth =
1602
+ teamlead.auth_mode;
1603
+ }
1604
+ if (teamlead.model)
1605
+ reconstructed.model = teamlead.model;
1606
+ if (teamlead.effort)
1607
+ reconstructed.effort = teamlead.effort;
1608
+ if (teamlead.service_tier)
1609
+ reconstructed.service_tier = teamlead.service_tier;
1610
+ if (teamlead.base_url)
1611
+ reconstructed.base_url = teamlead.base_url;
1612
+ return reconstructed;
1613
+ }
1614
+ function executorRealizationFromManifest(manifest) {
1615
+ return workerExecutorToRealization(manifest?.review_execution_profile?.runtime_route?.worker_executor);
1616
+ }
1617
+ function continuationAttemptId() {
1618
+ const timestamp = new Date()
1619
+ .toISOString()
1620
+ .replace(/[-:]/g, "")
1621
+ .replace(/\..+$/, "Z");
1622
+ return `${timestamp}-${crypto.randomUUID().slice(0, 8)}`;
1623
+ }
1624
+ async function copySupersededArtifacts(args) {
1625
+ const backupRoot = path.join(args.attemptRoot, "superseded-artifacts");
1626
+ const backups = [];
1627
+ for (const [index, sourceRef] of args.artifactRefs.entries()) {
1628
+ if (!(await fileExists(sourceRef)))
832
1629
  continue;
1630
+ await fs.mkdir(backupRoot, { recursive: true });
1631
+ const backupRef = path.join(backupRoot, `${String(index + 1).padStart(3, "0")}-${path.basename(sourceRef)}`);
1632
+ await fs.copyFile(sourceRef, backupRef);
1633
+ backups.push({ sourceRef, backupRef });
1634
+ }
1635
+ return backups;
1636
+ }
1637
+ async function restoreSupersededArtifacts(backups) {
1638
+ const restores = [];
1639
+ for (const backup of backups) {
833
1640
  try {
834
- return JSON.parse(trimmed);
1641
+ await fs.mkdir(path.dirname(backup.sourceRef), { recursive: true });
1642
+ await fs.copyFile(backup.backupRef, backup.sourceRef);
1643
+ restores.push({ ...backup, restored: true });
835
1644
  }
836
- catch {
837
- // Keep looking: progress messages are not JSON.
1645
+ catch (error) {
1646
+ restores.push({
1647
+ ...backup,
1648
+ restored: false,
1649
+ errorMessage: error instanceof Error ? error.message : String(error),
1650
+ });
838
1651
  }
839
1652
  }
840
- throw new Error("review invocation completed without a structured JSON result.");
1653
+ return restores;
841
1654
  }
842
- function isReviewInvokeShape(value) {
843
- if (value === null || typeof value !== "object")
844
- return false;
845
- const reviewResult = value.review_result;
846
- return reviewResult !== null && typeof reviewResult === "object";
1655
+ function continuationSessionArtifactRefs(args) {
1656
+ return [
1657
+ args.executionPlan.execution_result_path,
1658
+ path.join(args.sessionRoot, "review-run-manifest.yaml"),
1659
+ path.join(args.sessionRoot, "degradation-summary.yaml"),
1660
+ args.executionPlan.error_log_path,
1661
+ args.executionPlan.synthesis_output_path,
1662
+ args.executionPlan.deliberation_output_path,
1663
+ args.executionPlan.final_output_path,
1664
+ args.executionPlan.review_record_path,
1665
+ ];
847
1666
  }
848
- async function collectArtifactRefs(sessionRoot) {
849
- const candidates = {
850
- session_metadata: path.join(sessionRoot, "session-metadata.yaml"),
851
- interpretation: path.join(sessionRoot, "interpretation.yaml"),
852
- binding: path.join(sessionRoot, "binding.yaml"),
853
- execution_plan: path.join(sessionRoot, "execution-plan.yaml"),
854
- execution_result: path.join(sessionRoot, "execution-result.yaml"),
855
- actor_invocation_profiles: path.join(sessionRoot, "execution-preparation", "actor-invocation-profiles.yaml"),
856
- actor_consumer_bindings: path.join(sessionRoot, "execution-preparation", "actor-consumer-bindings.yaml"),
857
- domain_binding: path.join(sessionRoot, "execution-preparation", "domain-binding.yaml"),
858
- review_value_alignment_criteria: path.join(sessionRoot, "execution-preparation", "review-value-alignment-criteria.yaml"),
859
- review_target_profile: path.join(sessionRoot, "execution-preparation", "review-target-profile.yaml"),
860
- review_context_manifest: path.join(sessionRoot, "execution-preparation", "review-context-manifest.yaml"),
861
- lens_completion_barrier: path.join(sessionRoot, "lens-completion-barrier.yaml"),
862
- finding_ledger: path.join(sessionRoot, "finding-ledger.yaml"),
863
- finding_relation_graph: path.join(sessionRoot, "finding-relation-graph.yaml"),
864
- issue_ledger: path.join(sessionRoot, "issue-ledger.yaml"),
865
- issue_stance_matrix: path.join(sessionRoot, "issue-stance-matrix.yaml"),
866
- deliberation_plan: path.join(sessionRoot, "deliberation-plan.yaml"),
867
- problem_framing: path.join(sessionRoot, "problem-framing.yaml"),
868
- deliberation_output: path.join(sessionRoot, "deliberation.md"),
869
- synthesis_output: path.join(sessionRoot, "synthesis.md"),
870
- review_run_manifest: path.join(sessionRoot, "review-run-manifest.yaml"),
871
- degradation_summary: path.join(sessionRoot, "degradation-summary.yaml"),
872
- error_log: path.join(sessionRoot, "error-log.md"),
873
- final_output: path.join(sessionRoot, "final-output.md"),
874
- review_record: path.join(sessionRoot, "review-record.yaml"),
875
- };
876
- const entries = [];
877
- for (const [key, filePath] of Object.entries(candidates)) {
878
- if (await fileExists(filePath))
879
- entries.push([key, filePath]);
1667
+ async function resolveContinuationRequestText(args) {
1668
+ if (typeof args.requestText === "string" && args.requestText.trim().length > 0) {
1669
+ return args.requestText;
880
1670
  }
881
- return Object.fromEntries(entries);
1671
+ const reviewRecord = await readOptionalReviewRecord(path.join(args.sessionRoot, "review-record.yaml"));
1672
+ if (reviewRecord?.request_text)
1673
+ return reviewRecord.request_text;
1674
+ const interpretation = await readOptionalYaml(path.join(args.sessionRoot, "interpretation.yaml"));
1675
+ if (interpretation?.intent_summary)
1676
+ return interpretation.intent_summary;
1677
+ const targetProfile = await readOptionalYaml(path.join(args.sessionRoot, "execution-preparation", "review-target-profile.yaml"));
1678
+ if (targetProfile?.review_intent_summary) {
1679
+ return targetProfile.review_intent_summary;
1680
+ }
1681
+ const metadata = await readOptionalYaml(path.join(args.sessionRoot, "session-metadata.yaml"));
1682
+ return metadata?.requested_target
1683
+ ? `Continue review for ${metadata.requested_target}`
1684
+ : "Continue review";
882
1685
  }
883
1686
  async function directoryHasMarkdownFiles(directoryPath) {
884
1687
  try {
@@ -934,7 +1737,7 @@ async function listDomainDirs(root) {
934
1737
  try {
935
1738
  const entries = await fs.readdir(root, { withFileTypes: true });
936
1739
  return entries
937
- .filter((entry) => entry.isDirectory())
1740
+ .filter((entry) => entry.isDirectory() && !isDeprecatedDomainAlias(entry.name))
938
1741
  .map((entry) => entry.name)
939
1742
  .sort();
940
1743
  }
@@ -944,10 +1747,10 @@ async function listDomainDirs(root) {
944
1747
  }
945
1748
  export function createOntoReviewCoreApi(options = {}) {
946
1749
  const ontoHome = resolveRequiredOntoHome(options.ontoHome);
947
- return {
1750
+ const api = {
948
1751
  async prepareReview(request) {
949
- const argv = appendCommonReviewArgs([], request, ontoHome);
950
- const { result } = await withCapturedConsole(() => reviewPrepareOnly(argv));
1752
+ await validateRequestedDomainForDispatch(request, ontoHome);
1753
+ const result = await prepareReviewInvocationRequest(request, { ontoHome });
951
1754
  const sessionRoot = path.resolve(result.session_root);
952
1755
  const executionPlan = await readYamlDocument(path.join(sessionRoot, "execution-plan.yaml"));
953
1756
  const openingBriefInput = await buildPreparedOpeningBriefInput(sessionRoot, executionPlan);
@@ -962,9 +1765,17 @@ export function createOntoReviewCoreApi(options = {}) {
962
1765
  };
963
1766
  },
964
1767
  async runReview(request) {
965
- const argv = appendCommonReviewArgs(["--no-watch"], request, ontoHome);
1768
+ await validateRequestedDomainForDispatch(request, ontoHome);
1769
+ const requestHash = requestHashForReviewInput(request);
1770
+ const invocationId = `initial-${continuationAttemptId()}`;
966
1771
  let progressSequence = 0;
967
1772
  let observedSessionRoot = null;
1773
+ let sessionRootResolved = false;
1774
+ let activeAttemptWrite = null;
1775
+ let resolveSessionRoot = () => { };
1776
+ const sessionRootSeen = new Promise((resolve) => {
1777
+ resolveSessionRoot = resolve;
1778
+ });
968
1779
  const emitProgress = (event) => {
969
1780
  const observer = request.progressObserver;
970
1781
  if (!observer)
@@ -983,133 +1794,541 @@ export function createOntoReviewCoreApi(options = {}) {
983
1794
  // Progress notifications are transport-only and must not affect review execution.
984
1795
  }
985
1796
  };
986
- const captureObserver = request.progressObserver
987
- ? {
988
- stdout: (text) => {
989
- for (const line of text.split(/\r?\n/)) {
990
- const parsed = consoleLineProgressEvent({
991
- line,
992
- projectRoot: request.projectRoot,
993
- sessionRoot: observedSessionRoot,
994
- sequence: progressSequence + 1,
995
- });
996
- if (!parsed)
997
- continue;
998
- observedSessionRoot = parsed.sessionRoot ?? observedSessionRoot;
999
- try {
1000
- request.progressObserver?.(parsed.event);
1001
- }
1002
- catch {
1003
- // Progress notifications are transport-only and must not affect review execution.
1004
- }
1005
- progressSequence = parsed.event.sequence;
1006
- }
1797
+ const noteSessionRoot = (sessionRoot) => {
1798
+ const resolved = path.resolve(sessionRoot);
1799
+ observedSessionRoot = resolved;
1800
+ if (sessionRootResolved)
1801
+ return;
1802
+ sessionRootResolved = true;
1803
+ resolveSessionRoot(resolved);
1804
+ activeAttemptWrite = (async () => {
1805
+ const executionPlan = await readOptionalYaml(path.join(resolved, "execution-plan.yaml"));
1806
+ await writeActiveAttemptStarted({
1807
+ sessionRoot: resolved,
1808
+ attemptId: invocationId,
1809
+ attemptKind: "initial_review",
1810
+ sourceTool: "onto.review",
1811
+ requestHash,
1812
+ activeUnits: activeUnitsForInitialReview(executionPlan),
1813
+ });
1814
+ })().catch(() => {
1815
+ // Active-attempt metadata is an operational projection; review
1816
+ // execution remains artifact-truthful even if this write fails.
1817
+ });
1818
+ };
1819
+ const runnerProgressObserver = (event) => {
1820
+ if (event.sessionRoot)
1821
+ noteSessionRoot(event.sessionRoot);
1822
+ const stage = event.phase === "prepare"
1823
+ ? "session_planned"
1824
+ : event.phase === "execute"
1825
+ ? "runtime_step"
1826
+ : event.phase === "project"
1827
+ ? "completed"
1828
+ : "invoke_step";
1829
+ const current = event.phase === "resolve"
1830
+ ? 5
1831
+ : event.phase === "prepare"
1832
+ ? 20
1833
+ : event.phase === "execute"
1834
+ ? event.status === "completed" ? 80 : 40
1835
+ : event.phase === "complete"
1836
+ ? event.status === "completed" ? 95 : 85
1837
+ : 100;
1838
+ emitProgress({
1839
+ source: "artifact_status",
1840
+ stage,
1841
+ session_root: event.sessionRoot ?? observedSessionRoot,
1842
+ message: event.message,
1843
+ progress: {
1844
+ current,
1845
+ total: 100,
1846
+ label: event.phase,
1007
1847
  },
1848
+ });
1849
+ };
1850
+ const fullRun = (async () => {
1851
+ try {
1852
+ const invocation = await runReviewInvocation(request, {
1853
+ ontoHome,
1854
+ noWatch: true,
1855
+ progressObserver: runnerProgressObserver,
1856
+ });
1857
+ const parsed = invocation.output;
1858
+ const result = parsed.review_result;
1859
+ const status = result.record_status ?? "halted_partial";
1860
+ const startPreview = {
1861
+ entrypointPlan: parsed.entrypoint_plan,
1862
+ routeSummary: parsed.route_summary,
1863
+ ...(parsed.bounded_invoke_steps !== undefined
1864
+ ? { boundedInvokeSteps: parsed.bounded_invoke_steps }
1865
+ : {}),
1866
+ };
1867
+ const resolvedResultSessionRoot = path.resolve(result.session_root);
1868
+ noteSessionRoot(resolvedResultSessionRoot);
1869
+ await activeAttemptWrite;
1870
+ await writeEnvironmentWarningsFromStderr({
1871
+ sessionRoot: resolvedResultSessionRoot,
1872
+ stderr: invocation.stderr,
1873
+ });
1874
+ await updateActiveAttemptTerminal({
1875
+ sessionRoot: resolvedResultSessionRoot,
1876
+ status: status === "halted_partial" ? "halted_partial" : "completed",
1877
+ });
1878
+ const artifactRefs = await collectArtifactRefs(resolvedResultSessionRoot);
1879
+ const failures = await collectStructuredFailures(resolvedResultSessionRoot);
1880
+ const executionPlan = await readOptionalYaml(path.join(resolvedResultSessionRoot, "execution-plan.yaml"));
1881
+ const executionResult = await readOptionalYaml(path.join(resolvedResultSessionRoot, "execution-result.yaml"));
1882
+ const pipelineExecutionLedger = await buildPipelineExecutionLedgerIfPossible({
1883
+ sessionRoot: resolvedResultSessionRoot,
1884
+ artifactRefs,
1885
+ executionPlan,
1886
+ executionResult,
1887
+ });
1888
+ const reviewRecord = await readOptionalReviewRecord(path.join(resolvedResultSessionRoot, "review-record.yaml"));
1889
+ const resultClassificationSummary = await readReviewResultClassification(resolvedResultSessionRoot);
1890
+ const progressInput = await buildReviewStatusPresentationInput({
1891
+ sessionRoot: resolvedResultSessionRoot,
1892
+ status,
1893
+ artifactRefs,
1894
+ executionPlan,
1895
+ executionResult,
1896
+ reviewRecord,
1897
+ });
1898
+ const openingBriefInput = executionPlan
1899
+ ? await buildPreparedOpeningBriefInput(resolvedResultSessionRoot, executionPlan)
1900
+ : {
1901
+ presentation_contract_version: REVIEW_PRESENTATION_CONTRACT_VERSION,
1902
+ presentation_kind: "opening_brief",
1903
+ session_id: basenameSessionId(resolvedResultSessionRoot),
1904
+ session_root: resolvedResultSessionRoot,
1905
+ status,
1906
+ generated_from_artifact_refs: generatedFromArtifactRefs(artifactRefs),
1907
+ start_preview: startPreview,
1908
+ };
1909
+ const finalResultInput = {
1910
+ presentation_contract_version: REVIEW_PRESENTATION_CONTRACT_VERSION,
1911
+ presentation_kind: "final_result",
1912
+ session_id: basenameSessionId(resolvedResultSessionRoot),
1913
+ session_root: resolvedResultSessionRoot,
1914
+ status,
1915
+ generated_from_artifact_refs: generatedFromArtifactRefs(artifactRefs),
1916
+ result_overview: parsed.result_overview ?? null,
1917
+ result_classification_summary: resultClassificationSummary,
1918
+ review_result: result,
1919
+ };
1920
+ const llmPresentation = {
1921
+ openingBrief: buildOpeningBriefPresentation(openingBriefInput),
1922
+ progress: buildProgressPresentation(progressInput),
1923
+ ...(progressInput.halt
1924
+ ? {
1925
+ halt: buildHaltPresentation({
1926
+ ...progressInput,
1927
+ presentation_kind: "halt",
1928
+ }),
1929
+ }
1930
+ : {}),
1931
+ finalResult: buildFinalResultPresentation(finalResultInput),
1932
+ };
1933
+ emitProgress({
1934
+ source: "artifact_status",
1935
+ stage: "final_status",
1936
+ session_root: resolvedResultSessionRoot,
1937
+ message: `Review finished with status ${status}.`,
1938
+ progress: {
1939
+ current: 100,
1940
+ total: 100,
1941
+ label: "final status",
1942
+ },
1943
+ });
1944
+ return {
1945
+ sessionId: basenameSessionId(result.session_root),
1946
+ sessionRoot: resolvedResultSessionRoot,
1947
+ status,
1948
+ finalOutputPath: result.final_output_path,
1949
+ reviewRecordPath: result.review_record_path,
1950
+ executionResultPath: result.execution_result_path,
1951
+ reviewRunManifestPath: result.review_run_manifest_path ??
1952
+ path.join(resolvedResultSessionRoot, "review-run-manifest.yaml"),
1953
+ deliberationStatus: result.deliberation_status ?? null,
1954
+ participatingLensIds: result.participating_lens_ids ?? [],
1955
+ degradedLensIds: result.degraded_lens_ids ?? [],
1956
+ ...(result.summary !== undefined ? { summary: result.summary } : {}),
1957
+ ...(parsed.result_overview !== undefined
1958
+ ? { resultOverview: parsed.result_overview }
1959
+ : {}),
1960
+ artifactRefs,
1961
+ ...(pipelineExecutionLedger ? { pipelineExecutionLedger } : {}),
1962
+ resultClassificationSummary,
1963
+ ...failures,
1964
+ routeVisibility: await buildReviewRouteVisibilityFromSession(resolvedResultSessionRoot),
1965
+ startPreview,
1966
+ llmPresentation,
1967
+ runHandle: await buildReviewRunHandle({
1968
+ sessionRoot: resolvedResultSessionRoot,
1969
+ status,
1970
+ invocationId,
1971
+ }),
1972
+ runControl: progressInput.run_control,
1973
+ targetMaterialSupport: progressInput.target_material_support,
1974
+ environmentWarnings: progressInput.environment_warnings,
1975
+ };
1008
1976
  }
1009
- : undefined;
1010
- const captured = await withCapturedConsole(async () => {
1011
- const exitCode = await runReviewInvokeCli(argv);
1012
- if (exitCode !== 0) {
1013
- throw new Error(`review invocation failed with exit code ${exitCode}`);
1977
+ catch (error) {
1978
+ if (observedSessionRoot) {
1979
+ await updateActiveAttemptTerminal({
1980
+ sessionRoot: observedSessionRoot,
1981
+ status: "failed",
1982
+ errorMessage: error instanceof Error ? error.message : String(error),
1983
+ });
1984
+ }
1985
+ throw error;
1014
1986
  }
1015
- return exitCode;
1016
- }, captureObserver);
1017
- const parsed = parseReviewInvokeOutput(captured.stdout);
1018
- if (!isReviewInvokeShape(parsed)) {
1019
- throw new Error("review invocation returned an unexpected result shape.");
1987
+ })();
1988
+ if (request.returnRunningAfterMs !== undefined) {
1989
+ const waitMs = Math.max(0, request.returnRunningAfterMs);
1990
+ const earlyRunning = (async () => {
1991
+ const sessionRoot = await sessionRootSeen;
1992
+ if (activeAttemptWrite)
1993
+ await activeAttemptWrite;
1994
+ if (waitMs > 0) {
1995
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
1996
+ }
1997
+ return buildRunningReviewRunResult({
1998
+ sessionRoot,
1999
+ invocationId,
2000
+ });
2001
+ })();
2002
+ const winner = await Promise.race([
2003
+ fullRun.then((result) => ({ kind: "completed", result })),
2004
+ earlyRunning.then((result) => ({ kind: "running", result })),
2005
+ ]);
2006
+ if (winner.kind === "running") {
2007
+ fullRun.catch(() => {
2008
+ // The session artifacts and active-attempt projection record the failure.
2009
+ });
2010
+ }
2011
+ return winner.result;
1020
2012
  }
1021
- const result = parsed.review_result;
1022
- const status = result.record_status ?? "halted_partial";
1023
- const startPreview = {
1024
- entrypointPlan: parsed.entrypoint_plan,
1025
- routeSummary: parsed.route_summary,
1026
- ...(parsed.bounded_invoke_steps !== undefined
1027
- ? { boundedInvokeSteps: parsed.bounded_invoke_steps }
1028
- : {}),
1029
- };
1030
- const resolvedResultSessionRoot = path.resolve(result.session_root);
1031
- const artifactRefs = await collectArtifactRefs(resolvedResultSessionRoot);
1032
- const failures = await collectStructuredFailures(resolvedResultSessionRoot);
1033
- const executionPlan = await readOptionalYaml(path.join(resolvedResultSessionRoot, "execution-plan.yaml"));
1034
- const executionResult = await readOptionalYaml(path.join(resolvedResultSessionRoot, "execution-result.yaml"));
1035
- const reviewRecord = await readOptionalReviewRecord(path.join(resolvedResultSessionRoot, "review-record.yaml"));
1036
- const resultClassificationSummary = await readReviewResultClassification(resolvedResultSessionRoot);
1037
- const progressInput = await buildReviewStatusPresentationInput({
1038
- sessionRoot: resolvedResultSessionRoot,
1039
- status,
2013
+ return fullRun;
2014
+ },
2015
+ async continueReview(request) {
2016
+ const resolvedSessionRoot = path.resolve(request.sessionRoot);
2017
+ const sessionMetadataPath = path.join(resolvedSessionRoot, "session-metadata.yaml");
2018
+ const sessionMetadata = await readOptionalYaml(sessionMetadataPath);
2019
+ if (!sessionMetadata) {
2020
+ throw new Error(`Cannot continue review without session-metadata.yaml: ${resolvedSessionRoot}`);
2021
+ }
2022
+ const projectRoot = path.resolve(request.projectRoot ?? sessionMetadata.project_root);
2023
+ await assertSamePath({
2024
+ label: "ReviewSessionMetadata.project_root",
2025
+ expected: projectRoot,
2026
+ actual: sessionMetadata.project_root,
2027
+ });
2028
+ const artifactRefs = await collectArtifactRefs(resolvedSessionRoot);
2029
+ const executionPlan = await readOptionalYaml(path.join(resolvedSessionRoot, "execution-plan.yaml"));
2030
+ if (!executionPlan) {
2031
+ throw new Error(`Cannot continue review without execution-plan.yaml: ${resolvedSessionRoot}`);
2032
+ }
2033
+ if (executionPlan.session_id !== sessionMetadata.session_id) {
2034
+ throw new Error(`Review continuation session id mismatch: metadata=${sessionMetadata.session_id}, executionPlan=${executionPlan.session_id}`);
2035
+ }
2036
+ await assertSamePath({
2037
+ label: "ReviewExecutionPlan.session_metadata_path",
2038
+ expected: sessionMetadataPath,
2039
+ actual: executionPlan.session_metadata_path,
2040
+ });
2041
+ await assertReviewExecutionPlanSessionBoundary({
2042
+ sessionRoot: resolvedSessionRoot,
2043
+ executionPlan,
2044
+ });
2045
+ const activeRunControl = await buildRunControl(resolvedSessionRoot, "running");
2046
+ if (activeRunControl.alreadyRunning &&
2047
+ requestedUnitsMatchActive(activeRunControl.activeAttempt?.activeUnits ?? [], request.targetUnits)) {
2048
+ const status = await api.getReviewStatus(resolvedSessionRoot);
2049
+ return {
2050
+ sessionId: status.sessionId,
2051
+ sessionRoot: resolvedSessionRoot,
2052
+ decision: "already_running",
2053
+ status: "running",
2054
+ artifactRefs: status.artifactRefs,
2055
+ failureRefs: status.failureRefs,
2056
+ ...(status.pipelineExecutionLedger
2057
+ ? { pipelineExecutionLedger: status.pipelineExecutionLedger }
2058
+ : {}),
2059
+ resultClassificationSummary: await readReviewResultClassification(resolvedSessionRoot),
2060
+ ...(status.routeVisibility !== undefined
2061
+ ? { routeVisibility: status.routeVisibility }
2062
+ : {}),
2063
+ ...(status.llmPresentation !== undefined
2064
+ ? { llmPresentation: status.llmPresentation }
2065
+ : {}),
2066
+ ...(activeRunControl.activeAttempt
2067
+ ? { activeAttempt: activeRunControl.activeAttempt }
2068
+ : {}),
2069
+ };
2070
+ }
2071
+ const executionResult = await readOptionalYaml(path.join(resolvedSessionRoot, "execution-result.yaml"));
2072
+ const pipelineExecutionLedger = await buildPipelineExecutionLedgerIfPossible({
2073
+ sessionRoot: resolvedSessionRoot,
1040
2074
  artifactRefs,
1041
2075
  executionPlan,
1042
2076
  executionResult,
1043
- reviewRecord,
1044
2077
  });
1045
- const openingBriefInput = executionPlan
1046
- ? await buildPreparedOpeningBriefInput(resolvedResultSessionRoot, executionPlan)
1047
- : {
1048
- presentation_contract_version: REVIEW_PRESENTATION_CONTRACT_VERSION,
1049
- presentation_kind: "opening_brief",
1050
- session_id: basenameSessionId(resolvedResultSessionRoot),
1051
- session_root: resolvedResultSessionRoot,
2078
+ if (!pipelineExecutionLedger) {
2079
+ throw new Error(`Cannot continue review without a PipelineExecutionLedger: ${resolvedSessionRoot}`);
2080
+ }
2081
+ const continuationPlan = buildReviewContinuationPlan({
2082
+ ledger: pipelineExecutionLedger,
2083
+ ...(request.targetUnits !== undefined
2084
+ ? { targetUnits: request.targetUnits }
2085
+ : {}),
2086
+ });
2087
+ if (!continuationPlan.eligible) {
2088
+ throw new Error(`Review continuation is not eligible: ${continuationPlan.ineligibleReason ?? "unknown reason"}`);
2089
+ }
2090
+ const reviewRunManifest = await readOptionalYaml(path.join(resolvedSessionRoot, "review-run-manifest.yaml"));
2091
+ const executorRealization = request.executorRealization ??
2092
+ executorRealizationFromManifest(reviewRunManifest);
2093
+ if (!executorRealization) {
2094
+ throw new Error("Review continuation requires executorRealization when the prior review-run-manifest does not expose a worker executor.");
2095
+ }
2096
+ const actorProfiles = await readOptionalYaml(executionPlan.actor_invocation_profiles_path ??
2097
+ path.join(resolvedSessionRoot, "execution-preparation", "actor-invocation-profiles.yaml"));
2098
+ const manifestReviewExecutionProfile = request.executorRealization === undefined
2099
+ ? reviewExecutionProfileFromManifest(reviewRunManifest)
2100
+ : undefined;
2101
+ const actorProfileReviewExecutionProfile = manifestReviewExecutionProfile
2102
+ ? undefined
2103
+ : reviewExecutionProfileFromActorProfiles({
2104
+ actorProfiles,
2105
+ executorRealization,
2106
+ });
2107
+ const reviewExecutionProfile = manifestReviewExecutionProfile ?? actorProfileReviewExecutionProfile;
2108
+ const reviewExecutionProfileSource = manifestReviewExecutionProfile
2109
+ ? "review-run-manifest"
2110
+ : actorProfileReviewExecutionProfile
2111
+ ? "actor-invocation-profiles"
2112
+ : "none";
2113
+ const attemptId = continuationAttemptId();
2114
+ const attemptRoot = path.join(resolvedSessionRoot, "continuation-attempts", attemptId);
2115
+ const continuationPlanPath = path.join(attemptRoot, "continuation-plan.yaml");
2116
+ const attemptManifestPath = path.join(attemptRoot, "continuation-attempt.yaml");
2117
+ await writeYamlDocument(continuationPlanPath, continuationPlan);
2118
+ const supersededArtifactBackups = await copySupersededArtifacts({
2119
+ attemptRoot,
2120
+ artifactRefs: [
2121
+ ...new Set([
2122
+ ...continuationPlan.supersededArtifactRefs,
2123
+ ...continuationSessionArtifactRefs({
2124
+ sessionRoot: resolvedSessionRoot,
2125
+ executionPlan,
2126
+ }),
2127
+ ]),
2128
+ ],
2129
+ });
2130
+ const attemptStartedAt = isoNow();
2131
+ const writeAttemptManifest = async (status, extra = {}) => {
2132
+ await writeYamlDocument(attemptManifestPath, {
2133
+ schema_version: "1",
2134
+ attempt_id: attemptId,
2135
+ session_id: executionPlan.session_id,
2136
+ session_root: resolvedSessionRoot,
2137
+ created_at: attemptStartedAt,
2138
+ updated_at: isoNow(),
1052
2139
  status,
1053
- generated_from_artifact_refs: generatedFromArtifactRefs(artifactRefs),
1054
- start_preview: startPreview,
1055
- };
1056
- const finalResultInput = {
1057
- presentation_contract_version: REVIEW_PRESENTATION_CONTRACT_VERSION,
1058
- presentation_kind: "final_result",
1059
- session_id: basenameSessionId(resolvedResultSessionRoot),
1060
- session_root: resolvedResultSessionRoot,
1061
- status,
1062
- generated_from_artifact_refs: generatedFromArtifactRefs(artifactRefs),
1063
- result_overview: parsed.result_overview ?? null,
1064
- result_classification_summary: resultClassificationSummary,
1065
- review_result: result,
2140
+ executor_realization: executorRealization,
2141
+ target_units: request.targetUnits ?? [],
2142
+ continuation_plan_ref: continuationPlanPath,
2143
+ superseded_artifact_backups: supersededArtifactBackups,
2144
+ execution_route_provenance: {
2145
+ executor_realization: executorRealization,
2146
+ review_execution_profile_source: reviewExecutionProfileSource,
2147
+ requested_executor_realization: request.executorRealization ?? null,
2148
+ previous_execution_realization: executionPlan.execution_realization,
2149
+ previous_host_runtime: executionPlan.host_runtime,
2150
+ },
2151
+ ...extra,
2152
+ });
1066
2153
  };
1067
- const llmPresentation = {
1068
- openingBrief: buildOpeningBriefPresentation(openingBriefInput),
1069
- progress: buildProgressPresentation(progressInput),
1070
- ...(progressInput.halt
1071
- ? {
1072
- halt: buildHaltPresentation({
1073
- ...progressInput,
1074
- presentation_kind: "halt",
1075
- }),
1076
- }
2154
+ await writeAttemptManifest("started");
2155
+ await writeActiveAttemptStarted({
2156
+ sessionRoot: resolvedSessionRoot,
2157
+ attemptId,
2158
+ attemptKind: "continuation",
2159
+ sourceTool: "onto.review_continue",
2160
+ requestHash: null,
2161
+ activeUnits: continuationPlan.frontierUnits.map((unit) => unit.unitId),
2162
+ requestedFrontierUnits: request.targetUnits ?? [],
2163
+ });
2164
+ let promptExecutionResult;
2165
+ try {
2166
+ const executorConfig = buildExecutorConfigFromRealization(executorRealization, ontoHome);
2167
+ promptExecutionResult = (await withCapturedConsole(() => executeReviewPromptExecution({
2168
+ projectRoot,
2169
+ sessionRoot: resolvedSessionRoot,
2170
+ defaultExecutorConfig: executorConfig,
2171
+ ...(reviewExecutionProfile ? { reviewExecutionProfile } : {}),
2172
+ continuationPlan,
2173
+ }))).result;
2174
+ if (promptExecutionResult.synthesis_executed) {
2175
+ const requestText = await resolveContinuationRequestText({
2176
+ sessionRoot: resolvedSessionRoot,
2177
+ ...(request.requestText ? { requestText: request.requestText } : {}),
2178
+ });
2179
+ await withCapturedConsole(() => completeReviewSession([
2180
+ "--project-root",
2181
+ projectRoot,
2182
+ "--session-root",
2183
+ resolvedSessionRoot,
2184
+ "--request-text",
2185
+ requestText,
2186
+ ]));
2187
+ }
2188
+ await writeAttemptManifest(promptExecutionResult.synthesis_executed
2189
+ ? "completed"
2190
+ : "halted_partial", { prompt_execution_result: promptExecutionResult });
2191
+ await updateActiveAttemptTerminal({
2192
+ sessionRoot: resolvedSessionRoot,
2193
+ status: promptExecutionResult.synthesis_executed
2194
+ ? "completed"
2195
+ : "halted_partial",
2196
+ });
2197
+ }
2198
+ catch (error) {
2199
+ const restoredArtifactBackups = await restoreSupersededArtifacts(supersededArtifactBackups);
2200
+ const errorMessage = error instanceof Error ? error.message : String(error);
2201
+ await writeAttemptManifest("failed", {
2202
+ error_message: errorMessage,
2203
+ ...(promptExecutionResult
2204
+ ? { prompt_execution_result: promptExecutionResult }
2205
+ : {}),
2206
+ restored_artifact_backups: restoredArtifactBackups,
2207
+ });
2208
+ await updateActiveAttemptTerminal({
2209
+ sessionRoot: resolvedSessionRoot,
2210
+ status: "failed",
2211
+ errorMessage,
2212
+ });
2213
+ throw new ReviewContinuationError({
2214
+ message: `Review continuation failed: ${errorMessage}`,
2215
+ originalError: error,
2216
+ failureContent: {
2217
+ mcp_error_code: "ONTO_REVIEW_CONTINUATION_FAILED",
2218
+ session_id: executionPlan.session_id,
2219
+ session_root: resolvedSessionRoot,
2220
+ attempt_id: attemptId,
2221
+ attempt_root: attemptRoot,
2222
+ attempt_manifest_ref: attemptManifestPath,
2223
+ continuation_plan_ref: continuationPlanPath,
2224
+ continuation_plan: continuationPlan,
2225
+ superseded_artifact_backups: supersededArtifactBackups,
2226
+ restored_artifact_backups: restoredArtifactBackups,
2227
+ error_message: errorMessage,
2228
+ },
2229
+ });
2230
+ }
2231
+ if (!promptExecutionResult) {
2232
+ throw new Error("Review continuation finished without prompt execution result.");
2233
+ }
2234
+ const postStatus = await api.getReviewStatus(resolvedSessionRoot);
2235
+ return {
2236
+ sessionId: postStatus.sessionId,
2237
+ sessionRoot: resolvedSessionRoot,
2238
+ decision: "executed",
2239
+ status: postStatus.status,
2240
+ continuationPlan,
2241
+ continuationAttempt: {
2242
+ attemptId,
2243
+ attemptRoot,
2244
+ continuationPlanPath,
2245
+ attemptManifestPath,
2246
+ supersededArtifactBackups,
2247
+ },
2248
+ promptExecutionResult,
2249
+ artifactRefs: postStatus.artifactRefs,
2250
+ ...(postStatus.pipelineExecutionLedger
2251
+ ? { pipelineExecutionLedger: postStatus.pipelineExecutionLedger }
2252
+ : {}),
2253
+ resultClassificationSummary: await readReviewResultClassification(resolvedSessionRoot),
2254
+ failureRefs: postStatus.failureRefs,
2255
+ ...(postStatus.routeVisibility !== undefined
2256
+ ? { routeVisibility: postStatus.routeVisibility }
2257
+ : {}),
2258
+ ...(postStatus.llmPresentation !== undefined
2259
+ ? { llmPresentation: postStatus.llmPresentation }
1077
2260
  : {}),
1078
- finalResult: buildFinalResultPresentation(finalResultInput),
1079
2261
  };
1080
- emitProgress({
1081
- source: "artifact_status",
1082
- stage: "final_status",
1083
- session_root: resolvedResultSessionRoot,
1084
- message: `Review finished with status ${status}.`,
1085
- progress: {
1086
- current: 100,
1087
- total: 100,
1088
- label: "final status",
1089
- },
2262
+ },
2263
+ async cancelReview(request) {
2264
+ const resolvedSessionRoot = path.resolve(request.sessionRoot);
2265
+ const sessionMetadata = await readOptionalYaml(path.join(resolvedSessionRoot, "session-metadata.yaml"));
2266
+ if (!sessionMetadata) {
2267
+ throw new Error(`Cannot cancel review without session-metadata.yaml: ${resolvedSessionRoot}`);
2268
+ }
2269
+ const projectRoot = path.resolve(request.projectRoot ?? sessionMetadata.project_root);
2270
+ await assertSamePath({
2271
+ label: "ReviewSessionMetadata.project_root",
2272
+ expected: projectRoot,
2273
+ actual: sessionMetadata.project_root,
1090
2274
  });
2275
+ const statusBeforeCancel = await api.getReviewStatus(resolvedSessionRoot);
2276
+ if (reviewTerminalStatus(statusBeforeCancel.status)) {
2277
+ return {
2278
+ sessionId: statusBeforeCancel.sessionId,
2279
+ sessionRoot: resolvedSessionRoot,
2280
+ decision: "already_terminal",
2281
+ status: statusBeforeCancel.status,
2282
+ cancelRequestPath: reviewCancelRequestPath(resolvedSessionRoot),
2283
+ reason: "review is already terminal",
2284
+ artifactRefs: statusBeforeCancel.artifactRefs,
2285
+ ...(statusBeforeCancel.runControl
2286
+ ? { runControl: statusBeforeCancel.runControl }
2287
+ : {}),
2288
+ ...(statusBeforeCancel.llmPresentation
2289
+ ? { llmPresentation: statusBeforeCancel.llmPresentation }
2290
+ : {}),
2291
+ };
2292
+ }
2293
+ if (!statusBeforeCancel.runControl?.cancellationAvailable) {
2294
+ return {
2295
+ sessionId: statusBeforeCancel.sessionId,
2296
+ sessionRoot: resolvedSessionRoot,
2297
+ decision: "not_cancellable",
2298
+ status: statusBeforeCancel.status,
2299
+ cancelRequestPath: reviewCancelRequestPath(resolvedSessionRoot),
2300
+ reason: statusBeforeCancel.runControl?.statusReason ??
2301
+ "review is not currently cancellable",
2302
+ artifactRefs: statusBeforeCancel.artifactRefs,
2303
+ ...(statusBeforeCancel.runControl
2304
+ ? { runControl: statusBeforeCancel.runControl }
2305
+ : {}),
2306
+ ...(statusBeforeCancel.llmPresentation
2307
+ ? { llmPresentation: statusBeforeCancel.llmPresentation }
2308
+ : {}),
2309
+ };
2310
+ }
2311
+ const reason = request.reason?.trim() || "operator requested cancellation";
2312
+ const cancelRequest = {
2313
+ schema_version: "1",
2314
+ session_id: sessionMetadata.session_id,
2315
+ requested_at: isoNow(),
2316
+ requested_by: "mcp",
2317
+ reason,
2318
+ };
2319
+ const cancelRequestPath = reviewCancelRequestPath(resolvedSessionRoot);
2320
+ await writeYamlDocument(cancelRequestPath, cancelRequest);
2321
+ const status = await api.getReviewStatus(resolvedSessionRoot);
1091
2322
  return {
1092
- sessionId: basenameSessionId(result.session_root),
1093
- sessionRoot: resolvedResultSessionRoot,
1094
- status,
1095
- finalOutputPath: result.final_output_path,
1096
- reviewRecordPath: result.review_record_path,
1097
- executionResultPath: result.execution_result_path,
1098
- reviewRunManifestPath: result.review_run_manifest_path ??
1099
- path.join(resolvedResultSessionRoot, "review-run-manifest.yaml"),
1100
- deliberationStatus: result.deliberation_status ?? null,
1101
- participatingLensIds: result.participating_lens_ids ?? [],
1102
- degradedLensIds: result.degraded_lens_ids ?? [],
1103
- ...(result.summary !== undefined ? { summary: result.summary } : {}),
1104
- ...(parsed.result_overview !== undefined
1105
- ? { resultOverview: parsed.result_overview }
1106
- : {}),
1107
- artifactRefs,
1108
- resultClassificationSummary,
1109
- ...failures,
1110
- routeVisibility: await buildReviewRouteVisibilityFromSession(resolvedResultSessionRoot),
1111
- startPreview,
1112
- llmPresentation,
2323
+ sessionId: status.sessionId,
2324
+ sessionRoot: resolvedSessionRoot,
2325
+ decision: "requested",
2326
+ status: status.status,
2327
+ cancelRequestPath,
2328
+ reason,
2329
+ artifactRefs: status.artifactRefs,
2330
+ ...(status.runControl ? { runControl: status.runControl } : {}),
2331
+ ...(status.llmPresentation ? { llmPresentation: status.llmPresentation } : {}),
1113
2332
  };
1114
2333
  },
1115
2334
  async getReviewStatus(sessionRoot) {
@@ -1118,16 +2337,32 @@ export function createOntoReviewCoreApi(options = {}) {
1118
2337
  const failures = await collectStructuredFailures(resolvedSessionRoot);
1119
2338
  const executionPlan = await readOptionalYaml(path.join(resolvedSessionRoot, "execution-plan.yaml"));
1120
2339
  const executionResult = await readOptionalYaml(path.join(resolvedSessionRoot, "execution-result.yaml"));
2340
+ const pipelineExecutionLedger = await buildPipelineExecutionLedgerIfPossible({
2341
+ sessionRoot: resolvedSessionRoot,
2342
+ artifactRefs,
2343
+ executionPlan,
2344
+ executionResult,
2345
+ });
2346
+ const continuationPlan = pipelineExecutionLedger
2347
+ ? buildReviewContinuationPlan({ ledger: pipelineExecutionLedger })
2348
+ : undefined;
1121
2349
  const reviewRecord = await readOptionalReviewRecord(path.join(resolvedSessionRoot, "review-record.yaml"));
2350
+ const activeAttempt = await activeAttemptProjection(resolvedSessionRoot);
2351
+ const activeRunInProgress = activeAttempt?.status === "started" && !activeAttempt.isStale;
1122
2352
  const status = reviewRecord
1123
2353
  ? reviewRecord.record_status
1124
2354
  : executionResult?.execution_status === "halted_partial"
1125
2355
  ? "halted_partial"
1126
- : executionPlan
1127
- ? await hasRunArtifacts(resolvedSessionRoot, artifactRefs)
1128
- ? "running"
1129
- : "prepared"
1130
- : "unknown";
2356
+ : activeAttempt?.status === "failed"
2357
+ ? "failed"
2358
+ : activeAttempt?.status === "halted_partial"
2359
+ ? "halted_partial"
2360
+ : executionPlan
2361
+ ? (activeRunInProgress ||
2362
+ await hasRunArtifacts(resolvedSessionRoot, artifactRefs))
2363
+ ? "running"
2364
+ : "prepared"
2365
+ : "unknown";
1131
2366
  const progressInput = await buildReviewStatusPresentationInput({
1132
2367
  sessionRoot: resolvedSessionRoot,
1133
2368
  status,
@@ -1154,9 +2389,15 @@ export function createOntoReviewCoreApi(options = {}) {
1154
2389
  sessionRoot: resolvedSessionRoot,
1155
2390
  status,
1156
2391
  artifactRefs,
2392
+ ...(pipelineExecutionLedger ? { pipelineExecutionLedger } : {}),
2393
+ ...(continuationPlan ? { continuationPlan } : {}),
1157
2394
  ...failures,
1158
2395
  routeVisibility: await buildReviewRouteVisibilityFromSession(resolvedSessionRoot),
1159
2396
  llmPresentation,
2397
+ runControl: progressInput.run_control,
2398
+ targetMaterialSupport: progressInput.target_material_support,
2399
+ environmentWarnings: progressInput.environment_warnings,
2400
+ unitProgress: progressInput.progress.unit_progress,
1160
2401
  };
1161
2402
  }
1162
2403
  if (executionResult?.execution_status === "halted_partial") {
@@ -1165,9 +2406,15 @@ export function createOntoReviewCoreApi(options = {}) {
1165
2406
  sessionRoot: resolvedSessionRoot,
1166
2407
  status,
1167
2408
  artifactRefs,
2409
+ ...(pipelineExecutionLedger ? { pipelineExecutionLedger } : {}),
2410
+ ...(continuationPlan ? { continuationPlan } : {}),
1168
2411
  ...failures,
1169
2412
  routeVisibility: await buildReviewRouteVisibilityFromSession(resolvedSessionRoot),
1170
2413
  llmPresentation,
2414
+ runControl: progressInput.run_control,
2415
+ targetMaterialSupport: progressInput.target_material_support,
2416
+ environmentWarnings: progressInput.environment_warnings,
2417
+ unitProgress: progressInput.progress.unit_progress,
1171
2418
  };
1172
2419
  }
1173
2420
  if (executionPlan) {
@@ -1176,9 +2423,15 @@ export function createOntoReviewCoreApi(options = {}) {
1176
2423
  sessionRoot: resolvedSessionRoot,
1177
2424
  status,
1178
2425
  artifactRefs,
2426
+ ...(pipelineExecutionLedger ? { pipelineExecutionLedger } : {}),
2427
+ ...(continuationPlan ? { continuationPlan } : {}),
1179
2428
  ...failures,
1180
2429
  routeVisibility: await buildReviewRouteVisibilityFromSession(resolvedSessionRoot),
1181
2430
  llmPresentation,
2431
+ runControl: progressInput.run_control,
2432
+ targetMaterialSupport: progressInput.target_material_support,
2433
+ environmentWarnings: progressInput.environment_warnings,
2434
+ unitProgress: progressInput.progress.unit_progress,
1182
2435
  };
1183
2436
  }
1184
2437
  return {
@@ -1186,21 +2439,41 @@ export function createOntoReviewCoreApi(options = {}) {
1186
2439
  sessionRoot: resolvedSessionRoot,
1187
2440
  status: "unknown",
1188
2441
  artifactRefs,
2442
+ ...(pipelineExecutionLedger ? { pipelineExecutionLedger } : {}),
2443
+ ...(continuationPlan ? { continuationPlan } : {}),
1189
2444
  ...failures,
1190
2445
  routeVisibility: await buildReviewRouteVisibilityFromSession(resolvedSessionRoot),
1191
2446
  llmPresentation,
2447
+ runControl: progressInput.run_control,
2448
+ targetMaterialSupport: progressInput.target_material_support,
2449
+ environmentWarnings: progressInput.environment_warnings,
2450
+ unitProgress: progressInput.progress.unit_progress,
1192
2451
  };
1193
2452
  },
1194
- async getReviewResult(sessionRoot) {
2453
+ async getReviewResult(sessionRoot, options = {}) {
1195
2454
  const resolvedSessionRoot = path.resolve(sessionRoot);
2455
+ const projectionLevel = options.projectionLevel ?? "full";
1196
2456
  const artifactRefs = await collectArtifactRefs(resolvedSessionRoot);
1197
2457
  const { failureRefs } = await collectStructuredFailures(resolvedSessionRoot);
1198
2458
  const reviewRecordPath = path.join(resolvedSessionRoot, "review-record.yaml");
1199
2459
  const reviewRecord = await readValidatedReviewRecord(reviewRecordPath);
1200
- const finalOutputPath = reviewRecord.final_output_ref ?? path.join(resolvedSessionRoot, "final-output.md");
1201
- const finalOutputText = await readOptionalText(finalOutputPath);
2460
+ const resultSessionMetadata = await readOptionalYaml(path.join(resolvedSessionRoot, "session-metadata.yaml"));
2461
+ const finalOutputPath = await resolveReviewRecordFinalOutputPath({
2462
+ sessionRoot: resolvedSessionRoot,
2463
+ projectRoot: resultSessionMetadata?.project_root ?? null,
2464
+ finalOutputRef: reviewRecord.final_output_ref,
2465
+ });
2466
+ const finalOutputText = projectionLevel === "compact"
2467
+ ? undefined
2468
+ : await readOptionalText(finalOutputPath);
1202
2469
  const executionPlan = await readOptionalYaml(path.join(resolvedSessionRoot, "execution-plan.yaml"));
1203
2470
  const executionResult = await readOptionalYaml(path.join(resolvedSessionRoot, "execution-result.yaml"));
2471
+ const pipelineExecutionLedger = await buildPipelineExecutionLedgerIfPossible({
2472
+ sessionRoot: resolvedSessionRoot,
2473
+ artifactRefs,
2474
+ executionPlan,
2475
+ executionResult,
2476
+ });
1204
2477
  const resultClassificationSummary = await readReviewResultClassification(resolvedSessionRoot);
1205
2478
  const status = reviewRecord.record_status;
1206
2479
  const progressInput = await buildReviewStatusPresentationInput({
@@ -1219,15 +2492,36 @@ export function createOntoReviewCoreApi(options = {}) {
1219
2492
  status,
1220
2493
  generated_from_artifact_refs: generatedFromArtifactRefs(artifactRefs),
1221
2494
  result_classification_summary: resultClassificationSummary,
1222
- review_record: reviewRecord,
2495
+ review_record: projectionLevel === "full" ? reviewRecord : null,
2496
+ review_record_summary: {
2497
+ review_record_id: reviewRecord.review_record_id,
2498
+ record_status: reviewRecord.record_status,
2499
+ resolved_lens_ids: reviewRecord.resolved_lens_ids,
2500
+ participating_lens_ids: reviewRecord.participating_lens_ids,
2501
+ degraded_lens_ids: reviewRecord.degraded_lens_ids,
2502
+ deliberation_status: reviewRecord.deliberation_status,
2503
+ },
1223
2504
  };
2505
+ const targetMaterialSupport = await readTargetMaterialSupport(resolvedSessionRoot, executionPlan);
2506
+ const environmentWarnings = await readEnvironmentWarnings(resolvedSessionRoot);
1224
2507
  return {
1225
2508
  sessionId: reviewRecord.session_id,
1226
2509
  sessionRoot: resolvedSessionRoot,
1227
- reviewRecord,
2510
+ projectionLevel,
2511
+ reviewRecordSummary: {
2512
+ reviewRecordId: reviewRecord.review_record_id,
2513
+ recordStatus: reviewRecord.record_status,
2514
+ requestText: reviewRecord.request_text,
2515
+ resolvedLensIds: reviewRecord.resolved_lens_ids,
2516
+ participatingLensIds: reviewRecord.participating_lens_ids,
2517
+ degradedLensIds: reviewRecord.degraded_lens_ids,
2518
+ deliberationStatus: reviewRecord.deliberation_status,
2519
+ },
2520
+ ...(projectionLevel === "full" ? { reviewRecord } : {}),
1228
2521
  finalOutputPath,
1229
2522
  reviewRunManifestPath: path.join(resolvedSessionRoot, "review-run-manifest.yaml"),
1230
2523
  artifactRefs,
2524
+ ...(pipelineExecutionLedger ? { pipelineExecutionLedger } : {}),
1231
2525
  resultClassificationSummary,
1232
2526
  failureRefs,
1233
2527
  routeVisibility: await buildReviewRouteVisibilityFromSession(resolvedSessionRoot),
@@ -1243,9 +2537,87 @@ export function createOntoReviewCoreApi(options = {}) {
1243
2537
  : {}),
1244
2538
  finalResult: buildFinalResultPresentation(finalResultInput),
1245
2539
  },
2540
+ targetMaterialSupport,
2541
+ environmentWarnings,
1246
2542
  ...(finalOutputText !== undefined ? { finalOutputText } : {}),
1247
2543
  };
1248
2544
  },
2545
+ async findLatestReviewSessions(query) {
2546
+ const projectRoot = path.resolve(query.projectRoot);
2547
+ const reviewRoot = path.join(projectRoot, ".onto", "review");
2548
+ let entries;
2549
+ try {
2550
+ entries = await fs.readdir(reviewRoot, { withFileTypes: true });
2551
+ }
2552
+ catch {
2553
+ return [];
2554
+ }
2555
+ const createdAfterMs = query.createdAfter
2556
+ ? parseTimestampMs(query.createdAfter)
2557
+ : null;
2558
+ const targetFilter = query.target ? path.normalize(query.target) : null;
2559
+ const domainFilter = query.domain ? normalizeDomainValue(query.domain) : null;
2560
+ const matches = [];
2561
+ for (const entry of entries) {
2562
+ if (!entry.isDirectory())
2563
+ continue;
2564
+ const sessionRoot = path.join(reviewRoot, entry.name);
2565
+ const metadata = await readOptionalYaml(path.join(sessionRoot, "session-metadata.yaml"));
2566
+ if (!metadata)
2567
+ continue;
2568
+ const interpretation = await readOptionalYaml(path.join(sessionRoot, "interpretation.yaml"));
2569
+ const binding = await readOptionalYaml(path.join(sessionRoot, "binding.yaml"));
2570
+ const targetProfile = await readOptionalYaml(path.join(sessionRoot, "execution-preparation", "review-target-profile.yaml"));
2571
+ const createdAt = metadata.created_at ?? null;
2572
+ const createdAtMs = parseTimestampMs(createdAt);
2573
+ if (createdAfterMs !== null &&
2574
+ createdAtMs !== null &&
2575
+ createdAtMs < createdAfterMs) {
2576
+ continue;
2577
+ }
2578
+ if (targetFilter &&
2579
+ path.normalize(metadata.requested_target) !== targetFilter &&
2580
+ path.normalize(targetProfile?.requested_target ?? "") !== targetFilter) {
2581
+ continue;
2582
+ }
2583
+ const normalizedDomain = binding?.resolved_session_domain ??
2584
+ targetProfile?.domain ??
2585
+ normalizeDomainValue(metadata.requested_domain_token ?? "");
2586
+ if (domainFilter &&
2587
+ normalizeDomainValue(normalizedDomain) !== domainFilter) {
2588
+ continue;
2589
+ }
2590
+ const requestHash = requestHashFromArtifacts({
2591
+ metadata,
2592
+ interpretation,
2593
+ binding,
2594
+ });
2595
+ if (query.requestHash && requestHash !== query.requestHash) {
2596
+ continue;
2597
+ }
2598
+ const artifactRefs = await collectArtifactRefs(sessionRoot);
2599
+ const status = (await api.getReviewStatus(sessionRoot)).status;
2600
+ matches.push({
2601
+ sessionId: metadata.session_id ?? entry.name,
2602
+ sessionRoot,
2603
+ createdAt,
2604
+ requestedTarget: metadata.requested_target ?? null,
2605
+ requestedDomainToken: metadata.requested_domain_token ?? null,
2606
+ normalizedDomain: normalizedDomain === "none" || normalizedDomain.length === 0
2607
+ ? null
2608
+ : normalizeDomainValue(normalizedDomain),
2609
+ requestHash,
2610
+ status,
2611
+ artifactRefs,
2612
+ });
2613
+ }
2614
+ matches.sort((a, b) => {
2615
+ const left = parseTimestampMs(a.createdAt) ?? 0;
2616
+ const right = parseTimestampMs(b.createdAt) ?? 0;
2617
+ return right - left;
2618
+ });
2619
+ return matches.slice(0, query.limit ?? 5);
2620
+ },
1249
2621
  async listLenses() {
1250
2622
  const registry = loadCoreLensRegistry();
1251
2623
  return {
@@ -1268,4 +2640,5 @@ export function createOntoReviewCoreApi(options = {}) {
1268
2640
  return [...names].sort();
1269
2641
  },
1270
2642
  };
2643
+ return api;
1271
2644
  }