@indigoai-us/hq-cli 5.219.0 → 5.221.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.
@@ -12,7 +12,7 @@ export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
12
12
  export { LANE_HIRE_GRANTS_FLAG };
13
13
  import { type OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
14
14
  import { emitLaneDecisionEvents, type LaneMesh } from "../lib/lanes/mesh-events.js";
15
- import { LANE_HIRE_GRANTS_FLAG, MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
15
+ import { LANE_HIRE_GRANTS_FLAG, MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
16
16
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
17
17
  import { type RunCompactionFn } from "../lib/lanes/compaction.js";
18
18
  import { type KnownModelsDeps, type ProbeCommand } from "../lib/lanes/models.js";
@@ -107,6 +107,7 @@ export declare const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-ques
107
107
  export declare const LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG = "lanes.host-scoped-account-homes";
108
108
  export declare function parseSeniorRef(raw: string): SeniorRef;
109
109
  export declare function resolveAnswerChoice(choice: string, options: string[], freeform: boolean): string;
110
+ export declare function parseQuestionBody(raw: unknown): QuestionBody | null;
110
111
  /**
111
112
  * Spec §5.1: `hq lanes ask` from inside a lane can hit a binary or dist
112
113
  * being replaced mid-call (ENOENT / ETXTBSY / a module that no longer
@@ -14,6 +14,7 @@ import { readLocalOverride } from "@indigoai-us/hq-flags-client";
14
14
  import { Option } from "commander";
15
15
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
16
16
  import { registerLaneChecksCommand } from "./lanes-checks.js";
17
+ import { registerLaneReportCommands } from "./lanes-report.js";
17
18
  import { ensureCognitoToken } from "../utils/cognito-session.js";
18
19
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
19
20
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
@@ -42,7 +43,8 @@ import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_
42
43
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
43
44
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
44
45
  import { keepAliveExclusion, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
45
- import { questionBodyAbsPath, resolveQuestionCompany, } from "../lib/lanes/questions.js";
46
+ import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
47
+ import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
46
48
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
47
49
  import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
48
50
  import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
@@ -438,16 +440,20 @@ function readJsonFile(filePath) {
438
440
  throw err;
439
441
  }
440
442
  }
441
- function parseQuestionBody(raw) {
443
+ export function parseQuestionBody(raw) {
442
444
  if (raw === null || typeof raw !== "object" || Array.isArray(raw))
443
445
  return null;
444
446
  const obj = raw;
445
- const options = Array.isArray(obj.options)
446
- ? obj.options.filter((item) => typeof item === "string")
447
- : [];
447
+ if (typeof obj.question !== "string" || typeof obj.context !== "string" || !Array.isArray(obj.options) || obj.options.some((item) => typeof item !== "string"))
448
+ return null;
449
+ if (obj.recommended !== undefined && typeof obj.recommended !== "string")
450
+ return null;
451
+ if (obj.notes !== undefined && typeof obj.notes !== "string")
452
+ return null;
453
+ const options = obj.options.filter((item) => typeof item === "string");
448
454
  const body = {
449
- question: typeof obj.question === "string" ? obj.question : "",
450
- context: typeof obj.context === "string" ? obj.context : "",
455
+ question: obj.question,
456
+ context: obj.context,
451
457
  options,
452
458
  };
453
459
  if (typeof obj.recommended === "string")
@@ -1581,39 +1587,39 @@ function sameQuestionCompany(a, b) {
1581
1587
  const bSlug = b.slug.trim();
1582
1588
  return aSlug !== "" && aSlug === bSlug;
1583
1589
  }
1584
- function effectiveLaneQuestionCompany(lane) {
1585
- return lane.company ?? lane.mesh_company ?? null;
1590
+ function effectiveLaneQuestionCompany(lane, hqRoot) {
1591
+ return lane.company ?? lane.mesh_company ?? (hqRoot ? resolveProjectQuestionCompany(hqRoot, lane) : null);
1586
1592
  }
1587
- function questionCompanyForLane(question, lane) {
1588
- return question.company ?? effectiveLaneQuestionCompany(lane);
1593
+ function questionCompanyForLane(question, lane, hqRoot) {
1594
+ return question.company ?? effectiveLaneQuestionCompany(lane, hqRoot);
1589
1595
  }
1590
- async function resolveActiveQuestionCompany(ctx) {
1596
+ async function resolveActiveQuestionCompany(ctx, message = "Cannot access lane questions because the active HQ company could not be resolved") {
1591
1597
  try {
1592
1598
  return await ctx.resolveCompany();
1593
1599
  }
1594
1600
  catch (err) {
1595
1601
  if (!(err instanceof CompanySelectionError))
1596
1602
  throw err;
1597
- throw new LanesError("question_company_unavailable", "Cannot access lane questions because the active HQ company could not be resolved", {});
1603
+ throw new LanesError("question_company_unavailable", message, {});
1598
1604
  }
1599
1605
  }
1600
- function assertQuestionCompanyMatchesLane(question, lane, code) {
1601
- const laneCompany = effectiveLaneQuestionCompany(lane);
1606
+ function assertQuestionCompanyMatchesLane(question, lane, code, hqRoot) {
1607
+ const laneCompany = effectiveLaneQuestionCompany(lane, hqRoot);
1602
1608
  if (question.company != null && laneCompany !== null && !sameQuestionCompany(question.company, laneCompany)) {
1603
1609
  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
1610
  }
1605
1611
  }
1606
- function assertQuestionCompanyAccess(question, lane, requestedCompany) {
1607
- assertQuestionCompanyMatchesLane(question, lane, "question_company_mismatch");
1608
- const questionCompany = questionCompanyForLane(question, lane);
1612
+ function assertQuestionCompanyAccess(question, lane, requestedCompany, hqRoot) {
1613
+ assertQuestionCompanyMatchesLane(question, lane, "question_company_mismatch", hqRoot);
1614
+ const questionCompany = questionCompanyForLane(question, lane, hqRoot);
1609
1615
  if (questionCompany !== null && !sameQuestionCompany(questionCompany, requestedCompany)) {
1610
1616
  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
1617
  }
1612
1618
  }
1613
- function assertQuestionCompanyResolvedForAnswer(question, lane) {
1614
- if (questionCompanyForLane(question, lane) !== null)
1619
+ function assertQuestionCompanyResolvedForAnswer(question, lane, hqRoot, resolvedCompany) {
1620
+ if (questionCompanyForLane(question, lane, hqRoot) !== null || resolvedCompany)
1615
1621
  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`, { question_id: question.question_id, lane_id: lane.lane_id });
1622
+ 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
1623
  }
1618
1624
  function questionCompanyRefMatches(recordCompany, filter) {
1619
1625
  return recordCompany !== null &&
@@ -3579,7 +3585,7 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
3579
3585
  });
3580
3586
  }
3581
3587
  const ownerAnswer = ownerQuestion.answer;
3582
- const ownerLaneCompany = effectiveLaneQuestionCompany(lane);
3588
+ const ownerLaneCompany = effectiveLaneQuestionCompany(lane, ctx.hqRoot());
3583
3589
  if (!isQuestionAnswer(ownerAnswer) ||
3584
3590
  ownerQuestion.lane_id !== lane.lane_id ||
3585
3591
  (ownerQuestion.company !== null && ownerLaneCompany !== null &&
@@ -3596,8 +3602,8 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
3596
3602
  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
3603
  }
3598
3604
  if (current.company !== null &&
3599
- effectiveLaneQuestionCompany(lane) !== null &&
3600
- !sameQuestionCompany(current.company, effectiveLaneQuestionCompany(lane))) {
3605
+ effectiveLaneQuestionCompany(lane, ctx.hqRoot()) !== null &&
3606
+ !sameQuestionCompany(current.company, effectiveLaneQuestionCompany(lane, ctx.hqRoot()))) {
3601
3607
  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
3608
  }
3603
3609
  if (current.company !== null &&
@@ -3631,8 +3637,8 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
3631
3637
  if (!isLanesError(err) || err.error !== "question_not_found")
3632
3638
  throw err;
3633
3639
  const primaryLane = readLane(ctx.hqRoot(), lane.lane_id);
3634
- const primaryCompany = effectiveLaneQuestionCompany(primaryLane);
3635
- const ownerCompany = effectiveLaneQuestionCompany(lane);
3640
+ const primaryCompany = effectiveLaneQuestionCompany(primaryLane, ctx.hqRoot());
3641
+ const ownerCompany = effectiveLaneQuestionCompany(lane, ctx.hqRoot());
3636
3642
  if (primaryCompany !== null && ownerCompany !== null && !sameQuestionCompany(primaryCompany, ownerCompany)) {
3637
3643
  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
3644
  }
@@ -7092,10 +7098,7 @@ async function runAsk(ctx, opts) {
7092
7098
  }
7093
7099
  const lane = readLane(hqRoot, laneId);
7094
7100
  assertLaneRunsHereForOperatorMutation(ctx, lane, "ask");
7095
- const activeCompany = lane.company || lane.mesh_company
7096
- ? undefined
7097
- : await resolveActiveQuestionCompany(ctx);
7098
- const questionCompany = resolveQuestionCompany(lane, activeCompany);
7101
+ const questionCompany = await resolveQuestionCompanyWithFallback(hqRoot, lane, () => resolveActiveQuestionCompany(ctx, "Cannot raise a lane question because the active HQ company could not be resolved"));
7099
7102
  const optionList = Array.isArray(opts.option)
7100
7103
  ? opts.option
7101
7104
  : opts.option
@@ -7177,10 +7180,10 @@ async function runAsk(ctx, opts) {
7177
7180
  });
7178
7181
  }
7179
7182
  const OUTPOST_QUESTION_LOOKUP_CONCURRENCY = 4;
7180
- function groupQuestionLanesByCompany(lanes, fallbackCompany) {
7183
+ function groupQuestionLanesByCompany(hqRoot, lanes, fallbackCompany) {
7181
7184
  const groups = [];
7182
7185
  for (const lane of lanes) {
7183
- const company = effectiveLaneQuestionCompany(lane) ?? fallbackCompany ?? null;
7186
+ const company = effectiveLaneQuestionCompany(lane, hqRoot) ?? fallbackCompany ?? null;
7184
7187
  const existing = groups.find((group) => sameQuestionCompany(group.company, company));
7185
7188
  if (existing)
7186
7189
  existing.lanes.push(lane);
@@ -7329,10 +7332,10 @@ async function readOutpostQuestionDetail(ctx, owner) {
7329
7332
  throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, question_id: owner.question.question_id });
7330
7333
  }
7331
7334
  if (question.question_id !== owner.question.question_id ||
7332
- question.lane_id !== owner.lane.lane_id ||
7333
- !sameQuestionCompany(question.company, effectiveLaneQuestionCompany(owner.lane))) {
7335
+ question.lane_id !== owner.lane.lane_id) {
7334
7336
  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
7337
  }
7338
+ assertQuestionCompanyMatchesLane(question, owner.lane, "outpost_question_company_mismatch", ctx.hqRoot());
7336
7339
  const audience = parseOutpostQuestionAudience(response.audience, outpostId, 0);
7337
7340
  const body = parseOutpostQuestionBody(response.body, outpostId, question.question_id);
7338
7341
  return { question, audience, body };
@@ -7340,8 +7343,8 @@ async function readOutpostQuestionDetail(ctx, owner) {
7340
7343
  async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
7341
7344
  const hostedLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === "outpost" &&
7342
7345
  (!companyFilter ||
7343
- effectiveLaneQuestionCompany(lane) === null ||
7344
- questionCompanyRefMatches(effectiveLaneQuestionCompany(lane), companyFilter)));
7346
+ effectiveLaneQuestionCompany(lane, hqRoot) === null ||
7347
+ questionCompanyRefMatches(effectiveLaneQuestionCompany(lane, hqRoot), companyFilter)));
7345
7348
  const lanesByOutpost = new Map();
7346
7349
  for (const lane of hostedLanes) {
7347
7350
  if (lane.host.kind !== "outpost")
@@ -7354,7 +7357,7 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
7354
7357
  const errors = [];
7355
7358
  const eligibleGroups = [];
7356
7359
  for (const [outpostId, lanes] of [...lanesByOutpost.entries()].sort(([a], [b]) => a.localeCompare(b))) {
7357
- for (const group of groupQuestionLanesByCompany(lanes, companyFilter)) {
7360
+ for (const group of groupQuestionLanesByCompany(hqRoot, lanes, companyFilter)) {
7358
7361
  if (group.company === null) {
7359
7362
  if (await ctx.laneOutpostQuestionVisibilityEnabled(null)) {
7360
7363
  errors.push({
@@ -7413,9 +7416,7 @@ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
7413
7416
  const lane = localLanesById.get(question.lane_id);
7414
7417
  if (!lane)
7415
7418
  continue;
7416
- if (!sameQuestionCompany(question.company, effectiveLaneQuestionCompany(lane))) {
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
- }
7419
+ assertQuestionCompanyMatchesLane(question, lane, "outpost_question_company_mismatch", hqRoot);
7419
7420
  if (seenQuestionIds.has(question.question_id)) {
7420
7421
  throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned a duplicate question id`, { outpost_id: outpostId, question_id: question.question_id });
7421
7422
  }
@@ -7479,7 +7480,7 @@ async function runQuestionsList(ctx, opts) {
7479
7480
  const forFilter = opts.for ? parseForFilter(opts.for, ctx) : undefined;
7480
7481
  const company = opts.company
7481
7482
  ? await ctx.resolveCompany(opts.company)
7482
- : await resolveActiveQuestionCompany(ctx);
7483
+ : await resolveActiveQuestionCompany(ctx, "Cannot access lane questions because the active HQ company could not be resolved; re-run with --company <slug>");
7483
7484
  const localLanes = new Map(loadAllLanes(hqRoot).map((lane) => [lane.lane_id, lane]));
7484
7485
  const targeted = opts.from !== undefined || opts.project !== undefined || opts.story !== undefined;
7485
7486
  const localQuestions = loadAllQuestions(hqRoot)
@@ -7494,8 +7495,8 @@ async function runQuestionsList(ctx, opts) {
7494
7495
  return false;
7495
7496
  const lane = localLanes.get(q.lane_id);
7496
7497
  if (lane)
7497
- assertQuestionCompanyMatchesLane(q, lane, "question_company_mismatch");
7498
- const questionCompany = lane ? questionCompanyForLane(q, lane) : q.company;
7498
+ assertQuestionCompanyMatchesLane(q, lane, "question_company_mismatch", hqRoot);
7499
+ const questionCompany = lane ? questionCompanyForLane(q, lane, hqRoot) : q.company;
7499
7500
  if (questionCompany !== null && !sameQuestionCompany(questionCompany, company)) {
7500
7501
  if (targeted) {
7501
7502
  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 +7514,7 @@ async function runQuestionsList(ctx, opts) {
7513
7514
  ? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company)
7514
7515
  : { questions: [], errors: [] };
7515
7516
  for (const owner of outpostLoad.questions) {
7516
- assertQuestionCompanyAccess(owner.question, owner.lane, company);
7517
+ assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
7517
7518
  syncPrimaryQuestionDelivery(hqRoot, owner);
7518
7519
  }
7519
7520
  const remoteQuestionIds = new Set(outpostLoad.questions.map(({ question }) => question.question_id));
@@ -7542,7 +7543,7 @@ async function runQuestionsList(ctx, opts) {
7542
7543
  return false;
7543
7544
  if (opts.story && q.story_id !== opts.story)
7544
7545
  return false;
7545
- assertQuestionCompanyAccess(q, lane, company);
7546
+ assertQuestionCompanyAccess(q, lane, company, hqRoot);
7546
7547
  if (forFilter) {
7547
7548
  if (!audienceMatches(audience, forFilter))
7548
7549
  return false;
@@ -7618,9 +7619,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7618
7619
  assertNoSecretText("freeform answer", opts.freeform);
7619
7620
  if (opts.notes !== undefined)
7620
7621
  assertNoSecretText("answer notes", opts.notes);
7621
- const company = opts.company
7622
- ? await ctx.resolveCompany(opts.company)
7623
- : await resolveActiveQuestionCompany(ctx);
7622
+ const explicitCompany = opts.company ? await ctx.resolveCompany(opts.company) : undefined;
7624
7623
  let question;
7625
7624
  try {
7626
7625
  question = readQuestion(hqRoot, questionId);
@@ -7628,6 +7627,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7628
7627
  catch (err) {
7629
7628
  if (!isLanesError(err) || err.error !== "question_not_found")
7630
7629
  throw err;
7630
+ 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
7631
  const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company);
7632
7632
  if (lookup.errors.length > 0) {
7633
7633
  throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
@@ -7640,7 +7640,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7640
7640
  const allCompanyLookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
7641
7641
  const otherCompanyMatch = allCompanyLookup.questions.find((entry) => entry.question.question_id === questionId);
7642
7642
  if (otherCompanyMatch) {
7643
- assertQuestionCompanyAccess(otherCompanyMatch.question, otherCompanyMatch.lane, company);
7643
+ assertQuestionCompanyAccess(otherCompanyMatch.question, otherCompanyMatch.lane, company, hqRoot);
7644
7644
  }
7645
7645
  if (allCompanyLookup.errors.length > 0) {
7646
7646
  throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
@@ -7657,24 +7657,31 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7657
7657
  if (!owner) {
7658
7658
  throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
7659
7659
  }
7660
- assertQuestionCompanyAccess(owner.question, owner.lane, company);
7661
- assertQuestionCompanyResolvedForAnswer(owner.question, owner.lane);
7660
+ assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
7661
+ const ownerCompany = await resolveQuestionCompanyWithFallback(hqRoot, owner.lane, async () => explicitCompany ?? company);
7662
+ assertQuestionCompanyResolvedForAnswer(owner.question, owner.lane, hqRoot, ownerCompany);
7662
7663
  throwIfQuestionNotPending(owner.question);
7663
7664
  await yieldEventLoop();
7664
7665
  await dispatchOutpostQuestionAnswer(ctx, owner.lane, questionId, {
7665
7666
  ...opts,
7666
- company: questionListCompanyArg(effectiveLaneQuestionCompany(owner.lane)),
7667
+ company: questionListCompanyArg(ownerCompany),
7667
7668
  });
7668
7669
  return;
7669
7670
  }
7670
7671
  await yieldEventLoop();
7671
7672
  const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
7672
- assertQuestionCompanyAccess(question, askingLane, company);
7673
- assertQuestionCompanyResolvedForAnswer(question, askingLane);
7673
+ 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>`));
7674
+ const requestedCompany = explicitCompany ?? resolvedCompany;
7675
+ assertQuestionCompanyAccess(question, askingLane, requestedCompany, hqRoot);
7676
+ assertQuestionCompanyResolvedForAnswer(question, askingLane, hqRoot, resolvedCompany);
7677
+ const questionCompany = question.company ?? effectiveLaneQuestionCompany(askingLane, hqRoot) ?? resolvedCompany;
7678
+ const questionForBody = question.company === null
7679
+ ? { ...question, company: questionCompany }
7680
+ : question;
7674
7681
  if (askingLane.host.kind === "outpost" && !laneIsHostedHere(ctx, askingLane)) {
7675
7682
  await dispatchOutpostQuestionAnswer(ctx, askingLane, questionId, {
7676
7683
  ...opts,
7677
- company: questionListCompanyArg(effectiveLaneQuestionCompany(askingLane)),
7684
+ company: questionListCompanyArg(questionCompany),
7678
7685
  });
7679
7686
  return;
7680
7687
  }
@@ -7685,7 +7692,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7685
7692
  if (laneAcceptsDropboxDelivery(askingLane)) {
7686
7693
  assertLaneRunsHereForOperatorMutation(ctx, askingLane, "deliver an answer to");
7687
7694
  }
7688
- const body = requireQuestionBody(hqRoot, question);
7695
+ const body = requireQuestionBody(hqRoot, questionForBody);
7689
7696
  // A trimmed-empty --freeform is absent: it must not win over --choice and
7690
7697
  // must not become a freeform answer of "".
7691
7698
  const freeform = opts.freeform?.trim() || undefined;
@@ -7702,10 +7709,15 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7702
7709
  : "user";
7703
7710
  const updated = casUpdateQuestion(hqRoot, questionId, (current) => {
7704
7711
  throwIfQuestionNotPending(current);
7712
+ assertQuestionCompanyAccess(current, askingLane, requestedCompany, hqRoot);
7713
+ const recordedCompany = current.company ?? questionCompany;
7714
+ const currentForBody = current.company === null
7715
+ ? { ...current, company: recordedCompany }
7716
+ : current;
7705
7717
  let notesRef;
7706
7718
  let nextBody = current.body;
7707
7719
  if (opts.notes) {
7708
- const stored = writeQuestionBody(hqRoot, current, {
7720
+ const stored = writeQuestionBody(hqRoot, currentForBody, {
7709
7721
  ...body,
7710
7722
  notes: opts.notes,
7711
7723
  });
@@ -7714,6 +7726,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
7714
7726
  }
7715
7727
  return {
7716
7728
  ...current,
7729
+ company: recordedCompany,
7717
7730
  body: nextBody,
7718
7731
  status: "answered",
7719
7732
  answer: {
@@ -8499,6 +8512,18 @@ export function registerLanesCommand(parent, deps = {}) {
8499
8512
  trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
8500
8513
  now: ctx.now,
8501
8514
  }, wrap);
8515
+ registerLaneReportCommands(lanes, {
8516
+ hqRoot: ctx.hqRoot,
8517
+ resolveCompany: ctx.resolveCompany,
8518
+ trackerEnabled: ctx.laneTrackerEnabled,
8519
+ trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
8520
+ now: ctx.now,
8521
+ readQuestionBody: (root, question) => {
8522
+ const body = requireQuestionBody(root, question);
8523
+ assertQuestionBodySecretSafe(body);
8524
+ return body;
8525
+ },
8526
+ }, wrap);
8502
8527
  registerOutpostPrunerCommand(lanes);
8503
8528
  const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
8504
8529
  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