@indigoai-us/hq-cli 5.198.1 → 5.199.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 +20 -0
- package/dist/command-catalog.generated.d.ts +6 -0
- package/dist/command-catalog.generated.js +8 -0
- package/dist/commands/lanes.d.ts +3 -0
- package/dist/commands/lanes.js +470 -16
- package/dist/lib/core-utils/hook-timeout-debug-context-flag.js +5 -4
- package/dist/lib/doctor/client-health-reason-codes-flag.js +3 -2
- package/dist/lib/flag-registry-endpoint.d.ts +5 -0
- package/dist/lib/flag-registry-endpoint.js +9 -0
- package/dist/lib/flag-registry.d.ts +7 -3
- package/dist/lib/flag-registry.js +19 -7
- package/dist/lib/lanes/outpost-dispatch.d.ts +7 -0
- package/dist/lib/lanes/outpost-dispatch.js +19 -0
- package/dist/lib/lanes/outpost-pruner-install.js +4 -3
- package/dist/lib/lanes/outpost-worktree-pruner-flag.js +3 -3
- package/dist/lib/lanes/registry-flag.js +3 -2
- package/dist/node-network-compat-flag.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.199.0] — 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `hq lanes questions list --all-hosts` reads questions from Outposts that own
|
|
10
|
+
this primary's lanes. Rows include the host, and an unavailable Outpost is
|
|
11
|
+
named in `errors` while local questions remain available. Remote rows use the
|
|
12
|
+
audience computed by the owner, and eligible Outpost reads run with bounded
|
|
13
|
+
concurrency. The primary can show and answer an Outpost-only question, then
|
|
14
|
+
save the owner's confirmed answer locally. Cross-host lookup requires the
|
|
15
|
+
company-scoped `lanes.outpost-question-visibility` hq-flags gate. Use
|
|
16
|
+
`--company` to limit a list or answer to one company.
|
|
17
|
+
|
|
5
18
|
## [5.198.1] — 2026-09-25
|
|
6
19
|
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Registry-backed hq-flags readers now use the central registry endpoint when
|
|
23
|
+
`HQ_FLAGS_API_URL` is unset or blank. Explicit endpoints and local overrides
|
|
24
|
+
keep precedence, and process-wide gates retain their legacy fallback until a
|
|
25
|
+
registry snapshot is ready.
|
|
26
|
+
|
|
7
27
|
## [5.198.0] — 2026-09-25
|
|
8
28
|
|
|
9
29
|
### Added
|
|
@@ -7970,6 +7970,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7970
7970
|
readonly usage: "[options]";
|
|
7971
7971
|
readonly arguments: readonly [];
|
|
7972
7972
|
readonly options: readonly [{
|
|
7973
|
+
readonly flags: "--all-hosts";
|
|
7974
|
+
readonly description: "Include questions from Outposts that own this primary's lanes";
|
|
7975
|
+
}, {
|
|
7973
7976
|
readonly flags: "--for <ref>";
|
|
7974
7977
|
readonly description: "Who must decide (ref | me | user)";
|
|
7975
7978
|
}, {
|
|
@@ -8028,6 +8031,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8028
8031
|
}, {
|
|
8029
8032
|
readonly flags: "--freeform <text>";
|
|
8030
8033
|
readonly description: "Supply a free-form answer for a question with options";
|
|
8034
|
+
}, {
|
|
8035
|
+
readonly flags: "--company <slug|uid>";
|
|
8036
|
+
readonly description: "Require the question lane to match this company";
|
|
8031
8037
|
}, {
|
|
8032
8038
|
readonly flags: "--json";
|
|
8033
8039
|
readonly description: "JSON on stdout (always)";
|
|
@@ -10306,6 +10306,10 @@ export const COMMAND_CATALOG = [
|
|
|
10306
10306
|
"usage": "[options]",
|
|
10307
10307
|
"arguments": [],
|
|
10308
10308
|
"options": [
|
|
10309
|
+
{
|
|
10310
|
+
"flags": "--all-hosts",
|
|
10311
|
+
"description": "Include questions from Outposts that own this primary's lanes"
|
|
10312
|
+
},
|
|
10309
10313
|
{
|
|
10310
10314
|
"flags": "--for <ref>",
|
|
10311
10315
|
"description": "Who must decide (ref | me | user)"
|
|
@@ -10384,6 +10388,10 @@ export const COMMAND_CATALOG = [
|
|
|
10384
10388
|
"flags": "--freeform <text>",
|
|
10385
10389
|
"description": "Supply a free-form answer for a question with options"
|
|
10386
10390
|
},
|
|
10391
|
+
{
|
|
10392
|
+
"flags": "--company <slug|uid>",
|
|
10393
|
+
"description": "Require the question lane to match this company"
|
|
10394
|
+
},
|
|
10387
10395
|
{
|
|
10388
10396
|
"flags": "--json",
|
|
10389
10397
|
"description": "JSON on stdout (always)"
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -88,6 +88,8 @@ export interface LanesCommandDeps {
|
|
|
88
88
|
withGrantHireLocks?: typeof withLaneHireLocks;
|
|
89
89
|
/** Test seam for the Claude account-balancing gate. */
|
|
90
90
|
laneClaudeAccountBalanceEnabled?: (company?: LaneCompanyRef | null) => boolean | Promise<boolean>;
|
|
91
|
+
/** Test seam for the default-off Outpost question visibility gate. */
|
|
92
|
+
laneOutpostQuestionVisibilityEnabled?: (company?: LaneCompanyRef | null) => boolean | Promise<boolean>;
|
|
91
93
|
/** Test seam for the hq-flags gated tracker commands. */
|
|
92
94
|
laneTrackerEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
93
95
|
/** The local-only PR 2 implementation refuses if mesh storage is enabled. */
|
|
@@ -96,6 +98,7 @@ export interface LanesCommandDeps {
|
|
|
96
98
|
trackerActor?: () => string | null;
|
|
97
99
|
}
|
|
98
100
|
export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
101
|
+
export declare const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-question-visibility";
|
|
99
102
|
export declare function parseSeniorRef(raw: string): SeniorRef;
|
|
100
103
|
export declare function resolveAnswerChoice(choice: string, options: string[], freeform: boolean): string;
|
|
101
104
|
/**
|
package/dist/commands/lanes.js
CHANGED
|
@@ -22,7 +22,7 @@ import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGr
|
|
|
22
22
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
23
23
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
24
24
|
export { LANE_HIRE_GRANTS_FLAG };
|
|
25
|
-
import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionAnswerCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
25
|
+
import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionAnswerCommand, buildOutpostQuestionListCommand, buildOutpostQuestionShowCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
26
26
|
import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
|
|
27
27
|
import { BALANCE_MOVABLE_STATES, CLAUDE_ACCOUNT_BALANCE_FLAG, accountLoadsForReport, assertBalanceApplyEnabled, planClaudeBalanceMoves, selectClaudeAccount, } from "../lib/lanes/claude-account-balance.js";
|
|
28
28
|
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
@@ -31,7 +31,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
31
31
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
32
32
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
33
33
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
34
|
-
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
34
|
+
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
35
35
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
36
36
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
37
37
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
@@ -55,6 +55,7 @@ const VENDOR_LOGIN = {
|
|
|
55
55
|
};
|
|
56
56
|
export { emitLaneDecisionEvents };
|
|
57
57
|
export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
58
|
+
export const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-question-visibility";
|
|
58
59
|
function defaultLanePlacementEnabled() {
|
|
59
60
|
return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
|
|
60
61
|
}
|
|
@@ -72,6 +73,12 @@ function defaultLaneTrackerMeshEnabled(company) {
|
|
|
72
73
|
function defaultLaneHireGrantsEnabled(company) {
|
|
73
74
|
return readLanesRegistryFlag(LANE_HIRE_GRANTS_FLAG, { company });
|
|
74
75
|
}
|
|
76
|
+
function defaultLaneOutpostQuestionVisibilityEnabled(company, env) {
|
|
77
|
+
return readLanesRegistryFlag(LANE_OUTPOST_QUESTION_VISIBILITY_FLAG, {
|
|
78
|
+
company,
|
|
79
|
+
env,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
75
82
|
function withoutProviderHome(value) {
|
|
76
83
|
if (Array.isArray(value))
|
|
77
84
|
return value.map(withoutProviderHome);
|
|
@@ -271,6 +278,8 @@ function resolveDeps(deps = {}) {
|
|
|
271
278
|
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
272
279
|
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
273
280
|
laneClaudeAccountBalanceEnabled: deps.laneClaudeAccountBalanceEnabled ?? defaultLaneClaudeAccountBalanceEnabled,
|
|
281
|
+
laneOutpostQuestionVisibilityEnabled: deps.laneOutpostQuestionVisibilityEnabled ??
|
|
282
|
+
((company) => defaultLaneOutpostQuestionVisibilityEnabled(company, env)),
|
|
274
283
|
laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
|
|
275
284
|
laneTrackerMeshEnabled: deps.laneTrackerMeshEnabled ?? defaultLaneTrackerMeshEnabled,
|
|
276
285
|
trackerActor: deps.trackerActor ?? defaultTrackerActor,
|
|
@@ -1407,6 +1416,22 @@ function sameLaneCompany(a, b) {
|
|
|
1407
1416
|
return a === b;
|
|
1408
1417
|
return (a.uid !== "" && a.uid === b.uid) || (a.slug !== "" && a.slug === b.slug);
|
|
1409
1418
|
}
|
|
1419
|
+
function sameQuestionCompany(a, b) {
|
|
1420
|
+
if (a === null || b === null)
|
|
1421
|
+
return a === b;
|
|
1422
|
+
const aUid = a.uid.trim();
|
|
1423
|
+
const bUid = b.uid.trim();
|
|
1424
|
+
if (aUid || bUid)
|
|
1425
|
+
return aUid !== "" && aUid === bUid;
|
|
1426
|
+
const aSlug = a.slug.trim();
|
|
1427
|
+
const bSlug = b.slug.trim();
|
|
1428
|
+
return aSlug !== "" && aSlug === bSlug;
|
|
1429
|
+
}
|
|
1430
|
+
function questionCompanyRefMatches(recordCompany, filter) {
|
|
1431
|
+
return recordCompany !== null &&
|
|
1432
|
+
recordCompany !== undefined &&
|
|
1433
|
+
sameQuestionCompany(recordCompany, filter);
|
|
1434
|
+
}
|
|
1410
1435
|
function companyRefMatches(recordCompany, filter) {
|
|
1411
1436
|
return (recordCompany !== null &&
|
|
1412
1437
|
recordCompany !== undefined &&
|
|
@@ -3255,6 +3280,7 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
|
3255
3280
|
choice: options.choice,
|
|
3256
3281
|
freeform: options.freeform,
|
|
3257
3282
|
notes: options.notes,
|
|
3283
|
+
company: options.company,
|
|
3258
3284
|
}),
|
|
3259
3285
|
});
|
|
3260
3286
|
}
|
|
@@ -3318,7 +3344,9 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
|
3318
3344
|
});
|
|
3319
3345
|
}
|
|
3320
3346
|
const ownerAnswer = ownerQuestion.answer;
|
|
3321
|
-
if (!isQuestionAnswer(ownerAnswer)
|
|
3347
|
+
if (!isQuestionAnswer(ownerAnswer) ||
|
|
3348
|
+
ownerQuestion.lane_id !== lane.lane_id ||
|
|
3349
|
+
!sameQuestionCompany(ownerQuestion.company, lane.company)) {
|
|
3322
3350
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return the confirmed answer for question ${questionId}`, {
|
|
3323
3351
|
lane_id: lane.lane_id,
|
|
3324
3352
|
question_id: questionId,
|
|
@@ -3326,10 +3354,13 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
|
3326
3354
|
command_id: dispatched.commandId,
|
|
3327
3355
|
});
|
|
3328
3356
|
}
|
|
3329
|
-
const
|
|
3357
|
+
const updatePrimaryCopy = () => casUpdateQuestion(ctx.hqRoot(), questionId, (current) => {
|
|
3330
3358
|
if (current.lane_id !== lane.lane_id) {
|
|
3331
3359
|
throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} belongs to a different lane than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
|
|
3332
3360
|
}
|
|
3361
|
+
if (!sameQuestionCompany(current.company, lane.company)) {
|
|
3362
|
+
throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} belongs to a different company than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
|
|
3363
|
+
}
|
|
3333
3364
|
if (current.status === "pending") {
|
|
3334
3365
|
return { ...current, status: "answered", answer: ownerAnswer };
|
|
3335
3366
|
}
|
|
@@ -3338,6 +3369,26 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
|
3338
3369
|
}
|
|
3339
3370
|
throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} is ${current.status} or has a different answer than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
|
|
3340
3371
|
});
|
|
3372
|
+
let syncedQuestion;
|
|
3373
|
+
try {
|
|
3374
|
+
syncedQuestion = updatePrimaryCopy();
|
|
3375
|
+
}
|
|
3376
|
+
catch (err) {
|
|
3377
|
+
if (!isLanesError(err) || err.error !== "question_not_found")
|
|
3378
|
+
throw err;
|
|
3379
|
+
const primaryLane = readLane(ctx.hqRoot(), lane.lane_id);
|
|
3380
|
+
if (!sameQuestionCompany(primaryLane.company, lane.company)) {
|
|
3381
|
+
throw new LanesError("outpost_question_sync_conflict", `Primary lane ${lane.lane_id} belongs to a different company than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
|
|
3382
|
+
}
|
|
3383
|
+
try {
|
|
3384
|
+
syncedQuestion = createQuestion(ctx.hqRoot(), ownerQuestion);
|
|
3385
|
+
}
|
|
3386
|
+
catch (createErr) {
|
|
3387
|
+
if (!isLanesError(createErr) || createErr.error !== "question_exists")
|
|
3388
|
+
throw createErr;
|
|
3389
|
+
syncedQuestion = updatePrimaryCopy();
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3341
3392
|
printJson({
|
|
3342
3393
|
...remote,
|
|
3343
3394
|
lane_id: lane.lane_id,
|
|
@@ -6582,6 +6633,271 @@ async function runAsk(ctx, opts) {
|
|
|
6582
6633
|
ask_delivery_failed: failure,
|
|
6583
6634
|
});
|
|
6584
6635
|
}
|
|
6636
|
+
const OUTPOST_QUESTION_LOOKUP_CONCURRENCY = 4;
|
|
6637
|
+
function groupQuestionLanesByCompany(lanes) {
|
|
6638
|
+
const groups = [];
|
|
6639
|
+
for (const lane of lanes) {
|
|
6640
|
+
const existing = groups.find((group) => sameQuestionCompany(group.company, lane.company));
|
|
6641
|
+
if (existing)
|
|
6642
|
+
existing.lanes.push(lane);
|
|
6643
|
+
else
|
|
6644
|
+
groups.push({ company: lane.company, lanes: [lane] });
|
|
6645
|
+
}
|
|
6646
|
+
return groups;
|
|
6647
|
+
}
|
|
6648
|
+
function questionListCompanyArg(company) {
|
|
6649
|
+
if (company === null) {
|
|
6650
|
+
throw new LanesError("outpost_question_company_unavailable", "An Outpost question lookup requires a recorded company identity", {});
|
|
6651
|
+
}
|
|
6652
|
+
const value = company.uid.trim() || company.slug.trim();
|
|
6653
|
+
if (!value) {
|
|
6654
|
+
throw new LanesError("outpost_question_company_invalid", "An Outpost question lookup needs a company slug or uid", {});
|
|
6655
|
+
}
|
|
6656
|
+
return value;
|
|
6657
|
+
}
|
|
6658
|
+
async function readOutpostQuestionRecords(ctx, outpostId, company) {
|
|
6659
|
+
const companyArg = questionListCompanyArg(company);
|
|
6660
|
+
let result;
|
|
6661
|
+
try {
|
|
6662
|
+
result = await ctx.dispatchOutpostExec({
|
|
6663
|
+
outpostId,
|
|
6664
|
+
command: buildOutpostQuestionListCommand({ company: companyArg }),
|
|
6665
|
+
timeoutSeconds: CAPACITY_PROBE_TIMEOUT_MS / 1000,
|
|
6666
|
+
waitMs: CAPACITY_PROBE_TIMEOUT_MS,
|
|
6667
|
+
});
|
|
6668
|
+
}
|
|
6669
|
+
catch {
|
|
6670
|
+
throw new LanesError("outpost_question_list_unavailable", `Could not read questions from Outpost ${outpostId}`, { outpost_id: outpostId });
|
|
6671
|
+
}
|
|
6672
|
+
if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
|
|
6673
|
+
throw new LanesError("outpost_question_list_unavailable", `Outpost ${outpostId} did not return a successful question list`, { outpost_id: outpostId });
|
|
6674
|
+
}
|
|
6675
|
+
let body;
|
|
6676
|
+
try {
|
|
6677
|
+
body = JSON.parse(result.stdout);
|
|
6678
|
+
}
|
|
6679
|
+
catch {
|
|
6680
|
+
throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned invalid question-list JSON`, { outpost_id: outpostId });
|
|
6681
|
+
}
|
|
6682
|
+
if (body === null ||
|
|
6683
|
+
typeof body !== "object" ||
|
|
6684
|
+
Array.isArray(body) ||
|
|
6685
|
+
body.ok !== true ||
|
|
6686
|
+
body.action !== "questions.list" ||
|
|
6687
|
+
!Array.isArray(body.questions)) {
|
|
6688
|
+
throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned a malformed question list`, { outpost_id: outpostId });
|
|
6689
|
+
}
|
|
6690
|
+
const rows = body.questions;
|
|
6691
|
+
const questions = [];
|
|
6692
|
+
for (const [index, row] of rows.entries()) {
|
|
6693
|
+
if (row === null || typeof row !== "object" || Array.isArray(row)) {
|
|
6694
|
+
throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, index });
|
|
6695
|
+
}
|
|
6696
|
+
const rowObject = row;
|
|
6697
|
+
let question;
|
|
6698
|
+
try {
|
|
6699
|
+
question = parseQuestionRecord(row);
|
|
6700
|
+
}
|
|
6701
|
+
catch {
|
|
6702
|
+
throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, index });
|
|
6703
|
+
}
|
|
6704
|
+
const audience = parseOutpostQuestionAudience(rowObject.audience, outpostId, index);
|
|
6705
|
+
questions.push({ question, audience });
|
|
6706
|
+
}
|
|
6707
|
+
return questions;
|
|
6708
|
+
}
|
|
6709
|
+
function parseOutpostQuestionAudience(raw, outpostId, index) {
|
|
6710
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
6711
|
+
throw new LanesError("outpost_question_audience_invalid", `Outpost ${outpostId} returned a question without a valid owner-computed audience`, { outpost_id: outpostId, index });
|
|
6712
|
+
}
|
|
6713
|
+
const audience = raw;
|
|
6714
|
+
try {
|
|
6715
|
+
if (audience.kind === "user" && audience.id === undefined) {
|
|
6716
|
+
return parseSeniorRef("user");
|
|
6717
|
+
}
|
|
6718
|
+
if (typeof audience.kind !== "string" || typeof audience.id !== "string") {
|
|
6719
|
+
throw new Error("audience kind and id are invalid");
|
|
6720
|
+
}
|
|
6721
|
+
return parseSeniorRef(`${audience.kind}:${audience.id}`);
|
|
6722
|
+
}
|
|
6723
|
+
catch {
|
|
6724
|
+
throw new LanesError("outpost_question_audience_invalid", `Outpost ${outpostId} returned an invalid owner-computed question audience`, { outpost_id: outpostId, index });
|
|
6725
|
+
}
|
|
6726
|
+
}
|
|
6727
|
+
function parseOutpostQuestionBody(raw, outpostId, questionId) {
|
|
6728
|
+
if (raw === null)
|
|
6729
|
+
return null;
|
|
6730
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
6731
|
+
throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned a malformed body for question ${questionId}`, { outpost_id: outpostId, question_id: questionId });
|
|
6732
|
+
}
|
|
6733
|
+
const body = raw;
|
|
6734
|
+
if (typeof body.question !== "string" ||
|
|
6735
|
+
typeof body.context !== "string" ||
|
|
6736
|
+
!Array.isArray(body.options) ||
|
|
6737
|
+
body.options.some((option) => typeof option !== "string") ||
|
|
6738
|
+
(body.recommended !== undefined && typeof body.recommended !== "string") ||
|
|
6739
|
+
(body.notes !== undefined && typeof body.notes !== "string")) {
|
|
6740
|
+
throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned a malformed body for question ${questionId}`, { outpost_id: outpostId, question_id: questionId });
|
|
6741
|
+
}
|
|
6742
|
+
return parseQuestionBody(raw);
|
|
6743
|
+
}
|
|
6744
|
+
async function readOutpostQuestionDetail(ctx, owner) {
|
|
6745
|
+
if (owner.lane.host.kind !== "outpost") {
|
|
6746
|
+
throw new LanesError("outpost_question_owner_invalid", `Question ${owner.question.question_id} does not have an Outpost owner`, { question_id: owner.question.question_id, lane_id: owner.lane.lane_id });
|
|
6747
|
+
}
|
|
6748
|
+
const outpostId = owner.lane.host.id;
|
|
6749
|
+
let result;
|
|
6750
|
+
try {
|
|
6751
|
+
result = await ctx.dispatchOutpostExec({
|
|
6752
|
+
outpostId,
|
|
6753
|
+
command: buildOutpostQuestionShowCommand(owner.question.question_id),
|
|
6754
|
+
timeoutSeconds: CAPACITY_PROBE_TIMEOUT_MS / 1000,
|
|
6755
|
+
waitMs: CAPACITY_PROBE_TIMEOUT_MS,
|
|
6756
|
+
});
|
|
6757
|
+
}
|
|
6758
|
+
catch {
|
|
6759
|
+
throw new LanesError("outpost_question_show_unavailable", `Could not read question ${owner.question.question_id} from Outpost ${outpostId}`, { outpost_id: outpostId, question_id: owner.question.question_id });
|
|
6760
|
+
}
|
|
6761
|
+
if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
|
|
6762
|
+
throw new LanesError("outpost_question_show_unavailable", `Outpost ${outpostId} did not return question ${owner.question.question_id}`, { outpost_id: outpostId, question_id: owner.question.question_id });
|
|
6763
|
+
}
|
|
6764
|
+
let raw;
|
|
6765
|
+
try {
|
|
6766
|
+
raw = JSON.parse(result.stdout);
|
|
6767
|
+
}
|
|
6768
|
+
catch {
|
|
6769
|
+
throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned invalid question-show JSON`, { outpost_id: outpostId, question_id: owner.question.question_id });
|
|
6770
|
+
}
|
|
6771
|
+
if (raw === null ||
|
|
6772
|
+
typeof raw !== "object" ||
|
|
6773
|
+
Array.isArray(raw) ||
|
|
6774
|
+
raw.ok !== true ||
|
|
6775
|
+
raw.action !== "questions.show" ||
|
|
6776
|
+
!Object.prototype.hasOwnProperty.call(raw, "body")) {
|
|
6777
|
+
throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned a malformed question-show response`, { outpost_id: outpostId, question_id: owner.question.question_id });
|
|
6778
|
+
}
|
|
6779
|
+
const response = raw;
|
|
6780
|
+
let question;
|
|
6781
|
+
try {
|
|
6782
|
+
question = parseQuestionRecord(response.question);
|
|
6783
|
+
}
|
|
6784
|
+
catch {
|
|
6785
|
+
throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, question_id: owner.question.question_id });
|
|
6786
|
+
}
|
|
6787
|
+
if (question.question_id !== owner.question.question_id ||
|
|
6788
|
+
question.lane_id !== owner.lane.lane_id ||
|
|
6789
|
+
!sameQuestionCompany(question.company, owner.lane.company)) {
|
|
6790
|
+
throw new LanesError("outpost_question_company_mismatch", `Outpost ${outpostId} returned question ${owner.question.question_id} for a different lane or company`, { outpost_id: outpostId, question_id: owner.question.question_id });
|
|
6791
|
+
}
|
|
6792
|
+
const audience = parseOutpostQuestionAudience(response.audience, outpostId, 0);
|
|
6793
|
+
const body = parseOutpostQuestionBody(response.body, outpostId, question.question_id);
|
|
6794
|
+
return { question, audience, body };
|
|
6795
|
+
}
|
|
6796
|
+
async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
6797
|
+
const hostedLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === "outpost" &&
|
|
6798
|
+
(!companyFilter || questionCompanyRefMatches(lane.company, companyFilter)));
|
|
6799
|
+
const lanesByOutpost = new Map();
|
|
6800
|
+
for (const lane of hostedLanes) {
|
|
6801
|
+
if (lane.host.kind !== "outpost")
|
|
6802
|
+
continue;
|
|
6803
|
+
const lanes = lanesByOutpost.get(lane.host.id) ?? [];
|
|
6804
|
+
lanes.push(lane);
|
|
6805
|
+
lanesByOutpost.set(lane.host.id, lanes);
|
|
6806
|
+
}
|
|
6807
|
+
const questions = [];
|
|
6808
|
+
const errors = [];
|
|
6809
|
+
const eligibleGroups = [];
|
|
6810
|
+
for (const [outpostId, lanes] of [...lanesByOutpost.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
6811
|
+
for (const group of groupQuestionLanesByCompany(lanes)) {
|
|
6812
|
+
if (companyFilter && !questionCompanyRefMatches(group.company, companyFilter))
|
|
6813
|
+
continue;
|
|
6814
|
+
if (group.company === null) {
|
|
6815
|
+
errors.push({
|
|
6816
|
+
host: `outpost:${outpostId}`,
|
|
6817
|
+
error: "outpost_question_company_unavailable",
|
|
6818
|
+
});
|
|
6819
|
+
continue;
|
|
6820
|
+
}
|
|
6821
|
+
if (await ctx.laneOutpostQuestionVisibilityEnabled(group.company)) {
|
|
6822
|
+
eligibleGroups.push({ outpostId, group });
|
|
6823
|
+
}
|
|
6824
|
+
}
|
|
6825
|
+
}
|
|
6826
|
+
if (eligibleGroups.length === 0) {
|
|
6827
|
+
return { questions, errors: [...new Map(errors.map((entry) => [entry.host, entry])).values()] };
|
|
6828
|
+
}
|
|
6829
|
+
let knownOutpostIds;
|
|
6830
|
+
try {
|
|
6831
|
+
knownOutpostIds = await ctx.listOutpostIds();
|
|
6832
|
+
}
|
|
6833
|
+
catch {
|
|
6834
|
+
return {
|
|
6835
|
+
questions,
|
|
6836
|
+
errors: [...new Map([
|
|
6837
|
+
...errors,
|
|
6838
|
+
...[...new Set(eligibleGroups.map(({ outpostId }) => outpostId))].map((outpostId) => ({
|
|
6839
|
+
host: `outpost:${outpostId}`,
|
|
6840
|
+
error: "outpost_list_unavailable",
|
|
6841
|
+
})),
|
|
6842
|
+
].map((entry) => [entry.host, entry])).values()],
|
|
6843
|
+
};
|
|
6844
|
+
}
|
|
6845
|
+
const normalizedOutpostIds = knownOutpostIds.map((id) => typeof id === "string" ? id.trim() : "");
|
|
6846
|
+
if (knownOutpostIds.length > MAX_PLACEMENT_OUTPOSTS ||
|
|
6847
|
+
normalizedOutpostIds.some((id) => id === "") ||
|
|
6848
|
+
new Set(normalizedOutpostIds).size !== normalizedOutpostIds.length) {
|
|
6849
|
+
throw new LanesError("outpost_list_invalid", "Outpost list is malformed or exceeds the placement limit", { count: knownOutpostIds.length, maximum: MAX_PLACEMENT_OUTPOSTS });
|
|
6850
|
+
}
|
|
6851
|
+
const knownOutpostIdSet = new Set(normalizedOutpostIds);
|
|
6852
|
+
const loadedGroups = await mapWithConcurrency(eligibleGroups, OUTPOST_QUESTION_LOOKUP_CONCURRENCY, async ({ outpostId, group }) => {
|
|
6853
|
+
if (!knownOutpostIdSet.has(outpostId)) {
|
|
6854
|
+
return {
|
|
6855
|
+
questions: [],
|
|
6856
|
+
errors: [{ host: `outpost:${outpostId}`, error: "unknown_outpost" }],
|
|
6857
|
+
};
|
|
6858
|
+
}
|
|
6859
|
+
try {
|
|
6860
|
+
const remoteQuestions = await readOutpostQuestionRecords(ctx, outpostId, group.company);
|
|
6861
|
+
const localLanesById = new Map(group.lanes.map((lane) => [lane.lane_id, lane]));
|
|
6862
|
+
const groupQuestions = [];
|
|
6863
|
+
const seenQuestionIds = new Set();
|
|
6864
|
+
for (const { question, audience } of remoteQuestions) {
|
|
6865
|
+
const lane = localLanesById.get(question.lane_id);
|
|
6866
|
+
if (!lane)
|
|
6867
|
+
continue;
|
|
6868
|
+
if (!sameQuestionCompany(question.company, lane.company)) {
|
|
6869
|
+
throw new LanesError("outpost_question_company_mismatch", `Outpost ${outpostId} question ${question.question_id} does not match its primary lane company`, { outpost_id: outpostId, question_id: question.question_id });
|
|
6870
|
+
}
|
|
6871
|
+
if (seenQuestionIds.has(question.question_id)) {
|
|
6872
|
+
throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned a duplicate question id`, { outpost_id: outpostId, question_id: question.question_id });
|
|
6873
|
+
}
|
|
6874
|
+
seenQuestionIds.add(question.question_id);
|
|
6875
|
+
groupQuestions.push({
|
|
6876
|
+
question,
|
|
6877
|
+
lane,
|
|
6878
|
+
host: `outpost:${outpostId}`,
|
|
6879
|
+
audience,
|
|
6880
|
+
});
|
|
6881
|
+
}
|
|
6882
|
+
return { questions: groupQuestions, errors: [] };
|
|
6883
|
+
}
|
|
6884
|
+
catch (err) {
|
|
6885
|
+
return {
|
|
6886
|
+
questions: [],
|
|
6887
|
+
errors: [{
|
|
6888
|
+
host: `outpost:${outpostId}`,
|
|
6889
|
+
error: isLanesError(err) ? err.error : "outpost_question_list_unavailable",
|
|
6890
|
+
}],
|
|
6891
|
+
};
|
|
6892
|
+
}
|
|
6893
|
+
});
|
|
6894
|
+
questions.push(...loadedGroups.flatMap(({ questions: groupQuestions }) => groupQuestions));
|
|
6895
|
+
errors.push(...loadedGroups.flatMap(({ errors: groupErrors }) => groupErrors));
|
|
6896
|
+
return {
|
|
6897
|
+
questions,
|
|
6898
|
+
errors: [...new Map(errors.map((entry) => [entry.host, entry])).values()],
|
|
6899
|
+
};
|
|
6900
|
+
}
|
|
6585
6901
|
async function runQuestionsList(ctx, opts) {
|
|
6586
6902
|
const hqRoot = ctx.hqRoot();
|
|
6587
6903
|
const status = opts.status ? parseQuestionStatus(opts.status) : undefined;
|
|
@@ -6589,7 +6905,8 @@ async function runQuestionsList(ctx, opts) {
|
|
|
6589
6905
|
let company;
|
|
6590
6906
|
if (opts.company)
|
|
6591
6907
|
company = await ctx.resolveCompany(opts.company);
|
|
6592
|
-
const
|
|
6908
|
+
const localLanes = opts.allHosts ? new Map(loadAllLanes(hqRoot).map((lane) => [lane.lane_id, lane])) : undefined;
|
|
6909
|
+
const localQuestions = loadAllQuestions(hqRoot)
|
|
6593
6910
|
.filter((q) => {
|
|
6594
6911
|
if (status && q.status !== status)
|
|
6595
6912
|
return false;
|
|
@@ -6599,26 +6916,116 @@ async function runQuestionsList(ctx, opts) {
|
|
|
6599
6916
|
return false;
|
|
6600
6917
|
if (opts.story && q.story_id !== opts.story)
|
|
6601
6918
|
return false;
|
|
6602
|
-
if (company && !
|
|
6919
|
+
if (company && !(opts.allHosts
|
|
6920
|
+
? questionCompanyRefMatches(q.company, company)
|
|
6921
|
+
: companyRefMatches(q.company, company))) {
|
|
6603
6922
|
return false;
|
|
6604
6923
|
}
|
|
6924
|
+
if (company && localLanes) {
|
|
6925
|
+
const lane = localLanes.get(q.lane_id);
|
|
6926
|
+
if (!lane || !questionCompanyRefMatches(lane.company, company) || !sameQuestionCompany(q.company, lane.company)) {
|
|
6927
|
+
return false;
|
|
6928
|
+
}
|
|
6929
|
+
}
|
|
6605
6930
|
if (forFilter) {
|
|
6606
6931
|
const audience = resolveAudience(hqRoot, q, ctx);
|
|
6607
6932
|
if (!audienceMatches(audience, forFilter))
|
|
6608
6933
|
return false;
|
|
6609
6934
|
}
|
|
6610
6935
|
return true;
|
|
6611
|
-
})
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6936
|
+
});
|
|
6937
|
+
const outpostLoad = opts.allHosts
|
|
6938
|
+
? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company)
|
|
6939
|
+
: { questions: [], errors: [] };
|
|
6940
|
+
const remoteQuestionIds = new Set(outpostLoad.questions.map(({ question }) => question.question_id));
|
|
6941
|
+
const questions = [
|
|
6942
|
+
...localQuestions
|
|
6943
|
+
.filter((q) => !remoteQuestionIds.has(q.question_id))
|
|
6944
|
+
.map((q) => {
|
|
6945
|
+
const row = {
|
|
6946
|
+
...q,
|
|
6947
|
+
audience: resolveAudience(hqRoot, q, ctx),
|
|
6948
|
+
...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
|
|
6949
|
+
};
|
|
6950
|
+
if (!opts.allHosts)
|
|
6951
|
+
return row;
|
|
6952
|
+
const lane = localLanes?.get(q.lane_id);
|
|
6953
|
+
const host = lane?.host.kind === "outpost" ? `outpost:${lane.host.id}` : "local";
|
|
6954
|
+
return { ...row, host };
|
|
6955
|
+
}),
|
|
6956
|
+
...outpostLoad.questions
|
|
6957
|
+
.filter(({ question: q, lane, audience }) => {
|
|
6958
|
+
if (status && q.status !== status)
|
|
6959
|
+
return false;
|
|
6960
|
+
if (opts.from && q.lane_id !== opts.from && q.origin.id !== opts.from)
|
|
6961
|
+
return false;
|
|
6962
|
+
if (opts.project && q.project_id !== opts.project)
|
|
6963
|
+
return false;
|
|
6964
|
+
if (opts.story && q.story_id !== opts.story)
|
|
6965
|
+
return false;
|
|
6966
|
+
if (company && (!questionCompanyRefMatches(q.company, company) ||
|
|
6967
|
+
!questionCompanyRefMatches(lane.company, company) ||
|
|
6968
|
+
!sameQuestionCompany(q.company, lane.company))) {
|
|
6969
|
+
return false;
|
|
6970
|
+
}
|
|
6971
|
+
if (forFilter) {
|
|
6972
|
+
if (!audienceMatches(audience, forFilter))
|
|
6973
|
+
return false;
|
|
6974
|
+
}
|
|
6975
|
+
return true;
|
|
6976
|
+
})
|
|
6977
|
+
.map(({ question: q, host, audience }) => ({
|
|
6978
|
+
...q,
|
|
6979
|
+
host,
|
|
6980
|
+
audience,
|
|
6981
|
+
...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
|
|
6982
|
+
})),
|
|
6983
|
+
];
|
|
6984
|
+
printJson({
|
|
6985
|
+
ok: true,
|
|
6986
|
+
action: "questions.list",
|
|
6987
|
+
questions,
|
|
6988
|
+
...(outpostLoad.errors.length > 0 ? { errors: outpostLoad.errors } : {}),
|
|
6989
|
+
});
|
|
6618
6990
|
}
|
|
6619
6991
|
async function runQuestionsShow(ctx, questionId) {
|
|
6620
6992
|
const hqRoot = ctx.hqRoot();
|
|
6621
|
-
|
|
6993
|
+
let question;
|
|
6994
|
+
try {
|
|
6995
|
+
question = readQuestion(hqRoot, questionId);
|
|
6996
|
+
}
|
|
6997
|
+
catch (err) {
|
|
6998
|
+
if (!isLanesError(err) || err.error !== "question_not_found")
|
|
6999
|
+
throw err;
|
|
7000
|
+
const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
|
|
7001
|
+
if (lookup.errors.length > 0) {
|
|
7002
|
+
throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
|
|
7003
|
+
question_id: questionId,
|
|
7004
|
+
hosts: lookup.errors.map(({ host, error }) => ({ host, error })),
|
|
7005
|
+
});
|
|
7006
|
+
}
|
|
7007
|
+
const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
|
|
7008
|
+
if (matches.length === 0) {
|
|
7009
|
+
throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
|
|
7010
|
+
}
|
|
7011
|
+
if (matches.length > 1) {
|
|
7012
|
+
throw new LanesError("outpost_question_ambiguous", `Question ${questionId} is present on more than one Outpost`, { question_id: questionId, owner_count: matches.length });
|
|
7013
|
+
}
|
|
7014
|
+
const owner = matches[0];
|
|
7015
|
+
if (!owner) {
|
|
7016
|
+
throw new LanesError("outpost_question_ambiguous", `Question ${questionId} has no unique Outpost owner`, { question_id: questionId });
|
|
7017
|
+
}
|
|
7018
|
+
const detail = await readOutpostQuestionDetail(ctx, owner);
|
|
7019
|
+
printJson({
|
|
7020
|
+
ok: true,
|
|
7021
|
+
action: "questions.show",
|
|
7022
|
+
question: detail.question,
|
|
7023
|
+
audience: detail.audience,
|
|
7024
|
+
body: detail.body,
|
|
7025
|
+
host: owner.host,
|
|
7026
|
+
});
|
|
7027
|
+
return;
|
|
7028
|
+
}
|
|
6622
7029
|
const body = readQuestionBody(hqRoot, question);
|
|
6623
7030
|
printJson({
|
|
6624
7031
|
ok: true,
|
|
@@ -6630,11 +7037,56 @@ async function runQuestionsShow(ctx, questionId) {
|
|
|
6630
7037
|
}
|
|
6631
7038
|
async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
6632
7039
|
const hqRoot = ctx.hqRoot();
|
|
6633
|
-
const
|
|
7040
|
+
const company = opts.company ? await ctx.resolveCompany(opts.company) : undefined;
|
|
7041
|
+
let question;
|
|
7042
|
+
try {
|
|
7043
|
+
question = readQuestion(hqRoot, questionId);
|
|
7044
|
+
}
|
|
7045
|
+
catch (err) {
|
|
7046
|
+
if (!isLanesError(err) || err.error !== "question_not_found")
|
|
7047
|
+
throw err;
|
|
7048
|
+
const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company);
|
|
7049
|
+
if (lookup.errors.length > 0) {
|
|
7050
|
+
throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
|
|
7051
|
+
question_id: questionId,
|
|
7052
|
+
hosts: lookup.errors.map(({ host, error }) => ({ host, error })),
|
|
7053
|
+
});
|
|
7054
|
+
}
|
|
7055
|
+
const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
|
|
7056
|
+
if (matches.length === 0) {
|
|
7057
|
+
throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
|
|
7058
|
+
}
|
|
7059
|
+
if (matches.length > 1) {
|
|
7060
|
+
throw new LanesError("outpost_question_ambiguous", `Question ${questionId} is present on more than one Outpost`, { question_id: questionId, owner_count: matches.length });
|
|
7061
|
+
}
|
|
7062
|
+
const owner = matches[0];
|
|
7063
|
+
if (!owner) {
|
|
7064
|
+
throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
|
|
7065
|
+
}
|
|
7066
|
+
if (!sameQuestionCompany(owner.question.company, owner.lane.company)) {
|
|
7067
|
+
throw new LanesError("outpost_question_company_mismatch", `Question ${questionId} does not match its primary lane company`, { question_id: questionId, lane_id: owner.lane.lane_id });
|
|
7068
|
+
}
|
|
7069
|
+
throwIfQuestionNotPending(owner.question);
|
|
7070
|
+
await yieldEventLoop();
|
|
7071
|
+
await dispatchOutpostQuestionAnswer(ctx, owner.lane, questionId, {
|
|
7072
|
+
...opts,
|
|
7073
|
+
company: questionListCompanyArg(owner.lane.company),
|
|
7074
|
+
});
|
|
7075
|
+
return;
|
|
7076
|
+
}
|
|
6634
7077
|
await yieldEventLoop();
|
|
6635
7078
|
const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
|
|
7079
|
+
if (company &&
|
|
7080
|
+
(!questionCompanyRefMatches(question.company, company) ||
|
|
7081
|
+
!questionCompanyRefMatches(askingLane.company, company) ||
|
|
7082
|
+
!sameQuestionCompany(question.company, askingLane.company))) {
|
|
7083
|
+
throw new LanesError("question_company_mismatch", `Question ${questionId} does not belong to the requested company`, { question_id: questionId, lane_id: askingLane.lane_id });
|
|
7084
|
+
}
|
|
6636
7085
|
if (askingLane.host.kind === "outpost" && !laneIsHostedHere(ctx, askingLane)) {
|
|
6637
|
-
await dispatchOutpostQuestionAnswer(ctx, askingLane, questionId,
|
|
7086
|
+
await dispatchOutpostQuestionAnswer(ctx, askingLane, questionId, {
|
|
7087
|
+
...opts,
|
|
7088
|
+
...(askingLane.company ? { company: questionListCompanyArg(askingLane.company) } : {}),
|
|
7089
|
+
});
|
|
6638
7090
|
return;
|
|
6639
7091
|
}
|
|
6640
7092
|
throwIfQuestionNotPending(question);
|
|
@@ -7587,6 +8039,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7587
8039
|
questions
|
|
7588
8040
|
.command("list")
|
|
7589
8041
|
.description("List questions")
|
|
8042
|
+
.option("--all-hosts", "Include questions from Outposts that own this primary's lanes")
|
|
7590
8043
|
.option("--for <ref>", "Who must decide (ref | me | user)")
|
|
7591
8044
|
.option("--from <lane-id>", "Which lane asked")
|
|
7592
8045
|
.option("--company <slug|uid>", "Filter by company")
|
|
@@ -7610,6 +8063,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7610
8063
|
.option("--choice <label|N>", "1-based index or option label")
|
|
7611
8064
|
.option("--notes <text>", "Free-text notes stored with the body")
|
|
7612
8065
|
.option("--freeform <text>", "Supply a free-form answer for a question with options")
|
|
8066
|
+
.option("--company <slug|uid>", "Require the question lane to match this company")
|
|
7613
8067
|
.action(wrap(async (questionId, opts) => {
|
|
7614
8068
|
await runQuestionsAnswer(ctx, questionId, opts);
|
|
7615
8069
|
}));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
2
2
|
import { createFlagClient, } from "@indigoai-us/hq-flags-client";
|
|
3
|
+
import { resolveProcessFlagCompanyContext } from "../flag-registry.js";
|
|
4
|
+
import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
|
|
3
5
|
export const HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG = "hook-timeout-debug-context";
|
|
4
6
|
export const HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_FALLBACK = false;
|
|
5
7
|
export const HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_TIMEOUT_MS = 150;
|
|
@@ -20,12 +22,11 @@ function combineSignals(first, second) {
|
|
|
20
22
|
*/
|
|
21
23
|
export async function hookTimeoutDebugContextEnabled(dependencies = {}, callerSignal) {
|
|
22
24
|
const env = dependencies.env ?? process.env;
|
|
23
|
-
const endpoint = env
|
|
24
|
-
const companyUid = env
|
|
25
|
-
if (!
|
|
25
|
+
const endpoint = resolveFlagRegistryEndpoint(undefined, env);
|
|
26
|
+
const { companyUid, companySlug } = resolveProcessFlagCompanyContext(env);
|
|
27
|
+
if (!companyUid) {
|
|
26
28
|
return HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_FALLBACK;
|
|
27
29
|
}
|
|
28
|
-
const companySlug = env.HQ_COMPANY_SLUG?.trim() || "";
|
|
29
30
|
const deadline = AbortSignal.timeout(HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_TIMEOUT_MS);
|
|
30
31
|
const signal = callerSignal ? AbortSignal.any([callerSignal, deadline]) : deadline;
|
|
31
32
|
const doFetch = dependencies.fetch ?? globalThis.fetch;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
2
2
|
import { createFlagClient, } from "@indigoai-us/hq-flags-client";
|
|
3
|
+
import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
|
|
3
4
|
export const CLIENT_HEALTH_REASON_CODES_FLAG = "client-health-sync-reason-codes";
|
|
4
5
|
export const CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK = false;
|
|
5
6
|
export const CLIENT_HEALTH_REASON_CODES_FLAG_TIMEOUT_MS = 150;
|
|
@@ -32,9 +33,9 @@ export async function clientHealthReasonCodesEnabled(dependencies = {}, callerSi
|
|
|
32
33
|
/** Resolve the same default-off gate for one explicitly identified company. */
|
|
33
34
|
export async function clientHealthReasonCodesEnabledForCompany(company, dependencies = {}, callerSignal) {
|
|
34
35
|
const env = dependencies.env ?? process.env;
|
|
35
|
-
const endpoint = env
|
|
36
|
+
const endpoint = resolveFlagRegistryEndpoint(undefined, env);
|
|
36
37
|
const companyUid = company.companyUid.trim();
|
|
37
|
-
if (
|
|
38
|
+
if (!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
|
|
38
39
|
return CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK;
|
|
39
40
|
}
|
|
40
41
|
const companySlug = company.companySlug?.trim() || "";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const FLAG_REGISTRY_ENDPOINT_ENV = "HQ_FLAGS_API_URL";
|
|
2
|
+
export declare const FLAG_REGISTRY_DEFAULT_ENDPOINT = "https://hqapi.hq.computer";
|
|
3
|
+
/** Resolve one shared endpoint from an explicit option, environment, or service default. */
|
|
4
|
+
export declare function resolveFlagRegistryEndpoint(explicitEndpoint?: string, env?: NodeJS.ProcessEnv): string;
|
|
5
|
+
//# sourceMappingURL=flag-registry-endpoint.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const FLAG_REGISTRY_ENDPOINT_ENV = "HQ_FLAGS_API_URL";
|
|
2
|
+
export const FLAG_REGISTRY_DEFAULT_ENDPOINT = "https://hqapi.hq.computer";
|
|
3
|
+
/** Resolve one shared endpoint from an explicit option, environment, or service default. */
|
|
4
|
+
export function resolveFlagRegistryEndpoint(explicitEndpoint, env = process.env) {
|
|
5
|
+
return explicitEndpoint?.trim()
|
|
6
|
+
|| env[FLAG_REGISTRY_ENDPOINT_ENV]?.trim()
|
|
7
|
+
|| FLAG_REGISTRY_DEFAULT_ENDPOINT;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=flag-registry-endpoint.js.map
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type WorkerOptions } from "node:worker_threads";
|
|
9
9
|
import { type FlagClient, type FlagLookupOptions, type FlagSnapshot } from "@indigoai-us/hq-flags-client";
|
|
10
|
-
|
|
11
|
-
export declare const FLAG_REGISTRY_ENDPOINT_ENV = "HQ_FLAGS_API_URL";
|
|
10
|
+
export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
|
|
12
11
|
/** The synchronous capability that command gate call sites need. */
|
|
13
12
|
export type FlagReader = Pick<FlagClient, "isEnabled">;
|
|
14
13
|
interface WorkerLike {
|
|
@@ -20,6 +19,12 @@ export interface FlagRegistryDependencies {
|
|
|
20
19
|
endpoint?: string;
|
|
21
20
|
createWorker?: (filename: URL, options: WorkerOptions) => WorkerLike;
|
|
22
21
|
}
|
|
22
|
+
export interface ProcessFlagCompanyContext {
|
|
23
|
+
companyUid?: string;
|
|
24
|
+
companySlug?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Resolve the same company identity used by the process-wide flag reader. */
|
|
27
|
+
export declare function resolveProcessFlagCompanyContext(env?: NodeJS.ProcessEnv): ProcessFlagCompanyContext;
|
|
23
28
|
/**
|
|
24
29
|
* Begin one registry refresh without awaiting it or retaining the CLI process.
|
|
25
30
|
*
|
|
@@ -46,5 +51,4 @@ export declare function resolveProcessFlagGate(flagKey: string, lookup: FlagLook
|
|
|
46
51
|
export declare function currentFlagSnapshot(): FlagSnapshot | null;
|
|
47
52
|
/** Test-only reset for process-global readiness state. */
|
|
48
53
|
export declare function _resetFlagRegistryForTests(): void;
|
|
49
|
-
export {};
|
|
50
54
|
//# sourceMappingURL=flag-registry.d.ts.map
|
|
@@ -12,8 +12,8 @@ import { readSessionState } from "./work-context/state.js";
|
|
|
12
12
|
import { workContextRoot } from "./work-context/paths.js";
|
|
13
13
|
import { readCompaniesManifestMap } from "./work-context/repo-remote.js";
|
|
14
14
|
import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
|
|
15
|
-
|
|
16
|
-
export
|
|
15
|
+
import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
|
|
16
|
+
export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
|
|
17
17
|
const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
|
|
18
18
|
const COMPANY_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
19
19
|
let heldSnapshot = null;
|
|
@@ -76,6 +76,15 @@ function companyUidForContext(context, env = process.env) {
|
|
|
76
76
|
const manifestUid = readCompaniesManifestMap(hqRoot)?.[context.companySlug]?.cloud_uid;
|
|
77
77
|
return typeof manifestUid === "string" ? normalizedCompanyUid(manifestUid) : undefined;
|
|
78
78
|
}
|
|
79
|
+
/** Resolve the same company identity used by the process-wide flag reader. */
|
|
80
|
+
export function resolveProcessFlagCompanyContext(env = process.env) {
|
|
81
|
+
const context = processCompanyContext(env);
|
|
82
|
+
const companyUid = companyUidForContext(context, env);
|
|
83
|
+
return {
|
|
84
|
+
...(companyUid ? { companyUid } : {}),
|
|
85
|
+
...(context.companySlug ? { companySlug: context.companySlug } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
79
88
|
function processSnapshotReader(companyIdentifiers) {
|
|
80
89
|
return {
|
|
81
90
|
isEnabled(flagKey, lookup = {}) {
|
|
@@ -101,11 +110,9 @@ function processSnapshotReader(companyIdentifiers) {
|
|
|
101
110
|
export function kickFlagRegistryReadiness(dependencies = {}) {
|
|
102
111
|
if (processReader)
|
|
103
112
|
return processReader;
|
|
104
|
-
const endpoint = (dependencies.endpoint
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const company = processCompanyContext();
|
|
108
|
-
const companyUid = companyUidForContext(company);
|
|
113
|
+
const endpoint = resolveFlagRegistryEndpoint(dependencies.endpoint, process.env);
|
|
114
|
+
const company = resolveProcessFlagCompanyContext();
|
|
115
|
+
const companyUid = company.companyUid;
|
|
109
116
|
const companyIdentifiers = [companyUid, company.companySlug].filter((value) => Boolean(value));
|
|
110
117
|
processReader = processSnapshotReader(companyIdentifiers);
|
|
111
118
|
try {
|
|
@@ -149,6 +156,11 @@ export function resolveFlagGate(reader, flagKey, lookup, legacyFallback) {
|
|
|
149
156
|
}
|
|
150
157
|
/** Resolve a process gate against the shared snapshot reader, when configured. */
|
|
151
158
|
export function resolveProcessFlagGate(flagKey, lookup, legacyFallback) {
|
|
159
|
+
// The endpoint refresh is deliberately asynchronous. Until a validated
|
|
160
|
+
// snapshot arrives, preserve each caller's exact legacy fallback.
|
|
161
|
+
if (!processReader || !heldSnapshot) {
|
|
162
|
+
return localOverrideOrFallback(flagKey, legacyFallback);
|
|
163
|
+
}
|
|
152
164
|
return resolveFlagGate(processReader, flagKey, lookup, legacyFallback);
|
|
153
165
|
}
|
|
154
166
|
/**
|
|
@@ -46,6 +46,7 @@ export declare function buildOutpostQuestionAnswerCommand(input: {
|
|
|
46
46
|
choice?: string;
|
|
47
47
|
freeform?: string;
|
|
48
48
|
notes?: string;
|
|
49
|
+
company?: string;
|
|
49
50
|
hqBinary?: string;
|
|
50
51
|
}): string;
|
|
51
52
|
export interface OutpostExecPort {
|
|
@@ -73,6 +74,12 @@ export declare function buildOutpostPickupCommand(payloadUrl: string): string;
|
|
|
73
74
|
export declare function buildOutpostCapacityCommand(): string;
|
|
74
75
|
/** Read the lane records owned by an Outpost for exact per-account state counts. */
|
|
75
76
|
export declare function buildOutpostLaneListCommand(): string;
|
|
77
|
+
/** Read questions on one Outpost, scoped to a company when supplied. */
|
|
78
|
+
export declare function buildOutpostQuestionListCommand(input?: {
|
|
79
|
+
company?: string;
|
|
80
|
+
}): string;
|
|
81
|
+
/** Read one question's body and owner-computed audience from its Outpost. */
|
|
82
|
+
export declare function buildOutpostQuestionShowCommand(questionId: string): string;
|
|
76
83
|
/** Commit a queued lane move on the host that currently owns its state. */
|
|
77
84
|
export declare function buildOutpostMoveTargetCommand(input: {
|
|
78
85
|
laneId: string;
|
|
@@ -22,6 +22,7 @@ export function buildOutpostQuestionAnswerCommand(input) {
|
|
|
22
22
|
["--choice", input.choice],
|
|
23
23
|
["--freeform", input.freeform],
|
|
24
24
|
["--notes", input.notes],
|
|
25
|
+
["--company", input.company],
|
|
25
26
|
]) {
|
|
26
27
|
if (value !== undefined)
|
|
27
28
|
args.push(flag, value);
|
|
@@ -68,6 +69,24 @@ export function buildOutpostCapacityCommand() {
|
|
|
68
69
|
export function buildOutpostLaneListCommand() {
|
|
69
70
|
return withRemoteHqDir(joinCommandParts(["hq", "lanes", "list", "--json"]));
|
|
70
71
|
}
|
|
72
|
+
/** Read questions on one Outpost, scoped to a company when supplied. */
|
|
73
|
+
export function buildOutpostQuestionListCommand(input = {}) {
|
|
74
|
+
const args = ["hq", "lanes", "questions", "list"];
|
|
75
|
+
const company = input.company?.trim();
|
|
76
|
+
if (company)
|
|
77
|
+
args.push("--company", company);
|
|
78
|
+
return withRemoteHqDir(joinCommandParts(args));
|
|
79
|
+
}
|
|
80
|
+
/** Read one question's body and owner-computed audience from its Outpost. */
|
|
81
|
+
export function buildOutpostQuestionShowCommand(questionId) {
|
|
82
|
+
return withRemoteHqDir(joinCommandParts([
|
|
83
|
+
"hq",
|
|
84
|
+
"lanes",
|
|
85
|
+
"questions",
|
|
86
|
+
"show",
|
|
87
|
+
questionId,
|
|
88
|
+
]));
|
|
89
|
+
}
|
|
71
90
|
/** Commit a queued lane move on the host that currently owns its state. */
|
|
72
91
|
export function buildOutpostMoveTargetCommand(input) {
|
|
73
92
|
return withRemoteHqDir(joinCommandParts([
|
|
@@ -3,6 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { resolveHqBinary, resolveNodeBinary } from "../mesh/live/daemon/install.js";
|
|
5
5
|
import { SYSTEMD_PATH_ENV } from "../service-manager/systemd.js";
|
|
6
|
+
import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
|
|
6
7
|
export const OUTPOST_PRUNER_PRESSURE_SERVICE = "hq-outpost-worktree-pruner-pressure.service";
|
|
7
8
|
export const OUTPOST_PRUNER_PRESSURE_TIMER = "hq-outpost-worktree-pruner-pressure.timer";
|
|
8
9
|
export const OUTPOST_PRUNER_DAILY_SERVICE = "hq-outpost-worktree-pruner-daily.service";
|
|
@@ -160,10 +161,10 @@ export function installOutpostPrunerTimers(options) {
|
|
|
160
161
|
const nodeBinary = options.nodeBinary ?? resolveNodeBinary();
|
|
161
162
|
const hqBinary = options.hqBinary ?? resolveHqBinary();
|
|
162
163
|
const home = options.home ?? os.homedir();
|
|
163
|
-
const flagApiUrl = (options.flagApiUrl
|
|
164
|
+
const flagApiUrl = resolveFlagRegistryEndpoint(options.flagApiUrl);
|
|
164
165
|
const companyUid = (options.companyUid ?? process.env.HQ_COMPANY_UID ?? "").trim();
|
|
165
|
-
if (!
|
|
166
|
-
throw new TypeError("install requires
|
|
166
|
+
if (!companyUid) {
|
|
167
|
+
throw new TypeError("install requires HQ_COMPANY_UID for the timer services");
|
|
167
168
|
}
|
|
168
169
|
const flagEnvironment = [
|
|
169
170
|
systemdEnvironment("HQ_FLAGS_API_URL", flagApiUrl),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
2
2
|
import { createFlagClient } from "@indigoai-us/hq-flags-client";
|
|
3
|
+
import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
|
|
3
4
|
export const OUTPOST_WORKTREE_PRUNER_FLAG = "outpost.worktree-pruner";
|
|
4
5
|
export const OUTPOST_WORKTREE_PRUNER_FLAG_FALLBACK = false;
|
|
5
6
|
export const OUTPOST_WORKTREE_PRUNER_FLAG_TIMEOUT_MS = 5_000;
|
|
@@ -21,14 +22,13 @@ function reportFlagFailure(error) {
|
|
|
21
22
|
}
|
|
22
23
|
export async function outpostWorktreePrunerEnabled(dependencies = {}) {
|
|
23
24
|
const env = dependencies.env ?? process.env;
|
|
24
|
-
const endpoint = env
|
|
25
|
+
const endpoint = resolveFlagRegistryEndpoint(undefined, env);
|
|
25
26
|
const companyUid = env.HQ_COMPANY_UID?.trim() || "";
|
|
26
|
-
if (!
|
|
27
|
+
if (!companyUid) {
|
|
27
28
|
process.stderr.write(`${JSON.stringify({
|
|
28
29
|
event: "outpost.worktree_pruner.flag_config_missing",
|
|
29
30
|
flag: OUTPOST_WORKTREE_PRUNER_FLAG,
|
|
30
31
|
missing: [
|
|
31
|
-
...(!endpoint ? ["HQ_FLAGS_API_URL"] : []),
|
|
32
32
|
...(!companyUid ? ["HQ_COMPANY_UID"] : []),
|
|
33
33
|
],
|
|
34
34
|
fallback: OUTPOST_WORKTREE_PRUNER_FLAG_FALLBACK,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createFlagClient, readLocalOverride, } from "@indigoai-us/hq-flags-client";
|
|
2
2
|
import { ensureCognitoIdToken } from "../../utils/cognito-session.js";
|
|
3
|
-
|
|
3
|
+
import { FLAG_REGISTRY_DEFAULT_ENDPOINT, resolveFlagRegistryEndpoint, } from "../flag-registry-endpoint.js";
|
|
4
|
+
export const LANES_FLAG_API_DEFAULT_ENDPOINT = FLAG_REGISTRY_DEFAULT_ENDPOINT;
|
|
4
5
|
export const LANES_FLAG_READ_TIMEOUT_MS = 5_000;
|
|
5
6
|
const registryReads = new Map();
|
|
6
7
|
class MissingFlagTokenError extends Error {
|
|
@@ -45,7 +46,7 @@ export async function readLanesRegistryFlag(flagKey, options = {}) {
|
|
|
45
46
|
const localOverride = readLocalOverride(flagKey, env);
|
|
46
47
|
if (localOverride !== null)
|
|
47
48
|
return localOverride;
|
|
48
|
-
const endpoint = env
|
|
49
|
+
const endpoint = resolveFlagRegistryEndpoint(undefined, env);
|
|
49
50
|
const key = cacheKey(flagKey, options.company, endpoint);
|
|
50
51
|
const cached = registryReads.get(key);
|
|
51
52
|
if (cached)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFlagClient, readLocalOverride } from "@indigoai-us/hq-flags-client";
|
|
2
|
+
import { resolveFlagRegistryEndpoint } from "./lib/flag-registry-endpoint.js";
|
|
2
3
|
export const NODE_NETWORK_FORCE_IPV4_FLAG = "cli.network.force-ipv4";
|
|
3
4
|
export const NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK = false;
|
|
4
5
|
// Node waits up to 1s before trying the next address family. Keep this request
|
|
@@ -29,10 +30,9 @@ export async function nodeNetworkForceIpv4Enabled(dependencies = {}) {
|
|
|
29
30
|
const localOverride = readLocalOverride(NODE_NETWORK_FORCE_IPV4_FLAG, env);
|
|
30
31
|
if (localOverride !== null)
|
|
31
32
|
return localOverride;
|
|
32
|
-
const endpoint = env
|
|
33
|
+
const endpoint = resolveFlagRegistryEndpoint(undefined, env);
|
|
33
34
|
const companyUid = env.HQ_COMPANY_UID?.trim() || "";
|
|
34
|
-
if (
|
|
35
|
-
!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
|
|
35
|
+
if (!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
|
|
36
36
|
return NODE_NETWORK_FORCE_IPV4_FLAG_FALLBACK;
|
|
37
37
|
}
|
|
38
38
|
const companySlug = env.HQ_COMPANY_SLUG?.trim() || "";
|