@indigoai-us/hq-cli 5.345.42 → 5.345.44

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 (46) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/command-catalog.generated.d.ts +2 -2
  3. package/dist/command-catalog.generated.js +2 -2
  4. package/dist/commands/db-remote-deps.js +4 -0
  5. package/dist/commands/install-global.d.ts +8 -1
  6. package/dist/commands/install-global.js +128 -25
  7. package/dist/commands/lanes-checks.d.ts +1 -0
  8. package/dist/commands/lanes-checks.js +6 -1
  9. package/dist/commands/lanes-message.js +1 -0
  10. package/dist/commands/lanes-report.d.ts +2 -1
  11. package/dist/commands/lanes-report.js +63 -6
  12. package/dist/commands/lanes-resume.js +3 -0
  13. package/dist/commands/lanes.js +218 -49
  14. package/dist/index.js +43 -7
  15. package/dist/lib/anywhere-person-setting.d.ts +2 -0
  16. package/dist/lib/anywhere-person-setting.js +2 -1
  17. package/dist/lib/anywhere-runtime-fast-flag.js +8 -2
  18. package/dist/lib/core-utils/sentry-report.js +8 -1
  19. package/dist/lib/daemon/anywhere-listener.d.ts +16 -0
  20. package/dist/lib/daemon/anywhere-listener.js +79 -0
  21. package/dist/lib/daemon/run.js +30 -23
  22. package/dist/lib/db/control-plane.d.ts +5 -0
  23. package/dist/lib/db/control-plane.js +7 -0
  24. package/dist/lib/doctor/checks/anywhere.d.ts +10 -0
  25. package/dist/lib/doctor/checks/anywhere.js +45 -17
  26. package/dist/lib/flag-registry.d.ts +4 -0
  27. package/dist/lib/flag-registry.js +43 -2
  28. package/dist/lib/flag-snapshot-file.d.ts +17 -0
  29. package/dist/lib/flag-snapshot-file.js +153 -37
  30. package/dist/lib/lanes/flag-keys.d.ts +5 -0
  31. package/dist/lib/lanes/flag-keys.js +5 -0
  32. package/dist/lib/lanes/grants.js +1 -1
  33. package/dist/lib/lanes/reconcile.d.ts +2 -0
  34. package/dist/lib/lanes/reconcile.js +39 -1
  35. package/dist/lib/lanes/spawn.d.ts +4 -0
  36. package/dist/lib/lanes/spawn.js +132 -6
  37. package/dist/lib/lanes/store.js +35 -0
  38. package/dist/lib/lanes/tracker-report.d.ts +46 -0
  39. package/dist/lib/lanes/tracker-report.js +85 -1
  40. package/dist/lib/lanes/types.d.ts +3 -1
  41. package/dist/lib/lanes/types.js +1 -0
  42. package/dist/main.js +5 -1
  43. package/dist/startup-registration.d.ts +3 -0
  44. package/dist/startup-registration.js +11 -4
  45. package/dist/utils/resident-package-preload.js +5 -1
  46. package/package.json +1 -1
@@ -37,7 +37,7 @@ import { laneDepthCap } from "../lib/lanes/depth-cap.js";
37
37
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
38
38
  import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
39
39
  import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
40
- import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, } from "../lib/lanes/flag-keys.js";
40
+ import { LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, } from "../lib/lanes/flag-keys.js";
41
41
  import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
42
42
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
43
43
  import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
@@ -4221,12 +4221,99 @@ function authorizeFailedResume(ctx, lane, encodedContext) {
4221
4221
  }
4222
4222
  return context.actor;
4223
4223
  }
4224
- const callerAuthorization = callerSeniorAuthorization(ctx, lane.senior);
4224
+ const callerAuthorization = failedResumeCallerAuthorization(ctx, lane);
4225
4225
  if (!callerAuthorization.authorized) {
4226
- throw new LanesError("failed_resume_unauthorized", appendCallerSeniorReason("Only the lane's senior may resume a failed lane with --from-failed", callerAuthorization), { lane_id: lane.lane_id, reason: callerAuthorization.reason });
4226
+ throw new LanesError("failed_resume_unauthorized", appendCallerSeniorReason("Only an ancestor in the lane's senior history or its company manager may resume a failed lane with --from-failed", callerAuthorization), { lane_id: lane.lane_id, reason: callerAuthorization.reason });
4227
4227
  }
4228
4228
  return resumeProfileRepinActor(ctx);
4229
4229
  }
4230
+ function failedResumeCallerAuthorization(ctx, lane) {
4231
+ const direct = callerSeniorAuthorization(ctx, lane.senior);
4232
+ if (direct.authorized)
4233
+ return direct;
4234
+ const hqRoot = ctx.hqRoot();
4235
+ const targetCompany = resolveLaneCompany(hqRoot, lane).company;
4236
+ const visited = new Set();
4237
+ const pending = [
4238
+ lane.senior,
4239
+ ...lane.senior_history.flatMap((entry) => [entry.from, entry.to]),
4240
+ ];
4241
+ while (pending.length > 0) {
4242
+ const ref = pending.pop();
4243
+ const key = seniorKey(ref);
4244
+ if (visited.has(key))
4245
+ continue;
4246
+ visited.add(key);
4247
+ if (ref.kind === "user") {
4248
+ if (targetCompany === null && callerSeniorAuthorization(ctx, ref).authorized) {
4249
+ return { authorized: true };
4250
+ }
4251
+ continue;
4252
+ }
4253
+ if (!ref.id)
4254
+ continue;
4255
+ if (ref.kind === "lane") {
4256
+ const ancestor = tryReadLane(hqRoot, ref.id);
4257
+ if (!ancestor)
4258
+ continue;
4259
+ const ancestorCompany = resolveLaneCompany(hqRoot, ancestor).company;
4260
+ if (!sameLaneCompany(targetCompany, ancestorCompany))
4261
+ continue;
4262
+ if (callerSeniorAuthorization(ctx, ref).authorized)
4263
+ return { authorized: true };
4264
+ pending.push(ancestor.senior, ...ancestor.senior_history.flatMap((entry) => [entry.from, entry.to]));
4265
+ continue;
4266
+ }
4267
+ if (ref.kind === "session") {
4268
+ const loaded = loadSessionMeta(hqRoot, ref.id, ctx);
4269
+ if (loaded.status !== "ok")
4270
+ continue;
4271
+ const sessionCompany = companyBindingFromSessionMeta(loaded.doc);
4272
+ const sessionCompanyRef = sessionCompany.binding === "company"
4273
+ ? { slug: sessionCompany.slug ?? "", uid: sessionCompany.uid ?? "" }
4274
+ : null;
4275
+ if (targetCompany === null) {
4276
+ if (sessionCompany.binding !== "none")
4277
+ continue;
4278
+ }
4279
+ else if (sessionCompany.binding !== "company" ||
4280
+ !sameLaneCompany(targetCompany, sessionCompanyRef)) {
4281
+ continue;
4282
+ }
4283
+ if (callerSeniorAuthorization(ctx, ref).authorized)
4284
+ return { authorized: true };
4285
+ const value = sessionSeniorValue(loaded.doc);
4286
+ if (typeof value === "string" && value.trim() !== "") {
4287
+ try {
4288
+ pending.push(parseSeniorRef(value));
4289
+ }
4290
+ catch {
4291
+ continue;
4292
+ }
4293
+ }
4294
+ else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
4295
+ const senior = value;
4296
+ if (senior.kind === "user")
4297
+ continue;
4298
+ if ((senior.kind === "lane" || senior.kind === "session") &&
4299
+ typeof senior.id === "string" && senior.id.trim() !== "") {
4300
+ pending.push({ kind: senior.kind, id: senior.id });
4301
+ }
4302
+ }
4303
+ }
4304
+ }
4305
+ const callerLaneId = ctx.env.HQ_LANE_ID?.trim();
4306
+ const callerLane = callerLaneId ? tryReadLane(hqRoot, callerLaneId) : null;
4307
+ if (callerLane && targetCompany !== null) {
4308
+ const callerCompany = resolveLaneCompany(hqRoot, callerLane).company;
4309
+ if (callerCompany !== null &&
4310
+ sameLaneCompany(targetCompany, callerCompany) &&
4311
+ resolveManagerStatusTarget(hqRoot, callerLane).manager) {
4312
+ return { authorized: true };
4313
+ }
4314
+ }
4315
+ return { authorized: false, reason: "caller is not an ancestor lane or session, nor the lane's same-company manager" };
4316
+ }
4230
4317
  function workerProfileHashMismatch(lane, profile, callerAuthorization) {
4231
4318
  const message = `Worker profile ${JSON.stringify(profile.id)} on Outpost ${lane.host.id} does not match the hash recorded by the lane`;
4232
4319
  return new LanesError("worker_profile_hash_mismatch", callerAuthorization && !callerAuthorization.authorized
@@ -5852,17 +5939,23 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
5852
5939
  assigned_child_grants: allocated,
5853
5940
  });
5854
5941
  }
5855
- return retryUpdateLane(hqRoot, laneId, "command", (record) => ({
5856
- ...record,
5857
- hire_grant: hireGrant,
5858
- }));
5942
+ return retryUpdateLane(hqRoot, laneId, "command", (record) => isTerminalLaneState(record.state)
5943
+ ? { ...record, hire_grant: 0, released_hire_grant: hireGrant }
5944
+ : { ...record, hire_grant: hireGrant });
5859
5945
  });
5860
5946
  return updated;
5861
5947
  }
5862
5948
  async function printGrantResult(ctx, hqRoot, updated, hierarchy) {
5863
5949
  const finishedChildrenDoNotCount = await hireGrantFinishedChildrenDoNotCount(ctx, updated);
5864
5950
  const fields = laneHireFields(updated, hierarchy ?? loadAllLanes(hqRoot), undefined, finishedChildrenDoNotCount);
5865
- printJson({ ok: true, action: "grant", lane_id: updated.lane_id, lane: updated, ...fields });
5951
+ printJson({
5952
+ ok: true,
5953
+ action: "grant",
5954
+ lane_id: updated.lane_id,
5955
+ lane: updated,
5956
+ ...fields,
5957
+ hire_grant: updated.released_hire_grant ?? fields.hire_grant,
5958
+ });
5866
5959
  }
5867
5960
  /**
5868
5961
  * `hq lanes set`: change the model or effort a lane uses from its next
@@ -6836,7 +6929,7 @@ async function prepareAccountMove(ctx, lane, input) {
6836
6929
  account: target,
6837
6930
  ...(input.accountSource !== undefined ? { account_source: input.accountSource } : {}),
6838
6931
  ...(input.accountSelection !== undefined ? { account_selection: input.accountSelection } : {}),
6839
- admission_accounts: undefined,
6932
+ admission_accounts: input.preserveAdmissionAccounts ? current.admission_accounts : undefined,
6840
6933
  provider_session_id: sessionId,
6841
6934
  provider_session_id_absent: undefined,
6842
6935
  ...(input.cliModel !== undefined
@@ -6866,6 +6959,9 @@ async function prepareAccountMove(ctx, lane, input) {
6866
6959
  ...(input.refreshProfileSettings === true ? { refreshProfileSettings: true } : {}),
6867
6960
  at,
6868
6961
  });
6962
+ if (input.preserveAdmissionAccounts && current.admission_accounts !== undefined) {
6963
+ next.admission_accounts = [...current.admission_accounts];
6964
+ }
6869
6965
  if (input.deferTurnStart) {
6870
6966
  next.state = current.state;
6871
6967
  }
@@ -6989,53 +7085,112 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6989
7085
  });
6990
7086
  }
6991
7087
  catch (err) {
6992
- const accountReasons = err instanceof LanesError &&
6993
- err.error === "claude_account_balance_no_eligible_account" &&
6994
- Array.isArray(err.details.account_reasons)
6995
- ? err.details.account_reasons.filter((reason) => reason !== null && typeof reason === "object" && !Array.isArray(reason))
6996
- : [];
7088
+ if (!(err instanceof LanesError) || err.error !== "claude_account_balance_no_eligible_account")
7089
+ throw err;
6997
7090
  const unavailablePinReasons = new Set(["disabled", "home_missing", "not_logged_in", "quota_held"]);
6998
- const everyProfileAccountUnavailable = profilePin !== undefined && profilePin.length > 0 &&
6999
- profilePin.every((account) => accountReasons.some((reason) => reason.account === account && typeof reason.reason === "string" && unavailablePinReasons.has(reason.reason)));
7000
- if (!everyProfileAccountUnavailable || lane.account === undefined ||
7001
- (lane.admission_accounts !== undefined && !lane.admission_accounts.includes(lane.account)))
7091
+ if (profilePin === undefined || profilePin.length === 0)
7002
7092
  throw err;
7003
- const recordedSelection = selectClaudeAccount({
7004
- ...selectionInput,
7005
- allowedAccounts: [lane.account],
7006
- });
7093
+ // Admission scope can hide the profile accounts from the first selection's
7094
+ // diagnostics. Re-evaluate only the pin so exhaustion means an unavailable
7095
+ // account, not merely an empty intersection with admission_accounts.
7096
+ let pinnedAccountReasons;
7097
+ try {
7098
+ selectClaudeAccount({ ...selectionInput, allowedAccounts: profilePin });
7099
+ throw err;
7100
+ }
7101
+ catch (pinErr) {
7102
+ if (pinErr === err)
7103
+ throw pinErr;
7104
+ if (!(pinErr instanceof LanesError) || pinErr.error !== "claude_account_balance_no_eligible_account")
7105
+ throw err;
7106
+ pinnedAccountReasons = Array.isArray(pinErr.details.account_reasons)
7107
+ ? pinErr.details.account_reasons.filter((reason) => reason !== null && typeof reason === "object" && !Array.isArray(reason))
7108
+ : [];
7109
+ }
7110
+ pinnedAccountReasons = pinnedAccountReasons.filter((reason) => typeof reason.account === "string" && profilePin.includes(reason.account));
7111
+ const everyProfileAccountUnavailable = profilePin.every((account) => pinnedAccountReasons.some((reason) => reason.account === account && typeof reason.reason === "string" && unavailablePinReasons.has(reason.reason)));
7112
+ if (!everyProfileAccountUnavailable)
7113
+ throw err;
7114
+ // Refreshing profile settings is allowed to change the account pin. Keep
7115
+ // that operation on its existing selection path instead of applying this
7116
+ // fallback against a profile snapshot that is being refreshed.
7117
+ if (options.refreshProfileSettings === true)
7118
+ throw err;
7119
+ let fallbackSelection;
7120
+ if (lane.account !== undefined && lane.admission_accounts?.includes(lane.account) !== false) {
7121
+ try {
7122
+ fallbackSelection = selectClaudeAccount({ ...selectionInput, allowedAccounts: [lane.account] });
7123
+ }
7124
+ catch (recordedErr) {
7125
+ if (!(recordedErr instanceof LanesError) || recordedErr.error !== "claude_account_balance_no_eligible_account") {
7126
+ throw recordedErr;
7127
+ }
7128
+ }
7129
+ }
7130
+ if (fallbackSelection === undefined) {
7131
+ const resolution = resolveLaneCompany(hqRoot, lane);
7132
+ const company = resolution.company ?? undefined;
7133
+ const exhaustedPinFallbackEnabled = await readLanesRegistryFlagFailOff(LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, {
7134
+ company,
7135
+ ...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
7136
+ env: ctx.env,
7137
+ }, readLanesRegistryFlag);
7138
+ if (!exhaustedPinFallbackEnabled)
7139
+ throw err;
7140
+ try {
7141
+ fallbackSelection = selectClaudeAccount({
7142
+ ...selectionInput,
7143
+ ...(lane.admission_accounts !== undefined ? { allowedAccounts: lane.admission_accounts } : {}),
7144
+ });
7145
+ }
7146
+ catch (balanceErr) {
7147
+ if (!(balanceErr instanceof LanesError) || balanceErr.error !== "claude_account_balance_no_eligible_account") {
7148
+ throw balanceErr;
7149
+ }
7150
+ const balancerAccountReasons = Array.isArray(balanceErr.details.account_reasons)
7151
+ ? balanceErr.details.account_reasons.filter((reason) => reason !== null && typeof reason === "object" && !Array.isArray(reason) &&
7152
+ reason.reason !== "not_allowed_by_lane")
7153
+ : [];
7154
+ throw new LanesError(balanceErr.error, balanceErr.message, {
7155
+ ...balanceErr.details,
7156
+ account_reasons: balancerAccountReasons,
7157
+ profile_pin_reasons: pinnedAccountReasons,
7158
+ balancer_account_reasons: balancerAccountReasons,
7159
+ });
7160
+ }
7161
+ }
7007
7162
  selected = {
7008
- ...recordedSelection,
7009
- considered: recordedSelection.considered.map((entry) => {
7010
- const pinnedReason = profilePin.includes(entry.account)
7011
- ? accountReasons.find((reason) => reason.account === entry.account)?.reason
7012
- : undefined;
7163
+ ...fallbackSelection,
7164
+ considered: fallbackSelection.considered.map((entry) => {
7165
+ const pinnedReason = pinnedAccountReasons.find((reason) => reason.account === entry.account)?.reason;
7013
7166
  return typeof pinnedReason === "string"
7014
7167
  ? { ...entry, eligible: false, ineligible_reason: pinnedReason }
7015
7168
  : entry;
7016
7169
  }),
7017
7170
  skipped_accounts: [
7018
- ...recordedSelection.skipped_accounts.filter((entry) => !profilePin.includes(entry.account)),
7171
+ ...fallbackSelection.skipped_accounts.filter((entry) => !profilePin.includes(entry.account)),
7019
7172
  ...profilePin.map((account) => ({
7020
7173
  account,
7021
7174
  host: lane.host,
7022
- reason: String(accountReasons.find((entry) => entry.account === account)?.reason),
7175
+ reason: String(pinnedAccountReasons.find((entry) => entry.account === account)?.reason),
7023
7176
  })),
7024
7177
  ],
7025
7178
  };
7026
7179
  profilePinUnavailable = true;
7027
7180
  }
7028
- const historyReason = lane.account === undefined
7029
- ? "load_balance"
7030
- : currentConfig?.enabled !== true
7031
- ? "account_unavailable"
7032
- : currentLogin === false
7033
- ? "recorded_account_logged_out"
7034
- : currentLogin !== true
7035
- ? "account_unavailable"
7036
- : quotaBlockFor(loadQuotaState(hqRoot), "claude", lane.account, model, ctx.now()).blocked
7037
- ? "usage_limit"
7038
- : "load_balance";
7181
+ const historyReason = profilePinUnavailable
7182
+ ? "profile_pin_exhausted"
7183
+ : lane.account === undefined
7184
+ ? "load_balance"
7185
+ : currentConfig?.enabled !== true
7186
+ ? "account_unavailable"
7187
+ : currentLogin === false
7188
+ ? "recorded_account_logged_out"
7189
+ : currentLogin !== true
7190
+ ? "account_unavailable"
7191
+ : quotaBlockFor(loadQuotaState(hqRoot), "claude", lane.account, model, ctx.now()).blocked
7192
+ ? "usage_limit"
7193
+ : "load_balance";
7039
7194
  return {
7040
7195
  selection: {
7041
7196
  ...selected,
@@ -7224,19 +7379,26 @@ function assertLaneNotRunningForReparent(lane) {
7224
7379
  throw new LanesError("lane_running", `Cannot re-parent running lane ${lane.lane_id}; wait until it stops first`, { lane_id: lane.lane_id, state: lane.state });
7225
7380
  }
7226
7381
  }
7227
- function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false) {
7382
+ async function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false) {
7228
7383
  const hqRoot = ctx.hqRoot();
7229
7384
  if (!opts.senior) {
7230
7385
  throw new LanesError("missing_senior", "--senior is required", {});
7231
7386
  }
7232
7387
  const nextSenior = parseSeniorRef(opts.senior);
7233
7388
  assertSeniorExists(hqRoot, nextSenior, ctx);
7234
- const lane = readLane(hqRoot, laneId);
7389
+ let lane = readLane(hqRoot, laneId);
7235
7390
  assertLaneRunsHereForOperatorMutation(ctx, lane, "reparent");
7391
+ assertLaneNotRunningForReparent(lane);
7236
7392
  const seniorCompany = companyOfSenior(hqRoot, nextSenior, ctx);
7237
7393
  if (lane.company === null && seniorCompany.binding === "company") {
7238
- const company = seniorCompany.slug ?? seniorCompany.uid ?? "<company>";
7239
- throw new LanesError("lane_company_missing", `Lane ${laneId} has no company attribution. Repair it with hq lanes repair-company ${laneId} --company ${company}, then retry reparent.`, { lane_id: laneId, senior: nextSenior, senior_company: serializeSeniorCompany(seniorCompany) });
7394
+ const company = seniorCompany.slug ?? (seniorCompany.uid
7395
+ ? canonicalCompanySlugForUid(hqRoot, seniorCompany.uid, nextSenior)
7396
+ : undefined);
7397
+ if (!company) {
7398
+ throw new LanesError("lane_company_missing", `Lane ${laneId} has no company attribution and the destination senior's company cannot be resolved`, { lane_id: laneId, senior: nextSenior, senior_company: serializeSeniorCompany(seniorCompany) });
7399
+ }
7400
+ await repairCompanyOnOwner(ctx, laneId, { company }, false);
7401
+ lane = readLane(hqRoot, laneId);
7240
7402
  }
7241
7403
  assertSameTenant(lane.company, nextSenior, seniorCompany, { lane_id: laneId });
7242
7404
  if (nextSenior.kind === "lane" && seniorKey(lane.senior) !== seniorKey(nextSenior)) {
@@ -7372,7 +7534,7 @@ async function runRepairCompany(ctx, laneId, opts) {
7372
7534
  }
7373
7535
  await repairCompanyOnOwner(ctx, laneId, opts);
7374
7536
  }
7375
- async function repairCompanyOnOwner(ctx, laneId, opts) {
7537
+ async function repairCompanyOnOwner(ctx, laneId, opts, emitResult = true) {
7376
7538
  const hqRoot = ctx.hqRoot();
7377
7539
  const companySlug = opts.company?.trim();
7378
7540
  if (!companySlug) {
@@ -7450,7 +7612,9 @@ async function repairCompanyOnOwner(ctx, laneId, opts) {
7450
7612
  const repairedLane = readLane(hqRoot, laneId);
7451
7613
  syncLaneToProjectStoryLedger(hqRoot, repairedLane, migratedQuestionRefs);
7452
7614
  removeLaneFromProjectStoryLedger(hqRoot, initial);
7453
- printJson({ ok: true, action: "repair-company", lane_id: laneId, lane: repairedLane, migrated_pending_questions: migratedQuestionRefs.length });
7615
+ if (emitResult) {
7616
+ printJson({ ok: true, action: "repair-company", lane_id: laneId, lane: repairedLane, migrated_pending_questions: migratedQuestionRefs.length });
7617
+ }
7454
7618
  }
7455
7619
  async function runRepairCompanyOwnerTarget(ctx, laneId, opts) {
7456
7620
  if (!ctx.isSsmOwnerDispatch()) {
@@ -8476,7 +8640,8 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
8476
8640
  const moved = await prepareAccountMove(ctx, recoveredLane, {
8477
8641
  account: selection.account,
8478
8642
  cliModel: pendingModel,
8479
- accountHistoryReason: "keep_alive_resume",
8643
+ accountHistoryReason: automatic.historyReason === "profile_pin_exhausted" ? automatic.historyReason : "keep_alive_resume",
8644
+ ...(automatic.historyReason === "profile_pin_exhausted" ? { preserveAdmissionAccounts: true } : {}),
8480
8645
  ...(pendingModel === undefined && automatic.preservedModel !== undefined
8481
8646
  ? { preservedModel: automatic.preservedModel }
8482
8647
  : {}),
@@ -8826,6 +8991,10 @@ export function registerLanesCommand(parent, deps = {}) {
8826
8991
  hqRoot: ctx.hqRoot,
8827
8992
  resolveCompany: ctx.resolveCompany,
8828
8993
  now: ctx.now,
8994
+ focusDashboardEnabled: (company) => readLanesRegistryFlagFailOff(LANE_FOCUS_DASHBOARD_FLAG, {
8995
+ company,
8996
+ env: ctx.env,
8997
+ }, readLanesRegistryFlag),
8829
8998
  readQuestionBody: (root, question) => {
8830
8999
  const body = questionHandlers.requireQuestionBody(root, question);
8831
9000
  assertQuestionBodySecretSafe(body);
@@ -9185,10 +9354,10 @@ export function registerLanesCommand(parent, deps = {}) {
9185
9354
  }));
9186
9355
  lanes
9187
9356
  .command("resume")
9188
- .description("Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with the senior-only --from-failed option")
9357
+ .description("Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with --from-failed as a senior ancestor or same-company manager")
9189
9358
  .argument("<lane-id>", "Lane id")
9190
9359
  .option("--text <msg>", "Operator message for the resumed turn")
9191
- .option("--from-failed", "Explicitly resume a failed lane as its senior")
9360
+ .option("--from-failed", "Explicitly resume a failed lane as an ancestor or same-company manager")
9192
9361
  .option("--reason <text>", "Required explanation for resuming from failed")
9193
9362
  .option("--project <slug>", "Re-point project attribution; a changed story is named in the resumed prompt")
9194
9363
  .option("--story <id>", "Re-point story attribution; a changed story is named in the resumed prompt")
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { CLI_COMMAND_HELP_CATALOG_FLAG } from "./lib/cli-flag-keys.js";
12
12
  // heavy import (main.js) is lazy and reached only when a rejection fires.
13
13
  import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.js";
14
14
  import { installFastPathStreamErrorHandlers } from "./utils/fast-stream-errors.js";
15
+ import { fileURLToPath } from "node:url";
15
16
  // Install BEFORE any dispatch so a floated rejection on ANY path (the command
16
17
  // graph or the fast-core forwarder) becomes a non-zero exit. @sentry/node's
17
18
  // onUnhandledRejection integration defaults to 'warn' — it captures but does
@@ -20,6 +21,44 @@ import { installFastPathStreamErrorHandlers } from "./utils/fast-stream-errors.j
20
21
  // so runCli's finally still finalizes release health and flushes Sentry.
21
22
  installProcessRejectionBoundary();
22
23
  installFastPathStreamErrorHandlers();
24
+ async function runMainWithInstallTreeRecovery() {
25
+ let runCli;
26
+ const { registerCommandsWithRecovery } = await import("./startup-registration.js");
27
+ try {
28
+ const result = await registerCommandsWithRecovery({
29
+ register: async () => {
30
+ runCli = (await import("./main.js")).runCli;
31
+ },
32
+ argv: process.argv,
33
+ env: process.env,
34
+ entryPath: fileURLToPath(import.meta.url),
35
+ stderr: process.stderr,
36
+ });
37
+ if (result.reexecStatus !== undefined) {
38
+ process.exitCode = result.reexecStatus;
39
+ return;
40
+ }
41
+ if (!runCli)
42
+ throw new Error("HQ CLI startup did not load its command runner");
43
+ await runCli();
44
+ }
45
+ catch (error) {
46
+ // main.js may be the module that is missing, so the global rejection
47
+ // reporter cannot import its usual handler to print the recovery remedy.
48
+ const installTree = await import("./utils/install-tree-torn.js");
49
+ if (installTree.isInstallTreeTornError(error)) {
50
+ process.stderr.write(`${installTree.installTreeTornStderrLine(error)}\n`);
51
+ const sentry = await import("./sentry.js");
52
+ sentry.initSentry();
53
+ sentry.Sentry.captureException(error, installTree.installTreeTornCaptureContext(error));
54
+ sentry.finishSentrySession();
55
+ await sentry.Sentry.flush(2_000);
56
+ process.exitCode = 1;
57
+ return;
58
+ }
59
+ throw error;
60
+ }
61
+ }
23
62
  function dispatchCliEntry() {
24
63
  const versionRequest = isVersionRequest(process.argv);
25
64
  const helpRequest = !versionRequest && isCommandHelpRequest(process.argv);
@@ -51,8 +90,7 @@ function dispatchCliEntry() {
51
90
  await runCommandHelp(process.argv);
52
91
  return;
53
92
  }
54
- const { runCli } = await import("./main.js");
55
- await runCli();
93
+ await runMainWithInstallTreeRecovery();
56
94
  });
57
95
  }
58
96
  else if (parsedFastCore?.entry.kind === "native" &&
@@ -73,8 +111,7 @@ function dispatchCliEntry() {
73
111
  await runFastCore(process.argv, parsedFastCore);
74
112
  }
75
113
  else {
76
- const { runCli } = await import("./main.js");
77
- await runCli();
114
+ await runMainWithInstallTreeRecovery();
78
115
  }
79
116
  });
80
117
  }
@@ -88,8 +125,7 @@ function dispatchCliEntry() {
88
125
  return runSecretsEnvFastPath(request);
89
126
  },
90
127
  runCurrent: async () => {
91
- const { runCli } = await import("./main.js");
92
- await runCli();
128
+ await runMainWithInstallTreeRecovery();
93
129
  },
94
130
  });
95
131
  });
@@ -111,7 +147,7 @@ function dispatchCliEntry() {
111
147
  // successful command into exit 13 when that teardown promise remains pending.
112
148
  // A floated rejection here no longer exits 0 silently: the boundary installed
113
149
  // above turns it into a non-zero exit (HQ-CLI-W).
114
- void import("./main.js").then(({ runCli }) => runCli());
150
+ void runMainWithInstallTreeRecovery();
115
151
  }
116
152
  }
117
153
  function isNpmPackageEntry(entryUrl) {
@@ -1,5 +1,7 @@
1
1
  export interface ReadHqAnywherePersonSettingOptions {
2
2
  readSetting?: () => Promise<boolean | null>;
3
+ readFlag?: (env: NodeJS.ProcessEnv) => boolean;
4
+ refreshFlag?: () => Promise<void>;
3
5
  getPersonToken?: () => Promise<string>;
4
6
  fetchImpl?: typeof fetch;
5
7
  apiBaseUrl?: string;
@@ -33,7 +33,8 @@ export function evaluateAnywhereRuntime(flagEnabled, setting) {
33
33
  }
34
34
  /** The operator flag always wins; the person preference is fetched only while it is on. */
35
35
  export async function anywhereRuntimeAllowed(env = process.env, options = {}) {
36
- if (!anywhereRuntimeEnabledFromSnapshot(env))
36
+ await options.refreshFlag?.();
37
+ if (!(options.readFlag ?? anywhereRuntimeEnabledFromSnapshot)(env))
37
38
  return false;
38
39
  const setting = options.readSetting ? await options.readSetting() : await readHqAnywherePersonSetting({ ...options, env });
39
40
  return evaluateAnywhereRuntime(true, setting).enabled;
@@ -1,6 +1,6 @@
1
1
  import { readLocalOverride, resolveFlag } from "@indigoai-us/hq-flags-client";
2
2
  import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
3
- import { readFlagSnapshotFile } from "./flag-snapshot-file.js";
3
+ import { readFlagSnapshotFile, readPersonFlagSnapshotFile } from "./flag-snapshot-file.js";
4
4
  import { HQ_ANYWHERE_RUNTIME_FLAG } from "./anywhere-runtime-flag-key.js";
5
5
  /** Resolve the Anywhere gate from hq-flags without loading the CLI's large worker bundle. */
6
6
  export function anywhereRuntimeEnabledFromSnapshot(env = process.env) {
@@ -8,10 +8,16 @@ export function anywhereRuntimeEnabledFromSnapshot(env = process.env) {
8
8
  if (local !== null)
9
9
  return local;
10
10
  const company = resolveProcessFlagCompanyContext(env);
11
- const persisted = company.companyUid ? readFlagSnapshotFile(company.companyUid, { env }) : null;
11
+ const hasCompanyContext = Boolean(company.companyUid || company.companySlug);
12
+ const persisted = company.companyUid
13
+ ? readFlagSnapshotFile(company.companyUid, { env })
14
+ : hasCompanyContext
15
+ ? null
16
+ : readPersonFlagSnapshotFile({ env });
12
17
  const snapshot = persisted
13
18
  ? { version: 0, flags: persisted.flags }
14
19
  : null;
20
+ // HQ Anywhere authorizes a capability, so a missing or stale snapshot fails closed.
15
21
  return resolveFlag({
16
22
  flagKey: HQ_ANYWHERE_RUNTIME_FLAG,
17
23
  env,
@@ -147,8 +147,11 @@ const SAFE_SLOW_PHASES = new Set([
147
147
  "child_wait",
148
148
  "probe",
149
149
  "parse",
150
+ "no_phase_recorded",
150
151
  ]);
151
- const DEBUG_CONTEXT_PHASES = new Set(SAFE_SLOW_PHASES);
152
+ // This sentinel describes an absent active-phase record, not a phase a hook
153
+ // measures in phase_timings.
154
+ const DEBUG_CONTEXT_PHASES = new Set([...SAFE_SLOW_PHASES].filter((phase) => phase !== "no_phase_recorded"));
152
155
  /**
153
156
  * The only hook diagnostics that can be attached as metadata. The list omits
154
157
  * command text, prompts, payloads, company names, and repositories. `hook_path`
@@ -278,6 +281,10 @@ function safeSlowPhase(value) {
278
281
  }
279
282
  function resolvedSlowPhase(metadata) {
280
283
  const reported = safeSlowPhase(metadata.slow_phase);
284
+ // The hook explicitly recorded that no phase was active. Do not replace that
285
+ // finding with a different phase inferred from aggregate timing data.
286
+ if (reported === "no_phase_recorded")
287
+ return reported;
281
288
  if (reported !== "unknown" && reported !== "other")
282
289
  return reported;
283
290
  // Which measured master-hook phase consumed the most time when the caller's
@@ -0,0 +1,16 @@
1
+ import type { HqdServer } from "./server.js";
2
+ export interface HqdRuntimeListenerOptions {
3
+ isEnabled: () => Promise<boolean>;
4
+ startServer: () => Promise<HqdServer>;
5
+ onServerChange?: (server: HqdServer | null) => void;
6
+ intervalMs?: number;
7
+ schedule?: (callback: () => void, delayMs: number) => () => void;
8
+ log?: (error: unknown) => void;
9
+ }
10
+ export interface HqdRuntimeListener {
11
+ refresh(): Promise<void>;
12
+ close(): Promise<void>;
13
+ }
14
+ /** Keep the local hqd listener aligned with the live flag and person setting. */
15
+ export declare function startHqdRuntimeListener(options: HqdRuntimeListenerOptions): Promise<HqdRuntimeListener>;
16
+ //# sourceMappingURL=anywhere-listener.d.ts.map
@@ -0,0 +1,79 @@
1
+ /** Keep the local hqd listener aligned with the live flag and person setting. */
2
+ export async function startHqdRuntimeListener(options) {
3
+ const intervalMs = options.intervalMs ?? 60_000;
4
+ const log = options.log ?? (() => { });
5
+ const schedule = options.schedule ?? ((callback, delayMs) => {
6
+ const timer = setInterval(callback, delayMs);
7
+ timer.unref();
8
+ return () => clearInterval(timer);
9
+ });
10
+ let server = null;
11
+ let closed = false;
12
+ let inFlight = null;
13
+ const refresh = () => {
14
+ if (closed)
15
+ return Promise.resolve();
16
+ if (inFlight)
17
+ return inFlight;
18
+ inFlight = (async () => {
19
+ let enabled = false;
20
+ try {
21
+ enabled = await options.isEnabled();
22
+ }
23
+ catch (error) {
24
+ log(error);
25
+ }
26
+ if (closed || enabled === (server !== null))
27
+ return;
28
+ if (enabled) {
29
+ try {
30
+ const opened = await options.startServer();
31
+ if (closed) {
32
+ await opened.close();
33
+ return;
34
+ }
35
+ server = opened;
36
+ options.onServerChange?.(server);
37
+ }
38
+ catch (error) {
39
+ log(error);
40
+ }
41
+ return;
42
+ }
43
+ const current = server;
44
+ server = null;
45
+ options.onServerChange?.(null);
46
+ if (current) {
47
+ try {
48
+ await current.close();
49
+ }
50
+ catch (error) {
51
+ log(error);
52
+ }
53
+ }
54
+ })().finally(() => {
55
+ inFlight = null;
56
+ });
57
+ return inFlight;
58
+ };
59
+ await refresh();
60
+ if (closed)
61
+ return { refresh, close: async () => { } };
62
+ const cancel = schedule(() => { void refresh(); }, intervalMs);
63
+ return {
64
+ refresh,
65
+ async close() {
66
+ if (closed)
67
+ return;
68
+ closed = true;
69
+ cancel();
70
+ await inFlight;
71
+ const current = server;
72
+ server = null;
73
+ options.onServerChange?.(null);
74
+ if (current)
75
+ await current.close();
76
+ },
77
+ };
78
+ }
79
+ //# sourceMappingURL=anywhere-listener.js.map