@indigoai-us/hq-cli 5.219.0 → 5.220.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/commands/lanes.js +58 -51
- package/dist/lib/flag-registry-worker-mode-cache.d.ts +13 -0
- package/dist/lib/flag-registry-worker-mode-cache.js +146 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +2 -0
- package/dist/lib/flag-registry-worker-source.generated.js +3 -0
- package/dist/lib/flag-registry.d.ts +4 -1
- package/dist/lib/flag-registry.js +100 -9
- package/dist/lib/lanes/questions.d.ts +9 -1
- package/dist/lib/lanes/questions.js +84 -5
- package/dist/lib/lanes/reconcile.js +2 -4
- package/dist/lib/lanes/watcher.js +13 -7
- package/dist/sentry.js +41 -0
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.220.0] — 2026-09-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Lane questions now resolve their company from the lane's project before using
|
|
10
|
+
the active HQ company. Project ownership is used only when unique; otherwise
|
|
11
|
+
active-company selection applies without choosing a project owner. Legacy
|
|
12
|
+
questions can be answered with `--company <slug>` when lane or project
|
|
13
|
+
ownership cannot be resolved.
|
|
14
|
+
|
|
15
|
+
- CLI updates no longer interrupt background flag refreshes when the
|
|
16
|
+
self-contained worker rollout is enabled.
|
|
17
|
+
|
|
5
18
|
## [5.219.0] — 2026-09-26
|
|
6
19
|
|
|
7
20
|
### Added
|
package/dist/commands/lanes.js
CHANGED
|
@@ -42,7 +42,7 @@ import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_
|
|
|
42
42
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
43
43
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
44
44
|
import { keepAliveExclusion, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
45
|
-
import { questionBodyAbsPath,
|
|
45
|
+
import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
|
|
46
46
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
47
47
|
import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
48
48
|
import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
|
|
@@ -1581,39 +1581,39 @@ function sameQuestionCompany(a, b) {
|
|
|
1581
1581
|
const bSlug = b.slug.trim();
|
|
1582
1582
|
return aSlug !== "" && aSlug === bSlug;
|
|
1583
1583
|
}
|
|
1584
|
-
function effectiveLaneQuestionCompany(lane) {
|
|
1585
|
-
return lane.company ?? lane.mesh_company ?? null;
|
|
1584
|
+
function effectiveLaneQuestionCompany(lane, hqRoot) {
|
|
1585
|
+
return lane.company ?? lane.mesh_company ?? (hqRoot ? resolveProjectQuestionCompany(hqRoot, lane) : null);
|
|
1586
1586
|
}
|
|
1587
|
-
function questionCompanyForLane(question, lane) {
|
|
1588
|
-
return question.company ?? effectiveLaneQuestionCompany(lane);
|
|
1587
|
+
function questionCompanyForLane(question, lane, hqRoot) {
|
|
1588
|
+
return question.company ?? effectiveLaneQuestionCompany(lane, hqRoot);
|
|
1589
1589
|
}
|
|
1590
|
-
async function resolveActiveQuestionCompany(ctx) {
|
|
1590
|
+
async function resolveActiveQuestionCompany(ctx, message = "Cannot access lane questions because the active HQ company could not be resolved") {
|
|
1591
1591
|
try {
|
|
1592
1592
|
return await ctx.resolveCompany();
|
|
1593
1593
|
}
|
|
1594
1594
|
catch (err) {
|
|
1595
1595
|
if (!(err instanceof CompanySelectionError))
|
|
1596
1596
|
throw err;
|
|
1597
|
-
throw new LanesError("question_company_unavailable",
|
|
1597
|
+
throw new LanesError("question_company_unavailable", message, {});
|
|
1598
1598
|
}
|
|
1599
1599
|
}
|
|
1600
|
-
function assertQuestionCompanyMatchesLane(question, lane, code) {
|
|
1601
|
-
const laneCompany = effectiveLaneQuestionCompany(lane);
|
|
1600
|
+
function assertQuestionCompanyMatchesLane(question, lane, code, hqRoot) {
|
|
1601
|
+
const laneCompany = effectiveLaneQuestionCompany(lane, hqRoot);
|
|
1602
1602
|
if (question.company != null && laneCompany !== null && !sameQuestionCompany(question.company, laneCompany)) {
|
|
1603
1603
|
throw new LanesError(code, `Question ${question.question_id} does not match lane ${lane.lane_id}'s company`, { question_id: question.question_id, lane_id: lane.lane_id });
|
|
1604
1604
|
}
|
|
1605
1605
|
}
|
|
1606
|
-
function assertQuestionCompanyAccess(question, lane, requestedCompany) {
|
|
1607
|
-
assertQuestionCompanyMatchesLane(question, lane, "question_company_mismatch");
|
|
1608
|
-
const questionCompany = questionCompanyForLane(question, lane);
|
|
1606
|
+
function assertQuestionCompanyAccess(question, lane, requestedCompany, hqRoot) {
|
|
1607
|
+
assertQuestionCompanyMatchesLane(question, lane, "question_company_mismatch", hqRoot);
|
|
1608
|
+
const questionCompany = questionCompanyForLane(question, lane, hqRoot);
|
|
1609
1609
|
if (questionCompany !== null && !sameQuestionCompany(questionCompany, requestedCompany)) {
|
|
1610
1610
|
throw new LanesError("question_company_mismatch", `Question ${question.question_id} does not belong to the requested or active company`, { question_id: question.question_id, lane_id: lane.lane_id });
|
|
1611
1611
|
}
|
|
1612
1612
|
}
|
|
1613
|
-
function assertQuestionCompanyResolvedForAnswer(question, lane) {
|
|
1614
|
-
if (questionCompanyForLane(question, lane) !== null)
|
|
1613
|
+
function assertQuestionCompanyResolvedForAnswer(question, lane, hqRoot, resolvedCompany) {
|
|
1614
|
+
if (questionCompanyForLane(question, lane, hqRoot) !== null || resolvedCompany)
|
|
1615
1615
|
return;
|
|
1616
|
-
throw new LanesError("question_company_unavailable", `Cannot answer legacy question ${question.question_id}: its company cannot be resolved from the question or lane
|
|
1616
|
+
throw new LanesError("question_company_unavailable", `Cannot answer legacy question ${question.question_id}: its company cannot be resolved from the question or lane; re-run with --company <slug>`, { question_id: question.question_id, lane_id: lane.lane_id });
|
|
1617
1617
|
}
|
|
1618
1618
|
function questionCompanyRefMatches(recordCompany, filter) {
|
|
1619
1619
|
return recordCompany !== null &&
|
|
@@ -3579,7 +3579,7 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
|
3579
3579
|
});
|
|
3580
3580
|
}
|
|
3581
3581
|
const ownerAnswer = ownerQuestion.answer;
|
|
3582
|
-
const ownerLaneCompany = effectiveLaneQuestionCompany(lane);
|
|
3582
|
+
const ownerLaneCompany = effectiveLaneQuestionCompany(lane, ctx.hqRoot());
|
|
3583
3583
|
if (!isQuestionAnswer(ownerAnswer) ||
|
|
3584
3584
|
ownerQuestion.lane_id !== lane.lane_id ||
|
|
3585
3585
|
(ownerQuestion.company !== null && ownerLaneCompany !== null &&
|
|
@@ -3596,8 +3596,8 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
|
3596
3596
|
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 });
|
|
3597
3597
|
}
|
|
3598
3598
|
if (current.company !== null &&
|
|
3599
|
-
effectiveLaneQuestionCompany(lane) !== null &&
|
|
3600
|
-
!sameQuestionCompany(current.company, effectiveLaneQuestionCompany(lane))) {
|
|
3599
|
+
effectiveLaneQuestionCompany(lane, ctx.hqRoot()) !== null &&
|
|
3600
|
+
!sameQuestionCompany(current.company, effectiveLaneQuestionCompany(lane, ctx.hqRoot()))) {
|
|
3601
3601
|
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 });
|
|
3602
3602
|
}
|
|
3603
3603
|
if (current.company !== null &&
|
|
@@ -3631,8 +3631,8 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
|
3631
3631
|
if (!isLanesError(err) || err.error !== "question_not_found")
|
|
3632
3632
|
throw err;
|
|
3633
3633
|
const primaryLane = readLane(ctx.hqRoot(), lane.lane_id);
|
|
3634
|
-
const primaryCompany = effectiveLaneQuestionCompany(primaryLane);
|
|
3635
|
-
const ownerCompany = effectiveLaneQuestionCompany(lane);
|
|
3634
|
+
const primaryCompany = effectiveLaneQuestionCompany(primaryLane, ctx.hqRoot());
|
|
3635
|
+
const ownerCompany = effectiveLaneQuestionCompany(lane, ctx.hqRoot());
|
|
3636
3636
|
if (primaryCompany !== null && ownerCompany !== null && !sameQuestionCompany(primaryCompany, ownerCompany)) {
|
|
3637
3637
|
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 });
|
|
3638
3638
|
}
|
|
@@ -7092,10 +7092,7 @@ async function runAsk(ctx, opts) {
|
|
|
7092
7092
|
}
|
|
7093
7093
|
const lane = readLane(hqRoot, laneId);
|
|
7094
7094
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "ask");
|
|
7095
|
-
const
|
|
7096
|
-
? undefined
|
|
7097
|
-
: await resolveActiveQuestionCompany(ctx);
|
|
7098
|
-
const questionCompany = resolveQuestionCompany(lane, activeCompany);
|
|
7095
|
+
const questionCompany = await resolveQuestionCompanyWithFallback(hqRoot, lane, () => resolveActiveQuestionCompany(ctx, "Cannot raise a lane question because the active HQ company could not be resolved"));
|
|
7099
7096
|
const optionList = Array.isArray(opts.option)
|
|
7100
7097
|
? opts.option
|
|
7101
7098
|
: opts.option
|
|
@@ -7177,10 +7174,10 @@ async function runAsk(ctx, opts) {
|
|
|
7177
7174
|
});
|
|
7178
7175
|
}
|
|
7179
7176
|
const OUTPOST_QUESTION_LOOKUP_CONCURRENCY = 4;
|
|
7180
|
-
function groupQuestionLanesByCompany(lanes, fallbackCompany) {
|
|
7177
|
+
function groupQuestionLanesByCompany(hqRoot, lanes, fallbackCompany) {
|
|
7181
7178
|
const groups = [];
|
|
7182
7179
|
for (const lane of lanes) {
|
|
7183
|
-
const company = effectiveLaneQuestionCompany(lane) ?? fallbackCompany ?? null;
|
|
7180
|
+
const company = effectiveLaneQuestionCompany(lane, hqRoot) ?? fallbackCompany ?? null;
|
|
7184
7181
|
const existing = groups.find((group) => sameQuestionCompany(group.company, company));
|
|
7185
7182
|
if (existing)
|
|
7186
7183
|
existing.lanes.push(lane);
|
|
@@ -7329,10 +7326,10 @@ async function readOutpostQuestionDetail(ctx, owner) {
|
|
|
7329
7326
|
throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, question_id: owner.question.question_id });
|
|
7330
7327
|
}
|
|
7331
7328
|
if (question.question_id !== owner.question.question_id ||
|
|
7332
|
-
question.lane_id !== owner.lane.lane_id
|
|
7333
|
-
!sameQuestionCompany(question.company, effectiveLaneQuestionCompany(owner.lane))) {
|
|
7329
|
+
question.lane_id !== owner.lane.lane_id) {
|
|
7334
7330
|
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 });
|
|
7335
7331
|
}
|
|
7332
|
+
assertQuestionCompanyMatchesLane(question, owner.lane, "outpost_question_company_mismatch", ctx.hqRoot());
|
|
7336
7333
|
const audience = parseOutpostQuestionAudience(response.audience, outpostId, 0);
|
|
7337
7334
|
const body = parseOutpostQuestionBody(response.body, outpostId, question.question_id);
|
|
7338
7335
|
return { question, audience, body };
|
|
@@ -7340,8 +7337,8 @@ async function readOutpostQuestionDetail(ctx, owner) {
|
|
|
7340
7337
|
async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
7341
7338
|
const hostedLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === "outpost" &&
|
|
7342
7339
|
(!companyFilter ||
|
|
7343
|
-
effectiveLaneQuestionCompany(lane) === null ||
|
|
7344
|
-
questionCompanyRefMatches(effectiveLaneQuestionCompany(lane), companyFilter)));
|
|
7340
|
+
effectiveLaneQuestionCompany(lane, hqRoot) === null ||
|
|
7341
|
+
questionCompanyRefMatches(effectiveLaneQuestionCompany(lane, hqRoot), companyFilter)));
|
|
7345
7342
|
const lanesByOutpost = new Map();
|
|
7346
7343
|
for (const lane of hostedLanes) {
|
|
7347
7344
|
if (lane.host.kind !== "outpost")
|
|
@@ -7354,7 +7351,7 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
|
7354
7351
|
const errors = [];
|
|
7355
7352
|
const eligibleGroups = [];
|
|
7356
7353
|
for (const [outpostId, lanes] of [...lanesByOutpost.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
7357
|
-
for (const group of groupQuestionLanesByCompany(lanes, companyFilter)) {
|
|
7354
|
+
for (const group of groupQuestionLanesByCompany(hqRoot, lanes, companyFilter)) {
|
|
7358
7355
|
if (group.company === null) {
|
|
7359
7356
|
if (await ctx.laneOutpostQuestionVisibilityEnabled(null)) {
|
|
7360
7357
|
errors.push({
|
|
@@ -7413,9 +7410,7 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
|
7413
7410
|
const lane = localLanesById.get(question.lane_id);
|
|
7414
7411
|
if (!lane)
|
|
7415
7412
|
continue;
|
|
7416
|
-
|
|
7417
|
-
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 });
|
|
7418
|
-
}
|
|
7413
|
+
assertQuestionCompanyMatchesLane(question, lane, "outpost_question_company_mismatch", hqRoot);
|
|
7419
7414
|
if (seenQuestionIds.has(question.question_id)) {
|
|
7420
7415
|
throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned a duplicate question id`, { outpost_id: outpostId, question_id: question.question_id });
|
|
7421
7416
|
}
|
|
@@ -7479,7 +7474,7 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7479
7474
|
const forFilter = opts.for ? parseForFilter(opts.for, ctx) : undefined;
|
|
7480
7475
|
const company = opts.company
|
|
7481
7476
|
? await ctx.resolveCompany(opts.company)
|
|
7482
|
-
: await resolveActiveQuestionCompany(ctx);
|
|
7477
|
+
: await resolveActiveQuestionCompany(ctx, "Cannot access lane questions because the active HQ company could not be resolved; re-run with --company <slug>");
|
|
7483
7478
|
const localLanes = new Map(loadAllLanes(hqRoot).map((lane) => [lane.lane_id, lane]));
|
|
7484
7479
|
const targeted = opts.from !== undefined || opts.project !== undefined || opts.story !== undefined;
|
|
7485
7480
|
const localQuestions = loadAllQuestions(hqRoot)
|
|
@@ -7494,8 +7489,8 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7494
7489
|
return false;
|
|
7495
7490
|
const lane = localLanes.get(q.lane_id);
|
|
7496
7491
|
if (lane)
|
|
7497
|
-
assertQuestionCompanyMatchesLane(q, lane, "question_company_mismatch");
|
|
7498
|
-
const questionCompany = lane ? questionCompanyForLane(q, lane) : q.company;
|
|
7492
|
+
assertQuestionCompanyMatchesLane(q, lane, "question_company_mismatch", hqRoot);
|
|
7493
|
+
const questionCompany = lane ? questionCompanyForLane(q, lane, hqRoot) : q.company;
|
|
7499
7494
|
if (questionCompany !== null && !sameQuestionCompany(questionCompany, company)) {
|
|
7500
7495
|
if (targeted) {
|
|
7501
7496
|
throw new LanesError("question_company_mismatch", `Question ${q.question_id} does not belong to the requested or active company`, { question_id: q.question_id, lane_id: q.lane_id });
|
|
@@ -7513,7 +7508,7 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7513
7508
|
? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company)
|
|
7514
7509
|
: { questions: [], errors: [] };
|
|
7515
7510
|
for (const owner of outpostLoad.questions) {
|
|
7516
|
-
assertQuestionCompanyAccess(owner.question, owner.lane, company);
|
|
7511
|
+
assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
|
|
7517
7512
|
syncPrimaryQuestionDelivery(hqRoot, owner);
|
|
7518
7513
|
}
|
|
7519
7514
|
const remoteQuestionIds = new Set(outpostLoad.questions.map(({ question }) => question.question_id));
|
|
@@ -7542,7 +7537,7 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7542
7537
|
return false;
|
|
7543
7538
|
if (opts.story && q.story_id !== opts.story)
|
|
7544
7539
|
return false;
|
|
7545
|
-
assertQuestionCompanyAccess(q, lane, company);
|
|
7540
|
+
assertQuestionCompanyAccess(q, lane, company, hqRoot);
|
|
7546
7541
|
if (forFilter) {
|
|
7547
7542
|
if (!audienceMatches(audience, forFilter))
|
|
7548
7543
|
return false;
|
|
@@ -7618,9 +7613,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7618
7613
|
assertNoSecretText("freeform answer", opts.freeform);
|
|
7619
7614
|
if (opts.notes !== undefined)
|
|
7620
7615
|
assertNoSecretText("answer notes", opts.notes);
|
|
7621
|
-
const
|
|
7622
|
-
? await ctx.resolveCompany(opts.company)
|
|
7623
|
-
: await resolveActiveQuestionCompany(ctx);
|
|
7616
|
+
const explicitCompany = opts.company ? await ctx.resolveCompany(opts.company) : undefined;
|
|
7624
7617
|
let question;
|
|
7625
7618
|
try {
|
|
7626
7619
|
question = readQuestion(hqRoot, questionId);
|
|
@@ -7628,6 +7621,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7628
7621
|
catch (err) {
|
|
7629
7622
|
if (!isLanesError(err) || err.error !== "question_not_found")
|
|
7630
7623
|
throw err;
|
|
7624
|
+
const company = explicitCompany ?? await resolveActiveQuestionCompany(ctx, "Cannot look up this lane question because the active HQ company could not be resolved; re-run with --company <slug>");
|
|
7631
7625
|
const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company);
|
|
7632
7626
|
if (lookup.errors.length > 0) {
|
|
7633
7627
|
throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
|
|
@@ -7640,7 +7634,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7640
7634
|
const allCompanyLookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
|
|
7641
7635
|
const otherCompanyMatch = allCompanyLookup.questions.find((entry) => entry.question.question_id === questionId);
|
|
7642
7636
|
if (otherCompanyMatch) {
|
|
7643
|
-
assertQuestionCompanyAccess(otherCompanyMatch.question, otherCompanyMatch.lane, company);
|
|
7637
|
+
assertQuestionCompanyAccess(otherCompanyMatch.question, otherCompanyMatch.lane, company, hqRoot);
|
|
7644
7638
|
}
|
|
7645
7639
|
if (allCompanyLookup.errors.length > 0) {
|
|
7646
7640
|
throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
|
|
@@ -7657,24 +7651,31 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7657
7651
|
if (!owner) {
|
|
7658
7652
|
throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
|
|
7659
7653
|
}
|
|
7660
|
-
assertQuestionCompanyAccess(owner.question, owner.lane, company);
|
|
7661
|
-
|
|
7654
|
+
assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
|
|
7655
|
+
const ownerCompany = await resolveQuestionCompanyWithFallback(hqRoot, owner.lane, async () => explicitCompany ?? company);
|
|
7656
|
+
assertQuestionCompanyResolvedForAnswer(owner.question, owner.lane, hqRoot, ownerCompany);
|
|
7662
7657
|
throwIfQuestionNotPending(owner.question);
|
|
7663
7658
|
await yieldEventLoop();
|
|
7664
7659
|
await dispatchOutpostQuestionAnswer(ctx, owner.lane, questionId, {
|
|
7665
7660
|
...opts,
|
|
7666
|
-
company: questionListCompanyArg(
|
|
7661
|
+
company: questionListCompanyArg(ownerCompany),
|
|
7667
7662
|
});
|
|
7668
7663
|
return;
|
|
7669
7664
|
}
|
|
7670
7665
|
await yieldEventLoop();
|
|
7671
7666
|
const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
|
|
7672
|
-
|
|
7673
|
-
|
|
7667
|
+
const resolvedCompany = await resolveQuestionCompanyWithFallback(hqRoot, askingLane, async () => explicitCompany ?? question.company ?? await resolveActiveQuestionCompany(ctx, `Cannot answer legacy question ${questionId}: its company cannot be resolved from the question or lane; re-run with --company <slug>`));
|
|
7668
|
+
const requestedCompany = explicitCompany ?? resolvedCompany;
|
|
7669
|
+
assertQuestionCompanyAccess(question, askingLane, requestedCompany, hqRoot);
|
|
7670
|
+
assertQuestionCompanyResolvedForAnswer(question, askingLane, hqRoot, resolvedCompany);
|
|
7671
|
+
const questionCompany = question.company ?? effectiveLaneQuestionCompany(askingLane, hqRoot) ?? resolvedCompany;
|
|
7672
|
+
const questionForBody = question.company === null
|
|
7673
|
+
? { ...question, company: questionCompany }
|
|
7674
|
+
: question;
|
|
7674
7675
|
if (askingLane.host.kind === "outpost" && !laneIsHostedHere(ctx, askingLane)) {
|
|
7675
7676
|
await dispatchOutpostQuestionAnswer(ctx, askingLane, questionId, {
|
|
7676
7677
|
...opts,
|
|
7677
|
-
company: questionListCompanyArg(
|
|
7678
|
+
company: questionListCompanyArg(questionCompany),
|
|
7678
7679
|
});
|
|
7679
7680
|
return;
|
|
7680
7681
|
}
|
|
@@ -7685,7 +7686,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7685
7686
|
if (laneAcceptsDropboxDelivery(askingLane)) {
|
|
7686
7687
|
assertLaneRunsHereForOperatorMutation(ctx, askingLane, "deliver an answer to");
|
|
7687
7688
|
}
|
|
7688
|
-
const body = requireQuestionBody(hqRoot,
|
|
7689
|
+
const body = requireQuestionBody(hqRoot, questionForBody);
|
|
7689
7690
|
// A trimmed-empty --freeform is absent: it must not win over --choice and
|
|
7690
7691
|
// must not become a freeform answer of "".
|
|
7691
7692
|
const freeform = opts.freeform?.trim() || undefined;
|
|
@@ -7702,10 +7703,15 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7702
7703
|
: "user";
|
|
7703
7704
|
const updated = casUpdateQuestion(hqRoot, questionId, (current) => {
|
|
7704
7705
|
throwIfQuestionNotPending(current);
|
|
7706
|
+
assertQuestionCompanyAccess(current, askingLane, requestedCompany, hqRoot);
|
|
7707
|
+
const recordedCompany = current.company ?? questionCompany;
|
|
7708
|
+
const currentForBody = current.company === null
|
|
7709
|
+
? { ...current, company: recordedCompany }
|
|
7710
|
+
: current;
|
|
7705
7711
|
let notesRef;
|
|
7706
7712
|
let nextBody = current.body;
|
|
7707
7713
|
if (opts.notes) {
|
|
7708
|
-
const stored = writeQuestionBody(hqRoot,
|
|
7714
|
+
const stored = writeQuestionBody(hqRoot, currentForBody, {
|
|
7709
7715
|
...body,
|
|
7710
7716
|
notes: opts.notes,
|
|
7711
7717
|
});
|
|
@@ -7714,6 +7720,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7714
7720
|
}
|
|
7715
7721
|
return {
|
|
7716
7722
|
...current,
|
|
7723
|
+
company: recordedCompany,
|
|
7717
7724
|
body: nextBody,
|
|
7718
7725
|
status: "answered",
|
|
7719
7726
|
answer: {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG = "cli.flag-worker-self-contained";
|
|
2
|
+
interface CachedWorkerMode {
|
|
3
|
+
version: number;
|
|
4
|
+
value: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Keep the last server snapshot decision for this one startup-mode flag. */
|
|
7
|
+
export declare function flagRegistryWorkerModeCachePath(stateDir: string, scopeIdentifiers: readonly string[]): string;
|
|
8
|
+
/** A missing, invalid, or inaccessible cache never enables the rollout. */
|
|
9
|
+
export declare function readCachedFlagRegistryWorkerMode(filePath: string): CachedWorkerMode | null;
|
|
10
|
+
export declare function writeCachedFlagRegistryWorkerMode(filePath: string, snapshot: CachedWorkerMode): void;
|
|
11
|
+
export declare function flagRegistryStateDir(env?: NodeJS.ProcessEnv): string;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=flag-registry-worker-mode-cache.d.ts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
export const FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG = "cli.flag-worker-self-contained";
|
|
6
|
+
/** Keep the last server snapshot decision for this one startup-mode flag. */
|
|
7
|
+
export function flagRegistryWorkerModeCachePath(stateDir, scopeIdentifiers) {
|
|
8
|
+
const basename = scopeIdentifiers.length === 0
|
|
9
|
+
? "worker-mode-default.json"
|
|
10
|
+
: `worker-mode-${createHash("sha256")
|
|
11
|
+
.update([...scopeIdentifiers].sort().join("\0"))
|
|
12
|
+
.digest("hex")
|
|
13
|
+
.slice(0, 24)}.json`;
|
|
14
|
+
return path.join(stateDir, "flags", basename);
|
|
15
|
+
}
|
|
16
|
+
/** A missing, invalid, or inaccessible cache never enables the rollout. */
|
|
17
|
+
export function readCachedFlagRegistryWorkerMode(filePath) {
|
|
18
|
+
let latest = readCacheRecordIfPresent(filePath);
|
|
19
|
+
const directory = path.dirname(filePath);
|
|
20
|
+
if (!fs.existsSync(directory))
|
|
21
|
+
return latest;
|
|
22
|
+
const basename = path.basename(filePath);
|
|
23
|
+
const versionedPrefix = `${basename}.v`;
|
|
24
|
+
for (const entry of fs.readdirSync(directory)) {
|
|
25
|
+
if (!entry.startsWith(versionedPrefix) || !entry.endsWith(".json"))
|
|
26
|
+
continue;
|
|
27
|
+
const versionedPath = path.join(directory, entry);
|
|
28
|
+
const record = readCacheRecordIfPresent(versionedPath);
|
|
29
|
+
if (record)
|
|
30
|
+
latest = newerCacheRecord(latest, record);
|
|
31
|
+
}
|
|
32
|
+
return latest;
|
|
33
|
+
}
|
|
34
|
+
export function writeCachedFlagRegistryWorkerMode(filePath, snapshot) {
|
|
35
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
36
|
+
const existing = readCachedFlagRegistryWorkerMode(filePath);
|
|
37
|
+
if (existing && snapshot.version < existing.version)
|
|
38
|
+
return;
|
|
39
|
+
// Each registry version gets its own immutable destination. Concurrent
|
|
40
|
+
// processes can publish v7 and v8 in either order without an older value
|
|
41
|
+
// replacing the newer record.
|
|
42
|
+
const versionedPath = `${filePath}.v${snapshot.version}.json`;
|
|
43
|
+
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
44
|
+
try {
|
|
45
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(snapshot)}\n`, {
|
|
46
|
+
encoding: "utf8",
|
|
47
|
+
mode: 0o600,
|
|
48
|
+
flag: "wx",
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
fs.linkSync(tempPath, versionedPath);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (!isAlreadyExistsError(error))
|
|
55
|
+
throw error;
|
|
56
|
+
const existingVersion = readCacheRecord(versionedPath);
|
|
57
|
+
if (existingVersion.value !== snapshot.value) {
|
|
58
|
+
throw new Error("Conflicting flag-registry worker cache values share a version");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
if (fs.existsSync(tempPath))
|
|
64
|
+
fs.unlinkSync(tempPath);
|
|
65
|
+
}
|
|
66
|
+
pruneOlderCacheRecords(filePath);
|
|
67
|
+
}
|
|
68
|
+
export function flagRegistryStateDir(env = process.env) {
|
|
69
|
+
const override = env.HQ_STATE_DIR?.trim();
|
|
70
|
+
return override ? override : path.join(os.homedir(), ".hq");
|
|
71
|
+
}
|
|
72
|
+
function isCachedWorkerMode(value) {
|
|
73
|
+
return typeof value === "object" && value !== null &&
|
|
74
|
+
"version" in value && typeof value.version === "number" &&
|
|
75
|
+
Number.isSafeInteger(value.version) && value.version >= 0 &&
|
|
76
|
+
"value" in value && typeof value.value === "boolean";
|
|
77
|
+
}
|
|
78
|
+
function readCacheRecord(filePath) {
|
|
79
|
+
const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
80
|
+
if (!isCachedWorkerMode(value)) {
|
|
81
|
+
throw new Error("Cached flag-registry worker mode has an invalid shape");
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
function newerCacheRecord(current, candidate) {
|
|
86
|
+
if (!current || candidate.version > current.version)
|
|
87
|
+
return candidate;
|
|
88
|
+
if (candidate.version === current.version && candidate.value !== current.value) {
|
|
89
|
+
throw new Error("Conflicting flag-registry worker cache values share a version");
|
|
90
|
+
}
|
|
91
|
+
return current;
|
|
92
|
+
}
|
|
93
|
+
function pruneOlderCacheRecords(filePath) {
|
|
94
|
+
const latest = readCachedFlagRegistryWorkerMode(filePath);
|
|
95
|
+
if (!latest)
|
|
96
|
+
return;
|
|
97
|
+
const directory = path.dirname(filePath);
|
|
98
|
+
const basename = path.basename(filePath);
|
|
99
|
+
const versionedPrefix = `${basename}.v`;
|
|
100
|
+
for (const entry of fs.readdirSync(directory)) {
|
|
101
|
+
if (!entry.startsWith(versionedPrefix) || !entry.endsWith(".json"))
|
|
102
|
+
continue;
|
|
103
|
+
const recordPath = path.join(directory, entry);
|
|
104
|
+
const record = readCacheRecordIfPresent(recordPath);
|
|
105
|
+
if (record && record.version < latest.version) {
|
|
106
|
+
try {
|
|
107
|
+
fs.unlinkSync(recordPath);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (!isNotFoundError(error))
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (fs.existsSync(filePath)) {
|
|
116
|
+
const legacyRecord = readCacheRecordIfPresent(filePath);
|
|
117
|
+
if (legacyRecord && legacyRecord.version < latest.version) {
|
|
118
|
+
try {
|
|
119
|
+
fs.unlinkSync(filePath);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (!isNotFoundError(error))
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function readCacheRecordIfPresent(filePath) {
|
|
129
|
+
try {
|
|
130
|
+
return readCacheRecord(filePath);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (isNotFoundError(error))
|
|
134
|
+
return null;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function isAlreadyExistsError(error) {
|
|
139
|
+
return typeof error === "object" && error !== null &&
|
|
140
|
+
"code" in error && error.code === "EEXIST";
|
|
141
|
+
}
|
|
142
|
+
function isNotFoundError(error) {
|
|
143
|
+
return typeof error === "object" && error !== null &&
|
|
144
|
+
"code" in error && error.code === "ENOENT";
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=flag-registry-worker-mode-cache.js.map
|