@indigoai-us/hq-cli 5.345.44 → 5.345.46

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 (49) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/command-catalog.generated.d.ts +17 -1
  3. package/dist/command-catalog.generated.js +21 -1
  4. package/dist/commands/access.js +6 -4
  5. package/dist/commands/bot.js +1 -1
  6. package/dist/commands/db-destroy.js +5 -4
  7. package/dist/commands/install-global.d.ts +3 -0
  8. package/dist/commands/install-global.js +24 -2
  9. package/dist/commands/lanes-message.js +84 -51
  10. package/dist/commands/lanes-resume.js +44 -0
  11. package/dist/commands/lanes-shared.d.ts +25 -0
  12. package/dist/commands/lanes.d.ts +4 -0
  13. package/dist/commands/lanes.js +253 -15
  14. package/dist/commands/meetings.js +4 -1
  15. package/dist/lib/anywhere-person-setting.d.ts +11 -0
  16. package/dist/lib/anywhere-person-setting.js +19 -0
  17. package/dist/lib/daemon/ops/mcp.d.ts +10 -1
  18. package/dist/lib/daemon/ops/mcp.js +17 -10
  19. package/dist/lib/daemon/run.js +8 -2
  20. package/dist/lib/doctor/checks/sync-runtime.d.ts +2 -0
  21. package/dist/lib/doctor/checks/sync-runtime.js +26 -12
  22. package/dist/lib/flag-snapshot-file.js +33 -25
  23. package/dist/lib/install/claude-targets.js +7 -2
  24. package/dist/lib/install/codex-targets.d.ts +6 -0
  25. package/dist/lib/install/codex-targets.js +25 -0
  26. package/dist/lib/lanes/flag-keys.d.ts +1 -0
  27. package/dist/lib/lanes/flag-keys.js +1 -0
  28. package/dist/lib/lanes/focus-guard.d.ts +65 -0
  29. package/dist/lib/lanes/focus-guard.js +91 -0
  30. package/dist/lib/lanes/outpost-dispatch.d.ts +1 -0
  31. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  32. package/dist/lib/lanes/store.js +29 -0
  33. package/dist/lib/lanes/story-ledger.d.ts +14 -0
  34. package/dist/lib/lanes/story-ledger.js +46 -0
  35. package/dist/lib/lanes/types.d.ts +10 -2
  36. package/dist/lib/lanes/types.js +4 -3
  37. package/dist/utils/hook-trust.d.ts +7 -0
  38. package/dist/utils/hook-trust.js +40 -14
  39. package/dist/utils/package-use-lease.d.ts +1 -0
  40. package/dist/utils/package-use-lease.js +29 -0
  41. package/dist/utils/self-update-backoff.d.ts +17 -0
  42. package/dist/utils/self-update-backoff.js +73 -0
  43. package/dist/utils/self-update.d.ts +7 -0
  44. package/dist/utils/self-update.js +36 -0
  45. package/dist/utils/vault-api.d.ts +33 -1
  46. package/dist/utils/vault-api.js +55 -5
  47. package/dist/utils/version-gate.d.ts +13 -0
  48. package/dist/utils/version-gate.js +35 -5
  49. package/package.json +1 -1
@@ -37,7 +37,9 @@ 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_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";
40
+ import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
41
+ import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
42
+ import { LANE_FOCUS_GUARD_FLAG, 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
43
  import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
42
44
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
43
45
  import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
@@ -86,6 +88,133 @@ import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../l
86
88
  import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, spawnEnvelopeRepairRequest, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
87
89
  import { handOffInstallHere } from "../lib/daemon/handoff.js";
88
90
  export { spawnLaneWorker };
91
+ export function openPullRequestsForProject(hqRoot, metadata) {
92
+ const configured = metadata.repos;
93
+ if (!Array.isArray(configured)) {
94
+ throw new Error("project does not name repositories");
95
+ }
96
+ if (configured.length === 0)
97
+ return [];
98
+ const all = [];
99
+ const reposRoot = fs.realpathSync(path.resolve(hqRoot, "repos")) + path.sep;
100
+ for (const repo of configured) {
101
+ if (repo === null || typeof repo !== "object" || typeof repo.path !== "string") {
102
+ throw new Error("project repository entry is invalid");
103
+ }
104
+ const repoPath = repo.path;
105
+ if (!/^repos\/(private|public)\/[a-zA-Z0-9._/-]+$/.test(repoPath)) {
106
+ throw new Error("project repository path is outside repos");
107
+ }
108
+ if (repoPath.split("/").some((segment) => segment === "." || segment === "..")) {
109
+ throw new Error("project repository path is outside repos");
110
+ }
111
+ const repoDir = path.resolve(hqRoot, repoPath);
112
+ const canonicalRepoDir = fs.realpathSync(repoDir);
113
+ if (!canonicalRepoDir.startsWith(reposRoot))
114
+ throw new Error("project repository path is outside repos");
115
+ const remoteResult = spawnSync("git", ["-C", repoDir, "remote", "get-url", "origin"], {
116
+ encoding: "utf8",
117
+ timeout: 5_000,
118
+ windowsHide: true,
119
+ });
120
+ if (remoteResult.status !== 0)
121
+ throw new Error("project repository remote could not be read");
122
+ const remote = remoteResult.stdout.trim();
123
+ const match = remote.match(/^(?:https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/i);
124
+ if (!match)
125
+ throw new Error("project repository is not a GitHub repository");
126
+ const endpoint = `repos/${match[1]}/${match[2]}/pulls?state=open&per_page=100`;
127
+ const result = spawnSync("gh", ["api", "--paginate", "--slurp", endpoint], {
128
+ encoding: "utf8",
129
+ timeout: 20_000,
130
+ windowsHide: true,
131
+ });
132
+ if (result.status !== 0)
133
+ throw new Error("GitHub pull request lookup failed");
134
+ const pages = JSON.parse(result.stdout || "[]");
135
+ if (!Array.isArray(pages))
136
+ throw new Error("GitHub pull request response was invalid");
137
+ for (const page of pages) {
138
+ if (!Array.isArray(page))
139
+ throw new Error("GitHub pull request page was invalid");
140
+ for (const item of page) {
141
+ if (item === null || typeof item !== "object")
142
+ throw new Error("GitHub pull request entry was invalid");
143
+ const pr = item;
144
+ if (!Number.isInteger(pr.number) || typeof pr.created_at !== "string" || !Number.isFinite(Date.parse(pr.created_at))) {
145
+ throw new Error("GitHub pull request entry was invalid");
146
+ }
147
+ all.push({ number: pr.number, created_at: pr.created_at, repository: `${match[1]}/${match[2]}` });
148
+ }
149
+ }
150
+ }
151
+ return all;
152
+ }
153
+ async function focusGuardAssessment(ctx, input) {
154
+ if (input.overrideReason !== undefined) {
155
+ const normalizedReason = input.overrideReason.trim();
156
+ if (!normalizedReason) {
157
+ throw new LanesError("focus_override_reason_required", "--focus-override requires a non-empty reason", {
158
+ story_ids: input.storyIds,
159
+ });
160
+ }
161
+ assertNoSecretText("focus override reason", normalizedReason);
162
+ }
163
+ let enabled = false;
164
+ try {
165
+ enabled = ctx.focusGuardEnabled
166
+ ? await ctx.focusGuardEnabled(input.company ?? undefined)
167
+ : await readLanesRegistryFlagFailOff(LANE_FOCUS_GUARD_FLAG, {
168
+ company: input.company,
169
+ env: ctx.env,
170
+ });
171
+ }
172
+ catch (error) {
173
+ process.stderr.write(`${JSON.stringify({
174
+ event: "lanes.focus_guard.flag_read_failed",
175
+ error: error instanceof Error ? error.name : "UnknownError",
176
+ fallback: false,
177
+ })}\n`);
178
+ }
179
+ if (!enabled)
180
+ return { enabled: false, reasons: [] };
181
+ const project = readProjectStoryFocusData(ctx.hqRoot(), input.projectId, input.company?.slug);
182
+ let pullRequests;
183
+ let pullRequestLookupFailed = false;
184
+ if (input.checkTeamBlockers) {
185
+ try {
186
+ pullRequests = openPullRequestsForProject(ctx.hqRoot(), project.metadata);
187
+ }
188
+ catch (error) {
189
+ pullRequestLookupFailed = true;
190
+ process.stderr.write(`${JSON.stringify({
191
+ event: "lanes.focus_guard.pr_lookup_failed",
192
+ project_id: input.projectId,
193
+ error: error instanceof Error ? error.name : "UnknownError",
194
+ })}\n`);
195
+ }
196
+ }
197
+ const tenantLanes = loadAllLanes(ctx.hqRoot()).filter((lane) => lane.company?.slug === input.company?.slug && (lane.project_id === input.projectId ||
198
+ lane.stories?.some((story) => story.project_id === input.projectId) === true));
199
+ const reasons = assertFocusGuardAllows(enabled, {
200
+ storyIds: input.storyIds,
201
+ stories: project.userStories.map((story) => ({
202
+ id: typeof story.id === "string" ? story.id : "",
203
+ focus_id: story.focus_id,
204
+ kpi: story.kpi,
205
+ state: story.state,
206
+ status: story.status,
207
+ lane: story.lane,
208
+ lanes: story.lanes,
209
+ })),
210
+ lanes: tenantLanes,
211
+ ...(pullRequests === undefined ? {} : { pullRequests }),
212
+ pullRequestLookupFailed,
213
+ now: ctx.now(),
214
+ checkTeamBlockers: input.checkTeamBlockers,
215
+ }, input.overrideReason);
216
+ return { enabled: true, reasons };
217
+ }
89
218
  export { questionBodyAbsPath };
90
219
  export { MAX_LANE_DEPTH };
91
220
  export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
@@ -329,6 +458,7 @@ function resolveDeps(deps = {}) {
329
458
  fetchText: deps.fetchText ?? defaultFetchText,
330
459
  lanePlacementEnabled: deps.lanePlacementEnabled ?? defaultLanePlacementEnabled,
331
460
  storyLedgerEnabled: deps.storyLedgerEnabled ?? laneStoryLedgerEnabled,
461
+ focusGuardEnabled: deps.focusGuardEnabled,
332
462
  isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
333
463
  withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
334
464
  mergeReviewEnabled: deps.mergeReviewEnabled,
@@ -1726,11 +1856,13 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1726
1856
  const companyMatches = !hostlessUlidLookup || ownerLookupGate === true || (ownerLookupGate === "check-company" &&
1727
1857
  laneCompany !== undefined && laneCompany !== null &&
1728
1858
  await options.allowHostlessOwnerLookup(laneCompany));
1729
- if (lane.host.kind === "local" && ((!hostlessUlidLookup && lane.lane_id === laneId &&
1859
+ const exactOutpostOwnedMatch = !hostlessUlidLookup &&
1860
+ lane.lane_id === laneId && lane.host.kind === "outpost" && lane.host.id === outpostId;
1861
+ if (exactOutpostOwnedMatch || (lane.host.kind === "local" && ((!hostlessUlidLookup && lane.lane_id === laneId &&
1730
1862
  sanitizeHostComponent(lane.host.id) === hostSuffix) ||
1731
1863
  (hostlessUlidLookup && laneUlidPart(lane.lane_id) === requestedUlid &&
1732
1864
  sanitizeHostComponent(lane.host.id) === recordedSuffix &&
1733
- companyMatches))) {
1865
+ companyMatches)))) {
1734
1866
  matches.push({ outpostId, lane });
1735
1867
  }
1736
1868
  }
@@ -1784,7 +1916,7 @@ function commandLaneNotFound(laneId, outpostHint, details = {}) {
1784
1916
  const hint = outpostHint
1785
1917
  ? "; pass --outpost <id> to confirm a registered Outpost"
1786
1918
  : "";
1787
- return new LanesError("lane_not_found", `Lane ${laneId} was not found locally or on a registered Outpost with a matching host suffix${hint}`, { lane_id: laneId, ...details });
1919
+ return new LanesError("lane_not_found", `Could not resolve lane ${laneId}: no matching local record or reachable registered Outpost owner record was found${hint}`, { lane_id: laneId, ...details });
1788
1920
  }
1789
1921
  async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config, options = {}) {
1790
1922
  const localHost = laneHostFromCtx(ctx);
@@ -3033,6 +3165,12 @@ async function runCreate(ctx, opts) {
3033
3165
  if (opts.story !== undefined && opts.noStory !== undefined) {
3034
3166
  throw new LanesError("story_selection_conflict", "Pass either --story or --no-story, not both", {});
3035
3167
  }
3168
+ if (opts.focusOverride !== undefined) {
3169
+ if (!opts.focusOverride.trim()) {
3170
+ throw new LanesError("focus_override_reason_required", "--focus-override requires a non-empty reason", {});
3171
+ }
3172
+ assertNoSecretText("focus override reason", opts.focusOverride.trim());
3173
+ }
3036
3174
  if (opts.noStory !== undefined && !noStoryReason) {
3037
3175
  throw new LanesError("missing_no_story_reason", "--no-story requires a non-empty reason", {});
3038
3176
  }
@@ -3046,6 +3184,9 @@ async function runCreate(ctx, opts) {
3046
3184
  throw new LanesError("missing_story", "--additional-story values must be non-empty", {});
3047
3185
  }
3048
3186
  const storyIds = story ? [story, ...additionalStories] : [];
3187
+ if (opts.focusOverride !== undefined && storyIds.length === 0) {
3188
+ throw new LanesError("focus_override_story_required", "--focus-override requires --story", {});
3189
+ }
3049
3190
  if (new Set(storyIds).size !== storyIds.length) {
3050
3191
  throw new LanesError("duplicate_story", "A lane cannot carry the same project/story more than once", {
3051
3192
  project_id: project,
@@ -3113,6 +3254,15 @@ async function runCreate(ctx, opts) {
3113
3254
  if (storyIds.length > 0 && ctx.storyLedgerEnabled()) {
3114
3255
  validateProjectStories(hqRoot, project, storyIds, company?.slug);
3115
3256
  }
3257
+ const focusAssessment = storyIds.length > 0
3258
+ ? await focusGuardAssessment(ctx, {
3259
+ projectId: project,
3260
+ storyIds,
3261
+ company,
3262
+ checkTeamBlockers: true,
3263
+ overrideReason: opts.focusOverride,
3264
+ })
3265
+ : { enabled: false, reasons: [] };
3116
3266
  let meshCompany = company;
3117
3267
  if (meshCompanyFlag) {
3118
3268
  const resolvedMeshCompany = await ctx.resolveCompany(meshCompanyFlag);
@@ -3767,6 +3917,53 @@ async function runCreate(ctx, opts) {
3767
3917
  }
3768
3918
  let lane = persisted.lane;
3769
3919
  const { admission, validation } = persisted;
3920
+ if (focusAssessment.enabled && opts.focusOverride !== undefined) {
3921
+ const focusOverride = makeFocusOverride({
3922
+ reason: opts.focusOverride,
3923
+ laneId: lane.lane_id,
3924
+ at: ctx.now().toISOString(),
3925
+ storyId: story ?? "",
3926
+ });
3927
+ try {
3928
+ appendProjectFocusOverride(hqRoot, project, company?.slug, focusOverride);
3929
+ lane = casUpdateLane(hqRoot, lane.lane_id, (current) => ({
3930
+ ...appendLaneFocusOverride(current, focusOverride),
3931
+ }));
3932
+ }
3933
+ catch (error) {
3934
+ try {
3935
+ removeProjectFocusOverride(hqRoot, project, company?.slug, focusOverride);
3936
+ }
3937
+ catch (cleanupError) {
3938
+ process.stderr.write(`${JSON.stringify({
3939
+ event: "lanes.focus_guard.override_rollback_failed",
3940
+ lane_id: lane.lane_id,
3941
+ error: cleanupError instanceof Error ? cleanupError.name : "UnknownError",
3942
+ })}\n`);
3943
+ }
3944
+ try {
3945
+ casUpdateLane(hqRoot, lane.lane_id, (current) => ({
3946
+ ...current,
3947
+ state: "failed",
3948
+ queued_reason: undefined,
3949
+ queued_details: undefined,
3950
+ admission_reserved_at: undefined,
3951
+ terminal_reason: "focus_override_persist_failed",
3952
+ }));
3953
+ }
3954
+ catch (markError) {
3955
+ process.stderr.write(`${JSON.stringify({
3956
+ event: "lanes.focus_guard.lane_fail_mark_failed",
3957
+ lane_id: lane.lane_id,
3958
+ error: markError instanceof Error ? markError.name : "UnknownError",
3959
+ })}\n`);
3960
+ }
3961
+ throw new LanesError("focus_override_persist_failed", "Could not persist the focus override; lane launch was refused", {
3962
+ lane_id: lane.lane_id,
3963
+ error: error instanceof Error ? error.name : "UnknownError",
3964
+ });
3965
+ }
3966
+ }
3770
3967
  if (meshCompany) {
3771
3968
  try {
3772
3969
  lane = await ensureLaneThreadRecord(ctx, lane);
@@ -4521,6 +4718,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
4521
4718
  text: options.text,
4522
4719
  project: options.project,
4523
4720
  story: options.story,
4721
+ focusOverride: options.focusOverride,
4524
4722
  account: options.account,
4525
4723
  model: options.model,
4526
4724
  effort: options.effort,
@@ -4735,7 +4933,9 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
4735
4933
  if (action === "reparent") {
4736
4934
  for (const descendantId of changedDescendantIds) {
4737
4935
  const ownerDescendant = await readOutpostLaneSnapshot(ctx, outpostId, descendantId);
4738
- descendants.push(syncLaneRecordFromOwner(ctx.hqRoot(), descendantId, outpostId, ownerDescendant, undefined, { allowNonTerminal: true }));
4936
+ descendants.push(behavior.syncPrimary === false
4937
+ ? dispatchedOwnerLane(ctx, descendantId, outpostId, ownerDescendant, behavior)
4938
+ : syncLaneRecordFromOwner(ctx.hqRoot(), descendantId, outpostId, ownerDescendant, undefined, { allowNonTerminal: true }));
4739
4939
  }
4740
4940
  }
4741
4941
  const questionCancellation = action === "resume" && lane.state === "awaiting_input" && behavior.afterFallbackResume
@@ -5964,8 +6164,15 @@ async function printGrantResult(ctx, hqRoot, updated, hierarchy) {
5964
6164
  * next `set` or an explicit resume `--effort`/`--worker`.
5965
6165
  */
5966
6166
  async function runSet(ctx, laneId, opts) {
6167
+ if (opts.company !== undefined) {
6168
+ if (opts.model !== undefined || opts.effort !== undefined || opts.reason !== undefined) {
6169
+ throw new LanesError("lane_set_company_exclusive", "--company cannot be combined with --model, --effort, or --reason", { lane_id: laneId });
6170
+ }
6171
+ await runRepairCompany(ctx, laneId, { company: opts.company });
6172
+ return;
6173
+ }
5967
6174
  if (opts.model === undefined && opts.effort === undefined) {
5968
- throw new LanesError("lane_set_empty", "hq lanes set needs --model or --effort", { lane_id: laneId });
6175
+ throw new LanesError("lane_set_empty", "hq lanes set needs --company, --model, or --effort", { lane_id: laneId });
5969
6176
  }
5970
6177
  const reason = opts.reason?.trim();
5971
6178
  if (opts.reason !== undefined && !reason) {
@@ -7312,11 +7519,21 @@ function archiveResumeMessage(hqRoot, laneId, message) {
7312
7519
  fs.writeFileSync(path.join(dir, `${message.message_id}.resume.jsonl`), `${JSON.stringify(message)}\n`, "utf8");
7313
7520
  }
7314
7521
  async function runReparent(ctx, laneId, opts) {
7315
- const hqRoot = ctx.hqRoot();
7316
7522
  if (!opts.senior) {
7317
7523
  throw new LanesError("missing_senior", "--senior is required", {});
7318
7524
  }
7319
- const initial = readLane(hqRoot, laneId);
7525
+ const target = await resolveCommandLaneTarget(ctx, laneId);
7526
+ const initial = target.lane;
7527
+ if (target.remoteOnly) {
7528
+ await dispatchOutpostLaneCommand(ctx, initial, "reparent", {
7529
+ senior: opts.senior,
7530
+ reason: opts.reason,
7531
+ }, {
7532
+ syncPrimary: false,
7533
+ expectedOwnerHost: target.ownerHost,
7534
+ });
7535
+ return;
7536
+ }
7320
7537
  if (initial.host.kind === "outpost" && !laneRunsHere(initial, reconcileHostFromCtx(ctx))) {
7321
7538
  await dispatchOutpostLaneCommand(ctx, initial, "reparent", {
7322
7539
  senior: opts.senior,
@@ -7324,7 +7541,7 @@ async function runReparent(ctx, laneId, opts) {
7324
7541
  });
7325
7542
  return;
7326
7543
  }
7327
- const result = await reparentOnOwner(ctx, laneId, opts);
7544
+ const result = await reparentOnOwner(ctx, initial.lane_id, opts);
7328
7545
  printJson({
7329
7546
  ok: true,
7330
7547
  action: "reparent",
@@ -7579,7 +7796,7 @@ async function repairCompanyOnOwner(ctx, laneId, opts, emitResult = true) {
7579
7796
  if (current.company !== null) {
7580
7797
  if (sameLaneCompany(current.company, company))
7581
7798
  return current;
7582
- throw new LanesError("lane_company_already_set", `Lane ${laneId} already belongs to ${current.company.slug}; company repair only fills missing attribution`, { lane_id: laneId, company: current.company.slug, requested_company: company.slug });
7799
+ throw new LanesError("lane_company_already_set", `Cannot set lane ${laneId} to ${company.slug}: it already belongs to ${current.company.slug}`, { lane_id: laneId, company: current.company.slug, requested_company: company.slug });
7583
7800
  }
7584
7801
  return {
7585
7802
  ...current,
@@ -7935,7 +8152,7 @@ async function dispatchPrimaryLaneMessage(ctx, laneId, options) {
7935
8152
  printJson(response);
7936
8153
  return response;
7937
8154
  }
7938
- async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
8155
+ async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false, behavior = {}) {
7939
8156
  if (lane.host.kind !== "outpost") {
7940
8157
  throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch stop to`, { lane_id: lane.lane_id });
7941
8158
  }
@@ -7967,6 +8184,14 @@ async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
7967
8184
  const commandId = dispatched.commandId?.trim();
7968
8185
  const ownerError = ownerErrorFromOutput(dispatched.stdout);
7969
8186
  if (isDefiniteOutpostLaneNotFound(dispatched, lane.lane_id, ownerError)) {
8187
+ if (behavior.syncPrimary === false) {
8188
+ throw new LanesError("lane_not_found", ownerError.message, {
8189
+ ...ownerError.details,
8190
+ lane_id: lane.lane_id,
8191
+ outpost_id: outpostId,
8192
+ command_id: commandId,
8193
+ });
8194
+ }
7970
8195
  const at = ctx.now().toISOString();
7971
8196
  const updated = markRemoteLaneLost(ctx.hqRoot(), lane.lane_id, outpostId, at, "command", reason);
7972
8197
  if (updated.terminal_reason !== "remote_lost") {
@@ -8081,7 +8306,9 @@ async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
8081
8306
  state = ownerLane.state;
8082
8307
  cancelledQuestions = parseRemoteCancelledQuestionIds(remote.cancelled_questions, lane.lane_id, outpostId, commandId);
8083
8308
  }
8084
- const updated = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, ownerLane);
8309
+ const updated = behavior.syncPrimary === false
8310
+ ? ownerLane
8311
+ : syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, ownerLane);
8085
8312
  if (!quiet)
8086
8313
  printJson({
8087
8314
  ok: true,
@@ -8109,9 +8336,14 @@ async function runStop(ctx, laneId, reason, quiet = false) {
8109
8336
  if (operatorReason !== undefined)
8110
8337
  assertNoSecretText("stop reason", operatorReason);
8111
8338
  const hqRoot = ctx.hqRoot();
8112
- const lane = readLane(hqRoot, laneId);
8339
+ const target = await resolveCommandLaneTarget(ctx, laneId);
8340
+ const lane = target.lane;
8113
8341
  if (!quiet && lane.state !== "queued" && !isTerminalLaneState(lane.state)) {
8114
- process.stderr.write(stopTerminalWarning(laneId));
8342
+ process.stderr.write(stopTerminalWarning(lane.lane_id));
8343
+ }
8344
+ if (target.remoteOnly) {
8345
+ await dispatchOutpostLaneStop(ctx, lane, operatorReason, quiet, { syncPrimary: false });
8346
+ return;
8115
8347
  }
8116
8348
  if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
8117
8349
  await dispatchOutpostLaneStop(ctx, lane, operatorReason, quiet);
@@ -8885,6 +9117,7 @@ export function registerLanesCommand(parent, deps = {}) {
8885
9117
  casUpdateLane,
8886
9118
  dispatchOutpostLaneCommand,
8887
9119
  dispatchPrimaryLaneMessage,
9120
+ focusGuardAssessment,
8888
9121
  drainHookStatusFor,
8889
9122
  laneIsHostedHere,
8890
9123
  laneWithRecoveredProviderSession,
@@ -8916,6 +9149,7 @@ export function registerLanesCommand(parent, deps = {}) {
8916
9149
  automaticBalanceMoveHistoryReason,
8917
9150
  authorizeFailedResume,
8918
9151
  authorizeResumeProfileRepin,
9152
+ focusGuardAssessment,
8919
9153
  casUpdateLane,
8920
9154
  dispatchOutpostLaneCommand: async (commandCtx, lane, action, options, behavior) => {
8921
9155
  await dispatchOutpostLaneCommand(commandCtx, lane, action, options, behavior);
@@ -9031,6 +9265,7 @@ export function registerLanesCommand(parent, deps = {}) {
9031
9265
  .option("--mesh-company <slug|uid>", "Explicit Work Mesh company for a personal lane; membership is verified")
9032
9266
  .requiredOption("--project <slug>", "HQ project slug")
9033
9267
  .option("--story <id>", "Story id from the project's prd.json")
9268
+ .option("--focus-override <reason>", "Human-approved reason to override the lane focus guard")
9034
9269
  .addOption(noStoryOption)
9035
9270
  .option("--additional-story <id>", "Additional story carried by this lane; repeatable and discouraged when active", collectRepeatable)
9036
9271
  .requiredOption("--brief-file <path>", "Path to the lane brief")
@@ -9093,8 +9328,9 @@ export function registerLanesCommand(parent, deps = {}) {
9093
9328
  }));
9094
9329
  lanes
9095
9330
  .command("set")
9096
- .description("Change the model or effort a lane uses from its next resume")
9331
+ .description("Change lane settings or bind its missing company attribution")
9097
9332
  .argument("<lane-id>", "Lane id")
9333
+ .option("--company <slug>", "Bind missing company attribution to this lane")
9098
9334
  .option("--model <model>", "Model for the lane's next and later turns")
9099
9335
  .option("--effort <level>", "Effort for the lane's next and later turns")
9100
9336
  .option("--reason <text>", "Why the settings changed; recorded in settings_history")
@@ -9305,6 +9541,7 @@ export function registerLanesCommand(parent, deps = {}) {
9305
9541
  .option("--text-file <path>", "Read message text from a file")
9306
9542
  .option("--project <slug>", "Re-point project attribution; a changed story is named in the resumed prompt")
9307
9543
  .option("--story <id>", "Re-point story attribution; a changed story is named in the resumed prompt")
9544
+ .option("--focus-override <reason>", "Human-approved reason to override the lane focus guard")
9308
9545
  .option("--account <id>", "Move the lane's session to this account of its provider before the message resumes it")
9309
9546
  .option("--model <id>", "Model for the moved lane (with --account only)")
9310
9547
  .option("--effort <level>", "Reasoning effort for the resumed provider turn")
@@ -9361,6 +9598,7 @@ export function registerLanesCommand(parent, deps = {}) {
9361
9598
  .option("--reason <text>", "Required explanation for resuming from failed")
9362
9599
  .option("--project <slug>", "Re-point project attribution; a changed story is named in the resumed prompt")
9363
9600
  .option("--story <id>", "Re-point story attribution; a changed story is named in the resumed prompt")
9601
+ .option("--focus-override <reason>", "Human-approved reason to override the lane focus guard")
9364
9602
  .option("--account <id>", "Move the lane's session to this account of its provider before resuming")
9365
9603
  .option("--model <id>", "Model for the moved lane (with --account only)")
9366
9604
  .option("--effort <level>", "Reasoning effort for the resumed provider turn")
@@ -520,7 +520,10 @@ export function registerMeetingsCommand(program) {
520
520
  }
521
521
  const token = await ensureCognitoToken();
522
522
  const companyId = await getCompanyUid(token, companySlug);
523
- const recorderPersonUid = await resolveCallerPersonUid(token);
523
+ // The import endpoint records a person as the recorder (prs_* only).
524
+ const recorderPersonUid = await resolveCallerPersonUid(token, undefined, {
525
+ command: "hq meetings import",
526
+ });
524
527
  const res = await vaultApiFetch({
525
528
  token,
526
529
  method: "POST",
@@ -10,6 +10,17 @@ export interface ReadHqAnywherePersonSettingOptions {
10
10
  }
11
11
  /** Returns null when the person preference cannot be read; callers must fail closed. */
12
12
  export declare function readHqAnywherePersonSetting(options?: ReadHqAnywherePersonSettingOptions): Promise<boolean | null>;
13
+ /**
14
+ * Wrap a setting reader so a failed read (null) returns the last successfully
15
+ * read value for `graceMs`. A long-running caller such as hqd re-reads every
16
+ * minute, and one slow request would otherwise switch HQ Anywhere off for
17
+ * every session. A newly read value, including off, always applies at once;
18
+ * with no successful read inside the window the result stays null (closed).
19
+ */
20
+ export declare function stickyPersonSettingReader(read: () => Promise<boolean | null>, options: {
21
+ graceMs: number;
22
+ now?: () => number;
23
+ }): () => Promise<boolean | null>;
13
24
  export declare function evaluateAnywhereRuntime(flagEnabled: boolean, setting: boolean | null): {
14
25
  enabled: boolean;
15
26
  setting: boolean | null;
@@ -28,6 +28,25 @@ export async function readHqAnywherePersonSetting(options = {}) {
28
28
  return null;
29
29
  }
30
30
  }
31
+ /**
32
+ * Wrap a setting reader so a failed read (null) returns the last successfully
33
+ * read value for `graceMs`. A long-running caller such as hqd re-reads every
34
+ * minute, and one slow request would otherwise switch HQ Anywhere off for
35
+ * every session. A newly read value, including off, always applies at once;
36
+ * with no successful read inside the window the result stays null (closed).
37
+ */
38
+ export function stickyPersonSettingReader(read, options) {
39
+ const now = options.now ?? Date.now;
40
+ let last = null;
41
+ return async () => {
42
+ const value = await read();
43
+ if (value !== null) {
44
+ last = { value, at: now() };
45
+ return value;
46
+ }
47
+ return last && now() - last.at <= options.graceMs ? last.value : null;
48
+ };
49
+ }
31
50
  export function evaluateAnywhereRuntime(flagEnabled, setting) {
32
51
  return { enabled: flagEnabled && setting === true, setting };
33
52
  }
@@ -13,7 +13,16 @@ export interface McpToolResult {
13
13
  }
14
14
  export declare function jsonResult(value: unknown): McpToolResult;
15
15
  export declare function resolveHqMcpEndpoint(env?: NodeJS.ProcessEnv): URL;
16
- export declare function defaultMcpToken(): Promise<string>;
16
+ /** Test seams for {@link defaultMcpToken}. */
17
+ export interface McpTokenDeps {
18
+ tokenSource?: () => string;
19
+ mcpAudienceToken?: () => Promise<string>;
20
+ cognitoToken?: (options: {
21
+ tokenSource: "person" | "machine";
22
+ interactive: false;
23
+ }) => Promise<string>;
24
+ }
25
+ export declare function defaultMcpToken(deps?: McpTokenDeps): Promise<string>;
17
26
  export interface HqMcpBridgeOptions {
18
27
  endpoint?: URL;
19
28
  env?: NodeJS.ProcessEnv;
@@ -27,24 +27,31 @@ export function resolveHqMcpEndpoint(env = process.env) {
27
27
  throw error;
28
28
  }
29
29
  }
30
- export async function defaultMcpToken() {
31
- const { ensureCognitoToken, resolveCognitoTokenSource } = await import("../../../utils/cognito-session.js");
32
- if (resolveCognitoTokenSource() !== "machine") {
33
- const { getMcpAudienceToken, McpOAuthError } = await import("../mcp-oauth.js");
30
+ export async function defaultMcpToken(deps = {}) {
31
+ const session = deps.tokenSource && deps.cognitoToken ? null : await import("../../../utils/cognito-session.js");
32
+ const tokenSource = deps.tokenSource ?? session.resolveCognitoTokenSource;
33
+ const cognitoToken = deps.cognitoToken ?? ((options) => session.ensureCognitoToken(options));
34
+ if (tokenSource() !== "machine") {
35
+ const mcpAudienceToken = deps.mcpAudienceToken ?? (async () => (await import("../mcp-oauth.js")).getMcpAudienceToken());
34
36
  try {
35
- return await getMcpAudienceToken();
37
+ return await mcpAudienceToken();
36
38
  }
37
- catch (error) {
38
- if (error instanceof McpOAuthError)
39
- throw new McpOpError(error.code, error.message);
40
- throw new McpOpError("hq_mcp_auth_required", "Hosted MCP sign-in is required. Run `hq mcp login` and retry.");
39
+ catch {
40
+ // No `hq mcp login`: hosted hq-mcp also accepts the HQ sign-in client,
41
+ // so reuse the session hq-cli and the desktop app already hold.
42
+ }
43
+ try {
44
+ return await cognitoToken({ tokenSource: "person", interactive: false });
45
+ }
46
+ catch {
47
+ throw new McpOpError("hq_mcp_auth_required", "Sign in to HQ with `hq login`, then retry.");
41
48
  }
42
49
  }
43
50
  try {
44
51
  // Send this box's existing machine token. The hosted hq-mcp audience check
45
52
  // accepts it only when it was issued for hqMcpMachineClient; otherwise the
46
53
  // server rejects it. This helper does not inspect the credential's client ID.
47
- return await ensureCognitoToken({ tokenSource: "machine", interactive: false });
54
+ return await cognitoToken({ tokenSource: "machine", interactive: false });
48
55
  }
49
56
  catch {
50
57
  throw new McpOpError("hq_mcp_auth_required", "An hq-mcp machine token is unavailable. Configure this box with hq-pro's hqMcpMachineClient ID, then retry the daemon call.");
@@ -14,7 +14,7 @@ import { fileURLToPath } from "node:url";
14
14
  import { loadCachedTokens } from "@indigoai-us/hq-cloud";
15
15
  import { CLI_VERSION } from "../../cli-version.js";
16
16
  import { anywhereRuntimeEnabled } from "../../commands/anywhere-runtime-flag.js";
17
- import { anywhereRuntimeAllowed } from "../anywhere-person-setting.js";
17
+ import { anywhereRuntimeAllowed, readHqAnywherePersonSetting, stickyPersonSettingReader } from "../anywhere-person-setting.js";
18
18
  import { currentPackageUseLeaseInstall, currentPackageUseUpdateRequested } from "../../utils/package-use-lease.js";
19
19
  import { isLocalDependencyInstall, resolveRunningInstall } from "../../utils/version-gate.js";
20
20
  import { agentKitPaths } from "../agent-kit/paths.js";
@@ -77,6 +77,8 @@ export function handoffForPackageUpdateRequest(requested, restartDaemon) {
77
77
  const LEGACY_PROBE_TTL_MS = 60_000;
78
78
  /** A flag read that has not answered by then counts as off. */
79
79
  const HANDOFF_FLAG_TIMEOUT_MS = 10_000;
80
+ /** How long hqd keeps the last read HQ Anywhere setting when a re-read fails. */
81
+ const HQD_PERSON_SETTING_GRACE_MS = 30 * 60_000;
80
82
  export function syncRunnerSourceForPlan(stagedRunnerPath, hqCloud, legacyBundledRunnerPath = null, staged = { version: null }, currentSyncRunnerSource = null) {
81
83
  // A managed copy lives in the daemon directory, so it survives a CLI
82
84
  // replacement. Before lease release pickHqCloud returns it only when it is
@@ -838,13 +840,17 @@ export async function runDaemon(opts) {
838
840
  // hqd: the session/company socket every runtime talks to (hq-anywhere US-008).
839
841
  let hqdShutdown = null;
840
842
  let stopHeartbeat = () => { };
843
+ const readPersonSetting = stickyPersonSettingReader(() => readHqAnywherePersonSetting({
844
+ env,
845
+ onError: (name) => log("warn", `hqd person setting read failed (${name}); keeping the last read value for up to ${HQD_PERSON_SETTING_GRACE_MS / 60_000} minutes`),
846
+ }), { graceMs: HQD_PERSON_SETTING_GRACE_MS });
841
847
  const hqdListener = await startHqdRuntimeListener({
842
848
  isEnabled: async () => shouldStartHqd(await anywhereRuntimeAllowed(env, {
843
849
  refreshFlag: async () => {
844
850
  const { refreshFlagRegistrySnapshot } = await import("../flag-registry.js");
845
851
  await refreshFlagRegistrySnapshot(5_000);
846
852
  },
847
- onError: (name) => log("warn", `hqd person setting read failed (${name}); runtime remains off`),
853
+ readSetting: readPersonSetting,
848
854
  })),
849
855
  startServer: async () => {
850
856
  const startConfig = readDaemonConfig(paths.configPath);
@@ -1,5 +1,7 @@
1
1
  import type { CheckResult } from "../types.js";
2
2
  export declare const SYNC_RUNTIME_WINDOW_MS: number;
3
+ /** Ignore timing and restart signals until a newly started receiver has settled. */
4
+ export declare const SYNC_RUNTIME_RESTART_WARMUP_MS: number;
3
5
  export interface SyncRuntimeOptions {
4
6
  now: number;
5
7
  conflictCount?: number;