@indigoai-us/hq-cli 5.79.0 → 5.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.81.0]
6
+
7
+ ### Added
8
+
9
+ - Hidden `hq core checkpoint`: end-of-turn checkpoint + detached background
10
+ maintenance sibling (codex-first, pinned `gpt-5.6-terra` @ high effort;
11
+ claude fallback pinned `claude-opus-5` @ medium). Sibling distills
12
+ learnings into policies/knowledge under a single-flight lock;
13
+ `--gate-probe` caches Stop-gate eligibility (operator rollout domain,
14
+ `HQ_CHECKPOINT_GATE`/`HQ_CHECKPOINT_GATE_DOMAINS` overrides); session id
15
+ inferred from `CLAUDE_CODE_SESSION_ID` when omitted. (#286)
16
+
5
17
  ## [5.79.0]
6
18
 
7
19
  ### Added
@@ -22,6 +22,11 @@ import { type BannerLevel } from "../lib/narrow-hint-banner.js";
22
22
  * succeed when files were dropped" guarantee unit-testable.
23
23
  */
24
24
  export declare function scopeExcludedWarning(count: number): string | null;
25
+ /**
26
+ * Optional detail line listing the effective write prefixes when a push
27
+ * dropped files — helps debug ACL vs scope-excluded mismatches (feedback_7e9c535e).
28
+ */
29
+ export declare function scopeExcludedPrefixDetail(prefixSet: readonly string[] | undefined): string | null;
25
30
  /**
26
31
  * Bound foreground waits for a watcher/manual sync that currently owns the
27
32
  * per-root operation lock. The cloud engine reads this environment value on
@@ -159,6 +164,8 @@ export interface ShareCallOptions {
159
164
  skipUnchanged?: boolean;
160
165
  propagateDeletes?: boolean;
161
166
  propagateDeletePolicy?: "owned-only" | "currency-gated" | "all";
167
+ /** DEV-1768 push parity: filter upload plan to granted ACL prefixes. */
168
+ prefixSet?: PullScope["prefixSet"];
162
169
  }
163
170
  export interface ShareCallResult {
164
171
  filesUploaded: number;
@@ -172,6 +179,8 @@ export interface ShareCallResult {
172
179
  export interface PushAllDeps {
173
180
  vaultClient: PullAllVaultClient;
174
181
  share: (options: ShareCallOptions) => Promise<ShareCallResult>;
182
+ /** DEV-1768: resolve membership push scope (same as pull). */
183
+ resolveScope?: (companyUid: string, slug: string) => Promise<PullScope>;
175
184
  }
176
185
  export interface PushAllOptions {
177
186
  hqRoot: string;
@@ -32,6 +32,16 @@ export function scopeExcludedWarning(count) {
32
32
  `They were skipped, not synced. Re-run with --json to list them, or ` +
33
33
  `ask an admin to grant you write on those paths.`);
34
34
  }
35
+ /**
36
+ * Optional detail line listing the effective write prefixes when a push
37
+ * dropped files — helps debug ACL vs scope-excluded mismatches (feedback_7e9c535e).
38
+ */
39
+ export function scopeExcludedPrefixDetail(prefixSet) {
40
+ if (!prefixSet || prefixSet.length === 0) {
41
+ return " Granted write prefixes: (none — shared-mode membership with no explicit grants)";
42
+ }
43
+ return ` Granted write prefixes: ${prefixSet.join(", ")}`;
44
+ }
35
45
  /**
36
46
  * Resolve the `propagateDeletePolicy` for share() calls.
37
47
  *
@@ -268,6 +278,7 @@ export async function pushAll(options, deps) {
268
278
  }
269
279
  plan.push({
270
280
  slug,
281
+ companyUid: m.companyUid,
271
282
  shareOptions: {
272
283
  company: m.companyUid,
273
284
  hqRoot: options.hqRoot,
@@ -284,6 +295,7 @@ export async function pushAll(options, deps) {
284
295
  if (personal) {
285
296
  plan.push({
286
297
  slug: "personal",
298
+ companyUid: personal.uid,
287
299
  shareOptions: {
288
300
  company: personal.uid,
289
301
  hqRoot: options.hqRoot,
@@ -309,6 +321,17 @@ export async function pushAll(options, deps) {
309
321
  };
310
322
  for (const entry of plan) {
311
323
  result.attempted += 1;
324
+ if (entry.slug !== "personal" && deps.resolveScope) {
325
+ try {
326
+ const scope = await deps.resolveScope(entry.companyUid, entry.slug);
327
+ if (scope.prefixSet !== undefined) {
328
+ entry.shareOptions.prefixSet = scope.prefixSet;
329
+ }
330
+ }
331
+ catch {
332
+ // Degrade to no explicit scope filter — share() treats undefined as full access.
333
+ }
334
+ }
312
335
  try {
313
336
  const r = await deps.share(entry.shareOptions);
314
337
  result.filesUploaded += r.filesUploaded;
@@ -582,6 +605,8 @@ export function registerCloudCommands(program) {
582
605
  // 2. default: vend via cached Cognito session (the human CLI path).
583
606
  let entityContext;
584
607
  let vaultConfig;
608
+ /** Vault API config for membership/grant scope lookups. Set even on the pre-vended `--creds-from-stdin` path — share() accepts entityContext OR vaultConfig, not both. */
609
+ let scopeVaultConfig;
585
610
  if (options.credsFromStdin) {
586
611
  if (process.stdin.isTTY) {
587
612
  throw new Error("--creds-from-stdin requires JSON on stdin, but stdin is a " +
@@ -595,17 +620,22 @@ export function registerCloudCommands(program) {
595
620
  catch (e) {
596
621
  throw new Error(`--creds-from-stdin: failed to parse stdin as JSON: ${e instanceof Error ? e.message : String(e)}`);
597
622
  }
623
+ // Upload creds are pre-vended in entityContext, but prefix filtering
624
+ // still needs vault-service membership/grant resolution.
625
+ const accessToken = await ensureCognitoToken();
626
+ scopeVaultConfig = buildVaultConfig(accessToken);
598
627
  }
599
628
  else {
600
629
  const accessToken = await ensureCognitoToken();
601
630
  vaultConfig = buildVaultConfig(accessToken);
631
+ scopeVaultConfig = vaultConfig;
602
632
  }
603
633
  // Resolve the target. For `--personal`, look up the caller's
604
634
  // canonical person entity and force personalMode + journalSlug so
605
635
  // share() lands files at hqRoot directly (no companies/<slug>/
606
636
  // prefix). For everything else, the company is whatever the user
607
637
  // passed or the active company from .hq/config.json.
608
- let targetCompany = options.company;
638
+ let targetCompany = options.company ?? entityContext?.uid;
609
639
  let personalMode = false;
610
640
  let journalSlug;
611
641
  if (options.personal) {
@@ -646,6 +676,12 @@ export function registerCloudCommands(program) {
646
676
  // idToken — pre-vended `--creds-from-stdin` paths still get author
647
677
  // attribution as long as the caller is logged in locally.
648
678
  const author = resolveUploadAuthorFromCache();
679
+ let pushPrefixSet;
680
+ if (!personalMode && scopeVaultConfig) {
681
+ const scopeClient = new VaultClient(scopeVaultConfig);
682
+ const scope = await resolveCliPullScope(scopeClient, targetCompany, options.hqRoot);
683
+ pushPrefixSet = scope?.prefixSet;
684
+ }
649
685
  const result = await share({
650
686
  paths: targetPaths,
651
687
  company: targetCompany,
@@ -658,6 +694,7 @@ export function registerCloudCommands(program) {
658
694
  ...(personalMode ? { personalMode: true } : {}),
659
695
  ...(journalSlug !== undefined ? { journalSlug } : {}),
660
696
  ...(author ? { author } : {}),
697
+ ...(pushPrefixSet !== undefined ? { prefixSet: pushPrefixSet } : {}),
661
698
  });
662
699
  if (jsonMode) {
663
700
  // Synthetic terminal event so subprocess consumers can read final
@@ -681,6 +718,9 @@ export function registerCloudCommands(program) {
681
718
  log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
682
719
  `${result.filesSkipped} skipped, ${result.filesExcludedByScope} scope-excluded)`));
683
720
  log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)));
721
+ const prefixDetail = scopeExcludedPrefixDetail(pushPrefixSet);
722
+ if (prefixDetail)
723
+ log(chalk.yellow(prefixDetail));
684
724
  }
685
725
  else {
686
726
  log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
@@ -1091,6 +1131,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
1091
1131
  ...(skipPersonal ? { skipPersonal: true } : {}),
1092
1132
  }, {
1093
1133
  vaultClient: adapter,
1134
+ resolveScope: (companyUid, slug) => resolvePullScope(realClient, companyUid, slug, hqRoot),
1094
1135
  share: (opts) => share({
1095
1136
  paths: opts.paths,
1096
1137
  company: opts.company,
@@ -1113,6 +1154,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
1113
1154
  ...(opts.propagateDeletePolicy !== undefined
1114
1155
  ? { propagateDeletePolicy: opts.propagateDeletePolicy }
1115
1156
  : {}),
1157
+ ...(opts.prefixSet !== undefined ? { prefixSet: opts.prefixSet } : {}),
1116
1158
  ...(author ? { author } : {}),
1117
1159
  }),
1118
1160
  });
@@ -1195,6 +1237,12 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1195
1237
  // Push first so the subsequent pull doesn't redownload files we were
1196
1238
  // about to broadcast (matches hq-sync-runner ordering).
1197
1239
  console.log(chalk.dim(" → push leg"));
1240
+ let nowPushPrefixSet;
1241
+ if (!personalMode) {
1242
+ const scopeClient = new VaultClient(vaultConfig);
1243
+ const pushScope = await resolveCliPullScope(scopeClient, targetCompany, hqRoot);
1244
+ nowPushPrefixSet = pushScope?.prefixSet;
1245
+ }
1198
1246
  const pushResult = await share({
1199
1247
  paths: pushPaths,
1200
1248
  company: targetCompany,
@@ -1208,6 +1256,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1208
1256
  ...(personalMode ? { personalMode: true } : {}),
1209
1257
  ...(journalSlug !== undefined ? { journalSlug } : {}),
1210
1258
  ...(author ? { author } : {}),
1259
+ ...(nowPushPrefixSet !== undefined ? { prefixSet: nowPushPrefixSet } : {}),
1211
1260
  });
1212
1261
  const pushStatus = pushResult.aborted || pushResult.filesExcludedByScope > 0
1213
1262
  ? chalk.yellow("⚠")
@@ -1218,6 +1267,9 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1218
1267
  (pushResult.aborted ? " — aborted" : ""));
1219
1268
  if (pushResult.filesExcludedByScope > 0) {
1220
1269
  console.log(chalk.yellow(scopeExcludedWarning(pushResult.filesExcludedByScope)));
1270
+ const prefixDetail = scopeExcludedPrefixDetail(nowPushPrefixSet);
1271
+ if (prefixDetail)
1272
+ console.log(chalk.yellow(prefixDetail));
1221
1273
  }
1222
1274
  if (pushResult.aborted) {
1223
1275
  console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `hq core checkpoint` — synchronous end-of-turn state capture plus an
3
+ * optional detached HQ-maintenance sibling.
4
+ *
5
+ * The foreground command intentionally performs only local filesystem and git
6
+ * reads/writes. It never waits for the sibling, and it never performs network
7
+ * I/O itself.
8
+ */
9
+ import { Command } from "commander";
10
+ /**
11
+ * Kept in TypeScript rather than in a bundled asset: it is an instruction to
12
+ * a locally-installed agent, not a scaffold script that should be packaged.
13
+ */
14
+ export declare const SIBLING_PROMPT_TEMPLATE = "You are the HQ checkpoint sibling \u2014 a background maintenance agent for this\nHQ install. Your parent session's state is in <payloadPath>. Work\nquietly and do not ask questions; if something is ambiguous, record it in the\nreport instead of guessing.\n\n1. Read the payload. If it lists a transcript path that exists, you may read\n its tail for context (last ~200 lines); never quote secrets from it.\n2. Verify the checkpoint thread file named in the payload exists and is valid\n JSON; repair or enrich it if needed (keep its shape).\n3. For each entry in \"learnings\": if it is a reusable rule, distill it into a\n policy file under personal/policies/ (or companies/<company>/policies/ when\n the payload names a company and the lesson is company-specific), following\n core/knowledge/public/hq-core/policies-spec.md. Skip duplicates \u2014 search\n existing policies first.\n4. For durable facts (not rules), update knowledge under personal/knowledge/\n or companies/<company>/knowledge/.\n5. Hook or automation improvements go ONLY under personal/hooks/ as proposals.\n You must never write into .claude/, core/, .agents/, .codex/, or repos/.\n6. Write <runDir>/report.md \u2014 full prose: what you read, what you changed\n (paths), what you skipped and why.\n7. If workspace/checkpoints/sibling/pending.jsonl is non-empty when you\n finish, process those payloads the same way, then truncate the file.\n";
15
+ export declare function renderSiblingPrompt(runDir: string, payloadPath: string): string;
16
+ /** Attach the native checkpoint command to the hidden `hq core` group. */
17
+ export declare function registerCoreCheckpointCommand(core: Command): void;
18
+ //# sourceMappingURL=core-checkpoint.d.ts.map
@@ -0,0 +1,544 @@
1
+ /**
2
+ * `hq core checkpoint` — synchronous end-of-turn state capture plus an
3
+ * optional detached HQ-maintenance sibling.
4
+ *
5
+ * The foreground command intentionally performs only local filesystem and git
6
+ * reads/writes. It never waits for the sibling, and it never performs network
7
+ * I/O itself.
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { execFileSync, spawn } from "node:child_process";
12
+ import { homedir } from "node:os";
13
+ import { resolveLiveRoot } from "../utils/hq-roots.js";
14
+ import { peekIdToken } from "../utils/id-token.js";
15
+ const DEFAULT_TRIGGER = "stop-gate";
16
+ const BACKENDS = new Set(["auto", "claude", "codex", "none"]);
17
+ // Pinned by operator directive 2026-07-31; change defaults here deliberately.
18
+ const CODEX_SIBLING_MODEL = "gpt-5.6-terra";
19
+ const CODEX_SIBLING_REASONING_EFFORT = "high";
20
+ const CLAUDE_SIBLING_MODEL = "claude-opus-5";
21
+ const CLAUDE_SIBLING_EFFORT = "medium";
22
+ class CheckpointUsageError extends Error {
23
+ }
24
+ function printResult(line) {
25
+ process.stdout.write(`${line}\n`);
26
+ }
27
+ function printError(line) {
28
+ process.stderr.write(`${line}\n`);
29
+ }
30
+ /**
31
+ * Kept in TypeScript rather than in a bundled asset: it is an instruction to
32
+ * a locally-installed agent, not a scaffold script that should be packaged.
33
+ */
34
+ export const SIBLING_PROMPT_TEMPLATE = `You are the HQ checkpoint sibling — a background maintenance agent for this
35
+ HQ install. Your parent session's state is in <payloadPath>. Work
36
+ quietly and do not ask questions; if something is ambiguous, record it in the
37
+ report instead of guessing.
38
+
39
+ 1. Read the payload. If it lists a transcript path that exists, you may read
40
+ its tail for context (last ~200 lines); never quote secrets from it.
41
+ 2. Verify the checkpoint thread file named in the payload exists and is valid
42
+ JSON; repair or enrich it if needed (keep its shape).
43
+ 3. For each entry in "learnings": if it is a reusable rule, distill it into a
44
+ policy file under personal/policies/ (or companies/<company>/policies/ when
45
+ the payload names a company and the lesson is company-specific), following
46
+ core/knowledge/public/hq-core/policies-spec.md. Skip duplicates — search
47
+ existing policies first.
48
+ 4. For durable facts (not rules), update knowledge under personal/knowledge/
49
+ or companies/<company>/knowledge/.
50
+ 5. Hook or automation improvements go ONLY under personal/hooks/ as proposals.
51
+ You must never write into .claude/, core/, .agents/, .codex/, or repos/.
52
+ 6. Write <runDir>/report.md — full prose: what you read, what you changed
53
+ (paths), what you skipped and why.
54
+ 7. If workspace/checkpoints/sibling/pending.jsonl is non-empty when you
55
+ finish, process those payloads the same way, then truncate the file.
56
+ `;
57
+ export function renderSiblingPrompt(runDir, payloadPath) {
58
+ return SIBLING_PROMPT_TEMPLATE
59
+ .replaceAll("<runDir>", () => runDir)
60
+ .replaceAll("<payloadPath>", () => payloadPath);
61
+ }
62
+ function collect(value, previous) {
63
+ return [...previous, value];
64
+ }
65
+ function usage(message) {
66
+ throw new CheckpointUsageError(message);
67
+ }
68
+ function readPayload(payloadPath) {
69
+ if (!payloadPath)
70
+ return {};
71
+ let raw;
72
+ try {
73
+ raw = payloadPath === "-"
74
+ ? fs.readFileSync(0, "utf8")
75
+ : fs.readFileSync(payloadPath, "utf8");
76
+ }
77
+ catch {
78
+ usage(`checkpoint: could not read payload: ${payloadPath}`);
79
+ }
80
+ try {
81
+ const parsed = JSON.parse(raw);
82
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
83
+ usage("checkpoint: payload must be a JSON object");
84
+ }
85
+ return parsed;
86
+ }
87
+ catch (error) {
88
+ if (error instanceof CheckpointUsageError)
89
+ throw error;
90
+ usage("checkpoint: payload is not valid JSON");
91
+ }
92
+ }
93
+ function asOptionalString(value, field) {
94
+ if (value === undefined || value === null)
95
+ return undefined;
96
+ if (typeof value !== "string")
97
+ usage(`checkpoint: payload ${field} must be a string`);
98
+ return value;
99
+ }
100
+ function asStringList(value, field) {
101
+ if (value === undefined || value === null)
102
+ return [];
103
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
104
+ usage(`checkpoint: payload ${field} must be an array of strings`);
105
+ }
106
+ return value;
107
+ }
108
+ function wasPassed(command, option) {
109
+ return command.getOptionValueSource(option) === "cli";
110
+ }
111
+ function inferredSessionId() {
112
+ // Deliberately do not fall back to the newest transcript under
113
+ // ~/.claude/projects/<munged-cwd>: concurrent sessions make its mtime
114
+ // unreliable and can mis-attribute a checkpoint. Env var or nothing.
115
+ return process.env.CLAUDE_CODE_SESSION_ID?.trim() || undefined;
116
+ }
117
+ function resolveSessionId(options, command, payload) {
118
+ if (wasPassed(command, "sessionId"))
119
+ return options.sessionId;
120
+ return asOptionalString(payload.session_id, "session_id") ?? inferredSessionId();
121
+ }
122
+ function mergeInput(options, command, payload) {
123
+ const summary = wasPassed(command, "summary")
124
+ ? options.summary
125
+ : asOptionalString(payload.summary, "summary");
126
+ const files = wasPassed(command, "file")
127
+ ? options.file
128
+ : asStringList(payload.files, "files");
129
+ const learnings = wasPassed(command, "learning")
130
+ ? options.learning
131
+ : asStringList(payload.learnings, "learnings");
132
+ const decisions = wasPassed(command, "decision")
133
+ ? options.decision
134
+ : asStringList(payload.decisions, "decisions");
135
+ const nextSteps = wasPassed(command, "next")
136
+ ? options.next
137
+ : asStringList(payload.next_steps, "next_steps");
138
+ const tags = wasPassed(command, "tag")
139
+ ? options.tag
140
+ : asStringList(payload.tags, "tags");
141
+ const trigger = (wasPassed(command, "trigger")
142
+ ? options.trigger
143
+ : asOptionalString(payload.trigger, "trigger")) ?? DEFAULT_TRIGGER;
144
+ const company = wasPassed(command, "company")
145
+ ? options.company
146
+ : asOptionalString(payload.company, "company");
147
+ const sessionId = resolveSessionId(options, command, payload);
148
+ const transcript = wasPassed(command, "transcript")
149
+ ? options.transcript
150
+ : asOptionalString(payload.transcript, "transcript");
151
+ return {
152
+ summary,
153
+ files,
154
+ learnings,
155
+ decisions,
156
+ nextSteps,
157
+ tags,
158
+ trigger,
159
+ company,
160
+ sessionId,
161
+ transcript,
162
+ };
163
+ }
164
+ function formatTimestamp(date) {
165
+ const part = (value) => value.toString().padStart(2, "0");
166
+ return `${date.getUTCFullYear()}${part(date.getUTCMonth() + 1)}${part(date.getUTCDate())}-${part(date.getUTCHours())}${part(date.getUTCMinutes())}${part(date.getUTCSeconds())}`;
167
+ }
168
+ function summarySlug(summary) {
169
+ const words = summary.toLowerCase().match(/[a-z0-9]+/g)?.slice(0, 4) ?? [];
170
+ if (words.length === 0)
171
+ return "checkpoint-update";
172
+ if (words.length === 1)
173
+ words.push("update");
174
+ const slug = words.join("-").slice(0, 40).replace(/-+$/, "");
175
+ return slug || "checkpoint-update";
176
+ }
177
+ function titleFor(summary) {
178
+ const truncated = summary.length > 60 ? `${summary.slice(0, 57).trimEnd()}...` : summary;
179
+ return `Auto: ${truncated}`;
180
+ }
181
+ function readGitState(repoDir) {
182
+ try {
183
+ const read = (args) => execFileSync("git", ["-C", repoDir, ...args], {
184
+ encoding: "utf8",
185
+ stdio: ["ignore", "pipe", "ignore"],
186
+ }).trim();
187
+ if (read(["rev-parse", "--is-inside-work-tree"]) !== "true")
188
+ return null;
189
+ return {
190
+ branch: read(["rev-parse", "--abbrev-ref", "HEAD"]),
191
+ current_commit: read(["rev-parse", "--short", "HEAD"]),
192
+ dirty: read(["status", "--porcelain"]) !== "",
193
+ };
194
+ }
195
+ catch {
196
+ return null;
197
+ }
198
+ }
199
+ function gitStateFor(cwd, liveRoot) {
200
+ return (readGitState(cwd) ??
201
+ readGitState(liveRoot) ?? {
202
+ branch: "unknown",
203
+ current_commit: "unknown",
204
+ dirty: false,
205
+ });
206
+ }
207
+ function writeStamps(liveRoot, sessionId) {
208
+ const stateDir = path.join(liveRoot, "workspace", "orchestrator", "hook-state");
209
+ fs.mkdirSync(stateDir, { recursive: true });
210
+ const sessionKey = sessionId
211
+ ? sessionId.replace(/[^A-Za-z0-9._-]/g, "_") || "unknown"
212
+ : "unknown";
213
+ const timestamp = Math.floor(Date.now() / 1000).toString();
214
+ const stampPaths = [
215
+ path.join(stateDir, "checkpoint-cli-last"),
216
+ path.join(stateDir, `checkpoint-cli-last-${sessionKey}`),
217
+ ];
218
+ for (const stampPath of stampPaths)
219
+ fs.writeFileSync(stampPath, timestamp);
220
+ return stampPaths;
221
+ }
222
+ function gateEligibility() {
223
+ const forced = process.env.HQ_CHECKPOINT_GATE;
224
+ if (forced === "0")
225
+ return false;
226
+ if (forced === "1")
227
+ return true;
228
+ try {
229
+ const tokenPath = path.join(homedir(), ".hq", "cognito-tokens.json");
230
+ const tokens = JSON.parse(fs.readFileSync(tokenPath, "utf8"));
231
+ if (typeof tokens.idToken !== "string")
232
+ return false;
233
+ const email = peekIdToken(tokens.idToken).email;
234
+ if (typeof email !== "string")
235
+ return false;
236
+ const domain = email.split("@").at(-1)?.toLowerCase();
237
+ if (!domain)
238
+ return false;
239
+ const configured = (process.env.HQ_CHECKPOINT_GATE_DOMAINS ?? "")
240
+ .split(",")
241
+ .map((candidate) => candidate.trim().toLowerCase())
242
+ .filter(Boolean);
243
+ return domain === "getindigo.ai" || configured.includes(domain);
244
+ }
245
+ catch {
246
+ return false;
247
+ }
248
+ }
249
+ function writeGateVerdict(liveRoot) {
250
+ const stateDir = path.join(liveRoot, "workspace", "orchestrator", "hook-state");
251
+ fs.mkdirSync(stateDir, { recursive: true });
252
+ const eligible = gateEligibility();
253
+ fs.writeFileSync(path.join(stateDir, "checkpoint-gate-eligible"), eligible ? "1" : "0");
254
+ printResult(eligible ? "eligible" : "ineligible");
255
+ }
256
+ function backendOnPath(name) {
257
+ const pathValue = process.env.PATH;
258
+ if (!pathValue)
259
+ return false;
260
+ return pathValue.split(path.delimiter).some((directory) => {
261
+ if (!directory)
262
+ return false;
263
+ try {
264
+ fs.accessSync(path.join(directory, name), fs.constants.X_OK);
265
+ return true;
266
+ }
267
+ catch {
268
+ return false;
269
+ }
270
+ });
271
+ }
272
+ function resolveBackend(requested) {
273
+ const value = requested ?? "auto";
274
+ if (!BACKENDS.has(value))
275
+ usage(`checkpoint: unknown backend: ${value}`);
276
+ if (value === "auto") {
277
+ if (backendOnPath("codex"))
278
+ return "codex";
279
+ if (backendOnPath("claude"))
280
+ return "claude";
281
+ return "none";
282
+ }
283
+ return value;
284
+ }
285
+ function siblingPayload(input, threadPath) {
286
+ return {
287
+ summary: input.summary ?? "",
288
+ files: input.files,
289
+ learnings: input.learnings,
290
+ decisions: input.decisions,
291
+ next_steps: input.nextSteps,
292
+ tags: input.tags,
293
+ trigger: input.trigger,
294
+ company: input.company ?? null,
295
+ session_id: input.sessionId ?? null,
296
+ transcript: input.transcript ?? null,
297
+ thread_path: threadPath,
298
+ pending_payloads: [],
299
+ };
300
+ }
301
+ function existingSiblingPid(lockPath) {
302
+ try {
303
+ const pid = Number.parseInt(fs.readFileSync(lockPath, "utf8").trim(), 10);
304
+ if (!Number.isSafeInteger(pid) || pid <= 0)
305
+ return null;
306
+ process.kill(pid, 0);
307
+ return pid;
308
+ }
309
+ catch (error) {
310
+ if (error?.code === "EPERM") {
311
+ try {
312
+ return Number.parseInt(fs.readFileSync(lockPath, "utf8").trim(), 10);
313
+ }
314
+ catch {
315
+ return null;
316
+ }
317
+ }
318
+ return null;
319
+ }
320
+ }
321
+ function drainPending(pendingPath) {
322
+ if (!fs.existsSync(pendingPath))
323
+ return [];
324
+ const entries = [];
325
+ for (const line of fs.readFileSync(pendingPath, "utf8").split("\n")) {
326
+ if (!line.trim())
327
+ continue;
328
+ try {
329
+ entries.push(JSON.parse(line));
330
+ }
331
+ catch {
332
+ // A partial line must not prevent a future sibling from processing the
333
+ // valid queued payloads around it.
334
+ }
335
+ }
336
+ fs.writeFileSync(pendingPath, "");
337
+ return entries;
338
+ }
339
+ function startSibling(liveRoot, input, threadPath, backend) {
340
+ const stateDir = path.join(liveRoot, "workspace", "orchestrator", "hook-state");
341
+ const siblingRoot = path.join(liveRoot, "workspace", "checkpoints", "sibling");
342
+ const pendingPath = path.join(siblingRoot, "pending.jsonl");
343
+ const lockPath = path.join(stateDir, "checkpoint-sibling.lock");
344
+ const payload = siblingPayload(input, threadPath);
345
+ const activePid = existingSiblingPid(lockPath);
346
+ fs.mkdirSync(siblingRoot, { recursive: true });
347
+ if (activePid !== null) {
348
+ fs.appendFileSync(pendingPath, `${JSON.stringify(payload)}\n`);
349
+ printResult("checkpoint: sibling busy — payload queued");
350
+ return;
351
+ }
352
+ const runDir = path.join(siblingRoot, `${formatTimestamp(new Date())}-${summarySlug(input.summary ?? "checkpoint")}`);
353
+ fs.mkdirSync(runDir, { recursive: true });
354
+ payload.pending_payloads = drainPending(pendingPath);
355
+ const payloadPath = path.join(runDir, "payload.json");
356
+ fs.writeFileSync(payloadPath, `${JSON.stringify(payload, null, 2)}\n`);
357
+ const prompt = renderSiblingPrompt(runDir, payloadPath);
358
+ fs.writeFileSync(path.join(runDir, "prompt.md"), prompt);
359
+ const logPath = path.join(runDir, "run.log");
360
+ const logFd = fs.openSync(logPath, "a");
361
+ const args = backend === "claude"
362
+ ? [
363
+ "-p",
364
+ prompt,
365
+ "--model",
366
+ CLAUDE_SIBLING_MODEL,
367
+ "--effort",
368
+ CLAUDE_SIBLING_EFFORT,
369
+ "--permission-mode",
370
+ "acceptEdits",
371
+ ]
372
+ : [
373
+ "exec",
374
+ "--skip-git-repo-check",
375
+ "-s",
376
+ "workspace-write",
377
+ "--dangerously-bypass-hook-trust",
378
+ "-m",
379
+ CODEX_SIBLING_MODEL,
380
+ "-c",
381
+ `model_reasoning_effort=${CODEX_SIBLING_REASONING_EFFORT}`,
382
+ prompt,
383
+ ];
384
+ let child;
385
+ try {
386
+ child = spawn(backend, args, {
387
+ detached: true,
388
+ cwd: liveRoot,
389
+ stdio: ["ignore", logFd, logFd],
390
+ // This prevents the scaffold Stop hook from recursively launching a
391
+ // sibling when the background agent itself finishes a turn.
392
+ env: {
393
+ ...process.env,
394
+ HQ_CHECKPOINT_SIBLING: "1",
395
+ ...(backend === "claude" ? { CLAUDE_EFFORT: CLAUDE_SIBLING_EFFORT } : {}),
396
+ },
397
+ });
398
+ }
399
+ finally {
400
+ fs.closeSync(logFd);
401
+ }
402
+ child.unref();
403
+ if (!child.pid)
404
+ throw new Error("checkpoint: sibling did not return a PID");
405
+ fs.writeFileSync(lockPath, String(child.pid));
406
+ printResult(`checkpoint: sibling started (pid ${child.pid})`);
407
+ }
408
+ function hasGateProbeConflict(options, command) {
409
+ return [
410
+ "summary",
411
+ "file",
412
+ "learning",
413
+ "decision",
414
+ "next",
415
+ "tag",
416
+ "trigger",
417
+ "company",
418
+ "sessionId",
419
+ "transcript",
420
+ "payload",
421
+ "idle",
422
+ "agent",
423
+ "backend",
424
+ "dryRun",
425
+ ].some((option) => wasPassed(command, option));
426
+ }
427
+ function printDryRun(plan) {
428
+ printResult(JSON.stringify(plan, null, 2));
429
+ }
430
+ function runCheckpoint(options, command, group) {
431
+ const hqRoot = options.hqRoot ?? group.opts().hqRoot;
432
+ const liveRoot = resolveLiveRoot({ hqRoot });
433
+ if (options.gateProbe) {
434
+ if (hasGateProbeConflict(options, command)) {
435
+ usage("checkpoint: --gate-probe cannot be combined with checkpoint options");
436
+ }
437
+ writeGateVerdict(liveRoot);
438
+ return;
439
+ }
440
+ if (options.idle) {
441
+ if (options.dryRun) {
442
+ printDryRun({ live_root: liveRoot, idle: true, stamps: [], thread: null, sibling: null });
443
+ return;
444
+ }
445
+ writeStamps(liveRoot, resolveSessionId(options, command, readPayload(options.payload)));
446
+ printResult("checkpoint: idle (nothing to record)");
447
+ return;
448
+ }
449
+ const payload = readPayload(options.payload);
450
+ const input = mergeInput(options, command, payload);
451
+ if (!input.summary?.trim()) {
452
+ usage("checkpoint: --summary is required unless --idle or --gate-probe is used");
453
+ }
454
+ const backend = resolveBackend(options.backend);
455
+ const now = new Date();
456
+ const threadId = `T-${formatTimestamp(now)}-auto-${summarySlug(input.summary)}`;
457
+ const threadPath = path.join(liveRoot, "workspace", "threads", `${threadId}.json`);
458
+ const relativeThreadPath = path.relative(liveRoot, threadPath);
459
+ const stampPaths = [
460
+ path.join(liveRoot, "workspace", "orchestrator", "hook-state", "checkpoint-cli-last"),
461
+ path.join(liveRoot, "workspace", "orchestrator", "hook-state", `checkpoint-cli-last-${input.sessionId ? input.sessionId.replace(/[^A-Za-z0-9._-]/g, "_") || "unknown" : "unknown"}`),
462
+ ];
463
+ if (options.dryRun) {
464
+ printDryRun({
465
+ live_root: liveRoot,
466
+ idle: false,
467
+ thread: relativeThreadPath,
468
+ stamps: stampPaths.map((stampPath) => path.relative(liveRoot, stampPath)),
469
+ sibling: options.agent === false ? null : { backend },
470
+ });
471
+ return;
472
+ }
473
+ const thread = {
474
+ thread_id: threadId,
475
+ version: 1,
476
+ type: "auto-checkpoint",
477
+ created_at: now.toISOString(),
478
+ updated_at: now.toISOString(),
479
+ workspace_root: liveRoot,
480
+ cwd: process.cwd(),
481
+ git: gitStateFor(process.cwd(), liveRoot),
482
+ conversation_summary: input.summary,
483
+ files_touched: input.files,
484
+ next_steps: input.nextSteps,
485
+ learnings: input.learnings,
486
+ decisions: input.decisions,
487
+ session_id: input.sessionId ?? null,
488
+ metadata: {
489
+ title: titleFor(input.summary),
490
+ tags: ["auto-checkpoint", ...input.tags],
491
+ trigger: input.trigger,
492
+ },
493
+ };
494
+ fs.mkdirSync(path.dirname(threadPath), { recursive: true });
495
+ fs.writeFileSync(threadPath, `${JSON.stringify(thread, null, 2)}\n`);
496
+ writeStamps(liveRoot, input.sessionId);
497
+ printResult(`checkpoint: ${relativeThreadPath}`);
498
+ if (options.agent === false)
499
+ return;
500
+ if (backend === "none") {
501
+ printResult("checkpoint: sibling disabled (backend none)");
502
+ return;
503
+ }
504
+ startSibling(liveRoot, input, threadPath, backend);
505
+ }
506
+ function reportUsage(error) {
507
+ printError("Usage: hq core checkpoint --summary <text> [options]");
508
+ printError(error.message);
509
+ process.exit(2);
510
+ }
511
+ /** Attach the native checkpoint command to the hidden `hq core` group. */
512
+ export function registerCoreCheckpointCommand(core) {
513
+ core
514
+ .command("checkpoint")
515
+ .description("Record an end-of-turn checkpoint and start HQ maintenance")
516
+ .option("--summary <text>", "1–2 sentence outcome summary")
517
+ .option("--file <path>", "file touched this turn", collect, [])
518
+ .option("--learning <text>", "reusable lesson", collect, [])
519
+ .option("--decision <text>", "decision made", collect, [])
520
+ .option("--next <text>", "remaining next step", collect, [])
521
+ .option("--tag <tag>", "checkpoint tag", collect, [])
522
+ .option("--trigger <name>", "checkpoint trigger", DEFAULT_TRIGGER)
523
+ .option("--company <slug>", "active company scope")
524
+ .option("--session-id <id>", "caller session id (default: $CLAUDE_CODE_SESSION_ID when set)")
525
+ .option("--transcript <path>", "session transcript path")
526
+ .option("--payload <file|->", "JSON payload file, or - for stdin")
527
+ .option("--idle", "touch stamps without a checkpoint")
528
+ .option("--no-agent", "do not spawn the maintenance sibling")
529
+ .option("--backend <auto|claude|codex|none>", "sibling backend", "auto")
530
+ .option("--gate-probe", "write the local Stop-hook eligibility verdict")
531
+ .option("--hq-root <path>", "HQ installation to operate on")
532
+ .option("--dry-run", "print the planned checkpoint without writing")
533
+ .action((options, command) => {
534
+ try {
535
+ runCheckpoint(options, command, core);
536
+ }
537
+ catch (error) {
538
+ if (error instanceof CheckpointUsageError)
539
+ reportUsage(error);
540
+ throw error;
541
+ }
542
+ });
543
+ }
544
+ //# sourceMappingURL=core-checkpoint.js.map
@@ -29,6 +29,7 @@
29
29
  * `ScaffoldRoot`.
30
30
  */
31
31
  import { Option } from "commander";
32
+ import { registerCoreCheckpointCommand } from "./core-checkpoint.js";
32
33
  import { resolveLiveRoot } from "../utils/hq-roots.js";
33
34
  import { runBundledScript } from "../utils/run-bundled-script.js";
34
35
  /**
@@ -233,6 +234,9 @@ export function registerCoreCommands(program) {
233
234
  // collide with a wrapped script's own flags — everything after the
234
235
  // subcommand name is passed through untouched.
235
236
  .addOption(new Option("--hq-root <path>", "HQ installation to operate on (live-root commands)").hideHelp());
237
+ // This group primarily hosts manifest-driven bundled assets, but it also
238
+ // hosts native TypeScript plumbing when a scaffold contract needs it.
239
+ registerCoreCheckpointCommand(core);
236
240
  const runCommand = (entry, args = [], cmd, target) => {
237
241
  const scope = core.opts();
238
242
  // `cmd.args` is the authoritative operand list: with
@@ -78,7 +78,7 @@ export function buildInstalledView(hqRoot, hqVersion, pack, installedSources, ch
78
78
  const counts = { live: 0, broken: 0, missing: 0, foreign: 0 };
79
79
  const brokenLinks = [];
80
80
  for (const link of links) {
81
- const st = linkStatus(link);
81
+ const st = linkStatus(link, pack.dir);
82
82
  counts[st]++;
83
83
  if (st === 'broken')
84
84
  brokenLinks.push({ key: link.key, item: link.item, dst: link.dst });
@@ -3,24 +3,19 @@
3
3
  */
4
4
  import chalk from 'chalk';
5
5
  import { loadCachedTokens, isExpiring, isMachineIdentity, loadMachineCreds, } from '@indigoai-us/hq-cloud';
6
+ import { peekIdToken as decodeIdToken } from "../utils/id-token.js";
6
7
  function peekIdToken(idToken) {
7
- try {
8
- const payload = idToken.split('.')[1];
9
- if (!payload)
10
- return {};
11
- const pad = payload.length % 4 === 0 ? '' : '='.repeat(4 - (payload.length % 4));
12
- const normalized = payload.replace(/-/g, '+').replace(/_/g, '/') + pad;
13
- const decoded = JSON.parse(Buffer.from(normalized, 'base64').toString('utf-8'));
14
- return {
15
- email: decoded.email,
16
- sub: decoded.sub,
17
- entityType: decoded['custom:entityType'],
18
- entityUid: decoded['custom:entityUid'],
19
- };
20
- }
21
- catch {
22
- return {};
23
- }
8
+ const decoded = decodeIdToken(idToken);
9
+ return {
10
+ email: typeof decoded.email === "string" ? decoded.email : undefined,
11
+ sub: typeof decoded.sub === "string" ? decoded.sub : undefined,
12
+ entityType: typeof decoded["custom:entityType"] === "string"
13
+ ? decoded["custom:entityType"]
14
+ : undefined,
15
+ entityUid: typeof decoded["custom:entityUid"] === "string"
16
+ ? decoded["custom:entityUid"]
17
+ : undefined,
18
+ };
24
19
  }
25
20
  export function registerWhoamiCommand(program) {
26
21
  program
package/dist/index.js CHANGED
@@ -11,8 +11,12 @@ if (isVersionRequest(process.argv)) {
11
11
  process.stdout.write(`${CLI_VERSION}\n`);
12
12
  }
13
13
  else {
14
- const { runCli } = await import("./main.js");
15
- await runCli();
14
+ // Keep the command lifecycle detached from module evaluation, as it was
15
+ // before the fast --version split. Some best-effort teardown work uses
16
+ // unref'd handles; top-level-awaiting runCli() makes Node turn an otherwise
17
+ // successful command into exit 13 when that teardown promise remains pending.
18
+ // Rejections still become unhandled and preserve a genuine non-zero failure.
19
+ void import("./main.js").then(({ runCli }) => runCli());
16
20
  }
17
21
  export const __test__ = { isVersionRequest };
18
22
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Decode the payload of a locally cached JWT without verifying its signature.
3
+ *
4
+ * Callers use this only for data already trusted locally (display labels and
5
+ * local eligibility checks), never to authorize a remote request.
6
+ */
7
+ export declare function peekIdToken(idToken: string): Record<string, unknown>;
8
+ //# sourceMappingURL=id-token.d.ts.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Decode the payload of a locally cached JWT without verifying its signature.
3
+ *
4
+ * Callers use this only for data already trusted locally (display labels and
5
+ * local eligibility checks), never to authorize a remote request.
6
+ */
7
+ export function peekIdToken(idToken) {
8
+ try {
9
+ const payload = idToken.split(".")[1];
10
+ if (!payload)
11
+ return {};
12
+ const pad = payload.length % 4 === 0 ? "" : "=".repeat(4 - (payload.length % 4));
13
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/") + pad;
14
+ const decoded = JSON.parse(Buffer.from(normalized, "base64").toString("utf-8"));
15
+ return decoded && typeof decoded === "object" && !Array.isArray(decoded)
16
+ ? decoded
17
+ : {};
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ }
23
+ //# sourceMappingURL=id-token.js.map
@@ -75,8 +75,13 @@ export type LinkStatus = 'live' | 'broken' | 'missing' | 'foreign';
75
75
  * effect (a config merge) is handled separately (US-004/US-005).
76
76
  */
77
77
  export declare function contributionLinks(hqRoot: string, packDir: string, contributes: Partial<Record<PackContributeKey, string[]>>): WiredLink[];
78
+ /**
79
+ * Canonical absolute path — resolves symlinks and platform aliases such as
80
+ * macOS `/var` ↔ `/private/var` so equivalent paths compare equal.
81
+ */
82
+ export declare function canonicalPath(p: string): string;
78
83
  /** Classify a host path against the link that should own it. */
79
- export declare function linkStatus(link: WiredLink): LinkStatus;
84
+ export declare function linkStatus(link: WiredLink, packDir?: string): LinkStatus;
80
85
  /** A content pack's manifest plus the install-time stamped source. */
81
86
  export interface InstalledPackManifest extends PackManifest {
82
87
  source?: string;
@@ -132,8 +132,56 @@ export function contributionLinks(hqRoot, packDir, contributes) {
132
132
  }
133
133
  return links;
134
134
  }
135
+ /**
136
+ * Canonical absolute path — resolves symlinks and platform aliases such as
137
+ * macOS `/var` ↔ `/private/var` so equivalent paths compare equal.
138
+ */
139
+ export function canonicalPath(p) {
140
+ try {
141
+ return fs.realpathSync.native(p);
142
+ }
143
+ catch {
144
+ return path.resolve(p);
145
+ }
146
+ }
147
+ /** True when `candidate` resolves under `packDir` (pack directory containment). */
148
+ function targetUnderPackDir(packDir, candidate) {
149
+ const root = canonicalPath(packDir);
150
+ const resolved = canonicalPath(candidate);
151
+ const prefix = root.endsWith(path.sep) ? root : root + path.sep;
152
+ return resolved === root || resolved.startsWith(prefix);
153
+ }
154
+ /** True when `a` and `b` resolve to the same on-disk file (canonical path or inode). */
155
+ function samePayloadFile(a, b) {
156
+ const canonA = canonicalPath(a);
157
+ const canonB = canonicalPath(b);
158
+ if (canonA === canonB)
159
+ return true;
160
+ try {
161
+ const stA = fs.statSync(a);
162
+ const stB = fs.statSync(b);
163
+ return stA.dev === stB.dev && stA.ino === stB.ino;
164
+ }
165
+ catch {
166
+ return false;
167
+ }
168
+ }
169
+ /** True when the symlink target resolves to the declared payload (dir or file inside it). */
170
+ function pointsAtDeclaredPayload(link, resolvedTarget) {
171
+ if (samePayloadFile(link.src, resolvedTarget))
172
+ return true;
173
+ try {
174
+ const root = canonicalPath(link.src);
175
+ const resolved = canonicalPath(resolvedTarget);
176
+ const prefix = root.endsWith(path.sep) ? root : root + path.sep;
177
+ return resolved === root || resolved.startsWith(prefix);
178
+ }
179
+ catch {
180
+ return false;
181
+ }
182
+ }
135
183
  /** Classify a host path against the link that should own it. */
136
- export function linkStatus(link) {
184
+ export function linkStatus(link, packDir) {
137
185
  let st;
138
186
  try {
139
187
  st = fs.lstatSync(link.dst);
@@ -151,13 +199,21 @@ export function linkStatus(link) {
151
199
  catch {
152
200
  return 'foreign';
153
201
  }
154
- // scan-packages.sh writes the symlink target as the absolute `src` path, so a
155
- // direct compare is correct. Resolve both to be robust to trailing slashes.
202
+ // scan-packages.sh writes the symlink target as the absolute `src` path.
203
+ // Canonicalize both sides so `/var` vs `/private/var` (macOS) and symlink
204
+ // aliases classify as ours; optionally accept stale targets that still resolve
205
+ // inside this pack's directory.
156
206
  const resolvedTarget = path.resolve(path.dirname(link.dst), target);
157
- if (path.resolve(link.src) !== resolvedTarget) {
158
- return 'foreign'; // points at another pack / somewhere else
207
+ if (pointsAtDeclaredPayload(link, resolvedTarget)) {
208
+ return fs.existsSync(link.src) ? 'live' : 'broken';
209
+ }
210
+ if (packDir !== undefined && targetUnderPackDir(packDir, resolvedTarget)) {
211
+ // Stale or corrupted: symlink resolves inside this pack but not to the
212
+ // declared payload. Treat as broken (never live) so list does not report a
213
+ // healthy link; unwirePack still removes it on uninstall.
214
+ return 'broken';
159
215
  }
160
- return fs.existsSync(link.src) ? 'live' : 'broken';
216
+ return 'foreign'; // points at another pack / somewhere else
161
217
  }
162
218
  /** Absolute path to `<hqRoot>/core/packages`. */
163
219
  export function packagesDir(hqRoot) {
@@ -230,7 +286,7 @@ export function findDependentPacks(installed, name) {
230
286
  export function unwirePack(hqRoot, packDir, contributes) {
231
287
  const result = { unlinked: [], skipped: [] };
232
288
  for (const link of contributionLinks(hqRoot, packDir, contributes)) {
233
- const status = linkStatus(link);
289
+ const status = linkStatus(link, packDir);
234
290
  if (status === 'live' || status === 'broken') {
235
291
  try {
236
292
  fs.unlinkSync(link.dst);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.79.0",
3
+ "version": "5.81.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -29,7 +29,7 @@
29
29
  "dependencies": {
30
30
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
31
31
  "@aws-sdk/client-s3": "^3.1049.0",
32
- "@indigoai-us/hq-cloud": "^6.14.27",
32
+ "@indigoai-us/hq-cloud": "^6.14.37",
33
33
  "@indigoai-us/hq-onboarding": "^0.1.0",
34
34
  "@sentry/node": "^10.49.0",
35
35
  "better-sqlite3": "^12.11.1",