@indigoai-us/hq-cli 5.345.19 → 5.345.21

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 (30) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +1 -1
  3. package/assets/scaffold/core/scripts/lib/transcript-tail.sh +33 -23
  4. package/assets/scaffold/core/scripts/ontology-garden.mjs +86 -3
  5. package/assets/scaffold/core/scripts/ontology-source-worklist.sh +141 -4
  6. package/assets/scaffold/core/scripts/register-project.sh +82 -14
  7. package/assets/scaffold/core/scripts/resume-thread-lock.sh +5 -5
  8. package/assets/scaffold/core/scripts/share-suggestion-state.sh +10 -2
  9. package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +36 -4
  10. package/assets/scaffold/core/scripts/worktree-gc.sh +1 -1
  11. package/dist/commands/core-ontology-native.js +110 -4
  12. package/dist/commands/core-register-project-native.js +36 -3
  13. package/dist/commands/doctor.d.ts +3 -0
  14. package/dist/commands/doctor.js +78 -4
  15. package/dist/commands/integrations-connect.js +7 -3
  16. package/dist/commands/lanes.js +40 -26
  17. package/dist/lib/bot/continuity-download.js +4 -1
  18. package/dist/lib/core-utils/sentry-report.js +69 -13
  19. package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +6 -0
  20. package/dist/lib/doctor/client-health-reason-codes-flag.js +28 -7
  21. package/dist/lib/doctor/json-output.d.ts +4 -0
  22. package/dist/lib/doctor/json-output.js +5 -2
  23. package/dist/lib/lanes/autopilot.js +5 -1
  24. package/dist/lib/lanes/grants.d.ts +3 -0
  25. package/dist/lib/lanes/grants.js +10 -3
  26. package/dist/lib/lanes/index.d.ts +1 -1
  27. package/dist/lib/lanes/index.js +1 -1
  28. package/dist/utils/safe-caught-error.d.ts +3 -0
  29. package/dist/utils/safe-caught-error.js +16 -0
  30. package/package.json +1 -1
@@ -144,9 +144,41 @@ if [ "${HQ_WORK_MESH_RECONCILE_STUB:-}" = "1" ]; then
144
144
  fi
145
145
 
146
146
  HQ_BIN="$(command -v hq 2>/dev/null || true)"
147
- if [ -n "$HQ_BIN" ] && [ -f "$OBS_FILE" ]; then
148
- nohup "$HQ_BIN" mesh context reconcile --observation-file "$OBS_FILE" --machine \
149
- >/dev/null 2>&1 </dev/null &
150
- disown 2>/dev/null || true
147
+ if [ -z "$HQ_BIN" ]; then
148
+ printf '%s\n' 'Work Mesh reconcile skipped: hq CLI not found on PATH.' >&2
149
+ else
150
+ flag_reader="$ROOT/.claude/hooks/work-mesh-daemon-not-loaded-flag.cjs"
151
+ daemon_warning_enabled=false
152
+ if [[ -n "${HQ_FLAGS_API_URL:-}" \
153
+ && "${HQ_COMPANY_UID:-}" =~ ^cmp_[A-Za-z0-9]{3,128}$ ]] \
154
+ && command -v node >/dev/null 2>&1 && [[ -f "$flag_reader" ]]; then
155
+ daemon_warning_enabled="$(HQ_CLI_BIN="$HQ_BIN" node "$flag_reader")" || daemon_warning_enabled=false
156
+ fi
157
+ if [ "$daemon_warning_enabled" = "true" ]; then
158
+ # Status only reads the installed service and its live state. Keep this hook
159
+ # bounded, and fail quiet when the CLI or service manager cannot answer.
160
+ daemon_status_file="$(mktemp "${TMPDIR:-/tmp}/hq-work-mesh-daemon-status.XXXXXX" 2>/dev/null)" || daemon_status_file=""
161
+ if [ -n "$daemon_status_file" ]; then
162
+ HQ_NO_UPDATE_CHECK=1 "$HQ_BIN" mesh daemon status --json >"$daemon_status_file" 2>/dev/null &
163
+ daemon_status_pid=$!
164
+ ( sleep 2; kill "$daemon_status_pid" 2>/dev/null || true ) &
165
+ daemon_status_watchdog=$!
166
+ daemon_status_rc=0
167
+ wait "$daemon_status_pid" || daemon_status_rc=$?
168
+ kill "$daemon_status_watchdog" 2>/dev/null || true
169
+ wait "$daemon_status_watchdog" 2>/dev/null || true
170
+ if [ "$daemon_status_rc" -eq 0 ] && command -v jq >/dev/null 2>&1 && \
171
+ jq -e '(.ok == true) and (.running == false) and (.message | type == "string" and contains("installed ("))' \
172
+ "$daemon_status_file" >/dev/null 2>&1; then
173
+ printf '%s\n' 'Work Mesh daemon is installed but not loaded; start it with: hq mesh daemon install' >&2
174
+ fi
175
+ rm -f "$daemon_status_file" 2>/dev/null || true
176
+ fi
177
+ fi
178
+ if [ -f "$OBS_FILE" ]; then
179
+ nohup "$HQ_BIN" mesh context reconcile --observation-file "$OBS_FILE" --machine \
180
+ >/dev/null 2>&1 </dev/null &
181
+ disown 2>/dev/null || true
182
+ fi
151
183
  fi
152
184
  exit 0
@@ -249,7 +249,7 @@ for wt in "${CANDIDATES[@]:-}"; do
249
249
  created_epoch="$wt_mtime"
250
250
  stamp_file="$GC_META_DIR/$(path_key "$wt").json"
251
251
  if [[ -f "$stamp_file" ]]; then
252
- stamped=$(jq -r '.createdAtEpoch // empty' "$stamp_file" 2>/dev/null || echo "")
252
+ stamped=$(jq -r '.createdAtEpoch // empty' "$stamp_file" 2>/dev/null | tr -d '\r' || echo "")
253
253
  [[ -n "$stamped" ]] && created_epoch="$stamped"
254
254
  fi
255
255
  age_days=$(( (NOW - created_epoch) / 86400 ))
@@ -799,15 +799,119 @@ export function worklistCommand(args, root) {
799
799
  if (validation.code !== 0)
800
800
  return err(2, `${validation.stderr}${validation.stdout}${validation.stderr}`);
801
801
  const ledger = path.join(directory, ".processed");
802
+ const skipLedger = path.join(directory, ".skipped");
803
+ const parserVersion = 2;
802
804
  if (doneItem) {
803
- fs.appendFileSync(ledger, `${path.basename(doneItem)}\n`);
805
+ fs.appendFileSync(ledger, `${path.basename(doneItem)}\t${parserVersion}\n`);
804
806
  return ok();
805
807
  }
808
+ const hasFrontmatterField = (file, field) => {
809
+ if (!fs.existsSync(file))
810
+ return false;
811
+ let frontmatter = 0;
812
+ for (const rawLine of fs.readFileSync(file, "utf8").split("\n")) {
813
+ const line = rawLine.replace(/\r$/, "");
814
+ if (line === "---") {
815
+ frontmatter += 1;
816
+ if (frontmatter === 2)
817
+ return false;
818
+ continue;
819
+ }
820
+ if (frontmatter === 1 && line.startsWith(`${field}:`))
821
+ return true;
822
+ }
823
+ return false;
824
+ };
825
+ const rewriteIfChanged = (file, original, output) => {
826
+ if (output === original)
827
+ return;
828
+ const temporary = `${file}.native-${process.pid}`;
829
+ fs.writeFileSync(temporary, output);
830
+ fs.renameSync(temporary, file);
831
+ };
832
+ // Match the shell's parser-version migration. Unstamped legacy rows are
833
+ // stamped unless they carry the old resolved_participants bug signature;
834
+ // older parser versions are dropped so those sources are re-evaluated.
835
+ let processed = "";
836
+ if (fs.existsSync(ledger)) {
837
+ const original = fs.readFileSync(ledger, "utf8");
838
+ const kept = [];
839
+ let changed = false;
840
+ const rows = original.split("\n");
841
+ if (rows.at(-1) === "")
842
+ rows.pop();
843
+ for (const rawLine of rows) {
844
+ const fields = rawLine.split("\t");
845
+ const basename = (fields[0] ?? "").replace(/\r$/, "");
846
+ const version = (fields[1] ?? "").replace(/\r$/, "");
847
+ if (!basename)
848
+ continue;
849
+ if (version) {
850
+ if (/^[+-]?\d+$/.test(version) && Number(version) < parserVersion) {
851
+ changed = true;
852
+ continue;
853
+ }
854
+ kept.push(`${basename}\t${version}`);
855
+ continue;
856
+ }
857
+ changed = true;
858
+ if (hasFrontmatterField(path.join(directory, basename), "resolved_participants"))
859
+ continue;
860
+ kept.push(`${basename}\t${parserVersion}`);
861
+ }
862
+ processed = kept.join("\n");
863
+ if (kept.length > 0)
864
+ processed += "\n";
865
+ if (changed)
866
+ rewriteIfChanged(ledger, original, processed);
867
+ }
868
+ let skipped = "";
869
+ if (fs.existsSync(skipLedger)) {
870
+ const original = fs.readFileSync(skipLedger, "utf8");
871
+ const kept = [];
872
+ let changed = false;
873
+ const rows = original.split("\n");
874
+ if (rows.at(-1) === "")
875
+ rows.pop();
876
+ for (const rawLine of rows) {
877
+ const fields = rawLine.split("\t");
878
+ const basename = fields[0] ?? "";
879
+ const reason = fields[1] ?? "";
880
+ const version = fields[2] ?? "";
881
+ if (!basename)
882
+ continue;
883
+ if (!version || (/^[+-]?\d+$/.test(version) && Number(version) < parserVersion)) {
884
+ changed = true;
885
+ continue;
886
+ }
887
+ kept.push(`${basename}\t${reason}\t${version}`);
888
+ }
889
+ skipped = kept.join("\n");
890
+ if (kept.length > 0)
891
+ skipped += "\n";
892
+ if (changed)
893
+ rewriteIfChanged(skipLedger, original, skipped);
894
+ }
895
+ const isProcessed = (basename) => processed.split("\n").some((line) => {
896
+ if (!line)
897
+ return false;
898
+ const [name, version] = line.split("\t");
899
+ if (name !== basename)
900
+ return false;
901
+ if (version === undefined || version === "")
902
+ return true;
903
+ return Number(version) >= parserVersion;
904
+ });
905
+ const isSkippedAtCurrentVersion = (basename) => skipped.split("\n").some((line) => {
906
+ if (!line)
907
+ return false;
908
+ const fields = line.split("\t");
909
+ return fields[0] === basename && fields[2] === String(parserVersion);
910
+ });
806
911
  const run = awkField(sourceFile, "run", false);
807
912
  const rule = awkField(sourceFile, "audience_rule", false);
808
913
  if (run === "cloud" && !force)
809
914
  return err(4, "assigned to cloud agent (run: cloud); pass --force to run locally\n");
810
- const processed = fs.existsSync(ledger) ? fs.readFileSync(ledger, "utf8").split("\n") : [];
811
915
  const entries = fs.existsSync(directory)
812
916
  ? fs.readdirSync(directory).filter((name) => name.endsWith(".md") && !name.startsWith("."))
813
917
  .filter((name) => { try {
@@ -821,12 +925,12 @@ export function worklistCommand(args, root) {
821
925
  const output = [];
822
926
  let count = 0;
823
927
  for (const name of entries) {
824
- if (processed.includes(name))
928
+ if (isProcessed(name) || isSkippedAtCurrentVersion(name))
825
929
  continue;
826
930
  if (/^[+-]?\d+$/.test(limit) && count >= Number(limit))
827
931
  break;
828
932
  if (!/^[+-]?\d+$/.test(limit)) {
829
- process.stderr.write(`${resolveBundledAsset(`${ASSET}ontology-source-worklist.sh`)}: line 61: [: ${limit}: integer expression expected\n`);
933
+ process.stderr.write(`${resolveBundledAsset(`${ASSET}ontology-source-worklist.sh`)}: line 186: [: ${limit}: integer expression expected\n`);
830
934
  }
831
935
  count += 1;
832
936
  const file = path.join(directory, name);
@@ -860,6 +964,8 @@ export function worklistCommand(args, root) {
860
964
  }
861
965
  }
862
966
  if (!audience) {
967
+ fs.appendFileSync(skipLedger, `${name}\tno-audience\t${parserVersion}\n`);
968
+ skipped += `${name}\tno-audience\t${parserVersion}\n`;
863
969
  output.push(`${JSON.stringify({ file: relative, skip: "no-audience" })}\n`);
864
970
  continue;
865
971
  }
@@ -51,11 +51,44 @@ function companyUid(company, deps) {
51
51
  return direct;
52
52
  const file = path.join(deps.root, "companies", company, ".company-uid");
53
53
  try {
54
- return deps.fs.readFileSync(file, "utf8").replace(/[\s\u0085\u00a0]+/g, "");
54
+ const fromFile = deps.fs.readFileSync(file, "utf8").replace(/[\s\u0085\u00a0]+/g, "");
55
+ if (fromFile)
56
+ return fromFile;
57
+ }
58
+ catch { /* shell falls through to companies/manifest.yaml */ }
59
+ const manifest = path.join(deps.root, "companies", "manifest.yaml");
60
+ let contents;
61
+ try {
62
+ contents = deps.fs.readFileSync(manifest, "utf8");
55
63
  }
56
64
  catch {
57
65
  return null;
58
66
  }
67
+ let inCompanies = false;
68
+ let inCompany = false;
69
+ for (const line of contents.split(/\r?\n/u)) {
70
+ if (/^companies:[\t ]*$/u.test(line)) {
71
+ inCompanies = true;
72
+ continue;
73
+ }
74
+ if (inCompanies && /^[^\t ]/u.test(line)) {
75
+ inCompanies = false;
76
+ inCompany = false;
77
+ }
78
+ if (inCompanies && /^ {2}[^ #][^:]*:/u.test(line)) {
79
+ const slug = line.trim().split(/[\t ]+/u, 1)[0]?.replace(/:$/u, "");
80
+ inCompany = slug === company;
81
+ continue;
82
+ }
83
+ if (inCompany && /^ {2}[^\t ]/u.test(line))
84
+ break;
85
+ if (inCompany) {
86
+ const fields = line.trim().split(/[\t ]+/u);
87
+ if (fields[0] === "cloud_uid:" && fields[1])
88
+ return fields[1].replace(/["']/gu, "").replace(/[\t ]+$/u, "");
89
+ }
90
+ }
91
+ return null;
59
92
  }
60
93
  function boardPath(company, deps) { return path.join(deps.root, "companies", company, "board.json"); }
61
94
  function runHelpProbe(deps) {
@@ -217,7 +250,7 @@ function audit(company, deps) {
217
250
  return false;
218
251
  const uid = companyUid(company, deps);
219
252
  if (uid === null) {
220
- stderr += `no company uid for ${company} (set HQ_COMPANY_UID or companies/${company}/.company-uid)\n`;
253
+ stderr += `no company uid for ${company} (set HQ_COMPANY_UID, companies/${company}/.company-uid, or manifest cloud_uid)\n`;
221
254
  return true;
222
255
  }
223
256
  return !deps.fs.existsSync(path.join(deps.env.HOME || os.homedir(), ".hq/work-mesh/cache/projects", uid, `${project}.json`));
@@ -243,7 +276,7 @@ function storyNotDone(prd, deps, diagnostics) {
243
276
  diagnostics.push(jq.stderr);
244
277
  else if (jq.error?.code === "ENOENT") {
245
278
  const script = resolveBundledAsset(REGISTER_PROJECT_ASSET);
246
- diagnostics.push(`${script}: line 34: jq: command not found\n`);
279
+ diagnostics.push(`${script}: line 65: jq: command not found\n`);
247
280
  }
248
281
  return false;
249
282
  }
@@ -57,6 +57,8 @@ export interface RunDoctorOptions {
57
57
  json?: boolean;
58
58
  /** Resolve hq-flags independently for each company represented in sync results. */
59
59
  resolveClientHealthReasonCodesForCompany?: (companySlug: string | null) => Promise<boolean>;
60
+ /** Resolve the separate status-report flag per company in sync results. */
61
+ resolveClientHealthCheckStatusForCompany?: (companySlug: string | null) => Promise<boolean>;
60
62
  /** In text mode, also print every PASS result. Default: false. */
61
63
  verbose?: boolean;
62
64
  /** In text mode, emit ANSI colour. Default: false (no escape sequences). */
@@ -117,6 +119,7 @@ export interface RunDoctorResult {
117
119
  * read; all other default tiers remain local unless explicitly opted in.
118
120
  */
119
121
  export declare function runDoctor(options?: RunDoctorOptions): Promise<RunDoctorResult>;
122
+ export declare function createClientHealthCheckStatusCompanyResolver(hqRoot: string): (companySlug: string | null) => Promise<boolean>;
120
123
  /** Register the top-level `hq doctor` command so it appears in `hq --help`. */
121
124
  export declare function registerDoctorCommand(program: Command): void;
122
125
  //# sourceMappingURL=doctor.d.ts.map
@@ -32,7 +32,7 @@ import { DEEP_FAMILY_ID, DEEP_FAMILY_TITLE, runDeepGuardTests, } from "../lib/do
32
32
  import { PARITY_FAMILY_ID, PARITY_FAMILY_TITLE, runParityReplay, } from "../lib/doctor/deep/parity.js";
33
33
  import { applyFixes } from "../lib/doctor/fix/apply.js";
34
34
  import { renderDoctorFixJson } from "../lib/doctor/fix/json-output.js";
35
- import { clientHealthReasonCodesEnabled as isActiveCompanyClientHealthReasonCodesEnabled, clientHealthReasonCodesEnabledForCompany, } from "../lib/doctor/client-health-reason-codes-flag.js";
35
+ import { clientHealthCheckStatusEnabled as isActiveCompanyClientHealthCheckStatusEnabled, clientHealthCheckStatusEnabledForCompany, clientHealthReasonCodesEnabled as isActiveCompanyClientHealthReasonCodesEnabled, clientHealthReasonCodesEnabledForCompany, } from "../lib/doctor/client-health-reason-codes-flag.js";
36
36
  import { isCompanyJournalSlug } from "../lib/doctor/checks/sync-health.js";
37
37
  import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
38
38
  import * as readline from "node:readline";
@@ -166,15 +166,21 @@ export async function runDoctor(options = {}) {
166
166
  });
167
167
  }
168
168
  if (options.json) {
169
- const clientHealthReasonCodesEnabledCheckIds = options.resolveClientHealthReasonCodesForCompany
170
- ? await resolveClientHealthReasonCodesCheckIds(families, options.resolveClientHealthReasonCodesForCompany)
171
- : undefined;
169
+ const [clientHealthReasonCodesEnabledCheckIds, clientHealthCheckStatusEnabledCheckIds] = await Promise.all([
170
+ options.resolveClientHealthReasonCodesForCompany
171
+ ? resolveClientHealthReasonCodesCheckIds(families, options.resolveClientHealthReasonCodesForCompany)
172
+ : undefined,
173
+ options.resolveClientHealthCheckStatusForCompany
174
+ ? resolveClientHealthCheckStatusCheckIds(families, options.resolveClientHealthCheckStatusForCompany)
175
+ : undefined,
176
+ ]);
172
177
  write(renderJson(buildDoctorJson({
173
178
  hqRoot: hqRoot ?? "",
174
179
  families,
175
180
  platform,
176
181
  scope,
177
182
  clientHealthReasonCodesEnabledCheckIds,
183
+ clientHealthCheckStatusEnabledCheckIds,
178
184
  })));
179
185
  }
180
186
  else {
@@ -220,6 +226,34 @@ async function resolveClientHealthReasonCodesCheckIds(families, resolveForCompan
220
226
  }
221
227
  return enabledChecks;
222
228
  }
229
+ async function resolveClientHealthCheckStatusCheckIds(families, resolveForCompany) {
230
+ const checksByCompany = new Map();
231
+ for (const run of families) {
232
+ if (run.family.id !== "sync")
233
+ continue;
234
+ for (const result of run.results) {
235
+ if (result.status !== "FAIL" && result.status !== "WARN")
236
+ continue;
237
+ const scoped = /^sync\.(?:journal|manifest)\.(.+)$/.exec(result.checkId);
238
+ const companySlug = scoped && isCompanyJournalSlug(scoped[1]) ? scoped[1] : null;
239
+ const checkIds = checksByCompany.get(companySlug) ?? [];
240
+ checkIds.push(result.checkId);
241
+ checksByCompany.set(companySlug, checkIds);
242
+ }
243
+ }
244
+ const enabledChecks = new Set();
245
+ const decisions = await Promise.all([...checksByCompany.entries()].map(async ([companySlug, checkIds]) => ({
246
+ checkIds,
247
+ enabled: await resolveForCompany(companySlug),
248
+ })));
249
+ for (const decision of decisions) {
250
+ if (decision.enabled) {
251
+ for (const checkId of decision.checkIds)
252
+ enabledChecks.add(checkId);
253
+ }
254
+ }
255
+ return enabledChecks;
256
+ }
223
257
  function createClientHealthReasonCodeCompanyResolver(hqRoot) {
224
258
  const companies = readCompaniesManifestMap(hqRoot);
225
259
  return async (companySlug) => {
@@ -245,6 +279,37 @@ function createClientHealthReasonCodeCompanyResolver(hqRoot) {
245
279
  });
246
280
  };
247
281
  }
282
+ export function createClientHealthCheckStatusCompanyResolver(hqRoot) {
283
+ const companies = readCompaniesManifestMap(hqRoot);
284
+ return async (companySlug) => {
285
+ if (companySlug === null) {
286
+ return isActiveCompanyClientHealthCheckStatusEnabled();
287
+ }
288
+ const scopedCompanyUid = /^company:(cmp_[A-Za-z0-9]{3,128})$/.exec(companySlug);
289
+ if (scopedCompanyUid) {
290
+ return clientHealthCheckStatusEnabledForCompany({
291
+ companyUid: scopedCompanyUid[1],
292
+ });
293
+ }
294
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(companySlug)) {
295
+ return false;
296
+ }
297
+ const manifestUid = companies?.[companySlug]?.cloud_uid;
298
+ const activeSlug = process.env.HQ_COMPANY_SLUG?.trim() || "";
299
+ const activeUid = process.env.HQ_COMPANY_UID?.trim() || "";
300
+ const companyUid = typeof manifestUid === "string" && manifestUid.trim()
301
+ ? manifestUid.trim()
302
+ : companySlug === activeSlug
303
+ ? activeUid
304
+ : "";
305
+ if (!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid))
306
+ return false;
307
+ return clientHealthCheckStatusEnabledForCompany({
308
+ companyUid,
309
+ companySlug,
310
+ });
311
+ };
312
+ }
248
313
  /** Register the top-level `hq doctor` command so it appears in `hq --help`. */
249
314
  export function registerDoctorCommand(program) {
250
315
  program
@@ -334,6 +399,14 @@ export function registerDoctorCommand(program) {
334
399
  : undefined;
335
400
  })()
336
401
  : undefined;
402
+ const clientHealthCheckStatusResolver = json && opts.clientHealthReport === true
403
+ ? (() => {
404
+ const hqRoot = resolveHqRoot();
405
+ return hqRoot
406
+ ? createClientHealthCheckStatusCompanyResolver(hqRoot)
407
+ : undefined;
408
+ })()
409
+ : undefined;
337
410
  const result = await runDoctor({
338
411
  json,
339
412
  // `src/main.ts` also declares --verbose on the root program. When
@@ -350,6 +423,7 @@ export function registerDoctorCommand(program) {
350
423
  company: opts.company,
351
424
  only: parseOnly(opts.only),
352
425
  resolveClientHealthReasonCodesForCompany: clientHealthReasonCodesResolver,
426
+ resolveClientHealthCheckStatusForCompany: clientHealthCheckStatusResolver,
353
427
  });
354
428
  // Set the exit code rather than calling process.exit, so the CLI's
355
429
  // normal shutdown (telemetry flush) still runs. Non-zero means either an
@@ -19,6 +19,7 @@
19
19
  import chalk from "chalk";
20
20
  import open from "open";
21
21
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
22
+ import { safeCaughtError } from "../utils/safe-caught-error.js";
22
23
  import { runResidentCommandWithPackageLease, } from "../utils/resident-package-preload.js";
23
24
  import { consoleOrigin } from "../utils/console-origin.js";
24
25
  export { consoleOrigin };
@@ -29,6 +30,9 @@ import { completeOAuth, discoverDocs, installIntegration, listCatalog, pullBluep
29
30
  import { OAUTH_LOOPBACK_TIMEOUT_CODE, startLoopbackListener, } from "./integrations-oauth.js";
30
31
  /** hq-pro's machine code for "this endpoint needs a browser sign-in". */
31
32
  const OAUTH_REQUIRED_CODE = "INTEGRATION_FACTORY_OAUTH_REQUIRED";
33
+ function warnBrowserOpenFailure(error) {
34
+ console.warn("[hq-cli:integrations-connect] browser open failed", safeCaughtError(error));
35
+ }
32
36
  /**
33
37
  * A server-authoritative "this endpoint is OAuth-protected, run the browser
34
38
  * sign-in instead" signal, in EITHER of the two shapes hq-pro emits it:
@@ -679,7 +683,7 @@ async function consoleHandoff(target, opts) {
679
683
  console.error(chalk.yellow(`Note: --scopes cannot be carried into a console sign-in, so this connect will use ${target.label}'s default permissions.`));
680
684
  }
681
685
  if (url && opts.browser !== false)
682
- await open(url).catch(() => { });
686
+ await open(url).catch((error) => warnBrowserOpenFailure(error));
683
687
  console.error(chalk.dim("When the browser says it connected, run `hq integrations list` to confirm."));
684
688
  return null;
685
689
  }
@@ -712,7 +716,7 @@ export async function bringYourOwnClientHandoff(target, opts, reason, redirectUr
712
716
  console.error("The console shows the callback URL to register and takes the credentials.");
713
717
  printConsoleDestination(target, opts, url);
714
718
  if (url && opts.browser !== false)
715
- await open(url).catch(() => { });
719
+ await open(url).catch((error) => warnBrowserOpenFailure(error));
716
720
  return null;
717
721
  }
718
722
  /**
@@ -837,7 +841,7 @@ async function userClientSignIn(token, companyUid, target, opts, client, request
837
841
  if (googleHint)
838
842
  console.error(chalk.yellow(googleHint));
839
843
  if (opts.browser !== false)
840
- await open(started.authorizationUrl).catch(() => { });
844
+ await open(started.authorizationUrl).catch((error) => warnBrowserOpenFailure(error));
841
845
  console.error(chalk.dim("The browser finishes on the HQ console. When it says connected, " +
842
846
  "run `hq integrations list` to confirm."));
843
847
  return null;
@@ -62,7 +62,7 @@ import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, lan
62
62
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
63
63
  import { listLaneReviews } from "../lib/lanes/reviews.js";
64
64
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
65
- import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
65
+ import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, aliveDirectChildren, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
66
66
  import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
67
67
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
68
68
  import { laneCompanyUidSource, resolveLaneCompany } from "../lib/lanes/lane-company.js";
@@ -507,7 +507,12 @@ function laneHireFields(lane, lanes, aliveChildrenByParent, finishedChildrenDoNo
507
507
  };
508
508
  }
509
509
  function hireGrantFinishedChildrenDoNotCount(ctx, lane) {
510
- const resolution = resolveLaneCompany(ctx.hqRoot(), lane);
510
+ const resolution = resolveLaneCompany(ctx.hqRoot(), {
511
+ lane_id: lane.lane_id,
512
+ company: lane.company ?? null,
513
+ mesh_company: lane.mesh_company,
514
+ project_id: lane.project_id ?? "",
515
+ });
511
516
  const company = resolution.company ?? undefined;
512
517
  return readLanesRegistryFlagFailOff(LANE_HIRE_GRANT_FINISHED_LANES_FLAG, {
513
518
  company,
@@ -1036,9 +1041,15 @@ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
1036
1041
  !sameHireGrantSenior(lane.senior, grantContext.expected_senior))) {
1037
1042
  throw new LanesError("hire_grant_senior_changed", `Lane ${lane.lane_id}'s senior changed after the primary authorized its Outpost dispatch`, { lane_id: lane.lane_id, expected_senior: grantContext.expected_senior, current_senior: lane.senior });
1038
1043
  }
1039
- const finishedChildrenDoNotCount = isFinishedResumableHireChild(lane.state)
1040
- ? await hireGrantFinishedChildrenDoNotCount(ctx, lane)
1041
- : false;
1044
+ let finishedChildrenDoNotCount = false;
1045
+ if (isTerminalLaneState(lane.state) || isFinishedResumableHireChild(lane.state)) {
1046
+ const seniorLaneId = lane.senior.kind === "lane" ? lane.senior.id : undefined;
1047
+ const resumeSenior = seniorLaneId
1048
+ ? grantContext?.hierarchy.find((entry) => entry.lane_id === seniorLaneId) ??
1049
+ (listLaneIds(hqRoot).includes(seniorLaneId) ? readLane(hqRoot, seniorLaneId) : lane)
1050
+ : lane;
1051
+ finishedChildrenDoNotCount = await hireGrantFinishedChildrenDoNotCount(ctx, resumeSenior);
1052
+ }
1042
1053
  const shouldCheckGrant = isTerminalLaneState(lane.state) ||
1043
1054
  (finishedChildrenDoNotCount && isFinishedResumableHireChild(lane.state));
1044
1055
  if (!shouldCheckGrant ||
@@ -5736,6 +5747,7 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
5736
5747
  initial.senior.kind === "lane" && initial.senior.id
5737
5748
  ? await readRemoteHireGrantSenior(ctx, hqRoot, initial.senior.id)
5738
5749
  : undefined;
5750
+ const finishedChildrenDoNotCount = await hireGrantFinishedChildrenDoNotCount(ctx, initial);
5739
5751
  const updated = ctx.withGrantHireLocks(hqRoot, lockedLaneIds, () => {
5740
5752
  const current = readLane(hqRoot, laneId);
5741
5753
  const currentCallerAuthorization = authorization === "senior"
@@ -5779,11 +5791,11 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
5779
5791
  if (isTerminalLaneState(parent.state)) {
5780
5792
  throw new LanesError("hire_grant_senior_inactive", `Senior lane ${parent.lane_id} is ${parent.state} and cannot change grants`, { senior_lane_id: parent.lane_id, state: parent.state });
5781
5793
  }
5782
- const siblings = aliveDirectChildren(hierarchy, parent.lane_id);
5794
+ const siblings = childrenHoldingHireGrantCarve(hierarchy, parent.lane_id, finishedChildrenDoNotCount);
5783
5795
  const parentGrant = parent.hire_grant ?? 0;
5784
5796
  const assigned = assignedChildHireGrants(siblings, current.lane_id);
5785
5797
  const available = Math.max(0, parentGrant - assigned);
5786
- if (hireGrant > available) {
5798
+ if (hireGrant > (current.hire_grant ?? 0) && hireGrant > available) {
5787
5799
  throw new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${available} unassigned hire grant remaining for lane ${laneId}`, {
5788
5800
  senior_lane_id: parent.lane_id,
5789
5801
  parent_grant: parentGrant,
@@ -5794,7 +5806,7 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
5794
5806
  });
5795
5807
  }
5796
5808
  }
5797
- const children = aliveDirectChildren(hierarchy, current.lane_id);
5809
+ const children = childrenHoldingHireGrantCarve(hierarchy, current.lane_id, finishedChildrenDoNotCount);
5798
5810
  const allocated = assignedChildHireGrants(children);
5799
5811
  const minimumGrant = Math.max(allocated, children.length);
5800
5812
  if (hireGrant < minimumGrant) {
@@ -7063,6 +7075,7 @@ async function reparentOnOwner(ctx, laneId, opts) {
7063
7075
  const nextSenior = parseSeniorRef(opts.senior);
7064
7076
  assertSeniorExists(hqRoot, nextSenior, ctx);
7065
7077
  const initial = readLane(hqRoot, laneId);
7078
+ const finishedChildrenDoNotCount = await hireGrantFinishedChildrenDoNotCount(ctx, initial);
7066
7079
  const refreshParents = new Set();
7067
7080
  if (initial.senior.kind === "lane" && initial.senior.id)
7068
7081
  refreshParents.add(initial.senior.id);
@@ -7074,7 +7087,7 @@ async function reparentOnOwner(ctx, laneId, opts) {
7074
7087
  ...(initial.senior.kind === "lane" && initial.senior.id ? [initial.senior.id] : []),
7075
7088
  ...(nextSenior.kind === "lane" && nextSenior.id ? [nextSenior.id] : []),
7076
7089
  ];
7077
- return withLaneHireLocks(hqRoot, lockIds, () => runReparentLocked(ctx, laneId, opts));
7090
+ return withLaneHireLocks(hqRoot, lockIds, () => runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount));
7078
7091
  }
7079
7092
  async function runReparentOwnerTarget(ctx, laneId, opts) {
7080
7093
  if (!ctx.isSsmOwnerDispatch()) {
@@ -7094,7 +7107,7 @@ async function runReparentOwnerTarget(ctx, laneId, opts) {
7094
7107
  changed_descendant_ids: result.changedDescendantIds,
7095
7108
  });
7096
7109
  }
7097
- function runReparentLocked(ctx, laneId, opts) {
7110
+ function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false) {
7098
7111
  const hqRoot = ctx.hqRoot();
7099
7112
  if (!opts.senior) {
7100
7113
  throw new LanesError("missing_senior", "--senior is required", {});
@@ -7108,26 +7121,27 @@ function runReparentLocked(ctx, laneId, opts) {
7108
7121
  }
7109
7122
  const seniorCompany = companyOfSenior(hqRoot, nextSenior, ctx);
7110
7123
  assertSameTenant(lane.company, nextSenior, seniorCompany, { lane_id: laneId });
7111
- if (nextSenior.kind === "lane" &&
7112
- seniorKey(lane.senior) !== seniorKey(nextSenior)) {
7124
+ if (nextSenior.kind === "lane" && seniorKey(lane.senior) !== seniorKey(nextSenior)) {
7113
7125
  const parent = readLane(hqRoot, nextSenior.id);
7114
7126
  if (isTerminalLaneState(parent.state)) {
7115
7127
  throw new LanesError("hire_grant_senior_inactive", `Senior lane ${parent.lane_id} is ${parent.state} and cannot accept lane children`, { senior_lane_id: parent.lane_id, state: parent.state });
7116
7128
  }
7117
- const children = aliveDirectChildren(loadAllLanes(hqRoot), parent.lane_id);
7118
- if (children.length >= (parent.hire_grant ?? 0)) {
7119
- throw hireGrantCapacityError(parent, children);
7120
- }
7121
- const available = Math.max(0, (parent.hire_grant ?? 0) - assignedChildHireGrants(children));
7122
- const requested = lane.hire_grant ?? 0;
7123
- if (requested > available) {
7124
- throw new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${available} unassigned hire grant remaining for lane ${laneId}`, {
7125
- senior_lane_id: parent.lane_id,
7126
- parent_grant: parent.hire_grant ?? 0,
7127
- assigned_child_grants: assignedChildHireGrants(children),
7128
- requested_hire_grant: requested,
7129
- available_hire_grant: available,
7130
- });
7129
+ if (laneHoldsHireGrantCarve(lane, finishedChildrenDoNotCount)) {
7130
+ const children = childrenHoldingHireGrantCarve(loadAllLanes(hqRoot), parent.lane_id, finishedChildrenDoNotCount);
7131
+ if (children.length >= (parent.hire_grant ?? 0)) {
7132
+ throw hireGrantCapacityError(parent, children);
7133
+ }
7134
+ const available = Math.max(0, (parent.hire_grant ?? 0) - assignedChildHireGrants(children));
7135
+ const requested = lane.hire_grant ?? 0;
7136
+ if (requested > available) {
7137
+ throw new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${available} unassigned hire grant remaining for lane ${laneId}`, {
7138
+ senior_lane_id: parent.lane_id,
7139
+ parent_grant: parent.hire_grant ?? 0,
7140
+ assigned_child_grants: assignedChildHireGrants(children),
7141
+ requested_hire_grant: requested,
7142
+ available_hire_grant: available,
7143
+ });
7144
+ }
7131
7145
  }
7132
7146
  }
7133
7147
  if (walkHitsLane(hqRoot, nextSenior, laneId, ctx)) {
@@ -4,6 +4,7 @@ import * as path from "node:path";
4
4
  import { ensureCognitoIdToken } from "../../utils/cognito-session.js";
5
5
  import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
6
6
  import { vaultApiFetch } from "../../utils/vault-api.js";
7
+ import { safeCaughtError } from "../../utils/safe-caught-error.js";
7
8
  async function readOwnVault(input) {
8
9
  const client = new S3Client({ region: "us-east-1", credentials: input.credentials });
9
10
  try {
@@ -52,7 +53,9 @@ export async function downloadContinuitySnapshot(input, deps = { credential: ens
52
53
  }
53
54
  }
54
55
  finally {
55
- await reader.cancel().catch(() => { });
56
+ await reader.cancel().catch((error) => {
57
+ console.warn("[hq-cli:bot/continuity-download] reader.cancel failed", safeCaughtError(error));
58
+ });
56
59
  download.close();
57
60
  }
58
61
  const bytes = Buffer.concat(chunks);