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