@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/assets/scaffold/core/scripts/register-project.sh +5 -5
  3. package/assets/scaffold/core/scripts/repo-run-registry.sh +8 -8
  4. package/assets/scaffold/core/scripts/resume-thread-lock.sh +4 -4
  5. package/assets/scaffold/core/scripts/verify-story-deliverables.sh +1 -1
  6. package/assets/scaffold/core/scripts/worktree-gc.sh +1 -1
  7. package/dist/commands/core-handoff.js +69 -34
  8. package/dist/commands/core-hq-session-native.js +26 -5
  9. package/dist/commands/core-job-notify-fast.d.ts +2 -0
  10. package/dist/commands/core-job-notify-fast.js +6 -0
  11. package/dist/commands/core-job-probe-fast.d.ts +2 -0
  12. package/dist/commands/core-job-probe-fast.js +11 -0
  13. package/dist/commands/core-job-probe-native.js +1 -1
  14. package/dist/commands/core-job-remediate-fast.d.ts +2 -0
  15. package/dist/commands/core-job-remediate-fast.js +6 -0
  16. package/dist/commands/core-job-run-fast.d.ts +2 -0
  17. package/dist/commands/core-job-run-fast.js +6 -0
  18. package/dist/commands/core-pin-fast.d.ts +2 -0
  19. package/dist/commands/core-pin-fast.js +15 -0
  20. package/dist/commands/core-rebuild-index-fast.d.ts +6 -0
  21. package/dist/commands/core-rebuild-index-fast.js +39 -0
  22. package/dist/commands/core-register-project-native.js +11 -2
  23. package/dist/commands/core-repo-run-registry-native.js +47 -4
  24. package/dist/commands/core-session-journal-native.d.ts +1 -1
  25. package/dist/commands/core-session-journal-native.js +2 -2
  26. package/dist/commands/core-share-suggestion-state-native.js +6 -2
  27. package/dist/commands/core-work-mesh-live-rebind-native.js +5 -2
  28. package/dist/commands/core-worker-lint-fast.d.ts +2 -0
  29. package/dist/commands/core-worker-lint-fast.js +11 -0
  30. package/dist/commands/core.js +9 -6
  31. package/dist/fast-scaffold.js +49 -0
  32. package/dist/lib/core-utils/worktree-resume.d.ts +1 -0
  33. package/dist/lib/core-utils/worktree-resume.js +68 -31
  34. package/dist/lib/daemon/catalog.d.ts +10 -0
  35. package/dist/lib/daemon/catalog.js +8 -0
  36. package/dist/lib/daemon/lanes-reconcile-guard.d.ts +43 -0
  37. package/dist/lib/daemon/lanes-reconcile-guard.js +115 -0
  38. package/dist/lib/daemon/run.js +2 -0
  39. package/dist/lib/daemon/supervisor.d.ts +2 -0
  40. package/dist/lib/daemon/supervisor.js +28 -0
  41. package/dist/lib/lanes/autopilot-registry.d.ts +2 -5
  42. package/dist/lib/lanes/autopilot-registry.js +4 -4
  43. package/dist/lib/lanes/reconcile.d.ts +0 -5
  44. package/dist/lib/lanes/reconcile.js +4 -4
  45. package/dist/lib/lanes/types.d.ts +6 -5
  46. package/dist/utils/run-bundled-script.js +1 -1
  47. package/package.json +1 -1
@@ -0,0 +1,115 @@
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 * as fs from "node:fs";
29
+ import * as path from "node:path";
30
+ export const LANES_STORE_DIR = path.join("workspace", "lanes");
31
+ export const LANE_RECORDS_SUBDIR = "lanes";
32
+ export const QUESTION_RECORDS_SUBDIR = "questions";
33
+ export const LANES_CONFIG_FILE = path.join("personal", "lanes", "config.json");
34
+ export const LEAD_REGISTRY_FILE = path.join("workspace", "lane-briefs", "juniors.json");
35
+ /**
36
+ * Lane states that reconcile admission, heartbeats and liveness never act on.
37
+ * `blocked` is listed because reconcile only resumes a blocked lane when it is
38
+ * keep-alive, which is checked separately.
39
+ */
40
+ const QUIET_LANE_STATES = new Set(["done", "failed", "interrupted", "cancelled", "killed", "blocked"]);
41
+ const realFs = {
42
+ readdirSync: (dir) => fs.readdirSync(dir),
43
+ readFileSync: (file, encoding) => fs.readFileSync(file, encoding),
44
+ existsSync: (file) => fs.existsSync(file),
45
+ };
46
+ /** Same filter reconcile's `readDirJsonIds` applies. */
47
+ function recordNames(fsDeps, dir) {
48
+ let names;
49
+ try {
50
+ names = fsDeps.readdirSync(dir);
51
+ }
52
+ catch (err) {
53
+ if (err.code === "ENOENT")
54
+ return [];
55
+ throw err;
56
+ }
57
+ return names.filter((name) => name.endsWith(".json") && !name.startsWith(".") && !name.includes(".conflict-"));
58
+ }
59
+ function readRecord(fsDeps, file) {
60
+ const parsed = JSON.parse(fsDeps.readFileSync(file, "utf8"));
61
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
62
+ throw new Error(`${file} is not a JSON object`);
63
+ }
64
+ return parsed;
65
+ }
66
+ /** A lane that may need the 15-second cadence. Unknown shapes count as urgent. */
67
+ function laneIsUrgent(lane) {
68
+ if (typeof lane.state !== "string" || !QUIET_LANE_STATES.has(lane.state))
69
+ return true;
70
+ if (lane.keep_alive === true)
71
+ return true;
72
+ if (lane.pending_resume !== undefined)
73
+ return true;
74
+ return false;
75
+ }
76
+ export function lanesReconcileVerdict(hqRoot, fsDeps = realFs) {
77
+ try {
78
+ if (!hqRoot)
79
+ return "spawn";
80
+ const store = path.join(hqRoot, LANES_STORE_DIR);
81
+ const laneDir = path.join(store, LANE_RECORDS_SUBDIR);
82
+ const questionDir = path.join(store, QUESTION_RECORDS_SUBDIR);
83
+ const laneNames = recordNames(fsDeps, laneDir);
84
+ const questionNames = recordNames(fsDeps, questionDir);
85
+ for (const name of laneNames) {
86
+ if (laneIsUrgent(readRecord(fsDeps, path.join(laneDir, name))))
87
+ return "spawn";
88
+ }
89
+ for (const name of questionNames) {
90
+ const question = readRecord(fsDeps, path.join(questionDir, name));
91
+ if (typeof question.status !== "string" || question.status === "pending")
92
+ return "spawn";
93
+ }
94
+ if (laneNames.length === 0 &&
95
+ questionNames.length === 0 &&
96
+ !fsDeps.existsSync(path.join(hqRoot, LEAD_REGISTRY_FILE)) &&
97
+ !fsDeps.existsSync(path.join(hqRoot, LANES_CONFIG_FILE))) {
98
+ return "skip";
99
+ }
100
+ return "idle";
101
+ }
102
+ catch {
103
+ return "spawn";
104
+ }
105
+ }
106
+ /** The supervisor's `runGuard`: dispatches on the unit's declared guard. */
107
+ export function daemonRunGuard(unit) {
108
+ switch (unit.runGuard?.name) {
109
+ case "lanes-reconcile":
110
+ return lanesReconcileVerdict(unit.env.HQ_ROOT);
111
+ default:
112
+ return "spawn";
113
+ }
114
+ }
115
+ //# sourceMappingURL=lanes-reconcile-guard.js.map
@@ -45,6 +45,7 @@ import { adoptChild, createChildTracker, finishSuccession, handoffEligible, read
45
45
  import { listSyncRunnerProcesses, readWatchRunnerOwnerStatus, syncHeartbeatTimeoutMs, syncRunnerNodeOptions, watchRunnerOwnerStatusPath, } from "./sync-runner.js";
46
46
  import { STOP_ESCALATION_MS, createSupervisor, } from "./supervisor.js";
47
47
  import { userToolDirs } from "./tools.js";
48
+ import { daemonRunGuard } from "./lanes-reconcile-guard.js";
48
49
  export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
49
50
  /** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
50
51
  export const RESTART_EXIT_CODE = 75;
@@ -788,6 +789,7 @@ export async function runDaemon(opts) {
788
789
  return takeControlRequests(paths.requestsDir);
789
790
  },
790
791
  restartDaemon: () => restartDaemon(),
792
+ runGuard: daemonRunGuard,
791
793
  writeState: (state) => writeJsonAtomic(paths.statePath, state),
792
794
  log,
793
795
  syncOwnerBlockReason: () => {
@@ -111,6 +111,8 @@ export interface SupervisorDeps {
111
111
  cliVersion?: string;
112
112
  /** Services to take over from the previous daemon instead of starting them. */
113
113
  adopt?: readonly AdoptedUnit[];
114
+ /** Pre-run check for scheduled units that declare `runGuard`; a throw counts as `spawn`. */
115
+ runGuard?: (unit: UnitPlan) => "spawn" | "idle" | "skip";
114
116
  }
115
117
  export interface Supervisor {
116
118
  start(): void;
@@ -56,6 +56,9 @@ export function planKey(plan, cliVersion) {
56
56
  plan.command,
57
57
  plan.env,
58
58
  ];
59
+ // Only units that declare a guard carry it, so existing keys stay unchanged.
60
+ if (plan.runGuard !== undefined)
61
+ fields.push({ runGuard: plan.runGuard });
59
62
  if (cliVersion !== undefined && plan.command?.type === "hq" && !plan.keepAcrossCliUpdates)
60
63
  fields.push(cliVersion);
61
64
  return JSON.stringify(fields);
@@ -554,6 +557,27 @@ export function createSupervisor(deps) {
554
557
  runScheduled(slot);
555
558
  }, delayMs);
556
559
  };
560
+ /** True when the unit's run guard says this timed run has nothing to do. Fails open. */
561
+ const guardSaysWait = (slot) => {
562
+ const guard = slot.plan.runGuard;
563
+ if (!guard || !deps.runGuard)
564
+ return false;
565
+ let verdict;
566
+ try {
567
+ verdict = deps.runGuard(slot.plan);
568
+ }
569
+ catch (err) {
570
+ deps.log("warn", `${slot.plan.id} run check failed, running anyway: ${errorMessage(err)}`);
571
+ return false;
572
+ }
573
+ if (verdict === "skip")
574
+ return true;
575
+ if (verdict === "idle") {
576
+ const last = slot.state.lastRunAt;
577
+ return last !== undefined && now() - last < guard.idleIntervalSec * 1000;
578
+ }
579
+ return false;
580
+ };
557
581
  const runScheduled = (slot, manual = false) => {
558
582
  if (stopped)
559
583
  return;
@@ -564,6 +588,10 @@ export function createSupervisor(deps) {
564
588
  emit();
565
589
  return;
566
590
  }
591
+ if (!manual && guardSaysWait(slot)) {
592
+ emit();
593
+ return;
594
+ }
567
595
  slot.busy = true;
568
596
  slot.state.lastRunAt = now();
569
597
  const command = slot.plan.command;
@@ -1,7 +1,5 @@
1
1
  import type { LaneCompanyRef, LaneHost } from "./types.js";
2
- export declare const OUTPOST_LEAD_REGISTRY_SCHEMA = "hq-lanes-outpost-lead-registry.v1";
3
- export declare const OUTPOST_LEAD_REGISTRY_MAX_AGE_MS: number;
4
- export declare const OUTPOST_LEAD_REGISTRY_REFRESH_MS: number;
2
+ declare const OUTPOST_LEAD_REGISTRY_SCHEMA = "hq-lanes-outpost-lead-registry.v1";
5
3
  export declare const LEAD_REGISTRY_REL: string;
6
4
  export declare const OUTPOST_LEAD_REGISTRY_STATE_REL: string;
7
5
  export declare const LEAD_AUTOPILOT_FLAG = "lanes.lead-autopilot";
@@ -23,8 +21,6 @@ export declare function buildOutpostLeadRegistry(input: {
23
21
  outpostId: string;
24
22
  generatedAt: string;
25
23
  }): OutpostLeadRegistry;
26
- /** Stage a host-filtered registry on the Outpost using an atomic local rename. */
27
- export declare function buildOutpostLeadRegistryInstallCommand(registry: OutpostLeadRegistry): string;
28
24
  export declare function syncOutpostLeadRegistries(input: {
29
25
  hqRoot: string;
30
26
  outpostIds: readonly string[];
@@ -59,4 +55,5 @@ export declare function validateOutpostLeadRegistry(input: {
59
55
  outpostId: string;
60
56
  now: Date;
61
57
  }): OutpostLeadRegistry;
58
+ export {};
62
59
  //# sourceMappingURL=autopilot-registry.d.ts.map
@@ -4,9 +4,9 @@ import * as path from "node:path";
4
4
  import { joinCommandParts, withRemoteHqDir } from "@indigoai-us/hq-cloud/outposts";
5
5
  import { isControllerAliasOutpostId } from "./admission-host.js";
6
6
  import { LanesError } from "./errors.js";
7
- export const OUTPOST_LEAD_REGISTRY_SCHEMA = "hq-lanes-outpost-lead-registry.v1";
8
- export const OUTPOST_LEAD_REGISTRY_MAX_AGE_MS = 24 * 60 * 60 * 1000;
9
- export const OUTPOST_LEAD_REGISTRY_REFRESH_MS = 6 * 60 * 60 * 1000;
7
+ const OUTPOST_LEAD_REGISTRY_SCHEMA = "hq-lanes-outpost-lead-registry.v1";
8
+ const OUTPOST_LEAD_REGISTRY_MAX_AGE_MS = 24 * 60 * 60 * 1000;
9
+ const OUTPOST_LEAD_REGISTRY_REFRESH_MS = 6 * 60 * 60 * 1000;
10
10
  export const LEAD_REGISTRY_REL = path.join("workspace", "lane-briefs", "juniors.json");
11
11
  export const OUTPOST_LEAD_REGISTRY_STATE_REL = path.join("workspace", "lane-briefs", "autopilot-outpost-registry-state.json");
12
12
  export const LEAD_AUTOPILOT_FLAG = "lanes.lead-autopilot";
@@ -50,7 +50,7 @@ export function buildOutpostLeadRegistry(input) {
50
50
  };
51
51
  }
52
52
  /** Stage a host-filtered registry on the Outpost using an atomic local rename. */
53
- export function buildOutpostLeadRegistryInstallCommand(registry) {
53
+ function buildOutpostLeadRegistryInstallCommand(registry) {
54
54
  const encoded = Buffer.from(`${JSON.stringify(registry)}\n`, "utf8").toString("base64");
55
55
  const script = [
56
56
  'const fs=require("node:fs"),path=require("node:path");',
@@ -19,11 +19,6 @@ export declare const QUESTION_AGE_MS: number;
19
19
  export declare const MAX_AGED_ADVANCES: number;
20
20
  /** Aligns with watcher silence: a host that has not reported for this long is dead. */
21
21
  export declare const DEAD_HOST_MS: number;
22
- export declare const RECONCILE_LOCK_STALE_MS = 30000;
23
- export declare const RECONCILE_LOCK_TIMEOUT_MS = 5000;
24
- /** A pickup probe that has not cleared its marker within this window is abandoned. */
25
- export declare const PICKUP_VALIDATION_MAX_AGE_MS: number;
26
- export declare const PICKUP_VALIDATION_TIMEOUT_REASON: "pickup_validation_timeout";
27
22
  export declare const CLOSE_INTERRUPTED_DONE_STORY_WORKERS_FLAG = "lanes.close-interrupted-done-story-workers";
28
23
  export declare const RECONCILE_UNIT_NAME = "hq-lanes-reconcile.service";
29
24
  export declare const RECONCILE_TIMER_NAME = "hq-lanes-reconcile.timer";
@@ -45,11 +45,11 @@ export const QUESTION_AGE_MS = 60 * 60 * 1000;
45
45
  export const MAX_AGED_ADVANCES = MAX_LANE_DEPTH - 1;
46
46
  /** Aligns with watcher silence: a host that has not reported for this long is dead. */
47
47
  export const DEAD_HOST_MS = SILENCE_MS;
48
- export const RECONCILE_LOCK_STALE_MS = 30_000;
49
- export const RECONCILE_LOCK_TIMEOUT_MS = 5_000;
48
+ const RECONCILE_LOCK_STALE_MS = 30_000;
49
+ const RECONCILE_LOCK_TIMEOUT_MS = 5_000;
50
50
  /** A pickup probe that has not cleared its marker within this window is abandoned. */
51
- export const PICKUP_VALIDATION_MAX_AGE_MS = 15 * 60 * 1000;
52
- export const PICKUP_VALIDATION_TIMEOUT_REASON = "pickup_validation_timeout";
51
+ const PICKUP_VALIDATION_MAX_AGE_MS = 15 * 60 * 1000;
52
+ const PICKUP_VALIDATION_TIMEOUT_REASON = "pickup_validation_timeout";
53
53
  export const CLOSE_INTERRUPTED_DONE_STORY_WORKERS_FLAG = "lanes.close-interrupted-done-story-workers";
54
54
  const INTERRUPTED_DONE_STORY_MIN_AGE_MS = 24 * 60 * 60 * 1000;
55
55
  export const RECONCILE_UNIT_NAME = "hq-lanes-reconcile.service";
@@ -179,7 +179,7 @@ export declare const LANE_PROVIDERS: readonly ["claude", "codex", "grok"];
179
179
  export type LaneProvider = (typeof LANE_PROVIDERS)[number];
180
180
  export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry", "lead_reserve", "worker_profile_changed", "low_disk", "admit_failed"];
181
181
  export type QueuedReason = (typeof QUEUED_REASONS)[number];
182
- export type QueuedDetails = {
182
+ type QueuedDetails = {
183
183
  reason: "worker_profile_changed";
184
184
  expected_hash: string | null;
185
185
  actual_hash: string;
@@ -308,7 +308,7 @@ export interface SeniorHistoryEntry {
308
308
  by: string;
309
309
  reason: string;
310
310
  }
311
- export interface LaneStopHistoryEntry {
311
+ interface LaneStopHistoryEntry {
312
312
  at: string;
313
313
  reason: string;
314
314
  }
@@ -370,7 +370,7 @@ export interface AttributionHistoryEntry {
370
370
  at: string;
371
371
  story_switch_unacknowledged?: StorySwitchUnacknowledgedNote;
372
372
  }
373
- export interface StorySwitchPending {
373
+ interface StorySwitchPending {
374
374
  previous_project_id: string;
375
375
  previous_story_id: string;
376
376
  project_id: string;
@@ -416,7 +416,7 @@ export interface ManagerStatusGateHistoryEntry {
416
416
  resume_queued?: boolean;
417
417
  resume_skipped_reason?: "open_question" | "blocked" | "miss_limit" | "no_provider_session";
418
418
  }
419
- export interface BlockerFreshnessState {
419
+ interface BlockerFreshnessState {
420
420
  /** Stable hashes of the stale blocker labels seen at the last completed turn. */
421
421
  stale_keys: string[];
422
422
  stale_labels: string[];
@@ -440,7 +440,7 @@ export interface BlockerFreshnessHistoryEntry {
440
440
  reason?: string;
441
441
  delivery_recovered?: true;
442
442
  }
443
- export interface KrFreshnessState {
443
+ interface KrFreshnessState {
444
444
  /** Stable fingerprint of the last stalled KPI set, which is resumed once. */
445
445
  stalled_fingerprint?: string;
446
446
  }
@@ -1044,4 +1044,5 @@ export interface LanesConfig {
1044
1044
  export declare function isIsoDateTime(value: string): boolean;
1045
1045
  export declare function parseMajorSchema(schemaVersion: string, expectedPrefix: string, expectedMajor: number): void;
1046
1046
  export declare function parseLaneEnvelope(raw: unknown): LaneEnvelope;
1047
+ export {};
1047
1048
  //# sourceMappingURL=types.d.ts.map
@@ -33,7 +33,7 @@ import { spawnSync } from "child_process";
33
33
  import * as fs from "node:fs";
34
34
  import * as os from "node:os";
35
35
  import * as path from "path";
36
- import { resolveBundledAsset } from "./hq-roots.js";
36
+ import { resolveBundledAsset } from "./hq-roots-core.js";
37
37
  /**
38
38
  * Which interpreter runs a bundled asset, chosen by extension.
39
39
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.11",
3
+ "version": "5.345.12",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {