onto-mcp 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.onto/authority/core-lexicon.yaml +12 -0
  2. package/.onto/domains/software-engineering/competency_qs.md +192 -63
  3. package/.onto/domains/software-engineering/concepts.md +67 -5
  4. package/.onto/domains/software-engineering/conciseness_rules.md +22 -2
  5. package/.onto/domains/software-engineering/dependency_rules.md +78 -8
  6. package/.onto/domains/software-engineering/domain_scope.md +181 -150
  7. package/.onto/domains/software-engineering/extension_cases.md +318 -542
  8. package/.onto/domains/software-engineering/logic_rules.md +75 -3
  9. package/.onto/domains/software-engineering/problem_framing_profile.md +29 -2
  10. package/.onto/domains/software-engineering/prompt_interface.md +122 -0
  11. package/.onto/domains/software-engineering/structure_spec.md +53 -4
  12. package/.onto/principles/llm-native-development-guideline.md +20 -0
  13. package/.onto/principles/productization-charter.md +6 -0
  14. package/.onto/processes/evolve/material-kind-adapter-contract.md +6 -0
  15. package/.onto/processes/reconstruct/reconstruct-boundary-contract.md +468 -81
  16. package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +177 -0
  17. package/.onto/processes/reconstruct/source-profile-contract.md +39 -6
  18. package/.onto/processes/reconstruct/top-level-concept-discovery-contract.md +387 -0
  19. package/.onto/processes/review/binding-contract.md +8 -0
  20. package/.onto/processes/review/lens-registry.md +16 -0
  21. package/.onto/processes/review/pre-dispatch-contracts.md +34 -13
  22. package/.onto/processes/review/productized-live-path.md +3 -1
  23. package/.onto/processes/shared/pipeline-execution-ledger-contract.md +185 -0
  24. package/.onto/processes/shared/target-material-kind-contract.md +24 -2
  25. package/.onto/roles/axiology.md +7 -2
  26. package/AGENTS.md +4 -2
  27. package/README.md +52 -29
  28. package/dist/core-api/reconstruct-api.js +92 -5
  29. package/dist/core-api/review-api.js +1744 -371
  30. package/dist/core-runtime/cli/mock-review-unit-executor.js +17 -0
  31. package/dist/core-runtime/cli/render-review-final-output.js +9 -0
  32. package/dist/core-runtime/cli/review-invoke.js +387 -55
  33. package/dist/core-runtime/cli/run-review-prompt-execution.js +361 -90
  34. package/dist/core-runtime/path-boundary.js +58 -0
  35. package/dist/core-runtime/pipeline-execution-ledger.js +100 -0
  36. package/dist/core-runtime/reconstruct/artifact-types.js +33 -1
  37. package/dist/core-runtime/reconstruct/materialize-preparation.js +54 -4
  38. package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +342 -0
  39. package/dist/core-runtime/reconstruct/post-seed-validation.js +630 -0
  40. package/dist/core-runtime/reconstruct/record.js +105 -1
  41. package/dist/core-runtime/reconstruct/run.js +1594 -38
  42. package/dist/core-runtime/reconstruct/seed-candidate-validation.js +29 -0
  43. package/dist/core-runtime/review/continuation-plan.js +160 -0
  44. package/dist/core-runtime/review/execution-plan-boundary.js +123 -0
  45. package/dist/core-runtime/review/materializers.js +8 -3
  46. package/dist/core-runtime/review/pipeline-execution-ledger.js +250 -0
  47. package/dist/core-runtime/review/review-artifact-utils.js +15 -2
  48. package/dist/core-runtime/review/review-invocation-runner.js +604 -0
  49. package/dist/core-runtime/target-material-kind.js +43 -5
  50. package/dist/mcp/server.js +289 -59
  51. package/dist/mcp/tool-schemas.js +28 -2
  52. package/package.json +4 -2
  53. package/.onto/domains/llm-native-development/competency_qs.md +0 -430
  54. package/.onto/domains/llm-native-development/concepts.md +0 -242
  55. package/.onto/domains/llm-native-development/conciseness_rules.md +0 -163
  56. package/.onto/domains/llm-native-development/dependency_rules.md +0 -216
  57. package/.onto/domains/llm-native-development/domain_scope.md +0 -197
  58. package/.onto/domains/llm-native-development/extension_cases.md +0 -474
  59. package/.onto/domains/llm-native-development/logic_rules.md +0 -123
  60. package/.onto/domains/llm-native-development/prompt_interface.md +0 -49
  61. package/.onto/domains/llm-native-development/structure_spec.md +0 -245
@@ -14,6 +14,7 @@ import { REVIEW_EXECUTION_STEP_IDS, REVIEW_PROGRESS_TOTAL_STEPS, } from "../revi
14
14
  import { printOntoReleaseChannelNotice } from "../release-channel/release-channel.js";
15
15
  import { executeReviewViaCodexNested } from "./codex-nested-dispatch.js";
16
16
  import { ReviewStructuredFailureError, writeAndThrowStructuredFailureRecord, } from "../review/failure-records.js";
17
+ import { assertReviewExecutionPlanSessionBoundary, } from "../review/execution-plan-boundary.js";
17
18
  function errorMessage(error) {
18
19
  return error instanceof Error ? error.message : String(error);
19
20
  }
@@ -143,6 +144,7 @@ const DEFAULT_LENS_MAX_RETRIES = 10;
143
144
  /** Base delay for bounded linear retries. Synthesize reuses the base delay. */
144
145
  const DEFAULT_LENS_RETRY_INITIAL_DELAY_MS = 8000;
145
146
  const DEFAULT_REVIEW_UNIT_TIMEOUT_MS = 600_000;
147
+ const REVIEW_CANCEL_REQUEST_FILENAME = "review-cancel-request.yaml";
146
148
  class ReviewUnitTimeoutError extends Error {
147
149
  timeoutMs;
148
150
  constructor(unitId, timeoutMs) {
@@ -215,6 +217,12 @@ function haltArtifactFields(haltPhase, outcome) {
215
217
  function sleep(ms) {
216
218
  return new Promise((resolve) => setTimeout(resolve, ms));
217
219
  }
220
+ async function readReviewCancelRequest(sessionRoot) {
221
+ const cancelPath = path.join(sessionRoot, REVIEW_CANCEL_REQUEST_FILENAME);
222
+ if (!(await fileExists(cancelPath)))
223
+ return null;
224
+ return readYamlDocument(cancelPath);
225
+ }
218
226
  async function ensureNonEmptyOutputFile(outputPath) {
219
227
  if (!(await fileExists(outputPath))) {
220
228
  throw new Error(`Executor did not create output file: ${outputPath}`);
@@ -308,9 +316,14 @@ async function invokeExecutor(executorConfig, projectRoot, sessionRoot, dispatch
308
316
  ? combinedMessage
309
317
  : `Executor exited with code ${exitCode} for ${dispatch.unit_id}`);
310
318
  }
319
+ if (stderr.trim().length > 0) {
320
+ console.warn(`[review runner warning] ${dispatch.unit_id}: ${stderr.trim()}`);
321
+ }
311
322
  await ensureNonEmptyOutputFile(dispatch.output_path);
312
323
  }
313
324
  function toUnitExecutionResult(outcome) {
325
+ if (outcome.preservedResult)
326
+ return outcome.preservedResult;
314
327
  return {
315
328
  unit_id: outcome.dispatch.unit_id,
316
329
  unit_kind: outcome.dispatch.unit_kind,
@@ -326,6 +339,45 @@ function toUnitExecutionResult(outcome) {
326
339
  failure_message: outcome.failure?.message ?? null,
327
340
  };
328
341
  }
342
+ function allUnitExecutionResults(artifact) {
343
+ if (!artifact)
344
+ return [];
345
+ return [
346
+ ...artifact.lens_execution_results,
347
+ ...(artifact.issue_artifact_execution_results ?? []),
348
+ ...(artifact.deliberation_execution_results ?? []),
349
+ ...(artifact.synthesize_execution_result
350
+ ? [artifact.synthesize_execution_result]
351
+ : []),
352
+ ];
353
+ }
354
+ function outcomeFromPreviousResult(result) {
355
+ const startedAtMs = Date.parse(result.started_at);
356
+ const completedAtMs = Date.parse(result.completed_at);
357
+ return {
358
+ dispatch: {
359
+ unit_id: result.unit_id,
360
+ unit_kind: result.unit_kind,
361
+ packet_path: result.packet_path,
362
+ output_path: result.output_path,
363
+ },
364
+ success: result.status === "completed",
365
+ startedAtMs: Number.isFinite(startedAtMs) ? startedAtMs : Date.now(),
366
+ completedAtMs: Number.isFinite(completedAtMs) ? completedAtMs : Date.now(),
367
+ ...(result.status === "failed"
368
+ ? {
369
+ failure: {
370
+ unit_id: result.unit_id,
371
+ unit_kind: result.unit_kind,
372
+ packet_path: result.packet_path,
373
+ output_path: result.output_path,
374
+ message: result.failure_message ?? "Previous unit failed.",
375
+ },
376
+ }
377
+ : {}),
378
+ preservedResult: result,
379
+ };
380
+ }
329
381
  function deriveExecutionStatus(params) {
330
382
  if (!params.synthesisExecuted) {
331
383
  return "halted_partial";
@@ -959,7 +1011,7 @@ async function writeReviewRunManifest(executionPlan, artifact, reviewExecutionPr
959
1011
  synthesize: reviewExecutionProfile.synthesize,
960
1012
  deliberation: reviewExecutionProfile.deliberation,
961
1013
  runtime_route: {
962
- execution_realization: executionPlan.execution_realization,
1014
+ execution_realization: route.execution_realization,
963
1015
  host_runtime: route.artifact_host_runtime,
964
1016
  worker_executor: route.executor,
965
1017
  runtime_provider: route.resolved_provider,
@@ -1149,6 +1201,22 @@ function issueArtifactProgress(artifactId) {
1149
1201
  const spec = issueArtifactSpec(artifactId);
1150
1202
  return { step: spec.progress_step, label: spec.progress_label };
1151
1203
  }
1204
+ function issueArtifactOutputPath(executionPlan, artifactId) {
1205
+ switch (artifactId) {
1206
+ case "finding-ledger":
1207
+ return executionPlan.finding_ledger_path;
1208
+ case "finding-relation-graph":
1209
+ return executionPlan.finding_relation_graph_path;
1210
+ case "issue-ledger":
1211
+ return executionPlan.issue_ledger_path;
1212
+ case "issue-stance-matrix":
1213
+ return executionPlan.issue_stance_matrix_path;
1214
+ case "deliberation-plan":
1215
+ return executionPlan.deliberation_plan_path;
1216
+ case "problem-framing":
1217
+ return executionPlan.problem_framing_path;
1218
+ }
1219
+ }
1152
1220
  async function runIssueArtifactDispatch(args) {
1153
1221
  const seat = await writeIssueArtifactPromptPacket({
1154
1222
  artifactId: args.artifactId,
@@ -1289,7 +1357,17 @@ async function runControlledLensDeliberation(args) {
1289
1357
  label: "lens deliberation responses",
1290
1358
  details: [`participating_lens_count=${deliberationDispatches.length}`],
1291
1359
  });
1360
+ const shouldRunUnit = (unitId) => args.runUnitIds === undefined || args.runUnitIds.has(unitId);
1361
+ const preservedOutcomeForDispatch = (dispatch) => {
1362
+ const result = args.preservedResultsByUnitId?.get(dispatch.unit_id);
1363
+ if (!result || result.status !== "completed") {
1364
+ throw new Error(`Cannot preserve continuation unit without a completed prior result: ${dispatch.unit_id}`);
1365
+ }
1366
+ return outcomeFromPreviousResult(result);
1367
+ };
1292
1368
  for (const dispatch of deliberationDispatches) {
1369
+ if (!shouldRunUnit(dispatch.unit_id))
1370
+ continue;
1293
1371
  const lensId = dispatch.unit_id.replace(/^deliberation-/, "");
1294
1372
  const ownOutput = lensOutputById.get(lensId);
1295
1373
  if (!ownOutput) {
@@ -1323,6 +1401,10 @@ async function runControlledLensDeliberation(args) {
1323
1401
  const dispatch = deliberationDispatches[currentIndex];
1324
1402
  if (!dispatch)
1325
1403
  return;
1404
+ if (!shouldRunUnit(dispatch.unit_id)) {
1405
+ deliberationOutcomes[currentIndex] = preservedOutcomeForDispatch(dispatch);
1406
+ continue;
1407
+ }
1326
1408
  deliberationOutcomes[currentIndex] = await runSingleDispatchWithRetries({
1327
1409
  projectRoot,
1328
1410
  sessionRoot,
@@ -1349,41 +1431,47 @@ async function runControlledLensDeliberation(args) {
1349
1431
  content: await fs.readFile(dispatch.output_path, "utf8"),
1350
1432
  };
1351
1433
  }));
1352
- const teamleadPacketText = buildTeamleadControlledDeliberationPrompt({
1353
- session_id: executionPlan.session_id,
1354
- output_path: executionPlan.deliberation_output_path,
1355
- lens_outputs: lensOutputs,
1356
- lens_deliberation_responses: lensDeliberationResponses,
1357
- ...(issueArtifactContext ? { issue_artifact_context: issueArtifactContext } : {}),
1358
- });
1359
- await fs.writeFile(executionPlan.teamlead_deliberation_prompt_packet_path, `${teamleadPacketText.trimEnd()}\n`, "utf8");
1360
- await registerGeneratedPromptPacketRefForDispatch({
1361
- executionPlan,
1362
- consumerId: "controlled-deliberation",
1363
- packetPath: executionPlan.teamlead_deliberation_prompt_packet_path,
1364
- });
1365
1434
  const teamleadDispatch = {
1366
1435
  unit_id: "controlled-deliberation",
1367
1436
  unit_kind: "deliberation",
1368
1437
  packet_path: executionPlan.teamlead_deliberation_prompt_packet_path,
1369
1438
  output_path: executionPlan.deliberation_output_path,
1370
1439
  };
1371
- await emitReviewProgress({
1372
- executionPlan,
1373
- step: 10,
1374
- label: "teamlead controlled deliberation",
1375
- details: [`output_path=${executionPlan.deliberation_output_path}`],
1376
- });
1377
- const teamleadOutcome = await runSingleDispatchWithRetries({
1378
- projectRoot,
1379
- sessionRoot,
1380
- executionPlan,
1381
- executorConfig: teamleadExecutorConfig,
1382
- dispatch: teamleadDispatch,
1383
- maxRetries: 1,
1384
- retryInitialDelayMs: DEFAULT_LENS_RETRY_INITIAL_DELAY_MS,
1385
- unitTimeoutMs,
1386
- });
1440
+ let teamleadOutcome;
1441
+ if (shouldRunUnit(teamleadDispatch.unit_id)) {
1442
+ const teamleadPacketText = buildTeamleadControlledDeliberationPrompt({
1443
+ session_id: executionPlan.session_id,
1444
+ output_path: executionPlan.deliberation_output_path,
1445
+ lens_outputs: lensOutputs,
1446
+ lens_deliberation_responses: lensDeliberationResponses,
1447
+ ...(issueArtifactContext ? { issue_artifact_context: issueArtifactContext } : {}),
1448
+ });
1449
+ await fs.writeFile(executionPlan.teamlead_deliberation_prompt_packet_path, `${teamleadPacketText.trimEnd()}\n`, "utf8");
1450
+ await registerGeneratedPromptPacketRefForDispatch({
1451
+ executionPlan,
1452
+ consumerId: "controlled-deliberation",
1453
+ packetPath: executionPlan.teamlead_deliberation_prompt_packet_path,
1454
+ });
1455
+ await emitReviewProgress({
1456
+ executionPlan,
1457
+ step: 10,
1458
+ label: "teamlead controlled deliberation",
1459
+ details: [`output_path=${executionPlan.deliberation_output_path}`],
1460
+ });
1461
+ teamleadOutcome = await runSingleDispatchWithRetries({
1462
+ projectRoot,
1463
+ sessionRoot,
1464
+ executionPlan,
1465
+ executorConfig: teamleadExecutorConfig,
1466
+ dispatch: teamleadDispatch,
1467
+ maxRetries: 1,
1468
+ retryInitialDelayMs: DEFAULT_LENS_RETRY_INITIAL_DELAY_MS,
1469
+ unitTimeoutMs,
1470
+ });
1471
+ }
1472
+ else {
1473
+ teamleadOutcome = preservedOutcomeForDispatch(teamleadDispatch);
1474
+ }
1387
1475
  if (!teamleadOutcome.success) {
1388
1476
  throw new ReviewControlledDeliberationDispatchError(`Teamlead controlled deliberation failed: ${teamleadOutcome.failure?.message ?? "unknown error"}`, [...completedDeliberationOutcomes, teamleadOutcome], teamleadOutcome);
1389
1477
  }
@@ -1402,9 +1490,42 @@ export async function executeReviewPromptExecution(params) {
1402
1490
  const sessionRoot = path.resolve(params.sessionRoot);
1403
1491
  const executionPlanPath = path.join(sessionRoot, "execution-plan.yaml");
1404
1492
  const executionPlan = await readYamlDocument(executionPlanPath);
1493
+ await assertReviewExecutionPlanSessionBoundary({ sessionRoot, executionPlan });
1405
1494
  const executionStartedAtMs = Date.now();
1406
- await resetExecutionOutputs(executionPlan);
1407
- await pruneGeneratedPromptPacketRefs(executionPlan);
1495
+ const continuationPlan = params.continuationPlan;
1496
+ const continuationMode = continuationPlan !== undefined;
1497
+ const continuationRunUnitIds = new Set([
1498
+ ...(continuationPlan?.frontierUnits ?? []),
1499
+ ...(continuationPlan?.downstreamUnits ?? []),
1500
+ ]
1501
+ .filter((unit) => unit.dispatchDecision === "run")
1502
+ .map((unit) => unit.unitId));
1503
+ const previousExecutionResult = continuationMode && await fileExists(executionPlan.execution_result_path)
1504
+ ? await readYamlDocument(executionPlan.execution_result_path)
1505
+ : null;
1506
+ const previousResultsByUnitId = new Map(allUnitExecutionResults(previousExecutionResult).map((result) => [
1507
+ result.unit_id,
1508
+ result,
1509
+ ]));
1510
+ const shouldRunUnit = (unitId) => !continuationMode || continuationRunUnitIds.has(unitId);
1511
+ const preservedOutcomeForDispatch = (dispatch) => {
1512
+ const result = previousResultsByUnitId.get(dispatch.unit_id);
1513
+ if (!result || result.status !== "completed") {
1514
+ throw new Error(`Cannot preserve continuation unit without a completed prior result: ${dispatch.unit_id}`);
1515
+ }
1516
+ return outcomeFromPreviousResult(result);
1517
+ };
1518
+ if (continuationMode) {
1519
+ await appendMarkdownLogEntry(executionPlan.error_log_path, "runner continuation mode", [
1520
+ `frontier_units: ${continuationPlan.frontierUnits.map((unit) => unit.unitId).join(", ")}`,
1521
+ `downstream_units: ${continuationPlan.downstreamUnits.map((unit) => unit.unitId).join(", ")}`,
1522
+ `preserved_artifact_count: ${continuationPlan.preservedArtifactRefs.length}`,
1523
+ ].join("\n"));
1524
+ }
1525
+ else {
1526
+ await resetExecutionOutputs(executionPlan);
1527
+ await pruneGeneratedPromptPacketRefs(executionPlan);
1528
+ }
1408
1529
  await ensureReviewContextManifestReadyForDispatch(executionPlan);
1409
1530
  await emitReviewProgress({
1410
1531
  executionPlan,
@@ -1453,6 +1574,77 @@ export async function executeReviewPromptExecution(params) {
1453
1574
  }
1454
1575
  const executionOutcomes = new Array(lensDispatches.length);
1455
1576
  let nextLensIndex = 0;
1577
+ async function haltForCancellation(args) {
1578
+ const completedLensOutcomes = executionOutcomes.filter(isSuccessfulOutcome);
1579
+ const successfulLensDispatches = completedLensOutcomes.map((outcome) => outcome.dispatch);
1580
+ const executionFailures = executionOutcomes
1581
+ .filter(isFailureOutcome)
1582
+ .map((outcome) => outcome.failure);
1583
+ const degradedLensIds = executionFailures
1584
+ .filter((failure) => failure.unit_kind === "lens")
1585
+ .map((failure) => failure.unit_id);
1586
+ const haltReason = `Review cancelled by MCP request: ${args.cancelRequest.reason}`;
1587
+ await appendMarkdownLogEntry(executionPlan.error_log_path, "runner cancelled", [
1588
+ haltReason,
1589
+ `requested_at: ${args.cancelRequest.requested_at}`,
1590
+ `phase: ${args.phase}`,
1591
+ ].join("\n"));
1592
+ const executionCompletedAtMs = Date.now();
1593
+ await writeExecutionResultArtifact(executionPlan, {
1594
+ session_id: executionPlan.session_id,
1595
+ session_root: sessionRoot,
1596
+ execution_realization: executionPlan.execution_realization,
1597
+ host_runtime: executionPlan.host_runtime,
1598
+ review_mode: executionPlan.review_mode,
1599
+ execution_status: "halted_partial",
1600
+ execution_started_at: isoFromTimestamp(executionStartedAtMs),
1601
+ execution_completed_at: isoFromTimestamp(executionCompletedAtMs),
1602
+ total_duration_ms: Math.max(0, executionCompletedAtMs - executionStartedAtMs),
1603
+ max_concurrent_lenses: maxConcurrentLenses,
1604
+ observed_dispatch_width: observedDispatchWidth,
1605
+ planned_lens_ids: lensDispatches.map((dispatch) => dispatch.unit_id),
1606
+ participating_lens_ids: successfulLensDispatches.map((dispatch) => dispatch.unit_id),
1607
+ degraded_lens_ids: degradedLensIds,
1608
+ excluded_lens_ids: lensDispatches
1609
+ .map((dispatch) => dispatch.unit_id)
1610
+ .filter((lensId) => !successfulLensDispatches.some((dispatch) => dispatch.unit_id === lensId) &&
1611
+ !degradedLensIds.includes(lensId)),
1612
+ executed_lens_count: successfulLensDispatches.length,
1613
+ synthesis_executed: false,
1614
+ deliberation_status: args.deliberationStatus ?? "not_performed",
1615
+ halt_reason: haltReason,
1616
+ ...haltArtifactFields("cancellation", null),
1617
+ error_log_path: executionPlan.error_log_path,
1618
+ lens_completion_barrier_ref: executionPlan.lens_completion_barrier_path ??
1619
+ path.join(sessionRoot, "lens-completion-barrier.yaml"),
1620
+ lens_execution_results: executionOutcomes
1621
+ .filter((outcome) => outcome !== undefined)
1622
+ .map(toUnitExecutionResult),
1623
+ issue_artifact_execution_results: args.issueArtifactOutcomes?.map(toUnitExecutionResult) ?? [],
1624
+ deliberation_execution_results: args.deliberationExecutionResults?.map(toUnitExecutionResult) ?? [],
1625
+ synthesize_execution_result: args.synthesizeOutcome
1626
+ ? toUnitExecutionResult(args.synthesizeOutcome)
1627
+ : null,
1628
+ }, params.reviewExecutionProfile);
1629
+ return {
1630
+ session_root: sessionRoot,
1631
+ executed_lens_count: successfulLensDispatches.length,
1632
+ synthesis_output_path: executionPlan.synthesis_output_path,
1633
+ participating_lens_ids: successfulLensDispatches.map((dispatch) => dispatch.unit_id),
1634
+ degraded_lens_ids: degradedLensIds,
1635
+ synthesis_executed: false,
1636
+ error_log_path: executionPlan.error_log_path,
1637
+ halt_reason: haltReason,
1638
+ ...haltArtifactFields("cancellation", null),
1639
+ };
1640
+ }
1641
+ const initialCancelRequest = await readReviewCancelRequest(sessionRoot);
1642
+ if (initialCancelRequest) {
1643
+ return haltForCancellation({
1644
+ cancelRequest: initialCancelRequest,
1645
+ phase: "before_lens_dispatch",
1646
+ });
1647
+ }
1456
1648
  async function runLensWorker(workerIndex) {
1457
1649
  // Stagger initial dispatch to avoid thundering-herd on external APIs.
1458
1650
  // Only the very first dispatch of each worker is staggered; subsequent
@@ -1471,6 +1663,13 @@ export async function executeReviewPromptExecution(params) {
1471
1663
  if (!dispatch) {
1472
1664
  return;
1473
1665
  }
1666
+ if (await readReviewCancelRequest(sessionRoot)) {
1667
+ return;
1668
+ }
1669
+ if (!shouldRunUnit(dispatch.unit_id)) {
1670
+ executionOutcomes[currentIndex] = preservedOutcomeForDispatch(dispatch);
1671
+ continue;
1672
+ }
1474
1673
  console.log(`[review runner] starting ${dispatch.unit_kind}: ${dispatch.unit_id}`);
1475
1674
  await appendExecutionProgress(executionPlan.error_log_path, `runner dispatch started: ${dispatch.unit_id}`, [
1476
1675
  `unit_id: ${dispatch.unit_id}`,
@@ -1539,7 +1738,8 @@ export async function executeReviewPromptExecution(params) {
1539
1738
  }
1540
1739
  }
1541
1740
  }
1542
- if (params.reviewExecutionProfile?.mode === "nested-workers" &&
1741
+ if (!continuationMode &&
1742
+ params.reviewExecutionProfile?.mode === "nested-workers" &&
1543
1743
  params.reviewExecutionProfile.worker_executor === "codex") {
1544
1744
  console.log("[review runner] mode=nested-workers worker_executor=codex");
1545
1745
  await appendExecutionProgress(executionPlan.error_log_path, "runner profile dispatch: nested-workers", [
@@ -1611,6 +1811,13 @@ export async function executeReviewPromptExecution(params) {
1611
1811
  else {
1612
1812
  await Promise.all(Array.from({ length: Math.min(maxConcurrentLenses, lensDispatches.length) }, async (_, workerIndex) => runLensWorker(workerIndex)));
1613
1813
  }
1814
+ const postLensCancelRequest = await readReviewCancelRequest(sessionRoot);
1815
+ if (postLensCancelRequest) {
1816
+ return haltForCancellation({
1817
+ cancelRequest: postLensCancelRequest,
1818
+ phase: "after_lens_dispatch",
1819
+ });
1820
+ }
1614
1821
  const successfulLensDispatches = executionOutcomes
1615
1822
  .filter(isSuccessfulOutcome)
1616
1823
  .map((outcome) => outcome.dispatch);
@@ -1755,6 +1962,24 @@ export async function executeReviewPromptExecution(params) {
1755
1962
  };
1756
1963
  };
1757
1964
  for (const artifactId of PRE_DELIBERATION_ISSUE_ARTIFACT_IDS) {
1965
+ const cancelRequest = await readReviewCancelRequest(sessionRoot);
1966
+ if (cancelRequest) {
1967
+ return haltForCancellation({
1968
+ cancelRequest,
1969
+ phase: `before_issue_artifact:${artifactId}`,
1970
+ issueArtifactOutcomes,
1971
+ });
1972
+ }
1973
+ if (!shouldRunUnit(artifactId)) {
1974
+ issueArtifactOutcomes.push(preservedOutcomeForDispatch({
1975
+ unit_id: artifactId,
1976
+ unit_kind: "issue_artifact",
1977
+ packet_path: executionPlan.issue_artifact_prompt_packet_seats.find((seat) => seat.artifact_id === artifactId)?.packet_path ??
1978
+ path.join(executionPlan.prompt_packets_root, issueArtifactSpec(artifactId).prompt_packet_file_name),
1979
+ output_path: issueArtifactOutputPath(executionPlan, artifactId),
1980
+ }));
1981
+ continue;
1982
+ }
1758
1983
  try {
1759
1984
  issueArtifactOutcomes.push(await runIssueArtifactDispatch({
1760
1985
  projectRoot,
@@ -1778,6 +2003,14 @@ export async function executeReviewPromptExecution(params) {
1778
2003
  executionPlan,
1779
2004
  });
1780
2005
  let controlledDeliberation;
2006
+ const preDeliberationCancelRequest = await readReviewCancelRequest(sessionRoot);
2007
+ if (preDeliberationCancelRequest) {
2008
+ return haltForCancellation({
2009
+ cancelRequest: preDeliberationCancelRequest,
2010
+ phase: "before_controlled_deliberation",
2011
+ issueArtifactOutcomes,
2012
+ });
2013
+ }
1781
2014
  try {
1782
2015
  controlledDeliberation = await runControlledLensDeliberation({
1783
2016
  projectRoot,
@@ -1789,6 +2022,12 @@ export async function executeReviewPromptExecution(params) {
1789
2022
  maxConcurrentLenses,
1790
2023
  unitTimeoutMs,
1791
2024
  issueArtifactContext,
2025
+ ...(continuationMode
2026
+ ? {
2027
+ runUnitIds: continuationRunUnitIds,
2028
+ preservedResultsByUnitId: previousResultsByUnitId,
2029
+ }
2030
+ : {}),
1792
2031
  });
1793
2032
  }
1794
2033
  catch (error) {
@@ -1856,32 +2095,43 @@ export async function executeReviewPromptExecution(params) {
1856
2095
  ...haltArtifactFields("controlled_lens_deliberation", failedDeliberationOutcome),
1857
2096
  };
1858
2097
  }
1859
- try {
1860
- issueArtifactOutcomes.push(await runIssueArtifactDispatch({
1861
- projectRoot,
1862
- sessionRoot,
1863
- executionPlan,
1864
- executorConfig: teamleadExecutorConfig,
1865
- artifactId: "problem-framing",
1866
- lensOutputPaths,
1867
- deliberationResponsePaths: controlledDeliberation.deliberationDispatches.map((dispatch) => dispatch.output_path),
1868
- deliberationOutputPath: executionPlan.deliberation_output_path,
1869
- problemFramingProfileRef: await resolveProblemFramingProfileRef({
1870
- projectRoot,
1871
- executionPlan,
1872
- }),
1873
- unitTimeoutMs,
2098
+ if (!shouldRunUnit("problem-framing")) {
2099
+ issueArtifactOutcomes.push(preservedOutcomeForDispatch({
2100
+ unit_id: "problem-framing",
2101
+ unit_kind: "issue_artifact",
2102
+ packet_path: executionPlan.issue_artifact_prompt_packet_seats.find((seat) => seat.artifact_id === "problem-framing")?.packet_path ??
2103
+ path.join(executionPlan.prompt_packets_root, issueArtifactSpec("problem-framing").prompt_packet_file_name),
2104
+ output_path: executionPlan.problem_framing_path,
1874
2105
  }));
1875
2106
  }
1876
- catch (error) {
1877
- return haltAfterIssueArtifactFailure({
1878
- error,
1879
- deliberationStatus: "performed",
1880
- deliberationExecutionResults: [
1881
- ...controlledDeliberation.deliberationOutcomes,
1882
- controlledDeliberation.teamleadOutcome,
1883
- ],
1884
- });
2107
+ else {
2108
+ try {
2109
+ issueArtifactOutcomes.push(await runIssueArtifactDispatch({
2110
+ projectRoot,
2111
+ sessionRoot,
2112
+ executionPlan,
2113
+ executorConfig: teamleadExecutorConfig,
2114
+ artifactId: "problem-framing",
2115
+ lensOutputPaths,
2116
+ deliberationResponsePaths: controlledDeliberation.deliberationDispatches.map((dispatch) => dispatch.output_path),
2117
+ deliberationOutputPath: executionPlan.deliberation_output_path,
2118
+ problemFramingProfileRef: await resolveProblemFramingProfileRef({
2119
+ projectRoot,
2120
+ executionPlan,
2121
+ }),
2122
+ unitTimeoutMs,
2123
+ }));
2124
+ }
2125
+ catch (error) {
2126
+ return haltAfterIssueArtifactFailure({
2127
+ error,
2128
+ deliberationStatus: "performed",
2129
+ deliberationExecutionResults: [
2130
+ ...controlledDeliberation.deliberationOutcomes,
2131
+ controlledDeliberation.teamleadOutcome,
2132
+ ],
2133
+ });
2134
+ }
1885
2135
  }
1886
2136
  const synthesizePacketRuntimePath = path.join(executionPlan.prompt_packets_root, "synthesize.runtime.prompt.md");
1887
2137
  const synthesizePacketText = await fs.readFile(executionPlan.synthesize_prompt_packet_path, "utf8");
@@ -1905,47 +2155,68 @@ export async function executeReviewPromptExecution(params) {
1905
2155
  packet_path: synthesizePacketRuntimePath,
1906
2156
  output_path: executionPlan.synthesis_output_path,
1907
2157
  };
1908
- await emitReviewProgress({
1909
- executionPlan,
1910
- step: 12,
1911
- label: "synthesize and write execution result",
1912
- details: [`participating_lens_count=${successfulLensDispatches.length}`],
1913
- });
1914
- console.log("[review runner] starting synthesize: synthesize");
1915
- await appendExecutionProgress(executionPlan.error_log_path, "runner dispatch started: synthesize", [
1916
- `unit_id: ${synthesizeDispatch.unit_id}`,
1917
- `unit_kind: ${synthesizeDispatch.unit_kind}`,
1918
- `packet_path: ${synthesizeDispatch.packet_path}`,
1919
- `output_path: ${synthesizeDispatch.output_path}`,
1920
- ]);
2158
+ const preSynthesizeCancelRequest = await readReviewCancelRequest(sessionRoot);
2159
+ if (preSynthesizeCancelRequest) {
2160
+ return haltForCancellation({
2161
+ cancelRequest: preSynthesizeCancelRequest,
2162
+ phase: "before_synthesize",
2163
+ issueArtifactOutcomes,
2164
+ deliberationExecutionResults: [
2165
+ ...controlledDeliberation.deliberationOutcomes,
2166
+ controlledDeliberation.teamleadOutcome,
2167
+ ],
2168
+ deliberationStatus: "performed",
2169
+ });
2170
+ }
2171
+ if (shouldRunUnit("synthesize")) {
2172
+ await emitReviewProgress({
2173
+ executionPlan,
2174
+ step: 12,
2175
+ label: "synthesize and write execution result",
2176
+ details: [`participating_lens_count=${successfulLensDispatches.length}`],
2177
+ });
2178
+ console.log("[review runner] starting synthesize: synthesize");
2179
+ await appendExecutionProgress(executionPlan.error_log_path, "runner dispatch started: synthesize", [
2180
+ `unit_id: ${synthesizeDispatch.unit_id}`,
2181
+ `unit_kind: ${synthesizeDispatch.unit_kind}`,
2182
+ `packet_path: ${synthesizeDispatch.packet_path}`,
2183
+ `output_path: ${synthesizeDispatch.output_path}`,
2184
+ ]);
2185
+ }
1921
2186
  const synthesizeStartedAtMs = Date.now();
1922
2187
  const synthesizeMaxRetries = 1;
1923
2188
  let synthesizeOutcome = null;
1924
2189
  let synthesizeLastError = undefined;
1925
2190
  let synthesizeSucceeded = false;
1926
- for (let attempt = 0; attempt <= synthesizeMaxRetries; attempt++) {
1927
- try {
1928
- await invokeExecutor(synthesizeExecutorConfig, projectRoot, sessionRoot, synthesizeDispatch, unitTimeoutMs);
1929
- synthesizeSucceeded = true;
1930
- break;
1931
- }
1932
- catch (error) {
1933
- synthesizeLastError = error;
1934
- if (!isReviewUnitTimeoutError(error) && attempt < synthesizeMaxRetries) {
1935
- const retryDelay = DEFAULT_LENS_RETRY_INITIAL_DELAY_MS;
1936
- console.log(`[review runner] synthesize attempt ${attempt + 1} failed, retrying in ${retryDelay}ms...`);
1937
- await appendExecutionProgress(executionPlan.error_log_path, "runner synthesize retry", [
1938
- `attempt: ${attempt + 1}/${synthesizeMaxRetries}`,
1939
- `retry_delay_ms: ${retryDelay}`,
1940
- `error: ${error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)}`,
1941
- ]);
1942
- await sleep(retryDelay);
1943
- }
1944
- if (isReviewUnitTimeoutError(error))
2191
+ if (shouldRunUnit("synthesize")) {
2192
+ for (let attempt = 0; attempt <= synthesizeMaxRetries; attempt++) {
2193
+ try {
2194
+ await invokeExecutor(synthesizeExecutorConfig, projectRoot, sessionRoot, synthesizeDispatch, unitTimeoutMs);
2195
+ synthesizeSucceeded = true;
1945
2196
  break;
2197
+ }
2198
+ catch (error) {
2199
+ synthesizeLastError = error;
2200
+ if (!isReviewUnitTimeoutError(error) && attempt < synthesizeMaxRetries) {
2201
+ const retryDelay = DEFAULT_LENS_RETRY_INITIAL_DELAY_MS;
2202
+ console.log(`[review runner] synthesize attempt ${attempt + 1} failed, retrying in ${retryDelay}ms...`);
2203
+ await appendExecutionProgress(executionPlan.error_log_path, "runner synthesize retry", [
2204
+ `attempt: ${attempt + 1}/${synthesizeMaxRetries}`,
2205
+ `retry_delay_ms: ${retryDelay}`,
2206
+ `error: ${error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)}`,
2207
+ ]);
2208
+ await sleep(retryDelay);
2209
+ }
2210
+ if (isReviewUnitTimeoutError(error))
2211
+ break;
2212
+ }
1946
2213
  }
1947
2214
  }
1948
- if (synthesizeSucceeded) {
2215
+ else {
2216
+ synthesizeOutcome = preservedOutcomeForDispatch(synthesizeDispatch);
2217
+ synthesizeSucceeded = true;
2218
+ }
2219
+ if (synthesizeSucceeded && synthesizeOutcome === null) {
1949
2220
  synthesizeOutcome = {
1950
2221
  dispatch: synthesizeDispatch,
1951
2222
  success: true,
@@ -0,0 +1,58 @@
1
+ import fsSync from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ export function isPathInsideRoot(root, candidate) {
5
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
6
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
7
+ }
8
+ export async function realpathIfExists(targetPath) {
9
+ try {
10
+ return await fs.realpath(targetPath);
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function realpathIfExistsSync(targetPath) {
17
+ try {
18
+ return fsSync.realpathSync(targetPath);
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export async function realpathNearestExisting(targetPath) {
25
+ let current = path.resolve(targetPath);
26
+ while (true) {
27
+ const real = await realpathIfExists(current);
28
+ if (real)
29
+ return real;
30
+ const parent = path.dirname(current);
31
+ if (parent === current)
32
+ return null;
33
+ current = parent;
34
+ }
35
+ }
36
+ export function isPathInsideRootRealpathAwareSync(root, candidate) {
37
+ const realRoot = realpathIfExistsSync(root) ?? path.resolve(root);
38
+ const realCandidate = realpathIfExistsSync(candidate) ?? path.resolve(candidate);
39
+ return isPathInsideRoot(realRoot, realCandidate);
40
+ }
41
+ export async function assertPathInsideRoot(args) {
42
+ const root = path.resolve(args.root);
43
+ const candidate = path.resolve(args.candidate);
44
+ if (!isPathInsideRoot(root, candidate)) {
45
+ throw new Error(`${args.label} escapes allowed root: ${candidate}`);
46
+ }
47
+ const realRoot = (await realpathIfExists(root)) ?? root;
48
+ const realCandidate = await realpathIfExists(candidate);
49
+ if (realCandidate && !isPathInsideRoot(realRoot, realCandidate)) {
50
+ throw new Error(`${args.label} realpath escapes allowed root: ${realCandidate}`);
51
+ }
52
+ if (!realCandidate) {
53
+ const nearest = await realpathNearestExisting(path.dirname(candidate));
54
+ if (nearest && !isPathInsideRoot(realRoot, nearest)) {
55
+ throw new Error(`${args.label} parent realpath escapes allowed root: ${nearest}`);
56
+ }
57
+ }
58
+ }