@indigoai-us/hq-cli 5.294.0 → 5.296.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 (72) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/command-catalog.generated.d.ts +70 -9
  3. package/dist/command-catalog.generated.js +88 -12
  4. package/dist/commands/auth.d.ts +1 -0
  5. package/dist/commands/auth.js +76 -1
  6. package/dist/commands/core-job-native.d.ts +7 -0
  7. package/dist/commands/core-job-native.js +20 -0
  8. package/dist/commands/core-job-probe-native.d.ts +45 -0
  9. package/dist/commands/core-job-probe-native.js +564 -0
  10. package/dist/commands/core-job-remote-launcher.d.ts +7 -0
  11. package/dist/commands/core-job-remote-launcher.js +51 -0
  12. package/dist/commands/core-job-run-native.d.ts +47 -0
  13. package/dist/commands/core-job-run-native.js +1215 -0
  14. package/dist/commands/core.js +1 -1
  15. package/dist/commands/doctor.js +0 -74
  16. package/dist/commands/lanes-message.js +24 -5
  17. package/dist/commands/lanes-resume.js +1 -3
  18. package/dist/commands/lanes-shared.d.ts +1 -2
  19. package/dist/commands/lanes-stories.d.ts +1 -0
  20. package/dist/commands/lanes-stories.js +40 -1
  21. package/dist/commands/lanes.d.ts +1 -9
  22. package/dist/commands/lanes.js +197 -174
  23. package/dist/commands/run.js +5 -0
  24. package/dist/fast-scaffold.d.ts +0 -5
  25. package/dist/fast-scaffold.js +0 -10
  26. package/dist/index.js +1 -10
  27. package/dist/lib/core-utils/sentry-report.d.ts +0 -19
  28. package/dist/lib/core-utils/sentry-report.js +8 -61
  29. package/dist/lib/doctor/checks/claude-wiring.js +9 -0
  30. package/dist/lib/lanes/flag-keys.d.ts +2 -0
  31. package/dist/lib/lanes/flag-keys.js +2 -0
  32. package/dist/lib/lanes/manager-status-gate.d.ts +0 -17
  33. package/dist/lib/lanes/manager-status-gate.js +2 -43
  34. package/dist/lib/lanes/mesh-events.js +3 -0
  35. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -0
  36. package/dist/lib/lanes/outpost-dispatch.js +7 -1
  37. package/dist/lib/lanes/outpost-hire-cache.d.ts +0 -1
  38. package/dist/lib/lanes/outpost-hire-cache.js +0 -1
  39. package/dist/lib/lanes/outpost-snapshot-memo.d.ts +6 -0
  40. package/dist/lib/lanes/outpost-snapshot-memo.js +14 -0
  41. package/dist/lib/lanes/reconcile.d.ts +2 -3
  42. package/dist/lib/lanes/reconcile.js +67 -57
  43. package/dist/lib/lanes/spawn.d.ts +0 -2
  44. package/dist/lib/lanes/spawn.js +57 -5
  45. package/dist/lib/lanes/store.js +54 -0
  46. package/dist/lib/lanes/stories.d.ts +7 -1
  47. package/dist/lib/lanes/stories.js +16 -0
  48. package/dist/lib/lanes/types.d.ts +30 -1
  49. package/dist/lib/lanes/types.js +2 -2
  50. package/dist/lib/lanes/watcher.d.ts +4 -3
  51. package/dist/lib/lanes/watcher.js +122 -56
  52. package/dist/lib/mesh/live/daemon/run.js +18 -1
  53. package/dist/run/env-graph-guard.d.ts +2 -2
  54. package/dist/run/env-graph-guard.js +5 -2
  55. package/dist/run/hq-plugin.js +6 -5
  56. package/dist/utils/cognito-session.d.ts +6 -0
  57. package/dist/utils/cognito-session.js +9 -0
  58. package/dist/utils/self-update.js +31 -0
  59. package/dist/utils/version-gate.js +22 -3
  60. package/package.json +2 -2
  61. package/dist/fast-lanes-list-route.d.ts +0 -8
  62. package/dist/fast-lanes-list-route.js +0 -32
  63. package/dist/fast-lanes-list.d.ts +0 -3
  64. package/dist/fast-lanes-list.js +0 -224
  65. package/dist/lib/doctor/client-health-sentry-flag.d.ts +0 -23
  66. package/dist/lib/doctor/client-health-sentry-flag.js +0 -99
  67. package/dist/lib/doctor/client-health-sentry.d.ts +0 -71
  68. package/dist/lib/doctor/client-health-sentry.js +0 -286
  69. package/dist/lib/lanes/outpost-snapshot-cache.d.ts +0 -26
  70. package/dist/lib/lanes/outpost-snapshot-cache.js +0 -215
  71. package/dist/lib/lanes/outpost-snapshot-refresh.d.ts +0 -21
  72. package/dist/lib/lanes/outpost-snapshot-refresh.js +0 -329
@@ -36,8 +36,8 @@ import * as path from "node:path";
36
36
  import { LanesError } from "./errors.js";
37
37
  import { questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
38
38
  import { isTerminalLaneState, ENVELOPE_INVALID_REASON, parseLaneEnvelope, STORY_STATUSES, LANE_PROVIDERS, } from "./types.js";
39
- import { laneStories, storyStatusForEnvelope } from "./stories.js";
40
- import { appendCappedHistory, readLane, retryUpdateLane, withLaneHireLocks } from "./store.js";
39
+ import { laneStories, terminalEnvelopeStoryStatus } from "./stories.js";
40
+ import { appendCappedHistory, listQuestionIds, readLane, readQuestion, retryUpdateLane, withLaneHireLocks } from "./store.js";
41
41
  import { enforceRegistryApprovalGate } from "./approval-gate.js";
42
42
  import { readWorkerRunResult } from "./delivery.js";
43
43
  import { applyDefaultQuotaHold, quotaSignalFromFields, readQuotaSignal, recordAccountQuota, } from "./quota.js";
@@ -51,13 +51,15 @@ import { readLanesRegistryFlag } from "./registry-flag.js";
51
51
  import { laneCompanyUidSource, resolveLaneCompany } from "./lane-company.js";
52
52
  import { blockerFreshnessFingerprint, resolveBlockerFreshnessForLane, } from "./blocker-freshness.js";
53
53
  import { krFreshnessFingerprint, resolveKrFreshnessForLane, } from "./kr-freshness.js";
54
- import { MANAGER_STATUS_GATE_FLAG, managerStatusWasWritten, readStatusSnapshot, resolveManagerStatusTarget, managerStatusGateEnabledForLane, } from "./manager-status-gate.js";
54
+ import { managerStatusWasWritten, readStatusSnapshot, resolveManagerStatusTarget, } from "./manager-status-gate.js";
55
55
  import { queueLaneTerminalResume, queueManagerStatusGateResume } from "./keep-alive.js";
56
+ import { LANE_ENVELOPE_REPAIR_CONTINUE_FLAG } from "./flag-keys.js";
56
57
  import { monitorSessionDir } from "../monitor.js";
57
58
  import { laneRunDir, missingPidProbe, probePidRecord, readPidRecord, readProcStarttime, readSpawnManifest, envelopeRepairMode, spawnEnvelopeRepair, writePidRecord, retryLaneOnAlternateAccount, } from "./spawn.js";
58
59
  /** Spec §5.7: no signal for 20 minutes while the process is still alive. */
59
60
  export const SILENCE_MS = 20 * 60 * 1000;
60
61
  export const WATCHER_POLL_MS = 5_000;
62
+ export const ENVELOPE_REPAIR_CONTINUE_TEXT = "continue the task; the previous turn ended without an envelope.";
61
63
  /** Synced last_host_report_at cadence. Reconcile dead-host TTL is 20 minutes. */
62
64
  export const WATCHER_HEARTBEAT_MS = 60 * 1000;
63
65
  /**
@@ -72,6 +74,7 @@ export const WATCHER_DECISION_EVENTS = [
72
74
  "envelope_observed",
73
75
  "envelope_invalid",
74
76
  "envelope_repair",
77
+ "envelope_repair_continue",
75
78
  "classification",
76
79
  "terminal_write",
77
80
  "message_recovery",
@@ -420,27 +423,25 @@ export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, op
420
423
  artifacts: envelope.artifacts,
421
424
  ...(envelope.blocked_reason !== undefined ? { blocked_reason: envelope.blocked_reason } : {}),
422
425
  };
423
- next.story_status = envelope.story_status;
424
- next.story_complete = envelope.story_complete;
425
- next.story_complete_reason = envelope.story_complete_reason;
426
- const currentProject = current.project_id;
427
- const currentStory = current.story_id;
428
- const storyStatus = envelope.decision === "blocked"
429
- ? "blocked"
430
- : envelope.decision === "done"
431
- ? "review"
432
- : storyStatusForEnvelope(envelope.story_status);
433
- const updatedStories = laneStories(current).map((story) => story.project_id === currentProject && story.story_id === currentStory
434
- ? {
435
- ...story,
436
- status: storyStatus,
437
- updated_at: at,
438
- ...(envelope.decision === "blocked"
439
- ? { blocked_reason: envelope.blocked_reason ?? "" }
440
- : { blocked_reason: undefined }),
441
- }
442
- : story);
443
- next.stories = updatedStories;
426
+ const storyStatus = terminalEnvelopeStoryStatus(current, envelope.decision, envelope.story_status);
427
+ if (storyStatus !== undefined) {
428
+ next.story_status = envelope.story_status;
429
+ next.story_complete = envelope.story_complete;
430
+ next.story_complete_reason = envelope.story_complete_reason;
431
+ const currentProject = current.project_id;
432
+ const currentStory = current.story_id;
433
+ const updatedStories = laneStories(current).map((story) => story.project_id === currentProject && story.story_id === currentStory
434
+ ? {
435
+ ...story,
436
+ status: storyStatus,
437
+ updated_at: at,
438
+ ...(envelope.decision === "blocked"
439
+ ? { blocked_reason: envelope.blocked_reason ?? "" }
440
+ : { blocked_reason: undefined }),
441
+ }
442
+ : story);
443
+ next.stories = updatedStories;
444
+ }
444
445
  }
445
446
  return next;
446
447
  });
@@ -659,26 +660,9 @@ export function missingExpectedEnvelopeFile(hqRoot, laneId, envelopePath) {
659
660
  };
660
661
  }
661
662
  async function settleManagerStatusGate(hqRoot, lane, manifestTurnId, at, deps, logDecision) {
662
- const readFlag = deps.readManagerStatusGateFlag ?? readLanesRegistryFlag;
663
- let enabled = false;
664
- try {
665
- const resolution = resolveLaneCompany(hqRoot, lane);
666
- const company = resolution.company ?? undefined;
667
- enabled = await readFlag(MANAGER_STATUS_GATE_FLAG, {
668
- company,
669
- ...(company ? {
670
- companyUidSource: laneCompanyUidSource(resolution.source),
671
- } : {}),
672
- });
673
- }
674
- catch (err) {
675
- process.stderr.write(`hq lanes watcher: manager status gate flag read failed for ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}\n`);
676
- logDecision({ event: "status_gate_flag_failed", at, error: err instanceof Error ? err.name : "UnknownError" });
677
- }
678
- if (!enabled)
679
- return lane;
680
663
  let target;
681
664
  try {
665
+ resolveLaneCompany(hqRoot, lane);
682
666
  target = resolveManagerStatusTarget(hqRoot, lane);
683
667
  }
684
668
  catch (err) {
@@ -725,8 +709,8 @@ async function settleManagerStatusGate(hqRoot, lane, manifestTurnId, at, deps, l
725
709
  turn_id: gate.turn_id,
726
710
  path: gate.status_path,
727
711
  consecutive_misses: updated.status_gate?.consecutive_misses ?? 0,
728
- resume_queued: miss.resume_queued === true,
729
- resume_skipped_reason: miss.resume_skipped_reason,
712
+ resume_queued: "resume_queued" in miss && miss.resume_queued === true,
713
+ resume_skipped_reason: "resume_skipped_reason" in miss ? miss.resume_skipped_reason : undefined,
730
714
  state: updated.state,
731
715
  });
732
716
  }
@@ -1058,6 +1042,92 @@ function claimWatcherLoop(hqRoot, laneId, deps) {
1058
1042
  });
1059
1043
  };
1060
1044
  }
1045
+ async function maybeContinueAfterMissingEnvelopeRepair(hqRoot, lane, envelope, envelopeRef, at, deps, logDecision) {
1046
+ const repair = lane.envelope_repair;
1047
+ const missingRepair = repair?.attempted === true &&
1048
+ repair.succeeded === true &&
1049
+ repair.envelope_ref === envelopeRef &&
1050
+ repair.first_error.startsWith("Missing expected envelope file:");
1051
+ if (!missingRepair || envelope === null || envelopeRef === undefined) {
1052
+ if (envelope?.decision === "blocked")
1053
+ logDecision({ event: "envelope_repair_continue", at, outcome: "skipped", reason: "not_a_successful_missing_repair", missing_repair: missingRepair, envelope_ref: envelopeRef, repair_ref: repair?.envelope_ref, first_error: repair?.first_error, succeeded: repair?.succeeded });
1054
+ return lane;
1055
+ }
1056
+ const incompleteDone = envelope.decision === "done" && envelope.story_complete === false;
1057
+ const repairInstructionBlock = envelope.decision === "blocked" &&
1058
+ (envelope.questions?.length ?? 0) === 0 &&
1059
+ /(?:not to resume|do not resume|don't resume)(?: the)? task/i.test(envelope.blocked_reason ?? "");
1060
+ if (!incompleteDone && !repairInstructionBlock) {
1061
+ if (envelope.decision === "blocked")
1062
+ logDecision({ event: "envelope_repair_continue", at, outcome: "skipped", reason: "blocked_reason_not_repair_instruction", blocked_reason: envelope.blocked_reason });
1063
+ return lane;
1064
+ }
1065
+ if (lane.last_resume?.text === ENVELOPE_REPAIR_CONTINUE_TEXT) {
1066
+ logDecision({ event: "envelope_repair_continue", at, outcome: "skipped", reason: "continue_already_used" });
1067
+ return lane;
1068
+ }
1069
+ let pendingQuestion = false;
1070
+ try {
1071
+ for (const id of listQuestionIds(hqRoot)) {
1072
+ const question = readQuestion(hqRoot, id);
1073
+ if (question.lane_id === lane.lane_id && question.status === "pending") {
1074
+ pendingQuestion = true;
1075
+ break;
1076
+ }
1077
+ }
1078
+ }
1079
+ catch (error) {
1080
+ logDecision({ event: "envelope_repair_continue", at, outcome: "skipped", reason: "question_check_failed", error: error instanceof Error ? error.message : String(error) });
1081
+ return lane;
1082
+ }
1083
+ if (pendingQuestion) {
1084
+ logDecision({ event: "envelope_repair_continue", at, outcome: "skipped", reason: "pending_question" });
1085
+ return lane;
1086
+ }
1087
+ const readFlag = deps.readEnvelopeRepairContinueFlag ?? readLanesRegistryFlag;
1088
+ let enabled = false;
1089
+ try {
1090
+ const resolution = resolveLaneCompany(hqRoot, lane);
1091
+ const company = resolution.company ?? undefined;
1092
+ enabled = await readFlag(LANE_ENVELOPE_REPAIR_CONTINUE_FLAG, {
1093
+ company,
1094
+ ...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
1095
+ });
1096
+ }
1097
+ catch (error) {
1098
+ logDecision({ event: "envelope_repair_continue", at, outcome: "skipped", reason: "flag_read_failed", error: error instanceof Error ? error.message : String(error) });
1099
+ return lane;
1100
+ }
1101
+ if (!enabled)
1102
+ return lane;
1103
+ const queued = retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
1104
+ if (current.version !== lane.version ||
1105
+ (current.state !== "done" && current.state !== "blocked") ||
1106
+ current.last_envelope?.ref !== envelopeRef ||
1107
+ current.envelope_repair?.attempted !== true ||
1108
+ current.envelope_repair.succeeded !== true ||
1109
+ current.envelope_repair.envelope_ref !== envelopeRef ||
1110
+ current.envelope_repair.first_error !== repair.first_error ||
1111
+ current.last_resume?.text === ENVELOPE_REPAIR_CONTINUE_TEXT)
1112
+ return current;
1113
+ const result = queueLaneTerminalResume(current, at, ENVELOPE_REPAIR_CONTINUE_TEXT, { allowBlockedResume: true });
1114
+ if (!result.resumeQueued || result.lane.pending_resume?.session_id === undefined)
1115
+ return current;
1116
+ return {
1117
+ ...result.lane,
1118
+ status_gate_history: appendCappedHistory(current.status_gate_history ?? [], {
1119
+ event: "envelope_repair_continued",
1120
+ at,
1121
+ envelope_ref: envelopeRef,
1122
+ session_id: result.lane.pending_resume.session_id,
1123
+ }),
1124
+ };
1125
+ });
1126
+ if (queued.state === "queued" && queued.pending_resume?.text === ENVELOPE_REPAIR_CONTINUE_TEXT) {
1127
+ logDecision({ event: "envelope_repair_continue", at, outcome: "queued", envelope_ref: envelopeRef, session_id: queued.pending_resume.session_id });
1128
+ }
1129
+ return queued;
1130
+ }
1061
1131
  /** Detached and synchronous classifiers share one process-identity claim. */
1062
1132
  export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
1063
1133
  const release = claimWatcherLoop(hqRoot, laneId, deps);
@@ -1144,17 +1214,10 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1144
1214
  }
1145
1215
  const progressIntervalMs = deps.progressEventMinIntervalMs ?? readProgressIntervalMs(hqRoot, readLaneSoft(hqRoot, laneId));
1146
1216
  const spawnRepair = deps.spawnRepair ?? spawnEnvelopeRepair;
1147
- const retryAccount = deps.retryAccount ?? (async (root, lane, reason) => {
1148
- const managerStatusGateEnabled = await managerStatusGateEnabledForLane(root, lane, {
1149
- env: deps.retrySpawnDeps?.env,
1150
- readFlag: deps.readManagerStatusGateFlag,
1151
- });
1152
- return retryLaneOnAlternateAccount(root, lane, reason, "watcher", {
1153
- ...deps.retrySpawnDeps,
1154
- managerStatusGateEnabled,
1155
- admissionHost: (deps.resolveAdmissionHost ?? resolveProcessAdmissionHost)(),
1156
- });
1157
- });
1217
+ const retryAccount = deps.retryAccount ?? ((root, lane, reason) => retryLaneOnAlternateAccount(root, lane, reason, "watcher", {
1218
+ ...deps.retrySpawnDeps,
1219
+ admissionHost: (deps.resolveAdmissionHost ?? resolveProcessAdmissionHost)(),
1220
+ }));
1158
1221
  let lastSignalMs = now();
1159
1222
  let lastSeenMtime = 0;
1160
1223
  let lastAlive = null;
@@ -1542,7 +1605,10 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
1542
1605
  ((entry.event === "status_missed" && entry.resume_queued === true) ||
1543
1606
  (entry.event === "blocker_stale" && entry.resume_queued === true) ||
1544
1607
  (entry.event === "kr_stalled" && entry.resume_queued === true))) === true;
1545
- if (terminalWrite.changed && deps.recoverQueuedMessages && !managerStatusFollowupQueued) {
1608
+ if (terminalWrite.changed && !managerStatusFollowupQueued) {
1609
+ updated = await maybeContinueAfterMissingEnvelopeRepair(hqRoot, updated, effectiveEnvelope ?? null, latest?.ref, tick.emit.at, deps, logDecision);
1610
+ }
1611
+ if (terminalWrite.changed && deps.recoverQueuedMessages && !managerStatusFollowupQueued && updated.state !== "queued") {
1546
1612
  try {
1547
1613
  if (dropboxInboxHasMessages(hqRoot, laneId)) {
1548
1614
  await deps.recoverQueuedMessages(updated);
@@ -701,13 +701,30 @@ export async function runMeshDaemon(deps = {}) {
701
701
  if (!fs.existsSync(spoolPath)) {
702
702
  fs.writeFileSync(spoolPath, "", { mode: 0o600 });
703
703
  }
704
- watcher = fs.watch(path.dirname(spoolPath), { persistent: true }, (_event, filename) => {
704
+ const spoolWatcher = fs.watch(path.dirname(spoolPath), { persistent: true }, (_event, filename) => {
705
705
  if (stopped)
706
706
  return;
707
707
  if (shouldFlushOnSpoolDirEvent(filename)) {
708
708
  scheduleDebouncedFlush();
709
709
  }
710
710
  });
711
+ watcher = spoolWatcher;
712
+ spoolWatcher.on("error", (err) => {
713
+ // fs.watch can fail asynchronously after returning a watcher. Without
714
+ // an error listener Node treats EMFILE as an uncaught exception; the
715
+ // periodic flush below already provides a polling fallback.
716
+ if (watcher === spoolWatcher)
717
+ watcher = null;
718
+ try {
719
+ spoolWatcher.close();
720
+ }
721
+ catch (closeError) {
722
+ const closeMessage = closeError instanceof Error ? closeError.message : String(closeError);
723
+ log(dir, `fs.watch close failed after watcher error: ${closeMessage.slice(0, 120)}`);
724
+ }
725
+ const msg = err instanceof Error ? err.message : String(err);
726
+ log(dir, `fs.watch error; continuing interval flush: ${msg.slice(0, 120)}`);
727
+ });
711
728
  }
712
729
  catch (err) {
713
730
  const msg = err instanceof Error ? err.message : String(err);
@@ -1,5 +1,5 @@
1
1
  /** The varlock version hq-cli is pinned to (package.json `dependencies`). */
2
- export declare const VARLOCK_PINNED_VERSION = "1.0.0";
2
+ export declare const VARLOCK_PINNED_VERSION = "1.21.1";
3
3
  /** A schema/plugin source varlock could not finish loading. */
4
4
  interface GuardDataSource {
5
5
  readonly isValid: boolean;
@@ -68,7 +68,7 @@ export declare function assertEnvGraphLoaded(graph: LoadedEnvGraph): void;
68
68
  * rejection into a thrown EnvResolutionError instead of hanging on the promise
69
69
  * that never settles.
70
70
  *
71
- * varlock@1.0.0's resolveEnvValues dispatches its per-item resolver un-awaited
71
+ * varlock's resolveEnvValues dispatches its per-item resolver un-awaited
72
72
  * and with a dead `_reject`, so a throw inside it becomes a process-level
73
73
  * unhandled rejection AND leaves the returned promise permanently pending. We
74
74
  * race the resolve against a scoped `unhandledRejection` listener: whichever
@@ -34,7 +34,7 @@
34
34
  // actionable `hq: …` line WITHOUT a Sentry capture (see isVarlockEnvError).
35
35
  import { claimRejection, releaseRejection, } from "../unhandled-rejection-boundary.js";
36
36
  /** The varlock version hq-cli is pinned to (package.json `dependencies`). */
37
- export const VARLOCK_PINNED_VERSION = "1.0.0";
37
+ export const VARLOCK_PINNED_VERSION = "1.21.1";
38
38
  /**
39
39
  * A schema/entry file could not be loaded, so the env graph never finished
40
40
  * processing. Carries an actionable, file-naming message; classified by
@@ -114,7 +114,7 @@ export function assertEnvGraphLoaded(graph) {
114
114
  * rejection into a thrown EnvResolutionError instead of hanging on the promise
115
115
  * that never settles.
116
116
  *
117
- * varlock@1.0.0's resolveEnvValues dispatches its per-item resolver un-awaited
117
+ * varlock's resolveEnvValues dispatches its per-item resolver un-awaited
118
118
  * and with a dead `_reject`, so a throw inside it becomes a process-level
119
119
  * unhandled rejection AND leaves the returned promise permanently pending. We
120
120
  * race the resolve against a scoped `unhandledRejection` listener: whichever
@@ -143,6 +143,9 @@ export async function resolveEnvValuesOrThrow(graph) {
143
143
  });
144
144
  process.prependOnceListener("unhandledRejection", onRejection);
145
145
  try {
146
+ // Preserve current varlock's direct rejection unchanged. The race only
147
+ // turns a floated rejection (the 1.0.0 behavior) into a typed error rather
148
+ // than leaving the caller waiting on a promise that never settles.
146
149
  await Promise.race([graph.resolveEnvValues(), rejection]);
147
150
  }
148
151
  finally {
@@ -23,8 +23,9 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
23
23
  errorsByName: new Map(),
24
24
  loadedSecretsByName: new Map(),
25
25
  };
26
- // varlock@1.0.0's plugin-lib.js omits the Resolver export (d.ts/JS mismatch);
27
- // extract it at runtime from any already-registered built-in resolver's prototype.
26
+ // In varlock@1.21.1, Resolver remains declared in plugin-lib's .d.mts but
27
+ // is not exported from its runtime module. Derive the runtime base class from
28
+ // a built-in resolver registered on the graph; the shape test pins this API.
28
29
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
30
  let RuntimeResolver;
30
31
  try {
@@ -39,8 +40,8 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
39
40
  RuntimeResolver = proto;
40
41
  }
41
42
  catch (e) {
42
- throw new Error('varlock Resolver base class extraction failed — the varlock@1.0.0 d.ts/JS mismatch ' +
43
- 'may have been resolved; switch to `import { Resolver } from "varlock/plugin-lib"`. ' +
43
+ throw new Error('varlock Resolver base class extraction failed — varlock@1.21.1 still declares ' +
44
+ 'Resolver in plugin-lib types without exporting it at runtime. ' +
44
45
  `Underlying: ${e instanceof Error ? e.message : String(e)}`);
45
46
  }
46
47
  // HqResolver is declared INSIDE installHqPlugin so its static def.resolve
@@ -96,7 +97,7 @@ export function installHqPlugin(graph /* EnvGraph */, opts) {
96
97
  }
97
98
  }
98
99
  }
99
- // varlock@1.0.0's env-spec parser rejects dots in decorator names ([a-zA-Z0-9_] only),
100
+ // Varlock's env-spec parser rejects dots in decorator names ([a-zA-Z0-9_] only),
100
101
  // so `@hq.company` is not valid syntax. We register as `@hqCompany` (camelCase) instead.
101
102
  // Schema files must use `# @hqCompany("slug")` followed by a blank line so the parser
102
103
  // treats it as a file-level root decorator rather than an item decorator for the next var.
@@ -237,6 +237,12 @@ export declare function withMachineTokenStateDir<T>(fn: () => Promise<T>, opts?:
237
237
  home?: string;
238
238
  env?: NodeJS.ProcessEnv;
239
239
  }): Promise<T>;
240
+ /**
241
+ * Ensure the current machine identity has a valid session and return only the
242
+ * access/ID token pair needed by supported machine-session handoff scripts.
243
+ * The machine state directory is isolated from the person's session cache.
244
+ */
245
+ export declare function ensureMachineSessionTokens(): Promise<Pick<CognitoTokens, "accessToken" | "idToken" | "expiresAt">>;
240
246
  /**
241
247
  * True when a person-token refresh failed with a definitive Cognito auth
242
248
  * rejection (refresh token invalid/revoked). Transient network/5xx/timeouts
@@ -840,6 +840,15 @@ function withTokenStateDirSection(fn) {
840
840
  async function ensureMachineTokens() {
841
841
  return withMachineTokenStateDir(() => getValidMachineTokens(DEFAULT_COGNITO));
842
842
  }
843
+ /**
844
+ * Ensure the current machine identity has a valid session and return only the
845
+ * access/ID token pair needed by supported machine-session handoff scripts.
846
+ * The machine state directory is isolated from the person's session cache.
847
+ */
848
+ export async function ensureMachineSessionTokens() {
849
+ const tokens = await ensureMachineTokensMaybeFallback({ tokenSource: "machine" });
850
+ return { accessToken: tokens.accessToken, idToken: tokens.idToken, expiresAt: tokens.expiresAt };
851
+ }
843
852
  /**
844
853
  * Mint machine tokens (daemon state dir cache). Logs once when the fresh-box
845
854
  * default-creds fallback is what selected the machine path.
@@ -62,6 +62,8 @@ import * as fs from "node:fs";
62
62
  import * as path from "node:path";
63
63
  import semver from "semver";
64
64
  import chalk from "chalk";
65
+ import { Sentry } from "../sentry.js";
66
+ import { isCliTelemetryDisabled } from "./cli-telemetry.js";
65
67
  import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
66
68
  import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, isPrefixWritable, nonWritablePrefixNote, openInstallOutput, pnpmUpdateEnv, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, } from "./version-gate.js";
67
69
  import { canStageInstall, stagedNpmInstall } from "./staged-install.js";
@@ -499,6 +501,7 @@ async function updateAndReexec(argv, flavor, known, deps) {
499
501
  console.error(chalk.dim(` Or repair the install: hq rescue`));
500
502
  }
501
503
  console.error(chalk.dim(` Continuing the ${flavor.noun} on ${current}.`));
504
+ await captureSelfUpdateOutcome("update-failed", current, latestValid, install.manager, result.code, env, flavor.noun === "rescue");
502
505
  return { action: "update-failed", latest };
503
506
  }
504
507
  if (installSnapshot)
@@ -521,6 +524,7 @@ async function updateAndReexec(argv, flavor, known, deps) {
521
524
  if (converged === false) {
522
525
  (deps.markIneffective ?? markLatestIneffective)(latest);
523
526
  console.error(chalk.dim(` Continuing the ${flavor.noun} on ${current}.`));
527
+ await captureSelfUpdateOutcome("update-ineffective", current, latestValid, install.manager, undefined, env, flavor.noun === "rescue");
524
528
  return { action: "update-ineffective", latest };
525
529
  }
526
530
  // A resident mesh daemon keeps the old JS loaded after a global install.
@@ -550,6 +554,33 @@ async function updateAndReexec(argv, flavor, known, deps) {
550
554
  console.error(chalk.dim(`✓ hq-cli updated to ${latest}.`));
551
555
  return { action: "reexec", reexecStatus: status, latest };
552
556
  }
557
+ const SELF_UPDATE_ERROR_CLASSES = new Set(["MODULE_NOT_FOUND", "EACCES", "EEXIST"]);
558
+ async function captureSelfUpdateOutcome(action, fromVersion, targetVersion, installFlavor, code, env, flushBeforeExit) {
559
+ if (isCliTelemetryDisabled(env))
560
+ return;
561
+ const error = new Error(action === "update-failed" ? "Self-update failed" : "Self-update ineffective");
562
+ // This event needs no stack: frames would expose the machine's install path.
563
+ error.stack = undefined;
564
+ Sentry.captureException(error, {
565
+ fingerprint: ["self-update", action],
566
+ tags: {
567
+ from_version: fromVersion,
568
+ target_version: targetVersion,
569
+ install_flavor: installFlavor,
570
+ error_class: code && SELF_UPDATE_ERROR_CLASSES.has(code) ? code : "other",
571
+ },
572
+ });
573
+ if (flushBeforeExit) {
574
+ try {
575
+ if (!(await Sentry.flush(500))) {
576
+ console.error(chalk.dim("hq: Sentry flush timed out after self-update."));
577
+ }
578
+ }
579
+ catch {
580
+ console.error(chalk.dim("hq: Sentry flush failed after self-update."));
581
+ }
582
+ }
583
+ }
553
584
  async function readStagedSelfUpdateFlag(deps, env) {
554
585
  try {
555
586
  if (deps.stagedSelfUpdate)
@@ -706,13 +706,32 @@ export function detachUpdateSupervisor() {
706
706
  try {
707
707
  rmSync(tempDir, { recursive: true, force: true });
708
708
  }
709
- catch {
710
- // Best-effort cleanup of a temp dir.
709
+ catch (error) {
710
+ const failureClass = error &&
711
+ typeof error === "object" &&
712
+ "code" in error &&
713
+ typeof error.code === "string" &&
714
+ /^[A-Z0-9_]{1,32}$/.test(error.code)
715
+ ? error.code
716
+ : error instanceof Error
717
+ ? "Error"
718
+ : "UnknownError";
719
+ console.error(chalk.yellow(`Could not remove update supervisor temp dir (${failureClass}).`));
711
720
  }
712
721
  },
713
722
  };
714
723
  }
715
- catch {
724
+ catch (error) {
725
+ const failureClass = error &&
726
+ typeof error === "object" &&
727
+ "code" in error &&
728
+ typeof error.code === "string" &&
729
+ /^[A-Z0-9_]{1,32}$/.test(error.code)
730
+ ? error.code
731
+ : error instanceof Error
732
+ ? "Error"
733
+ : "UnknownError";
734
+ console.error(chalk.yellow(`Could not copy update supervisor (${failureClass}).`));
716
735
  if (dir)
717
736
  rmSync(dir, { recursive: true, force: true });
718
737
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.294.0",
3
+ "version": "5.296.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -58,7 +58,7 @@
58
58
  "semver": "^7.6.3",
59
59
  "simple-git": "^3.27.0",
60
60
  "smol-toml": "1.6.1",
61
- "varlock": "1.0.0"
61
+ "varlock": "1.21.1"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@eslint/js": "^10.0.1",
@@ -1,8 +0,0 @@
1
- export interface FastLanesListRouteDeps {
2
- flagEnabled: (flagKey: string, env: NodeJS.ProcessEnv) => Promise<boolean>;
3
- runFastList: () => Promise<void>;
4
- runPlacementList: () => Promise<void>;
5
- }
6
- /** Returns false unless this exact list form is safely enabled in the local flag snapshot. */
7
- export declare function runFastLanesListRequest(argv: readonly string[], env?: NodeJS.ProcessEnv, deps?: FastLanesListRouteDeps): Promise<boolean>;
8
- //# sourceMappingURL=fast-lanes-list-route.d.ts.map
@@ -1,32 +0,0 @@
1
- import { isFastCachedLanesListRequest } from "./fast-scaffold.js";
2
- import { fastFlagEnabled } from "./native-fast-route.js";
3
- import { registerOutpostLaneSnapshotRefreshChild } from "./lib/lanes/outpost-snapshot-refresh.js";
4
- import { LANE_OUTPOST_SNAPSHOT_CACHE_FLAG } from "./lib/lanes/outpost-hire-cache.js";
5
- import { LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lib/lanes/flag-keys.js";
6
- /** Returns false unless this exact list form is safely enabled in the local flag snapshot. */
7
- export async function runFastLanesListRequest(argv, env = process.env, deps = {
8
- flagEnabled: fastFlagEnabled,
9
- runFastList: async () => {
10
- const { runFastCachedLanesList } = await import("./fast-lanes-list.js");
11
- await runFastCachedLanesList();
12
- },
13
- runPlacementList: async () => {
14
- const { runFastCachedLanesListFromEntrypoint } = await import("./commands/lanes.js");
15
- await runFastCachedLanesListFromEntrypoint();
16
- },
17
- }) {
18
- registerOutpostLaneSnapshotRefreshChild(env);
19
- if (!isFastCachedLanesListRequest(argv))
20
- return false;
21
- if (env.HQ_INTERNAL_LANES_REFRESH_REMOTE_SNAPSHOT === "1")
22
- return false;
23
- if (!await deps.flagEnabled(LANE_OUTPOST_SNAPSHOT_CACHE_FLAG, env))
24
- return false;
25
- const lanePlacementEnabled = await deps.flagEnabled(LANE_OUTPOST_LOAD_BALANCING_FLAG, env);
26
- if (lanePlacementEnabled)
27
- await deps.runPlacementList();
28
- else
29
- await deps.runFastList();
30
- return true;
31
- }
32
- //# sourceMappingURL=fast-lanes-list-route.js.map
@@ -1,3 +0,0 @@
1
- /** Lightweight implementation for the exact cache-first `lanes list --json` entrypoint. */
2
- export declare function runFastCachedLanesList(): Promise<void>;
3
- //# sourceMappingURL=fast-lanes-list.d.ts.map