@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.
@@ -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
- !sameQuestionCompany(ownerQuestion.company, lane.company)) {
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 (!sameQuestionCompany(current.company, lane.company)) {
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 { ...current, status: "answered", answer: ownerAnswer };
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
- return current;
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
- if (!sameQuestionCompany(primaryLane.company, lane.company)) {
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 existing = groups.find((group) => sameQuestionCompany(group.company, lane.company));
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: lane.company, lanes: [lane] });
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 || questionCompanyRefMatches(lane.company, 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
- if (!sameQuestionCompany(question.company, lane.company)) {
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
- let company;
7387
- if (opts.company)
7388
- company = await ctx.resolveCompany(opts.company);
7389
- const localLanes = opts.allHosts ? new Map(loadAllLanes(hqRoot).map((lane) => [lane.lane_id, lane])) : undefined;
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
- if (company && !(opts.allHosts
7401
- ? questionCompanyRefMatches(q.company, company)
7402
- : companyRefMatches(q.company, company))) {
7403
- return false;
7404
- }
7405
- if (company && localLanes) {
7406
- const lane = localLanes.get(q.lane_id);
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?.get(q.lane_id);
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
- if (company && (!questionCompanyRefMatches(q.company, company) ||
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
- const company = opts.company ? await ctx.resolveCompany(opts.company) : undefined;
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
- if (!sameQuestionCompany(owner.question.company, owner.lane.company)) {
7548
- 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 });
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(owner.lane.company),
7661
+ company: questionListCompanyArg(ownerCompany),
7555
7662
  });
7556
7663
  return;
7557
7664
  }
7558
7665
  await yieldEventLoop();
7559
7666
  const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
7560
- if (company &&
7561
- (!questionCompanyRefMatches(question.company, company) ||
7562
- !questionCompanyRefMatches(askingLane.company, company) ||
7563
- !sameQuestionCompany(question.company, askingLane.company))) {
7564
- throw new LanesError("question_company_mismatch", `Question ${questionId} does not belong to the requested company`, { question_id: questionId, lane_id: askingLane.lane_id });
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
- ...(askingLane.company ? { company: questionListCompanyArg(askingLane.company) } : {}),
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, question);
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, current, {
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