@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.
- package/CHANGELOG.md +13 -0
- package/dist/command-catalog.generated.d.ts +3699 -3
- package/dist/command-catalog.generated.js +5462 -1759
- package/dist/command-help.d.ts +2 -0
- package/dist/command-help.js +13 -0
- package/dist/commands/cloud.d.ts +5 -0
- package/dist/commands/cloud.js +51 -1
- package/dist/commands/install-global.d.ts +21 -0
- package/dist/commands/install-global.js +267 -32
- package/dist/commands/lanes-message.js +59 -6
- package/dist/commands/lanes-resume.js +66 -15
- package/dist/commands/lanes-shared.d.ts +9 -2
- package/dist/commands/lanes.js +97 -8
- package/dist/commands/mcp-status.js +58 -1
- package/dist/commands/pack-install.d.ts +3 -1
- package/dist/commands/pack-install.js +17 -4
- package/dist/commands/packs.js +17 -0
- package/dist/commands/pkg-install.js +13 -1
- package/dist/fast-scaffold.d.ts +2 -0
- package/dist/fast-scaffold.js +16 -0
- package/dist/index.js +19 -2
- package/dist/lazy-commands.d.ts +1 -0
- package/dist/lazy-commands.js +64 -4
- package/dist/lib/cli-flag-keys.d.ts +3 -0
- package/dist/lib/cli-flag-keys.js +3 -0
- package/dist/lib/daemon/ops/mcp.js +11 -10
- package/dist/lib/daemon/ops/policy.d.ts +3 -2
- package/dist/lib/daemon/ops/policy.js +12 -6
- package/dist/lib/daemon/server.d.ts +3 -1
- package/dist/lib/daemon/server.js +63 -7
- package/dist/lib/doctor/checks/anywhere.js +58 -4
- package/dist/lib/install/chatgpt-targets.d.ts +8 -0
- package/dist/lib/install/chatgpt-targets.js +15 -0
- package/dist/lib/install/codex-pack-contributions.d.ts +8 -0
- package/dist/lib/install/codex-pack-contributions.js +205 -0
- package/dist/lib/install/codex-targets.d.ts +1 -1
- package/dist/lib/install/codex-targets.js +2 -1
- package/dist/lib/install/grok-targets.d.ts +8 -0
- package/dist/lib/install/grok-targets.js +15 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/keep-alive-audit.d.ts +3 -2
- package/dist/lib/lanes/keep-alive-audit.js +36 -8
- package/dist/lib/lanes/spawn.js +14 -10
- package/dist/lib/lanes/store.js +29 -2
- package/dist/lib/lanes/types.d.ts +10 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/sync-invalid-entity-warning.d.ts +32 -0
- package/dist/lib/sync-invalid-entity-warning.js +205 -0
- package/dist/main.d.ts +2 -6
- package/dist/main.js +3 -41
- package/dist/stream-error-boundary.d.ts +7 -0
- package/dist/stream-error-boundary.js +33 -0
- 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
|
-
|
|
417
|
-
? resolveResumeProfile(ctx, lane,
|
|
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
|
|
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
|
-
|
|
230
|
-
? resolveResumeProfile(ctx, lane,
|
|
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, {
|
|
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 ? {
|
|
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;
|
package/dist/commands/lanes.js
CHANGED
|
@@ -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(
|
|
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 (
|
|
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 =
|
|
6635
|
-
? profilePin
|
|
6636
|
-
: profilePin
|
|
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 =
|
|
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
|
|
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.
|