@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.11

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 (43) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/build-info.json +5 -5
  3. package/dist/cli/help.js +3 -3
  4. package/dist/cli/run-cli.js +127 -8
  5. package/dist/runtime/context.js +3 -1
  6. package/dist/schemas/code-review-result.schema.json +1 -1
  7. package/dist/schemas/code-work-batch.schema.json +4 -3
  8. package/dist/schemas/code-work-result.schema.json +1 -1
  9. package/dist/schemas/harness-config.schema.json +23 -0
  10. package/dist/schemas/plan-review-decision.schema.json +1 -1
  11. package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
  12. package/dist/services/cleanup.js +18 -8
  13. package/dist/services/code-checks.js +194 -44
  14. package/dist/services/engines.js +4 -4
  15. package/dist/services/eval-snapshots.js +10 -5
  16. package/dist/services/harness-config.js +66 -0
  17. package/dist/services/hooks.js +62 -28
  18. package/dist/services/lanes.js +1 -0
  19. package/dist/services/managed-processes.js +169 -0
  20. package/dist/services/merge-server.js +8 -2
  21. package/dist/services/portable-refs.js +57 -0
  22. package/dist/services/prompts.js +4 -2
  23. package/dist/services/run-engine-bindings.js +19 -61
  24. package/dist/services/run-projection.js +10 -8
  25. package/dist/services/runs.js +71 -9
  26. package/dist/services/schema-validation.js +11 -11
  27. package/dist/services/session-identity.js +19 -0
  28. package/dist/services/sessions.js +26 -11
  29. package/dist/services/stage-lifecycle.js +15 -8
  30. package/dist/services/stage-pause.js +35 -20
  31. package/dist/services/usage.js +74 -42
  32. package/dist/services/vnext-code-review.js +82 -41
  33. package/dist/services/vnext-code.js +98 -34
  34. package/dist/services/vnext-fanout.js +5 -12
  35. package/dist/services/vnext-merge.js +144 -65
  36. package/dist/services/vnext-plan-review.js +50 -35
  37. package/dist/services/vnext-plan.js +69 -21
  38. package/dist/services/vnext-protocolize.js +6 -6
  39. package/dist/services/vnext-specify.js +6 -6
  40. package/dist/services/work-registry.js +150 -40
  41. package/dist/storage/database.js +128 -2
  42. package/package.json +1 -1
  43. package/tools/audit-runtime-fix-boundaries.mjs +96 -0
@@ -4,7 +4,7 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { AppError } from "../shared/errors.js";
6
6
  import { resolveProjectRoot } from "../storage/paths.js";
7
- import { aggregateCheckDeclarations, checkReceipts, codeExecutionEnvironment, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceFingerprint } from "./code-checks.js";
7
+ import { aggregateCheckDeclarations, checkReceipts, codeExecutionEnvironment, finalCodeCheckDeclarations, runCodeChecks, unchangedFinalGateFailures, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
8
8
  import { requireProjectByRoot } from "./projects.js";
9
9
  import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, getFlowRunVariables, gitFacts } from "./runs.js";
10
10
  import { validateSchema } from "./schema-validation.js";
@@ -53,7 +53,8 @@ export async function startVnextCode(context, input) {
53
53
  file: batch,
54
54
  projectRoot: run.workspace_root,
55
55
  ddFlowHome: context.ddFlowHome,
56
- runId: input.runId
56
+ runId: input.runId,
57
+ runRoot: home
57
58
  });
58
59
  validateWorkBatchFile(batch);
59
60
  const root = path.join(home, stageDir);
@@ -134,9 +135,23 @@ export async function finishVnextCode(context, input) {
134
135
  throw new AppError("obligation_coverage_incomplete", "CODE graph does not cover every accepted obligation", 2, { missing });
135
136
  }
136
137
  // Reject a malformed semantic receipt before running the expensive gate.
137
- const verification = verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id });
138
- if (verification.verdict !== "passed" || verification.unresolved.length > 0) {
139
- throw new AppError("code_verification_incomplete", "CODE verification is not accepted; create or complete the necessary repair Work first", 2, { verification });
138
+ const verification = verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
139
+ if (verification.verdict === "blocked") {
140
+ return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, instruction: "CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke this same stage finish command again." };
141
+ }
142
+ if (verification.verdict === "passed" && verification.unresolved.length > 0) {
143
+ throw new AppError("verification_contradictory", "A passed CODE verification cannot contain unresolved obligations", 2, { unresolved: verification.unresolved });
144
+ }
145
+ if (verification.verdict === "needs_repair") {
146
+ const repair = addVnextCodeRepair(context, {
147
+ projectRoot,
148
+ runId: run.id,
149
+ originWorkIds: works.filter((work) => work.status === "completed").map((work) => work.work_id),
150
+ semanticUnresolved: verification.unresolved.length ? verification.unresolved : [verification.summary],
151
+ verificationPath: input.verificationFile,
152
+ objective: verification.summary
153
+ });
154
+ return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, instruction: "The semantic verification is not accepted. Run the returned repair Work, then update code-verification.json and invoke this same stage finish command again." };
140
155
  }
141
156
  const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
142
157
  const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: checks });
@@ -174,18 +189,26 @@ export async function finishVnextCode(context, input) {
174
189
  }
175
190
  const allReceipts = checkReceipts(context, { projectId: project.id, runId: run.id });
176
191
  const finalReceipts = latestReceiptsByCommand(allReceipts);
192
+ const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: allReceipts, runId: run.id, runHome: home });
193
+ const unresolvedAcceptance = (projectedVerification.acceptance ?? []).filter((item) => item.status === "unresolved");
194
+ if (unresolvedAcceptance.length)
195
+ throw new AppError("code_acceptance_unresolved", "CODE cannot finish until every due acceptance criterion has current checks and evidence", 2, { unresolved: unresolvedAcceptance });
177
196
  const now = context.now();
178
197
  const timing = stageTiming(home, stage, now);
179
- const changedPaths = [...new Set(works.flatMap((work) => resultPaths(work.result)))];
198
+ const reportedPaths = [...new Set(works.flatMap((work) => resultPaths(work.result)))];
199
+ const observedChangedPaths = workspaceChangedPaths(run.workspace_root);
200
+ const changedPaths = observedChangedPaths ?? reportedPaths;
201
+ const missingReportedPaths = observedChangedPaths === null ? [] : reportedPaths.filter((item) => !observedChangedPaths.includes(item));
202
+ if (missingReportedPaths.length)
203
+ throw new AppError("changed_path_not_materialized", "CODE Work reported paths that are not changed in the accepted workspace", 2, { paths: missingReportedPaths });
180
204
  const next = nextAction(run, changedPaths);
181
- writeProtocolFlowStatus(run.workspace_root, home, run.id, next === "start_code_review" ? "CODE complete; CODE-REVIEW is next." : next === "start_merge" ? "CODE complete; MERGE is queued." : "CODE complete; this RUN reached its configured terminal boundary.");
182
205
  const report = {
183
206
  schema_id: "dd-flow/stage-report@1",
184
207
  run_id: run.id,
185
208
  stage,
186
209
  generated_at: now,
187
210
  verdict: "done",
188
- verification: verificationProjection(works, finalReceipts),
211
+ verification: projectedVerification,
189
212
  semantic: {
190
213
  result: `Completed ${works.length} CODE Work item${works.length === 1 ? "" : "s"}; ${finalReceipts.filter((receipt) => receipt.status === "passed").length} current final check receipt${finalReceipts.length === 1 ? "" : "s"} passed.`,
191
214
  acceptance: [...expected],
@@ -213,6 +236,10 @@ export async function finishVnextCode(context, input) {
213
236
  artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" },
214
237
  validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" }
215
238
  };
239
+ // Creating the MERGE request is part of accepting this terminal handoff.
240
+ // Do it before materialising a terminal stage report or changing RUN state,
241
+ // so an invalid merge policy leaves CODE safely running and retryable.
242
+ const mergeRequest = next === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
216
243
  writeReport(root, report);
217
244
  completeFlowRunStage(context, {
218
245
  projectRoot,
@@ -237,7 +264,6 @@ export async function finishVnextCode(context, input) {
237
264
  else {
238
265
  advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
239
266
  }
240
- const mergeRequest = next === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id }) : null;
241
267
  return {
242
268
  ok: true,
243
269
  run_id: run.id,
@@ -255,8 +281,8 @@ export function addVnextCodeRepair(context, input) {
255
281
  const home = requireHome(run);
256
282
  if (!input.objective.trim())
257
283
  throw new AppError("validation", "Repair objective must not be empty", 2);
258
- if (!input.checkReceiptId && !input.reviewFindingIds?.length) {
259
- throw new AppError("validation", "Repair needs a failed check receipt or CODE-REVIEW finding", 2);
284
+ if (!input.checkReceiptId && !input.reviewFindings?.length && !input.semanticUnresolved?.length) {
285
+ throw new AppError("validation", "Repair needs failed check evidence, a CODE-REVIEW finding, or an unresolved CODE verification", 2);
260
286
  }
261
287
  const receipt = input.checkReceiptId ? context.db.get("SELECT id, declaration_id, run_id, command, status, receipt_path, stdout_path, stderr_path FROM check_receipts WHERE project_id = ? AND id = ?", [
262
288
  project.id,
@@ -269,11 +295,19 @@ export function addVnextCodeRepair(context, input) {
269
295
  }
270
296
  const origins = [...new Set(input.originWorkIds)].map((id) => requireCodeWork(context, project.id, run.id, id));
271
297
  const packets = origins.map((work) => packet(work));
272
- const invariantPackets = receipt
298
+ const invariantPackets = receipt || input.semanticUnresolved?.length
273
299
  ? codeWorks(context, project.id, run.id).map((work) => packet(work)).filter((value) => !value.repair)
274
300
  : packets;
275
301
  const first = packets[0];
276
- const declaredChecks = finalCodeCheckDeclarations(run.workspace_root, packets.flatMap((value) => value.checks));
302
+ // A later repair may be caused by any accepted aggregate declaration, not
303
+ // only the checks copied into its immediate repair parent. Otherwise a
304
+ // repair that fixes one failed aggregate check can make a second, already
305
+ // declared aggregate failure impossible to repair. The immutable source
306
+ // of repair eligibility is the original CODE graph.
307
+ const declaredChecks = finalCodeCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id)
308
+ .map((work) => packet(work))
309
+ .filter((value) => !value.repair)
310
+ .flatMap((value) => value.checks));
277
311
  const failedCheck = receipt ? declaredChecks.find((check) => check.id === receipt.declaration_id) : undefined;
278
312
  if (receipt && !failedCheck) {
279
313
  throw new AppError("repair_check_declaration_missing", "Repair receipt is not backed by an accepted CODE check declaration", 2, {
@@ -281,7 +315,8 @@ export function addVnextCodeRepair(context, input) {
281
315
  declaration_id: receipt.declaration_id
282
316
  });
283
317
  }
284
- const key = input.reviewFindingIds?.length ? "code-review-repair" : "code-gate-repair";
318
+ const semanticRepair = Boolean(input.semanticUnresolved?.length);
319
+ const key = input.reviewFindings?.length ? "code-review-repair" : semanticRepair ? "code-verification-repair" : "code-gate-repair";
285
320
  const receiptWriteScope = receipt ? receiptRepairPaths(run.workspace_root, receipt) : [];
286
321
  const repair = {
287
322
  schema_id: "dd-flow/code-work-packet@5",
@@ -291,22 +326,23 @@ export function addVnextCodeRepair(context, input) {
291
326
  repair: {
292
327
  origin_work_ids: origins.map((work) => work.work_id),
293
328
  ...(receipt ? { check_receipt_id: receipt.id, failure_receipt_path: receipt.receipt_path } : {}),
294
- ...(input.reviewFindingIds?.length ? { review_finding_ids: unique(input.reviewFindingIds), review_evidence_refs: unique(input.reviewEvidenceRefs ?? []), review_check_refs: unique(input.reviewCheckRefs ?? (input.reviewChecks ?? []).map((check) => check.id)) } : {})
329
+ ...(input.reviewFindings?.length ? { review_findings: input.reviewFindings } : {}),
330
+ ...(semanticRepair ? { semantic_unresolved: unique(input.semanticUnresolved ?? []), verification_path: input.verificationPath } : {})
295
331
  },
296
332
  task: input.objective,
297
333
  semantic_spine: {
298
- user_outcome: receipt ? `Restore the accepted behavior after aggregate failure: ${input.objective}` : `Resolve accepted CODE-REVIEW finding: ${input.objective}`,
334
+ user_outcome: receipt ? `Restore the accepted behavior after aggregate failure: ${input.objective}` : semanticRepair ? `Close the unresolved CODE verification: ${input.objective}` : `Resolve accepted CODE-REVIEW finding: ${input.objective}`,
299
335
  component_responsibility: "Diagnose and repair the failed accepted CODE result without changing unrelated behavior.",
300
336
  must_preserve: unique(invariantPackets.flatMap((value) => value.semantic_spine.must_preserve)),
301
337
  non_goals: unique(invariantPackets.flatMap((value) => value.semantic_spine.non_goals)),
302
- acceptance_contribution: receipt ? `Make failed check pass: ${receipt.command}` : `Resolve CODE-REVIEW finding(s): ${input.reviewFindingIds.join(", ")}`
338
+ acceptance_contribution: receipt ? `Make failed check pass: ${receipt.command}` : semanticRepair ? `Resolve CODE verification gap(s): ${input.semanticUnresolved.join("; ")}` : `Resolve CODE-REVIEW finding(s): ${input.reviewFindings.map((finding) => finding.finding_ref).join(", ")}`
303
339
  },
304
340
  requirements: uniqueBy(invariantPackets.flatMap((value) => value.requirements), (value) => value.id),
305
341
  acceptance: uniqueBy(invariantPackets.flatMap((value) => value.acceptance), (value) => JSON.stringify(value)),
306
342
  // A CODE-REVIEW repair changes delivered code/evidence, never the
307
343
  // already accepted PLAN or its ownership declaration.
308
344
  document_updates: [],
309
- required_read: unique([...(receipt ? [receipt.receipt_path] : input.reviewEvidenceRefs ?? []), ...packets.flatMap((value) => value.required_read)]),
345
+ required_read: unique([...(receipt ? [receipt.receipt_path] : input.reviewFindings?.flatMap((finding) => finding.evidence_refs) ?? []), ...(semanticRepair && input.verificationPath ? [input.verificationPath] : []), ...packets.flatMap((value) => value.required_read)]),
310
346
  discovery_boundary: unique(packets.flatMap((value) => value.discovery_boundary)),
311
347
  // These are collision-avoidance hints. Receipt paths enrich the coordinator
312
348
  // picture but never restrict the repair's project-local edits.
@@ -314,7 +350,7 @@ export function addVnextCodeRepair(context, input) {
314
350
  // Receipts record the resolved shell command. A repair must retain the
315
351
  // accepted declaration (including its immutable @check alias) and only
316
352
  // change when it runs, so work finish can validate it again.
317
- checks: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}) }),
353
+ checks: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}), ...(semanticRepair ? { semanticChecks: declaredChecks } : {}) }),
318
354
  provides_checks: [],
319
355
  stop_conditions: unique([
320
356
  ...invariantPackets.flatMap((value) => value.stop_conditions),
@@ -339,16 +375,18 @@ export function addVnextCodeRepair(context, input) {
339
375
  type: "code_repair_created",
340
376
  work_id: id,
341
377
  origin_work_ids: input.originWorkIds,
342
- ...(receipt ? { check_receipt_id: receipt.id } : { review_finding_ids: input.reviewFindingIds })
378
+ ...(receipt ? { check_receipt_id: receipt.id } : input.reviewFindings?.length ? { review_finding_ids: input.reviewFindings.map((finding) => finding.finding_ref) } : { semantic_unresolved: input.semanticUnresolved })
343
379
  });
344
380
  refreshRunWorkProjection(context, project.id, run.id);
345
381
  return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
346
382
  }
347
- /** The repair Work runs only the check that motivated it; the stage retains the aggregate gate. */
383
+ /** Retain the causal declaration for worker context; run_at keeps aggregate gates at stage scope. */
348
384
  export function selectRepairChecks(input) {
349
- const checks = input.failedCheck
350
- ? [{ ...input.failedCheck, purpose: "Re-run the failed accepted check.", run_at: "work" }]
351
- : (input.reviewChecks ?? []).map((check) => ({ ...check, purpose: `Prove the CODE-REVIEW repair: ${check.purpose}`, run_at: "work" }));
385
+ // Gate placement belongs to the accepted PLAN. `runCodeChecks` executes only
386
+ // work-scoped entries, while retaining the aggregate declaration tells the
387
+ // worker exactly what will be rerun by the coordinator.
388
+ const candidates = input.failedCheck ? [input.failedCheck] : input.reviewChecks ?? input.semanticChecks ?? [];
389
+ const checks = candidates.map((check) => ({ ...check, purpose: `${input.semanticChecks ? "Prove the CODE verification repair" : "Prove the CODE-REVIEW repair"}: ${check.purpose}` }));
352
390
  return uniqueBy(checks, (check) => check.id);
353
391
  }
354
392
  function receiptRepairPaths(workspaceRoot, receipt) {
@@ -494,19 +532,35 @@ function processAlive(pid) {
494
532
  return false;
495
533
  }
496
534
  }
497
- export function verificationProjection(works, receipts) {
535
+ /**
536
+ * The immutable Work result is a repair history. The stage report is a current
537
+ * acceptance projection, so a registered receipt from an older check epoch
538
+ * must never stand in for the receipt that currently proves the declaration.
539
+ */
540
+ export function verificationProjection(works, receipts, options = {}) {
498
541
  const packets = works.map((work) => packet(work)).filter((value) => value !== null);
499
542
  const declarations = uniqueBy(packets.flatMap((value) => value.checks), (value) => value.id);
500
543
  const receiptByDeclaration = new Map(receipts.flatMap((receipt) => receipt.check_refs.map((ref) => [ref, receipt])));
501
- const evidence = new Map();
544
+ const rawEvidence = new Map();
502
545
  for (const work of works) {
503
546
  const result = readJsonResult(work.result);
504
547
  for (const item of result.evidence ?? [])
505
548
  if (item.criterion_id)
506
- evidence.set(item.criterion_id, unique([...(evidence.get(item.criterion_id) ?? []), ...(item.refs ?? [])]));
549
+ rawEvidence.set(item.criterion_id, unique([...(rawEvidence.get(item.criterion_id) ?? []), ...(item.refs ?? [])]));
507
550
  }
508
551
  const acceptance = uniqueBy(packets.flatMap((value) => value.acceptance), (value) => value.criterion_id);
509
- const hasEvidence = (criterionId) => (evidence.get(criterionId)?.length ?? 0) > 0;
552
+ const historicalReceiptRefs = new Set((options.historicalReceipts ?? receipts).flatMap((receipt) => receiptReferences(receipt, options)));
553
+ const acceptanceEvidence = new Map(acceptance.map((item) => {
554
+ const reported = rawEvidence.get(item.criterion_id) ?? [];
555
+ const current = unique(item.check_refs.flatMap((checkRef) => {
556
+ const receipt = receiptByDeclaration.get(checkRef);
557
+ return receipt?.status === "passed" ? [currentReceiptRef(receipt, options)] : [];
558
+ }));
559
+ const evidenceRefs = unique([...reported.filter((ref) => !historicalReceiptRefs.has(ref)), ...current]);
560
+ const historical = reported.filter((ref) => historicalReceiptRefs.has(ref) && !current.includes(ref));
561
+ return [item.criterion_id, { evidenceRefs, historical }];
562
+ }));
563
+ const hasEvidence = (criterionId) => (acceptanceEvidence.get(criterionId)?.evidenceRefs.length ?? 0) > 0;
510
564
  const isPassed = (checkId, criterionId) => receiptByDeclaration.get(checkId)?.status === "passed" || (declarations.find((check) => check.id === checkId)?.run_at === "external" && hasEvidence(criterionId));
511
565
  return {
512
566
  checks: declarations.map((check) => {
@@ -517,10 +571,20 @@ export function verificationProjection(works, receipts) {
517
571
  acceptance: acceptance.map((item) => {
518
572
  const external = item.check_refs.some((id) => declarations.find((check) => check.id === id)?.run_at === "external");
519
573
  const confirmed = item.check_refs.every((id) => isPassed(id, item.criterion_id)) && hasEvidence(item.criterion_id);
520
- return { criterion_id: item.criterion_id, gate: item.gate, status: confirmed ? (external ? "confirmed_with_external_evidence" : "confirmed") : ["work", "code", "readiness"].includes(item.gate) ? "unresolved" : "not_due", check_refs: item.check_refs, evidence_refs: evidence.get(item.criterion_id) ?? [] };
574
+ const evidence = acceptanceEvidence.get(item.criterion_id) ?? { evidenceRefs: [], historical: [] };
575
+ return { criterion_id: item.criterion_id, gate: item.gate, status: confirmed ? (external ? "confirmed_with_external_evidence" : "confirmed") : ["work", "code", "readiness"].includes(item.gate) ? "unresolved" : "not_due", check_refs: item.check_refs, evidence_refs: evidence.evidenceRefs, ...(evidence.historical.length ? { reported_evidence_refs: evidence.historical } : {}) };
521
576
  })
522
577
  };
523
578
  }
579
+ function currentReceiptRef(receipt, options) {
580
+ if (!options.runId || !options.runHome)
581
+ return receipt.receipt_path;
582
+ const relative = path.relative(options.runHome, receipt.receipt_path).split(path.sep).join("/");
583
+ return relative && !relative.startsWith("../") ? `run://${options.runId}/${relative}` : receipt.receipt_path;
584
+ }
585
+ function receiptReferences(receipt, options) {
586
+ return unique([receipt.receipt_path, currentReceiptRef(receipt, options)]);
587
+ }
524
588
  function readJsonResult(value) { try {
525
589
  const parsed = JSON.parse(value ?? "{}");
526
590
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
@@ -572,7 +636,7 @@ function requireRootWork(context, projectId, runId) {
572
636
  return work;
573
637
  }
574
638
  function requireRun(context, projectId, runId) {
575
- const run = context.db.get("SELECT id, project_id, run_home_path, workspace_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
639
+ const run = context.db.get("SELECT id, project_id, run_root, workspace_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
576
640
  if (!run)
577
641
  throw new AppError("not_found", "RUN is not registered", 1);
578
642
  return run;
@@ -630,16 +694,16 @@ function readReadiness(file) {
630
694
  }
631
695
  }
632
696
  function requireHome(run) {
633
- if (!run.run_home_path)
634
- throw new AppError("runtime_missing", "RUN workspace is unavailable", 1);
635
- return run.run_home_path;
697
+ if (!run.run_root)
698
+ throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1);
699
+ return run.run_root;
636
700
  }
637
701
  function finishCommand(context, runId, projectRoot, verificationFile) {
638
702
  return `${flowCommand(context)} stage finish ${runId} --stage code --verification-file ${JSON.stringify(verificationFile)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`;
639
703
  }
640
704
  function readVerification(context, input) {
641
705
  const file = path.resolve(input.file);
642
- validateSchema({ schemaName: "code-verification", file, projectRoot: input.projectRoot, ddFlowHome: context.ddFlowHome, runId: input.runId });
706
+ validateSchema({ schemaName: "code-verification", file, projectRoot: input.projectRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.runRoot });
643
707
  return JSON.parse(fs.readFileSync(file, "utf8"));
644
708
  }
645
709
  function verificationForFinish(context, input) {
@@ -7,12 +7,6 @@ import { getFlowRunVariables } from "./runs.js";
7
7
  import { listWorks } from "./work-registry.js";
8
8
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
9
9
  export const subagentCapacityKey = "runtime.subagent.available_slots";
10
- export const capacityProbe = {
11
- fanout_size: 15,
12
- probe_hold_seconds: 60,
13
- cleanup_deadline_seconds: 180,
14
- completion_token: "AGENT-NN"
15
- };
16
10
  export function fanoutState(input) {
17
11
  if (!input.hasWork)
18
12
  return input.dispatch === "none" ? "coordinator_required" : "dispatch_required";
@@ -42,10 +36,10 @@ export function readFanoutDescriptor(stageRoot) {
42
36
  export function getVnextFanoutStatus(context, input) {
43
37
  const projectRoot = resolveProjectRoot(input.projectRoot);
44
38
  const project = requireProjectByRoot(context, projectRoot);
45
- const run = context.db.get("SELECT run_home_path FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
46
- if (!run?.run_home_path)
47
- throw new AppError("not_found", "RUN workspace is unavailable", 1, { run_id: input.runId });
48
- const stageRoot = path.join(run.run_home_path, vnextStageDirectory(input.stage));
39
+ const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
40
+ if (!run?.run_root)
41
+ throw new AppError("not_found", "RUN artifact root is unavailable", 1, { run_id: input.runId });
42
+ const stageRoot = path.join(run.run_root, vnextStageDirectory(input.stage));
49
43
  const descriptor = readFanoutDescriptor(stageRoot);
50
44
  if (!descriptor)
51
45
  return { ok: true, run_id: input.runId, stage: input.stage, orchestration: null };
@@ -70,8 +64,7 @@ export function getVnextFanoutStatus(context, input) {
70
64
  state: fanoutState({ dispatch: descriptor.dispatch, hasWork, capacityRequired: descriptor.capacity_required, capacityKnown, created: counts.created ?? 0, running: counts.running ?? 0 }),
71
65
  capacity: {
72
66
  run_key: subagentCapacityKey,
73
- available_slots: capacityKnown ? available : null,
74
- ...(descriptor.capacity_required && !capacityKnown ? { probe: capacityProbe } : {})
67
+ available_slots: capacityKnown ? available : null
75
68
  },
76
69
  works: { ...counts, ready: ready.works.map((work) => ({ work_id: work.work_id, task: work.task, start_command: work.start_command })) }
77
70
  }