@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +25 -2
  2. package/dist/command-catalog.generated.d.ts +11 -536
  3. package/dist/command-catalog.generated.js +14 -691
  4. package/dist/commands/daemon.d.ts +0 -3
  5. package/dist/commands/daemon.js +1 -173
  6. package/dist/commands/lanes-checks.d.ts +0 -1
  7. package/dist/commands/lanes-checks.js +217 -54
  8. package/dist/commands/lanes-hold.js +40 -2
  9. package/dist/commands/lanes-message.js +98 -14
  10. package/dist/commands/lanes-shared.d.ts +3 -2
  11. package/dist/commands/lanes.d.ts +2 -4
  12. package/dist/commands/lanes.js +3 -23
  13. package/dist/commands/sync-mode.d.ts +0 -11
  14. package/dist/commands/sync-mode.js +0 -30
  15. package/dist/lib/daemon/catalog.d.ts +2 -18
  16. package/dist/lib/daemon/catalog.js +2 -18
  17. package/dist/lib/daemon/config.d.ts +0 -6
  18. package/dist/lib/daemon/config.js +2 -15
  19. package/dist/lib/daemon/requests.d.ts +0 -4
  20. package/dist/lib/daemon/requests.js +0 -2
  21. package/dist/lib/daemon/run.js +19 -78
  22. package/dist/lib/daemon/supervisor.d.ts +0 -8
  23. package/dist/lib/daemon/supervisor.js +1 -101
  24. package/dist/lib/doctor/checks/scaffold-cli-floor.d.ts +7 -0
  25. package/dist/lib/doctor/checks/scaffold-cli-floor.js +73 -0
  26. package/dist/lib/doctor/registry.js +2 -0
  27. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  28. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  29. package/dist/lib/lanes/hire-grant-dispatch.d.ts +6 -1
  30. package/dist/lib/lanes/hire-grant-dispatch.js +78 -3
  31. package/dist/lib/lanes/keep-alive.d.ts +3 -2
  32. package/dist/lib/lanes/keep-alive.js +2 -2
  33. package/dist/lib/lanes/merge-command.d.ts +1 -0
  34. package/dist/lib/lanes/merge-command.js +299 -33
  35. package/dist/lib/lanes/orphans.d.ts +7 -0
  36. package/dist/lib/lanes/orphans.js +39 -4
  37. package/dist/lib/lanes/tracker-store.d.ts +2 -18
  38. package/dist/lib/lanes/tracker-store.js +1 -172
  39. package/dist/lib/scaffold-cli-floor.d.ts +9 -0
  40. package/dist/lib/scaffold-cli-floor.js +50 -0
  41. package/package.json +2 -2
  42. package/dist/commands/lanes-import.d.ts +0 -14
  43. package/dist/commands/lanes-import.js +0 -1210
  44. package/dist/commands/lanes-tracker.d.ts +0 -18
  45. package/dist/commands/lanes-tracker.js +0 -452
  46. package/dist/lib/daemon/sync-runner.d.ts +0 -58
  47. package/dist/lib/daemon/sync-runner.js +0 -277
@@ -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 { effectiveSyncOwner, readDaemonConfig } from "./config.js";
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
- const env = { ...base, ...unit.env, HQ_DAEMON_UNIT: unit.id, HQ_NO_UPDATE_CHECK: "1" };
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 ?? (env.HQ_ROOT?.trim() || undefined);
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
- const services = enabledServices(machineType, config);
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: isSignedIn,
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 logPath = paths.unitLogPath(unit.id);
352
- const logFd = openPrivateAppend(logPath);
353
- const logStream = fs.createWriteStream(logPath, { fd: logFd, autoClose: true });
354
- logStream.on("error", (err) => log("error", `could not write ${unit.id} log ${logPath}: ${err.message}`));
355
- const cwd = unit.env.HQ_ROOT && fs.existsSync(unit.env.HQ_ROOT) ? unit.env.HQ_ROOT : home;
356
- const child = spawn(file, args, {
357
- cwd,
358
- env: unitProcessEnv(env, unit),
359
- stdio: ["ignore", "pipe", "pipe"],
360
- windowsHide: true,
361
- });
362
- if (unit.service === "sync" && child.pid !== undefined)
363
- daemonSyncRunnerPids.add(child.pid);
364
- child.stdout?.pipe(logStream, { end: false });
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