@deksden-com/dd-flow-cli 0.9.0-beta.127 → 0.9.0-beta.133

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 (53) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/build-info.json +6 -6
  3. package/dist/cli/command-inputs.js +2 -2
  4. package/dist/cli/run-cli.js +6 -6
  5. package/dist/cli/runtime-maintenance-diagnostics.js +1 -1
  6. package/dist/cli/runtime-maintenance-ingress.js +5 -4
  7. package/dist/cli/runtime-process.js +27 -2
  8. package/dist/cli.js +7 -1
  9. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -3
  10. package/dist/harness-runtime/lib/dd-agy.mjs +4 -3
  11. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +2 -1
  12. package/dist/harness-runtime/lib/dd-codex.mjs +4 -1
  13. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  14. package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
  15. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -1
  16. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
  17. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +4 -3
  18. package/dist/harness-runtime/lib/dd-zcode.mjs +13 -4
  19. package/dist/harness-runtime/lib/lease-renewal.d.mts +5 -5
  20. package/dist/harness-runtime/lib/lease-renewal.mjs +18 -5
  21. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  22. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  23. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -1
  24. package/dist/harness-runtime/lib/native-hook-command.mjs +13 -5
  25. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  26. package/dist/harness-runtime/lib/process-json.mjs +41 -9
  27. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  28. package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
  29. package/dist/services/branch-context.js +3 -3
  30. package/dist/services/check-runner-evidence.js +121 -0
  31. package/dist/services/cleanup.js +1 -1
  32. package/dist/services/code-checks.js +113 -31
  33. package/dist/services/compatibility-preflight.js +9 -2
  34. package/dist/services/dashboard.js +33 -26
  35. package/dist/services/engines.js +88 -57
  36. package/dist/services/lifecycle-invocations.js +4 -2
  37. package/dist/services/managed-lease-monitor.js +9 -4
  38. package/dist/services/prompts.js +3 -1
  39. package/dist/services/protocols.js +3 -1
  40. package/dist/services/repair-intents.js +4 -2
  41. package/dist/services/run-control-worker.js +2 -1
  42. package/dist/services/run-controller.js +20 -17
  43. package/dist/services/run-fork.js +2 -2
  44. package/dist/services/runtime-scope-worker.js +2 -2
  45. package/dist/services/stage-lifecycle.js +13 -7
  46. package/dist/services/tooling.js +32 -23
  47. package/dist/services/vnext-code-review.js +2 -0
  48. package/dist/services/vnext-code.js +5 -1
  49. package/dist/services/vnext-merge.js +4 -1
  50. package/dist/services/vnext-protocolize.js +17 -16
  51. package/dist/services/work-registry.js +3 -1
  52. package/dist/services/worktrees.js +16 -14
  53. package/package.json +1 -1
@@ -2,7 +2,8 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { spawnSync } from "node:child_process";
5
+ import { commandResult, commandText } from '../harness-runtime/lib/process-json.mjs';
6
+ import { publishSetup, SETUP_STARTUP_MS } from '../harness-runtime/lib/setup-progress.mjs';
6
7
  import { AppError } from "../shared/errors.js";
7
8
  import { ensureDir } from "../storage/paths.js";
8
9
  const manifestRelativePath = path.join("tools", "worktrunk-manifest.json");
@@ -71,7 +72,7 @@ export function inspectManagedWorktrunk(context) {
71
72
  };
72
73
  }
73
74
  }
74
- export function ensureManagedWorktrunk(context) {
75
+ export async function ensureManagedWorktrunk(context) {
75
76
  const manifest = worktrunkManifest();
76
77
  const platform = worktrunkPlatform();
77
78
  const cachedRoot = worktrunkCacheRoot(context, platform);
@@ -92,14 +93,14 @@ export function ensureManagedWorktrunk(context) {
92
93
  next_action: "Place the verified official artifact in the managed tool cache and retry."
93
94
  });
94
95
  }
95
- installWorktrunkArtifact(manifest, platform, cachedRoot);
96
+ await installWorktrunkArtifact(manifest, platform, cachedRoot);
96
97
  return verifyCachedWorktrunk(context, manifest, platform);
97
98
  }
98
99
  finally {
99
100
  fs.rmSync(lockDir, { recursive: true, force: true });
100
101
  }
101
102
  }
102
- function installWorktrunkArtifact(manifest, platform, targetRoot) {
103
+ async function installWorktrunkArtifact(manifest, platform, targetRoot) {
103
104
  const artifact = artifactFor(manifest, platform);
104
105
  const parent = path.dirname(targetRoot);
105
106
  ensureDir(parent);
@@ -107,8 +108,17 @@ function installWorktrunkArtifact(manifest, platform, targetRoot) {
107
108
  const archivePath = path.join(tempRoot, `worktrunk.${artifact.archive}`);
108
109
  const extractedRoot = path.join(tempRoot, "extracted");
109
110
  try {
110
- const download = spawnSync("curl", ["--fail", "--location", "--silent", "--show-error", "--max-time", "120", "-o", archivePath, artifact.url], { encoding: "utf8" });
111
- if (download.status !== 0) {
111
+ let observedBytes = 0;
112
+ publishSetup('probe_started', 'worktrunk-download');
113
+ const download = await commandResult("curl", ["--fail", "--location", "--silent", "--show-error", "-o", archivePath, artifact.url], { startupMs: SETUP_STARTUP_MS, progress: () => {
114
+ const bytes = fs.existsSync(archivePath) ? fs.statSync(archivePath).size : 0;
115
+ if (bytes > observedBytes) {
116
+ observedBytes = bytes;
117
+ publishSetup('transfer_progress', `worktrunk-transfer:${bytes}`);
118
+ }
119
+ return observedBytes;
120
+ } });
121
+ if (download.exit_code !== 0) {
112
122
  throw new AppError("tool_unavailable", "Unable to download the pinned Worktrunk artifact", 1, {
113
123
  tool: manifest.tool,
114
124
  version: manifest.version,
@@ -118,6 +128,7 @@ function installWorktrunkArtifact(manifest, platform, targetRoot) {
118
128
  stderr: sanitize(download.stderr)
119
129
  });
120
130
  }
131
+ publishSetup('probe_completed', 'worktrunk-download');
121
132
  const archiveChecksum = sha256File(archivePath);
122
133
  if (archiveChecksum !== artifact.sha256) {
123
134
  throw new AppError("tool_integrity_failed", "Pinned Worktrunk artifact checksum mismatch", 1, {
@@ -129,12 +140,12 @@ function installWorktrunkArtifact(manifest, platform, targetRoot) {
129
140
  });
130
141
  }
131
142
  ensureDir(extractedRoot);
132
- extractArchive(archivePath, extractedRoot, artifact.archive);
143
+ await extractArchive(archivePath, extractedRoot, artifact.archive);
133
144
  const extractedBinary = findBinary(extractedRoot);
134
145
  if (!extractedBinary)
135
146
  throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive does not contain wt", 1, { platform });
136
147
  fs.chmodSync(extractedBinary, 0o755);
137
- verifyWorktrunkVersion(extractedBinary, manifest.version);
148
+ await verifyWorktrunkVersion(extractedBinary, manifest.version);
138
149
  const binaryChecksum = sha256File(extractedBinary);
139
150
  const stagedRoot = path.join(tempRoot, "managed");
140
151
  ensureDir(stagedRoot);
@@ -186,7 +197,7 @@ function verifyCachedWorktrunk(context, manifest, platform) {
186
197
  actual_binary_sha256: actualBinaryChecksum
187
198
  });
188
199
  }
189
- verifyWorktrunkVersion(bin, manifest.version);
200
+ // The admitted binary checksum binds the version probe performed at install.
190
201
  return {
191
202
  bin,
192
203
  version: manifest.version,
@@ -195,23 +206,23 @@ function verifyCachedWorktrunk(context, manifest, platform) {
195
206
  binary_sha256: actualBinaryChecksum
196
207
  };
197
208
  }
198
- function extractArchive(archive, destination, format) {
209
+ async function extractArchive(archive, destination, format) {
199
210
  if (format === "tar.xz") {
200
- const listing = spawnSync("tar", ["-tJf", archive], { encoding: "utf8" });
201
- if (listing.status !== 0 || listing.stdout.split("\n").some(unsafeArchivePath)) {
211
+ const listing = await commandResult("tar", ["-tJf", archive], { startupMs: SETUP_STARTUP_MS });
212
+ if (listing.exit_code !== 0 || listing.stdout.split("\n").some(unsafeArchivePath)) {
202
213
  throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive has an unsafe or unreadable file list", 1);
203
214
  }
204
- const extracted = spawnSync("tar", ["-xJf", archive, "-C", destination], { encoding: "utf8" });
205
- if (extracted.status !== 0)
215
+ const extracted = await commandResult("tar", ["-xJvf", archive, "-C", destination], { startupMs: SETUP_STARTUP_MS });
216
+ if (extracted.exit_code !== 0)
206
217
  throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive extraction failed", 1, { stderr: sanitize(extracted.stderr) });
207
218
  return;
208
219
  }
209
- const listing = spawnSync("unzip", ["-Z1", archive], { encoding: "utf8" });
210
- if (listing.status !== 0 || listing.stdout.split("\n").some(unsafeArchivePath)) {
220
+ const listing = await commandResult("unzip", ["-Z1", archive], { startupMs: SETUP_STARTUP_MS });
221
+ if (listing.exit_code !== 0 || listing.stdout.split("\n").some(unsafeArchivePath)) {
211
222
  throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive has an unsafe or unreadable file list", 1);
212
223
  }
213
- const extracted = spawnSync("unzip", ["-q", archive, "-d", destination], { encoding: "utf8" });
214
- if (extracted.status !== 0)
224
+ const extracted = await commandResult("unzip", [archive, "-d", destination], { startupMs: SETUP_STARTUP_MS });
225
+ if (extracted.exit_code !== 0)
215
226
  throw new AppError("tool_integrity_failed", "Pinned Worktrunk archive extraction failed", 1, { stderr: sanitize(extracted.stderr) });
216
227
  }
217
228
  function findBinary(root) {
@@ -229,13 +240,11 @@ function findBinary(root) {
229
240
  }
230
241
  return null;
231
242
  }
232
- function verifyWorktrunkVersion(bin, expected) {
233
- const result = spawnSync(bin, ["--version"], { encoding: "utf8" });
234
- const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
235
- if (result.status !== 0 || !output.includes(expected)) {
243
+ async function verifyWorktrunkVersion(bin, expected) {
244
+ const output = await commandText(bin, ['--version'], { startupMs: SETUP_STARTUP_MS, phase: 'native-version' });
245
+ if (!output.includes(expected)) {
236
246
  throw new AppError("tool_integrity_failed", "Managed Worktrunk version verification failed", 1, {
237
247
  expected_version: expected,
238
- status: result.status,
239
248
  output: sanitize(output)
240
249
  });
241
250
  }
@@ -4,6 +4,7 @@ import crypto from "node:crypto";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { AppError } from "../shared/errors.js";
7
+ import { assertSourceRepairEligible } from "./check-runner-evidence.js";
7
8
  import { resolveProjectRoot } from "../storage/paths.js";
8
9
  import { requireProjectByRoot } from "./projects.js";
9
10
  import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, prepareFlowRunStageCompletion, freezeFlowRunReviewMode, gitFacts } from "./runs.js";
@@ -243,6 +244,7 @@ export async function finishVnextCodeReview(context, input) {
243
244
  const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", receipts, semanticFile: decisionFile });
244
245
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
245
246
  if (failed.length) {
247
+ assertSourceRepairEligible(failed);
246
248
  context.db.writeTransaction(() => acceptVerificationMatrix(context, matrix));
247
249
  publishVerificationMatrix(context, matrix);
248
250
  throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
@@ -7,8 +7,9 @@ import fs from "node:fs";
7
7
  import { prepareAgentOutputFile } from "../storage/agent-output.js";
8
8
  import path from "node:path";
9
9
  import { AppError } from "../shared/errors.js";
10
+ import { assertSourceRepairEligible } from "./check-runner-evidence.js";
10
11
  import { resolveProjectRoot } from "../storage/paths.js";
11
- import { aggregateCheckDeclarations, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
12
+ import { aggregateCheckDeclarations, checkReceipts, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
12
13
  import { requireProjectByRoot } from "./projects.js";
13
14
  import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, prepareFlowRunStageAttachment, completeFlowRunStage, prepareFlowRunStageCompletion, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
14
15
  import { validateSchema } from "./schema-validation.js";
@@ -224,6 +225,7 @@ export async function finishVnextCode(context, input) {
224
225
  const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", receipts, semanticFile: input.verificationFile });
225
226
  const matrixInstruction = matrix ? verificationMatrixInstruction(matrix.metadata, home, "semantic verification in code-verification.json") : "";
226
227
  if (failed.length) {
228
+ assertSourceRepairEligible(failed);
227
229
  context.db.writeTransaction(() => acceptVerificationMatrix(context, matrix));
228
230
  publishVerificationMatrix(context, matrix);
229
231
  const continuation = registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks, generation });
@@ -406,6 +408,8 @@ export function prepareVnextCodeRepair(context, input) {
406
408
  check_receipt_id: input.checkReceiptId
407
409
  });
408
410
  }
411
+ if (input.checkReceiptId)
412
+ assertSourceRepairEligible(checkReceipts(context, { projectId: project.id, runId: run.id }).filter(item => item.id === input.checkReceiptId));
409
413
  const origins = [...new Set(input.originWorkIds)].map((id) => requireCodeWork(context, project.id, run.id, id));
410
414
  const packets = origins.map((work) => packet(work));
411
415
  const invariantPackets = receipt || input.semanticUnresolved?.length
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import { execFileSync, spawnSync } from "node:child_process";
8
8
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
9
9
  import { AppError } from "../shared/errors.js";
10
+ import { assertSourceRepairEligible, checkRepairDisposition } from "./check-runner-evidence.js";
10
11
  import { resolveProjectRoot } from "../storage/paths.js";
11
12
  import { requireProjectByRoot } from "./projects.js";
12
13
  import { nextMergeRequestId } from "./ids.js";
@@ -314,7 +315,8 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
314
315
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
315
316
  if (failed.length) {
316
317
  context.db.run("UPDATE merge_requests SET status = 'action_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_gate_failed", failures: failed }), context.now(), request.merge_request_id]);
317
- throw new AppError("merge_gate_failed", "Integrated target checks failed. Classify retained evidence: source defects use source repair; restored environment uses retry.", 2, { failures: failed, repair_command: repairCommand(context, request, projectRoot), continuation: { ...recoveryEvidenceInput(run.run_root, failed[0].id), command_after_input: finishCommand(context, run.id, request, projectRoot, failed[0].id) } });
318
+ const sourceRepair = failed.every(receipt => checkRepairDisposition(receipt) === "source_repair");
319
+ throw new AppError("merge_gate_failed", "Integrated target checks failed. Only assertion-backed failures permit source repair; tooling or unknown evidence requires diagnosis/settlement first.", 2, { failures: failed, ...(sourceRepair ? { repair_command: repairCommand(context, request, projectRoot) } : { outcome: "blocked", recoverable: false }), continuation: { ...recoveryEvidenceInput(run.run_root, failed[0].id), command_after_input: finishCommand(context, run.id, request, projectRoot, failed[0].id) } });
318
320
  }
319
321
  const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
320
322
  const missingRefs = frozenGateRefs(gate).filter((ref) => !passedRefs.has(ref));
@@ -476,6 +478,7 @@ export async function repairVnextMerge(context, input) {
476
478
  if (request.project_id !== project.id || request.status !== "action_required" || errorCode(request) !== "merge_gate_failed") {
477
479
  throw new AppError("invalid_merge_repair_state", "MERGE source repair requires an action-required failed integration gate", 2, { merge_request_id: input.requestId, status: request.status, code: errorCode(request) });
478
480
  }
481
+ assertSourceRepairEligible(checkReceipts(context, { projectId: project.id, runId: request.run_id }).filter(receipt => failedReceiptIds(request).includes(receipt.id)));
479
482
  abortIntegration(request);
480
483
  const originWorkIds = sourceCodeWorkIds(context, request.project_id, request.run_id);
481
484
  if (!originWorkIds.length)
@@ -71,10 +71,10 @@ export function prepareVnextProtocolizeArtifacts(context, input) {
71
71
  return { projectRoot, project, run, work, stageRoot, specifyPath, specifyMarkdownPath, obligations, resultPath, promptPath, template, projectCatalog, workspace };
72
72
  }
73
73
  /** Materialize the next-stage packet; this is execution, not read-only CLI preparation. */
74
- export function prepareVnextProtocolize(context, input, prepared = prepareVnextProtocolizeArtifacts(context, input)) {
74
+ export async function prepareVnextProtocolize(context, input, prepared = prepareVnextProtocolizeArtifacts(context, input)) {
75
75
  const { projectRoot, project, run, work, stageRoot, specifyPath, specifyMarkdownPath, obligations, resultPath, promptPath, template, projectCatalog } = prepared;
76
76
  fs.mkdirSync(stageRoot, { recursive: true });
77
- const policy = provisionWorkspaceForProtocolize(context, projectRoot, run, prepared.workspace);
77
+ const policy = await provisionWorkspaceForProtocolize(context, projectRoot, run, prepared.workspace);
78
78
  const routedRun = requireRun(context, projectRoot, run.id);
79
79
  writeJson(path.join(stageRoot, "work-context.json"), { schema_id: "dd-flow/vnext-protocolize-context@2", system: { run_id: run.id, work_id: work.work_id, stage, handoff: input.handoff }, input: { accepted_specify_json: specifyPath, accepted_specify_markdown: specifyMarkdownPath }, catalog: projectCatalog, workspace: { project_root: projectRoot, workspace_root: routedRun.workspace_root, stage_root: stageRoot, git_policy: policy } });
80
80
  if (!fs.existsSync(resultPath))
@@ -138,7 +138,7 @@ function readProtocolizeContext(file) {
138
138
  catch { /* retained context is not a caller argument */ }
139
139
  throw new AppError("runtime_context_invalid", "PROTOCOLIZE context is missing a valid frozen handoff", 1, { file });
140
140
  }
141
- export function startVnextProtocolize(context, input, prepared = prepareVnextProtocolizeStart(context, input)) {
141
+ export async function startVnextProtocolize(context, input, prepared = prepareVnextProtocolizeStart(context, input)) {
142
142
  const { projectRoot, project, run, work, protocolizeRoot, contextPath, packetInput, packet, stageAttached, mode, promptPath } = prepared;
143
143
  assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
144
144
  const previous = context.db.get("SELECT ws.session_id, s.stop_reason FROM work_sessions ws LEFT JOIN sessions s ON s.project_id = ? AND s.session_id = ws.session_id WHERE ws.work_id = ? ORDER BY ws.created_at DESC LIMIT 1", [project.id, work.work_id]);
@@ -153,7 +153,7 @@ export function startVnextProtocolize(context, input, prepared = prepareVnextPro
153
153
  if (mode === "new_session" && previous?.session_id === sessionId)
154
154
  throw new AppError("handoff_session_mismatch", "Project policy requires PROTOCOLIZE to start in a fresh session", 1);
155
155
  if (packet)
156
- prepareVnextProtocolize(context, packetInput, packet);
156
+ await prepareVnextProtocolize(context, packetInput, packet);
157
157
  const frozenContext = prepared.frozenContext ?? readProtocolizeContext(contextPath);
158
158
  const externalContext = applyExternalStageContext({ context, stageRoot: protocolizeRoot, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
159
159
  const binding = bindStageCoordinatorWork(context, { workId: work.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(requireRunHome(run), "02-protocolize", "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
@@ -178,7 +178,7 @@ function specifyHandoff(file, workId) {
178
178
  catch { /* reported below */ }
179
179
  throw new AppError("runtime_context_invalid", "SPECIFY Work is missing its immutable handoff policy", 1, { work_id: workId });
180
180
  }
181
- export function finishVnextProtocolize(context, input) {
181
+ export async function finishVnextProtocolize(context, input) {
182
182
  const projectRoot = resolveProjectRoot(input.projectRoot);
183
183
  const project = requireProjectByRoot(context, projectRoot);
184
184
  const run = requireRun(context, projectRoot, input.runId);
@@ -205,7 +205,7 @@ export function finishVnextProtocolize(context, input) {
205
205
  const resolvedPolicy = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, stage: "protocolize" });
206
206
  const receipt = readWorkspaceRouteReceipt(root);
207
207
  const workspaceRoot = run.workspace_root;
208
- const intent = prepareProtocolizeIntent(context, run, resultFile, result, obligations);
208
+ const intent = await prepareProtocolizeIntent(context, run, resultFile, result, obligations);
209
209
  publishProtocolizeIntent(context, project.id, intent);
210
210
  const protocolIds = intent.protocol_ids;
211
211
  const changedFiles = intent.changed_files;
@@ -246,7 +246,7 @@ export function finishVnextProtocolize(context, input) {
246
246
  const next = stageSuccessor(context, { projectRoot, runId: run.id, stage: "plan", command: `${flowCommand(context)} stage start ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json`, metadata: { cwd: workspaceRoot, session_mode: resolvedPolicy.stage_session_mode } });
247
247
  return { ok: true, outcome: "protocolized", run_id: run.id, protocol_ids: protocolIds, workspace_root: workspaceRoot, git_policy: resolvedPolicy, next_action: "await_plan", ...(next.command ? { next_command: next.command } : {}), next, artifacts: { result: resultFile, report_json: reportJson, report_markdown: reportMarkdown, report_html: reportHtml, protocols: protocolIds.map((id) => path.join(workspaceRoot, ".memory-bank", "protocol", id, "summary.md")) } };
248
248
  }
249
- function prepareProtocolizeIntent(context, run, resultFile, result, obligations) {
249
+ async function prepareProtocolizeIntent(context, run, resultFile, result, obligations) {
250
250
  const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.id]);
251
251
  const index = row ? JSON.parse(row.index_json) : null;
252
252
  const attempt = index?.stage_runs?.find(item => item.stage === stage)?.attempt;
@@ -270,11 +270,12 @@ function prepareProtocolizeIntent(context, run, resultFile, result, obligations)
270
270
  ? { ...result.feature, epic_path: path.join(draft, path.relative(projectRoot, result.feature.epic_path)) }
271
271
  : result.feature;
272
272
  const draftResult = { ...result, feature };
273
- const changedFiles = materialize(draft, run.id, requireRunHome(run), draftResult, obligations, protocolIds, psetId);
273
+ const materialized = await materialize(draft, run.id, requireRunHome(run), draftResult, obligations, protocolIds, psetId);
274
+ const changedFiles = materialized.files;
274
275
  const files = changedFiles.map(relative => {
275
- const source = path.join(projectRoot, relative), target = path.join(draft, relative);
276
+ const target = path.join(draft, relative);
276
277
  const bytes = fs.readFileSync(target, "utf8");
277
- return { relative, before_sha256: fs.existsSync(source) ? sha256(fs.readFileSync(source)) : null, after_sha256: sha256(bytes), bytes };
278
+ return { relative, before_sha256: materialized.before[path.relative(draft, target)] ?? null, after_sha256: sha256(bytes), bytes };
278
279
  });
279
280
  const candidate = { run_id: run.id, attempt, workspace_root: projectRoot, result_sha256: resultSha, protocol_ids: protocolIds, pset_id: psetId, changed_files: changedFiles, files };
280
281
  return context.db.writeTransaction(() => {
@@ -366,7 +367,7 @@ function ensureManagedWorktreeRecorded(context, input) {
366
367
  recordManagedWorktree(context, input);
367
368
  }
368
369
  function sha256(bytes) { return crypto.createHash("sha256").update(bytes).digest("hex"); }
369
- function materialize(projectRoot, runId, runHome, result, obligations, ids, psetId) {
370
+ async function materialize(projectRoot, runId, runHome, result, obligations, ids, psetId) {
370
371
  const psetPath = psetId ? path.join(projectRoot, ".memory-bank", "protocol", "_set", `${psetId}.md`) : null;
371
372
  const created = [];
372
373
  const changed = new Set();
@@ -404,8 +405,8 @@ function materialize(projectRoot, runId, runHome, result, obligations, ids, pset
404
405
  const absolute = path.resolve(projectRoot, file);
405
406
  return created.some((directory) => absolute.startsWith(`${directory}${path.sep}`)) || absolute === psetPath;
406
407
  });
407
- runTargetedMemoryBankLint(projectRoot, materializedFiles);
408
- return files;
408
+ await runTargetedMemoryBankLint(projectRoot, materializedFiles);
409
+ return { files, before: Object.fromEntries([...originals].map(([file, bytes]) => [path.relative(projectRoot, file), bytes === null ? null : sha256(bytes)])) };
409
410
  }
410
411
  catch (error) {
411
412
  for (const [file, content] of originals) {
@@ -670,7 +671,7 @@ function prepareWorkspaceForProtocolize(context, projectRoot, projectId, run, st
670
671
  prepareManagedWorktreeBootstrapSources(projectRoot);
671
672
  return { kind: "feature", receiptPath, config, stableFacts, sessionMode, branch, create };
672
673
  }
673
- function provisionWorkspaceForProtocolize(context, projectRoot, run, prepared) {
674
+ async function provisionWorkspaceForProtocolize(context, projectRoot, run, prepared) {
674
675
  if (prepared.kind === "retained")
675
676
  return prepared.policy;
676
677
  const { receiptPath } = prepared;
@@ -679,8 +680,8 @@ function provisionWorkspaceForProtocolize(context, projectRoot, run, prepared) {
679
680
  return prepared.policy;
680
681
  }
681
682
  const { config, stableFacts, sessionMode, branch, create } = prepared;
682
- const created = createManagedWorktree(context, create, create);
683
- const bootstrap = bootstrapManagedWorktree(context, { projectRoot, worktreePath: created.worktreePath });
683
+ const created = await createManagedWorktree(context, create, create);
684
+ const bootstrap = await bootstrapManagedWorktree(context, { projectRoot, worktreePath: created.worktreePath });
684
685
  rebindFlowRunWorkspace(context, { projectRoot, runId: run.id, workspaceRoot: created.worktreePath, reason: "PROTOCOLIZE start provisioned the configured feature-worktree route" });
685
686
  const policy = { route: "feature_worktree", integration_branch: config.workspace.integration_branch, base_ref: stableFacts.head, feature_branch: branch, worktree_path: created.worktreePath, reason: "Project workspace configuration requires a managed feature worktree before durable protocol documents exist.", provision_stage: "protocolize_start", stage_session_mode: sessionMode, provisioned: true, bootstrap_status: bootstrap.status };
686
687
  writeJson(receiptPath, { schema_id: "dd-flow/vnext-workspace-route@1", policy, created, bootstrap });
@@ -12,7 +12,7 @@ import { validateSchema } from "./schema-validation.js";
12
12
  import { resolveProjectRoot, resolveRunReferences } from "../storage/paths.js";
13
13
  import { prepareAgentOutputFile } from "../storage/agent-output.js";
14
14
  import { refreshRunSessionProjection } from "./run-projection.js";
15
- import { ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateCheckDeclaration, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
15
+ import { assertSourceRepairEligible, ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateCheckDeclaration, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
16
16
  import { nextWorkId, nextWorkIds } from "./ids.js";
17
17
  import { appendFlowRunTimelineEvent } from "./runs.js";
18
18
  import { resolveStageTransition } from "./execution-policy.js";
@@ -1104,6 +1104,8 @@ async function settle(context, id, status, result, progress, retry) {
1104
1104
  validateFailedCheckRetry(context, { projectId: work.project_id, runId: work.run_id, receiptId: retry.receiptId, declarations: requiredChecks, reason: retry.reason });
1105
1105
  receipts = await runCodeChecks(context, { projectId: work.project_id, runId: work.run_id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, workId: work.work_id, artifactDir, scope: "work", checks: requiredChecks, ...(retry ? { retryReceiptId: retry.receiptId, retryReason: retry.reason } : {}), ...(progress ? { progress } : {}) });
1106
1106
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
1107
+ if (failed.length)
1108
+ assertSourceRepairEligible(failed);
1107
1109
  if (failed.length)
1108
1110
  throw new AppError("work_checks_failed", "Work remains running because required checks failed", 2, { work_id: id, failures: failed, all_receipts: receipts });
1109
1111
  }
@@ -10,6 +10,8 @@ import { persistProtocolState, readProtocolRuntimeState, requireProtocol } from
10
10
  import { requireProjectByRoot } from "./projects.js";
11
11
  import { ensureManagedWorktrunk, inspectManagedWorktrunk } from "./tooling.js";
12
12
  import { propagateOwnedWorkspaceHooks } from "./workspace-files.js";
13
+ import { commandResult } from '../harness-runtime/lib/process-json.mjs';
14
+ import { SETUP_STARTUP_MS } from '../harness-runtime/lib/setup-progress.mjs';
13
15
  export function planWorktree(context, input) {
14
16
  const protocol = scopedProtocol(context, input.projectRoot, input.protocolId);
15
17
  const state = readProtocolRuntimeState(context, protocol).state;
@@ -40,11 +42,11 @@ export function prepareWorktreeCreate(context, input) {
40
42
  }
41
43
  return prepareManagedWorktree({ ...input, projectRoot: protocol.project_root });
42
44
  }
43
- export function createWorktreeRecord(context, input, prepared = prepareWorktreeCreate(context, input)) {
45
+ export async function createWorktreeRecord(context, input, prepared = prepareWorktreeCreate(context, input)) {
44
46
  const protocol = scopedProtocol(context, input.projectRoot, input.protocolId);
45
47
  if (activeWorktreeRecord(context, protocol.project_id, protocol.id))
46
48
  throw new AppError("worktree_already_exists", "Protocol already has an active worktree record", 1);
47
- const created = createManagedWorktree(context, prepared, prepared);
49
+ const created = await createManagedWorktree(context, prepared, prepared);
48
50
  recordManagedWorktree(context, { protocolId: protocol.id, projectRoot: protocol.project_root, branch: prepared.branch, base: prepared.base, created });
49
51
  return { ok: true, worktree: activeWorktreeRecord(context, protocol.project_id, protocol.id) };
50
52
  }
@@ -82,13 +84,13 @@ export function prepareManagedWorktree(input) {
82
84
  return { projectRoot, branch: input.branch, base: base.stdout.trim(), path: destination };
83
85
  }
84
86
  /** Creates the checkout before a PRT exists. PROTOCOLIZE materializes the PRT only inside this checkout. */
85
- export function createManagedWorktree(context, input, prepared = prepareManagedWorktree(input)) {
87
+ export async function createManagedWorktree(context, input, prepared = prepareManagedWorktree(input)) {
86
88
  const { projectRoot } = prepared;
87
- const worktrunk = ensureManagedWorktrunk(context);
89
+ const worktrunk = await ensureManagedWorktrunk(context);
88
90
  // Worktrunk owns creation; the CLI supplies its project-scoped service path
89
91
  // as a one-shot config override rather than relying on a developer's user config.
90
92
  const command = [worktrunk.bin, "-C", projectRoot, "--config-set", `worktree-path = ${JSON.stringify(prepared.path)}`, "switch", "--create", prepared.branch, "--base", prepared.base, "--no-cd", "--format=json", "-y"];
91
- const result = runExternal(command);
93
+ const result = await runExternal(command);
92
94
  if (result.exit_code !== 0)
93
95
  throw new AppError("worktrunk_command_unavailable", "Worktrunk switch failed", 1, { command: sanitizeCommand(command), result });
94
96
  const response = parseWorktrunkResponse(result.stdout, command);
@@ -137,16 +139,16 @@ export function prepareManagedWorktreeBootstrap(input) {
137
139
  throw new AppError("bootstrap_unavailable", "Bootstrap worktree must be a directory", 1, { worktree_path: worktreePath });
138
140
  return { projectRoot, worktreePath };
139
141
  }
140
- export function bootstrapManagedWorktree(context, input, prepared = prepareManagedWorktreeBootstrap(input)) {
142
+ export async function bootstrapManagedWorktree(context, input, prepared = prepareManagedWorktreeBootstrap(input)) {
141
143
  const { worktreePath } = prepared;
142
- const worktrunk = ensureManagedWorktrunk(context);
144
+ const worktrunk = await ensureManagedWorktrunk(context);
143
145
  const commands = [
144
146
  [worktrunk.bin, "-C", worktreePath, "step", "copy-ignored", "--require-include"],
145
147
  [worktrunk.bin, "-C", worktreePath, "hook", "pre-start", "--yes"]
146
148
  ];
147
149
  const results = [];
148
150
  for (const command of commands) {
149
- const result = runExternal(command);
151
+ const result = await runExternal(command);
150
152
  results.push({ command: sanitizeCommand(command), ...result });
151
153
  if (result.exit_code !== 0) {
152
154
  throw new AppError("bootstrap_failed", "Project-owned Worktrunk bootstrap failed", 1, { command: sanitizeCommand(command), result, results });
@@ -167,14 +169,14 @@ export function prepareWorktreeBootstrap(context, input) {
167
169
  }
168
170
  return { protocol, record, managed: prepareManagedWorktreeBootstrap({ projectRoot: protocol.project_root, worktreePath: record.worktree_path }) };
169
171
  }
170
- export function bootstrapWorktree(context, input, prepared = prepareWorktreeBootstrap(context, input)) {
172
+ export async function bootstrapWorktree(context, input, prepared = prepareWorktreeBootstrap(context, input)) {
171
173
  const { protocol, record } = prepared;
172
174
  const active = activeWorktreeRecord(context, protocol.project_id, protocol.id);
173
175
  if (!active || active.worktree_path !== record.worktree_path)
174
176
  throw new AppError("worktree_active", "Worktree changed after bootstrap preparation", 1);
175
177
  let result;
176
178
  try {
177
- result = bootstrapManagedWorktree(context, prepared.managed, prepared.managed);
179
+ result = await bootstrapManagedWorktree(context, prepared.managed, prepared.managed);
178
180
  }
179
181
  catch (error) {
180
182
  if (error instanceof AppError && error.code === "bootstrap_failed")
@@ -254,16 +256,16 @@ export function worktreeRecord(context, projectId, protocolId) {
254
256
  function activeWorktreeRecord(context, projectId, protocolId) {
255
257
  return context.db.get("SELECT * FROM worktree_records WHERE project_id = ? AND protocol_id = ? AND status = 'active'", [projectId, protocolId]);
256
258
  }
257
- function runExternal(command) {
259
+ async function runExternal(command) {
258
260
  const [bin, ...args] = command;
259
261
  if (!bin) {
260
262
  return { exit_code: null, stdout: "", stderr: "Missing executable." };
261
263
  }
262
- const result = spawnSync(bin, args, { encoding: "utf8" });
264
+ const result = await commandResult(bin, args, { startupMs: SETUP_STARTUP_MS });
263
265
  return {
264
- exit_code: typeof result.status === "number" ? result.status : null,
266
+ exit_code: result.exit_code,
265
267
  stdout: sanitizeOutput(result.stdout),
266
- stderr: sanitizeOutput(result.stderr || result.error?.message || "")
268
+ stderr: sanitizeOutput(result.stderr)
267
269
  };
268
270
  }
269
271
  function sanitizeCommand(command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.127",
3
+ "version": "0.9.0-beta.133",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.23.0",