@indigoai-us/hq-cli 5.256.0 → 5.258.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.
@@ -21,6 +21,7 @@ import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
21
21
  import { registerLaneChecksCommand } from "./lanes-checks.js";
22
22
  import { registerLaneReportCommands } from "./lanes-report.js";
23
23
  import { registerLaneTrackerImportCommand } from "./lanes-import.js";
24
+ import { registerLaneRepoMergeHoldCommand } from "./lanes-hold.js";
24
25
  import { ensureCognitoToken } from "../utils/cognito-session.js";
25
26
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
26
27
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
@@ -60,7 +61,7 @@ import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
60
61
  import { managerStatusGateEnabledForLane, resolveManagerStatusGateForLane, } from "../lib/lanes/manager-status-gate.js";
61
62
  import { ensureSessionHome } from "../lib/lanes/session-relink.js";
62
63
  import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
63
- import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
64
+ import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, spawnEnvelopeRepairRequest, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
64
65
  import { handOffInstallHere } from "../lib/daemon/handoff.js";
65
66
  export { spawnLaneWorker };
66
67
  export { questionBodyAbsPath };
@@ -6892,6 +6893,12 @@ export function registerLanesCommand(parent, deps = {}) {
6892
6893
  return body;
6893
6894
  },
6894
6895
  }, wrap);
6896
+ registerLaneRepoMergeHoldCommand(lanes, {
6897
+ hqRoot: ctx.hqRoot,
6898
+ resolveCompany: ctx.resolveCompany,
6899
+ now: ctx.now,
6900
+ env: ctx.env,
6901
+ }, wrap);
6895
6902
  registerOutpostPrunerCommand(lanes);
6896
6903
  const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
6897
6904
  noStoryOption.attributeName = () => "noStory";
@@ -7148,6 +7155,28 @@ export function registerLanesCommand(parent, deps = {}) {
7148
7155
  .action(wrap(async (laneId) => {
7149
7156
  printJson(await messageHandlers.recoverQueuedMessages(ctx, laneId));
7150
7157
  }));
7158
+ lanes
7159
+ .command("_spawn-envelope-repair", { hidden: true })
7160
+ .action(wrap(async () => {
7161
+ let payload;
7162
+ try {
7163
+ payload = JSON.parse(ctx.readStdin());
7164
+ }
7165
+ catch (err) {
7166
+ throw new LanesError("repair_request_invalid", `Envelope repair request is invalid JSON: ${err instanceof Error ? err.message : String(err)}`, {});
7167
+ }
7168
+ printJson(spawnEnvelopeRepairRequest(ctx.hqRoot(), payload, {
7169
+ env: ctx.env,
7170
+ hostname: ctx.hostname(),
7171
+ existsSync: ctx.existsSync,
7172
+ spawnProcess: ctx.spawnProcess,
7173
+ proveDetached: ctx.proveDetached,
7174
+ killProcess: ctx.killProcess,
7175
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
7176
+ runCompaction: ctx.runCompaction,
7177
+ locateTranscript: ctx.locateTranscript,
7178
+ }));
7179
+ }));
7151
7180
  lanes
7152
7181
  .command("_monitor-message", { hidden: true })
7153
7182
  .argument("<lane-id>", "Lane id")
@@ -23,11 +23,21 @@ import { spawnSync } from 'child_process';
23
23
  import chalk from 'chalk';
24
24
  import semverSatisfies from 'semver/functions/satisfies.js';
25
25
  import { findHqRoot } from '../utils/manifest.js';
26
+ import { resolveHqRoot as resolveDetectedHqRoot } from './doctor.js';
26
27
  import { classify, resolveLatest, resolveLatestMarketplace, runScanPackages, installPack, } from './pack-install.js';
27
28
  import { contributionLinks, linkStatus, listInstalledPacks, findDependentPacks, readPackManifest, unwirePack, unwirePackMcp, readHqVersion, readRecommendedPackages, packagesDir, } from '../utils/pack-contributions.js';
28
29
  function resolveRoot(opts) {
29
30
  return opts.hqRoot ? path.resolve(opts.hqRoot) : findHqRoot();
30
31
  }
32
+ function resolvePackListRoot(opts) {
33
+ if (opts.hqRoot)
34
+ return path.resolve(opts.hqRoot);
35
+ const hqRoot = resolveDetectedHqRoot(process.cwd());
36
+ if (!hqRoot) {
37
+ throw new Error('hq packages: not inside an HQ tree. Run this command from inside your HQ root or pass --hq-root <path>.');
38
+ }
39
+ return hqRoot;
40
+ }
31
41
  /** JSON when --json or when piped (non-TTY); human-readable on a terminal. */
32
42
  function wantsJson(opts) {
33
43
  return opts.json === true || !process.stdout.isTTY;
@@ -377,7 +387,7 @@ export function registerPacksListCommand(parent) {
377
387
  .option('--eval-conditionals', 'Evaluate catalog conditional predicates (runs bash)')
378
388
  .action(async (opts) => {
379
389
  try {
380
- const view = await buildListView(resolveRoot(opts), !!opts.checkUpdates, !!opts.evalConditionals, !!opts.refresh);
390
+ const view = await buildListView(resolvePackListRoot(opts), !!opts.checkUpdates, !!opts.evalConditionals, !!opts.refresh);
381
391
  if (wantsJson(opts))
382
392
  emitJson(view);
383
393
  else
@@ -1,10 +1,11 @@
1
1
  /**
2
- * hq reindex — surface namespaced skills, mirror the personal overlay into
3
- * core/, regenerate the workers registry, and converge HQ-owned hook trust for
4
- * Codex, Grok, and Claude Code.
2
+ * hq reindex — surface namespaced skills and commands, clean up retired
3
+ * personal-overlay mirror links, capture coding-harness session logs, regenerate
4
+ * the workers registry, and converge HQ-owned hook trust for Codex, Grok, and
5
+ * Claude Code.
5
6
  *
6
- * Thin wrapper over @indigoai-us/hq-cloud's reindex(), which execs the bundled
7
- * scripts/reindex.sh against the HQ root. This command is what the hq-core
7
+ * Thin wrapper over @indigoai-us/hq-cloud's native TypeScript reindex(), which
8
+ * runs against the HQ root. This command is what the hq-core
8
9
  * reindex hook shim calls on Stop / PostToolUse, and what sync()/rescue() call
9
10
  * after they change on-disk sources.
10
11
  *
@@ -5,7 +5,7 @@
5
5
  * ~/.hq-agent/host-key.pub Ed25519 public key, SPKI PEM
6
6
  * ~/.hq-agent/machine-creds.json credentials + hostKeyPath/companySlug/apiBaseUrl, 0600
7
7
  * ~/.hq-agent/kit.json kit install settings (hq root, intervals)
8
- * ~/.hq-agent/state/ component-{sync,mesh,inbox}, last-heartbeat.json
8
+ * ~/.hq-agent/state/ component-{sync,inbox}, last-heartbeat.json
9
9
  * ~/.hq-agent/inbox/ mirrored inbox items (one JSON per message)
10
10
  * ~/.hq-agent/attachments/<scope>/ message attachments fetched on demand (0700 / 0600)
11
11
  * ~/.hq-agent/logs/*.log one log per kit service
@@ -5,7 +5,7 @@
5
5
  * ~/.hq-agent/host-key.pub Ed25519 public key, SPKI PEM
6
6
  * ~/.hq-agent/machine-creds.json credentials + hostKeyPath/companySlug/apiBaseUrl, 0600
7
7
  * ~/.hq-agent/kit.json kit install settings (hq root, intervals)
8
- * ~/.hq-agent/state/ component-{sync,mesh,inbox}, last-heartbeat.json
8
+ * ~/.hq-agent/state/ component-{sync,inbox}, last-heartbeat.json
9
9
  * ~/.hq-agent/inbox/ mirrored inbox items (one JSON per message)
10
10
  * ~/.hq-agent/attachments/<scope>/ message attachments fetched on demand (0700 / 0600)
11
11
  * ~/.hq-agent/logs/*.log one log per kit service
@@ -1,12 +1,10 @@
1
1
  /**
2
2
  * Kit service: heartbeat reporter.
3
3
  *
4
- * Every 60 s POST /v1/agents/{uid}/heartbeat with
5
- * `{ components: { sync, mesh, inbox }, cliVersion, hostInfo }` — the
6
- * external-agent body from api-contract v1, mirroring the hosted box's
7
- * hq-agent-heartbeat.sh (which folds per-component stamp files into one
8
- * beat). Component values are read from the stamps the other three services
9
- * write; a missing or stale stamp reports `error`, never silence.
4
+ * Every 60 s POST /v1/agents/{uid}/heartbeat with the enabled component status
5
+ * values (`sync` and `inbox`), plus cliVersion and hostInfo. The external-agent
6
+ * body mirrors the hosted box's hq-agent-heartbeat.sh, which folds per-component
7
+ * stamp files into one beat. A missing or stale enabled stamp reports `error`.
10
8
  *
11
9
  * The last successful beat is recorded at state/last-heartbeat.json so
12
10
  * `hq agent probe` can report its age.
@@ -1,12 +1,10 @@
1
1
  /**
2
2
  * Kit service: heartbeat reporter.
3
3
  *
4
- * Every 60 s POST /v1/agents/{uid}/heartbeat with
5
- * `{ components: { sync, mesh, inbox }, cliVersion, hostInfo }` — the
6
- * external-agent body from api-contract v1, mirroring the hosted box's
7
- * hq-agent-heartbeat.sh (which folds per-component stamp files into one
8
- * beat). Component values are read from the stamps the other three services
9
- * write; a missing or stale stamp reports `error`, never silence.
4
+ * Every 60 s POST /v1/agents/{uid}/heartbeat with the enabled component status
5
+ * values (`sync` and `inbox`), plus cliVersion and hostInfo. The external-agent
6
+ * body mirrors the hosted box's hq-agent-heartbeat.sh, which folds per-component
7
+ * stamp files into one beat. A missing or stale enabled stamp reports `error`.
10
8
  *
11
9
  * The last successful beat is recorded at state/last-heartbeat.json so
12
10
  * `hq agent probe` can report its age.
@@ -0,0 +1,26 @@
1
+ import type { LaneRecord } from "./types.js";
2
+ import type { EnvelopeRepairSpawnInput } from "./spawn.js";
3
+ interface EnvelopeRepairDispatchResult {
4
+ error?: Error;
5
+ status: number | null;
6
+ signal: NodeJS.Signals | null;
7
+ stdout: string;
8
+ stderr: string;
9
+ }
10
+ interface EnvelopeRepairDispatchOptions {
11
+ encoding: "utf8";
12
+ timeout: number;
13
+ input: string;
14
+ env: NodeJS.ProcessEnv;
15
+ }
16
+ export interface EnvelopeRepairDispatchDeps {
17
+ env?: NodeJS.ProcessEnv;
18
+ spawnSync?: (command: string, args: string[], options: EnvelopeRepairDispatchOptions) => EnvelopeRepairDispatchResult;
19
+ }
20
+ /**
21
+ * Dispatch repair into the currently installed CLI. A watcher can stay alive
22
+ * across upgrades, so repair must not call its in-memory spawn implementation.
23
+ */
24
+ export declare function spawnEnvelopeRepairViaCurrentCli(hqRoot: string, lane: LaneRecord, input: EnvelopeRepairSpawnInput, deps?: EnvelopeRepairDispatchDeps): number;
25
+ export {};
26
+ //# sourceMappingURL=envelope-repair-dispatch.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { spawnSync as nodeSpawnSync } from "node:child_process";
2
+ import * as path from "node:path";
3
+ import { LanesError } from "./errors.js";
4
+ /**
5
+ * Dispatch repair into the currently installed CLI. A watcher can stay alive
6
+ * across upgrades, so repair must not call its in-memory spawn implementation.
7
+ */
8
+ export function spawnEnvelopeRepairViaCurrentCli(hqRoot, lane, input, deps = {}) {
9
+ const request = {
10
+ lane_id: lane.lane_id,
11
+ input: {
12
+ ...(input.session_id !== undefined ? { session_id: input.session_id } : {}),
13
+ envelopePath: input.envelopePath,
14
+ error: input.error,
15
+ rawText: input.rawText,
16
+ ...(input.missing !== undefined ? { missing: input.missing } : {}),
17
+ },
18
+ };
19
+ const run = deps.spawnSync ?? ((command, args, options) => {
20
+ const result = nodeSpawnSync(command, args, options);
21
+ return {
22
+ ...(result.error !== undefined ? { error: result.error } : {}),
23
+ status: result.status,
24
+ signal: result.signal,
25
+ stdout: result.stdout ?? "",
26
+ stderr: result.stderr ?? "",
27
+ };
28
+ });
29
+ const env = deps.env ?? process.env;
30
+ const cliEntry = env.HQ_LANE_CLI_ENTRY?.trim();
31
+ if (!cliEntry || !path.isAbsolute(cliEntry) || path.resolve(cliEntry) !== cliEntry) {
32
+ throw new LanesError("repair_cli_entry_invalid", "Lane watcher has no valid absolute hq CLI entrypoint for envelope repair", { lane_id: lane.lane_id });
33
+ }
34
+ const result = run(process.execPath, [cliEntry, "lanes", "_spawn-envelope-repair"], {
35
+ encoding: "utf8",
36
+ timeout: 120_000,
37
+ input: JSON.stringify(request),
38
+ env: {
39
+ ...env,
40
+ HQ_ROOT: hqRoot,
41
+ HQ_NO_UPDATE_CHECK: "1",
42
+ HQ_TELEMETRY: "0",
43
+ },
44
+ });
45
+ if (result.error) {
46
+ throw new LanesError("repair_cli_spawn_failed", `Current hq CLI could not start the envelope repair: ${result.error.message}`, { lane_id: lane.lane_id });
47
+ }
48
+ if (result.status !== 0) {
49
+ const detail = result.stderr.trim() || `exit ${result.status ?? result.signal ?? "without status"}`;
50
+ throw new LanesError("repair_cli_failed", `Current hq CLI envelope repair failed: ${detail}`, { lane_id: lane.lane_id, status: result.status, signal: result.signal });
51
+ }
52
+ let parsed;
53
+ try {
54
+ parsed = JSON.parse(result.stdout);
55
+ }
56
+ catch (err) {
57
+ throw new LanesError("repair_cli_result_invalid", `Current hq CLI returned invalid envelope repair JSON: ${err instanceof Error ? err.message : String(err)}`, { lane_id: lane.lane_id });
58
+ }
59
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
60
+ throw new LanesError("repair_cli_result_invalid", "Current hq CLI returned an invalid envelope repair result", { lane_id: lane.lane_id });
61
+ }
62
+ const response = parsed;
63
+ if (!Number.isInteger(response.pid) ||
64
+ Number(response.pid) <= 0 ||
65
+ !Array.isArray(response.profile_warnings) ||
66
+ !response.profile_warnings.every((warning) => Array.isArray(warning) && warning.every((field) => typeof field === "string"))) {
67
+ throw new LanesError("repair_cli_result_invalid", "Current hq CLI returned an invalid envelope repair result", { lane_id: lane.lane_id });
68
+ }
69
+ for (const warning of response.profile_warnings) {
70
+ input.onProfileWarning?.(warning);
71
+ }
72
+ return Number(response.pid);
73
+ }
74
+ //# sourceMappingURL=envelope-repair-dispatch.js.map
@@ -17,6 +17,7 @@ export interface ReadLanesFlagOptions {
17
17
  getToken?: () => string | Promise<string>;
18
18
  reportFailure?: (error: unknown) => void;
19
19
  }
20
+ export declare function readLanesFlagLocalOverride(flagKey: string, env?: NodeJS.ProcessEnv): boolean | null;
20
21
  /**
21
22
  * Read a lanes flag once for each company identity in this CLI process. The
22
23
  * cache contains only an in-flight or settled boolean promise; it is never
@@ -3,6 +3,9 @@ import { ensureCognitoIdToken } from "../../utils/cognito-session.js";
3
3
  import { FLAG_REGISTRY_DEFAULT_ENDPOINT, resolveFlagRegistryEndpoint, } from "../flag-registry-endpoint.js";
4
4
  export const LANES_FLAG_API_DEFAULT_ENDPOINT = FLAG_REGISTRY_DEFAULT_ENDPOINT;
5
5
  export const LANES_FLAG_READ_TIMEOUT_MS = 5_000;
6
+ export function readLanesFlagLocalOverride(flagKey, env = process.env) {
7
+ return readLocalOverride(flagKey, env);
8
+ }
6
9
  const registryReads = new Map();
7
10
  class MissingFlagTokenError extends Error {
8
11
  constructor() {
@@ -37,7 +40,7 @@ function cacheKey(flagKey, company, endpoint) {
37
40
  */
38
41
  export async function readLanesRegistryFlag(flagKey, options = {}) {
39
42
  const env = options.env ?? process.env;
40
- const localOverride = readLocalOverride(flagKey, env);
43
+ const localOverride = readLanesFlagLocalOverride(flagKey, env);
41
44
  if (localOverride !== null)
42
45
  return localOverride;
43
46
  const endpoint = resolveFlagRegistryEndpoint(undefined, env);
@@ -68,6 +68,8 @@ export type ProveDetachedFn = (pid: number) => DetachProof;
68
68
  export type KillProcessFn = (pid: number, signal?: NodeJS.Signals) => void;
69
69
  export interface SpawnLaneDeps {
70
70
  env?: NodeJS.ProcessEnv;
71
+ /** Absolute CLI entry captured when the watcher is armed; tests may inject it. */
72
+ hqCliEntry?: string;
71
73
  hostname?: string;
72
74
  /** The caller's host scope for admission checks; reconcile supplies its resolved host. */
73
75
  admissionHost?: AdmissionHost;
@@ -311,6 +313,16 @@ export interface EnvelopeRepairSpawnInput {
311
313
  /** Repair-only warning hook for profile metadata ignored during recovery. */
312
314
  onProfileWarning?: (fields: readonly string[]) => void;
313
315
  }
316
+ /**
317
+ * Handle the private request sent by an older, long-running watcher to the
318
+ * currently installed `hq` executable. The persisted repair attempt is the
319
+ * authorization for this one-shot spawn; the request cannot select an
320
+ * arbitrary lane or write outside that lane's envelope directory.
321
+ */
322
+ export declare function spawnEnvelopeRepairRequest(hqRoot: string, rawRequest: unknown, deps?: SpawnLaneDeps): {
323
+ pid: number;
324
+ profile_warnings: string[][];
325
+ };
314
326
  /** How the spec §4 repair turn reaches the provider (recorded as `envelope_repair.mode`). */
315
327
  export type EnvelopeRepairMode = "resume" | "fresh_turn";
316
328
  /**
@@ -1508,10 +1508,15 @@ export function armLaneWatcher(hqRoot, laneId, deps = {}) {
1508
1508
  const runDir = laneRunDir(hqRoot, laneId);
1509
1509
  const logsDir = path.join(runDir, "logs");
1510
1510
  ensureDir(logsDir);
1511
+ const cliEntry = deps.hqCliEntry?.trim() || process.argv[1]?.trim();
1512
+ if (!cliEntry) {
1513
+ throw new LanesError("watcher_cli_entry_unavailable", "Cannot arm the lane watcher without the current hq CLI entrypoint", { lane_id: laneId });
1514
+ }
1511
1515
  const watcherPid = spawnDetached(spawnProcess, process.execPath, [watcherMainPath(), laneId], {
1512
1516
  ...env,
1513
1517
  HQ_ROOT: hqRoot,
1514
1518
  HQ_LANE_ID: laneId,
1519
+ HQ_LANE_CLI_ENTRY: path.resolve(cliEntry),
1515
1520
  }, hqRoot, path.join(logsDir, "watcher.out"), path.join(logsDir, "watcher.err"));
1516
1521
  writePidRecord(path.join(runDir, "watcher.pid"), {
1517
1522
  pid: watcherPid,
@@ -2228,6 +2233,88 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2228
2233
  }
2229
2234
  return laneLaunchResult(readLane(hqRoot, lane.lane_id));
2230
2235
  }
2236
+ /**
2237
+ * Handle the private request sent by an older, long-running watcher to the
2238
+ * currently installed `hq` executable. The persisted repair attempt is the
2239
+ * authorization for this one-shot spawn; the request cannot select an
2240
+ * arbitrary lane or write outside that lane's envelope directory.
2241
+ */
2242
+ export function spawnEnvelopeRepairRequest(hqRoot, rawRequest, deps = {}) {
2243
+ const invalid = (message) => {
2244
+ throw new LanesError("repair_request_invalid", message, {});
2245
+ };
2246
+ if (!rawRequest || typeof rawRequest !== "object" || Array.isArray(rawRequest)) {
2247
+ return invalid("Envelope repair request must be an object");
2248
+ }
2249
+ const request = rawRequest;
2250
+ if (Object.keys(request).some((key) => key !== "lane_id" && key !== "input")) {
2251
+ return invalid("Envelope repair request has unknown fields");
2252
+ }
2253
+ if (typeof request.lane_id !== "string" ||
2254
+ !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(request.lane_id)) {
2255
+ return invalid("Envelope repair request must include a lane_id");
2256
+ }
2257
+ if (!request.input || typeof request.input !== "object" || Array.isArray(request.input)) {
2258
+ return invalid("Envelope repair request input must be an object");
2259
+ }
2260
+ const rawInput = request.input;
2261
+ const allowedInputFields = new Set([
2262
+ "session_id",
2263
+ "envelopePath",
2264
+ "error",
2265
+ "rawText",
2266
+ "missing",
2267
+ ]);
2268
+ if (Object.keys(rawInput).some((key) => !allowedInputFields.has(key))) {
2269
+ return invalid("Envelope repair request input has unknown fields");
2270
+ }
2271
+ if (typeof rawInput.envelopePath !== "string" ||
2272
+ !path.isAbsolute(rawInput.envelopePath) ||
2273
+ path.resolve(rawInput.envelopePath) !== rawInput.envelopePath ||
2274
+ typeof rawInput.error !== "string" ||
2275
+ !rawInput.error.trim() ||
2276
+ typeof rawInput.rawText !== "string" ||
2277
+ (rawInput.session_id !== undefined &&
2278
+ rawInput.session_id !== null &&
2279
+ (typeof rawInput.session_id !== "string" || !rawInput.session_id.trim())) ||
2280
+ (rawInput.missing !== undefined && typeof rawInput.missing !== "boolean")) {
2281
+ return invalid("Envelope repair request input has invalid fields");
2282
+ }
2283
+ const lane = readLane(hqRoot, request.lane_id);
2284
+ const repair = lane.envelope_repair;
2285
+ if (lane.state !== "running" || !repair?.attempted || repair.succeeded) {
2286
+ throw new LanesError("repair_request_not_authorized", `Lane ${lane.lane_id} has no pending envelope repair attempt`, { lane_id: lane.lane_id });
2287
+ }
2288
+ if (repair.first_error !== rawInput.error) {
2289
+ throw new LanesError("repair_request_stale", `Lane ${lane.lane_id} envelope repair request no longer matches its recorded error`, { lane_id: lane.lane_id });
2290
+ }
2291
+ const envelopeDir = path.resolve(laneRunDir(hqRoot, lane.lane_id), "envelopes");
2292
+ const envelopePath = path.resolve(rawInput.envelopePath);
2293
+ if (path.dirname(envelopePath) !== envelopeDir ||
2294
+ !path.basename(envelopePath).endsWith(".json") ||
2295
+ typeof repair.envelope_ref !== "string" ||
2296
+ path.posix.dirname(repair.envelope_ref) !== "envelopes" ||
2297
+ !path.posix.basename(repair.envelope_ref).endsWith(".json")) {
2298
+ throw new LanesError("repair_request_path_invalid", `Lane ${lane.lane_id} envelope repair request does not name a lane envelope`, { lane_id: lane.lane_id });
2299
+ }
2300
+ const recordedEnvelopePath = path.resolve(laneRunDir(hqRoot, lane.lane_id), repair.envelope_ref);
2301
+ if (envelopePath !== recordedEnvelopePath) {
2302
+ throw new LanesError("repair_request_envelope_mismatch", `Lane ${lane.lane_id} envelope repair request does not match the recorded repair slot`, { lane_id: lane.lane_id, envelope_ref: repair.envelope_ref });
2303
+ }
2304
+ const warnings = [];
2305
+ const input = {
2306
+ envelopePath,
2307
+ error: rawInput.error,
2308
+ rawText: rawInput.rawText,
2309
+ ...(typeof rawInput.session_id === "string" || rawInput.session_id === null
2310
+ ? { session_id: rawInput.session_id }
2311
+ : {}),
2312
+ ...(typeof rawInput.missing === "boolean" ? { missing: rawInput.missing } : {}),
2313
+ onProfileWarning: (fields) => warnings.push([...fields]),
2314
+ };
2315
+ const pid = spawnEnvelopeRepair(hqRoot, lane, input, deps);
2316
+ return { pid, profile_warnings: warnings };
2317
+ }
2231
2318
  /**
2232
2319
  * `resume`: claude and codex continue the recorded session with the repair
2233
2320
  * prompt. `fresh_turn`: grok gets a new single-turn invocation on the same
@@ -5,5 +5,7 @@
5
5
  * Usage: node watcher-main.js <lane-id>
6
6
  * Requires HQ_ROOT in the environment.
7
7
  */
8
- export {};
8
+ import { type EnvelopeRepairDispatchDeps } from "./envelope-repair-dispatch.js";
9
+ import type { WatcherLoopDeps } from "./watcher.js";
10
+ export declare function watcherMainDeps(hqRoot: string, laneId: string, env?: NodeJS.ProcessEnv, repairDispatchDeps?: Pick<EnvelopeRepairDispatchDeps, "spawnSync">): WatcherLoopDeps;
9
11
  //# sourceMappingURL=watcher-main.d.ts.map
@@ -6,12 +6,33 @@
6
6
  * Requires HQ_ROOT in the environment.
7
7
  */
8
8
  import { spawnSync } from "node:child_process";
9
+ import { existsSync, realpathSync } from "node:fs";
9
10
  import * as path from "node:path";
10
11
  import { fileURLToPath } from "node:url";
11
12
  import { runWatcherLoop } from "./watcher.js";
13
+ import { spawnEnvelopeRepairViaCurrentCli, } from "./envelope-repair-dispatch.js";
12
14
  import { LanesError } from "./errors.js";
13
15
  import { openMeshTransport } from "../mesh/client.js";
14
16
  import { CompanySelectionError } from "../../utils/company-selection-error.js";
17
+ export function watcherMainDeps(hqRoot, laneId, env = process.env, repairDispatchDeps = {}) {
18
+ const cliEntry = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../index.js");
19
+ return {
20
+ resolveQuestionActiveCompany,
21
+ spawnRepair: (root, lane, input) => spawnEnvelopeRepairViaCurrentCli(root, lane, input, { ...repairDispatchDeps, env }),
22
+ recoverQueuedMessages: async () => {
23
+ const result = spawnSync(process.execPath, [cliEntry, "lanes", "_recover-queued-messages", laneId], {
24
+ env,
25
+ encoding: "utf8",
26
+ timeout: 120_000,
27
+ });
28
+ if (result.error)
29
+ throw result.error;
30
+ if (result.status !== 0) {
31
+ throw new Error(`recovery command exited ${result.status ?? result.signal ?? "without status"}: ${(result.stderr ?? "").trim()}`);
32
+ }
33
+ },
34
+ };
35
+ }
15
36
  async function resolveQuestionActiveCompany() {
16
37
  try {
17
38
  const { company } = await openMeshTransport({});
@@ -30,25 +51,14 @@ function main() {
30
51
  process.stderr.write("hq lanes watcher: requires lane id argv and HQ_ROOT in the environment\n");
31
52
  process.exit(1);
32
53
  }
33
- const cliEntry = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../index.js");
34
- void runWatcherLoop(hqRoot, laneId, {
35
- resolveQuestionActiveCompany,
36
- recoverQueuedMessages: async () => {
37
- const result = spawnSync(process.execPath, [cliEntry, "lanes", "_recover-queued-messages", laneId], {
38
- env: process.env,
39
- encoding: "utf8",
40
- timeout: 120_000,
41
- });
42
- if (result.error)
43
- throw result.error;
44
- if (result.status !== 0) {
45
- throw new Error(`recovery command exited ${result.status ?? result.signal ?? "without status"}: ${(result.stderr ?? "").trim()}`);
46
- }
47
- },
48
- }).catch((err) => {
54
+ void runWatcherLoop(hqRoot, laneId, watcherMainDeps(hqRoot, laneId, process.env)).catch((err) => {
49
55
  process.stderr.write(`hq lanes watcher: ${err instanceof Error ? err.message : String(err)}${err instanceof Error && err.stack ? `\n${err.stack}` : ""}\n`);
50
56
  process.exit(1);
51
57
  });
52
58
  }
53
- main();
59
+ const invokedEntryPath = process.argv[1] ? path.resolve(process.argv[1]) : undefined;
60
+ const currentModule = realpathSync(fileURLToPath(import.meta.url));
61
+ if (invokedEntryPath && existsSync(invokedEntryPath) && realpathSync(invokedEntryPath) === currentModule) {
62
+ main();
63
+ }
54
64
  //# sourceMappingURL=watcher-main.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.256.0",
3
+ "version": "5.258.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {