@indigoai-us/hq-cli 5.345.160 → 5.345.162

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 (73) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/assets/scaffold/core/scripts/session-journal.sh +116 -24
  3. package/dist/command-catalog.generated.d.ts +262 -5
  4. package/dist/command-catalog.generated.js +322 -5
  5. package/dist/command-registration-plan.d.ts +6 -0
  6. package/dist/command-registration-plan.js +1 -0
  7. package/dist/commands/agent-inbox.js +6 -2
  8. package/dist/commands/auth.js +6 -5
  9. package/dist/commands/core-session-helper-runtime.d.ts +1 -0
  10. package/dist/commands/core-session-journal-native.d.ts +1 -0
  11. package/dist/commands/core-session-journal-native.js +158 -34
  12. package/dist/commands/feedback.d.ts +12 -0
  13. package/dist/commands/feedback.js +52 -0
  14. package/dist/commands/files-browse.d.ts +8 -0
  15. package/dist/commands/files-browse.js +83 -1
  16. package/dist/commands/install-global.d.ts +2 -0
  17. package/dist/commands/install-global.js +24 -1
  18. package/dist/commands/integrations-connect.js +1 -0
  19. package/dist/commands/integrations-manage.d.ts +2 -0
  20. package/dist/commands/integrations-manage.js +6 -4
  21. package/dist/commands/lanes-link.js +5 -3
  22. package/dist/commands/lanes-loop.js +7 -7
  23. package/dist/commands/lanes-questions.js +54 -4
  24. package/dist/commands/lanes-session-start.d.ts +12 -0
  25. package/dist/commands/lanes-session-start.js +93 -12
  26. package/dist/commands/lanes.js +2 -1
  27. package/dist/commands/meetings.d.ts +11 -0
  28. package/dist/commands/meetings.js +41 -11
  29. package/dist/commands/sync-held.d.ts +87 -0
  30. package/dist/commands/sync-held.js +191 -0
  31. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3830 -2812
  32. package/dist/hq-cloud-sync-runner/package.json +1 -1
  33. package/dist/index.js +21 -0
  34. package/dist/lanes-session-start-argv.d.ts +7 -0
  35. package/dist/lanes-session-start-argv.js +9 -0
  36. package/dist/lib/agent-kit/inbox-state.d.ts +5 -4
  37. package/dist/lib/agent-kit/inbox-state.js +24 -3
  38. package/dist/lib/bot/log.d.ts +3 -2
  39. package/dist/lib/bot/progress.d.ts +23 -1
  40. package/dist/lib/bot/progress.js +43 -5
  41. package/dist/lib/bot/run.js +2 -0
  42. package/dist/lib/bot/runtime/claude.d.ts +12 -0
  43. package/dist/lib/bot/runtime/claude.js +20 -0
  44. package/dist/lib/bot/runtime/index.d.ts +10 -1
  45. package/dist/lib/bot/runtime/index.js +14 -2
  46. package/dist/lib/bot/runtime/types.d.ts +8 -0
  47. package/dist/lib/doctor/checks/ps1-shims.d.ts +21 -0
  48. package/dist/lib/doctor/checks/ps1-shims.js +82 -0
  49. package/dist/lib/doctor/fix/apply.d.ts +13 -1
  50. package/dist/lib/doctor/fix/apply.js +38 -3
  51. package/dist/lib/doctor/fix/remediation.d.ts +9 -1
  52. package/dist/lib/doctor/fix/remediation.js +22 -0
  53. package/dist/lib/doctor/registry.js +2 -0
  54. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  55. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  56. package/dist/lib/lanes/links.d.ts +2 -1
  57. package/dist/lib/lanes/links.js +235 -17
  58. package/dist/lib/lanes/loop.d.ts +17 -0
  59. package/dist/lib/lanes/loop.js +235 -5
  60. package/dist/lib/lanes/questions.d.ts +1 -0
  61. package/dist/lib/lanes/registry-flag.d.ts +24 -0
  62. package/dist/lib/lanes/registry-flag.js +72 -6
  63. package/dist/lib/lanes/store.d.ts +4 -2
  64. package/dist/lib/lanes/store.js +8 -8
  65. package/dist/lib/windows/ps1-shims.d.ts +60 -0
  66. package/dist/lib/windows/ps1-shims.js +152 -0
  67. package/dist/utils/self-update.d.ts +2 -0
  68. package/dist/utils/self-update.js +8 -0
  69. package/dist/utils/staged-npm-install.js +13 -2
  70. package/dist/utils/token-keychain.d.ts +11 -8
  71. package/dist/utils/token-keychain.js +36 -15
  72. package/package.json +8 -8
  73. package/scripts/install-hook-engine.mjs +12 -0
@@ -7,7 +7,7 @@ import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
7
7
  import { spawnLaneWorker } from "../lib/lanes/spawn.js";
8
8
  import { raiseLaneQuestion } from "../lib/lanes/questions.js";
9
9
  import { isTerminalLaneState } from "../lib/lanes/types.js";
10
- import { clearLoopControllerPid, loopAdmissionPath, runLoopLane, setLoopControllerPid } from "../lib/lanes/loop.js";
10
+ import { HANDOFF_HOLD_CHOICES, clearLoopControllerPid, loopAdmissionPath, runLoopLane, setLoopControllerPid } from "../lib/lanes/loop.js";
11
11
  import { laneStories } from "../lib/lanes/stories.js";
12
12
  import { readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
13
13
  import { LANE_LOOP_HANDOFF_REPAIR_FLAG } from "../lib/lanes/flag-keys.js";
@@ -255,7 +255,7 @@ export async function runLaneLoopController(ctx, laneId, deps) {
255
255
  signal.removeEventListener("abort", abortPhase);
256
256
  }
257
257
  },
258
- raiseQuestion: ({ envelope, envelopeId, attempt, error, questionId }) => {
258
+ raiseQuestion: ({ envelope, envelopeId, attempt, error, questionId, reason }) => {
259
259
  const current = readLane(hqRoot, laneId);
260
260
  const data = envelope;
261
261
  raiseLaneQuestion({
@@ -265,11 +265,11 @@ export async function runLaneLoopController(ctx, laneId, deps) {
265
265
  raisedVia: "cli",
266
266
  at: ctx.now().toISOString(),
267
267
  body: {
268
- question: `Loop phase ${String(data.phase ?? envelopeId)} stalled twice. How should it proceed?`,
269
- context: `${error}\nThe phase is parked; later envelopes remain queued. Stall attempt: ${attempt}.`,
270
- options: ["Retry with a longer deadline", "Reroute to another worker", "Drop this phase and block the story"],
271
- recommended: "Retry with a longer deadline",
272
- loop_stall: { lane_id: laneId, envelope_id: envelopeId, attempt },
268
+ question: reason === "handoff_needs_repair" ? `Loop phase ${String(data.phase ?? envelopeId)} has an invalid handoff. How should it proceed?` : `Loop phase ${String(data.phase ?? envelopeId)} stalled twice. How should it proceed?`,
269
+ context: reason === "handoff_needs_repair" ? `${error}\nThe worker file is preserved and the phase is parked; later envelopes remain queued. Repair the handoff file before accepting it. Retrying can repeat external effects.` : `${error}\nThe phase is parked; later envelopes remain queued. Stall attempt: ${attempt}.`,
270
+ options: reason === "handoff_needs_repair" ? [...HANDOFF_HOLD_CHOICES] : ["Retry with a longer deadline", "Reroute to another worker", "Drop this phase and block the story"],
271
+ recommended: reason === "handoff_needs_repair" ? HANDOFF_HOLD_CHOICES[0] : "Retry with a longer deadline",
272
+ loop_stall: { lane_id: laneId, envelope_id: envelopeId, attempt, ...(reason ? { reason } : {}) },
273
273
  },
274
274
  });
275
275
  },
@@ -1,4 +1,5 @@
1
1
  import * as path from "node:path";
2
+ import { readSessionCompanyLock } from "../lib/session-company-lock.js";
2
3
  import { isQuestionImported } from "../lib/lanes/imported-question.js";
3
4
  import { buildOutpostQuestionAnswerCommand, buildOutpostQuestionCancelCommand, buildOutpostQuestionShowCommand, dispatchErrorMessage } from "../lib/lanes/outpost-dispatch.js";
4
5
  import { assertNoSecretText } from "../lib/lanes/text-scan.js";
@@ -6,7 +7,7 @@ import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
6
7
  import { appendCappedHistory, createQuestion, expandLocalLaneIdPrefix, isLanesError, listSessionLiveness, parseLivenessState, parseQuestionRecord, parseQuestionStatus, readDaemonPresenceConnected, readLane, readQuestion, wakeAnsweredLaneNow, sessionLiveness, appendDropboxMessage, answerDropboxMessage, laneAcceptsDropboxDelivery, laneIdForAnswerDelivery, writeQuestionBody } from "../lib/lanes/index.js";
7
8
  import { resolveQuestionCompanyWithFallback } from "../lib/lanes/questions.js";
8
9
  import { LanesError } from "../lib/lanes/errors.js";
9
- import { applyLoopStallAnswer, findLoopStallResult } from "../lib/lanes/loop.js";
10
+ import { HANDOFF_HOLD_CHOICES, applyLoopStallAnswer, findLoopStallResult, loopHandoffHoldIdentity } from "../lib/lanes/loop.js";
10
11
  export function resolveAnswerChoice(choice, options, freeform) {
11
12
  const trimmed = choice.trim();
12
13
  if (/^[0-9]+$/.test(trimmed)) {
@@ -847,11 +848,48 @@ export function createLaneQuestionsHandlers(deps) {
847
848
  const retryingSameLoopAnswer = isLoopStallQuestion && question.status === "answered" && question.answer?.choice === choice &&
848
849
  question.answer.answer_kind === incomingAnswerKind && incomingNotesMatch &&
849
850
  deps.sameQuestionAnswer(question.answer, { ...question.answer });
850
- if (question.status !== "pending" && !retryingSameLoopAnswer)
851
- throwIfQuestionNotPending(question);
852
851
  const loopStallResult = isLoopStallQuestion
853
852
  ? findLoopStallResult(path.join(hqRoot, "workspace", "lanes-runs", askingLane.lane_id), questionId)
854
853
  : undefined;
854
+ if (loopStallResult?.hold_reason === "handoff_needs_repair" && !HANDOFF_HOLD_CHOICES.includes(choice)) {
855
+ throw new LanesError("invalid_handoff_disposition", "Choose an explicit handoff repair disposition", { question_id: questionId });
856
+ }
857
+ const handoffIdentity = askingLane.loop_mode ? loopHandoffHoldIdentity(path.join(hqRoot, "workspace", "lanes-runs", askingLane.lane_id)) : undefined;
858
+ const changingHeldAnswer = body.loop_stall?.reason === "handoff_needs_repair" && isLoopStallQuestion &&
859
+ question.status === "answered" && question.answer?.choice === HANDOFF_HOLD_CHOICES[2] &&
860
+ choice !== HANDOFF_HOLD_CHOICES[2] && handoffIdentity?.unresolved === true &&
861
+ handoffIdentity.question_id === questionId && handoffIdentity.envelope_id === body.loop_stall.envelope_id &&
862
+ loopStallResult?.hold_reason === "handoff_needs_repair";
863
+ if (question.status !== "pending" && !retryingSameLoopAnswer && !changingHeldAnswer)
864
+ throwIfQuestionNotPending(question);
865
+ const isHandoffQuestion = body.loop_stall?.reason === "handoff_needs_repair" || loopStallResult?.hold_reason === "handoff_needs_repair" || handoffIdentity !== undefined;
866
+ if (isHandoffQuestion) {
867
+ assertQuestionAnswerAuthorized(ctx, askingLane);
868
+ assertLaneRunsHereForOperatorMutation(ctx, askingLane, "resolve a handoff repair hold on");
869
+ const targetCompany = questionCompanyForLane(question, askingLane, hqRoot);
870
+ const lock = readSessionCompanyLock({ ...ctx.env, HQ_ROOT: hqRoot });
871
+ const actorLaneId = ctx.env.HQ_LANE_ID?.trim();
872
+ if (actorLaneId) {
873
+ const actorLane = readLane(hqRoot, actorLaneId);
874
+ const actorCompany = actorLane.company ?? actorLane.mesh_company;
875
+ if (!targetCompany || !actorCompany || !sameQuestionCompany(actorCompany, targetCompany))
876
+ throw new LanesError("handoff_answer_company_mismatch", "Caller lane does not own this handoff company", { question_id: questionId });
877
+ }
878
+ // Lane-only callers may use their owned lane identity; a present session lock
879
+ // always constrains that identity and may never be widened by HQ_LANE_ID.
880
+ if ((lock || !actorLaneId) && (!lock || !targetCompany || !lock.companies.includes(targetCompany.slug))) {
881
+ throw new LanesError("handoff_answer_company_mismatch", "The current session is not bound to this handoff company", { question_id: questionId });
882
+ }
883
+ if (!isLoopStallQuestion || body.loop_stall?.reason !== "handoff_needs_repair" ||
884
+ (handoffIdentity && (handoffIdentity.question_id !== questionId || handoffIdentity.envelope_id !== body.loop_stall.envelope_id)) ||
885
+ (loopStallResult && loopStallResult.envelope_id !== body.loop_stall.envelope_id)) {
886
+ throw new LanesError("handoff_question_identity_mismatch", "The persisted handoff question and hold identities do not match", { question_id: questionId });
887
+ }
888
+ if (!HANDOFF_HOLD_CHOICES.includes(choice))
889
+ throw new LanesError("invalid_handoff_disposition", "Choose an explicit handoff repair disposition", { question_id: questionId });
890
+ if (!loopStallResult && question.status === "pending")
891
+ throw new LanesError("handoff_result_missing", "The handoff hold result is missing; no disposition was applied", { question_id: questionId });
892
+ }
855
893
  const at = ctx.now().toISOString();
856
894
  const answeredBy = sessionIdFromEnv(ctx.env)
857
895
  ? `session:${sessionIdFromEnv(ctx.env)}`
@@ -860,7 +898,14 @@ export function createLaneQuestionsHandlers(deps) {
860
898
  if (retryingSameLoopAnswer && current.status === "answered" && current.answer?.choice === choice &&
861
899
  deps.sameQuestionAnswer(current.answer, question.answer))
862
900
  return current;
863
- throwIfQuestionNotPending(current);
901
+ const currentHold = changingHeldAnswer ? loopHandoffHoldIdentity(path.join(hqRoot, "workspace", "lanes-runs", askingLane.lane_id)) : undefined;
902
+ const stillHeld = changingHeldAnswer && current.status === "answered" &&
903
+ current.answer?.choice === HANDOFF_HOLD_CHOICES[2] &&
904
+ deps.sameQuestionAnswer(current.answer, question.answer) &&
905
+ currentHold?.unresolved === true && currentHold.question_id === questionId &&
906
+ currentHold.envelope_id === body.loop_stall?.envelope_id;
907
+ if (!stillHeld)
908
+ throwIfQuestionNotPending(current);
864
909
  assertQuestionCompanyAccess(current, askingLane, requestedCompany, hqRoot);
865
910
  const recordedCompany = current.company ?? questionCompany;
866
911
  const currentForBody = current.company === null
@@ -882,6 +927,7 @@ export function createLaneQuestionsHandlers(deps) {
882
927
  body: nextBody,
883
928
  status: "answered",
884
929
  delivered_at: null,
930
+ ...(stillHeld && current.answer ? { answer_history: [...(current.answer_history ?? []), current.answer] } : {}),
885
931
  answer: {
886
932
  choice,
887
933
  answer_kind: freeform ? "freeform" : "option",
@@ -911,6 +957,10 @@ export function createLaneQuestionsHandlers(deps) {
911
957
  printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: resolved.action });
912
958
  return;
913
959
  }
960
+ if (isHandoffQuestion && resolved.action === "parked") {
961
+ printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: "parked" });
962
+ return;
963
+ }
914
964
  const controllerPid = await deps.restartLoopController(ctx, askingLane.lane_id);
915
965
  printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: resolved.action, controller_pid: controllerPid });
916
966
  return;
@@ -8,9 +8,21 @@ export interface SessionStartOptions {
8
8
  stderr?: Pick<NodeJS.WriteStream, "write">;
9
9
  resolveRoot?: (env: NodeJS.ProcessEnv, cwd?: string) => string;
10
10
  startSweep?: (root: string, env: NodeJS.ProcessEnv) => void;
11
+ /** Starts the off-hot-path registry read when an offline flag value is missing or old. */
12
+ refreshFlags?: (env: NodeJS.ProcessEnv) => void;
11
13
  }
14
+ /** Offline flag values older than this trigger one background registry read. */
15
+ export declare const SESSION_START_FLAG_REFRESH_AFTER_MS: number;
12
16
  /** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
13
17
  export declare function startSessionStartOrphanSweep(root: string, env?: NodeJS.ProcessEnv, spawnDetached?: typeof spawn, rootScoped?: boolean): void;
18
+ /**
19
+ * Start a detached `hq lanes session-start --refresh-flags` so the registry
20
+ * read never runs inside the SessionStart hook. A short lease keeps a burst of
21
+ * session starts to one refresh. An env without a state dir never spawns.
22
+ */
23
+ export declare function startSessionStartFlagRefresh(env: NodeJS.ProcessEnv, spawnDetached?: typeof spawn, now?: number): boolean;
24
+ /** Read the session-start flags from the registry, refreshing the offline cache. */
25
+ export declare function refreshSessionStartFlags(env?: NodeJS.ProcessEnv): Promise<void>;
14
26
  /** SessionStart-compatible, fail-open conduct bootstrap. */
15
27
  export declare function runLaneSessionStart(options?: SessionStartOptions): Promise<void>;
16
28
  export declare function registerLaneSessionStartCommand(lanes: Command): void;
@@ -4,8 +4,17 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
7
- import { readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
7
+ import { readLanesRegistryFlagOffline, readLanesRegistryFlagResult } from "../lib/lanes/registry-flag.js";
8
8
  import { LANE_CONDUCT_SESSION_START_FLAG, LANE_ORPHAN_SWEEP_CONTEXT_FLAG, LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG } from "../lib/lanes/flag-keys.js";
9
+ const SESSION_START_FLAGS = [
10
+ LANE_CONDUCT_SESSION_START_FLAG,
11
+ LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG,
12
+ LANE_ORPHAN_SWEEP_CONTEXT_FLAG,
13
+ ];
14
+ /** Offline flag values older than this trigger one background registry read. */
15
+ export const SESSION_START_FLAG_REFRESH_AFTER_MS = 10 * 60 * 1000;
16
+ const SESSION_START_FLAG_REFRESH_LEASE_MS = 60 * 1000;
17
+ const SESSION_START_FLAG_REFRESH_LEASE = "lanes-session-start-flag-refresh.lock";
9
18
  const CODEX_DEFAULT_KEYS = ["conduct_engine", "conduct_child_model", "conduct_child_effort", "conduct_default_source"];
10
19
  function readSessionMetaValue(text, key) {
11
20
  const line = text.split(/\r?\n/).find((entry) => new RegExp(`^\\s*${key}:`).test(entry));
@@ -247,6 +256,58 @@ export function startSessionStartOrphanSweep(root, env = process.env, spawnDetac
247
256
  // SessionStart's orphan report is advisory and must never block startup.
248
257
  }
249
258
  }
259
+ function flagRefreshLeaseDir(env) {
260
+ const stateDir = env.HQ_STATE_DIR?.trim();
261
+ if (stateDir)
262
+ return stateDir;
263
+ const home = env.HOME?.trim() || env.USERPROFILE?.trim();
264
+ return home ? path.join(home, ".hq") : null;
265
+ }
266
+ /**
267
+ * Start a detached `hq lanes session-start --refresh-flags` so the registry
268
+ * read never runs inside the SessionStart hook. A short lease keeps a burst of
269
+ * session starts to one refresh. An env without a state dir never spawns.
270
+ */
271
+ export function startSessionStartFlagRefresh(env, spawnDetached = spawn, now = Date.now()) {
272
+ const dir = flagRefreshLeaseDir(env);
273
+ const cli = process.argv[1];
274
+ if (!dir || !cli)
275
+ return false;
276
+ try {
277
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
278
+ const lease = path.join(dir, SESSION_START_FLAG_REFRESH_LEASE);
279
+ try {
280
+ if (now - fs.statSync(lease).mtimeMs < SESSION_START_FLAG_REFRESH_LEASE_MS)
281
+ return false;
282
+ fs.rmSync(lease, { force: true });
283
+ }
284
+ catch (error) {
285
+ if (error.code !== "ENOENT")
286
+ return false;
287
+ }
288
+ try {
289
+ fs.closeSync(fs.openSync(lease, "wx", 0o600));
290
+ }
291
+ catch {
292
+ return false;
293
+ }
294
+ const child = spawnDetached(process.execPath, [cli, "lanes", "session-start", "--refresh-flags"], {
295
+ detached: true,
296
+ stdio: "ignore",
297
+ env: { ...env, HQ_NO_UPDATE_CHECK: "1" },
298
+ });
299
+ child.on("error", () => { });
300
+ child.unref();
301
+ return true;
302
+ }
303
+ catch {
304
+ return false;
305
+ }
306
+ }
307
+ /** Read the session-start flags from the registry, refreshing the offline cache. */
308
+ export async function refreshSessionStartFlags(env = process.env) {
309
+ await Promise.all(SESSION_START_FLAGS.map((flag) => readLanesRegistryFlagResult(flag, { env, defaultValue: true, reportFailure: () => undefined }).catch(() => undefined)));
310
+ }
250
311
  /** SessionStart-compatible, fail-open conduct bootstrap. */
251
312
  export async function runLaneSessionStart(options = {}) {
252
313
  const env = options.env ?? process.env;
@@ -278,19 +339,33 @@ export async function runLaneSessionStart(options = {}) {
278
339
  if (/(^|,)auto-conduct(,|$)/.test(disabledHooks))
279
340
  return;
280
341
  const root = (options.resolveRoot ?? ((e, cwd) => resolveLanesHqRoot({ env: e, cwd })))(env, options.cwd);
281
- const flagEnabled = await readLanesRegistryFlagFailOpen(LANE_CONDUCT_SESSION_START_FLAG, {
282
- env,
283
- defaultValue: true,
284
- reportFailure: (error) => {
285
- const kind = error instanceof Error ? error.name : "Error";
286
- debug(`hq lanes session-start: flag read failed (${kind})`);
287
- },
288
- });
342
+ // Flags come from offline state only. A missing or old value fails open and
343
+ // schedules one background registry read for the next session start.
344
+ let refreshNeeded = false;
345
+ const readFlag = (flag) => {
346
+ let read = null;
347
+ try {
348
+ read = readLanesRegistryFlagOffline(flag, { env, defaultValue: true });
349
+ }
350
+ catch {
351
+ read = null;
352
+ }
353
+ if (!read || (read.source !== "local-override" && read.ageMs > SESSION_START_FLAG_REFRESH_AFTER_MS))
354
+ refreshNeeded = true;
355
+ return read?.value ?? true;
356
+ };
357
+ const flagEnabled = readFlag(LANE_CONDUCT_SESSION_START_FLAG);
358
+ const rootScoped = readFlag(LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG);
359
+ const contextEnabled = readFlag(LANE_ORPHAN_SWEEP_CONTEXT_FLAG);
360
+ if (refreshNeeded) {
361
+ try {
362
+ (options.refreshFlags ?? ((e) => { startSessionStartFlagRefresh(e); }))(env);
363
+ }
364
+ catch { /* advisory */ }
365
+ }
289
366
  if (!flagEnabled)
290
367
  return;
291
- const rootScoped = await readLanesRegistryFlagFailOpen(LANE_ORPHAN_SWEEP_ROOT_KEYING_FLAG, { env, defaultValue: true });
292
368
  (options.startSweep ?? ((r, e) => startSessionStartOrphanSweep(r, e, spawn, rootScoped)))(root, env);
293
- const contextEnabled = await readLanesRegistryFlagFailOpen(LANE_ORPHAN_SWEEP_CONTEXT_FLAG, { env, defaultValue: true });
294
369
  if (contextEnabled) {
295
370
  const count = readOrphanCount(root, env, rootScoped, (error) => debug(`hq lanes session-start: orphan report read failed (${error instanceof Error ? error.name : "Error"})`));
296
371
  if (count > 0)
@@ -324,6 +399,12 @@ export async function runLaneSessionStart(options = {}) {
324
399
  export function registerLaneSessionStartCommand(lanes) {
325
400
  lanes.command("session-start", { hidden: true })
326
401
  .description("Internal SessionStart bootstrap")
327
- .action(async () => runLaneSessionStart());
402
+ .option("--refresh-flags", "Refresh the offline session-start flag values from the registry")
403
+ .action(async (opts) => {
404
+ if (opts.refreshFlags)
405
+ await refreshSessionStartFlags();
406
+ else
407
+ await runLaneSessionStart();
408
+ });
328
409
  }
329
410
  //# sourceMappingURL=lanes-session-start.js.map
@@ -717,7 +717,8 @@ export function parseQuestionBody(raw) {
717
717
  typeof obj.loop_stall.envelope_id !== "string" ||
718
718
  typeof obj.loop_stall.attempt !== "number" ||
719
719
  !Number.isSafeInteger(obj.loop_stall.attempt) ||
720
- obj.loop_stall.attempt < 1))
720
+ obj.loop_stall.attempt < 1 ||
721
+ (obj.loop_stall.reason !== undefined && obj.loop_stall.reason !== "handoff_needs_repair")))
721
722
  return null;
722
723
  const options = obj.options.filter((item) => typeof item === "string");
723
724
  const body = {
@@ -9,5 +9,16 @@ export declare const SHORT_ID_MAX_PAGES = 50;
9
9
  * names no plan: the flat plans are closed.
10
10
  */
11
11
  export declare function meetingPaymentRequiredLines(body: Record<string, unknown>, resolvePlan?: () => Promise<PricingPlan | null>): Promise<string[]>;
12
+ /**
13
+ * The company a `hq meetings` call is about: the explicit `--company` value,
14
+ * otherwise the company an explicitly named HQ session is bound to (the session's company
15
+ * lock, from `scope-capability.json` or `meta.yaml`). A session bound to
16
+ * Personal, an unbound session, or an unreadable lock gives undefined so the
17
+ * server's own default applies. Before this, every subcommand sent no
18
+ * company at all without `--company`, so the list, get, transcript and
19
+ * search views silently showed only the caller's personal recordings.
20
+ * Feedback 04771691-fcbc-4847-899a-6c966c561b52.
21
+ */
22
+ export declare function sessionCompanySlug(env?: NodeJS.ProcessEnv): string | undefined;
12
23
  export declare function registerMeetingsCommand(program: Command): void;
13
24
  //# sourceMappingURL=meetings.d.ts.map
@@ -1,6 +1,8 @@
1
1
  import chalk from "chalk";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { ensureCognitoToken } from "../utils/cognito-session.js";
4
+ import { readSessionCompanyLock } from "../lib/session-company-lock.js";
5
+ import { sessionIdFromEnv } from "../lib/core-utils/session-id.js";
4
6
  import { vaultApiFetch, getCompanyUid, resolveCallerPersonUid, } from "../utils/vault-api.js";
5
7
  import { isPlanGateError, planGateErrorFromPayload, } from "../utils/plan-gate-error.js";
6
8
  import { mapWithConcurrency } from "./sync-manifest.js";
@@ -434,11 +436,39 @@ function printMeetingTable(meetings) {
434
436
  ].join(" "));
435
437
  }
436
438
  }
439
+ /**
440
+ * The company a `hq meetings` call is about: the explicit `--company` value,
441
+ * otherwise the company an explicitly named HQ session is bound to (the session's company
442
+ * lock, from `scope-capability.json` or `meta.yaml`). A session bound to
443
+ * Personal, an unbound session, or an unreadable lock gives undefined so the
444
+ * server's own default applies. Before this, every subcommand sent no
445
+ * company at all without `--company`, so the list, get, transcript and
446
+ * search views silently showed only the caller's personal recordings.
447
+ * Feedback 04771691-fcbc-4847-899a-6c966c561b52.
448
+ */
449
+ export function sessionCompanySlug(env = process.env) {
450
+ // The shared reader also supports the last-writer-wins .current pointer.
451
+ // It cannot identify a plain terminal or cron caller's session.
452
+ if (!sessionIdFromEnv(env))
453
+ return undefined;
454
+ let lock;
455
+ try {
456
+ lock = readSessionCompanyLock(env);
457
+ }
458
+ catch (err) {
459
+ // Fall through to the server default, but say why so an unbound call is
460
+ // never mistaken for a working company scope.
461
+ console.error(chalk.dim(` (session company not applied: ${err instanceof Error ? err.message : String(err)})`));
462
+ return undefined;
463
+ }
464
+ const primary = lock?.companies[0];
465
+ return primary && primary !== "personal" ? primary : undefined;
466
+ }
437
467
  export function registerMeetingsCommand(program) {
438
468
  const meetings = program
439
469
  .command("meetings")
440
470
  .description("View and search meeting recordings, transcripts, and notes")
441
- .option("--company <slug>", "Company slug (for multi-company users)")
471
+ .option("--company <slug>", "Company slug (defaults to the company this HQ session is bound to)")
442
472
  .option("--json", "Output raw JSON instead of formatted text");
443
473
  // ── hq meetings list ──────────────────────────────────────────────
444
474
  meetings
@@ -450,7 +480,7 @@ export function registerMeetingsCommand(program) {
450
480
  try {
451
481
  const token = await ensureCognitoToken();
452
482
  const query = {};
453
- const companySlug = meetings.opts().company;
483
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
454
484
  if (opts.limit)
455
485
  query.limit = opts.limit;
456
486
  if (opts.next)
@@ -488,9 +518,9 @@ export function registerMeetingsCommand(program) {
488
518
  .action(async (recallBotId, opts) => {
489
519
  try {
490
520
  const token = await ensureCognitoToken();
491
- const companySlug = meetings.opts().company;
521
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
492
522
  if (!companySlug)
493
- throw new Error("--company <slug|uid> is required for live transcripts");
523
+ throw new Error("--company <slug|uid> is required for live transcripts (this session is not bound to a company)");
494
524
  const companyId = companySlug.startsWith("cmp_") ? companySlug : await getCompanyUid(token, companySlug);
495
525
  if (!recallBotId) {
496
526
  if (opts.follow || opts.since)
@@ -610,9 +640,9 @@ export function registerMeetingsCommand(program) {
610
640
  .description("Import a normalized historical transcript JSON file")
611
641
  .action(async (file) => {
612
642
  try {
613
- const companySlug = meetings.opts().company;
643
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
614
644
  if (!companySlug) {
615
- throw new Error("--company <slug> is required for historical imports");
645
+ throw new Error("--company <slug> is required for historical imports (this session is not bound to a company)");
616
646
  }
617
647
  const bytes = await readFile(file);
618
648
  if (bytes.byteLength > 5 * 1024 * 1024) {
@@ -666,7 +696,7 @@ export function registerMeetingsCommand(program) {
666
696
  .action(async (meetingUrl) => {
667
697
  try {
668
698
  const token = await ensureCognitoToken();
669
- const companySlug = meetings.opts().company;
699
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
670
700
  // Always name a company. With no companyId the backend files the bot
671
701
  // under Personal, which can never record, so an entitled user would
672
702
  // get the payment-required refusal. Without --company this uses the
@@ -707,7 +737,7 @@ export function registerMeetingsCommand(program) {
707
737
  try {
708
738
  const token = await ensureCognitoToken();
709
739
  const query = {};
710
- const companySlug = meetings.opts().company;
740
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
711
741
  if (companySlug)
712
742
  query.companyId = await getCompanyUid(token, companySlug);
713
743
  const meetingId = await resolveShortId(token, rawId, query);
@@ -832,7 +862,7 @@ export function registerMeetingsCommand(program) {
832
862
  try {
833
863
  const token = await ensureCognitoToken();
834
864
  const params = { q: query };
835
- const companySlug = meetings.opts().company;
865
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
836
866
  if (companySlug)
837
867
  params.companyId = await getCompanyUid(token, companySlug);
838
868
  const res = await vaultApiFetch({
@@ -863,7 +893,7 @@ export function registerMeetingsCommand(program) {
863
893
  try {
864
894
  const token = await ensureCognitoToken();
865
895
  const query = {};
866
- const companySlug = meetings.opts().company;
896
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
867
897
  if (companySlug)
868
898
  query.companyId = await getCompanyUid(token, companySlug);
869
899
  const meetingId = await resolveShortId(token, rawId, query);
@@ -932,7 +962,7 @@ export function registerMeetingsCommand(program) {
932
962
  try {
933
963
  const token = await ensureCognitoToken();
934
964
  const query = {};
935
- const companySlug = meetings.opts().company;
965
+ const companySlug = meetings.opts().company ?? sessionCompanySlug();
936
966
  if (companySlug)
937
967
  query.companyId = await getCompanyUid(token, companySlug);
938
968
  const meetingId = await resolveShortId(token, rawId, query);
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `hq sync held` and `hq sync release-held` (feedback_3bf773c5).
3
+ *
4
+ * After a different person signs in on the same computer, hq-cloud holds every
5
+ * upload and cloud delete for each sync scope (the personal vault and each
6
+ * company) until the signed-in person reviews what is held and releases it.
7
+ * These commands are the person's way to see the hold and make that decision.
8
+ *
9
+ * The hq-cloud exports (`listPushIdentityFences`, `releasePushIdentityFences`)
10
+ * ship after the hq-cloud version this CLI pins, so they are feature-detected
11
+ * on the module namespace, the same way `lib/hq-cloud-manifest.ts` does it. A
12
+ * static named import of a missing ESM export would stop every command that
13
+ * loads this file.
14
+ */
15
+ import { Command } from "commander";
16
+ export interface HeldScope {
17
+ slug: string;
18
+ uid: string;
19
+ sinceMs: number;
20
+ held?: {
21
+ count: number;
22
+ samplePaths: string[];
23
+ at: string;
24
+ };
25
+ }
26
+ interface FenceExports {
27
+ listPushIdentityFences: (stateDir: string) => HeldScope[];
28
+ releasePushIdentityFences: (stateDir: string, identities: readonly string[], slugs?: readonly string[]) => string[];
29
+ }
30
+ export declare const HELD_UNAVAILABLE_MESSAGE = "this HQ build cannot list or release held uploads yet; update HQ and try again";
31
+ export declare function fenceExports(mod?: Record<string, unknown>): FenceExports | undefined;
32
+ /**
33
+ * One authenticated snapshot of who is signed in: the Cognito sub of the
34
+ * session token and, when asked for, the personal vault uid resolved with that
35
+ * same token. The sub never comes from the upload-author label, which needs an
36
+ * email; hq-cloud#1040 identifies email-less sessions by sub alone.
37
+ */
38
+ export interface HeldIdentity {
39
+ sub: string;
40
+ personalUid?: string;
41
+ }
42
+ export interface SyncHeldDeps {
43
+ exports: FenceExports | undefined;
44
+ stateDir: string;
45
+ /**
46
+ * Resolve the signed-in identity from one token. `interactive` allows a
47
+ * sign-in or refresh; the result is taken after it, never mixed with an
48
+ * identity read before it.
49
+ */
50
+ identity: (options: {
51
+ needPersonal: boolean;
52
+ interactive: boolean;
53
+ }) => Promise<HeldIdentity>;
54
+ confirm: (question: string) => Promise<boolean>;
55
+ log: (line: string) => void;
56
+ }
57
+ export declare function scopeLabel(slug: string): string;
58
+ export declare function runSyncHeld(deps: SyncHeldDeps, json?: boolean): Promise<number>;
59
+ export interface ReleaseHeldOptions {
60
+ company?: string;
61
+ personal?: boolean;
62
+ yes?: boolean;
63
+ json?: boolean;
64
+ }
65
+ export declare function runReleaseHeld(deps: SyncHeldDeps, options: ReleaseHeldOptions): Promise<number>;
66
+ /**
67
+ * The signed-in identity from the session token `ensureCognitoToken` selects
68
+ * (person or machine, the same choice every vault call makes). The personal
69
+ * vault uid is resolved with that same token: an agent principal uses its own
70
+ * entity uid, as the sync runner does; a person uses the canonical person
71
+ * entity.
72
+ */
73
+ export declare function resolveHeldIdentity(options: {
74
+ needPersonal: boolean;
75
+ interactive: boolean;
76
+ }, io: {
77
+ ensureToken: (interactive: boolean) => Promise<string>;
78
+ listPersons: (token: string) => Promise<Array<{
79
+ uid: string;
80
+ type: string;
81
+ createdAt: string;
82
+ }>>;
83
+ }): Promise<HeldIdentity>;
84
+ export declare function createDefaultSyncHeldDeps(): SyncHeldDeps;
85
+ export declare function registerSyncHeldCommands(syncCmd: Command): void;
86
+ export {};
87
+ //# sourceMappingURL=sync-held.d.ts.map