@indigoai-us/hq-cli 5.218.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 +33 -0
- package/dist/command-catalog.generated.d.ts +18 -0
- package/dist/command-catalog.generated.js +23 -0
- package/dist/commands/lanes-checks.d.ts +13 -0
- package/dist/commands/lanes-checks.js +558 -0
- package/dist/commands/lanes.js +172 -49
- 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/config.d.ts +2 -1
- package/dist/lib/lanes/config.js +30 -0
- package/dist/lib/lanes/questions.d.ts +21 -7
- package/dist/lib/lanes/questions.js +157 -18
- package/dist/lib/lanes/reconcile.d.ts +3 -1
- package/dist/lib/lanes/reconcile.js +22 -5
- package/dist/lib/lanes/spawn.js +28 -16
- package/dist/lib/lanes/store.js +15 -1
- package/dist/lib/lanes/tracker-checks.d.ts +77 -0
- package/dist/lib/lanes/tracker-checks.js +134 -0
- package/dist/lib/lanes/types.d.ts +15 -0
- package/dist/lib/lanes/watcher-main.js +17 -1
- package/dist/lib/lanes/watcher.d.ts +4 -1
- package/dist/lib/lanes/watcher.js +36 -7
- package/dist/sentry.js +41 -0
- package/package.json +5 -2
package/dist/commands/lanes.js
CHANGED
|
@@ -13,6 +13,7 @@ import * as path from "node:path";
|
|
|
13
13
|
import { readLocalOverride } from "@indigoai-us/hq-flags-client";
|
|
14
14
|
import { Option } from "commander";
|
|
15
15
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
16
|
+
import { registerLaneChecksCommand } from "./lanes-checks.js";
|
|
16
17
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
17
18
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
18
19
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
@@ -33,6 +34,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
33
34
|
import { resolveActiveMembershipCompany } from "../lib/mesh/api.js";
|
|
34
35
|
import { openMeshTransport, requireToken } from "../lib/mesh/client.js";
|
|
35
36
|
import { CompanySelectionError } from "../utils/company-selection-error.js";
|
|
37
|
+
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
36
38
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
37
39
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
38
40
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
@@ -40,7 +42,7 @@ import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_
|
|
|
40
42
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
41
43
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
42
44
|
import { keepAliveExclusion, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
43
|
-
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
45
|
+
import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
|
|
44
46
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
45
47
|
import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
46
48
|
import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
|
|
@@ -1579,6 +1581,40 @@ function sameQuestionCompany(a, b) {
|
|
|
1579
1581
|
const bSlug = b.slug.trim();
|
|
1580
1582
|
return aSlug !== "" && aSlug === bSlug;
|
|
1581
1583
|
}
|
|
1584
|
+
function effectiveLaneQuestionCompany(lane, hqRoot) {
|
|
1585
|
+
return lane.company ?? lane.mesh_company ?? (hqRoot ? resolveProjectQuestionCompany(hqRoot, lane) : null);
|
|
1586
|
+
}
|
|
1587
|
+
function questionCompanyForLane(question, lane, hqRoot) {
|
|
1588
|
+
return question.company ?? effectiveLaneQuestionCompany(lane, hqRoot);
|
|
1589
|
+
}
|
|
1590
|
+
async function resolveActiveQuestionCompany(ctx, message = "Cannot access lane questions because the active HQ company could not be resolved") {
|
|
1591
|
+
try {
|
|
1592
|
+
return await ctx.resolveCompany();
|
|
1593
|
+
}
|
|
1594
|
+
catch (err) {
|
|
1595
|
+
if (!(err instanceof CompanySelectionError))
|
|
1596
|
+
throw err;
|
|
1597
|
+
throw new LanesError("question_company_unavailable", message, {});
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
function assertQuestionCompanyMatchesLane(question, lane, code, hqRoot) {
|
|
1601
|
+
const laneCompany = effectiveLaneQuestionCompany(lane, hqRoot);
|
|
1602
|
+
if (question.company != null && laneCompany !== null && !sameQuestionCompany(question.company, laneCompany)) {
|
|
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
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
function assertQuestionCompanyAccess(question, lane, requestedCompany, hqRoot) {
|
|
1607
|
+
assertQuestionCompanyMatchesLane(question, lane, "question_company_mismatch", hqRoot);
|
|
1608
|
+
const questionCompany = questionCompanyForLane(question, lane, hqRoot);
|
|
1609
|
+
if (questionCompany !== null && !sameQuestionCompany(questionCompany, requestedCompany)) {
|
|
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
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
function assertQuestionCompanyResolvedForAnswer(question, lane, hqRoot, resolvedCompany) {
|
|
1614
|
+
if (questionCompanyForLane(question, lane, hqRoot) !== null || resolvedCompany)
|
|
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; re-run with --company <slug>`, { question_id: question.question_id, lane_id: lane.lane_id });
|
|
1617
|
+
}
|
|
1582
1618
|
function questionCompanyRefMatches(recordCompany, filter) {
|
|
1583
1619
|
return recordCompany !== null &&
|
|
1584
1620
|
recordCompany !== undefined &&
|
|
@@ -3543,9 +3579,11 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
|
3543
3579
|
});
|
|
3544
3580
|
}
|
|
3545
3581
|
const ownerAnswer = ownerQuestion.answer;
|
|
3582
|
+
const ownerLaneCompany = effectiveLaneQuestionCompany(lane, ctx.hqRoot());
|
|
3546
3583
|
if (!isQuestionAnswer(ownerAnswer) ||
|
|
3547
3584
|
ownerQuestion.lane_id !== lane.lane_id ||
|
|
3548
|
-
|
|
3585
|
+
(ownerQuestion.company !== null && ownerLaneCompany !== null &&
|
|
3586
|
+
!sameQuestionCompany(ownerQuestion.company, ownerLaneCompany))) {
|
|
3549
3587
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return the confirmed answer for question ${questionId}`, {
|
|
3550
3588
|
lane_id: lane.lane_id,
|
|
3551
3589
|
question_id: questionId,
|
|
@@ -3557,14 +3595,31 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
|
3557
3595
|
if (current.lane_id !== lane.lane_id) {
|
|
3558
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 });
|
|
3559
3597
|
}
|
|
3560
|
-
if (
|
|
3598
|
+
if (current.company !== null &&
|
|
3599
|
+
effectiveLaneQuestionCompany(lane, ctx.hqRoot()) !== null &&
|
|
3600
|
+
!sameQuestionCompany(current.company, effectiveLaneQuestionCompany(lane, ctx.hqRoot()))) {
|
|
3561
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 });
|
|
3562
3602
|
}
|
|
3603
|
+
if (current.company !== null &&
|
|
3604
|
+
ownerQuestion.company !== null &&
|
|
3605
|
+
!sameQuestionCompany(current.company, ownerQuestion.company)) {
|
|
3606
|
+
throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} has a different company than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
|
|
3607
|
+
}
|
|
3563
3608
|
if (current.status === "pending") {
|
|
3564
|
-
return {
|
|
3609
|
+
return {
|
|
3610
|
+
...current,
|
|
3611
|
+
company: current.company ?? ownerQuestion.company,
|
|
3612
|
+
status: "answered",
|
|
3613
|
+
answer: ownerAnswer,
|
|
3614
|
+
delivered_at: ownerQuestion.delivered_at ?? current.delivered_at ?? null,
|
|
3615
|
+
};
|
|
3565
3616
|
}
|
|
3566
3617
|
if (current.status === "answered" && sameQuestionAnswer(current.answer, ownerAnswer)) {
|
|
3567
|
-
|
|
3618
|
+
const deliveredAt = ownerQuestion.delivered_at ?? current.delivered_at ?? null;
|
|
3619
|
+
const company = current.company ?? ownerQuestion.company;
|
|
3620
|
+
return deliveredAt === current.delivered_at && company === current.company
|
|
3621
|
+
? current
|
|
3622
|
+
: { ...current, company, delivered_at: deliveredAt };
|
|
3568
3623
|
}
|
|
3569
3624
|
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 });
|
|
3570
3625
|
});
|
|
@@ -3576,7 +3631,9 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
|
3576
3631
|
if (!isLanesError(err) || err.error !== "question_not_found")
|
|
3577
3632
|
throw err;
|
|
3578
3633
|
const primaryLane = readLane(ctx.hqRoot(), lane.lane_id);
|
|
3579
|
-
|
|
3634
|
+
const primaryCompany = effectiveLaneQuestionCompany(primaryLane, ctx.hqRoot());
|
|
3635
|
+
const ownerCompany = effectiveLaneQuestionCompany(lane, ctx.hqRoot());
|
|
3636
|
+
if (primaryCompany !== null && ownerCompany !== null && !sameQuestionCompany(primaryCompany, ownerCompany)) {
|
|
3580
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 });
|
|
3581
3638
|
}
|
|
3582
3639
|
try {
|
|
@@ -7035,6 +7092,7 @@ async function runAsk(ctx, opts) {
|
|
|
7035
7092
|
}
|
|
7036
7093
|
const lane = readLane(hqRoot, laneId);
|
|
7037
7094
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "ask");
|
|
7095
|
+
const questionCompany = await resolveQuestionCompanyWithFallback(hqRoot, lane, () => resolveActiveQuestionCompany(ctx, "Cannot raise a lane question because the active HQ company could not be resolved"));
|
|
7038
7096
|
const optionList = Array.isArray(opts.option)
|
|
7039
7097
|
? opts.option
|
|
7040
7098
|
: opts.option
|
|
@@ -7059,6 +7117,7 @@ async function runAsk(ctx, opts) {
|
|
|
7059
7117
|
body,
|
|
7060
7118
|
raisedVia: "cli",
|
|
7061
7119
|
at,
|
|
7120
|
+
activeCompany: questionCompany,
|
|
7062
7121
|
questionId,
|
|
7063
7122
|
});
|
|
7064
7123
|
questionId = withBody.question_id;
|
|
@@ -7084,6 +7143,8 @@ async function runAsk(ctx, opts) {
|
|
|
7084
7143
|
return;
|
|
7085
7144
|
}
|
|
7086
7145
|
catch (err) {
|
|
7146
|
+
if (isLanesError(err) && err.error === "secret_text_refused")
|
|
7147
|
+
throw err;
|
|
7087
7148
|
lastErr = err;
|
|
7088
7149
|
process.stderr.write(`hq lanes ask: attempt ${i + 1}/${attempts} failed for ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
7089
7150
|
if (i === attempts - 1)
|
|
@@ -7113,14 +7174,15 @@ async function runAsk(ctx, opts) {
|
|
|
7113
7174
|
});
|
|
7114
7175
|
}
|
|
7115
7176
|
const OUTPOST_QUESTION_LOOKUP_CONCURRENCY = 4;
|
|
7116
|
-
function groupQuestionLanesByCompany(lanes) {
|
|
7177
|
+
function groupQuestionLanesByCompany(hqRoot, lanes, fallbackCompany) {
|
|
7117
7178
|
const groups = [];
|
|
7118
7179
|
for (const lane of lanes) {
|
|
7119
|
-
const
|
|
7180
|
+
const company = effectiveLaneQuestionCompany(lane, hqRoot) ?? fallbackCompany ?? null;
|
|
7181
|
+
const existing = groups.find((group) => sameQuestionCompany(group.company, company));
|
|
7120
7182
|
if (existing)
|
|
7121
7183
|
existing.lanes.push(lane);
|
|
7122
7184
|
else
|
|
7123
|
-
groups.push({ company
|
|
7185
|
+
groups.push({ company, lanes: [lane] });
|
|
7124
7186
|
}
|
|
7125
7187
|
return groups;
|
|
7126
7188
|
}
|
|
@@ -7264,17 +7326,19 @@ async function readOutpostQuestionDetail(ctx, owner) {
|
|
|
7264
7326
|
throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, question_id: owner.question.question_id });
|
|
7265
7327
|
}
|
|
7266
7328
|
if (question.question_id !== owner.question.question_id ||
|
|
7267
|
-
question.lane_id !== owner.lane.lane_id
|
|
7268
|
-
!sameQuestionCompany(question.company, owner.lane.company)) {
|
|
7329
|
+
question.lane_id !== owner.lane.lane_id) {
|
|
7269
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 });
|
|
7270
7331
|
}
|
|
7332
|
+
assertQuestionCompanyMatchesLane(question, owner.lane, "outpost_question_company_mismatch", ctx.hqRoot());
|
|
7271
7333
|
const audience = parseOutpostQuestionAudience(response.audience, outpostId, 0);
|
|
7272
7334
|
const body = parseOutpostQuestionBody(response.body, outpostId, question.question_id);
|
|
7273
7335
|
return { question, audience, body };
|
|
7274
7336
|
}
|
|
7275
7337
|
async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
7276
7338
|
const hostedLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === "outpost" &&
|
|
7277
|
-
(!companyFilter ||
|
|
7339
|
+
(!companyFilter ||
|
|
7340
|
+
effectiveLaneQuestionCompany(lane, hqRoot) === null ||
|
|
7341
|
+
questionCompanyRefMatches(effectiveLaneQuestionCompany(lane, hqRoot), companyFilter)));
|
|
7278
7342
|
const lanesByOutpost = new Map();
|
|
7279
7343
|
for (const lane of hostedLanes) {
|
|
7280
7344
|
if (lane.host.kind !== "outpost")
|
|
@@ -7287,7 +7351,7 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
|
7287
7351
|
const errors = [];
|
|
7288
7352
|
const eligibleGroups = [];
|
|
7289
7353
|
for (const [outpostId, lanes] of [...lanesByOutpost.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
7290
|
-
for (const group of groupQuestionLanesByCompany(lanes)) {
|
|
7354
|
+
for (const group of groupQuestionLanesByCompany(hqRoot, lanes, companyFilter)) {
|
|
7291
7355
|
if (group.company === null) {
|
|
7292
7356
|
if (await ctx.laneOutpostQuestionVisibilityEnabled(null)) {
|
|
7293
7357
|
errors.push({
|
|
@@ -7346,9 +7410,7 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
|
7346
7410
|
const lane = localLanesById.get(question.lane_id);
|
|
7347
7411
|
if (!lane)
|
|
7348
7412
|
continue;
|
|
7349
|
-
|
|
7350
|
-
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 });
|
|
7351
|
-
}
|
|
7413
|
+
assertQuestionCompanyMatchesLane(question, lane, "outpost_question_company_mismatch", hqRoot);
|
|
7352
7414
|
if (seenQuestionIds.has(question.question_id)) {
|
|
7353
7415
|
throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned a duplicate question id`, { outpost_id: outpostId, question_id: question.question_id });
|
|
7354
7416
|
}
|
|
@@ -7379,14 +7441,42 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
|
7379
7441
|
errors: [...new Map(errors.map((entry) => [entry.host, entry])).values()],
|
|
7380
7442
|
};
|
|
7381
7443
|
}
|
|
7444
|
+
function syncPrimaryQuestionDelivery(hqRoot, owner) {
|
|
7445
|
+
const deliveredAt = owner.question.delivered_at;
|
|
7446
|
+
if (!deliveredAt)
|
|
7447
|
+
return;
|
|
7448
|
+
try {
|
|
7449
|
+
casUpdateQuestion(hqRoot, owner.question.question_id, (current) => {
|
|
7450
|
+
if (current.lane_id !== owner.lane.lane_id) {
|
|
7451
|
+
throw new LanesError("outpost_question_sync_conflict", `Primary question ${current.question_id} belongs to a different lane than its Outpost owner`, { lane_id: owner.lane.lane_id, question_id: current.question_id });
|
|
7452
|
+
}
|
|
7453
|
+
if (current.company !== null &&
|
|
7454
|
+
owner.question.company !== null &&
|
|
7455
|
+
!sameQuestionCompany(current.company, owner.question.company)) {
|
|
7456
|
+
throw new LanesError("outpost_question_sync_conflict", `Primary question ${current.question_id} belongs to a different company than its Outpost owner`, { lane_id: owner.lane.lane_id, question_id: current.question_id });
|
|
7457
|
+
}
|
|
7458
|
+
const company = current.company ?? owner.question.company;
|
|
7459
|
+
const nextDeliveredAt = current.delivered_at ?? deliveredAt;
|
|
7460
|
+
return company === current.company && nextDeliveredAt === current.delivered_at
|
|
7461
|
+
? current
|
|
7462
|
+
: { ...current, company, delivered_at: nextDeliveredAt };
|
|
7463
|
+
});
|
|
7464
|
+
}
|
|
7465
|
+
catch (err) {
|
|
7466
|
+
if (isLanesError(err) && err.error === "question_not_found")
|
|
7467
|
+
return;
|
|
7468
|
+
throw err;
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7382
7471
|
async function runQuestionsList(ctx, opts) {
|
|
7383
7472
|
const hqRoot = ctx.hqRoot();
|
|
7384
7473
|
const status = opts.status ? parseQuestionStatus(opts.status) : undefined;
|
|
7385
7474
|
const forFilter = opts.for ? parseForFilter(opts.for, ctx) : undefined;
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
const localLanes =
|
|
7475
|
+
const company = opts.company
|
|
7476
|
+
? await ctx.resolveCompany(opts.company)
|
|
7477
|
+
: await resolveActiveQuestionCompany(ctx, "Cannot access lane questions because the active HQ company could not be resolved; re-run with --company <slug>");
|
|
7478
|
+
const localLanes = new Map(loadAllLanes(hqRoot).map((lane) => [lane.lane_id, lane]));
|
|
7479
|
+
const targeted = opts.from !== undefined || opts.project !== undefined || opts.story !== undefined;
|
|
7390
7480
|
const localQuestions = loadAllQuestions(hqRoot)
|
|
7391
7481
|
.filter((q) => {
|
|
7392
7482
|
if (status && q.status !== status)
|
|
@@ -7397,16 +7487,15 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7397
7487
|
return false;
|
|
7398
7488
|
if (opts.story && q.story_id !== opts.story)
|
|
7399
7489
|
return false;
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
if (!lane || !questionCompanyRefMatches(lane.company, company) || !sameQuestionCompany(q.company, lane.company)) {
|
|
7408
|
-
return false;
|
|
7490
|
+
const lane = localLanes.get(q.lane_id);
|
|
7491
|
+
if (lane)
|
|
7492
|
+
assertQuestionCompanyMatchesLane(q, lane, "question_company_mismatch", hqRoot);
|
|
7493
|
+
const questionCompany = lane ? questionCompanyForLane(q, lane, hqRoot) : q.company;
|
|
7494
|
+
if (questionCompany !== null && !sameQuestionCompany(questionCompany, company)) {
|
|
7495
|
+
if (targeted) {
|
|
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 });
|
|
7409
7497
|
}
|
|
7498
|
+
return false;
|
|
7410
7499
|
}
|
|
7411
7500
|
if (forFilter) {
|
|
7412
7501
|
const audience = resolveAudience(hqRoot, q, ctx);
|
|
@@ -7418,6 +7507,10 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7418
7507
|
const outpostLoad = opts.allHosts
|
|
7419
7508
|
? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company)
|
|
7420
7509
|
: { questions: [], errors: [] };
|
|
7510
|
+
for (const owner of outpostLoad.questions) {
|
|
7511
|
+
assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
|
|
7512
|
+
syncPrimaryQuestionDelivery(hqRoot, owner);
|
|
7513
|
+
}
|
|
7421
7514
|
const remoteQuestionIds = new Set(outpostLoad.questions.map(({ question }) => question.question_id));
|
|
7422
7515
|
const questions = [
|
|
7423
7516
|
...localQuestions
|
|
@@ -7430,7 +7523,7 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7430
7523
|
};
|
|
7431
7524
|
if (!opts.allHosts)
|
|
7432
7525
|
return row;
|
|
7433
|
-
const lane = localLanes
|
|
7526
|
+
const lane = localLanes.get(q.lane_id);
|
|
7434
7527
|
const host = lane?.host.kind === "outpost" ? `outpost:${lane.host.id}` : "local";
|
|
7435
7528
|
return { ...row, host };
|
|
7436
7529
|
}),
|
|
@@ -7444,11 +7537,7 @@ async function runQuestionsList(ctx, opts) {
|
|
|
7444
7537
|
return false;
|
|
7445
7538
|
if (opts.story && q.story_id !== opts.story)
|
|
7446
7539
|
return false;
|
|
7447
|
-
|
|
7448
|
-
!questionCompanyRefMatches(lane.company, company) ||
|
|
7449
|
-
!sameQuestionCompany(q.company, lane.company))) {
|
|
7450
|
-
return false;
|
|
7451
|
-
}
|
|
7540
|
+
assertQuestionCompanyAccess(q, lane, company, hqRoot);
|
|
7452
7541
|
if (forFilter) {
|
|
7453
7542
|
if (!audienceMatches(audience, forFilter))
|
|
7454
7543
|
return false;
|
|
@@ -7518,7 +7607,13 @@ async function runQuestionsShow(ctx, questionId) {
|
|
|
7518
7607
|
}
|
|
7519
7608
|
async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
7520
7609
|
const hqRoot = ctx.hqRoot();
|
|
7521
|
-
|
|
7610
|
+
if (opts.choice !== undefined)
|
|
7611
|
+
assertNoSecretText("answer choice", opts.choice);
|
|
7612
|
+
if (opts.freeform !== undefined)
|
|
7613
|
+
assertNoSecretText("freeform answer", opts.freeform);
|
|
7614
|
+
if (opts.notes !== undefined)
|
|
7615
|
+
assertNoSecretText("answer notes", opts.notes);
|
|
7616
|
+
const explicitCompany = opts.company ? await ctx.resolveCompany(opts.company) : undefined;
|
|
7522
7617
|
let question;
|
|
7523
7618
|
try {
|
|
7524
7619
|
question = readQuestion(hqRoot, questionId);
|
|
@@ -7526,6 +7621,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7526
7621
|
catch (err) {
|
|
7527
7622
|
if (!isLanesError(err) || err.error !== "question_not_found")
|
|
7528
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>");
|
|
7529
7625
|
const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company);
|
|
7530
7626
|
if (lookup.errors.length > 0) {
|
|
7531
7627
|
throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
|
|
@@ -7535,6 +7631,17 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7535
7631
|
}
|
|
7536
7632
|
const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
|
|
7537
7633
|
if (matches.length === 0) {
|
|
7634
|
+
const allCompanyLookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
|
|
7635
|
+
const otherCompanyMatch = allCompanyLookup.questions.find((entry) => entry.question.question_id === questionId);
|
|
7636
|
+
if (otherCompanyMatch) {
|
|
7637
|
+
assertQuestionCompanyAccess(otherCompanyMatch.question, otherCompanyMatch.lane, company, hqRoot);
|
|
7638
|
+
}
|
|
7639
|
+
if (allCompanyLookup.errors.length > 0) {
|
|
7640
|
+
throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
|
|
7641
|
+
question_id: questionId,
|
|
7642
|
+
hosts: allCompanyLookup.errors.map(({ host, error }) => ({ host, error })),
|
|
7643
|
+
});
|
|
7644
|
+
}
|
|
7538
7645
|
throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
|
|
7539
7646
|
}
|
|
7540
7647
|
if (matches.length > 1) {
|
|
@@ -7544,29 +7651,31 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7544
7651
|
if (!owner) {
|
|
7545
7652
|
throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
|
|
7546
7653
|
}
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
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);
|
|
7550
7657
|
throwIfQuestionNotPending(owner.question);
|
|
7551
7658
|
await yieldEventLoop();
|
|
7552
7659
|
await dispatchOutpostQuestionAnswer(ctx, owner.lane, questionId, {
|
|
7553
7660
|
...opts,
|
|
7554
|
-
company: questionListCompanyArg(
|
|
7661
|
+
company: questionListCompanyArg(ownerCompany),
|
|
7555
7662
|
});
|
|
7556
7663
|
return;
|
|
7557
7664
|
}
|
|
7558
7665
|
await yieldEventLoop();
|
|
7559
7666
|
const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
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;
|
|
7566
7675
|
if (askingLane.host.kind === "outpost" && !laneIsHostedHere(ctx, askingLane)) {
|
|
7567
7676
|
await dispatchOutpostQuestionAnswer(ctx, askingLane, questionId, {
|
|
7568
7677
|
...opts,
|
|
7569
|
-
|
|
7678
|
+
company: questionListCompanyArg(questionCompany),
|
|
7570
7679
|
});
|
|
7571
7680
|
return;
|
|
7572
7681
|
}
|
|
@@ -7577,7 +7686,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7577
7686
|
if (laneAcceptsDropboxDelivery(askingLane)) {
|
|
7578
7687
|
assertLaneRunsHereForOperatorMutation(ctx, askingLane, "deliver an answer to");
|
|
7579
7688
|
}
|
|
7580
|
-
const body = requireQuestionBody(hqRoot,
|
|
7689
|
+
const body = requireQuestionBody(hqRoot, questionForBody);
|
|
7581
7690
|
// A trimmed-empty --freeform is absent: it must not win over --choice and
|
|
7582
7691
|
// must not become a freeform answer of "".
|
|
7583
7692
|
const freeform = opts.freeform?.trim() || undefined;
|
|
@@ -7594,10 +7703,15 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7594
7703
|
: "user";
|
|
7595
7704
|
const updated = casUpdateQuestion(hqRoot, questionId, (current) => {
|
|
7596
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;
|
|
7597
7711
|
let notesRef;
|
|
7598
7712
|
let nextBody = current.body;
|
|
7599
7713
|
if (opts.notes) {
|
|
7600
|
-
const stored = writeQuestionBody(hqRoot,
|
|
7714
|
+
const stored = writeQuestionBody(hqRoot, currentForBody, {
|
|
7601
7715
|
...body,
|
|
7602
7716
|
notes: opts.notes,
|
|
7603
7717
|
});
|
|
@@ -7606,6 +7720,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
|
|
|
7606
7720
|
}
|
|
7607
7721
|
return {
|
|
7608
7722
|
...current,
|
|
7723
|
+
company: recordedCompany,
|
|
7609
7724
|
body: nextBody,
|
|
7610
7725
|
status: "answered",
|
|
7611
7726
|
answer: {
|
|
@@ -8303,6 +8418,7 @@ async function runReconcile(ctx, opts) {
|
|
|
8303
8418
|
throttleDeps: ctx.throttleDeps,
|
|
8304
8419
|
livenessDeps: livenessDepsFromCtx(ctx),
|
|
8305
8420
|
presenceConnected: ctx.presenceConnected,
|
|
8421
|
+
resolveQuestionActiveCompany: () => resolveActiveQuestionCompany(ctx),
|
|
8306
8422
|
mesh: ctx.mesh,
|
|
8307
8423
|
env: ctx.env,
|
|
8308
8424
|
});
|
|
@@ -8383,6 +8499,13 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8383
8499
|
now: ctx.now,
|
|
8384
8500
|
actor: ctx.trackerActor,
|
|
8385
8501
|
}, wrap);
|
|
8502
|
+
registerLaneChecksCommand(lanes, {
|
|
8503
|
+
hqRoot: ctx.hqRoot,
|
|
8504
|
+
resolveCompany: ctx.resolveCompany,
|
|
8505
|
+
trackerEnabled: ctx.laneTrackerEnabled,
|
|
8506
|
+
trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
|
|
8507
|
+
now: ctx.now,
|
|
8508
|
+
}, wrap);
|
|
8386
8509
|
registerOutpostPrunerCommand(lanes);
|
|
8387
8510
|
const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
|
|
8388
8511
|
noStoryOption.attributeName = () => "noStory";
|
|
@@ -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
|