onto-mcp 0.3.1 → 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 (52) hide show
  1. package/.onto/authority/core-lexicon.yaml +1 -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/reconstruct/reconstruct-boundary-contract.md +278 -91
  15. package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +45 -12
  16. package/.onto/processes/reconstruct/source-profile-contract.md +39 -6
  17. package/.onto/processes/reconstruct/top-level-concept-discovery-contract.md +387 -0
  18. package/.onto/processes/review/lens-registry.md +16 -0
  19. package/.onto/processes/shared/target-material-kind-contract.md +18 -2
  20. package/.onto/roles/axiology.md +7 -2
  21. package/AGENTS.md +3 -2
  22. package/README.md +39 -33
  23. package/dist/core-api/reconstruct-api.js +22 -5
  24. package/dist/core-api/review-api.js +1288 -533
  25. package/dist/core-runtime/cli/mock-review-unit-executor.js +17 -0
  26. package/dist/core-runtime/cli/review-invoke.js +23 -48
  27. package/dist/core-runtime/cli/run-review-prompt-execution.js +122 -0
  28. package/dist/core-runtime/path-boundary.js +58 -0
  29. package/dist/core-runtime/reconstruct/artifact-types.js +5 -0
  30. package/dist/core-runtime/reconstruct/materialize-preparation.js +54 -4
  31. package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +38 -2
  32. package/dist/core-runtime/reconstruct/post-seed-validation.js +13 -0
  33. package/dist/core-runtime/reconstruct/record.js +11 -0
  34. package/dist/core-runtime/reconstruct/run.js +1133 -26
  35. package/dist/core-runtime/reconstruct/seed-candidate-validation.js +29 -0
  36. package/dist/core-runtime/review/execution-plan-boundary.js +123 -0
  37. package/dist/core-runtime/review/materializers.js +8 -3
  38. package/dist/core-runtime/review/review-artifact-utils.js +15 -2
  39. package/dist/core-runtime/review/review-invocation-runner.js +604 -0
  40. package/dist/core-runtime/target-material-kind.js +43 -5
  41. package/dist/mcp/server.js +158 -39
  42. package/dist/mcp/tool-schemas.js +22 -2
  43. package/package.json +3 -1
  44. package/.onto/domains/llm-native-development/competency_qs.md +0 -430
  45. package/.onto/domains/llm-native-development/concepts.md +0 -242
  46. package/.onto/domains/llm-native-development/conciseness_rules.md +0 -163
  47. package/.onto/domains/llm-native-development/dependency_rules.md +0 -216
  48. package/.onto/domains/llm-native-development/domain_scope.md +0 -197
  49. package/.onto/domains/llm-native-development/extension_cases.md +0 -474
  50. package/.onto/domains/llm-native-development/logic_rules.md +0 -123
  51. package/.onto/domains/llm-native-development/prompt_interface.md +0 -49
  52. package/.onto/domains/llm-native-development/structure_spec.md +0 -245
@@ -4,13 +4,16 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { resolveOntoHome } from "../core-runtime/discovery/onto-home.js";
6
6
  import { loadCoreLensRegistry } from "../core-runtime/discovery/lens-registry.js";
7
- import { fileExists, isoFromTimestamp, isoNow, readYamlDocument, writeYamlDocument, } 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";
8
10
  import { buildReviewRouteVisibilityFromSession, } from "../core-runtime/review/route-visibility.js";
9
11
  import { readValidatedReviewRecord } from "../core-runtime/review/review-record-validation.js";
10
12
  import { readReviewResultClassification } from "../core-runtime/review/review-result-classification.js";
11
13
  import { REVIEW_EXECUTION_STEP_IDS, REVIEW_PROGRESS_STEPS, REVIEW_PROGRESS_TOTAL_STEPS, reviewProgressStepById, reviewProgressStepIdFromHalt, } from "../core-runtime/review/review-progress-contract.js";
14
+ import { collectReviewInvocationArtifactRefs, prepareReviewInvocationRequest, runReviewInvocation, } from "../core-runtime/review/review-invocation-runner.js";
12
15
  import { completeReviewSession } from "../core-runtime/cli/complete-review-session.js";
13
- import { buildExecutorConfigFromRealization, reviewPrepareOnly, runReviewInvokeCli, } from "../core-runtime/cli/review-invoke.js";
16
+ import { buildExecutorConfigFromRealization, } from "../core-runtime/cli/review-invoke.js";
14
17
  import { executeReviewPromptExecution, } from "../core-runtime/cli/run-review-prompt-execution.js";
15
18
  import { buildReviewPipelineExecutionLedger, } from "../core-runtime/review/pipeline-execution-ledger.js";
16
19
  import { buildReviewContinuationPlan, } from "../core-runtime/review/continuation-plan.js";
@@ -24,6 +27,14 @@ export class ReviewContinuationError extends Error {
24
27
  this.failureContent = args.failureContent;
25
28
  }
26
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
+ }
27
38
  function stringifyConsoleArgs(args) {
28
39
  return args
29
40
  .map((arg) => {
@@ -89,53 +100,6 @@ async function withCapturedConsole(action, observer) {
89
100
  function resolveRequiredOntoHome(explicit) {
90
101
  return resolveOntoHome(explicit);
91
102
  }
92
- function appendCommonReviewArgs(args, request, ontoHome) {
93
- const result = [
94
- ...args,
95
- request.target,
96
- request.intent,
97
- "--project-root",
98
- path.resolve(request.projectRoot),
99
- ];
100
- result.push("--onto-home", ontoHome);
101
- if (request.domain && request.noDomain) {
102
- throw new Error("Use either domain or noDomain, not both.");
103
- }
104
- if (request.domain) {
105
- result.push("--domain", request.domain);
106
- }
107
- if (request.noDomain) {
108
- result.push("--no-domain");
109
- }
110
- if (request.reviewMode) {
111
- result.push("--review-mode", request.reviewMode);
112
- }
113
- if (request.targetScopeKind) {
114
- result.push("--target-scope-kind", request.targetScopeKind);
115
- }
116
- if (request.primaryRef) {
117
- result.push("--primary-ref", request.primaryRef);
118
- }
119
- for (const memberRef of request.memberRefs ?? []) {
120
- result.push("--member-ref", memberRef);
121
- }
122
- if (request.bundleKind) {
123
- result.push("--bundle-kind", request.bundleKind);
124
- }
125
- if (request.diffRange) {
126
- result.push("--diff-range", request.diffRange);
127
- }
128
- if (request.executorRealization) {
129
- result.push("--executor-realization", request.executorRealization);
130
- }
131
- for (const lensId of request.lensIds ?? []) {
132
- result.push("--lens-id", lensId);
133
- }
134
- if (request.confirmValueAlignment) {
135
- result.push("--confirm-value-alignment");
136
- }
137
- return result;
138
- }
139
103
  function basenameSessionId(sessionRoot) {
140
104
  return path.basename(path.resolve(sessionRoot));
141
105
  }
@@ -147,36 +111,44 @@ async function readOptionalYaml(filePath) {
147
111
  async function readOptionalReviewRecord(filePath) {
148
112
  if (!(await fileExists(filePath)))
149
113
  return null;
150
- return readValidatedReviewRecord(filePath);
151
- }
152
- async function readOptionalText(filePath) {
153
- if (!(await fileExists(filePath)))
154
- return undefined;
155
- return fs.readFile(filePath, "utf8");
156
- }
157
- function isInsidePath(root, candidate) {
158
- const relative = path.relative(path.resolve(root), path.resolve(candidate));
159
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
160
- }
161
- async function realpathIfExists(targetPath) {
162
114
  try {
163
- return await fs.realpath(targetPath);
115
+ return await readValidatedReviewRecord(filePath);
164
116
  }
165
117
  catch {
166
118
  return null;
167
119
  }
168
120
  }
169
- async function realpathNearestExisting(targetPath) {
170
- let current = path.resolve(targetPath);
171
- while (true) {
172
- const real = await realpathIfExists(current);
173
- if (real)
174
- return real;
175
- const parent = path.dirname(current);
176
- if (parent === current)
177
- return null;
178
- current = parent;
179
- }
121
+ async function readOptionalText(filePath) {
122
+ if (!(await fileExists(filePath)))
123
+ return undefined;
124
+ return fs.readFile(filePath, "utf8");
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;
180
152
  }
181
153
  async function assertSamePath(args) {
182
154
  const expected = path.resolve(args.expected);
@@ -190,115 +162,6 @@ async function assertSamePath(args) {
190
162
  }
191
163
  throw new Error(`${args.label} mismatch: expected ${expected}, received ${actual}`);
192
164
  }
193
- async function assertRefInsideSession(args) {
194
- const sessionRoot = path.resolve(args.sessionRoot);
195
- const resolvedRef = path.isAbsolute(args.ref)
196
- ? path.resolve(args.ref)
197
- : path.resolve(sessionRoot, args.ref);
198
- if (!isInsidePath(sessionRoot, resolvedRef)) {
199
- throw new Error(`Review continuation blocked because ${args.label} escapes the session root: ${resolvedRef}`);
200
- }
201
- const realSessionRoot = (await realpathIfExists(sessionRoot)) ?? sessionRoot;
202
- const realRef = await realpathIfExists(resolvedRef);
203
- if (realRef && !isInsidePath(realSessionRoot, realRef)) {
204
- throw new Error(`Review continuation blocked because ${args.label} realpath escapes the session root: ${realRef}`);
205
- }
206
- if (!realRef) {
207
- const realNearest = await realpathNearestExisting(path.dirname(resolvedRef));
208
- if (realNearest && !isInsidePath(realSessionRoot, realNearest)) {
209
- throw new Error(`Review continuation blocked because ${args.label} parent realpath escapes the session root: ${realNearest}`);
210
- }
211
- }
212
- }
213
- async function validateReviewExecutionPlanSessionBoundary(args) {
214
- const { executionPlan, sessionRoot } = args;
215
- const plannedSessionRoot = path.isAbsolute(executionPlan.session_root)
216
- ? executionPlan.session_root
217
- : path.resolve(sessionRoot, executionPlan.session_root);
218
- await assertSamePath({
219
- label: "ReviewExecutionPlan.session_root",
220
- expected: sessionRoot,
221
- actual: plannedSessionRoot,
222
- });
223
- const refs = [
224
- { label: "interpretation_artifact_path", ref: executionPlan.interpretation_artifact_path },
225
- { label: "binding_output_path", ref: executionPlan.binding_output_path },
226
- { label: "session_metadata_path", ref: executionPlan.session_metadata_path },
227
- { label: "execution_preparation_root", ref: executionPlan.execution_preparation_root },
228
- { label: "round1_root", ref: executionPlan.round1_root },
229
- { label: "prompt_packets_root", ref: executionPlan.prompt_packets_root },
230
- {
231
- label: "teamlead_deliberation_prompt_packet_path",
232
- ref: executionPlan.teamlead_deliberation_prompt_packet_path,
233
- },
234
- { label: "synthesize_prompt_packet_path", ref: executionPlan.synthesize_prompt_packet_path },
235
- { label: "actor_invocation_profiles_path", ref: executionPlan.actor_invocation_profiles_path },
236
- { label: "actor_consumer_bindings_path", ref: executionPlan.actor_consumer_bindings_path },
237
- { label: "domain_binding_path", ref: executionPlan.domain_binding_path },
238
- { label: "review_target_profile_path", ref: executionPlan.review_target_profile_path },
239
- {
240
- label: "review_value_alignment_criteria_path",
241
- ref: executionPlan.review_value_alignment_criteria_path,
242
- },
243
- { label: "review_context_manifest_path", ref: executionPlan.review_context_manifest_path },
244
- { label: "synthesis_output_path", ref: executionPlan.synthesis_output_path },
245
- { label: "finding_ledger_path", ref: executionPlan.finding_ledger_path },
246
- {
247
- label: "finding_relation_graph_path",
248
- ref: executionPlan.finding_relation_graph_path,
249
- },
250
- { label: "issue_ledger_path", ref: executionPlan.issue_ledger_path },
251
- { label: "issue_stance_matrix_path", ref: executionPlan.issue_stance_matrix_path },
252
- { label: "deliberation_plan_path", ref: executionPlan.deliberation_plan_path },
253
- { label: "problem_framing_path", ref: executionPlan.problem_framing_path },
254
- { label: "lens_completion_barrier_path", ref: executionPlan.lens_completion_barrier_path },
255
- { label: "deliberation_root_path", ref: executionPlan.deliberation_root_path },
256
- { label: "deliberation_output_path", ref: executionPlan.deliberation_output_path },
257
- { label: "execution_result_path", ref: executionPlan.execution_result_path },
258
- { label: "error_log_path", ref: executionPlan.error_log_path },
259
- { label: "final_output_path", ref: executionPlan.final_output_path },
260
- { label: "review_record_path", ref: executionPlan.review_record_path },
261
- ...executionPlan.lens_execution_seats.map((seat) => ({
262
- label: `lens_execution_seats.${seat.lens_id}.output_path`,
263
- ref: seat.output_path,
264
- })),
265
- ...executionPlan.lens_prompt_packet_seats.flatMap((seat) => [
266
- {
267
- label: `lens_prompt_packet_seats.${seat.lens_id}.packet_path`,
268
- ref: seat.packet_path,
269
- },
270
- {
271
- label: `lens_prompt_packet_seats.${seat.lens_id}.output_path`,
272
- ref: seat.output_path,
273
- },
274
- ]),
275
- ...executionPlan.issue_artifact_prompt_packet_seats.flatMap((seat) => [
276
- {
277
- label: `issue_artifact_prompt_packet_seats.${seat.artifact_id}.packet_path`,
278
- ref: seat.packet_path,
279
- },
280
- {
281
- label: `issue_artifact_prompt_packet_seats.${seat.artifact_id}.output_path`,
282
- ref: seat.output_path,
283
- },
284
- ]),
285
- ...executionPlan.lens_deliberation_prompt_packet_seats.flatMap((seat) => [
286
- {
287
- label: `lens_deliberation_prompt_packet_seats.${seat.lens_id}.packet_path`,
288
- ref: seat.packet_path,
289
- },
290
- {
291
- label: `lens_deliberation_prompt_packet_seats.${seat.lens_id}.output_path`,
292
- ref: seat.output_path,
293
- },
294
- ]),
295
- ];
296
- for (const { label, ref } of refs) {
297
- if (!ref)
298
- continue;
299
- await assertRefInsideSession({ sessionRoot, ref, label });
300
- }
301
- }
302
165
  function buildOpeningBriefPresentation(input) {
303
166
  return {
304
167
  prompt: [
@@ -340,6 +203,399 @@ const OPENING_PRESENTATION_SOURCE_REF_KEYS = [
340
203
  "review_target_profile",
341
204
  "review_context_manifest",
342
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
+ }
343
599
  function compactSeverityCounts(summary) {
344
600
  return [
345
601
  `blocker=${summary.severity_counts.blocker}`,
@@ -373,140 +629,6 @@ function buildHaltPresentation(input) {
373
629
  input,
374
630
  };
375
631
  }
376
- function progressEvent(args) {
377
- return {
378
- presentation_contract_version: REVIEW_PRESENTATION_CONTRACT_VERSION,
379
- event_kind: "mcp_progress",
380
- sequence: args.sequence,
381
- generated_at: isoNow(),
382
- source: args.source,
383
- stage: args.stage,
384
- session_root: args.sessionRoot,
385
- message: args.message,
386
- progress: {
387
- current: args.current,
388
- total: args.total ?? 100,
389
- ...(args.exactStep !== undefined ? { exact_step: args.exactStep } : {}),
390
- ...(args.exactTotal !== undefined ? { exact_total: args.exactTotal } : {}),
391
- ...(args.label !== undefined ? { label: args.label } : {}),
392
- },
393
- };
394
- }
395
- function progressUnitsForInvokeStep(step) {
396
- switch (step) {
397
- case 1:
398
- return 5;
399
- case 2:
400
- return 10;
401
- case 3:
402
- return 90;
403
- default:
404
- return 0;
405
- }
406
- }
407
- function progressUnitsForRuntimeStep(step, total) {
408
- if (total <= 0)
409
- return 10;
410
- return Math.min(89, 10 + Math.round((step / total) * 75));
411
- }
412
- function parseSessionRootLine(projectRoot, line) {
413
- const match = /^\s*session_root:\s+(.+?)\s*$/.exec(line);
414
- if (!match?.[1])
415
- return null;
416
- const rawSessionRoot = match[1];
417
- return path.isAbsolute(rawSessionRoot)
418
- ? rawSessionRoot
419
- : path.resolve(projectRoot, rawSessionRoot);
420
- }
421
- function consoleLineProgressEvent(args) {
422
- const plannedSessionRoot = parseSessionRootLine(args.projectRoot, args.line);
423
- if (plannedSessionRoot) {
424
- return {
425
- sessionRoot: plannedSessionRoot,
426
- event: progressEvent({
427
- sequence: args.sequence,
428
- source: "review_invoke_console",
429
- stage: "session_planned",
430
- sessionRoot: plannedSessionRoot,
431
- message: `Review session planned at ${plannedSessionRoot}.`,
432
- current: 1,
433
- label: "session planned",
434
- }),
435
- };
436
- }
437
- if (args.line.trim() === "[review start]") {
438
- return {
439
- sessionRoot: args.sessionRoot,
440
- event: progressEvent({
441
- sequence: args.sequence,
442
- source: "review_invoke_console",
443
- stage: "start_preview",
444
- sessionRoot: args.sessionRoot,
445
- message: "Review start preview generated.",
446
- current: 0,
447
- label: "start preview",
448
- }),
449
- };
450
- }
451
- const invokeStepMatch = /^\[review invoke\] step (\d+)\/3\s+(.+?)\s*$/.exec(args.line);
452
- if (invokeStepMatch?.[1] && invokeStepMatch[2]) {
453
- const step = Number.parseInt(invokeStepMatch[1], 10);
454
- const label = invokeStepMatch[2];
455
- return {
456
- sessionRoot: args.sessionRoot,
457
- event: progressEvent({
458
- sequence: args.sequence,
459
- source: "review_invoke_console",
460
- stage: "invoke_step",
461
- sessionRoot: args.sessionRoot,
462
- message: label,
463
- current: progressUnitsForInvokeStep(step),
464
- exactStep: step,
465
- exactTotal: 3,
466
- label,
467
- }),
468
- };
469
- }
470
- const runtimeStepMatch = /^\[review progress\]\s+(\d+)\/(\d+)\s+(.+?)\s*$/.exec(args.line);
471
- if (runtimeStepMatch?.[1] && runtimeStepMatch[2] && runtimeStepMatch[3]) {
472
- const step = Number.parseInt(runtimeStepMatch[1], 10);
473
- const total = Number.parseInt(runtimeStepMatch[2], 10);
474
- const label = runtimeStepMatch[3];
475
- return {
476
- sessionRoot: args.sessionRoot,
477
- event: progressEvent({
478
- sequence: args.sequence,
479
- source: "review_invoke_console",
480
- stage: "runtime_step",
481
- sessionRoot: args.sessionRoot,
482
- message: label,
483
- current: progressUnitsForRuntimeStep(step, total),
484
- exactStep: step,
485
- exactTotal: total,
486
- label,
487
- }),
488
- };
489
- }
490
- const completedMatch = /^\[review invoke\] completed 3\/3\s+(.+?)\s*$/.exec(args.line);
491
- if (completedMatch?.[1]) {
492
- const label = completedMatch[1];
493
- return {
494
- sessionRoot: args.sessionRoot,
495
- event: progressEvent({
496
- sequence: args.sequence,
497
- source: "review_invoke_console",
498
- stage: "completed",
499
- sessionRoot: args.sessionRoot,
500
- message: label,
501
- current: 98,
502
- exactStep: 3,
503
- exactTotal: 3,
504
- label,
505
- }),
506
- };
507
- }
508
- return null;
509
- }
510
632
  function generatedFromArtifactRefs(artifactRefs, keys = PRESENTATION_SOURCE_REF_KEYS) {
511
633
  const refs = {};
512
634
  for (const key of keys) {
@@ -622,6 +744,241 @@ function livenessSummary(args) {
622
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}`;
623
745
  }
624
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
+ }
625
982
  async function existingSeatIds(seats) {
626
983
  const existing = [];
627
984
  for (const seat of seats ?? []) {
@@ -637,7 +994,7 @@ async function completedProgressStepIds(params) {
637
994
  const completed = [];
638
995
  if (params.executionPlan)
639
996
  completed.push("manifest_validation");
640
- 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);
641
998
  const completedLensIds = await existingSeatIds(params.executionPlan?.lens_execution_seats);
642
999
  const allPlannedLensesCompleted = plannedLensIds.length > 0 && completedLensIds.length >= plannedLensIds.length;
643
1000
  if (allPlannedLensesCompleted || params.artifactRefs.lens_completion_barrier) {
@@ -656,8 +1013,7 @@ async function completedProgressStepIds(params) {
656
1013
  if (params.artifactRefs.deliberation_plan)
657
1014
  completed.push("deliberation_plan");
658
1015
  const deliberationIds = await existingSeatIds(params.executionPlan?.lens_deliberation_prompt_packet_seats);
659
- const plannedDeliberationIds = params.executionPlan?.lens_deliberation_prompt_packet_seats.map((seat) => seat.lens_id) ??
660
- [];
1016
+ const plannedDeliberationIds = (params.executionPlan?.lens_deliberation_prompt_packet_seats ?? []).map((seat) => seat.lens_id);
661
1017
  if ((plannedDeliberationIds.length > 0 &&
662
1018
  deliberationIds.length >= plannedDeliberationIds.length) ||
663
1019
  params.artifactRefs.deliberation_output) {
@@ -691,14 +1047,13 @@ async function activeUnits(params) {
691
1047
  return typeof unitId === "string" && unitId.length > 0 ? [unitId] : [];
692
1048
  }
693
1049
  if (params.currentStepId === "lens_dispatch") {
694
- 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);
695
1051
  const completed = new Set(await existingSeatIds(params.executionPlan?.lens_execution_seats));
696
1052
  const pending = planned.filter((lensId) => !completed.has(lensId));
697
1053
  return (pending.length > 0 ? pending : planned).map((lensId) => `lens:${lensId}`);
698
1054
  }
699
1055
  if (params.currentStepId === "lens_deliberation_responses") {
700
- const planned = params.executionPlan?.lens_deliberation_prompt_packet_seats.map((seat) => seat.lens_id) ??
701
- [];
1056
+ const planned = (params.executionPlan?.lens_deliberation_prompt_packet_seats ?? []).map((seat) => seat.lens_id);
702
1057
  const completed = new Set(await existingSeatIds(params.executionPlan?.lens_deliberation_prompt_packet_seats));
703
1058
  return planned
704
1059
  .filter((lensId) => !completed.has(lensId))
@@ -825,6 +1180,24 @@ async function buildReviewStatusPresentationInput(params) {
825
1180
  const completedStatus = params.status === "completed" || params.status === "completed_with_degradation";
826
1181
  const sessionStartMs = parseTimestampMs(params.executionResult?.execution_started_at) ??
827
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
+ });
828
1201
  const progress = {
829
1202
  current_step: completedStatus
830
1203
  ? totalSteps
@@ -848,12 +1221,7 @@ async function buildReviewStatusPresentationInput(params) {
848
1221
  params.artifactRefs.execution_plan ? "execution_plan" : null,
849
1222
  ].filter((step) => step !== null)
850
1223
  : completedSteps,
851
- active_units: await activeUnits({
852
- status: params.status,
853
- currentStepId,
854
- executionPlan: params.executionPlan,
855
- executionResult: params.executionResult,
856
- }),
1224
+ active_units: progressActiveUnits,
857
1225
  pending_units: completedStatus
858
1226
  ? []
859
1227
  : params.status === "prepared"
@@ -872,6 +1240,7 @@ async function buildReviewStatusPresentationInput(params) {
872
1240
  : currentStepId
873
1241
  ? `next ${stepById(currentStepId).label} artifact or timeout`
874
1242
  : null,
1243
+ unit_progress: unitProgress,
875
1244
  };
876
1245
  const completedLensIds = await existingSeatIds(params.executionPlan?.lens_execution_seats);
877
1246
  const halt = haltPresentation({
@@ -904,6 +1273,8 @@ async function buildReviewStatusPresentationInput(params) {
904
1273
  secondsSinceLastArtifact,
905
1274
  }),
906
1275
  };
1276
+ const runControl = await buildRunControl(params.sessionRoot, params.status);
1277
+ const targetMaterialSupport = await readTargetMaterialSupport(params.sessionRoot, params.executionPlan);
907
1278
  return {
908
1279
  presentation_contract_version: REVIEW_PRESENTATION_CONTRACT_VERSION,
909
1280
  presentation_kind: "progress",
@@ -922,6 +1293,9 @@ async function buildReviewStatusPresentationInput(params) {
922
1293
  }),
923
1294
  result_classification_summary: resultClassificationSummary,
924
1295
  halt,
1296
+ run_control: runControl,
1297
+ target_material_support: targetMaterialSupport,
1298
+ environment_warnings: await readEnvironmentWarnings(params.sessionRoot),
925
1299
  };
926
1300
  }
927
1301
  async function buildPreparedOpeningBriefInput(sessionRoot, executionPlan) {
@@ -986,74 +1360,118 @@ async function buildPreparedOpeningBriefInput(sessionRoot, executionPlan) {
986
1360
  },
987
1361
  };
988
1362
  }
989
- function parseReviewInvokeOutput(stdout) {
990
- for (const line of [...stdout].reverse()) {
991
- const trimmed = line.trim();
992
- if (!trimmed.startsWith("{"))
993
- continue;
994
- try {
995
- return JSON.parse(trimmed);
996
- }
997
- catch {
998
- // Keep looking: progress messages are not JSON.
999
- }
1000
- }
1001
- throw new Error("review invocation completed without a structured JSON result.");
1002
- }
1003
- function isReviewInvokeShape(value) {
1004
- if (value === null || typeof value !== "object")
1005
- return false;
1006
- const reviewResult = value.review_result;
1007
- return reviewResult !== null && typeof reviewResult === "object";
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
+ };
1008
1405
  }
1009
- async function collectArtifactRefs(sessionRoot) {
1010
- const candidates = {
1011
- session_metadata: path.join(sessionRoot, "session-metadata.yaml"),
1012
- interpretation: path.join(sessionRoot, "interpretation.yaml"),
1013
- binding: path.join(sessionRoot, "binding.yaml"),
1014
- execution_plan: path.join(sessionRoot, "execution-plan.yaml"),
1015
- execution_result: path.join(sessionRoot, "execution-result.yaml"),
1016
- actor_invocation_profiles: path.join(sessionRoot, "execution-preparation", "actor-invocation-profiles.yaml"),
1017
- actor_consumer_bindings: path.join(sessionRoot, "execution-preparation", "actor-consumer-bindings.yaml"),
1018
- domain_binding: path.join(sessionRoot, "execution-preparation", "domain-binding.yaml"),
1019
- review_value_alignment_criteria: path.join(sessionRoot, "execution-preparation", "review-value-alignment-criteria.yaml"),
1020
- review_target_profile: path.join(sessionRoot, "execution-preparation", "review-target-profile.yaml"),
1021
- review_context_manifest: path.join(sessionRoot, "execution-preparation", "review-context-manifest.yaml"),
1022
- lens_completion_barrier: path.join(sessionRoot, "lens-completion-barrier.yaml"),
1023
- finding_ledger: path.join(sessionRoot, "finding-ledger.yaml"),
1024
- finding_relation_graph: path.join(sessionRoot, "finding-relation-graph.yaml"),
1025
- issue_ledger: path.join(sessionRoot, "issue-ledger.yaml"),
1026
- issue_stance_matrix: path.join(sessionRoot, "issue-stance-matrix.yaml"),
1027
- deliberation_plan: path.join(sessionRoot, "deliberation-plan.yaml"),
1028
- problem_framing: path.join(sessionRoot, "problem-framing.yaml"),
1029
- deliberation_output: path.join(sessionRoot, "deliberation.md"),
1030
- synthesis_output: path.join(sessionRoot, "synthesis.md"),
1031
- review_run_manifest: path.join(sessionRoot, "review-run-manifest.yaml"),
1032
- degradation_summary: path.join(sessionRoot, "degradation-summary.yaml"),
1033
- error_log: path.join(sessionRoot, "error-log.md"),
1034
- final_output: path.join(sessionRoot, "final-output.md"),
1035
- review_record: path.join(sessionRoot, "review-record.yaml"),
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),
1036
1429
  };
1037
- const entries = [];
1038
- for (const [key, filePath] of Object.entries(candidates)) {
1039
- if (await fileExists(filePath))
1040
- entries.push([key, filePath]);
1430
+ if (executionPlan) {
1431
+ llmPresentation.openingBrief = buildOpeningBriefPresentation(await buildPreparedOpeningBriefInput(sessionRoot, executionPlan));
1041
1432
  }
1042
- return Object.fromEntries(entries);
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);
1043
1456
  }
1044
1457
  async function buildPipelineExecutionLedgerIfPossible(args) {
1045
1458
  if (!args.executionPlan)
1046
1459
  return undefined;
1047
1460
  const reviewRunManifest = await readOptionalYaml(path.join(args.sessionRoot, "review-run-manifest.yaml"));
1048
1461
  const lensCompletionBarrier = await readOptionalYaml(path.join(args.sessionRoot, "lens-completion-barrier.yaml"));
1049
- return buildReviewPipelineExecutionLedger({
1050
- sessionRoot: args.sessionRoot,
1051
- artifactRefs: args.artifactRefs,
1052
- executionPlan: args.executionPlan,
1053
- executionResult: args.executionResult,
1054
- reviewRunManifest,
1055
- lensCompletionBarrier,
1056
- });
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
+ }
1057
1475
  }
1058
1476
  function workerExecutorToRealization(workerExecutor) {
1059
1477
  if (workerExecutor === "mock")
@@ -1319,7 +1737,7 @@ async function listDomainDirs(root) {
1319
1737
  try {
1320
1738
  const entries = await fs.readdir(root, { withFileTypes: true });
1321
1739
  return entries
1322
- .filter((entry) => entry.isDirectory())
1740
+ .filter((entry) => entry.isDirectory() && !isDeprecatedDomainAlias(entry.name))
1323
1741
  .map((entry) => entry.name)
1324
1742
  .sort();
1325
1743
  }
@@ -1331,8 +1749,8 @@ export function createOntoReviewCoreApi(options = {}) {
1331
1749
  const ontoHome = resolveRequiredOntoHome(options.ontoHome);
1332
1750
  const api = {
1333
1751
  async prepareReview(request) {
1334
- const argv = appendCommonReviewArgs([], request, ontoHome);
1335
- const { result } = await withCapturedConsole(() => reviewPrepareOnly(argv));
1752
+ await validateRequestedDomainForDispatch(request, ontoHome);
1753
+ const result = await prepareReviewInvocationRequest(request, { ontoHome });
1336
1754
  const sessionRoot = path.resolve(result.session_root);
1337
1755
  const executionPlan = await readYamlDocument(path.join(sessionRoot, "execution-plan.yaml"));
1338
1756
  const openingBriefInput = await buildPreparedOpeningBriefInput(sessionRoot, executionPlan);
@@ -1347,9 +1765,17 @@ export function createOntoReviewCoreApi(options = {}) {
1347
1765
  };
1348
1766
  },
1349
1767
  async runReview(request) {
1350
- const argv = appendCommonReviewArgs(["--no-watch"], request, ontoHome);
1768
+ await validateRequestedDomainForDispatch(request, ontoHome);
1769
+ const requestHash = requestHashForReviewInput(request);
1770
+ const invocationId = `initial-${continuationAttemptId()}`;
1351
1771
  let progressSequence = 0;
1352
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
+ });
1353
1779
  const emitProgress = (event) => {
1354
1780
  const observer = request.progressObserver;
1355
1781
  if (!observer)
@@ -1368,141 +1794,223 @@ export function createOntoReviewCoreApi(options = {}) {
1368
1794
  // Progress notifications are transport-only and must not affect review execution.
1369
1795
  }
1370
1796
  };
1371
- const captureObserver = request.progressObserver
1372
- ? {
1373
- stdout: (text) => {
1374
- for (const line of text.split(/\r?\n/)) {
1375
- const parsed = consoleLineProgressEvent({
1376
- line,
1377
- projectRoot: request.projectRoot,
1378
- sessionRoot: observedSessionRoot,
1379
- sequence: progressSequence + 1,
1380
- });
1381
- if (!parsed)
1382
- continue;
1383
- observedSessionRoot = parsed.sessionRoot ?? observedSessionRoot;
1384
- try {
1385
- request.progressObserver?.(parsed.event);
1386
- }
1387
- catch {
1388
- // Progress notifications are transport-only and must not affect review execution.
1389
- }
1390
- progressSequence = parsed.event.sequence;
1391
- }
1392
- },
1393
- }
1394
- : undefined;
1395
- const captured = await withCapturedConsole(async () => {
1396
- const exitCode = await runReviewInvokeCli(argv);
1397
- if (exitCode !== 0) {
1398
- throw new Error(`review invocation failed with exit code ${exitCode}`);
1399
- }
1400
- return exitCode;
1401
- }, captureObserver);
1402
- const parsed = parseReviewInvokeOutput(captured.stdout);
1403
- if (!isReviewInvokeShape(parsed)) {
1404
- throw new Error("review invocation returned an unexpected result shape.");
1405
- }
1406
- const result = parsed.review_result;
1407
- const status = result.record_status ?? "halted_partial";
1408
- const startPreview = {
1409
- entrypointPlan: parsed.entrypoint_plan,
1410
- routeSummary: parsed.route_summary,
1411
- ...(parsed.bounded_invoke_steps !== undefined
1412
- ? { boundedInvokeSteps: parsed.bounded_invoke_steps }
1413
- : {}),
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
+ });
1414
1818
  };
1415
- const resolvedResultSessionRoot = path.resolve(result.session_root);
1416
- const artifactRefs = await collectArtifactRefs(resolvedResultSessionRoot);
1417
- const failures = await collectStructuredFailures(resolvedResultSessionRoot);
1418
- const executionPlan = await readOptionalYaml(path.join(resolvedResultSessionRoot, "execution-plan.yaml"));
1419
- const executionResult = await readOptionalYaml(path.join(resolvedResultSessionRoot, "execution-result.yaml"));
1420
- const pipelineExecutionLedger = await buildPipelineExecutionLedgerIfPossible({
1421
- sessionRoot: resolvedResultSessionRoot,
1422
- artifactRefs,
1423
- executionPlan,
1424
- executionResult,
1425
- });
1426
- const reviewRecord = await readOptionalReviewRecord(path.join(resolvedResultSessionRoot, "review-record.yaml"));
1427
- const resultClassificationSummary = await readReviewResultClassification(resolvedResultSessionRoot);
1428
- const progressInput = await buildReviewStatusPresentationInput({
1429
- sessionRoot: resolvedResultSessionRoot,
1430
- status,
1431
- artifactRefs,
1432
- executionPlan,
1433
- executionResult,
1434
- reviewRecord,
1435
- });
1436
- const openingBriefInput = executionPlan
1437
- ? await buildPreparedOpeningBriefInput(resolvedResultSessionRoot, executionPlan)
1438
- : {
1439
- presentation_contract_version: REVIEW_PRESENTATION_CONTRACT_VERSION,
1440
- presentation_kind: "opening_brief",
1441
- session_id: basenameSessionId(resolvedResultSessionRoot),
1442
- session_root: resolvedResultSessionRoot,
1443
- status,
1444
- generated_from_artifact_refs: generatedFromArtifactRefs(artifactRefs),
1445
- start_preview: startPreview,
1446
- };
1447
- const finalResultInput = {
1448
- presentation_contract_version: REVIEW_PRESENTATION_CONTRACT_VERSION,
1449
- presentation_kind: "final_result",
1450
- session_id: basenameSessionId(resolvedResultSessionRoot),
1451
- session_root: resolvedResultSessionRoot,
1452
- status,
1453
- generated_from_artifact_refs: generatedFromArtifactRefs(artifactRefs),
1454
- result_overview: parsed.result_overview ?? null,
1455
- result_classification_summary: resultClassificationSummary,
1456
- review_result: result,
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,
1847
+ },
1848
+ });
1457
1849
  };
1458
- const llmPresentation = {
1459
- openingBrief: buildOpeningBriefPresentation(openingBriefInput),
1460
- progress: buildProgressPresentation(progressInput),
1461
- ...(progressInput.halt
1462
- ? {
1463
- halt: buildHaltPresentation({
1464
- ...progressInput,
1465
- presentation_kind: "halt",
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,
1466
1971
  }),
1972
+ runControl: progressInput.run_control,
1973
+ targetMaterialSupport: progressInput.target_material_support,
1974
+ environmentWarnings: progressInput.environment_warnings,
1975
+ };
1976
+ }
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
+ });
1467
1984
  }
1468
- : {}),
1469
- finalResult: buildFinalResultPresentation(finalResultInput),
1470
- };
1471
- emitProgress({
1472
- source: "artifact_status",
1473
- stage: "final_status",
1474
- session_root: resolvedResultSessionRoot,
1475
- message: `Review finished with status ${status}.`,
1476
- progress: {
1477
- current: 100,
1478
- total: 100,
1479
- label: "final status",
1480
- },
1481
- });
1482
- return {
1483
- sessionId: basenameSessionId(result.session_root),
1484
- sessionRoot: resolvedResultSessionRoot,
1485
- status,
1486
- finalOutputPath: result.final_output_path,
1487
- reviewRecordPath: result.review_record_path,
1488
- executionResultPath: result.execution_result_path,
1489
- reviewRunManifestPath: result.review_run_manifest_path ??
1490
- path.join(resolvedResultSessionRoot, "review-run-manifest.yaml"),
1491
- deliberationStatus: result.deliberation_status ?? null,
1492
- participatingLensIds: result.participating_lens_ids ?? [],
1493
- degradedLensIds: result.degraded_lens_ids ?? [],
1494
- ...(result.summary !== undefined ? { summary: result.summary } : {}),
1495
- ...(parsed.result_overview !== undefined
1496
- ? { resultOverview: parsed.result_overview }
1497
- : {}),
1498
- artifactRefs,
1499
- ...(pipelineExecutionLedger ? { pipelineExecutionLedger } : {}),
1500
- resultClassificationSummary,
1501
- ...failures,
1502
- routeVisibility: await buildReviewRouteVisibilityFromSession(resolvedResultSessionRoot),
1503
- startPreview,
1504
- llmPresentation,
1505
- };
1985
+ throw error;
1986
+ }
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;
2012
+ }
2013
+ return fullRun;
1506
2014
  },
1507
2015
  async continueReview(request) {
1508
2016
  const resolvedSessionRoot = path.resolve(request.sessionRoot);
@@ -1530,10 +2038,36 @@ export function createOntoReviewCoreApi(options = {}) {
1530
2038
  expected: sessionMetadataPath,
1531
2039
  actual: executionPlan.session_metadata_path,
1532
2040
  });
1533
- await validateReviewExecutionPlanSessionBoundary({
2041
+ await assertReviewExecutionPlanSessionBoundary({
1534
2042
  sessionRoot: resolvedSessionRoot,
1535
2043
  executionPlan,
1536
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
+ }
1537
2071
  const executionResult = await readOptionalYaml(path.join(resolvedSessionRoot, "execution-result.yaml"));
1538
2072
  const pipelineExecutionLedger = await buildPipelineExecutionLedgerIfPossible({
1539
2073
  sessionRoot: resolvedSessionRoot,
@@ -1618,6 +2152,15 @@ export function createOntoReviewCoreApi(options = {}) {
1618
2152
  });
1619
2153
  };
1620
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
+ });
1621
2164
  let promptExecutionResult;
1622
2165
  try {
1623
2166
  const executorConfig = buildExecutorConfigFromRealization(executorRealization, ontoHome);
@@ -1645,6 +2188,12 @@ export function createOntoReviewCoreApi(options = {}) {
1645
2188
  await writeAttemptManifest(promptExecutionResult.synthesis_executed
1646
2189
  ? "completed"
1647
2190
  : "halted_partial", { prompt_execution_result: promptExecutionResult });
2191
+ await updateActiveAttemptTerminal({
2192
+ sessionRoot: resolvedSessionRoot,
2193
+ status: promptExecutionResult.synthesis_executed
2194
+ ? "completed"
2195
+ : "halted_partial",
2196
+ });
1648
2197
  }
1649
2198
  catch (error) {
1650
2199
  const restoredArtifactBackups = await restoreSupersededArtifacts(supersededArtifactBackups);
@@ -1656,6 +2205,11 @@ export function createOntoReviewCoreApi(options = {}) {
1656
2205
  : {}),
1657
2206
  restored_artifact_backups: restoredArtifactBackups,
1658
2207
  });
2208
+ await updateActiveAttemptTerminal({
2209
+ sessionRoot: resolvedSessionRoot,
2210
+ status: "failed",
2211
+ errorMessage,
2212
+ });
1659
2213
  throw new ReviewContinuationError({
1660
2214
  message: `Review continuation failed: ${errorMessage}`,
1661
2215
  originalError: error,
@@ -1681,6 +2235,7 @@ export function createOntoReviewCoreApi(options = {}) {
1681
2235
  return {
1682
2236
  sessionId: postStatus.sessionId,
1683
2237
  sessionRoot: resolvedSessionRoot,
2238
+ decision: "executed",
1684
2239
  status: postStatus.status,
1685
2240
  continuationPlan,
1686
2241
  continuationAttempt: {
@@ -1705,6 +2260,77 @@ export function createOntoReviewCoreApi(options = {}) {
1705
2260
  : {}),
1706
2261
  };
1707
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,
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);
2322
+ return {
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 } : {}),
2332
+ };
2333
+ },
1708
2334
  async getReviewStatus(sessionRoot) {
1709
2335
  const resolvedSessionRoot = path.resolve(sessionRoot);
1710
2336
  const artifactRefs = await collectArtifactRefs(resolvedSessionRoot);
@@ -1721,15 +2347,22 @@ export function createOntoReviewCoreApi(options = {}) {
1721
2347
  ? buildReviewContinuationPlan({ ledger: pipelineExecutionLedger })
1722
2348
  : undefined;
1723
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;
1724
2352
  const status = reviewRecord
1725
2353
  ? reviewRecord.record_status
1726
2354
  : executionResult?.execution_status === "halted_partial"
1727
2355
  ? "halted_partial"
1728
- : executionPlan
1729
- ? await hasRunArtifacts(resolvedSessionRoot, artifactRefs)
1730
- ? "running"
1731
- : "prepared"
1732
- : "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";
1733
2366
  const progressInput = await buildReviewStatusPresentationInput({
1734
2367
  sessionRoot: resolvedSessionRoot,
1735
2368
  status,
@@ -1761,6 +2394,10 @@ export function createOntoReviewCoreApi(options = {}) {
1761
2394
  ...failures,
1762
2395
  routeVisibility: await buildReviewRouteVisibilityFromSession(resolvedSessionRoot),
1763
2396
  llmPresentation,
2397
+ runControl: progressInput.run_control,
2398
+ targetMaterialSupport: progressInput.target_material_support,
2399
+ environmentWarnings: progressInput.environment_warnings,
2400
+ unitProgress: progressInput.progress.unit_progress,
1764
2401
  };
1765
2402
  }
1766
2403
  if (executionResult?.execution_status === "halted_partial") {
@@ -1774,6 +2411,10 @@ export function createOntoReviewCoreApi(options = {}) {
1774
2411
  ...failures,
1775
2412
  routeVisibility: await buildReviewRouteVisibilityFromSession(resolvedSessionRoot),
1776
2413
  llmPresentation,
2414
+ runControl: progressInput.run_control,
2415
+ targetMaterialSupport: progressInput.target_material_support,
2416
+ environmentWarnings: progressInput.environment_warnings,
2417
+ unitProgress: progressInput.progress.unit_progress,
1777
2418
  };
1778
2419
  }
1779
2420
  if (executionPlan) {
@@ -1787,6 +2428,10 @@ export function createOntoReviewCoreApi(options = {}) {
1787
2428
  ...failures,
1788
2429
  routeVisibility: await buildReviewRouteVisibilityFromSession(resolvedSessionRoot),
1789
2430
  llmPresentation,
2431
+ runControl: progressInput.run_control,
2432
+ targetMaterialSupport: progressInput.target_material_support,
2433
+ environmentWarnings: progressInput.environment_warnings,
2434
+ unitProgress: progressInput.progress.unit_progress,
1790
2435
  };
1791
2436
  }
1792
2437
  return {
@@ -1799,16 +2444,28 @@ export function createOntoReviewCoreApi(options = {}) {
1799
2444
  ...failures,
1800
2445
  routeVisibility: await buildReviewRouteVisibilityFromSession(resolvedSessionRoot),
1801
2446
  llmPresentation,
2447
+ runControl: progressInput.run_control,
2448
+ targetMaterialSupport: progressInput.target_material_support,
2449
+ environmentWarnings: progressInput.environment_warnings,
2450
+ unitProgress: progressInput.progress.unit_progress,
1802
2451
  };
1803
2452
  },
1804
- async getReviewResult(sessionRoot) {
2453
+ async getReviewResult(sessionRoot, options = {}) {
1805
2454
  const resolvedSessionRoot = path.resolve(sessionRoot);
2455
+ const projectionLevel = options.projectionLevel ?? "full";
1806
2456
  const artifactRefs = await collectArtifactRefs(resolvedSessionRoot);
1807
2457
  const { failureRefs } = await collectStructuredFailures(resolvedSessionRoot);
1808
2458
  const reviewRecordPath = path.join(resolvedSessionRoot, "review-record.yaml");
1809
2459
  const reviewRecord = await readValidatedReviewRecord(reviewRecordPath);
1810
- const finalOutputPath = reviewRecord.final_output_ref ?? path.join(resolvedSessionRoot, "final-output.md");
1811
- 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);
1812
2469
  const executionPlan = await readOptionalYaml(path.join(resolvedSessionRoot, "execution-plan.yaml"));
1813
2470
  const executionResult = await readOptionalYaml(path.join(resolvedSessionRoot, "execution-result.yaml"));
1814
2471
  const pipelineExecutionLedger = await buildPipelineExecutionLedgerIfPossible({
@@ -1835,12 +2492,32 @@ export function createOntoReviewCoreApi(options = {}) {
1835
2492
  status,
1836
2493
  generated_from_artifact_refs: generatedFromArtifactRefs(artifactRefs),
1837
2494
  result_classification_summary: resultClassificationSummary,
1838
- 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
+ },
1839
2504
  };
2505
+ const targetMaterialSupport = await readTargetMaterialSupport(resolvedSessionRoot, executionPlan);
2506
+ const environmentWarnings = await readEnvironmentWarnings(resolvedSessionRoot);
1840
2507
  return {
1841
2508
  sessionId: reviewRecord.session_id,
1842
2509
  sessionRoot: resolvedSessionRoot,
1843
- 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 } : {}),
1844
2521
  finalOutputPath,
1845
2522
  reviewRunManifestPath: path.join(resolvedSessionRoot, "review-run-manifest.yaml"),
1846
2523
  artifactRefs,
@@ -1860,9 +2537,87 @@ export function createOntoReviewCoreApi(options = {}) {
1860
2537
  : {}),
1861
2538
  finalResult: buildFinalResultPresentation(finalResultInput),
1862
2539
  },
2540
+ targetMaterialSupport,
2541
+ environmentWarnings,
1863
2542
  ...(finalOutputText !== undefined ? { finalOutputText } : {}),
1864
2543
  };
1865
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
+ },
1866
2621
  async listLenses() {
1867
2622
  const registry = loadCoreLensRegistry();
1868
2623
  return {