@indigoai-us/hq-cli 5.273.0-alpha.0 → 5.273.0
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 +25 -2
- package/dist/command-catalog.generated.d.ts +11 -536
- package/dist/command-catalog.generated.js +14 -691
- package/dist/commands/daemon.d.ts +0 -3
- package/dist/commands/daemon.js +1 -173
- package/dist/commands/lanes-checks.d.ts +0 -1
- package/dist/commands/lanes-checks.js +217 -54
- package/dist/commands/lanes-hold.js +40 -2
- package/dist/commands/lanes-message.js +98 -14
- package/dist/commands/lanes-shared.d.ts +3 -2
- package/dist/commands/lanes.d.ts +2 -4
- package/dist/commands/lanes.js +3 -23
- package/dist/commands/sync-mode.d.ts +0 -11
- package/dist/commands/sync-mode.js +0 -30
- package/dist/lib/daemon/catalog.d.ts +2 -18
- package/dist/lib/daemon/catalog.js +2 -18
- package/dist/lib/daemon/config.d.ts +0 -6
- package/dist/lib/daemon/config.js +2 -15
- package/dist/lib/daemon/requests.d.ts +0 -4
- package/dist/lib/daemon/requests.js +0 -2
- package/dist/lib/daemon/run.js +19 -78
- package/dist/lib/daemon/supervisor.d.ts +0 -8
- package/dist/lib/daemon/supervisor.js +1 -101
- package/dist/lib/doctor/checks/scaffold-cli-floor.d.ts +7 -0
- package/dist/lib/doctor/checks/scaffold-cli-floor.js +73 -0
- package/dist/lib/doctor/registry.js +2 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +6 -1
- package/dist/lib/lanes/hire-grant-dispatch.js +78 -3
- package/dist/lib/lanes/keep-alive.d.ts +3 -2
- package/dist/lib/lanes/keep-alive.js +2 -2
- package/dist/lib/lanes/merge-command.d.ts +1 -0
- package/dist/lib/lanes/merge-command.js +299 -33
- package/dist/lib/lanes/orphans.d.ts +7 -0
- package/dist/lib/lanes/orphans.js +39 -4
- package/dist/lib/lanes/tracker-store.d.ts +2 -18
- package/dist/lib/lanes/tracker-store.js +1 -172
- package/dist/lib/scaffold-cli-floor.d.ts +9 -0
- package/dist/lib/scaffold-cli-floor.js +50 -0
- package/package.json +2 -2
- package/dist/commands/lanes-import.d.ts +0 -14
- package/dist/commands/lanes-import.js +0 -1210
- package/dist/commands/lanes-tracker.d.ts +0 -18
- package/dist/commands/lanes-tracker.js +0 -452
- package/dist/lib/daemon/sync-runner.d.ts +0 -58
- package/dist/lib/daemon/sync-runner.js +0 -277
package/dist/lib/daemon/run.js
CHANGED
|
@@ -16,7 +16,7 @@ import { agentKitPaths } from "../agent-kit/paths.js";
|
|
|
16
16
|
import { readBotConfig } from "../bot/config.js";
|
|
17
17
|
import { botsRoot } from "../bot/paths.js";
|
|
18
18
|
import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits } from "./catalog.js";
|
|
19
|
-
import {
|
|
19
|
+
import { readDaemonConfig } from "./config.js";
|
|
20
20
|
import { findLegacyUnits, legacyOwnerLookup } from "./legacy.js";
|
|
21
21
|
import { readLegacySyncEnv } from "./legacy-sync-env.js";
|
|
22
22
|
import { acquireDaemonLock, releaseDaemonLock } from "./lock.js";
|
|
@@ -26,7 +26,6 @@ import { appendPrivate, daemonPaths, ensurePrivateDir, openPrivateAppend } from
|
|
|
26
26
|
import { takeControlRequests } from "./requests.js";
|
|
27
27
|
import { currentHqCloud, hqCloudRunners } from "./hq-cloud.js";
|
|
28
28
|
import { runnerSessionName } from "./remote-control.js";
|
|
29
|
-
import { listSyncRunnerProcesses, readWatchRunnerOwnerStatus, syncHeartbeatTimeoutMs, syncRunnerNodeOptions, watchRunnerOwnerStatusPath, } from "./sync-runner.js";
|
|
30
29
|
import { STOP_ESCALATION_MS, createSupervisor } from "./supervisor.js";
|
|
31
30
|
import { userToolDirs } from "./tools.js";
|
|
32
31
|
export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
|
|
@@ -214,10 +213,7 @@ function makeLogger(paths, foreground) {
|
|
|
214
213
|
* an on-failure unit down. The daemon's updater handles CLI updates.
|
|
215
214
|
*/
|
|
216
215
|
export function unitProcessEnv(base, unit) {
|
|
217
|
-
|
|
218
|
-
if (unit.service === "sync")
|
|
219
|
-
env.NODE_OPTIONS = syncRunnerNodeOptions(env);
|
|
220
|
-
return env;
|
|
216
|
+
return { ...base, ...unit.env, HQ_DAEMON_UNIT: unit.id, HQ_NO_UPDATE_CHECK: "1" };
|
|
221
217
|
}
|
|
222
218
|
export async function runDaemon(opts) {
|
|
223
219
|
const env = opts.env ?? process.env;
|
|
@@ -262,11 +258,10 @@ export async function runDaemon(opts) {
|
|
|
262
258
|
return legacyCache.units;
|
|
263
259
|
};
|
|
264
260
|
let warnedMissingPin = null;
|
|
265
|
-
const daemonSyncRunnerPids = new Set();
|
|
266
261
|
const plan = () => {
|
|
267
262
|
const config = readDaemonConfig(paths.configPath);
|
|
268
263
|
const legacy = legacyUnits();
|
|
269
|
-
const configured = config.hqRoot ??
|
|
264
|
+
const configured = config.hqRoot ?? env.HQ_ROOT?.trim() ?? undefined;
|
|
270
265
|
const hqRoot = configured && fs.existsSync(configured) ? configured : undefined;
|
|
271
266
|
// Re-picked every reconcile, so a newer hq-cloud from the updater is used as soon as it lands.
|
|
272
267
|
const hqCloud = currentHqCloud(paths.root);
|
|
@@ -278,33 +273,7 @@ export async function runDaemon(opts) {
|
|
|
278
273
|
}
|
|
279
274
|
}
|
|
280
275
|
const runners = hqCloud ? hqCloudRunners(hqCloud.root) : { syncRunner: null, heartbeat: null, sessionHost: null };
|
|
281
|
-
|
|
282
|
-
const isSignedIn = signedIn(env, home, log);
|
|
283
|
-
const syncOwner = effectiveSyncOwner(config, machineType);
|
|
284
|
-
let syncRunnerProcesses = [];
|
|
285
|
-
let syncRunnerOwnerPid = null;
|
|
286
|
-
if (machineType === "desktop" &&
|
|
287
|
-
syncOwner === "daemon" &&
|
|
288
|
-
services.includes("sync") &&
|
|
289
|
-
!config.syncPaused &&
|
|
290
|
-
hqRoot &&
|
|
291
|
-
isSignedIn) {
|
|
292
|
-
const processList = listSyncRunnerProcesses(platform);
|
|
293
|
-
if (!processList.ok) {
|
|
294
|
-
syncRunnerProcesses = null;
|
|
295
|
-
log("warn", `${processList.error}; sync will wait before taking ownership`);
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
syncRunnerProcesses = processList.processes;
|
|
299
|
-
try {
|
|
300
|
-
syncRunnerOwnerPid = readWatchRunnerOwnerStatus(watchRunnerOwnerStatusPath(hqRoot, home, env))?.pid ?? null;
|
|
301
|
-
}
|
|
302
|
-
catch (error) {
|
|
303
|
-
log("warn", `could not verify the sync watch-owner status: ${error instanceof Error ? error.message : String(error)}`);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return planUnits(services, {
|
|
276
|
+
return planUnits(enabledServices(machineType, config), {
|
|
308
277
|
machineType,
|
|
309
278
|
platform,
|
|
310
279
|
home,
|
|
@@ -313,20 +282,14 @@ export async function runDaemon(opts) {
|
|
|
313
282
|
heartbeatRunnerPath: runners.heartbeat,
|
|
314
283
|
sessionHostRunnerPath: runners.sessionHost,
|
|
315
284
|
legacySyncEnv: readLegacySyncEnv(platform),
|
|
316
|
-
signedIn:
|
|
285
|
+
signedIn: signedIn(env, home, log),
|
|
317
286
|
kitConfigured: fs.existsSync(agentKitPaths(home, env).kitConfigPath),
|
|
318
|
-
syncPaused: config.syncPaused === true,
|
|
319
|
-
syncOwner,
|
|
320
287
|
desktopAppManagesSync: desktopAppManagesSync({
|
|
321
288
|
machineType,
|
|
322
289
|
host: opts.host,
|
|
323
290
|
legacy,
|
|
324
291
|
appRunning: () => desktopAppRunning(platform),
|
|
325
292
|
}),
|
|
326
|
-
syncRunnerProcesses,
|
|
327
|
-
syncRunnerOwnerPid,
|
|
328
|
-
daemonSyncRunnerPids,
|
|
329
|
-
syncHeartbeatTimeoutMs: syncHeartbeatTimeoutMs(env),
|
|
330
293
|
bots: listLocalBots(),
|
|
331
294
|
legacyOwner: legacyOwnerLookup(legacy),
|
|
332
295
|
cliSelfUpdatable: updatable,
|
|
@@ -348,27 +311,20 @@ export async function runDaemon(opts) {
|
|
|
348
311
|
: command.type === "node"
|
|
349
312
|
? [opts.nodeBinary, [command.script, ...command.args]]
|
|
350
313
|
: [command.file, command.args];
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
child.stderr?.pipe(logStream, { end: false });
|
|
366
|
-
child.on("close", () => {
|
|
367
|
-
if (child.pid !== undefined)
|
|
368
|
-
daemonSyncRunnerPids.delete(child.pid);
|
|
369
|
-
logStream.end();
|
|
370
|
-
});
|
|
371
|
-
return child;
|
|
314
|
+
const fd = openPrivateAppend(paths.unitLogPath(unit.id));
|
|
315
|
+
try {
|
|
316
|
+
const cwd = unit.env.HQ_ROOT && fs.existsSync(unit.env.HQ_ROOT) ? unit.env.HQ_ROOT : home;
|
|
317
|
+
const child = spawn(file, args, {
|
|
318
|
+
cwd,
|
|
319
|
+
env: unitProcessEnv(env, unit),
|
|
320
|
+
stdio: ["ignore", fd, fd],
|
|
321
|
+
windowsHide: true,
|
|
322
|
+
});
|
|
323
|
+
return child;
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
fs.closeSync(fd);
|
|
327
|
+
}
|
|
372
328
|
};
|
|
373
329
|
// Set once the shutdown path exists below; a restart request before then is dropped.
|
|
374
330
|
let restartDaemon = () => { };
|
|
@@ -390,21 +346,6 @@ export async function runDaemon(opts) {
|
|
|
390
346
|
restartDaemon: () => restartDaemon(),
|
|
391
347
|
writeState: (state) => writeJsonAtomic(paths.statePath, state),
|
|
392
348
|
log,
|
|
393
|
-
syncOwnerBlockReason: () => {
|
|
394
|
-
const config = readDaemonConfig(paths.configPath);
|
|
395
|
-
const hqRoot = config.hqRoot ?? env.HQ_ROOT?.trim();
|
|
396
|
-
if (!hqRoot)
|
|
397
|
-
return undefined;
|
|
398
|
-
const statusPath = watchRunnerOwnerStatusPath(hqRoot, home, env);
|
|
399
|
-
try {
|
|
400
|
-
const owner = readWatchRunnerOwnerStatus(statusPath);
|
|
401
|
-
return owner ? `sync owned by ${owner.owner} pid ${owner.pid}` : undefined;
|
|
402
|
-
}
|
|
403
|
-
catch (err) {
|
|
404
|
-
log("error", `could not read sync owner status at ${statusPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
405
|
-
return undefined;
|
|
406
|
-
}
|
|
407
|
-
},
|
|
408
349
|
});
|
|
409
350
|
return await new Promise((resolve) => {
|
|
410
351
|
let stopping = false;
|
|
@@ -20,16 +20,12 @@
|
|
|
20
20
|
import type { UnitPlan } from "./catalog.js";
|
|
21
21
|
import type { MachineType } from "./machine-type.js";
|
|
22
22
|
import type { ControlRequest } from "./requests.js";
|
|
23
|
-
import { syncPassResult } from "./sync-runner.js";
|
|
24
23
|
export declare const BACKOFF_BASE_MS = 1000;
|
|
25
24
|
export declare const BACKOFF_MAX_MS = 60000;
|
|
26
25
|
export declare const HEALTHY_RUN_MS = 60000;
|
|
27
26
|
export declare const STOP_ESCALATION_MS = 10000;
|
|
28
27
|
export interface SupervisedChild {
|
|
29
28
|
pid?: number;
|
|
30
|
-
stdout?: {
|
|
31
|
-
on(event: "data" | "end", handler: (chunk?: Buffer | string) => void): unknown;
|
|
32
|
-
};
|
|
33
29
|
on(event: "exit", handler: (code: number | null, signal: string | null) => void): unknown;
|
|
34
30
|
on(event: "error", handler: (err: Error) => void): unknown;
|
|
35
31
|
kill(signal?: NodeJS.Signals | number): boolean;
|
|
@@ -53,8 +49,6 @@ export interface UnitState {
|
|
|
53
49
|
lastRunAt?: number;
|
|
54
50
|
nextRunAt?: number;
|
|
55
51
|
lastResult?: "ok" | "failed";
|
|
56
|
-
lastHeartbeatAt?: string;
|
|
57
|
-
lastPassResult?: ReturnType<typeof syncPassResult>;
|
|
58
52
|
}
|
|
59
53
|
/** An app that runs the daemon as its own child process instead of an OS unit. */
|
|
60
54
|
export type HostApp = "desktop";
|
|
@@ -85,8 +79,6 @@ export interface SupervisorDeps {
|
|
|
85
79
|
clearTimeout?: (handle: unknown) => void;
|
|
86
80
|
reconcileMs?: number;
|
|
87
81
|
requestPollMs?: number;
|
|
88
|
-
/** Read hq-cloud's lease status after it exits with the lease-conflict code. */
|
|
89
|
-
syncOwnerBlockReason?: () => string | undefined;
|
|
90
82
|
}
|
|
91
83
|
export interface Supervisor {
|
|
92
84
|
start(): void;
|
|
@@ -17,8 +17,6 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Processes, time, and state output are injected; `run.ts` wires the real ones.
|
|
19
19
|
*/
|
|
20
|
-
import { StringDecoder } from "node:string_decoder";
|
|
21
|
-
import { SYNC_RUNNER_LEASE_CONFLICT_EXIT, SYNC_RUNNER_LEASE_RETRY_MS, parseSyncRunnerProtocolLine, syncPassResult, } from "./sync-runner.js";
|
|
22
20
|
export const BACKOFF_BASE_MS = 1_000;
|
|
23
21
|
export const BACKOFF_MAX_MS = 60_000;
|
|
24
22
|
export const HEALTHY_RUN_MS = 60_000;
|
|
@@ -27,15 +25,7 @@ export function backoffMs(attempt) {
|
|
|
27
25
|
return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** Math.max(0, attempt));
|
|
28
26
|
}
|
|
29
27
|
function planKey(plan) {
|
|
30
|
-
return JSON.stringify([
|
|
31
|
-
plan.kind,
|
|
32
|
-
plan.restart,
|
|
33
|
-
plan.intervalSec,
|
|
34
|
-
plan.initialDelaySec,
|
|
35
|
-
plan.heartbeatTimeoutMs,
|
|
36
|
-
plan.command,
|
|
37
|
-
plan.env,
|
|
38
|
-
]);
|
|
28
|
+
return JSON.stringify([plan.kind, plan.restart, plan.intervalSec, plan.initialDelaySec, plan.command, plan.env]);
|
|
39
29
|
}
|
|
40
30
|
function errorMessage(err) {
|
|
41
31
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -78,11 +68,6 @@ export function createSupervisor(deps) {
|
|
|
78
68
|
clearT(slot.timer);
|
|
79
69
|
slot.timer = undefined;
|
|
80
70
|
};
|
|
81
|
-
const clearHeartbeatTimer = (slot) => {
|
|
82
|
-
if (slot.heartbeatTimer !== undefined)
|
|
83
|
-
clearT(slot.heartbeatTimer);
|
|
84
|
-
slot.heartbeatTimer = undefined;
|
|
85
|
-
};
|
|
86
71
|
const setBlocked = (slot) => {
|
|
87
72
|
const block = slot.plan.blocked;
|
|
88
73
|
slot.state.status = block.state;
|
|
@@ -93,7 +78,6 @@ export function createSupervisor(deps) {
|
|
|
93
78
|
/** Stop the slot's child (if any), then run `then` once it has exited. */
|
|
94
79
|
const terminate = (slot, then) => {
|
|
95
80
|
clearTimer(slot);
|
|
96
|
-
clearHeartbeatTimer(slot);
|
|
97
81
|
const child = slot.child;
|
|
98
82
|
if (!child) {
|
|
99
83
|
then?.();
|
|
@@ -138,69 +122,6 @@ export function createSupervisor(deps) {
|
|
|
138
122
|
const watch = (slot, child) => {
|
|
139
123
|
live.add(child);
|
|
140
124
|
let ended = false;
|
|
141
|
-
const timeoutMs = slot.plan.heartbeatTimeoutMs;
|
|
142
|
-
const decoder = timeoutMs ? new StringDecoder("utf8") : null;
|
|
143
|
-
let stdoutRemainder = "";
|
|
144
|
-
let lastHeartbeatPersistedAt = now() - 15_000;
|
|
145
|
-
const armHeartbeatWatchdog = () => {
|
|
146
|
-
if (!timeoutMs)
|
|
147
|
-
return;
|
|
148
|
-
clearHeartbeatTimer(slot);
|
|
149
|
-
slot.heartbeatTimer = setT(() => {
|
|
150
|
-
slot.heartbeatTimer = undefined;
|
|
151
|
-
if (stopped || slot.child !== child || slot.expectingExit)
|
|
152
|
-
return;
|
|
153
|
-
deps.log("warn", `${slot.plan.id} emitted no sync protocol heartbeat for ${timeoutMs}ms; restarting it`);
|
|
154
|
-
terminate(slot, () => scheduleRestart(slot));
|
|
155
|
-
emit();
|
|
156
|
-
}, timeoutMs);
|
|
157
|
-
};
|
|
158
|
-
const acceptProtocolLine = (line) => {
|
|
159
|
-
let event;
|
|
160
|
-
try {
|
|
161
|
-
event = parseSyncRunnerProtocolLine(line);
|
|
162
|
-
}
|
|
163
|
-
catch (err) {
|
|
164
|
-
deps.log("error", `${slot.plan.id} emitted invalid sync protocol output (${Buffer.byteLength(line)} bytes): ${errorMessage(err)}`);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (!event)
|
|
168
|
-
return;
|
|
169
|
-
const at = now();
|
|
170
|
-
slot.state.lastHeartbeatAt = new Date(at).toISOString();
|
|
171
|
-
const pass = syncPassResult(event, slot.state.lastHeartbeatAt);
|
|
172
|
-
if (pass)
|
|
173
|
-
slot.state.lastPassResult = pass;
|
|
174
|
-
armHeartbeatWatchdog();
|
|
175
|
-
if (pass || at - lastHeartbeatPersistedAt >= 15_000) {
|
|
176
|
-
lastHeartbeatPersistedAt = at;
|
|
177
|
-
emit();
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
if (decoder && child.stdout) {
|
|
181
|
-
child.stdout.on("data", (chunk) => {
|
|
182
|
-
if (chunk === undefined)
|
|
183
|
-
return;
|
|
184
|
-
stdoutRemainder += decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
185
|
-
let newline = stdoutRemainder.indexOf("\n");
|
|
186
|
-
while (newline >= 0) {
|
|
187
|
-
acceptProtocolLine(stdoutRemainder.slice(0, newline).trim());
|
|
188
|
-
stdoutRemainder = stdoutRemainder.slice(newline + 1);
|
|
189
|
-
newline = stdoutRemainder.indexOf("\n");
|
|
190
|
-
}
|
|
191
|
-
// A malformed or unexpectedly large line is not a heartbeat. Bound
|
|
192
|
-
// retained data so a broken child cannot grow daemon memory forever.
|
|
193
|
-
if (Buffer.byteLength(stdoutRemainder) > 1024 * 1024)
|
|
194
|
-
stdoutRemainder = "";
|
|
195
|
-
});
|
|
196
|
-
child.stdout.on("end", () => {
|
|
197
|
-
stdoutRemainder += decoder.end();
|
|
198
|
-
if (stdoutRemainder)
|
|
199
|
-
acceptProtocolLine(stdoutRemainder.trim());
|
|
200
|
-
stdoutRemainder = "";
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
armHeartbeatWatchdog();
|
|
204
125
|
const onEnd = (code, signal) => {
|
|
205
126
|
if (ended)
|
|
206
127
|
return;
|
|
@@ -211,7 +132,6 @@ export function createSupervisor(deps) {
|
|
|
211
132
|
if (slot.child !== child)
|
|
212
133
|
return;
|
|
213
134
|
slot.child = undefined;
|
|
214
|
-
clearHeartbeatTimer(slot);
|
|
215
135
|
if (slot.killTimer !== undefined) {
|
|
216
136
|
clearT(slot.killTimer);
|
|
217
137
|
slot.killTimer = undefined;
|
|
@@ -239,20 +159,6 @@ export function createSupervisor(deps) {
|
|
|
239
159
|
emit();
|
|
240
160
|
return;
|
|
241
161
|
}
|
|
242
|
-
if (slot.plan.service === "sync" && code === SYNC_RUNNER_LEASE_CONFLICT_EXIT) {
|
|
243
|
-
const reason = deps.syncOwnerBlockReason?.() ?? "sync owned by another runner";
|
|
244
|
-
slot.state.status = "waiting";
|
|
245
|
-
slot.state.reason = reason;
|
|
246
|
-
slot.state.nextRunAt = now() + SYNC_RUNNER_LEASE_RETRY_MS;
|
|
247
|
-
deps.log("info", `${reason}; retrying in ${SYNC_RUNNER_LEASE_RETRY_MS}ms`);
|
|
248
|
-
slot.timer = setT(() => {
|
|
249
|
-
slot.timer = undefined;
|
|
250
|
-
startLong(slot);
|
|
251
|
-
emit();
|
|
252
|
-
}, SYNC_RUNNER_LEASE_RETRY_MS);
|
|
253
|
-
emit();
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
162
|
deps.log("warn", `${slot.plan.id} exited (code=${code ?? "null"} signal=${signal ?? "none"})`);
|
|
257
163
|
if (ranFor >= HEALTHY_RUN_MS)
|
|
258
164
|
slot.attempts = 0;
|
|
@@ -463,11 +369,6 @@ export function createSupervisor(deps) {
|
|
|
463
369
|
emit();
|
|
464
370
|
};
|
|
465
371
|
const handleRequest = (req) => {
|
|
466
|
-
if (req.action === "reconcile") {
|
|
467
|
-
deps.log("info", "reconciling daemon configuration (requested)");
|
|
468
|
-
reconcile();
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
372
|
if (req.action === "restart-daemon") {
|
|
472
373
|
deps.log("info", "restarting the daemon (requested)");
|
|
473
374
|
if (deps.restartDaemon)
|
|
@@ -574,7 +475,6 @@ export function createSupervisor(deps) {
|
|
|
574
475
|
clearT(requestTimer);
|
|
575
476
|
for (const slot of slots.values()) {
|
|
576
477
|
clearTimer(slot);
|
|
577
|
-
clearHeartbeatTimer(slot);
|
|
578
478
|
slot.child = undefined;
|
|
579
479
|
if (slot.killTimer !== undefined)
|
|
580
480
|
clearT(slot.killTimer);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
|
|
2
|
+
export declare const SCAFFOLD_CLI_FLOOR_FAMILY_ID = "scaffold-cli-floor";
|
|
3
|
+
export declare const SCAFFOLD_CLI_FLOOR_FAMILY_TITLE = "Scaffold CLI compatibility";
|
|
4
|
+
export declare const SCAFFOLD_CLI_FLOOR_CHECK_ID = "scaffold-cli-floor.requires-hq-cli";
|
|
5
|
+
export declare const scaffoldCliFloorFamily: CheckFamily;
|
|
6
|
+
export declare function checkScaffoldCliFloor(context: CheckContext): CheckResult[];
|
|
7
|
+
//# sourceMappingURL=scaffold-cli-floor.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as yaml from "js-yaml";
|
|
4
|
+
import { CLI_VERSION } from "../../../cli-version.js";
|
|
5
|
+
import { compareScaffoldCliFloor } from "../../scaffold-cli-floor.js";
|
|
6
|
+
export const SCAFFOLD_CLI_FLOOR_FAMILY_ID = "scaffold-cli-floor";
|
|
7
|
+
export const SCAFFOLD_CLI_FLOOR_FAMILY_TITLE = "Scaffold CLI compatibility";
|
|
8
|
+
export const SCAFFOLD_CLI_FLOOR_CHECK_ID = "scaffold-cli-floor.requires-hq-cli";
|
|
9
|
+
export const scaffoldCliFloorFamily = {
|
|
10
|
+
id: SCAFFOLD_CLI_FLOOR_FAMILY_ID,
|
|
11
|
+
title: SCAFFOLD_CLI_FLOOR_FAMILY_TITLE,
|
|
12
|
+
run: (context) => Promise.resolve(checkScaffoldCliFloor(context)),
|
|
13
|
+
};
|
|
14
|
+
export function checkScaffoldCliFloor(context) {
|
|
15
|
+
const coreYamlPath = path.join(context.hqRoot, "core", "core.yaml");
|
|
16
|
+
if (!fs.existsSync(coreYamlPath)) {
|
|
17
|
+
return [skipped(coreYamlPath, "core/core.yaml is missing")];
|
|
18
|
+
}
|
|
19
|
+
let document;
|
|
20
|
+
try {
|
|
21
|
+
document = yaml.load(fs.readFileSync(coreYamlPath, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
status: "WARN",
|
|
27
|
+
checkId: SCAFFOLD_CLI_FLOOR_CHECK_ID,
|
|
28
|
+
target: coreYamlPath,
|
|
29
|
+
message: "core/core.yaml could not be parsed, so the hq-cli floor was not checked.",
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
if (!document || typeof document !== "object" || Array.isArray(document)) {
|
|
34
|
+
if (document === undefined || document === null) {
|
|
35
|
+
return [skipped(coreYamlPath, "core/core.yaml does not declare requiresHqCli")];
|
|
36
|
+
}
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
status: "WARN",
|
|
40
|
+
checkId: SCAFFOLD_CLI_FLOOR_CHECK_ID,
|
|
41
|
+
target: coreYamlPath,
|
|
42
|
+
message: "core/core.yaml is not a mapping, so the hq-cli floor was not checked.",
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
const required = document.requiresHqCli;
|
|
47
|
+
const comparison = compareScaffoldCliFloor(required, CLI_VERSION);
|
|
48
|
+
if (comparison.status === "skip") {
|
|
49
|
+
return [skipped(coreYamlPath, "core/core.yaml does not declare requiresHqCli")];
|
|
50
|
+
}
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
status: comparison.status === "pass"
|
|
54
|
+
? "PASS"
|
|
55
|
+
: comparison.status === "fail"
|
|
56
|
+
? "FAIL"
|
|
57
|
+
: "WARN",
|
|
58
|
+
checkId: SCAFFOLD_CLI_FLOOR_CHECK_ID,
|
|
59
|
+
target: coreYamlPath,
|
|
60
|
+
message: comparison.message,
|
|
61
|
+
...(comparison.remediation ? { remediation: comparison.remediation } : {}),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
function skipped(target, reason) {
|
|
66
|
+
return {
|
|
67
|
+
status: "NA",
|
|
68
|
+
checkId: SCAFFOLD_CLI_FLOOR_CHECK_ID,
|
|
69
|
+
target,
|
|
70
|
+
message: `Skipped: ${reason}; no hq-cli floor was checked.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=scaffold-cli-floor.js.map
|
|
@@ -23,6 +23,7 @@ import { integrationsFamily } from "./checks/integrations.js";
|
|
|
23
23
|
import { syncHealthFamily } from "./checks/sync-health.js";
|
|
24
24
|
import { workContextFamily } from "./checks/work-context.js";
|
|
25
25
|
import { botsFamily } from "./checks/bots.js";
|
|
26
|
+
import { scaffoldCliFloorFamily } from "./checks/scaffold-cli-floor.js";
|
|
26
27
|
import { fixtureCoverageFamily } from "./fixtures/discover.js";
|
|
27
28
|
import { checkClaudeWiring } from "./checks/claude-wiring.js";
|
|
28
29
|
/**
|
|
@@ -236,6 +237,7 @@ function hasCommandHook(settings, event) {
|
|
|
236
237
|
export function createDefaultRegistry() {
|
|
237
238
|
const registry = new DoctorRegistry();
|
|
238
239
|
registry.register(hooksFamily);
|
|
240
|
+
registry.register(scaffoldCliFloorFamily);
|
|
239
241
|
// Fixture coverage (US-007): reports UNTESTED for every registered hook with
|
|
240
242
|
// no fixture and prints the tested/total coverage line. A separate family so
|
|
241
243
|
// it needs no change to the hooks tier, per the registry's extensibility
|