@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.
- package/CHANGELOG.md +58 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/command-inputs.js +2 -2
- package/dist/cli/run-cli.js +6 -6
- package/dist/cli/runtime-maintenance-diagnostics.js +1 -1
- package/dist/cli/runtime-maintenance-ingress.js +5 -4
- package/dist/cli/runtime-process.js +27 -2
- package/dist/cli.js +7 -1
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy.mjs +4 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -1
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-zcode.mjs +13 -4
- package/dist/harness-runtime/lib/lease-renewal.d.mts +5 -5
- package/dist/harness-runtime/lib/lease-renewal.mjs +18 -5
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -1
- package/dist/harness-runtime/lib/native-hook-command.mjs +13 -5
- package/dist/harness-runtime/lib/process-json.d.mts +5 -0
- package/dist/harness-runtime/lib/process-json.mjs +41 -9
- package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
- package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
- package/dist/services/branch-context.js +3 -3
- package/dist/services/check-runner-evidence.js +121 -0
- package/dist/services/cleanup.js +1 -1
- package/dist/services/code-checks.js +113 -31
- package/dist/services/compatibility-preflight.js +9 -2
- package/dist/services/dashboard.js +33 -26
- package/dist/services/engines.js +88 -57
- package/dist/services/lifecycle-invocations.js +4 -2
- package/dist/services/managed-lease-monitor.js +9 -4
- package/dist/services/prompts.js +3 -1
- package/dist/services/protocols.js +3 -1
- package/dist/services/repair-intents.js +4 -2
- package/dist/services/run-control-worker.js +2 -1
- package/dist/services/run-controller.js +20 -17
- package/dist/services/run-fork.js +2 -2
- package/dist/services/runtime-scope-worker.js +2 -2
- package/dist/services/stage-lifecycle.js +13 -7
- package/dist/services/tooling.js +32 -23
- package/dist/services/vnext-code-review.js +2 -0
- package/dist/services/vnext-code.js +5 -1
- package/dist/services/vnext-merge.js +4 -1
- package/dist/services/vnext-protocolize.js +17 -16
- package/dist/services/work-registry.js +3 -1
- package/dist/services/worktrees.js +16 -14
- package/package.json +1 -1
package/dist/services/engines.js
CHANGED
|
@@ -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
|
|
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:
|
|
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 =
|
|
165
|
+
const checks = [];
|
|
166
|
+
for (const manifest of installed) {
|
|
143
167
|
const diagnostics = manifestDiagnostics(manifest);
|
|
144
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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 ((
|
|
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
|
}
|
package/dist/services/prompts.js
CHANGED
|
@@ -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
|
|
8
|
-
throw new AppError("repair_intent_invalid", "Repair requires
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
db.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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.
|
|
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)
|