@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,9 @@ 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 { spawn, spawnSync } from "node:child_process";
5
+ import { spawn } from "node:child_process";
6
+ import { commandText } from '../harness-runtime/lib/process-json.mjs';
7
+ import { SETUP_STARTUP_MS, publishSetup } from '../harness-runtime/lib/setup-progress.mjs';
6
8
  import { createRequire } from "node:module";
7
9
  import { getCliBuildInfo } from "./build-info.js";
8
10
  import { AppError } from "../shared/errors.js";
@@ -32,6 +34,24 @@ export function isRouterNativeCommand(args) {
32
34
  return routerNativeFamilies.has(first) || first === "codex" && args[1] === "hook" && args[2] === "handle";
33
35
  }
34
36
  export function installCurrentEngine(context, input = {}) {
37
+ const installation = prepareInstallation(context, input);
38
+ const first = installation.next();
39
+ if (first.done && input.forRouting)
40
+ return first.value;
41
+ const manifest = first.done ? requireManifest(path.join(String(first.value.path), 'engine.json')) : first.value;
42
+ return probeEntrypoint(manifest, undefined, context.ddFlowHome, process.env).then(() => first.done ? first.value : installation.next().value)
43
+ .catch(error => {
44
+ let cleanup_error;
45
+ try {
46
+ installation.return({});
47
+ }
48
+ catch (cleanup) {
49
+ cleanup_error = { message: String(cleanup).slice(0, 2000) };
50
+ }
51
+ throw new AppError('engine_install_failed', 'Staged engine admission failed', 1, { ...error.details, cause: error.code, probe_error: { code: error.code, details: error.details }, ...(cleanup_error ? { cleanup_error } : {}) });
52
+ });
53
+ }
54
+ function* prepareInstallation(context, input) {
35
55
  const build = getCliBuildInfo();
36
56
  const packageRoot = findPackageRoot(path.dirname(sourceFile));
37
57
  if (!packageRoot) {
@@ -67,6 +87,7 @@ export function installCurrentEngine(context, input = {}) {
67
87
  const tmp = path.join(parent, `.${path.basename(target)}.tmp-${process.pid}-${Date.now()}`);
68
88
  fs.rmSync(tmp, { recursive: true, force: true });
69
89
  ensureDir(tmp);
90
+ publishSetup('probe_started', `engine-staging:${tmp}`);
70
91
  copyPackageSnapshot(packageRoot, tmp);
71
92
  const stagedManifest = buildManifest(context, build.package_name, build.version, packageRoot, target, tmp);
72
93
  const stagedChecksum = stagedManifest.integrity.checksum;
@@ -95,13 +116,15 @@ export function installCurrentEngine(context, input = {}) {
95
116
  const manifest = { ...stagedManifest, package_root: activationTarget, snapshot_root: activationTarget };
96
117
  fs.writeFileSync(path.join(tmp, "engine.json"), `${JSON.stringify(manifest, null, 2)}\n`);
97
118
  const checkedManifest = requireManifest(path.join(tmp, "engine.json"));
98
- const stagedDiagnostics = manifestDiagnostics({ ...checkedManifest, package_root: tmp, snapshot_root: tmp }, { checkEntrypoint: !input.forRouting });
119
+ const stagedDiagnostics = manifestDiagnostics({ ...checkedManifest, package_root: tmp, snapshot_root: tmp }, { checkEntrypoint: false });
99
120
  if (stagedDiagnostics.length > 0) {
100
121
  throw new AppError("engine_install_failed", "Staged dd-flow engine failed its integrity or dependency health check", 1, {
101
122
  diagnostics: stagedDiagnostics,
102
123
  path: tmp
103
124
  });
104
125
  }
126
+ if (!input.forRouting)
127
+ yield { ...checkedManifest, package_root: tmp, snapshot_root: tmp };
105
128
  activateEngineAtomically(tmp, activationTarget);
106
129
  const installed = requireManifest(path.join(activationTarget, "engine.json"));
107
130
  return {
@@ -136,13 +159,23 @@ export function resolveEngine(context, input = {}) {
136
159
  }
137
160
  return { ok: true, schema_id: "dd-flow/engine-resolve@1", selection, contexts: compatibilityContexts(context, { ...input, classification }) };
138
161
  }
139
- export function doctorEngines(context, input = {}) {
162
+ export async function doctorEngines(context, input = {}) {
140
163
  const root = engineStoreRoot(context.ddFlowHome);
141
164
  const installed = readInstalledManifests(root);
142
- const checks = installed.map(manifest => {
165
+ const checks = [];
166
+ for (const manifest of installed) {
143
167
  const diagnostics = manifestDiagnostics(manifest);
144
- return { engine: manifestSummary(manifest, diagnostics), status: diagnostics.includes("entrypoint_probe_unavailable") ? "unverified" : diagnostics.length ? "corrupt" : "ok", diagnostics };
145
- });
168
+ let probe_error;
169
+ if (!diagnostics.length)
170
+ try {
171
+ await probeEntrypoint(manifest, undefined, context.ddFlowHome, context.env);
172
+ }
173
+ catch (error) {
174
+ probe_error = error instanceof AppError ? error.details : { message: String(error) };
175
+ diagnostics.push(error instanceof AppError && error.code === 'engine_probe_failed' ? 'entrypoint_dependency_load_failed' : 'entrypoint_probe_unavailable');
176
+ }
177
+ checks.push({ engine: manifestSummary(manifest, diagnostics), status: diagnostics.includes("entrypoint_probe_unavailable") ? "unverified" : diagnostics.length ? "corrupt" : "ok", diagnostics, ...(probe_error ? { probe_error } : {}) });
178
+ }
146
179
  const classification = input.classification ?? classifyCliOperation(["engine", "doctor"], input.env ?? context.env);
147
180
  const selection = selectEngine(context, input, {}, classification);
148
181
  return {
@@ -155,13 +188,14 @@ export function doctorEngines(context, input = {}) {
155
188
  exit_code: checks.every((check) => check.status === "ok") && selection.status === "selected" ? 0 : 1
156
189
  };
157
190
  }
158
- export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedProjectRoot) {
191
+ export async function routeArgsThroughEngine(context, args, io, stdin, env, resolvedProjectRoot) {
159
192
  if (isEngineMode(env) || isRouterNativeCommand(args))
160
193
  return Promise.resolve(null);
161
194
  const classification = classifyCliOperation(args, env);
162
195
  const bound = boundRunEngineForArgs(context, args, resolvedProjectRoot ?? resolveExplicitProjectRoot(args) ?? undefined);
163
196
  if (bound) {
164
197
  const entrypoint = resolveManifestEntrypoint(bound.manifest);
198
+ await probeEntrypoint(bound.manifest, undefined, context.ddFlowHome, process.env);
165
199
  return spawnEngine(bound.manifest, entrypoint, args, io, stdin, env, getCliBuildInfo().version);
166
200
  }
167
201
  const selection = selectEngine(context, { projectRoot: resolvedProjectRoot ?? resolveExplicitProjectRoot(args) ?? undefined, env }, { allowCurrentInProcess: true }, classification);
@@ -180,9 +214,18 @@ export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedPr
180
214
  return Promise.resolve({ routed: true, exitCode: 0, inProcess: true });
181
215
  }
182
216
  const entrypoint = resolveManifestEntrypoint(selection.selected);
217
+ try {
218
+ await probeEntrypoint(selection.selected, undefined, context.ddFlowHome, process.env);
219
+ }
220
+ catch (cause) {
221
+ const error = cause;
222
+ if (error.code !== 'engine_probe_failed')
223
+ throw error;
224
+ throw new AppError('missing_engine', 'Selected engine cannot load its runtime dependencies', 1, { probe: error.details });
225
+ }
183
226
  return spawnEngine(selection.selected, entrypoint, args, io, stdin, env, current.version);
184
227
  }
185
- export function prepareRunEngineBind(context, input) {
228
+ export async function prepareRunEngineBind(context, input) {
186
229
  const reason = requiredText(input.reason, "reason");
187
230
  const projectRoot = resolveProjectRoot(input.projectRoot);
188
231
  const located = locateRunRoot(context.db, projectRoot, input.runId);
@@ -204,12 +247,18 @@ export function prepareRunEngineBind(context, input) {
204
247
  requested
205
248
  });
206
249
  }
250
+ await probeEntrypoint(manifest, ['run', 'status', located.run_id, '--project-root', projectRoot, '--json'], context.ddFlowHome, context.env, 'run_engine');
207
251
  return { kind: "create", projectRoot, located, manifest, requested, reason, entrypoint: resolveManifestEntrypoint(manifest) };
208
252
  }
209
- export function bindRunEngine(context, input, prepared = prepareRunEngineBind(context, input)) {
253
+ export async function bindRunEngine(context, input, supplied) {
254
+ const prepared = supplied ?? await prepareRunEngineBind(context, input);
210
255
  if (prepared.kind === "replay")
211
256
  return { ok: true, action: "run_engine_bind", changed: false, binding: prepared.binding };
212
- const { projectRoot, located, manifest, requested, reason, entrypoint } = prepared;
257
+ const { projectRoot, located, manifest, requested, reason } = prepared;
258
+ const current = locateRunRoot(context.db, projectRoot, input.runId);
259
+ if (!current || current.run_root !== located.run_root)
260
+ throw new AppError('run_engine_binding_conflict', 'RUN identity changed during probe', 1);
261
+ requireBoundManifest(context.ddFlowHome, { packageName: manifest.package_name, packageVersion: manifest.package_version, integrityChecksum: requested.integrity_checksum });
213
262
  const existing = readRunEngineBinding(located.binding_path, { allowMissing: true });
214
263
  if (existing) {
215
264
  if (sameEngineIdentity(existing.engine, requested))
@@ -217,23 +266,6 @@ export function bindRunEngine(context, input, prepared = prepareRunEngineBind(co
217
266
  throw new AppError("run_engine_binding_immutable", "RUN engine binding cannot be changed", 1, { run_id: located.run_id, current: existing.engine, requested });
218
267
  }
219
268
  const probeArgs = ["run", "status", located.run_id, "--project-root", projectRoot, "--json"];
220
- const probe = spawnSync(process.execPath, [entrypoint, ...probeArgs], {
221
- cwd: manifest.snapshot_root,
222
- env: { ...process.env, ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: manifest.snapshot_root },
223
- encoding: "utf8",
224
- timeout: 30_000
225
- });
226
- if (probe.error || probe.signal)
227
- throw new AppError("run_engine_probe_unavailable", "Selected engine observation could not complete; compatibility is unknown", 1, { run_id: located.run_id, cause: probe.error?.message, signal: probe.signal });
228
- if (probe.status !== 0) {
229
- throw new AppError("run_engine_probe_failed", "Selected engine cannot read the RUN authority", 1, {
230
- run_id: located.run_id,
231
- engine: manifestSummary(manifest),
232
- command: `dd-flow ${probeArgs.join(" ")}`,
233
- exit_code: probe.status,
234
- stderr: probe.stderr.slice(0, 2000)
235
- });
236
- }
237
269
  const binding = {
238
270
  schema_id: runEngineBindingSchemaId,
239
271
  run_id: located.run_id,
@@ -272,7 +304,7 @@ export function bindCurrentEngineToRun(context, input) {
272
304
  /** Forks are a new execution authority. The source binding stays immutable;
273
305
  * this replaces only the copied binding after proving the selected engine can
274
306
  * read the copied RUN. */
275
- export function upgradeForkedRunEngine(context, input) {
307
+ export async function upgradeForkedRunEngine(context, input) {
276
308
  const projectRoot = resolveProjectRoot(input.projectRoot);
277
309
  const located = locateRunRoot(context.db, projectRoot, input.runId);
278
310
  if (!located)
@@ -283,20 +315,16 @@ export function upgradeForkedRunEngine(context, input) {
283
315
  ...(input.integrityChecksum ? { integrityChecksum: input.integrityChecksum } : {})
284
316
  });
285
317
  const requested = engineIdentity(manifest);
286
- readRunEngineBinding(located.binding_path);
318
+ const priorBinding = fs.readFileSync(located.binding_path, 'utf8');
287
319
  const lineage = path.join(located.run_root, "fork-lineage.json");
288
320
  if (!fs.existsSync(lineage))
289
321
  throw new AppError("fork_lineage_missing", "Engine upgrade is permitted only for a derived RUN", 1, { run_id: located.run_id });
290
322
  const probeArgs = ["run", "status", located.run_id, "--project-root", projectRoot, "--json"];
291
- const probe = spawnSync(process.execPath, [resolveManifestEntrypoint(manifest), ...probeArgs], {
292
- cwd: manifest.snapshot_root,
293
- env: { ...process.env, ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: manifest.snapshot_root },
294
- encoding: "utf8", timeout: 30_000
295
- });
296
- if (probe.error || probe.signal)
297
- throw new AppError("fork_engine_probe_unavailable", "Selected engine observation could not complete; compatibility is unknown", 1, { run_id: located.run_id, cause: probe.error?.message, signal: probe.signal });
298
- if (probe.status !== 0)
299
- throw new AppError("fork_engine_probe_failed", "Selected engine cannot read the forked RUN", 1, { run_id: located.run_id, engine: manifestSummary(manifest), stderr: probe.stderr.slice(0, 2000) });
323
+ const priorLineage = fs.readFileSync(lineage, 'utf8');
324
+ await probeEntrypoint(manifest, probeArgs, context.ddFlowHome, context.env, 'fork_engine');
325
+ if (fs.readFileSync(located.binding_path, 'utf8') !== priorBinding || fs.readFileSync(lineage, 'utf8') !== priorLineage)
326
+ throw new AppError('fork_engine_binding_conflict', 'Fork identity changed during probe', 1);
327
+ requireBoundManifest(context.ddFlowHome, { packageName: manifest.package_name, packageVersion: manifest.package_version, integrityChecksum: manifest.integrity.checksum });
300
328
  const binding = { schema_id: runEngineBindingSchemaId, run_id: located.run_id, project_root: projectRoot, bound_at: context.now(), source: "fork_upgrade", reason: requiredText(input.reason, "reason"), engine: requested, probe: { status: "passed", command: `dd-flow ${probeArgs.join(" ")}` } };
301
329
  const temporary = `${located.binding_path}.fork-${process.pid}-${Date.now()}.tmp`;
302
330
  fs.writeFileSync(temporary, `${JSON.stringify(binding, null, 2)}\n`);
@@ -678,10 +706,13 @@ function buildManifest(context, packageName, version, packageRoot, snapshotRoot,
678
706
  };
679
707
  }
680
708
  function copyPackageSnapshot(packageRoot, target) {
709
+ let copied = 0;
681
710
  for (const [relative, source] of packageSnapshotFiles(packageRoot)) {
682
711
  const destination = path.join(target, relative);
683
712
  ensureDir(path.dirname(destination));
684
713
  fs.copyFileSync(source, destination);
714
+ if (++copied % 64 === 0)
715
+ publishSetup('transfer_progress', `${target}:${copied}`);
685
716
  }
686
717
  }
687
718
  /** The same source inventory drives validation and copying; no staging writes. */
@@ -920,26 +951,20 @@ function manifestDiagnostics(manifest, options = {}) {
920
951
  }
921
952
  }
922
953
  }
923
- if (options.checkEntrypoint !== false && entrypoint && fs.existsSync(entrypoint) && !diagnostics.includes("entrypoint_outside_snapshot")) {
924
- const health = spawnSync(process.execPath, [entrypoint, "--version", "--json"], {
925
- cwd: snapshotRoot,
926
- env: {
927
- ...process.env,
928
- DD_FLOW_ENGINE_MODE: "1",
929
- DD_FLOW_ENGINE_HOME: snapshotRoot,
930
- DD_FLOW_ENGINE_HEALTHCHECK: "1"
931
- },
932
- encoding: "utf8",
933
- stdio: ["ignore", "pipe", "pipe"],
934
- timeout: 15_000
935
- });
936
- if (health.error || health.signal)
937
- diagnostics.push("entrypoint_probe_unavailable");
938
- else if (health.status !== 0)
939
- diagnostics.push("entrypoint_dependency_load_failed");
940
- }
941
954
  return diagnostics;
942
955
  }
956
+ async function probeEntrypoint(manifest, args = ['--version', '--json'], runtimeHome, env = {}, purpose = 'engine') {
957
+ publishSetup('probe_started', manifest.snapshot_root);
958
+ try {
959
+ await commandText(resolveManifestEntrypoint(manifest), args, { cwd: manifest.snapshot_root,
960
+ env: { ...env, DD_FLOW_ENGINE_MODE: '1', DD_FLOW_ENGINE_HOME: manifest.snapshot_root, ...(runtimeHome ? { DD_FLOW_HOME: runtimeHome } : {}), ...(args[0] === '--version' ? { DD_FLOW_ENGINE_HEALTHCHECK: '1' } : {}) }, startupMs: SETUP_STARTUP_MS, phase: 'engine-probe' });
961
+ publishSetup('probe_completed', manifest.snapshot_root);
962
+ }
963
+ catch (cause) {
964
+ const error = cause;
965
+ throw new AppError(error.details?.reason === 'child_exit' && error.details?.signal === null && error.details?.exit_code !== null ? `${purpose}_probe_failed` : `${purpose}_probe_unavailable`, 'Selected engine probe did not complete successfully', 1, { cause: error.code, message: error.message, ...error.details });
966
+ }
967
+ }
943
968
  function manifestSummary(manifest, diagnostics = manifestDiagnostics(manifest, { checkEntrypoint: false })) {
944
969
  return {
945
970
  schema_id: manifest.schema_id,
@@ -1104,16 +1129,22 @@ function findPackageRoot(start) {
1104
1129
  }
1105
1130
  }
1106
1131
  export function engineSnapshotChecksum(root) {
1132
+ publishSetup('probe_started', `engine-digest:${root}`);
1107
1133
  const files = [];
1108
1134
  collectFiles(root, root, files);
1109
1135
  const checksum = crypto.createHash("sha256");
1136
+ let checked = 0;
1110
1137
  for (const file of files.sort()) {
1111
1138
  checksum.update(file);
1112
1139
  checksum.update("\0");
1113
1140
  checksum.update(fs.readFileSync(path.join(root, file)));
1141
+ if (++checked % 64 === 0)
1142
+ publishSetup('identity_digest_progress', `${root}:${checked}`);
1114
1143
  checksum.update("\0");
1115
1144
  }
1116
- return checksum.digest("hex");
1145
+ const result = checksum.digest("hex");
1146
+ publishSetup('identity_digest_completed', root);
1147
+ return result;
1117
1148
  }
1118
1149
  function collectFiles(root, current, files) {
1119
1150
  for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ import { checkRepairDisposition } from "./check-runner-evidence.js";
2
3
  import path from "node:path";
3
4
  import fs from "node:fs";
4
5
  import { quote } from "shell-quote";
@@ -70,8 +71,9 @@ export function isRetryableLifecycleRejection(code, details = {}) {
70
71
  return details.phase === "prepare" && details.effect === "no_effect" && details.recoverable === true;
71
72
  if (["invocation_command_mismatch", "invocation_argument_mismatch"].includes(code))
72
73
  return details.recoverable !== false;
73
- return details.recoverable === true || ["work_checks_failed", "code_gate_failed", "code_review_gate_failed", "merge_gate_failed",
74
- "review_repair_incomplete", "plan_review_incomplete", "semantic_result_invalid",
74
+ if (["work_checks_failed", "code_gate_failed", "code_review_gate_failed", "merge_gate_failed"].includes(code))
75
+ return Array.isArray(details.failures) && details.failures.length > 0 && details.failures.every(receipt => checkRepairDisposition(receipt) === "source_repair");
76
+ return details.recoverable === true || ["review_repair_incomplete", "plan_review_incomplete", "semantic_result_invalid",
75
77
  "review_evidence_invalid", "evidence_obligation_unknown", "invalid_evidence_ref",
76
78
  "evidence_ref_missing", "evidence_line_out_of_range", "document_update_missing",
77
79
  "document_update_not_materialized", "review_repair_no_change"].includes(code)
@@ -2,8 +2,8 @@ import { setTimeout as delay } from "node:timers/promises";
2
2
  import { AppError } from "../shared/errors.js";
3
3
  import { createRenewalState, maintenanceRetryable, renewalConfirmed, renewalDelay, renewalFailure, renewalRemaining, RENEWAL_POLICY } from "../harness-runtime/lib/lease-renewal.mjs";
4
4
  /** Registry renewals happen outside RUN writers. Assertions never renew. */
5
- export function managedLeaseMonitor(renew) {
6
- const episode = createRenewalState();
5
+ export function managedLeaseMonitor(renew, leaseExpiresAt = null) {
6
+ const episode = createRenewalState({ leaseExpiresAt });
7
7
  let expiry = 0;
8
8
  let failure = null;
9
9
  let lost = false;
@@ -11,6 +11,11 @@ export function managedLeaseMonitor(renew) {
11
11
  let closing = false;
12
12
  let pending;
13
13
  const shutdown = new AbortController();
14
+ const retainAuthority = (leaseExpiresAt) => {
15
+ if (!renewalConfirmed(episode, performance.now(), leaseExpiresAt))
16
+ throw new AppError("managed_lease_receipt_invalid", "Confirmation has no current persisted expiry", 1);
17
+ expiry = Date.parse(leaseExpiresAt);
18
+ };
14
19
  const uncertain = () => new AppError("controller_lease_unconfirmed", "Managed lease requires confirmation outside the writer", 1, { cause: failure, exhausted: episode.exhausted, closing });
15
20
  const assertNoRenewal = () => {
16
21
  if (closing)
@@ -46,7 +51,7 @@ export function managedLeaseMonitor(renew) {
46
51
  throw new AppError("managed_lease_receipt_invalid", "Renewal has no valid persisted expiry", 1);
47
52
  if (value <= Date.now())
48
53
  throw new AppError("controller_lease_unconfirmed", "Renewal receipt has already expired", 1);
49
- if ((!failure || renewalRemaining(episode)) && renewalConfirmed(episode)) {
54
+ if (renewalConfirmed(episode, performance.now(), receipt.lease_expires_at)) {
50
55
  expiry = value;
51
56
  failure = null;
52
57
  }
@@ -122,7 +127,7 @@ export function managedLeaseMonitor(renew) {
122
127
  }
123
128
  });
124
129
  };
125
- return { tick, assertNoRenewal, assertConfirmed, ensureConfirmed, background,
130
+ return { tick, assertNoRenewal, assertConfirmed, ensureConfirmed, background, retainAuthority,
126
131
  close: () => { closing = true; shutdown.abort(); },
127
132
  isConfirmed: () => !closing && !lost && !failure && Date.now() < expiry };
128
133
  }
@@ -268,7 +268,9 @@ function assertGitFacts(workspaceRoot, expected, runId) {
268
268
  if (!expected?.head && !expected?.branch)
269
269
  return;
270
270
  const read = (args) => {
271
- const result = spawnSync("git", ["-C", workspaceRoot, ...args.split(" ")], { encoding: "utf8" });
271
+ const result = spawnSync("git", ["-C", workspaceRoot, ...args.split(" ")], { encoding: "utf8", timeout: 5000 });
272
+ if (result.error || result.signal)
273
+ throw new AppError("git_observation_unavailable", "Cannot establish RUN workspace identity", 1, { run_id: runId, effect: "unknown" });
272
274
  return result.status === 0 ? result.stdout.trim() || null : null;
273
275
  };
274
276
  const actual = { branch: read("branch --show-current"), head: read("rev-parse HEAD") };
@@ -668,7 +668,9 @@ function removeProtocolWorktreeBeforeCancel(protocol, state, record, force) {
668
668
  }
669
669
  const existsBefore = fs.existsSync(worktreePath);
670
670
  if (existsBefore && !force) {
671
- const dirty = spawnSync("git", ["-C", worktreePath, "status", "--porcelain"], { encoding: "utf8" });
671
+ const dirty = spawnSync("git", ["-C", worktreePath, "status", "--porcelain"], { encoding: "utf8", timeout: 5000 });
672
+ if (dirty.error || dirty.status !== 0)
673
+ throw new AppError("workspace_status_failed", "Cannot establish worktree status before removal", 1, { path: worktreePath, cause: dirty.error?.message ?? dirty.stderr });
672
674
  if (dirty.status === 0 && dirty.stdout.trim().length > 0) {
673
675
  throw new AppError("worktree_dirty", "Refusing to remove dirty worktree without --force --reason", 1, {
674
676
  path: worktreePath
@@ -2,10 +2,12 @@ import crypto from "node:crypto";
2
2
  import { stageAttempt, stageCycle, bindStageWork } from "./stage-work-graph.js";
3
3
  import { assertRunMutationAllowed } from "./run-recovery.js";
4
4
  import { AppError } from "../shared/errors.js";
5
+ import { assertSourceRepairEligible } from "./code-checks.js";
5
6
  /** Checks finish before entering this synchronous registration transaction. */
6
7
  export function ensureAggregateRepair(context, input, create) {
7
- if (!input.failures.length || input.failures.some(receipt => receipt.status !== "failed" && receipt.status !== "aborted"))
8
- throw new AppError("repair_intent_invalid", "Repair requires settled failed receipts", 1);
8
+ if (!input.failures.length)
9
+ throw new AppError("repair_intent_invalid", "Repair requires assertion-backed failed receipts", 1);
10
+ assertSourceRepairEligible(input.failures);
9
11
  const causes = input.failures.map(receipt => ({ id: receipt.id, epoch: receipt.verification_epoch, fingerprint: receipt.workspace_fingerprint })).sort((a, b) => a.id.localeCompare(b.id));
10
12
  return ensureRepairIntent(context, { ...input, causes: { receipts: causes, origins: [...(input.origins ?? [])].sort() } }, create);
11
13
  }
@@ -268,7 +268,8 @@ export async function serveRunControl(context, input) {
268
268
  }
269
269
  if (!worker.process_lease_token)
270
270
  throw new AppError("run_control_owner_mismatch", "Control owner has no resource lease", 1);
271
- confirmManagedProcess(resourceContext, { id: worker.process_id, leaseToken: worker.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
271
+ const confirmed = confirmManagedProcess(resourceContext, { id: worker.process_id, leaseToken: worker.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
272
+ lease.retainAuthority(confirmed.lease_expires_at);
272
273
  let priorObservation = null;
273
274
  try {
274
275
  priorObservation = worker.snapshot_json ? JSON.parse(worker.snapshot_json) : null;
@@ -176,22 +176,21 @@ export async function serveRunController(context, input) {
176
176
  claimControllerProcess(context, row);
177
177
  if (!row.process_id || !row.process_lease_token)
178
178
  throw new AppError("controller_owner_mismatch", "Managed owner has no resource lease", 1);
179
- confirmManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
180
- const lease = controllerLeaseMonitor(attemptMs => {
181
- const deadline = Date.now() + attemptMs;
182
- const receipt = renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: deadline }, { id: row.process_id, leaseToken: row.process_lease_token });
183
- if (receipt.owned) {
184
- // These exact-owner keepalive writes are idempotent. Finish the resource
185
- // transaction before acquiring RUN writer; both share one attempt budget.
186
- const db = context.db.withDeadline(deadline);
187
- if (db.get("SELECT 1 FROM merge_requests WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching' LIMIT 1", [row.project_id, row.run_id, row.controller_id]))
188
- db.writeTransaction(() => {
189
- const guard = recoveryGuard({ ...context, db }, row.project_id, row.run_id);
190
- if ((guard?.generation ?? 0) === row.generation && !["draining", "sealed", "resuming"].includes(guard?.status ?? ""))
191
- db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching'", [new Date(Date.now() + 120_000).toISOString(), context.now(), row.project_id, row.run_id, row.controller_id]);
192
- });
193
- }
194
- return receipt;
179
+ const confirmed = confirmManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
180
+ const lease = controllerLeaseMonitor(attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: row.process_id, leaseToken: row.process_lease_token }), confirmed.lease_expires_at);
181
+ // Resource and RUN commits are distinct effects. A RUN keepalive failure must
182
+ // neither erase a resource receipt nor grant RUN dispatch authority.
183
+ const mergeLease = controllerLeaseMonitor(attemptMs => {
184
+ const expiry = new Date(Date.now() + 120_000).toISOString();
185
+ const db = context.db.withDeadline(Date.now() + attemptMs);
186
+ if (db.get("SELECT 1 FROM merge_requests WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching' LIMIT 1", [row.project_id, row.run_id, row.controller_id]))
187
+ db.writeTransaction(() => {
188
+ const guard = recoveryGuard({ ...context, db }, row.project_id, row.run_id);
189
+ if ((guard?.generation ?? 0) !== row.generation || ["draining", "sealed", "resuming"].includes(guard?.status ?? ""))
190
+ throw new AppError("controller_lease_lost", "RUN generation changed before MERGE keepalive", 1);
191
+ db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching'", [expiry, context.now(), row.project_id, row.run_id, row.controller_id]);
192
+ });
193
+ return { owned: true, lease_expires_at: expiry };
195
194
  });
196
195
  const assertLeaseFence = () => {
197
196
  const current = requireController(context, row.controller_id);
@@ -201,12 +200,14 @@ export async function serveRunController(context, input) {
201
200
  };
202
201
  const heartbeat = setInterval(() => {
203
202
  lease.background(assertLeaseFence, () => { });
203
+ mergeLease.background(assertLeaseFence, () => { });
204
204
  }, 30_000);
205
205
  heartbeat.unref();
206
206
  const manifest = JSON.parse(row.manifest_json);
207
207
  const state = JSON.parse(row.state_json);
208
208
  const assertPhysicalOwner = () => {
209
209
  lease.assertNoRenewal();
210
+ mergeLease.assertNoRenewal();
210
211
  assertManagedProcessLease({ ...context, databaseDeadlineAt: Date.now() + 5_000 }, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid });
211
212
  const current = requireController(context, row.controller_id);
212
213
  if (current.owner_pid !== process.pid || current.owner_token !== row.owner_token || !["running", "capturing", "waiting_for_user", "waiting_for_context", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(current.status))
@@ -219,6 +220,7 @@ export async function serveRunController(context, input) {
219
220
  };
220
221
  try {
221
222
  await lease.ensureConfirmed(assertLeaseFence);
223
+ await mergeLease.ensureConfirmed(assertLeaseFence);
222
224
  context.db.writeTransaction(() => {
223
225
  markControllerDispatchReady(context, row);
224
226
  if (!state.recovery && (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) === row.generation && requireRun(context, row.project_id, row.run_id).status === "paused")
@@ -227,7 +229,7 @@ export async function serveRunController(context, input) {
227
229
  appendEvent(context, row.controller_id, "owner_started", { pid: process.pid, generation: row.generation });
228
230
  if (state.recovery && !["done", "failed", "cancelled"].includes(requireRun(context, row.project_id, row.run_id).status))
229
231
  await resumeControllerSession(context, row, manifest, state, assertPhysicalOwner);
230
- await executeController(context, row, manifest, state, assertOwner, assertPhysicalOwner, () => lease.ensureConfirmed(assertLeaseFence));
232
+ await executeController(context, row, manifest, state, assertOwner, assertPhysicalOwner, async () => { await lease.ensureConfirmed(assertLeaseFence); await mergeLease.ensureConfirmed(assertLeaseFence); });
231
233
  return controllerReceipt(requireController(context, row.controller_id));
232
234
  }
233
235
  catch (error) {
@@ -273,6 +275,7 @@ export async function serveRunController(context, input) {
273
275
  }
274
276
  finally {
275
277
  lease.close();
278
+ mergeLease.close();
276
279
  clearInterval(heartbeat);
277
280
  finishOwner();
278
281
  }
@@ -57,7 +57,7 @@ function assertForkOutput(output, intentFile, intent) {
57
57
  throw new AppError("fork_retry_inspect_required", "Fork output contains data not owned by the interrupted fork", 1, { output, unknown });
58
58
  }
59
59
  }
60
- export function forkRunFromSnapshot(context, input, prepared = prepareRunFork(input)) {
60
+ export async function forkRunFromSnapshot(context, input, prepared = prepareRunFork(input)) {
61
61
  if ("receipt" in prepared)
62
62
  return prepared.receipt;
63
63
  const { manifest, snapshot, output, intent, intentFile, receiptFile, requestId } = prepared;
@@ -95,7 +95,7 @@ export function forkRunFromSnapshot(context, input, prepared = prepareRunFork(in
95
95
  // CLI into the new home. Its exact version/checksum were checked before
96
96
  // any interrupted-copy cleanup; snapshots exclude installed engines.
97
97
  installCurrentEngine(restoredContext, { forRouting: true });
98
- const binding = upgradeForkedRunEngine(restoredContext, {
98
+ const binding = await upgradeForkedRunEngine(restoredContext, {
99
99
  projectRoot,
100
100
  runId: String(restored.run_id),
101
101
  engineVersion: intent.engine_version,
@@ -171,9 +171,9 @@ export async function serveRuntimeScopeControl(context, scopeId, generation) {
171
171
  const record = registerManagedProcess(context, { kind: "scope-control", ownerId: key, operationId: `scope-control:${key}:${token}`, ownerPid: process.pid, metadata: { role: "control", scope_id: scopeId, generation } });
172
172
  worker.process_id = record.id;
173
173
  worker.process_lease_token = record.lease_token;
174
- confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
174
+ const confirmed = confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
175
175
  db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-control-worker' AND resource_key = ? AND lease_token = ?", [JSON.stringify(worker), context.now(), key, token]);
176
- lease = managedLeaseMonitor(attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: record.id, leaseToken: record.lease_token }));
176
+ lease = managedLeaseMonitor(attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: record.id, leaseToken: record.lease_token }), confirmed.lease_expires_at);
177
177
  let timerFailure;
178
178
  const assertScopeCurrent = () => {
179
179
  const latest = runtimeBudgetStatus(context, scopeId);
@@ -2,6 +2,8 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
+ import { commandResult } from '../harness-runtime/lib/process-json.mjs';
6
+ import { SETUP_STARTUP_MS } from '../harness-runtime/lib/setup-progress.mjs';
5
7
  import { requireProjectByRoot } from "./projects.js";
6
8
  import { appendAudit } from "./audit.js";
7
9
  import { persistProtocolState, requireProtocol, readProtocolRuntimeState } from "./protocols.js";
@@ -283,7 +285,7 @@ export function prepareStageFinish(context, input) {
283
285
  const planFinish = input.stage === "plan" ? preparePlanFinish(context, projectRoot, view, stageRoot) : undefined;
284
286
  return { projectRoot, runId, view, existing, runHome, stageRoot, semanticFile, semantic, status, source, planFinish };
285
287
  }
286
- export function finishStage(context, input, prepared = prepareStageFinish(context, input)) {
288
+ export async function finishStage(context, input, prepared = prepareStageFinish(context, input)) {
287
289
  const { projectRoot, runId, view, runHome, stageRoot, semanticFile, semantic, status, source, planFinish } = prepared;
288
290
  assertRunMutationAllowed(context, view.run.project_id, view.run.id);
289
291
  fs.mkdirSync(stageRoot, { recursive: true });
@@ -293,7 +295,7 @@ export function finishStage(context, input, prepared = prepareStageFinish(contex
293
295
  if (coverage.jobs.missing?.length) {
294
296
  throw new AppError("worker_jobs_incomplete", "Stage finish requires all declared worker jobs", 1, { coverage });
295
297
  }
296
- const lint = runTargetedMemoryBankLint(projectRoot);
298
+ const lint = await runTargetedMemoryBankLint(projectRoot);
297
299
  if (planFinish)
298
300
  boundCanonicalPlan(context, { projectId: view.run.project_id, protocolId: view.run.subject.id, planPath: planFinish.plan_path }, planFinish.canonical);
299
301
  const report = buildStageReport(context, view, input.stage, status, semantic, stageRoot, lint, planFinish);
@@ -833,7 +835,7 @@ function workerCoverage(context, projectId, runId) {
833
835
  jobs: { expected: jobs.map((job) => job.job_id), missing: missingJobs }
834
836
  };
835
837
  }
836
- export function runTargetedMemoryBankLint(projectRoot, selectedFiles) {
838
+ export async function runTargetedMemoryBankLint(projectRoot, selectedFiles) {
837
839
  const files = [...new Set((selectedFiles ?? gitChangedFiles(projectRoot)).filter((file) => file.startsWith(".memory-bank/") && file.toLowerCase().endsWith(".md") && !file.split(/[\\/]/u).some((part) => part === ".." || part.startsWith(".env"))))];
838
840
  for (const file of files)
839
841
  ensureWithin(projectRoot, path.resolve(projectRoot, file), "lint target");
@@ -841,9 +843,13 @@ export function runTargetedMemoryBankLint(projectRoot, selectedFiles) {
841
843
  return { ok: true, status: "not_applicable_no_changed_docs", files, finding_count: 0 };
842
844
  }
843
845
  const binary = process.env.MB_LINT_BIN ?? "mb-lint";
844
- const result = spawnSync(binary, ["--root", projectRoot, "--files", ...files, "--format", "json"], { cwd: projectRoot, encoding: "utf8" });
845
- if (result.error || result.status === null) {
846
- throw new AppError("lint_gate_unavailable", "Selected-file mb-lint is unavailable", 1, { binary, files, cause: String(result.error ?? "process did not start") });
846
+ let result;
847
+ try {
848
+ result = await commandResult(binary, ["--root", projectRoot, "--files", ...files, "--format", "json"], { cwd: projectRoot, startupMs: SETUP_STARTUP_MS });
849
+ }
850
+ catch (cause) {
851
+ const error = cause;
852
+ throw new AppError("lint_gate_unavailable", "Selected-file mb-lint is unavailable", 1, { binary, files, cause: error.code, ...error.details });
847
853
  }
848
854
  let parsed = {};
849
855
  try {
@@ -852,7 +858,7 @@ export function runTargetedMemoryBankLint(projectRoot, selectedFiles) {
852
858
  catch {
853
859
  throw new AppError("lint_gate_invalid_output", "Selected-file mb-lint did not return JSON", 1, { binary, files });
854
860
  }
855
- if (result.status !== 0) {
861
+ if (result.exit_code !== 0) {
856
862
  throw new AppError("lint_gate_failed", "Selected-file mb-lint reported blocking findings", 1, { binary, files, result: parsed });
857
863
  }
858
864
  const findingCount = parsed && typeof parsed === "object" && Array.isArray(parsed.findings)