@indigoai-us/hq-cli 5.328.0 → 5.330.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/command-catalog.generated.d.ts +3699 -3
  3. package/dist/command-catalog.generated.js +5462 -1759
  4. package/dist/command-help.d.ts +2 -0
  5. package/dist/command-help.js +13 -0
  6. package/dist/commands/cloud.d.ts +5 -0
  7. package/dist/commands/cloud.js +51 -1
  8. package/dist/commands/install-global.d.ts +21 -0
  9. package/dist/commands/install-global.js +267 -32
  10. package/dist/commands/lanes-message.js +59 -6
  11. package/dist/commands/lanes-resume.js +66 -15
  12. package/dist/commands/lanes-shared.d.ts +9 -2
  13. package/dist/commands/lanes.js +97 -8
  14. package/dist/commands/mcp-status.js +58 -1
  15. package/dist/commands/pack-install.d.ts +3 -1
  16. package/dist/commands/pack-install.js +17 -4
  17. package/dist/commands/packs.js +17 -0
  18. package/dist/commands/pkg-install.js +13 -1
  19. package/dist/fast-scaffold.d.ts +2 -0
  20. package/dist/fast-scaffold.js +16 -0
  21. package/dist/index.js +19 -2
  22. package/dist/lazy-commands.d.ts +1 -0
  23. package/dist/lazy-commands.js +64 -4
  24. package/dist/lib/cli-flag-keys.d.ts +3 -0
  25. package/dist/lib/cli-flag-keys.js +3 -0
  26. package/dist/lib/daemon/ops/mcp.js +11 -10
  27. package/dist/lib/daemon/ops/policy.d.ts +3 -2
  28. package/dist/lib/daemon/ops/policy.js +12 -6
  29. package/dist/lib/daemon/server.d.ts +3 -1
  30. package/dist/lib/daemon/server.js +63 -7
  31. package/dist/lib/doctor/checks/anywhere.js +58 -4
  32. package/dist/lib/install/chatgpt-targets.d.ts +8 -0
  33. package/dist/lib/install/chatgpt-targets.js +15 -0
  34. package/dist/lib/install/codex-pack-contributions.d.ts +8 -0
  35. package/dist/lib/install/codex-pack-contributions.js +205 -0
  36. package/dist/lib/install/codex-targets.d.ts +1 -1
  37. package/dist/lib/install/codex-targets.js +2 -1
  38. package/dist/lib/install/grok-targets.d.ts +8 -0
  39. package/dist/lib/install/grok-targets.js +15 -0
  40. package/dist/lib/lanes/flag-keys.d.ts +2 -0
  41. package/dist/lib/lanes/flag-keys.js +2 -0
  42. package/dist/lib/lanes/keep-alive-audit.d.ts +3 -2
  43. package/dist/lib/lanes/keep-alive-audit.js +36 -8
  44. package/dist/lib/lanes/spawn.js +14 -10
  45. package/dist/lib/lanes/store.js +29 -2
  46. package/dist/lib/lanes/types.d.ts +10 -0
  47. package/dist/lib/lanes/types.js +1 -1
  48. package/dist/lib/sync-invalid-entity-warning.d.ts +32 -0
  49. package/dist/lib/sync-invalid-entity-warning.js +205 -0
  50. package/dist/main.d.ts +2 -6
  51. package/dist/main.js +3 -41
  52. package/dist/stream-error-boundary.d.ts +7 -0
  53. package/dist/stream-error-boundary.js +33 -0
  54. package/package.json +1 -1
@@ -297,6 +297,25 @@ export function createLaneMessageHandlers(deps) {
297
297
  return;
298
298
  }
299
299
  assertLaneRunsHereForOperatorMutation(ctx, lane, "message");
300
+ const projectId = opts.project?.trim() || lane.project_id;
301
+ const storyId = opts.story?.trim() || lane.story_id;
302
+ if (opts.story !== undefined && !opts.story.trim()) {
303
+ throw new LanesError("missing_story", "--story must be non-empty", {});
304
+ }
305
+ const refreshProfileSettings = lane.state === "done" &&
306
+ (projectId !== lane.project_id || storyId !== lane.story_id) &&
307
+ await deps.reuseProfileRefreshEnabled(ctx, lane);
308
+ const refreshWorker = opts.worker ?? (refreshProfileSettings ? lane.worker_profile : undefined);
309
+ let resumeProfile = refreshProfileSettings && refreshWorker !== undefined
310
+ ? resolveResumeProfile(ctx, lane, refreshWorker, true)
311
+ : undefined;
312
+ if (resumeProfile !== undefined) {
313
+ const providerPinned = lane.provider_source === undefined || lane.provider_source === "cli";
314
+ const effectiveProvider = providerPinned ? lane.provider : resumeProfile.profile.meta.provider;
315
+ if (effectiveProvider !== lane.provider) {
316
+ throw new LanesError("profile_provider_changed", `Worker profile now selects ${effectiveProvider}; this lane runs ${lane.provider}; create a new lane`, { lane_id: lane.lane_id, lane_provider: lane.provider, profile_provider: effectiveProvider });
317
+ }
318
+ }
300
319
  if (lane.state === "running") {
301
320
  const workerPidPath = path.join(laneRunDir(hqRoot, laneId), "worker.pid");
302
321
  const workerRecordExists = fs.existsSync(workerPidPath);
@@ -413,8 +432,8 @@ export function createLaneMessageHandlers(deps) {
413
432
  if (cliEffort !== undefined && mechanism === "dropbox") {
414
433
  throw new LanesError("lane_effort_requires_resume", `Lane ${laneId} is still running; --effort can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
415
434
  }
416
- const resumeProfile = opts.worker !== undefined
417
- ? resolveResumeProfile(ctx, lane, opts.worker)
435
+ resumeProfile ??= refreshWorker !== undefined
436
+ ? resolveResumeProfile(ctx, lane, refreshWorker, refreshProfileSettings)
418
437
  : undefined;
419
438
  if (opts.model !== undefined && opts.account === undefined) {
420
439
  throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
@@ -425,6 +444,7 @@ export function createLaneMessageHandlers(deps) {
425
444
  try {
426
445
  automaticAccountSelection = await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile, {
427
446
  tolerateInvalidHostLaneLists: lane.host.kind === "local",
447
+ ...(refreshProfileSettings ? { refreshProfileSettings: true } : {}),
428
448
  });
429
449
  }
430
450
  catch (err) {
@@ -446,7 +466,15 @@ export function createLaneMessageHandlers(deps) {
446
466
  automaticAccountSelection = undefined;
447
467
  }
448
468
  }
449
- const resumeEffort = resolveResumeEffort(ctx, lane, {
469
+ const profileRefreshAccount = refreshProfileSettings && resumeProfile !== undefined &&
470
+ lane.account_source !== undefined && lane.account_source !== "cli" &&
471
+ resumeProfile.profile.meta.provider === lane.provider
472
+ ? resumeProfile.profile.meta.account ?? (resumeProfile.profile.meta.accounts !== undefined &&
473
+ !resumeProfile.profile.meta.accounts.includes(lane.account ?? "")
474
+ ? resumeProfile.profile.meta.accounts[0]
475
+ : undefined)
476
+ : undefined;
477
+ const resumeEffort = refreshProfileSettings && cliEffort === undefined ? undefined : resolveResumeEffort(ctx, lane, {
450
478
  cliEffort,
451
479
  profile: resumeProfile,
452
480
  account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
@@ -454,8 +482,6 @@ export function createLaneMessageHandlers(deps) {
454
482
  });
455
483
  const messageId = mintMessageId();
456
484
  const at = ctx.now().toISOString();
457
- const projectId = opts.project?.trim() || lane.project_id;
458
- const storyId = opts.story?.trim() || lane.story_id;
459
485
  if (opts.story !== undefined && !opts.story.trim()) {
460
486
  throw new LanesError("missing_story", "--story must be non-empty", {});
461
487
  }
@@ -589,6 +615,7 @@ export function createLaneMessageHandlers(deps) {
589
615
  account: opts.account,
590
616
  cliModel: opts.model?.trim() || undefined,
591
617
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
618
+ ...(refreshProfileSettings ? { refreshProfileSettings: true, accountSource: "cli" } : {}),
592
619
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
593
620
  ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
594
621
  pending: {
@@ -616,6 +643,7 @@ export function createLaneMessageHandlers(deps) {
616
643
  cliModel: undefined,
617
644
  ...(automatic.historyReason !== undefined ? { accountHistoryReason: automatic.historyReason } : {}),
618
645
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
646
+ ...(refreshProfileSettings ? { refreshProfileSettings: true, accountSource: "balanced" } : {}),
619
647
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
620
648
  ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
621
649
  pending: {
@@ -638,13 +666,38 @@ export function createLaneMessageHandlers(deps) {
638
666
  lane = persistClaudeAccountSelection(hqRoot, lane, selection);
639
667
  }
640
668
  }
669
+ if (move === null && profileRefreshAccount !== undefined && profileRefreshAccount !== lane.account) {
670
+ move = await prepareAccountMove(ctx, lane, {
671
+ account: profileRefreshAccount,
672
+ cliModel: undefined,
673
+ workerProfile: resumeProfile,
674
+ refreshProfileSettings: true,
675
+ accountSource: "profile",
676
+ ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
677
+ ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
678
+ pending: {
679
+ trigger: messageHistoryTrigger,
680
+ text: deliveryText,
681
+ ...(isMonitorEvent ? { monitor_event: true } : {}),
682
+ message_id: messageId,
683
+ compact: opts.compact !== false,
684
+ },
685
+ });
686
+ lane = move.lane;
687
+ if (move.queued) {
688
+ storySwitchDeliveryStarted = true;
689
+ deps.onLaneMessageQueued?.(laneId, "account_move");
690
+ archiveResumeMessage(hqRoot, laneId, dropboxMessage);
691
+ }
692
+ }
641
693
  }
642
- if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
694
+ if (resumeProfile !== undefined && !refreshProfileSettings && (opts.account === undefined || keepCurrentAccount)) {
643
695
  validateResumeProfileModel(ctx, lane, resumeProfile.profile);
644
696
  }
645
697
  if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined || (keepCurrentAccount && opts.model !== undefined))) {
646
698
  lane = recordResumeSetup(ctx, lane, {
647
699
  ...(resumeProfile !== undefined ? { profile: resumeProfile } : {}),
700
+ ...(refreshProfileSettings ? { refreshProfileSettings: true } : {}),
648
701
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
649
702
  ...(keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
650
703
  });
@@ -185,6 +185,28 @@ export function createLaneResumeHandlers(deps) {
185
185
  const pendingQuestion = lane.state === "awaiting_input"
186
186
  ? await deps.hasPendingQuestionForLane(ctx, lane)
187
187
  : false;
188
+ const projectId = opts.project?.trim() || lane.project_id;
189
+ const storyId = opts.story?.trim() || lane.story_id;
190
+ if (opts.story !== undefined && !opts.story.trim()) {
191
+ throw new LanesError("missing_story", "--story must be non-empty", {});
192
+ }
193
+ if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
194
+ validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
195
+ }
196
+ const attributionChanged = projectId !== lane.project_id || storyId !== lane.story_id;
197
+ const refreshProfileSettings = lane.state === "done" && attributionChanged &&
198
+ await deps.reuseProfileRefreshEnabled(ctx, lane);
199
+ const refreshWorker = opts.worker ?? (refreshProfileSettings ? lane.worker_profile : undefined);
200
+ let resumeProfile = refreshProfileSettings && refreshWorker !== undefined
201
+ ? resolveResumeProfile(ctx, lane, refreshWorker, true)
202
+ : undefined;
203
+ if (resumeProfile !== undefined) {
204
+ const providerPinned = lane.provider_source === undefined || lane.provider_source === "cli";
205
+ const effectiveProvider = providerPinned ? lane.provider : resumeProfile.profile.meta.provider;
206
+ if (effectiveProvider !== lane.provider) {
207
+ throw new LanesError("profile_provider_changed", `Worker profile now selects ${effectiveProvider}; this lane runs ${lane.provider}; create a new lane`, { lane_id: lane.lane_id, lane_provider: lane.provider, profile_provider: effectiveProvider });
208
+ }
209
+ }
188
210
  const refusal = queuedProfileRecovery || (lane.state === "awaiting_input" && !pendingQuestion)
189
211
  ? null
190
212
  : resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true });
@@ -200,15 +222,6 @@ export function createLaneResumeHandlers(deps) {
200
222
  const resumedFrom = lane.state;
201
223
  const text = opts.text?.trim() ? opts.text : undefined;
202
224
  const at = ctx.now().toISOString();
203
- const projectId = opts.project?.trim() || lane.project_id;
204
- const storyId = opts.story?.trim() || lane.story_id;
205
- if (opts.story !== undefined && !opts.story.trim()) {
206
- throw new LanesError("missing_story", "--story must be non-empty", {});
207
- }
208
- if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
209
- validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
210
- }
211
- const attributionChanged = projectId !== lane.project_id || storyId !== lane.story_id;
212
225
  const messageId = text !== undefined || attributionChanged ? mintMessageId() : undefined;
213
226
  const notice = storySwitchNotice(lane.project_id, lane.story_id, projectId, storyId);
214
227
  const deliveryText = notice === undefined ? text : `${notice}${text === undefined ? "" : `\n\n${text}`}`;
@@ -226,8 +239,8 @@ export function createLaneResumeHandlers(deps) {
226
239
  }
227
240
  meshThreadId = await ctx.mesh.ensureProjectThread(requestedMeshCompany, projectId);
228
241
  }
229
- const resumeProfile = opts.worker !== undefined
230
- ? resolveResumeProfile(ctx, lane, opts.worker)
242
+ resumeProfile ??= refreshWorker !== undefined
243
+ ? resolveResumeProfile(ctx, lane, refreshWorker, refreshProfileSettings)
231
244
  : undefined;
232
245
  const profileRepin = authorizeResumeProfileRepin(ctx, lane, resumeProfile, opts.profileRepinContext);
233
246
  const keepAlive = opts.keepAlive === true
@@ -236,7 +249,7 @@ export function createLaneResumeHandlers(deps) {
236
249
  const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
237
250
  if (keepCurrentAccount)
238
251
  assertCurrentResumeAccountAvailable(ctx, lane);
239
- if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
252
+ if (resumeProfile !== undefined && !refreshProfileSettings && (opts.account === undefined || keepCurrentAccount)) {
240
253
  validateResumeProfileModel(ctx, lane, resumeProfile.profile);
241
254
  }
242
255
  if (lane.state === "queued" && lane.queued_reason === "worker_profile_changed") {
@@ -262,9 +275,20 @@ export function createLaneResumeHandlers(deps) {
262
275
  return;
263
276
  }
264
277
  const automaticAccountSelection = opts.account === undefined
265
- ? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile, { repairUnavailableAccount: true })
278
+ ? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile, {
279
+ repairUnavailableAccount: true,
280
+ ...(refreshProfileSettings ? { refreshProfileSettings: true } : {}),
281
+ })
282
+ : undefined;
283
+ const profileRefreshAccount = refreshProfileSettings && resumeProfile !== undefined &&
284
+ lane.account_source !== undefined && lane.account_source !== "cli" &&
285
+ resumeProfile.profile.meta.provider === lane.provider
286
+ ? resumeProfile.profile.meta.account ?? (resumeProfile.profile.meta.accounts !== undefined &&
287
+ !resumeProfile.profile.meta.accounts.includes(lane.account ?? "")
288
+ ? resumeProfile.profile.meta.accounts[0]
289
+ : undefined)
266
290
  : undefined;
267
- const resumeEffort = resolveResumeEffort(ctx, lane, {
291
+ const resumeEffort = refreshProfileSettings && cliEffort === undefined ? undefined : resolveResumeEffort(ctx, lane, {
268
292
  cliEffort,
269
293
  profile: resumeProfile,
270
294
  account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
@@ -325,6 +349,7 @@ export function createLaneResumeHandlers(deps) {
325
349
  account: opts.account,
326
350
  cliModel: opts.model?.trim() || undefined,
327
351
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
352
+ ...(refreshProfileSettings ? { refreshProfileSettings: true, accountSource: "cli" } : {}),
328
353
  ...(profileRepin !== undefined ? { profileRepin } : {}),
329
354
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
330
355
  ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
@@ -355,6 +380,7 @@ export function createLaneResumeHandlers(deps) {
355
380
  ? { preservedModel: automaticAccountSelection.preservedModel }
356
381
  : {}),
357
382
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
383
+ ...(refreshProfileSettings ? { refreshProfileSettings: true, accountSource: "balanced" } : {}),
358
384
  ...(profileRepin !== undefined ? { profileRepin } : {}),
359
385
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
360
386
  ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
@@ -377,8 +403,33 @@ export function createLaneResumeHandlers(deps) {
377
403
  lane = persistClaudeAccountSelection(hqRoot, lane, selection);
378
404
  }
379
405
  }
406
+ else if (profileRefreshAccount !== undefined && profileRefreshAccount !== lane.account) {
407
+ move = await prepareAccountMove(ctx, lane, {
408
+ account: profileRefreshAccount,
409
+ cliModel: undefined,
410
+ workerProfile: resumeProfile,
411
+ refreshProfileSettings: true,
412
+ accountSource: "profile",
413
+ ...(profileRepin !== undefined ? { profileRepin } : {}),
414
+ ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
415
+ ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
416
+ pending: {
417
+ trigger: "resume",
418
+ resumed_from: resumedFrom,
419
+ ...(opts.fromFailed === true ? { from_failed: true } : {}),
420
+ ...(failedResumeExpectedSenior !== undefined ? { expected_senior: failedResumeExpectedSenior } : {}),
421
+ ...(deliveryText !== undefined ? { text: deliveryText } : {}),
422
+ ...(messageId !== undefined ? { message_id: messageId } : {}),
423
+ compact: opts.compact !== false,
424
+ },
425
+ });
426
+ lane = move.lane;
427
+ }
380
428
  lane = recordResumeSetup(ctx, lane, {
381
- ...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
429
+ ...(move === null && resumeProfile !== undefined ? {
430
+ profile: resumeProfile,
431
+ ...(refreshProfileSettings ? { refreshProfileSettings: true } : {}),
432
+ } : {}),
382
433
  ...(move === null && profileRepin !== undefined ? { profileRepin } : {}),
383
434
  ...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
384
435
  ...(keepAlive !== undefined ? { keepAlive } : {}),
@@ -107,6 +107,10 @@ export interface PrepareAccountMoveInput {
107
107
  /** Copy the session and change account while preserving the between-turn state. */
108
108
  deferTurnStart?: boolean;
109
109
  accountSelection?: LaneAccountSelection;
110
+ /** Apply current profile settings during a done-lane story reuse that also moves accounts. */
111
+ refreshProfileSettings?: boolean;
112
+ /** Provenance recorded for the account this move selects. */
113
+ accountSource?: "cli" | "balanced" | "profile";
110
114
  }
111
115
  export interface RecordResumeSetupInput {
112
116
  profile?: ResolvedResumeProfile;
@@ -118,6 +122,8 @@ export interface RecordResumeSetupInput {
118
122
  profileRepin?: ResumeProfileRepinAuthorization;
119
123
  /** Clear only the recoverable worker_profile_changed queue after its profile is revalidated. */
120
124
  clearWorkerProfileChangedQueue?: boolean;
125
+ /** Apply current profile provider/model/account/effort for a done-lane story reuse. */
126
+ refreshProfileSettings?: boolean;
121
127
  }
122
128
  export declare function accountRequestKeepsCurrentLane(lane: LaneRecord, rawAccount: string): boolean;
123
129
  export declare function assertCurrentResumeAccountAvailable(ctx: Ctx, lane: LaneRecord): void;
@@ -191,7 +197,8 @@ export interface LaneMessageHandlerDeps {
191
197
  resolveActiveQuestionCompany(ctx: Ctx, message?: string): Promise<LaneCompanyRef>;
192
198
  hasPendingQuestionForLane(ctx: Ctx, lane: LaneRecord): Promise<boolean>;
193
199
  cancelFallbackQuestionsForLane(ctx: Ctx, lane: LaneRecord, trigger: "message" | "resume", at: string): Promise<FallbackQuestionCancellationResult>;
194
- resolveResumeProfile(ctx: Ctx, lane: LaneRecord, workerId: string): ResolvedResumeProfile;
200
+ resolveResumeProfile(ctx: Ctx, lane: LaneRecord, workerId: string, allowProviderChange?: boolean): ResolvedResumeProfile;
201
+ reuseProfileRefreshEnabled(ctx: Ctx, lane: LaneRecord): Promise<boolean>;
195
202
  selectAutomaticClaudeResumeAccount(ctx: Ctx, lane: LaneRecord, workerProfile?: ResolvedResumeProfile, options?: {
196
203
  repairUnavailableAccount?: boolean;
197
204
  tolerateInvalidHostLaneLists?: boolean;
@@ -203,7 +210,7 @@ export interface LaneMessageHandlerDeps {
203
210
  validateResumeProfileModel(ctx: Ctx, lane: LaneRecord, profile: WorkerProfile): void;
204
211
  withResumeHireAdmission<T>(ctx: Ctx, lane: LaneRecord, fn: () => T, grantContext?: OutpostHireGrantContext): Promise<T>;
205
212
  }
206
- export interface LaneResumeHandlerDeps extends Pick<LaneMessageHandlerDeps, "onLaneWorkerStarted" | "archiveResumeMessage" | "assertLaneRunsHereForAdmission" | "assertLaneRunsHereForOperatorMutation" | "assessLaneAdmission" | "casUpdateLane" | "laneIsHostedHere" | "laneWithRecoveredProviderSession" | "mintMessageId" | "prepareAccountMove" | "persistClaudeAccountSelection" | "printJson" | "recordResumeSetup" | "resolveCommandLane" | "hasPendingQuestionForLane" | "cancelFallbackQuestionsForLane" | "resolveResumeProfile" | "selectAutomaticClaudeResumeAccount" | "validateResumeProfileModel" | "withResumeHireAdmission"> {
213
+ export interface LaneResumeHandlerDeps extends Pick<LaneMessageHandlerDeps, "onLaneWorkerStarted" | "archiveResumeMessage" | "assertLaneRunsHereForAdmission" | "assertLaneRunsHereForOperatorMutation" | "assessLaneAdmission" | "casUpdateLane" | "laneIsHostedHere" | "laneWithRecoveredProviderSession" | "mintMessageId" | "prepareAccountMove" | "persistClaudeAccountSelection" | "printJson" | "recordResumeSetup" | "resolveCommandLane" | "hasPendingQuestionForLane" | "cancelFallbackQuestionsForLane" | "resolveResumeProfile" | "reuseProfileRefreshEnabled" | "selectAutomaticClaudeResumeAccount" | "validateResumeProfileModel" | "withResumeHireAdmission"> {
207
214
  dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "resume", options: LaneResumeOptions, behavior?: OutpostLaneDispatchBehavior): Promise<void>;
208
215
  authorizeFailedResume(ctx: Ctx, lane: LaneRecord, encodedContext?: string): string;
209
216
  authorizeResumeProfileRepin(ctx: Ctx, lane: LaneRecord, profile: ResolvedResumeProfile | undefined, encodedContext: string | undefined): ResumeProfileRepinAuthorization | undefined;
@@ -35,7 +35,7 @@ import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
35
35
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
36
36
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
37
37
  import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
38
- 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, } from "../lib/lanes/flag-keys.js";
38
+ 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";
39
39
  import { CODEX_ROLLOUT_RETENTION_FLAG, configuredCodexRolloutHomes, lowDiskGuardEnabledForLane, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
40
40
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
41
41
  import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
@@ -500,6 +500,15 @@ function hireGrantFinishedChildrenDoNotCount(ctx, lane) {
500
500
  env: ctx.env,
501
501
  }, readLanesRegistryFlag);
502
502
  }
503
+ function reuseProfileRefreshEnabled(ctx, lane) {
504
+ const resolution = resolveLaneCompany(ctx.hqRoot(), lane);
505
+ const company = resolution.company ?? undefined;
506
+ return readLanesRegistryFlagFailOff(LANE_REUSE_REFRESHES_PROFILE_FLAG, {
507
+ company,
508
+ ...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
509
+ env: ctx.env,
510
+ }, readLanesRegistryFlag);
511
+ }
503
512
  function mergeOwnerLaneHierarchy(supplied, ownerLanes) {
504
513
  const byId = new Map();
505
514
  for (const lane of supplied)
@@ -3581,7 +3590,17 @@ async function runCreate(ctx, opts) {
3581
3590
  keep_alive: keepAlive,
3582
3591
  ...(keepAlive ? { keep_alive_opted_in_at: createdAt } : {}),
3583
3592
  provider,
3593
+ provider_source: explicitProvider !== undefined
3594
+ ? "cli"
3595
+ : profile?.meta.provider !== undefined
3596
+ ? "profile"
3597
+ : "default",
3584
3598
  account: pinnedAccount,
3599
+ account_source: accountWasExplicit
3600
+ ? "cli"
3601
+ : profileAccount !== undefined
3602
+ ? "profile"
3603
+ : "balanced",
3585
3604
  ...(accountSelection !== undefined ? { account_selection: accountSelection } : {}),
3586
3605
  ...(defaultPlacement !== undefined
3587
3606
  ? { placement_default: defaultPlacement }
@@ -6238,14 +6257,14 @@ async function runAdmit(ctx, laneId, opts) {
6238
6257
  });
6239
6258
  }, ctx.sleep);
6240
6259
  }
6241
- function resolveResumeProfile(ctx, lane, workerId) {
6260
+ function resolveResumeProfile(ctx, lane, workerId, allowProviderChange = false) {
6242
6261
  const worker = resolveLaneWorker(ctx.hqRoot(), workerId.trim(), lane.company?.slug ?? lane.mesh_company?.slug, undefined, lane.worker_profile === workerId.trim() && lane.registry_worker !== undefined
6243
6262
  ? { registryWorkerRecord: lane.registry_worker }
6244
6263
  : {});
6245
6264
  if (worker.profile === undefined) {
6246
6265
  throw new LanesError("worker_profile_required", `Worker ${JSON.stringify(workerId)} resolves to a registry worker, not a reusable provider profile; resume --worker requires personal/workers/<id>/meta.yaml`, { worker: workerId, path: worker.path });
6247
6266
  }
6248
- if (worker.profile.meta.provider !== lane.provider) {
6267
+ if (!allowProviderChange && worker.profile.meta.provider !== lane.provider) {
6249
6268
  throw new LanesError("worker_profile_provider_mismatch", `Worker profile ${JSON.stringify(workerId)} uses provider ${worker.profile.meta.provider}, but lane ${lane.lane_id} uses ${lane.provider}; resume cannot change a lane's provider`, {
6250
6269
  worker: workerId,
6251
6270
  lane_id: lane.lane_id,
@@ -6299,7 +6318,7 @@ function applyResumeProfileAndEffort(current, next, input) {
6299
6318
  };
6300
6319
  next.worker_profile_repin_history = appendCappedHistory(current.worker_profile_repin_history ?? [], entry);
6301
6320
  }
6302
- next.worker_profile_ignored = ignoredWorkerProfileFields(current.provider);
6321
+ next.worker_profile_ignored = ignoredWorkerProfileFields(next.provider);
6303
6322
  next.worker_policy_delivery = undefined;
6304
6323
  if (profile.meta.keep_alive !== undefined) {
6305
6324
  next.keep_alive = profile.meta.keep_alive;
@@ -6308,7 +6327,64 @@ function applyResumeProfileAndEffort(current, next, input) {
6308
6327
  : undefined;
6309
6328
  next.keep_alive_blocked_reason = undefined;
6310
6329
  }
6311
- if (next.model_source !== "cli" && next.model_source !== "preserved") {
6330
+ if (input.refreshProfileSettings === true) {
6331
+ const providerPinned = current.provider_source === undefined || current.provider_source === "cli";
6332
+ const accountPinned = next.account_source === undefined || next.account_source === "cli";
6333
+ const targetProvider = providerPinned ? current.provider : profile.meta.provider;
6334
+ if (targetProvider !== current.provider) {
6335
+ throw new LanesError("profile_provider_changed", `Worker profile now selects ${targetProvider}; this lane runs ${current.provider}; create a new lane`, { lane_id: current.lane_id, lane_provider: current.provider, profile_provider: targetProvider });
6336
+ }
6337
+ const profileMatchesProvider = profile.meta.provider === targetProvider;
6338
+ const nextModel = next.model_source === "cli" || next.model_source === "preserved"
6339
+ ? next.model
6340
+ : current.model_source === "cli" || current.model_source === "preserved" ||
6341
+ (providerPinned && !profileMatchesProvider)
6342
+ ? current.model
6343
+ : profileMatchesProvider ? profile.meta.model : undefined;
6344
+ const nextEffort = current.effort_source === "cli" || (providerPinned && !profileMatchesProvider)
6345
+ ? current.effort
6346
+ : profileMatchesProvider ? profile.meta.effort : undefined;
6347
+ const profileAccounts = profileMatchesProvider ? profile.meta.accounts : undefined;
6348
+ const profileAccount = profileMatchesProvider && profileAccounts === undefined
6349
+ ? profile.meta.account
6350
+ : profileAccounts?.includes(next.account ?? "") ? next.account : profileAccounts?.[0];
6351
+ next.provider = targetProvider;
6352
+ next.provider_source = providerPinned ? current.provider_source : "profile";
6353
+ if (!accountPinned && profileMatchesProvider) {
6354
+ next.account = profileAccount;
6355
+ next.account_source = profileAccount === undefined ? "balanced" : "profile";
6356
+ next.account_selection = undefined;
6357
+ }
6358
+ if (profileAccounts !== undefined) {
6359
+ if (accountPinned && current.account !== undefined && !profileAccounts.includes(current.account)) {
6360
+ throw new LanesError("profile_account_not_allowed", `Worker profile ${profile.id} no longer allows pinned lane account ${current.account}`, { lane_id: current.lane_id, worker: profile.id, account: current.account, allowed_accounts: profileAccounts });
6361
+ }
6362
+ next.admission_accounts = [...profileAccounts];
6363
+ }
6364
+ next.model = nextModel;
6365
+ next.model_source = next.model_source === "cli" || next.model_source === "preserved"
6366
+ ? next.model_source
6367
+ : current.model_source === "cli" || current.model_source === "preserved"
6368
+ ? current.model_source
6369
+ : nextModel === undefined ? "none" : "profile";
6370
+ next.effort = nextEffort;
6371
+ next.effort_source = current.effort_source === "cli"
6372
+ ? "cli"
6373
+ : nextEffort === undefined ? "none" : "profile";
6374
+ next.profile_settings_pinned = true;
6375
+ if (current.provider !== next.provider || current.model !== next.model || current.effort !== next.effort) {
6376
+ const entry = {
6377
+ at: input.at ?? new Date().toISOString(),
6378
+ actor: `profile:${profile.id}`,
6379
+ reason: "worker_profile_refresh",
6380
+ ...(current.provider !== next.provider ? { provider: { from: current.provider, to: next.provider } } : {}),
6381
+ ...(current.model !== next.model ? { model: { from: current.model ?? null, to: next.model ?? "(provider default)" } } : {}),
6382
+ ...(current.effort !== next.effort ? { effort: { from: current.effort ?? null, to: next.effort ?? "(provider default)" } } : {}),
6383
+ };
6384
+ next.settings_history = appendCappedHistory(current.settings_history ?? [], entry);
6385
+ }
6386
+ }
6387
+ else if (next.model_source !== "cli" && next.model_source !== "preserved") {
6312
6388
  if (profile.meta.provider === current.provider && profile.meta.model !== undefined) {
6313
6389
  next.model = profile.meta.model;
6314
6390
  next.model_source = "profile";
@@ -6355,6 +6431,7 @@ function recordResumeSetup(ctx, lane, input) {
6355
6431
  input.cliModel === undefined &&
6356
6432
  input.meshCompany === undefined &&
6357
6433
  input.clearWorkerProfileChangedQueue !== true &&
6434
+ input.refreshProfileSettings !== true &&
6358
6435
  lane.keep_alive_blocked_reason !== "terminal_state")
6359
6436
  return lane;
6360
6437
  return casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => {
@@ -6470,6 +6547,12 @@ async function prepareAccountMove(ctx, lane, input) {
6470
6547
  const profile = input.workerProfile?.profile ?? (lane.worker_profile !== undefined
6471
6548
  ? loadWorkerProfile(hqRoot, lane.worker_profile)
6472
6549
  : undefined);
6550
+ if (input.refreshProfileSettings === true &&
6551
+ profile?.meta.provider === lane.provider &&
6552
+ profile.meta.accounts !== undefined &&
6553
+ !profile.meta.accounts.includes(target)) {
6554
+ throw new LanesError("profile_account_not_allowed", `Account ${target} is not in worker profile ${profile.id}'s allowed accounts`, { lane_id: laneId, worker: profile.id, account: target, allowed_accounts: profile.meta.accounts });
6555
+ }
6473
6556
  const profileDefaultsApply = profile !== undefined && profile.meta.provider === lane.provider;
6474
6557
  const pinnedModel = input.preservedModel ?? (input.workerProfile !== undefined
6475
6558
  ? lane.model_source === "cli" || lane.model_source === "preserved" ? lane.model : undefined
@@ -6556,6 +6639,7 @@ async function prepareAccountMove(ctx, lane, input) {
6556
6639
  const next = {
6557
6640
  ...current,
6558
6641
  account: target,
6642
+ ...(input.accountSource !== undefined ? { account_source: input.accountSource } : {}),
6559
6643
  ...(input.accountSelection !== undefined ? { account_selection: input.accountSelection } : {}),
6560
6644
  admission_accounts: undefined,
6561
6645
  provider_session_id: sessionId,
@@ -6584,6 +6668,7 @@ async function prepareAccountMove(ctx, lane, input) {
6584
6668
  ...(input.workerProfile !== undefined ? { profile: input.workerProfile } : {}),
6585
6669
  ...(input.profileRepin !== undefined ? { profileRepin: input.profileRepin } : {}),
6586
6670
  ...(input.effort !== undefined ? { effort: input.effort } : {}),
6671
+ ...(input.refreshProfileSettings === true ? { refreshProfileSettings: true } : {}),
6587
6672
  at,
6588
6673
  });
6589
6674
  if (input.deferTurnStart) {
@@ -6631,9 +6716,11 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6631
6716
  const profileAccount = profileDefaultsApply ? profile?.meta.account : undefined;
6632
6717
  const profileAccounts = profileDefaultsApply ? profile?.meta.accounts : undefined;
6633
6718
  const profilePin = profileAccounts ?? (profileAccount === undefined ? undefined : [profileAccount]);
6634
- const allowedAccounts = profilePin !== undefined && lane.admission_accounts !== undefined
6635
- ? profilePin.filter((account) => lane.admission_accounts.includes(account))
6636
- : profilePin ?? lane.admission_accounts;
6719
+ const allowedAccounts = options.refreshProfileSettings === true
6720
+ ? profilePin ?? lane.admission_accounts
6721
+ : profilePin !== undefined && lane.admission_accounts !== undefined
6722
+ ? profilePin.filter((account) => lane.admission_accounts.includes(account))
6723
+ : profilePin ?? lane.admission_accounts;
6637
6724
  const pinnedModel = workerProfile !== undefined
6638
6725
  ? lane.model_source === "cli" || lane.model_source === "preserved" ? lane.model : undefined
6639
6726
  : lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
@@ -8280,6 +8367,7 @@ export function registerLanesCommand(parent, deps = {}) {
8280
8367
  hasPendingQuestionForLane: questionHandlers.hasPendingQuestionForLane,
8281
8368
  cancelFallbackQuestionsForLane: questionHandlers.cancelFallbackQuestionsForLane,
8282
8369
  resolveResumeProfile,
8370
+ reuseProfileRefreshEnabled,
8283
8371
  selectAutomaticClaudeResumeAccount,
8284
8372
  validateResumeProfileModel,
8285
8373
  withResumeHireAdmission,
@@ -8307,6 +8395,7 @@ export function registerLanesCommand(parent, deps = {}) {
8307
8395
  hasPendingQuestionForLane: questionHandlers.hasPendingQuestionForLane,
8308
8396
  cancelFallbackQuestionsForLane: questionHandlers.cancelFallbackQuestionsForLane,
8309
8397
  resolveResumeProfile,
8398
+ reuseProfileRefreshEnabled,
8310
8399
  selectAutomaticClaudeResumeAccount,
8311
8400
  validateResumeProfileModel,
8312
8401
  withResumeHireAdmission,
@@ -24,6 +24,7 @@
24
24
  import * as fs from 'fs';
25
25
  import * as path from 'path';
26
26
  import { anywhereRuntimeEnabled } from './anywhere-runtime-flag.js';
27
+ import { claudeTargets } from '../lib/install/claude-targets.js';
27
28
  import { HQ_PACK_PROVENANCE_KEY, SECRET_REDACTION, backupRoot, claudeConfigFormat, claudeConfigPath, codexConfigFormat, codexConfigPath, CODEX_MCP_SERVERS_KEY, ConfigParseError, isCodexInstalled, readConfigDoc, resolveEnv, } from './mcp-registration.js';
28
29
  // ---------------------------------------------------------------------------
29
30
  // Pure status computation.
@@ -84,6 +85,49 @@ function summarizeDef(def) {
84
85
  function summariesDiffer(a, b) {
85
86
  return JSON.stringify(a) !== JSON.stringify(b);
86
87
  }
88
+ function findAnywherePluginMcp(home) {
89
+ const claude = path.join(home, '.claude');
90
+ try {
91
+ const targets = claudeTargets(home);
92
+ const state = JSON.parse(fs.readFileSync(targets.stateFile, 'utf8'));
93
+ const pluginId = state.plugin?.pluginId;
94
+ if (typeof pluginId !== 'string' || pluginId.length === 0)
95
+ return undefined;
96
+ const settings = JSON.parse(fs.readFileSync(targets.settingsJson, 'utf8'));
97
+ if (settings.enabledPlugins?.[pluginId] !== true)
98
+ return undefined;
99
+ const installedFile = path.join(claude, 'plugins', 'installed_plugins.json');
100
+ const installed = JSON.parse(fs.readFileSync(installedFile, 'utf8'));
101
+ const install = installed.plugins?.[pluginId]?.find((entry) => typeof entry.installPath === 'string');
102
+ if (!install || typeof install.installPath !== 'string' || !path.isAbsolute(install.installPath))
103
+ return undefined;
104
+ const installDir = path.resolve(install.installPath);
105
+ const cacheRoot = path.resolve(claude, 'plugins', 'cache');
106
+ const cacheRelative = path.relative(cacheRoot, installDir);
107
+ if (cacheRelative.startsWith('..') || path.isAbsolute(cacheRelative) || !cacheRelative.split(path.sep).includes('hq'))
108
+ return undefined;
109
+ const file = path.join(installDir, '.mcp.json');
110
+ const stat = fs.lstatSync(file);
111
+ if (!stat.isFile() || stat.isSymbolicLink())
112
+ return undefined;
113
+ const doc = JSON.parse(fs.readFileSync(file, 'utf8'));
114
+ const server = doc.mcpServers?.hq;
115
+ return server && typeof server === 'object' && !Array.isArray(server) ? server : undefined;
116
+ }
117
+ catch (error) {
118
+ if (error.code === 'ENOENT')
119
+ return undefined;
120
+ console.warn(`Skipping unavailable hq-anywhere plugin MCP metadata: ${error.message}`);
121
+ return undefined;
122
+ }
123
+ }
124
+ function pluginAndCodexMcpEquivalent(plugin, codex) {
125
+ const pArgs = Array.isArray(plugin.args) ? plugin.args : [];
126
+ const cArgs = Array.isArray(codex.args) ? codex.args : [];
127
+ const pluginTarget = plugin.type === 'stdio' && plugin.command === '/bin/sh' && pArgs.length === 3 && pArgs[0] === '${CLAUDE_PLUGIN_ROOT}/scripts/hq-claude-plugin-launch.sh' && pArgs[1] === 'mcp' && pArgs[2] === 'serve';
128
+ const codexTarget = codex.type === 'stdio' && typeof codex.command === 'string' && /(^|\/)hq$/.test(codex.command) && cArgs.length === 2 && cArgs[0] === 'mcp' && cArgs[1] === 'serve';
129
+ return pluginTarget && codexTarget && JSON.stringify(headerKeysOf(plugin)) === JSON.stringify(headerKeysOf(codex)) && JSON.stringify(envKeysOf(plugin)) === JSON.stringify(envKeysOf(codex));
130
+ }
87
131
  /** Scan the Claude runtime: read `~/.claude.json`, collect HQ-owned servers. */
88
132
  function scanClaude(env) {
89
133
  const configPath = claudeConfigPath(env);
@@ -201,6 +245,17 @@ export function computeMcpStatus(env) {
201
245
  const checkHealth = anywhereRuntimeEnabled();
202
246
  const claudeScan = scanClaude(env);
203
247
  const codexScan = scanCodex(env);
248
+ // Claude plugins keep their MCP manifest beside the installed plugin rather
249
+ // than in ~/.claude.json. Pair the HQ plugin with the HQ pack's Codex entry
250
+ // so equivalent launchers do not show as absent or drifted.
251
+ const pluginMcp = checkHealth ? findAnywherePluginMcp(env.home) : undefined;
252
+ if (pluginMcp) {
253
+ for (const [name, found] of codexScan.hqServers) {
254
+ if (found.pack === 'hq-anywhere' || found.pack.endsWith('/hq-anywhere')) {
255
+ claudeScan.hqServers.set(name, { def: pluginMcp, pack: found.pack });
256
+ }
257
+ }
258
+ }
204
259
  // Union of every HQ-owned server name across both runtimes.
205
260
  const names = new Set([...claudeScan.hqServers.keys(), ...codexScan.hqServers.keys()]);
206
261
  const servers = [];
@@ -232,7 +287,9 @@ export function computeMcpStatus(env) {
232
287
  // DRIFT = present in both but transport-relevant content differs (no header VALUES).
233
288
  let drifted = false;
234
289
  if (onClaude && onCodex && claudeFound && codexFound) {
235
- drifted = summariesDiffer(summarizeDef(claudeFound.def), summarizeDef(codexFound.def));
290
+ drifted = pluginMcp && claudeFound.def === pluginMcp && pluginAndCodexMcpEquivalent(pluginMcp, codexFound.def)
291
+ ? false
292
+ : summariesDiffer(summarizeDef(claudeFound.def), summarizeDef(codexFound.def));
236
293
  }
237
294
  servers.push({
238
295
  server: name,
@@ -278,7 +278,9 @@ export declare function verifyArtifact(input: VerifyArtifactInput): void;
278
278
  * @param item the bare server name declared under `contributes.mcp`
279
279
  */
280
280
  export declare function validateMcpManifest(payloadDir: string, item: string): void;
281
- export declare function validateManifest(payloadDir: string, hqVersion: string | null): PackManifest;
281
+ export declare function validateManifest(payloadDir: string, hqVersion: string | null, options?: {
282
+ allowReservedName?: boolean;
283
+ }): PackManifest;
282
284
  /**
283
285
  * Enforce a pack's `requires.packs`: every named dependency MUST already be
284
286
  * installed (and satisfy its optional semver RANGE) before we write anything.