@indigoai-us/hq-cli 5.329.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 +5 -0
- package/dist/commands/cloud.d.ts +5 -0
- package/dist/commands/cloud.js +51 -1
- 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/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- 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/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.330.0] — 2026-10-02
|
|
6
|
+
- `hq lanes message` and `hq lanes resume` can refresh model, effort, and account settings from the current worker profile when a done lane starts a new story, behind the default-off `lanes.reuse-refreshes-profile` flag. Reused settings are recorded in lane history; provider-changing reuse is refused without changing the lane.
|
|
7
|
+
|
|
8
|
+
- The default-off `sync.warn-invalid-ontology-entities` hq-flag warns before push with each invalid local entity path and its frontmatter field names. Sync continues and leaves the files unchanged; omitted `aliases` is accepted because hq-pro supplies an empty-list default, while a malformed aliases value is still reported.
|
|
9
|
+
|
|
5
10
|
## [5.329.0] — 2026-10-02
|
|
6
11
|
- `hq lanes keep-alive-audit` no longer reports a keep-alive lead as not restarted when the matching launch has rolled out of capped resume histories; if only queue-time evidence remains, it reports a confirmed untimed restart instead of assigning an on-time or late launch time.
|
|
7
12
|
|
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -239,6 +239,11 @@ export interface ShareCallResult {
|
|
|
239
239
|
export interface PushAllDeps {
|
|
240
240
|
vaultClient: PullAllVaultClient;
|
|
241
241
|
share: (options: ShareCallOptions) => Promise<ShareCallResult>;
|
|
242
|
+
beforeShare?: (input: {
|
|
243
|
+
slug: string;
|
|
244
|
+
companyUid: string;
|
|
245
|
+
companyRoot: string;
|
|
246
|
+
}) => Promise<void>;
|
|
242
247
|
/** DEV-1768: resolve membership push scope (same as pull). */
|
|
243
248
|
resolveScope?: (companyUid: string, slug: string) => Promise<PullScope>;
|
|
244
249
|
}
|
package/dist/commands/cloud.js
CHANGED
|
@@ -17,10 +17,11 @@ import chalk from "chalk";
|
|
|
17
17
|
import * as fs from "fs";
|
|
18
18
|
import * as path from "path";
|
|
19
19
|
import { share, sync, getStateDir, listJournals, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, syncDoctor, } from "@indigoai-us/hq-cloud";
|
|
20
|
-
import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
20
|
+
import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, loadCachedTokens, } from "../utils/cognito-session.js";
|
|
21
21
|
import { resolveUploadAuthor } from "../utils/upload-author.js";
|
|
22
22
|
import { companyFolderExceedsThreshold, emitNarrowHint, isStrictRefusal, resolveBannerLevel, resolveNarrowHintPresentationLevel, resolveNarrowHintMinBytes, } from "../lib/narrow-hint-banner.js";
|
|
23
23
|
import { beginSyncHealthReport, } from "../utils/client-health.js";
|
|
24
|
+
import { companySlugFromSyncPaths, warnAboutInvalidOntologyEntities, } from "../lib/sync-invalid-entity-warning.js";
|
|
24
25
|
import { isWorkBearingSyncResult, recordFirstWorkBearingSync, } from "../lib/first-sync-next-step.js";
|
|
25
26
|
import { maybeShowCliInviteNudge } from "../lib/cli-invite-nudge.js";
|
|
26
27
|
import { createPlanLimitRefusalTracker, describeRefusedCount, describeRefusedFiles, mergePlanLimitRefusals, renderPlanLimitRefusalLines, trackingProgressRecorder, } from "../lib/billing/plan-limit-refusals.js";
|
|
@@ -504,6 +505,22 @@ export async function pushAll(options, deps) {
|
|
|
504
505
|
}
|
|
505
506
|
}
|
|
506
507
|
try {
|
|
508
|
+
if (entry.slug !== "personal") {
|
|
509
|
+
try {
|
|
510
|
+
await deps.beforeShare?.({
|
|
511
|
+
slug: entry.slug,
|
|
512
|
+
companyUid: entry.companyUid,
|
|
513
|
+
companyRoot: path.join(options.hqRoot, "companies", entry.slug),
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
const name = error instanceof Error ? error.name : "UnknownError";
|
|
518
|
+
const errorClass = /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(name)
|
|
519
|
+
? name
|
|
520
|
+
: "UnknownError";
|
|
521
|
+
process.stderr.write(`HQ Sync entity diagnostic failed (${errorClass}); continuing upload.\n`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
507
524
|
const r = await deps.share(entry.shareOptions);
|
|
508
525
|
result.filesUploaded += r.filesUploaded;
|
|
509
526
|
result.bytesUploaded += r.bytesUploaded;
|
|
@@ -926,6 +943,20 @@ export function registerCloudCommands(program) {
|
|
|
926
943
|
});
|
|
927
944
|
const planLimitTracker = createPlanLimitRefusalTracker(companySlugForBilling(progressCompany));
|
|
928
945
|
syncHealth = beginSyncHealthReport();
|
|
946
|
+
if (!personalMode) {
|
|
947
|
+
const companyUid = entityContext?.uid ??
|
|
948
|
+
(targetCompany?.startsWith("cmp_") ? targetCompany : "");
|
|
949
|
+
const companySlug = companySlugFromSyncPaths(options.hqRoot, targetPaths);
|
|
950
|
+
if (companySlug) {
|
|
951
|
+
await warnAboutInvalidOntologyEntities({
|
|
952
|
+
hqRoot: options.hqRoot,
|
|
953
|
+
companyRoot: path.join(options.hqRoot, "companies", companySlug),
|
|
954
|
+
companyUid,
|
|
955
|
+
companySlug,
|
|
956
|
+
idToken: loadCachedTokens()?.idToken ?? "",
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
}
|
|
929
960
|
const result = await share({
|
|
930
961
|
paths: targetPaths,
|
|
931
962
|
company: targetCompany,
|
|
@@ -1541,6 +1572,13 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
|
|
|
1541
1572
|
}, {
|
|
1542
1573
|
vaultClient: adapter,
|
|
1543
1574
|
resolveScope: (companyUid, slug) => resolvePullScope(realClient, companyUid, slug, hqRoot),
|
|
1575
|
+
beforeShare: async ({ slug, companyUid, companyRoot }) => warnAboutInvalidOntologyEntities({
|
|
1576
|
+
hqRoot,
|
|
1577
|
+
companyRoot,
|
|
1578
|
+
companyUid,
|
|
1579
|
+
companySlug: slug,
|
|
1580
|
+
idToken: loadCachedTokens()?.idToken ?? "",
|
|
1581
|
+
}),
|
|
1544
1582
|
share: async (opts) => {
|
|
1545
1583
|
const progressCompany = opts.personalMode
|
|
1546
1584
|
? null
|
|
@@ -1692,6 +1730,18 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1692
1730
|
const pushScope = await resolveCliPullScope(scopeClient, targetCompany ?? pushSlug, hqRoot, (companyUid) => { nudgeResolvedCompanyUid = companyUid; });
|
|
1693
1731
|
nowPushPrefixSet = pushScope?.prefixSet;
|
|
1694
1732
|
}
|
|
1733
|
+
if (!personalMode) {
|
|
1734
|
+
const companySlug = companySlugFromSyncPaths(hqRoot, pushPaths);
|
|
1735
|
+
if (companySlug) {
|
|
1736
|
+
await warnAboutInvalidOntologyEntities({
|
|
1737
|
+
hqRoot,
|
|
1738
|
+
companyRoot: path.join(hqRoot, "companies", companySlug),
|
|
1739
|
+
companyUid: targetCompany?.startsWith("cmp_") ? targetCompany : "",
|
|
1740
|
+
companySlug,
|
|
1741
|
+
idToken: loadCachedTokens()?.idToken ?? "",
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1695
1745
|
const pushProgressCompany = personalMode ? null : (pushSlug ?? null);
|
|
1696
1746
|
const planLimitTracker = createPlanLimitRefusalTracker(companySlugForBilling(pushProgressCompany));
|
|
1697
1747
|
const pushResult = await share({
|
|
@@ -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,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** hq-flags keys used by lane command behavior. */
|
|
2
2
|
export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
3
|
+
/** Refresh provider/model/effort from a worker profile when a done lane takes a new story. */
|
|
4
|
+
export declare const LANE_REUSE_REFRESHES_PROFILE_FLAG = "lanes.reuse-refreshes-profile";
|
|
3
5
|
/** Enables one automatic continuation after a missing-envelope repair. */
|
|
4
6
|
export declare const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
|
|
5
7
|
/** Count only children with live work against their senior's hire grant. */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** hq-flags keys used by lane command behavior. */
|
|
2
2
|
export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
3
|
+
/** Refresh provider/model/effort from a worker profile when a done lane takes a new story. */
|
|
4
|
+
export const LANE_REUSE_REFRESHES_PROFILE_FLAG = "lanes.reuse-refreshes-profile";
|
|
3
5
|
/** Enables one automatic continuation after a missing-envelope repair. */
|
|
4
6
|
export const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
|
|
5
7
|
/** Count only children with live work against their senior's hire grant. */
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -1733,8 +1733,8 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1733
1733
|
});
|
|
1734
1734
|
}
|
|
1735
1735
|
const profileDefaultsApply = profile !== undefined && profile.meta.provider === existing.provider;
|
|
1736
|
-
const profileModel = profileDefaultsApply ? profile.meta.model : undefined;
|
|
1737
|
-
const profileEffort = profileDefaultsApply ? profile.meta.effort : undefined;
|
|
1736
|
+
const profileModel = profileDefaultsApply && existing.profile_settings_pinned !== true ? profile.meta.model : undefined;
|
|
1737
|
+
const profileEffort = profileDefaultsApply && existing.profile_settings_pinned !== true ? profile.meta.effort : undefined;
|
|
1738
1738
|
const ignoredProfileFields = profile
|
|
1739
1739
|
? ignoredWorkerProfileFields(existing.provider)
|
|
1740
1740
|
: [];
|
|
@@ -1765,7 +1765,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1765
1765
|
: undefined;
|
|
1766
1766
|
// `hq lanes set` on a running lane leaves the change pending until this spawn.
|
|
1767
1767
|
const pendingSettings = existing.pending_settings;
|
|
1768
|
-
let pinnedModel = pendingSettings?.model ?? (existing.model_source === undefined || existing.model_source === "cli" || existing.model_source === "preserved"
|
|
1768
|
+
let pinnedModel = pendingSettings?.model ?? (existing.profile_settings_pinned === true || existing.model_source === undefined || existing.model_source === "cli" || existing.model_source === "preserved"
|
|
1769
1769
|
? existing.model
|
|
1770
1770
|
: undefined);
|
|
1771
1771
|
const accountDeps = {
|
|
@@ -1929,7 +1929,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1929
1929
|
: undefined;
|
|
1930
1930
|
const effort = launch.effort ?? pendingResumeEffort ?? resolveLaneEffort({
|
|
1931
1931
|
cliEffort: pendingSettings?.effort ?? (existing.effort_source === "cli" ? existing.effort : undefined),
|
|
1932
|
-
profileEffort,
|
|
1932
|
+
profileEffort: existing.profile_settings_pinned === true ? existing.effort : profileEffort,
|
|
1933
1933
|
accountEffort: accountCfg?.effort,
|
|
1934
1934
|
providerEffort: config.providers[lane.provider].effort,
|
|
1935
1935
|
model,
|
|
@@ -2138,7 +2138,11 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2138
2138
|
? "cli"
|
|
2139
2139
|
: preservedAccountModel !== undefined || existing.model_source === "preserved"
|
|
2140
2140
|
? "preserved"
|
|
2141
|
-
: "cli"
|
|
2141
|
+
: existing.model_source === "cli"
|
|
2142
|
+
? "cli"
|
|
2143
|
+
: existing.profile_settings_pinned === true
|
|
2144
|
+
? "profile"
|
|
2145
|
+
: "cli"
|
|
2142
2146
|
: profileModel !== undefined
|
|
2143
2147
|
? "profile"
|
|
2144
2148
|
: accountCfg?.model !== undefined
|
|
@@ -2483,8 +2487,8 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
2483
2487
|
})
|
|
2484
2488
|
: undefined;
|
|
2485
2489
|
const profileDefaultsApply = profile !== undefined && profile.meta.provider === lane.provider;
|
|
2486
|
-
const profileModel = profileDefaultsApply ? profile.meta.model : undefined;
|
|
2487
|
-
const profileEffort = profileDefaultsApply ? profile.meta.effort : undefined;
|
|
2490
|
+
const profileModel = profileDefaultsApply && lane.profile_settings_pinned !== true ? profile.meta.model : undefined;
|
|
2491
|
+
const profileEffort = profileDefaultsApply && lane.profile_settings_pinned !== true ? profile.meta.effort : undefined;
|
|
2488
2492
|
const ignoredProfileFields = profile
|
|
2489
2493
|
? ignoredWorkerProfileFields(lane.provider)
|
|
2490
2494
|
: [];
|
|
@@ -2506,7 +2510,7 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
2506
2510
|
ignoredFields: ignoredProfileFields,
|
|
2507
2511
|
}
|
|
2508
2512
|
: undefined;
|
|
2509
|
-
const pinnedModel = lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
|
|
2513
|
+
const pinnedModel = lane.profile_settings_pinned === true || lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
|
|
2510
2514
|
? lane.model
|
|
2511
2515
|
: undefined;
|
|
2512
2516
|
const resolved = resolveAccount(lane.provider, lane.account, config, {
|
|
@@ -2534,7 +2538,7 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
2534
2538
|
});
|
|
2535
2539
|
const effort = resolveLaneEffort({
|
|
2536
2540
|
cliEffort: lane.effort_source === "cli" ? lane.effort : undefined,
|
|
2537
|
-
profileEffort,
|
|
2541
|
+
profileEffort: lane.profile_settings_pinned === true ? lane.effort : profileEffort,
|
|
2538
2542
|
accountEffort: accountCfg?.effort,
|
|
2539
2543
|
providerEffort: config.providers[lane.provider].effort,
|
|
2540
2544
|
model,
|
|
@@ -2618,7 +2622,7 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
|
|
|
2618
2622
|
if (model !== undefined) {
|
|
2619
2623
|
next.model = model;
|
|
2620
2624
|
next.model_source = pinnedModel !== undefined
|
|
2621
|
-
? lane.model_source === "preserved" ? "preserved" : "cli"
|
|
2625
|
+
? lane.model_source === "preserved" ? "preserved" : lane.model_source === "profile" || lane.profile_settings_pinned === true ? "profile" : "cli"
|
|
2622
2626
|
: profileModel !== undefined
|
|
2623
2627
|
? "profile"
|
|
2624
2628
|
: accountCfg?.model !== undefined
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -652,6 +652,27 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
652
652
|
if (!isLaneProvider(raw.provider)) {
|
|
653
653
|
throw new LaneSchemaError(`Unknown provider ${JSON.stringify(raw.provider)}`, { path: filePath, value: raw.provider });
|
|
654
654
|
}
|
|
655
|
+
if (raw.provider_source !== undefined && !["cli", "profile", "default"].includes(String(raw.provider_source))) {
|
|
656
|
+
throw new LaneSchemaError("provider_source must be cli, profile, or default", {
|
|
657
|
+
path: filePath,
|
|
658
|
+
field: "provider_source",
|
|
659
|
+
value: raw.provider_source,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
if (raw.account_source !== undefined && !["cli", "profile", "balanced"].includes(String(raw.account_source))) {
|
|
663
|
+
throw new LaneSchemaError("account_source must be cli, profile, or balanced", {
|
|
664
|
+
path: filePath,
|
|
665
|
+
field: "account_source",
|
|
666
|
+
value: raw.account_source,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
if (raw.profile_settings_pinned !== undefined && typeof raw.profile_settings_pinned !== "boolean") {
|
|
670
|
+
throw new LaneSchemaError("profile_settings_pinned must be a boolean", {
|
|
671
|
+
path: filePath,
|
|
672
|
+
field: "profile_settings_pinned",
|
|
673
|
+
value: raw.profile_settings_pinned,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
655
676
|
if (raw.worker_policy_delivery !== undefined &&
|
|
656
677
|
raw.worker_policy_delivery !== "hook" &&
|
|
657
678
|
raw.worker_policy_delivery !== "prompt") {
|
|
@@ -867,8 +888,8 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
867
888
|
for (const [index, entry] of raw.settings_history.entries()) {
|
|
868
889
|
const field = `settings_history[${index}]`;
|
|
869
890
|
if (!isPlainObject(entry) ||
|
|
870
|
-
Object.keys(entry).some((key) => !["at", "actor", "model", "effort", "reason"].includes(key)) ||
|
|
871
|
-
(entry.model === undefined && entry.effort === undefined)) {
|
|
891
|
+
Object.keys(entry).some((key) => !["at", "actor", "provider", "model", "effort", "reason"].includes(key)) ||
|
|
892
|
+
(entry.provider === undefined && entry.model === undefined && entry.effort === undefined)) {
|
|
872
893
|
throw new LaneSchemaError(`${field} must be a settings history entry`, { path: filePath, field });
|
|
873
894
|
}
|
|
874
895
|
requireTimestamp(entry.at, `${field}.at`, filePath);
|
|
@@ -878,6 +899,12 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
878
899
|
if (entry.reason !== undefined && (typeof entry.reason !== "string" || entry.reason.trim() === "")) {
|
|
879
900
|
throw new LaneSchemaError(`${field}.reason must be a non-empty string when present`, { path: filePath, field });
|
|
880
901
|
}
|
|
902
|
+
if (entry.provider !== undefined && (!isPlainObject(entry.provider) ||
|
|
903
|
+
Object.keys(entry.provider).some((key) => key !== "from" && key !== "to") ||
|
|
904
|
+
!isLaneProvider(entry.provider.from) ||
|
|
905
|
+
!isLaneProvider(entry.provider.to))) {
|
|
906
|
+
throw new LaneSchemaError(`${field}.provider must be a {from, to} change`, { path: filePath, field });
|
|
907
|
+
}
|
|
881
908
|
for (const key of ["model", "effort"]) {
|
|
882
909
|
const change = entry[key];
|
|
883
910
|
if (change === undefined)
|
|
@@ -312,6 +312,10 @@ export interface LaneStopHistoryEntry {
|
|
|
312
312
|
export interface LaneSettingsHistoryEntry {
|
|
313
313
|
at: string;
|
|
314
314
|
actor: string;
|
|
315
|
+
provider?: {
|
|
316
|
+
from: LaneProvider;
|
|
317
|
+
to: LaneProvider;
|
|
318
|
+
};
|
|
315
319
|
model?: {
|
|
316
320
|
from: string | null;
|
|
317
321
|
to: string;
|
|
@@ -578,7 +582,13 @@ export interface LaneRecord {
|
|
|
578
582
|
/** How policy files were delivered on the most recent profile spawn. */
|
|
579
583
|
worker_policy_delivery?: "hook" | "prompt";
|
|
580
584
|
provider: LaneProvider;
|
|
585
|
+
/** Whether provider came from an explicit create override or the worker profile/default. */
|
|
586
|
+
provider_source?: "cli" | "profile" | "default";
|
|
581
587
|
account?: string;
|
|
588
|
+
/** Explicit create account choices remain pinned during profile refreshes. */
|
|
589
|
+
account_source?: "cli" | "profile" | "balanced";
|
|
590
|
+
/** Reuse snapshots the worker profile's resolved settings for the new story. */
|
|
591
|
+
profile_settings_pinned?: boolean;
|
|
582
592
|
/** Recent Claude account-selection evidence for a create or automatic resume move. */
|
|
583
593
|
account_selection?: LaneAccountSelection;
|
|
584
594
|
/** Eligible accounts for a queue entry created without an explicit account. */
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -231,7 +231,7 @@ export const LANE_FIELD_WRITERS = {
|
|
|
231
231
|
pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
|
|
232
232
|
priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], registry_worker: ["creator", "command", "spawn"], registry_worker_approval_history: ["command", "watcher", "reconcile"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
|
|
233
233
|
keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile"], keep_alive_history: ["reconcile"], keep_alive_blocked_resume_streak: ["reconcile", "spawn", "watcher"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
|
|
234
|
-
provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], fallback_question_cancellation_failures: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
234
|
+
provider: ["creator", "command"], provider_source: ["creator", "command"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_source: ["creator", "command"], profile_settings_pinned: ["command"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], fallback_question_cancellation_failures: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
235
235
|
version: ["creator"], created_at: ["creator"], updated_at: ["creator"], ended_at: ["reconcile"], envelope_summary: ["reconcile"], attribution_history: ["command", "watcher", "reconcile"], story_switch_pending: ["command", "watcher", "reconcile"], story_switch_unacknowledged: ["watcher", "reconcile"],
|
|
236
236
|
last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
|
|
237
237
|
last_message_at: ["command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher", "reconcile"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { FlagClient, FlagClientOptions } from "@indigoai-us/hq-flags-client";
|
|
2
|
+
export declare const SYNC_INVALID_ENTITY_WARNING_FLAG = "sync.warn-invalid-ontology-entities";
|
|
3
|
+
export declare const SYNC_INVALID_ENTITY_WARNING_FALLBACK = false;
|
|
4
|
+
export declare const SYNC_INVALID_ENTITY_WARNING_TIMEOUT_MS = 1500;
|
|
5
|
+
type FlagClientLike = Pick<FlagClient, "ready" | "isEnabled" | "close">;
|
|
6
|
+
export interface InvalidEntityFrontmatter {
|
|
7
|
+
path: string;
|
|
8
|
+
fields: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface SyncInvalidEntityWarningDependencies {
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
createClient?: (options: FlagClientOptions) => FlagClientLike;
|
|
13
|
+
idToken?: string;
|
|
14
|
+
writeLine?: (line: string) => void;
|
|
15
|
+
}
|
|
16
|
+
/** Check only the entity directory that this sync push will upload. */
|
|
17
|
+
export declare function findInvalidOntologyEntities(args: {
|
|
18
|
+
companyRoot: string;
|
|
19
|
+
hqRoot: string;
|
|
20
|
+
}): InvalidEntityFrontmatter[];
|
|
21
|
+
export declare function companySlugFromSyncPaths(hqRoot: string, syncPaths: string[]): string | undefined;
|
|
22
|
+
/** Warns by path and field names only. Any diagnostic failure leaves sync running. */
|
|
23
|
+
export declare function warnAboutInvalidOntologyEntities(args: {
|
|
24
|
+
hqRoot: string;
|
|
25
|
+
companyRoot: string;
|
|
26
|
+
companyUid: string;
|
|
27
|
+
companySlug: string;
|
|
28
|
+
idToken?: string;
|
|
29
|
+
dependencies?: Omit<SyncInvalidEntityWarningDependencies, "idToken">;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=sync-invalid-entity-warning.d.ts.map
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { createFlagClient, readLocalOverride } from "@indigoai-us/hq-flags-client";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import * as yaml from "js-yaml";
|
|
6
|
+
import { resolveFlagRegistryEndpoint } from "./flag-registry-endpoint.js";
|
|
7
|
+
export const SYNC_INVALID_ENTITY_WARNING_FLAG = "sync.warn-invalid-ontology-entities";
|
|
8
|
+
export const SYNC_INVALID_ENTITY_WARNING_FALLBACK = false;
|
|
9
|
+
export const SYNC_INVALID_ENTITY_WARNING_TIMEOUT_MS = 1500;
|
|
10
|
+
const ENTITY_TYPES = new Set([
|
|
11
|
+
"person", "project", "company", "concept", "product", "tool",
|
|
12
|
+
"tool/technology", "model", "platform", "contact", "deal", "contract",
|
|
13
|
+
"invoice", "account", "journal-entry", "ar-invoice", "ap-bill",
|
|
14
|
+
]);
|
|
15
|
+
const REQUIRED_FIELDS = [
|
|
16
|
+
"id", "type", "canonical_name", "confidence", "status",
|
|
17
|
+
"domain", "first_seen", "last_updated", "signal_count",
|
|
18
|
+
];
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
function validRequiredFields(frontmatter) {
|
|
23
|
+
const invalid = new Set();
|
|
24
|
+
for (const field of REQUIRED_FIELDS) {
|
|
25
|
+
if (!(field in frontmatter))
|
|
26
|
+
invalid.add(field);
|
|
27
|
+
}
|
|
28
|
+
if (typeof frontmatter.id !== "string" || frontmatter.id.length === 0) {
|
|
29
|
+
invalid.add("id");
|
|
30
|
+
}
|
|
31
|
+
if (typeof frontmatter.type !== "string" || !ENTITY_TYPES.has(frontmatter.type)) {
|
|
32
|
+
invalid.add("type");
|
|
33
|
+
}
|
|
34
|
+
if (typeof frontmatter.canonical_name !== "string" ||
|
|
35
|
+
frontmatter.canonical_name.length === 0) {
|
|
36
|
+
invalid.add("canonical_name");
|
|
37
|
+
}
|
|
38
|
+
if ("aliases" in frontmatter &&
|
|
39
|
+
(!Array.isArray(frontmatter.aliases) ||
|
|
40
|
+
!frontmatter.aliases.every((value) => typeof value === "string"))) {
|
|
41
|
+
invalid.add("aliases");
|
|
42
|
+
}
|
|
43
|
+
if (typeof frontmatter.confidence !== "number" ||
|
|
44
|
+
!Number.isFinite(frontmatter.confidence) ||
|
|
45
|
+
frontmatter.confidence < 0 ||
|
|
46
|
+
frontmatter.confidence > 1) {
|
|
47
|
+
invalid.add("confidence");
|
|
48
|
+
}
|
|
49
|
+
if (frontmatter.status !== "draft" && frontmatter.status !== "confirmed") {
|
|
50
|
+
invalid.add("status");
|
|
51
|
+
}
|
|
52
|
+
if (!Array.isArray(frontmatter.domain) ||
|
|
53
|
+
!frontmatter.domain.every((value) => typeof value === "string")) {
|
|
54
|
+
invalid.add("domain");
|
|
55
|
+
}
|
|
56
|
+
if (typeof frontmatter.first_seen !== "string" || frontmatter.first_seen.length === 0) {
|
|
57
|
+
invalid.add("first_seen");
|
|
58
|
+
}
|
|
59
|
+
if (typeof frontmatter.last_updated !== "string" ||
|
|
60
|
+
frontmatter.last_updated.length === 0) {
|
|
61
|
+
invalid.add("last_updated");
|
|
62
|
+
}
|
|
63
|
+
if (typeof frontmatter.signal_count !== "number" ||
|
|
64
|
+
!Number.isInteger(frontmatter.signal_count) ||
|
|
65
|
+
frontmatter.signal_count < 0) {
|
|
66
|
+
invalid.add("signal_count");
|
|
67
|
+
}
|
|
68
|
+
if (invalid.size === 0 &&
|
|
69
|
+
frontmatter.identity_version !== 2 &&
|
|
70
|
+
frontmatter.id !== createHash("sha256")
|
|
71
|
+
.update(`${frontmatter.type}:${frontmatter.canonical_name}`)
|
|
72
|
+
.digest("hex")) {
|
|
73
|
+
invalid.add("id");
|
|
74
|
+
}
|
|
75
|
+
return [...invalid].sort();
|
|
76
|
+
}
|
|
77
|
+
function parseInvalidEntityFile(filePath, displayPath) {
|
|
78
|
+
try {
|
|
79
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
80
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
81
|
+
if (!match)
|
|
82
|
+
return { path: displayPath, fields: ["_frontmatter"] };
|
|
83
|
+
// Match the ontology reader's YAML parser semantics: ISO date scalars stay
|
|
84
|
+
// strings so Zod's `z.string()` fields are checked as the server checks them.
|
|
85
|
+
const parsed = yaml.load(match[1], { schema: yaml.CORE_SCHEMA });
|
|
86
|
+
if (!isRecord(parsed))
|
|
87
|
+
return { path: displayPath, fields: ["_root"] };
|
|
88
|
+
const fields = validRequiredFields(parsed);
|
|
89
|
+
return fields.length ? { path: displayPath, fields } : null;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// YAML parser errors can embed user content; report the location only.
|
|
93
|
+
return { path: displayPath, fields: ["_frontmatter"] };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Check only the entity directory that this sync push will upload. */
|
|
97
|
+
export function findInvalidOntologyEntities(args) {
|
|
98
|
+
const entityRoot = path.join(args.companyRoot, "ontology", "entities");
|
|
99
|
+
if (!fs.existsSync(entityRoot))
|
|
100
|
+
return [];
|
|
101
|
+
const found = [];
|
|
102
|
+
const typeEntries = fs.readdirSync(entityRoot, { withFileTypes: true });
|
|
103
|
+
for (const typeEntry of typeEntries) {
|
|
104
|
+
if (!typeEntry.isDirectory())
|
|
105
|
+
continue;
|
|
106
|
+
const typeRoot = path.join(entityRoot, typeEntry.name);
|
|
107
|
+
for (const entityEntry of fs.readdirSync(typeRoot, { withFileTypes: true })) {
|
|
108
|
+
if (!entityEntry.isFile() || !entityEntry.name.endsWith(".md"))
|
|
109
|
+
continue;
|
|
110
|
+
const filePath = path.join(typeRoot, entityEntry.name);
|
|
111
|
+
const relativePath = path.relative(args.hqRoot, filePath).split(path.sep).join("/");
|
|
112
|
+
const invalid = parseInvalidEntityFile(filePath, relativePath);
|
|
113
|
+
if (invalid)
|
|
114
|
+
found.push(invalid);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return found.sort((a, b) => a.path.localeCompare(b.path));
|
|
118
|
+
}
|
|
119
|
+
export function companySlugFromSyncPaths(hqRoot, syncPaths) {
|
|
120
|
+
const companiesRoot = path.resolve(hqRoot, "companies");
|
|
121
|
+
for (const syncPath of syncPaths) {
|
|
122
|
+
const relative = path.relative(companiesRoot, path.resolve(syncPath));
|
|
123
|
+
if (!relative || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
124
|
+
continue;
|
|
125
|
+
const slug = relative.split(path.sep)[0];
|
|
126
|
+
if (slug && slug !== ".")
|
|
127
|
+
return slug;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
function safeErrorClass(error) {
|
|
132
|
+
const name = error instanceof Error ? error.name : "UnknownError";
|
|
133
|
+
return /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(name) ? name : "UnknownError";
|
|
134
|
+
}
|
|
135
|
+
async function warningEnabled(args) {
|
|
136
|
+
const env = args.dependencies.env ?? process.env;
|
|
137
|
+
const override = readLocalOverride(SYNC_INVALID_ENTITY_WARNING_FLAG, env);
|
|
138
|
+
if (override !== null)
|
|
139
|
+
return override;
|
|
140
|
+
const companyUid = args.companyUid || env.HQ_COMPANY_UID?.trim() || "";
|
|
141
|
+
const companySlug = args.companySlug || env.HQ_COMPANY_SLUG?.trim() || "";
|
|
142
|
+
if (!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
|
|
143
|
+
return SYNC_INVALID_ENTITY_WARNING_FALLBACK;
|
|
144
|
+
}
|
|
145
|
+
const token = args.dependencies.idToken ?? "";
|
|
146
|
+
if (!token)
|
|
147
|
+
return SYNC_INVALID_ENTITY_WARNING_FALLBACK;
|
|
148
|
+
let client;
|
|
149
|
+
let failed = false;
|
|
150
|
+
try {
|
|
151
|
+
client = (args.dependencies.createClient ?? createFlagClient)({
|
|
152
|
+
endpoint: resolveFlagRegistryEndpoint(undefined, env),
|
|
153
|
+
companyUid,
|
|
154
|
+
...(/^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/.test(companySlug)
|
|
155
|
+
? { companyIdentifiers: [companySlug] }
|
|
156
|
+
: {}),
|
|
157
|
+
env: {},
|
|
158
|
+
getToken: () => token,
|
|
159
|
+
refreshIntervalMs: 0,
|
|
160
|
+
requestTimeoutMs: SYNC_INVALID_ENTITY_WARNING_TIMEOUT_MS,
|
|
161
|
+
onError: (error) => {
|
|
162
|
+
failed = true;
|
|
163
|
+
(args.dependencies.writeLine ?? ((line) => process.stderr.write(line)))(`HQ Sync entity-warning flag lookup failed (${safeErrorClass(error)}); defaulting off.\n`);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
await client.ready();
|
|
167
|
+
return !failed && client.isEnabled(SYNC_INVALID_ENTITY_WARNING_FLAG, {
|
|
168
|
+
fallback: SYNC_INVALID_ENTITY_WARNING_FALLBACK,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
(args.dependencies.writeLine ?? ((line) => process.stderr.write(line)))(`HQ Sync entity-warning flag lookup failed (${safeErrorClass(error)}); defaulting off.\n`);
|
|
173
|
+
return SYNC_INVALID_ENTITY_WARNING_FALLBACK;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
client?.close();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Warns by path and field names only. Any diagnostic failure leaves sync running. */
|
|
180
|
+
export async function warnAboutInvalidOntologyEntities(args) {
|
|
181
|
+
try {
|
|
182
|
+
const enabled = await warningEnabled({
|
|
183
|
+
companyUid: args.companyUid,
|
|
184
|
+
companySlug: args.companySlug,
|
|
185
|
+
dependencies: { ...args.dependencies, idToken: args.idToken },
|
|
186
|
+
});
|
|
187
|
+
if (!enabled)
|
|
188
|
+
return;
|
|
189
|
+
const invalid = findInvalidOntologyEntities(args);
|
|
190
|
+
const writeLine = args.dependencies?.writeLine ?? ((line) => process.stderr.write(line));
|
|
191
|
+
for (const item of invalid) {
|
|
192
|
+
writeLine(`HQ Sync warning: ${item.path} has frontmatter rejected by ontology matching: ${item.fields.join(", ")}. Sync will continue and leave the file unchanged. Fix the entity if you want it included in ontology results.\n`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const writeLine = args.dependencies?.writeLine ?? ((line) => process.stderr.write(line));
|
|
197
|
+
try {
|
|
198
|
+
writeLine(`HQ Sync entity diagnostic failed (${safeErrorClass(error)}); sync will continue.\n`);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// A closed stderr must not prevent the upload from preserving user data.
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=sync-invalid-entity-warning.js.map
|