@indigoai-us/hq-cli 5.345.11 → 5.345.12
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 +7 -0
- package/assets/scaffold/core/scripts/register-project.sh +5 -5
- package/assets/scaffold/core/scripts/repo-run-registry.sh +8 -8
- package/assets/scaffold/core/scripts/resume-thread-lock.sh +4 -4
- package/assets/scaffold/core/scripts/verify-story-deliverables.sh +1 -1
- package/assets/scaffold/core/scripts/worktree-gc.sh +1 -1
- package/dist/commands/core-handoff.js +69 -34
- package/dist/commands/core-hq-session-native.js +26 -5
- package/dist/commands/core-job-notify-fast.d.ts +2 -0
- package/dist/commands/core-job-notify-fast.js +6 -0
- package/dist/commands/core-job-probe-fast.d.ts +2 -0
- package/dist/commands/core-job-probe-fast.js +11 -0
- package/dist/commands/core-job-probe-native.js +1 -1
- package/dist/commands/core-job-remediate-fast.d.ts +2 -0
- package/dist/commands/core-job-remediate-fast.js +6 -0
- package/dist/commands/core-job-run-fast.d.ts +2 -0
- package/dist/commands/core-job-run-fast.js +6 -0
- package/dist/commands/core-pin-fast.d.ts +2 -0
- package/dist/commands/core-pin-fast.js +15 -0
- package/dist/commands/core-rebuild-index-fast.d.ts +6 -0
- package/dist/commands/core-rebuild-index-fast.js +39 -0
- package/dist/commands/core-register-project-native.js +11 -2
- package/dist/commands/core-repo-run-registry-native.js +47 -4
- package/dist/commands/core-session-journal-native.d.ts +1 -1
- package/dist/commands/core-session-journal-native.js +2 -2
- package/dist/commands/core-share-suggestion-state-native.js +6 -2
- package/dist/commands/core-work-mesh-live-rebind-native.js +5 -2
- package/dist/commands/core-worker-lint-fast.d.ts +2 -0
- package/dist/commands/core-worker-lint-fast.js +11 -0
- package/dist/commands/core.js +9 -6
- package/dist/fast-scaffold.js +49 -0
- package/dist/lib/core-utils/worktree-resume.d.ts +1 -0
- package/dist/lib/core-utils/worktree-resume.js +68 -31
- package/dist/lib/daemon/catalog.d.ts +10 -0
- package/dist/lib/daemon/catalog.js +8 -0
- package/dist/lib/daemon/lanes-reconcile-guard.d.ts +43 -0
- package/dist/lib/daemon/lanes-reconcile-guard.js +115 -0
- package/dist/lib/daemon/run.js +2 -0
- package/dist/lib/daemon/supervisor.d.ts +2 -0
- package/dist/lib/daemon/supervisor.js +28 -0
- package/dist/lib/lanes/autopilot-registry.d.ts +2 -5
- package/dist/lib/lanes/autopilot-registry.js +4 -4
- package/dist/lib/lanes/reconcile.d.ts +0 -5
- package/dist/lib/lanes/reconcile.js +4 -4
- package/dist/lib/lanes/types.d.ts +6 -5
- package/dist/utils/run-bundled-script.js +1 -1
- package/package.json +1 -1
|
@@ -28,7 +28,16 @@ function epoch(value, parseDate) {
|
|
|
28
28
|
}
|
|
29
29
|
return parseDate(value);
|
|
30
30
|
}
|
|
31
|
+
function nativePath(value) {
|
|
32
|
+
if (process.platform === "win32") {
|
|
33
|
+
const gitBashPath = /^\/([A-Za-z])\/(.*)$/u.exec(value);
|
|
34
|
+
if (gitBashPath)
|
|
35
|
+
return `${gitBashPath[1]}:\\${gitBashPath[2].replaceAll("/", "\\")}`;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
31
39
|
function pathAbs(value, cwd, f) {
|
|
40
|
+
value = nativePath(value);
|
|
32
41
|
if (value.startsWith("/"))
|
|
33
42
|
return value;
|
|
34
43
|
const parent = path.dirname(value);
|
|
@@ -40,8 +49,29 @@ function pathAbs(value, cwd, f) {
|
|
|
40
49
|
return value;
|
|
41
50
|
}
|
|
42
51
|
}
|
|
52
|
+
function shellAbsolutePath(value) {
|
|
53
|
+
if (process.platform !== "win32")
|
|
54
|
+
return value;
|
|
55
|
+
// The shell companion stores drive paths with forward slashes. The shared
|
|
56
|
+
// shellDisplayPath helper emits Git Bash mount paths for command output, but
|
|
57
|
+
// that is not the registry's persisted path format.
|
|
58
|
+
if (/^\/[A-Za-z]\//u.test(value))
|
|
59
|
+
return value;
|
|
60
|
+
return path.win32.resolve(nativePath(value)).replaceAll("\\", "/");
|
|
61
|
+
}
|
|
62
|
+
function shellScope(value) {
|
|
63
|
+
return value.startsWith("worktree:") ? `worktree:${shellAbsolutePath(value.slice("worktree:".length))}` : value;
|
|
64
|
+
}
|
|
43
65
|
function worktreeScopeCovers(target, scope) {
|
|
44
66
|
const rawRoot = scope.slice("worktree:".length);
|
|
67
|
+
if (process.platform === "win32") {
|
|
68
|
+
const canonical = (value) => {
|
|
69
|
+
return path.win32.normalize(nativePath(value).replaceAll("/", "\\")).replace(/[\\/]+$/u, "").toLowerCase();
|
|
70
|
+
};
|
|
71
|
+
const canonicalTarget = canonical(target);
|
|
72
|
+
const canonicalRoot = canonical(rawRoot);
|
|
73
|
+
return canonicalTarget === canonicalRoot || canonicalTarget.startsWith(`${canonicalRoot}\\`);
|
|
74
|
+
}
|
|
45
75
|
const trimmedRoot = rawRoot.replace(/\/+$/, "");
|
|
46
76
|
const root = trimmedRoot === "" && rawRoot.startsWith("/") ? "/" : trimmedRoot;
|
|
47
77
|
if (root === "/")
|
|
@@ -135,7 +165,10 @@ export function runRepoRunRegistry(args, deps) {
|
|
|
135
165
|
f.rmSync(lockDir, { recursive: true, force: true });
|
|
136
166
|
}
|
|
137
167
|
catch {
|
|
138
|
-
|
|
168
|
+
const displayLock = process.platform === "win32"
|
|
169
|
+
? `${root.replace(/[\\/]+$/u, "")}/workspace/orchestrator/active-runs.json.lock`
|
|
170
|
+
: lockDir;
|
|
171
|
+
throw new Error(`ERROR: could not remove stale registry lock: ${displayLock}`);
|
|
139
172
|
}
|
|
140
173
|
continue;
|
|
141
174
|
}
|
|
@@ -157,6 +190,10 @@ export function runRepoRunRegistry(args, deps) {
|
|
|
157
190
|
const value = Number(raw);
|
|
158
191
|
if (!Number.isFinite(value))
|
|
159
192
|
return false;
|
|
193
|
+
// Git Bash's kill -0 -1 probes all signalable processes and succeeds on
|
|
194
|
+
// Windows; Node's process.kill does not accept the same sentinel there.
|
|
195
|
+
if (process.platform === "win32" && value === -1)
|
|
196
|
+
return true;
|
|
160
197
|
try {
|
|
161
198
|
return deps.isPidAlive ? deps.isPidAlive(value) : (process.kill(value, 0), true);
|
|
162
199
|
}
|
|
@@ -207,7 +244,7 @@ export function runRepoRunRegistry(args, deps) {
|
|
|
207
244
|
catch { /* keep resolved path */ }
|
|
208
245
|
while (dir !== "/" && dir) {
|
|
209
246
|
if (f.existsSync(path.join(dir, ".git")))
|
|
210
|
-
return dir;
|
|
247
|
+
return process.platform === "win32" ? shellAbsolutePath(dir) : dir;
|
|
211
248
|
const parent = path.dirname(dir);
|
|
212
249
|
if (parent === dir)
|
|
213
250
|
break;
|
|
@@ -256,7 +293,8 @@ export function runRepoRunRegistry(args, deps) {
|
|
|
256
293
|
let runId = valueAfter(rest, "--run-id"), runPid = valueAfter(rest, "--pid"), sessionId = valueAfter(rest, "--session-id"), command = valueAfter(rest, "--command"), project = valueAfter(rest, "--project"), repo = valueAfter(rest, "--repo"), scope = valueAfter(rest, "--scope"), host = valueAfter(rest, "--host");
|
|
257
294
|
if (!runPid || !command || !repo || !scope)
|
|
258
295
|
return die("register requires --pid --command --repo --scope");
|
|
259
|
-
repo = pathAbs(repo, deps.cwd, f);
|
|
296
|
+
repo = shellAbsolutePath(pathAbs(repo, deps.cwd, f));
|
|
297
|
+
scope = shellScope(scope);
|
|
260
298
|
if (!runId)
|
|
261
299
|
runId = `${command.replaceAll("/", "-")}-${Math.floor(now().getTime() / 1000)}-${project || "none"}-${runPid}`;
|
|
262
300
|
if (!host)
|
|
@@ -357,7 +395,12 @@ export function runRepoRunRegistry(args, deps) {
|
|
|
357
395
|
const owners = runs.filter((run) => covers(run) && String(run?.pid) !== myPid && run?.session_id !== mySession);
|
|
358
396
|
if (!owners.length)
|
|
359
397
|
return output();
|
|
360
|
-
const lines = owners.map((run) =>
|
|
398
|
+
const lines = owners.map((run) => {
|
|
399
|
+
// This diagnostic interpolates the stored fields directly in Bash.
|
|
400
|
+
// Keep their original spelling (including a trailing slash) rather
|
|
401
|
+
// than canonicalizing paths only on the native side.
|
|
402
|
+
return `run_id=${run?.run_id} pid=${run?.pid} command=${run?.command} project=${run?.project} scope=${run?.scope} repo=${run?.repo_path} started=${run?.started_at} heartbeat=${run?.heartbeat_at}`;
|
|
403
|
+
}).join("\n") + "\n";
|
|
361
404
|
return output(2, "", lines);
|
|
362
405
|
}
|
|
363
406
|
return die(`unknown subcommand: ${sub}`);
|
|
@@ -10,6 +10,6 @@ export type SessionJournalOperationResult = {
|
|
|
10
10
|
/** Execute one native helper call while retaining its two output byte streams. */
|
|
11
11
|
export declare function runSessionJournalOperation(args: readonly string[], _stdin: string, cwd: string, hqRoot: string | undefined, dependencies?: SessionHelperRuntimeOverrides): SessionJournalOperationResult;
|
|
12
12
|
/** Native implementation of the fail-soft session journal helper. */
|
|
13
|
-
export declare function runSessionJournalFast(args: readonly string[],
|
|
13
|
+
export declare function runSessionJournalFast(args: readonly string[], _stdin: string, cwd: string, hqRoot: string | undefined, dependencies?: SessionHelperRuntimeOverrides): number;
|
|
14
14
|
export {};
|
|
15
15
|
//# sourceMappingURL=core-session-journal-native.d.ts.map
|
|
@@ -243,8 +243,8 @@ export function runSessionJournalOperation(args, _stdin, cwd, hqRoot, dependenci
|
|
|
243
243
|
return { code, stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr) };
|
|
244
244
|
}
|
|
245
245
|
/** Native implementation of the fail-soft session journal helper. */
|
|
246
|
-
export function runSessionJournalFast(args,
|
|
247
|
-
const result = runSessionJournalOperation(args,
|
|
246
|
+
export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies = {}) {
|
|
247
|
+
const result = runSessionJournalOperation(args, _stdin, cwd, hqRoot, dependencies);
|
|
248
248
|
process.stdout.write(result.stdout);
|
|
249
249
|
process.stderr.write(result.stderr);
|
|
250
250
|
return result.code;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Native port of share-suggestion-state.sh; kept dependency-light for hooks. */
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
|
|
4
5
|
import { isByteOrientedLocale } from "../lib/core-utils/byte-oriented-locale.js";
|
|
5
6
|
export const SHARE_SUGGESTION_STATE_ASSET = "core/scripts/share-suggestion-state.sh";
|
|
6
7
|
const isObj = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -170,10 +171,13 @@ export function runShareSuggestionState(args, stdin, deps) {
|
|
|
170
171
|
}
|
|
171
172
|
catch { /* locate the first existing non-directory parent when present */ }
|
|
172
173
|
}
|
|
174
|
+
const displayBlockedPath = platform === "win32"
|
|
175
|
+
? shellDisplayPath(blockedPath)
|
|
176
|
+
: blockedPath;
|
|
173
177
|
const reason = code === "ENOTDIR" ? "Not a directory" : code === "EACCES" || code === "EPERM" ? "Permission denied" : code === "EEXIST" ? "File exists" : (error instanceof Error ? error.message : String(error));
|
|
174
178
|
const mkdirError = platform === "darwin"
|
|
175
|
-
? `mkdir: ${
|
|
176
|
-
: `mkdir: cannot create directory ‘${
|
|
179
|
+
? `mkdir: ${displayBlockedPath}: ${reason}\n`
|
|
180
|
+
: `mkdir: cannot create directory ‘${displayBlockedPath}’: ${reason}\n`;
|
|
177
181
|
return failFromShellMain(`${mkdirError}share-suggestion-state: unable to create state directory\n`);
|
|
178
182
|
}
|
|
179
183
|
const sessionUnits = platform === "darwin" && !isByteOrientedLocale(env, platform)
|
|
@@ -4,6 +4,7 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { generateUlid } from "../lib/mesh/live/ulid.js";
|
|
6
6
|
import { formatSpoolLine } from "../lib/mesh/live/format-spool-line.js";
|
|
7
|
+
import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
|
|
7
8
|
export const WORK_MESH_LIVE_REBIND_ASSET = "core/scripts/work-mesh-live-rebind.sh";
|
|
8
9
|
const WORK_MESH_LIVE_REBIND_LIBRARY_ASSET = "core/scripts/lib/work-mesh-live-rebind.sh";
|
|
9
10
|
const USAGE = "usage: work-mesh-live-rebind.sh --session <sid> ...\n";
|
|
@@ -118,7 +119,7 @@ function enqueueLifecycle(kind, sid, harness, adapter, company, project, task, d
|
|
|
118
119
|
at: `${now.toISOString().slice(0, 19)}.000Z`,
|
|
119
120
|
seq,
|
|
120
121
|
cwd: deps.env.PWD ?? "",
|
|
121
|
-
hqRoot: deps.env.HQ_ROOT ?? deps.root,
|
|
122
|
+
hqRoot: shellDisplayPath(deps.env.HQ_ROOT ?? deps.root),
|
|
122
123
|
};
|
|
123
124
|
if (company)
|
|
124
125
|
event.companySlug = company;
|
|
@@ -247,7 +248,9 @@ export function runWorkMeshLiveRebind(args, deps) {
|
|
|
247
248
|
: cause?.code === "ENOSPC" ? "No space left on device"
|
|
248
249
|
: "Input/output error";
|
|
249
250
|
const script = path.join(deps.root, WORK_MESH_LIVE_REBIND_LIBRARY_ASSET);
|
|
250
|
-
|
|
251
|
+
const scriptPath = shellDisplayPath(script);
|
|
252
|
+
const targetPath = shellDisplayPath(error.target);
|
|
253
|
+
return { code: 1, stdout: "", stderr: `${scriptPath}: line 34: ${targetPath}: ${message}\n` };
|
|
251
254
|
}
|
|
252
255
|
}
|
|
253
256
|
export async function runWorkMeshLiveRebindFast(args, _stdin, cwd, root) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Isolated fast-entry adapter for native worker-skill linting. */
|
|
2
|
+
import { runWorkerSkillLint } from "./core-worker-lint-native.js";
|
|
3
|
+
export function runWorkerSkillLintFast(args, _stdin, cwd) {
|
|
4
|
+
const result = runWorkerSkillLint([...args], { cwd });
|
|
5
|
+
if (result.stdout)
|
|
6
|
+
process.stdout.write(result.stdout);
|
|
7
|
+
if (result.stderr)
|
|
8
|
+
process.stderr.write(result.stderr);
|
|
9
|
+
return result.code;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=core-worker-lint-fast.js.map
|
package/dist/commands/core.js
CHANGED
|
@@ -631,12 +631,15 @@ export function registerCoreCommands(program) {
|
|
|
631
631
|
}
|
|
632
632
|
});
|
|
633
633
|
describeCoreCommand(rebuildCommand, {
|
|
634
|
-
entries:
|
|
635
|
-
name:
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
634
|
+
entries: [
|
|
635
|
+
{ name: "rebuild-index", execution: "native", root: "live" },
|
|
636
|
+
...REBUILD_INDEX_TARGETS.map((entry) => ({
|
|
637
|
+
name: `rebuild-index ${entry.target}`,
|
|
638
|
+
execution: "native",
|
|
639
|
+
asset: entry.asset,
|
|
640
|
+
root: "live",
|
|
641
|
+
})),
|
|
642
|
+
],
|
|
640
643
|
});
|
|
641
644
|
for (const entry of SCAFFOLD_COMMANDS) {
|
|
642
645
|
const command = core
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -11,6 +11,55 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/** The relocated scripts that qualify for the entrypoint fast path. */
|
|
13
13
|
export const FAST_SCAFFOLD_SCRIPTS = [
|
|
14
|
+
...["notify", "probe", "remediate", "run"].map((name) => ({
|
|
15
|
+
name: `job ${name}`,
|
|
16
|
+
kind: "native",
|
|
17
|
+
modulePath: `./commands/core-job-${name}-fast.js`,
|
|
18
|
+
exportName: name === "notify" ? "runJobNotifyFast"
|
|
19
|
+
: name === "probe" ? "runJobProbeFast"
|
|
20
|
+
: name === "remediate" ? "runJobRemediateFast"
|
|
21
|
+
: "runJobFast",
|
|
22
|
+
flagKey: "hq-core-native-fast-route",
|
|
23
|
+
familyFlagKey: "core-native-jobs",
|
|
24
|
+
asset: `core/scripts/hq-job-${name}.sh`,
|
|
25
|
+
root: "live",
|
|
26
|
+
shellCwd: "live",
|
|
27
|
+
shellHqRoot: "live",
|
|
28
|
+
requiresNodeNetworkCompatibility: name === "probe" || name === "run",
|
|
29
|
+
})),
|
|
30
|
+
{
|
|
31
|
+
name: "pin",
|
|
32
|
+
kind: "native",
|
|
33
|
+
modulePath: "./commands/core-pin-fast.js",
|
|
34
|
+
exportName: "runPinFast",
|
|
35
|
+
flagKey: "hq-core-native-fast-route",
|
|
36
|
+
familyFlagKey: "core-native-utilities",
|
|
37
|
+
asset: "core/scripts/hq-pin.sh",
|
|
38
|
+
root: "live",
|
|
39
|
+
shellCwd: "live",
|
|
40
|
+
shellHqRoot: "live",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "worker lint",
|
|
44
|
+
kind: "native",
|
|
45
|
+
modulePath: "./commands/core-worker-lint-fast.js",
|
|
46
|
+
exportName: "runWorkerSkillLintFast",
|
|
47
|
+
flagKey: "hq-core-native-fast-route",
|
|
48
|
+
familyFlagKey: "core-native-worker-skills",
|
|
49
|
+
asset: "core/scripts/lint-shared-worker-skills.sh",
|
|
50
|
+
root: "cwd",
|
|
51
|
+
shellCwd: "caller",
|
|
52
|
+
shellHqRoot: "none",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "rebuild-index",
|
|
56
|
+
kind: "native",
|
|
57
|
+
modulePath: "./commands/core-rebuild-index-fast.js",
|
|
58
|
+
exportName: "runRebuildIndexFast",
|
|
59
|
+
flagKey: "hq-core-native-fast-route",
|
|
60
|
+
root: "live",
|
|
61
|
+
fallbackToCommandGraph: true,
|
|
62
|
+
},
|
|
14
63
|
{
|
|
15
64
|
name: "checkpoint-stop-gate",
|
|
16
65
|
kind: "native",
|
|
@@ -9,6 +9,7 @@ export type CoreIo = {
|
|
|
9
9
|
env?: NodeJS.ProcessEnv;
|
|
10
10
|
cwd?: string;
|
|
11
11
|
now?: () => Date;
|
|
12
|
+
randomBytes?: (size: number) => Buffer;
|
|
12
13
|
};
|
|
13
14
|
export declare function worktreeGcPathKey(value: string): string;
|
|
14
15
|
export declare function runWorktreeGc(args: readonly string[], hqRoot: string, io?: CoreIo): number;
|
|
@@ -5,6 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { serializeJqJson } from "./jq-json-serializer.js";
|
|
8
|
+
import { shellDisplayPath } from "./shell-display-path.js";
|
|
8
9
|
export const WORKTREE_GC_ASSET = "core/scripts/worktree-gc.sh";
|
|
9
10
|
export const CLEAN_HANDOFF_ASSET = "core/scripts/clean-worktree-reconcile-handoff.sh";
|
|
10
11
|
export const RESUME_LOCK_ASSET = "core/scripts/resume-thread-lock.sh";
|
|
@@ -13,6 +14,7 @@ const stdout = (io, value) => (io.stdout ?? ((v) => process.stdout.write(v)))(va
|
|
|
13
14
|
const stderr = (io, value) => (io.stderr ?? ((v) => process.stderr.write(v)))(value);
|
|
14
15
|
const envOf = (io) => io.env ?? process.env;
|
|
15
16
|
const nowOf = (io) => (io.now ?? (() => new Date()))();
|
|
17
|
+
const randomBytesOf = (io, size) => (io.randomBytes ?? randomBytes)(size);
|
|
16
18
|
const stripTrailingNewlines = (value) => value.replace(/\n+$/u, "");
|
|
17
19
|
function git(args, options = {}) {
|
|
18
20
|
const childEnv = options.env;
|
|
@@ -61,6 +63,23 @@ function fileExists(file) {
|
|
|
61
63
|
return false;
|
|
62
64
|
}
|
|
63
65
|
}
|
|
66
|
+
function sameDirectory(left, right) {
|
|
67
|
+
try {
|
|
68
|
+
const a = fs.statSync(left), b = fs.statSync(right);
|
|
69
|
+
if (a.dev !== 0 && b.dev !== 0 && a.ino !== 0 && b.ino !== 0)
|
|
70
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
71
|
+
}
|
|
72
|
+
catch { /* compare canonical spellings below */ }
|
|
73
|
+
const canonical = (value) => path.resolve(value).replace(/^\\\\\?\\/u, "").replaceAll("/", "\\").toLowerCase();
|
|
74
|
+
return canonical(left) === canonical(right);
|
|
75
|
+
}
|
|
76
|
+
function shellJsonPath(value) {
|
|
77
|
+
const display = shellDisplayPath(value);
|
|
78
|
+
if (process.platform !== "win32")
|
|
79
|
+
return display;
|
|
80
|
+
const match = /^\/([a-z])\/(.*)$/u.exec(display);
|
|
81
|
+
return match?.[1] && match[2] !== undefined ? `${match[1].toUpperCase()}:/${match[2]}` : display;
|
|
82
|
+
}
|
|
64
83
|
function statMtime(file) {
|
|
65
84
|
try {
|
|
66
85
|
return Math.floor(fs.statSync(file).mtimeMs / 1000);
|
|
@@ -119,7 +138,10 @@ function sessionReferences(hqRoot, worktree) {
|
|
|
119
138
|
path.join(hqRoot, "workspace/threads/handoff.json"),
|
|
120
139
|
path.join(hqRoot, "workspace/locks"),
|
|
121
140
|
];
|
|
122
|
-
|
|
141
|
+
// The shell receives the Git Bash spelling of this path. Session references
|
|
142
|
+
// written by shell hooks use that spelling too, while Node's win32 path is
|
|
143
|
+
// drive-letter based. Compare the representation the shell actually sees.
|
|
144
|
+
const needle = Buffer.from(shellDisplayPath(worktree), "utf8");
|
|
123
145
|
const visit = (target) => {
|
|
124
146
|
let info;
|
|
125
147
|
try {
|
|
@@ -187,7 +209,16 @@ function duSizeKb(root) {
|
|
|
187
209
|
return;
|
|
188
210
|
if (info.isFile())
|
|
189
211
|
seen.add(inode);
|
|
190
|
-
|
|
212
|
+
// Windows Git Bash du counts file payloads in 1 KiB units. Node's Windows
|
|
213
|
+
// directory sizes include metadata that du does not count, so omit those
|
|
214
|
+
// directory entries and round file payloads as du -sk does.
|
|
215
|
+
bytes += process.platform === "win32" && info.isDirectory()
|
|
216
|
+
? 0
|
|
217
|
+
: typeof info.blocks === "number" && !(process.platform === "win32" && info.blocks === 0)
|
|
218
|
+
? info.blocks * 512
|
|
219
|
+
: process.platform === "win32"
|
|
220
|
+
? Math.max(1, Math.ceil(info.size / 1024)) * 1024
|
|
221
|
+
: info.size;
|
|
191
222
|
if (info.isDirectory()) {
|
|
192
223
|
let children;
|
|
193
224
|
try {
|
|
@@ -338,9 +369,9 @@ export function runWorktreeGc(args, hqRoot, io = {}) {
|
|
|
338
369
|
const skip = (wt, repo, branch, isHqRoot, ageDays, sizeKb, reason) => {
|
|
339
370
|
skipped++;
|
|
340
371
|
if (reason === "remove-failed")
|
|
341
|
-
record({ path: wt, repo, branch, hq_root: isHqRoot, age_days: ageDays, size_kb: sizeKb, action: "skipped", reason });
|
|
372
|
+
record({ path: shellJsonPath(wt), repo: shellJsonPath(repo), branch, hq_root: isHqRoot, age_days: ageDays, size_kb: sizeKb, action: "skipped", reason });
|
|
342
373
|
if (!json)
|
|
343
|
-
stdout(io, ` skip [${reason}] ${wt}\n`);
|
|
374
|
+
stdout(io, ` skip [${reason}] ${shellDisplayPath(wt)}\n`);
|
|
344
375
|
};
|
|
345
376
|
const runFetch = (repo) => {
|
|
346
377
|
const timeoutName = executableOnPath("timeout", env) ? "timeout" : executableOnPath("gtimeout", env) ? "gtimeout" : undefined;
|
|
@@ -372,11 +403,7 @@ export function runWorktreeGc(args, hqRoot, io = {}) {
|
|
|
372
403
|
skip(wt, "", "", false, null, null, "git-error");
|
|
373
404
|
continue;
|
|
374
405
|
}
|
|
375
|
-
|
|
376
|
-
try {
|
|
377
|
-
isHqRoot = fs.realpathSync(mainRepo) === fs.realpathSync(hqRoot);
|
|
378
|
-
}
|
|
379
|
-
catch { /* both paths are already resolved by the core root resolver */ }
|
|
406
|
+
const isHqRoot = sameDirectory(mainRepo, hqRoot);
|
|
380
407
|
const branchResult = gitText(["-C", wt, "symbolic-ref", "--short", "HEAD"]);
|
|
381
408
|
const branch = branchResult.status === 0 ? commandOutput(branchResult) : "";
|
|
382
409
|
const headResult = gitText(["-C", wt, "rev-parse", "HEAD"]);
|
|
@@ -401,7 +428,9 @@ export function runWorktreeGc(args, hqRoot, io = {}) {
|
|
|
401
428
|
continue;
|
|
402
429
|
}
|
|
403
430
|
let createdEpoch = wtMtime;
|
|
404
|
-
|
|
431
|
+
// worktree-gc.sh hashes the path as Bash/Git Bash spells it. On Windows a
|
|
432
|
+
// native path (C:\...) and the shell path (/c/...) have different keys.
|
|
433
|
+
const stampFile = path.join(metaDir, `${worktreeGcPathKey(shellDisplayPath(wt))}.json`);
|
|
405
434
|
if (fileExists(stampFile)) {
|
|
406
435
|
try {
|
|
407
436
|
const stamp = JSON.parse(fs.readFileSync(stampFile, "utf8"));
|
|
@@ -465,9 +494,9 @@ export function runWorktreeGc(args, hqRoot, io = {}) {
|
|
|
465
494
|
if (!apply) {
|
|
466
495
|
would++;
|
|
467
496
|
reclaimedKb += sizeKb;
|
|
468
|
-
record({ path: wt, repo: mainRepo, branch, hq_root: isHqRoot, age_days: ageDays, size_kb: sizeKb, action: "would-remove", reason: "safe" });
|
|
497
|
+
record({ path: shellJsonPath(wt), repo: shellJsonPath(mainRepo), branch, hq_root: isHqRoot, age_days: ageDays, size_kb: sizeKb, action: "would-remove", reason: "safe" });
|
|
469
498
|
if (!json)
|
|
470
|
-
stdout(io, ` DRY would remove (${Math.floor(sizeKb / 1024)} MB, ${ageDays}d) ${wt}\n`);
|
|
499
|
+
stdout(io, ` DRY would remove (${Math.floor(sizeKb / 1024)} MB, ${ageDays}d) ${shellDisplayPath(wt)}\n`);
|
|
471
500
|
continue;
|
|
472
501
|
}
|
|
473
502
|
if (wtGit(mainRepo, isHqRoot, ["worktree", "remove", wt]).status !== 0) {
|
|
@@ -483,9 +512,9 @@ export function runWorktreeGc(args, hqRoot, io = {}) {
|
|
|
483
512
|
catch { /* rm -f */ }
|
|
484
513
|
removed++;
|
|
485
514
|
reclaimedKb += sizeKb;
|
|
486
|
-
record({ path: wt, repo: mainRepo, branch, hq_root: isHqRoot, age_days: ageDays, size_kb: sizeKb, action: "removed", reason: "safe" });
|
|
515
|
+
record({ path: shellJsonPath(wt), repo: shellJsonPath(mainRepo), branch, hq_root: isHqRoot, age_days: ageDays, size_kb: sizeKb, action: "removed", reason: "safe" });
|
|
487
516
|
if (!json)
|
|
488
|
-
stdout(io, ` GC removed (${Math.floor(sizeKb / 1024)} MB, ${ageDays}d) ${wt}\n`);
|
|
517
|
+
stdout(io, ` GC removed (${Math.floor(sizeKb / 1024)} MB, ${ageDays}d) ${shellDisplayPath(wt)}\n`);
|
|
489
518
|
}
|
|
490
519
|
const reclaimedGb = Number((reclaimedKb / 1024 / 1024).toFixed(2));
|
|
491
520
|
if (json) {
|
|
@@ -554,18 +583,18 @@ export function runCleanWorktreeReconcileHandoff(args, io = {}) {
|
|
|
554
583
|
}
|
|
555
584
|
const rawRoot = args[0] ?? "";
|
|
556
585
|
const branch = args[1] ?? "";
|
|
557
|
-
if (!rawRoot.startsWith("/")) {
|
|
586
|
+
if (!rawRoot.startsWith("/") && !(process.platform === "win32" && path.win32.isAbsolute(rawRoot))) {
|
|
558
587
|
stderr(io, `ERROR: HQ root must be an absolute path\n`);
|
|
559
588
|
cleanUsage(io);
|
|
560
589
|
return 64;
|
|
561
590
|
}
|
|
562
591
|
if (!fileExists(rawRoot) || !fs.statSync(rawRoot).isDirectory()) {
|
|
563
|
-
return cleanGitError(io, `ERROR: HQ root is not a directory: ${rawRoot}`);
|
|
592
|
+
return cleanGitError(io, `ERROR: HQ root is not a directory: ${shellDisplayPath(rawRoot)}`);
|
|
564
593
|
}
|
|
565
594
|
const hqRoot = realPathOrResolved(rawRoot);
|
|
566
595
|
const top = gitText(["-C", hqRoot, "rev-parse", "--show-toplevel"]);
|
|
567
596
|
if (top.status !== 0)
|
|
568
|
-
return cleanGitError(io, `ERROR: HQ root is not a Git worktree: ${hqRoot}`);
|
|
597
|
+
return cleanGitError(io, `ERROR: HQ root is not a Git worktree: ${shellDisplayPath(hqRoot)}`);
|
|
569
598
|
const topLevel = realPathOrResolved(commandOutput(top));
|
|
570
599
|
if (topLevel !== hqRoot)
|
|
571
600
|
return cleanGitError(io, `ERROR: supplied path is not the Git worktree root: ${hqRoot}`);
|
|
@@ -640,7 +669,7 @@ export function runCleanWorktreeReconcileHandoff(args, io = {}) {
|
|
|
640
669
|
let movedFile = "";
|
|
641
670
|
try {
|
|
642
671
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
643
|
-
const candidate = path.join(destinationDir, `.clean-worktree-handoff.${
|
|
672
|
+
const candidate = path.join(destinationDir, `.clean-worktree-handoff.${randomBytesOf(io, 4).toString("hex").slice(0, 6)}`);
|
|
644
673
|
try {
|
|
645
674
|
fs.writeFileSync(candidate, "", { flag: "wx", mode: 0o600 });
|
|
646
675
|
movedFile = candidate;
|
|
@@ -769,12 +798,12 @@ function lockState(threadId, relativeLockPath, lockDir, recordPath, staleSeconds
|
|
|
769
798
|
}
|
|
770
799
|
return locked(record.session_id, record.resumed_at, record.generation);
|
|
771
800
|
}
|
|
772
|
-
function writeLockRecord(lockDir, recordPath, threadId, sessionId, now) {
|
|
801
|
+
function writeLockRecord(lockDir, recordPath, threadId, sessionId, now, io) {
|
|
773
802
|
const epoch = Math.floor(now.getTime() / 1000);
|
|
774
803
|
const timestamp = now.toISOString().replace(/\.\d{3}Z$/u, "Z");
|
|
775
804
|
let temporary = "";
|
|
776
805
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
777
|
-
const generation = `.current.${
|
|
806
|
+
const generation = `.current.${randomBytesOf(io, 8).toString("base64url").slice(0, 6)}`;
|
|
778
807
|
const candidate = path.join(lockDir, generation);
|
|
779
808
|
try {
|
|
780
809
|
fs.writeFileSync(candidate, serializeJqJson({ version: 1, thread_id: threadId, session_id: sessionId, resumed_at: timestamp, resumed_epoch: epoch, generation }, "compact"), { flag: "wx", mode: 0o600 });
|
|
@@ -858,7 +887,7 @@ export function runResumeThreadLock(args, hqRoot, io = {}) {
|
|
|
858
887
|
fs.mkdirSync(locksDir, { recursive: true });
|
|
859
888
|
try {
|
|
860
889
|
fs.mkdirSync(lockDir);
|
|
861
|
-
const generation = writeLockRecord(lockDir, recordPath, threadId, sessionId, now());
|
|
890
|
+
const generation = writeLockRecord(lockDir, recordPath, threadId, sessionId, now(), io);
|
|
862
891
|
emit({ status: "acquired", thread_id: threadId, lock_path: relativeLockPath, session_id: sessionId, lock_generation: generation });
|
|
863
892
|
return 0;
|
|
864
893
|
}
|
|
@@ -915,7 +944,7 @@ export function runResumeThreadLock(args, hqRoot, io = {}) {
|
|
|
915
944
|
emit(current);
|
|
916
945
|
return 4;
|
|
917
946
|
}
|
|
918
|
-
const generation = writeLockRecord(lockDir, recordPath, threadId, sessionId, now());
|
|
947
|
+
const generation = writeLockRecord(lockDir, recordPath, threadId, sessionId, now(), io);
|
|
919
948
|
emit({ status: "acquired", thread_id: threadId, lock_path: relativeLockPath, session_id: sessionId, lock_generation: generation });
|
|
920
949
|
return 0;
|
|
921
950
|
}
|
|
@@ -1008,7 +1037,7 @@ async function drainResponseBody(response) {
|
|
|
1008
1037
|
export async function runVerifyStoryDeliverables(args, hqRoot, io = {}) {
|
|
1009
1038
|
const env = envOf(io);
|
|
1010
1039
|
let prdPath = "", storyId = "", repo = "";
|
|
1011
|
-
let json = false, strict = false, write = false;
|
|
1040
|
+
let json = false, strict = false, write = false, repoExplicit = false;
|
|
1012
1041
|
let evidenceJson = "[]", commitsJson = "[]";
|
|
1013
1042
|
let offline = env.HQ_DELIVERABLES_OFFLINE ?? "0";
|
|
1014
1043
|
for (let index = 0; index < args.length;) {
|
|
@@ -1021,8 +1050,10 @@ export async function runVerifyStoryDeliverables(args, hqRoot, io = {}) {
|
|
|
1021
1050
|
prdPath = value;
|
|
1022
1051
|
else if (arg === "--story")
|
|
1023
1052
|
storyId = value;
|
|
1024
|
-
else if (arg === "--repo")
|
|
1053
|
+
else if (arg === "--repo") {
|
|
1025
1054
|
repo = value;
|
|
1055
|
+
repoExplicit = true;
|
|
1056
|
+
}
|
|
1026
1057
|
else if (arg === "--evidence-json")
|
|
1027
1058
|
evidenceJson = value;
|
|
1028
1059
|
else
|
|
@@ -1058,9 +1089,12 @@ export async function runVerifyStoryDeliverables(args, hqRoot, io = {}) {
|
|
|
1058
1089
|
stderr(io, `usage: --prd <prd.json> --story <id> [--repo <path>]\n`);
|
|
1059
1090
|
return 2;
|
|
1060
1091
|
}
|
|
1092
|
+
const displayedPrdPath = process.platform === "win32" && path.win32.isAbsolute(prdPath)
|
|
1093
|
+
? shellDisplayPath(prdPath)
|
|
1094
|
+
: prdPath;
|
|
1061
1095
|
const prdFilePath = path.resolve(hqRoot, prdPath);
|
|
1062
1096
|
if (!isReadable(prdFilePath)) {
|
|
1063
|
-
stderr(io, `verify-story-deliverables: cannot read ${
|
|
1097
|
+
stderr(io, `verify-story-deliverables: cannot read ${displayedPrdPath}\n`);
|
|
1064
1098
|
return 2;
|
|
1065
1099
|
}
|
|
1066
1100
|
let prd;
|
|
@@ -1073,7 +1107,7 @@ export async function runVerifyStoryDeliverables(args, hqRoot, io = {}) {
|
|
|
1073
1107
|
const userStories = Array.isArray(prd.userStories) ? prd.userStories : [];
|
|
1074
1108
|
const story = userStories.find((item) => item && item.id === storyId);
|
|
1075
1109
|
if (!story) {
|
|
1076
|
-
stderr(io, `verify-story-deliverables: story ${storyId} not in ${
|
|
1110
|
+
stderr(io, `verify-story-deliverables: story ${storyId} not in ${displayedPrdPath}\n`);
|
|
1077
1111
|
return 2;
|
|
1078
1112
|
}
|
|
1079
1113
|
if (!repo) {
|
|
@@ -1081,7 +1115,10 @@ export async function runVerifyStoryDeliverables(args, hqRoot, io = {}) {
|
|
|
1081
1115
|
? prd.metadata : {};
|
|
1082
1116
|
repo = jqText(metadata.repoPath);
|
|
1083
1117
|
}
|
|
1084
|
-
|
|
1118
|
+
// The shell sees Git Bash's /c/... spelling, which remains absolute. Native
|
|
1119
|
+
// receives the corresponding Win32 C:\\... path; preserve that absolute
|
|
1120
|
+
// location instead of prefixing HQ_ROOT and pointing at a nonexistent path.
|
|
1121
|
+
if (repo && !repo.startsWith("/") && !(repoExplicit && path.win32.isAbsolute(repo)))
|
|
1085
1122
|
repo = `${hqRoot}/${repo}`;
|
|
1086
1123
|
const evidence = jsonDecode(evidenceJson);
|
|
1087
1124
|
if (!jqArrayCheck(evidence)) {
|
|
@@ -1099,7 +1136,7 @@ export async function runVerifyStoryDeliverables(args, hqRoot, io = {}) {
|
|
|
1099
1136
|
// deliverables are left to jq's normal processing behavior.
|
|
1100
1137
|
const refsRaw = declaredRaw;
|
|
1101
1138
|
if (!Array.isArray(refsRaw)) {
|
|
1102
|
-
stderr(io, `verify-story-deliverables: story ${storyId} not in ${
|
|
1139
|
+
stderr(io, `verify-story-deliverables: story ${storyId} not in ${displayedPrdPath}\n`);
|
|
1103
1140
|
return 2;
|
|
1104
1141
|
}
|
|
1105
1142
|
}
|
|
@@ -1138,11 +1175,11 @@ export async function runVerifyStoryDeliverables(args, hqRoot, io = {}) {
|
|
|
1138
1175
|
else if (kind === "repo-path")
|
|
1139
1176
|
ok = Boolean(repo) && fileExists(`${repo}/${value}`);
|
|
1140
1177
|
else if (kind === "branch" && repo && fileExists(repo)) {
|
|
1141
|
-
ok = git(["-C", repo, "show-ref", "--verify", "--quiet", `refs/heads/${value}`]).status === 0 ||
|
|
1142
|
-
git(["-C", repo, "show-ref", "--verify", "--quiet", `refs/remotes/origin/${value}`]).status === 0;
|
|
1178
|
+
ok = git(["-C", repo, "show-ref", "--verify", "--quiet", `refs/heads/${value}`], { env }).status === 0 ||
|
|
1179
|
+
git(["-C", repo, "show-ref", "--verify", "--quiet", `refs/remotes/origin/${value}`], { env }).status === 0;
|
|
1143
1180
|
}
|
|
1144
1181
|
else if (kind === "commit" && repo && fileExists(repo)) {
|
|
1145
|
-
ok = git(["-C", repo, "cat-file", "-e", `${value}^{commit}`]).status === 0;
|
|
1182
|
+
ok = git(["-C", repo, "cat-file", "-e", `${value}^{commit}`], { env }).status === 0;
|
|
1146
1183
|
}
|
|
1147
1184
|
else if (kind === "url")
|
|
1148
1185
|
ok = offline === "1" || await urlIsPresent(value);
|
|
@@ -65,6 +65,16 @@ export interface UnitPlan {
|
|
|
65
65
|
drainSignal?: "SIGUSR2";
|
|
66
66
|
/** A new CLI version alone does not restart this unit; it restarts itself when idle. */
|
|
67
67
|
keepAcrossCliUpdates?: true;
|
|
68
|
+
/**
|
|
69
|
+
* Scheduled units only: before each timed run the supervisor asks
|
|
70
|
+
* `SupervisorDeps.runGuard` whether there is work. `spawn` runs as usual,
|
|
71
|
+
* `idle` runs at most once per `idleIntervalSec`, `skip` does not run. A
|
|
72
|
+
* guard error, or no guard in the supervisor, runs as usual.
|
|
73
|
+
*/
|
|
74
|
+
runGuard?: {
|
|
75
|
+
name: "lanes-reconcile";
|
|
76
|
+
idleIntervalSec: number;
|
|
77
|
+
};
|
|
68
78
|
}
|
|
69
79
|
export interface PlanContext {
|
|
70
80
|
machineType: MachineType;
|
|
@@ -230,10 +230,18 @@ function planService(service, ctx) {
|
|
|
230
230
|
return [
|
|
231
231
|
base(service, {
|
|
232
232
|
kind: "scheduled",
|
|
233
|
+
// Polls every 15 s so keep-alive resumes are admitted on time (#1396).
|
|
234
|
+
// Each pass is a fresh `hq` node process (~1.8 s CPU, ~200 MB RSS), so
|
|
235
|
+
// desktops first check the lane store in-process
|
|
236
|
+
// (lanes-reconcile-guard.ts) and fall back to the pre-#1396 60 s
|
|
237
|
+
// cadence, or no run, when no lane can need the 15 s poll.
|
|
233
238
|
intervalSec: 15,
|
|
234
239
|
initialDelaySec: 30,
|
|
235
240
|
command: { type: "hq", args: ["lanes", "reconcile"] },
|
|
236
241
|
env: ctx.hqRoot ? { HQ_ROOT: ctx.hqRoot } : {},
|
|
242
|
+
...(ctx.machineType === "desktop"
|
|
243
|
+
? { runGuard: { name: "lanes-reconcile", idleIntervalSec: 60 } }
|
|
244
|
+
: {}),
|
|
237
245
|
blocked: firstBlock([[!ctx.hqRoot, NO_HQ_ROOT]]),
|
|
238
246
|
}),
|
|
239
247
|
];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-spawn check for the desktop `lanes-reconcile` unit.
|
|
3
|
+
*
|
|
4
|
+
* Every reconcile pass is a fresh `hq` node process (~1.8 s CPU, ~200 MB RSS),
|
|
5
|
+
* so a 15-second poll costs about 12% of a core even when the lane store holds
|
|
6
|
+
* nothing to do. This check reads the lane store in-process (readdir + small
|
|
7
|
+
* JSON reads, no child process) and tells the supervisor how urgent the next
|
|
8
|
+
* pass is:
|
|
9
|
+
*
|
|
10
|
+
* - `spawn`: something the 15-second cadence exists for may be due — lane
|
|
11
|
+
* admission, a running lane's heartbeat, or a keep-alive resume (#1396).
|
|
12
|
+
* - `idle`: only work that ran on the pre-#1396 60-second cadence remains
|
|
13
|
+
* (terminal lanes, worker-profile resumes, question aging, Codex rollout
|
|
14
|
+
* retention, lead-autopilot registry). The supervisor runs it at most once
|
|
15
|
+
* per `idleIntervalSec`.
|
|
16
|
+
* - `skip`: the store holds no lane or question records, no lead registry and
|
|
17
|
+
* no lanes config, so a reconcile pass has no input to act on.
|
|
18
|
+
*
|
|
19
|
+
* Any error reading the store returns `spawn` (fail open), as does any record
|
|
20
|
+
* this check does not understand.
|
|
21
|
+
*
|
|
22
|
+
* The paths below mirror `LANES_STORE_REL`, `LANE_RECORDS_DIR`,
|
|
23
|
+
* `QUESTION_RECORDS_DIR` (lanes/store.ts), `LANES_CONFIG_REL` (lanes/config.ts)
|
|
24
|
+
* and `LEAD_REGISTRY_REL` (lanes/autopilot-registry.ts). They are copied, not
|
|
25
|
+
* imported, so the daemon does not load the lanes modules; a test pins them to
|
|
26
|
+
* the originals.
|
|
27
|
+
*/
|
|
28
|
+
import type { UnitPlan } from "./catalog.js";
|
|
29
|
+
export type LanesReconcileVerdict = "spawn" | "idle" | "skip";
|
|
30
|
+
export declare const LANES_STORE_DIR: string;
|
|
31
|
+
export declare const LANE_RECORDS_SUBDIR = "lanes";
|
|
32
|
+
export declare const QUESTION_RECORDS_SUBDIR = "questions";
|
|
33
|
+
export declare const LANES_CONFIG_FILE: string;
|
|
34
|
+
export declare const LEAD_REGISTRY_FILE: string;
|
|
35
|
+
export interface LanesReconcileGuardFs {
|
|
36
|
+
readdirSync: (dir: string) => string[];
|
|
37
|
+
readFileSync: (file: string, encoding: "utf8") => string;
|
|
38
|
+
existsSync: (file: string) => boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function lanesReconcileVerdict(hqRoot: string | undefined, fsDeps?: LanesReconcileGuardFs): LanesReconcileVerdict;
|
|
41
|
+
/** The supervisor's `runGuard`: dispatches on the unit's declared guard. */
|
|
42
|
+
export declare function daemonRunGuard(unit: Pick<UnitPlan, "runGuard" | "env">): LanesReconcileVerdict;
|
|
43
|
+
//# sourceMappingURL=lanes-reconcile-guard.d.ts.map
|