@indigoai-us/hq-cli 5.198.0 → 5.199.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,7 +22,7 @@ import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGr
22
22
  import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
23
23
  export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
24
24
  export { LANE_HIRE_GRANTS_FLAG };
25
- import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionAnswerCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
25
+ import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionAnswerCommand, buildOutpostQuestionListCommand, buildOutpostQuestionShowCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
26
26
  import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
27
27
  import { BALANCE_MOVABLE_STATES, CLAUDE_ACCOUNT_BALANCE_FLAG, accountLoadsForReport, assertBalanceApplyEnabled, planClaudeBalanceMoves, selectClaudeAccount, } from "../lib/lanes/claude-account-balance.js";
28
28
  import { parseUserYaml } from "../utils/user-yaml-error.js";
@@ -31,7 +31,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
31
31
  import { openMeshTransport } from "../lib/mesh/client.js";
32
32
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
33
33
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
34
- import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
34
+ import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
35
35
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
36
36
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
37
37
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
@@ -55,6 +55,7 @@ const VENDOR_LOGIN = {
55
55
  };
56
56
  export { emitLaneDecisionEvents };
57
57
  export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
58
+ export const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-question-visibility";
58
59
  function defaultLanePlacementEnabled() {
59
60
  return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
60
61
  }
@@ -72,6 +73,12 @@ function defaultLaneTrackerMeshEnabled(company) {
72
73
  function defaultLaneHireGrantsEnabled(company) {
73
74
  return readLanesRegistryFlag(LANE_HIRE_GRANTS_FLAG, { company });
74
75
  }
76
+ function defaultLaneOutpostQuestionVisibilityEnabled(company, env) {
77
+ return readLanesRegistryFlag(LANE_OUTPOST_QUESTION_VISIBILITY_FLAG, {
78
+ company,
79
+ env,
80
+ });
81
+ }
75
82
  function withoutProviderHome(value) {
76
83
  if (Array.isArray(value))
77
84
  return value.map(withoutProviderHome);
@@ -271,6 +278,8 @@ function resolveDeps(deps = {}) {
271
278
  isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
272
279
  withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
273
280
  laneClaudeAccountBalanceEnabled: deps.laneClaudeAccountBalanceEnabled ?? defaultLaneClaudeAccountBalanceEnabled,
281
+ laneOutpostQuestionVisibilityEnabled: deps.laneOutpostQuestionVisibilityEnabled ??
282
+ ((company) => defaultLaneOutpostQuestionVisibilityEnabled(company, env)),
274
283
  laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
275
284
  laneTrackerMeshEnabled: deps.laneTrackerMeshEnabled ?? defaultLaneTrackerMeshEnabled,
276
285
  trackerActor: deps.trackerActor ?? defaultTrackerActor,
@@ -1407,6 +1416,22 @@ function sameLaneCompany(a, b) {
1407
1416
  return a === b;
1408
1417
  return (a.uid !== "" && a.uid === b.uid) || (a.slug !== "" && a.slug === b.slug);
1409
1418
  }
1419
+ function sameQuestionCompany(a, b) {
1420
+ if (a === null || b === null)
1421
+ return a === b;
1422
+ const aUid = a.uid.trim();
1423
+ const bUid = b.uid.trim();
1424
+ if (aUid || bUid)
1425
+ return aUid !== "" && aUid === bUid;
1426
+ const aSlug = a.slug.trim();
1427
+ const bSlug = b.slug.trim();
1428
+ return aSlug !== "" && aSlug === bSlug;
1429
+ }
1430
+ function questionCompanyRefMatches(recordCompany, filter) {
1431
+ return recordCompany !== null &&
1432
+ recordCompany !== undefined &&
1433
+ sameQuestionCompany(recordCompany, filter);
1434
+ }
1410
1435
  function companyRefMatches(recordCompany, filter) {
1411
1436
  return (recordCompany !== null &&
1412
1437
  recordCompany !== undefined &&
@@ -3255,6 +3280,7 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
3255
3280
  choice: options.choice,
3256
3281
  freeform: options.freeform,
3257
3282
  notes: options.notes,
3283
+ company: options.company,
3258
3284
  }),
3259
3285
  });
3260
3286
  }
@@ -3318,7 +3344,9 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
3318
3344
  });
3319
3345
  }
3320
3346
  const ownerAnswer = ownerQuestion.answer;
3321
- if (!isQuestionAnswer(ownerAnswer)) {
3347
+ if (!isQuestionAnswer(ownerAnswer) ||
3348
+ ownerQuestion.lane_id !== lane.lane_id ||
3349
+ !sameQuestionCompany(ownerQuestion.company, lane.company)) {
3322
3350
  throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return the confirmed answer for question ${questionId}`, {
3323
3351
  lane_id: lane.lane_id,
3324
3352
  question_id: questionId,
@@ -3326,10 +3354,13 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
3326
3354
  command_id: dispatched.commandId,
3327
3355
  });
3328
3356
  }
3329
- const syncedQuestion = casUpdateQuestion(ctx.hqRoot(), questionId, (current) => {
3357
+ const updatePrimaryCopy = () => casUpdateQuestion(ctx.hqRoot(), questionId, (current) => {
3330
3358
  if (current.lane_id !== lane.lane_id) {
3331
3359
  throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} belongs to a different lane than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
3332
3360
  }
3361
+ if (!sameQuestionCompany(current.company, lane.company)) {
3362
+ throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} belongs to a different company than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
3363
+ }
3333
3364
  if (current.status === "pending") {
3334
3365
  return { ...current, status: "answered", answer: ownerAnswer };
3335
3366
  }
@@ -3338,6 +3369,26 @@ async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
3338
3369
  }
3339
3370
  throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} is ${current.status} or has a different answer than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
3340
3371
  });
3372
+ let syncedQuestion;
3373
+ try {
3374
+ syncedQuestion = updatePrimaryCopy();
3375
+ }
3376
+ catch (err) {
3377
+ if (!isLanesError(err) || err.error !== "question_not_found")
3378
+ throw err;
3379
+ const primaryLane = readLane(ctx.hqRoot(), lane.lane_id);
3380
+ if (!sameQuestionCompany(primaryLane.company, lane.company)) {
3381
+ throw new LanesError("outpost_question_sync_conflict", `Primary lane ${lane.lane_id} belongs to a different company than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
3382
+ }
3383
+ try {
3384
+ syncedQuestion = createQuestion(ctx.hqRoot(), ownerQuestion);
3385
+ }
3386
+ catch (createErr) {
3387
+ if (!isLanesError(createErr) || createErr.error !== "question_exists")
3388
+ throw createErr;
3389
+ syncedQuestion = updatePrimaryCopy();
3390
+ }
3391
+ }
3341
3392
  printJson({
3342
3393
  ...remote,
3343
3394
  lane_id: lane.lane_id,
@@ -6582,6 +6633,271 @@ async function runAsk(ctx, opts) {
6582
6633
  ask_delivery_failed: failure,
6583
6634
  });
6584
6635
  }
6636
+ const OUTPOST_QUESTION_LOOKUP_CONCURRENCY = 4;
6637
+ function groupQuestionLanesByCompany(lanes) {
6638
+ const groups = [];
6639
+ for (const lane of lanes) {
6640
+ const existing = groups.find((group) => sameQuestionCompany(group.company, lane.company));
6641
+ if (existing)
6642
+ existing.lanes.push(lane);
6643
+ else
6644
+ groups.push({ company: lane.company, lanes: [lane] });
6645
+ }
6646
+ return groups;
6647
+ }
6648
+ function questionListCompanyArg(company) {
6649
+ if (company === null) {
6650
+ throw new LanesError("outpost_question_company_unavailable", "An Outpost question lookup requires a recorded company identity", {});
6651
+ }
6652
+ const value = company.uid.trim() || company.slug.trim();
6653
+ if (!value) {
6654
+ throw new LanesError("outpost_question_company_invalid", "An Outpost question lookup needs a company slug or uid", {});
6655
+ }
6656
+ return value;
6657
+ }
6658
+ async function readOutpostQuestionRecords(ctx, outpostId, company) {
6659
+ const companyArg = questionListCompanyArg(company);
6660
+ let result;
6661
+ try {
6662
+ result = await ctx.dispatchOutpostExec({
6663
+ outpostId,
6664
+ command: buildOutpostQuestionListCommand({ company: companyArg }),
6665
+ timeoutSeconds: CAPACITY_PROBE_TIMEOUT_MS / 1000,
6666
+ waitMs: CAPACITY_PROBE_TIMEOUT_MS,
6667
+ });
6668
+ }
6669
+ catch {
6670
+ throw new LanesError("outpost_question_list_unavailable", `Could not read questions from Outpost ${outpostId}`, { outpost_id: outpostId });
6671
+ }
6672
+ if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
6673
+ throw new LanesError("outpost_question_list_unavailable", `Outpost ${outpostId} did not return a successful question list`, { outpost_id: outpostId });
6674
+ }
6675
+ let body;
6676
+ try {
6677
+ body = JSON.parse(result.stdout);
6678
+ }
6679
+ catch {
6680
+ throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned invalid question-list JSON`, { outpost_id: outpostId });
6681
+ }
6682
+ if (body === null ||
6683
+ typeof body !== "object" ||
6684
+ Array.isArray(body) ||
6685
+ body.ok !== true ||
6686
+ body.action !== "questions.list" ||
6687
+ !Array.isArray(body.questions)) {
6688
+ throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned a malformed question list`, { outpost_id: outpostId });
6689
+ }
6690
+ const rows = body.questions;
6691
+ const questions = [];
6692
+ for (const [index, row] of rows.entries()) {
6693
+ if (row === null || typeof row !== "object" || Array.isArray(row)) {
6694
+ throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, index });
6695
+ }
6696
+ const rowObject = row;
6697
+ let question;
6698
+ try {
6699
+ question = parseQuestionRecord(row);
6700
+ }
6701
+ catch {
6702
+ throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, index });
6703
+ }
6704
+ const audience = parseOutpostQuestionAudience(rowObject.audience, outpostId, index);
6705
+ questions.push({ question, audience });
6706
+ }
6707
+ return questions;
6708
+ }
6709
+ function parseOutpostQuestionAudience(raw, outpostId, index) {
6710
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
6711
+ throw new LanesError("outpost_question_audience_invalid", `Outpost ${outpostId} returned a question without a valid owner-computed audience`, { outpost_id: outpostId, index });
6712
+ }
6713
+ const audience = raw;
6714
+ try {
6715
+ if (audience.kind === "user" && audience.id === undefined) {
6716
+ return parseSeniorRef("user");
6717
+ }
6718
+ if (typeof audience.kind !== "string" || typeof audience.id !== "string") {
6719
+ throw new Error("audience kind and id are invalid");
6720
+ }
6721
+ return parseSeniorRef(`${audience.kind}:${audience.id}`);
6722
+ }
6723
+ catch {
6724
+ throw new LanesError("outpost_question_audience_invalid", `Outpost ${outpostId} returned an invalid owner-computed question audience`, { outpost_id: outpostId, index });
6725
+ }
6726
+ }
6727
+ function parseOutpostQuestionBody(raw, outpostId, questionId) {
6728
+ if (raw === null)
6729
+ return null;
6730
+ if (typeof raw !== "object" || Array.isArray(raw)) {
6731
+ throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned a malformed body for question ${questionId}`, { outpost_id: outpostId, question_id: questionId });
6732
+ }
6733
+ const body = raw;
6734
+ if (typeof body.question !== "string" ||
6735
+ typeof body.context !== "string" ||
6736
+ !Array.isArray(body.options) ||
6737
+ body.options.some((option) => typeof option !== "string") ||
6738
+ (body.recommended !== undefined && typeof body.recommended !== "string") ||
6739
+ (body.notes !== undefined && typeof body.notes !== "string")) {
6740
+ throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned a malformed body for question ${questionId}`, { outpost_id: outpostId, question_id: questionId });
6741
+ }
6742
+ return parseQuestionBody(raw);
6743
+ }
6744
+ async function readOutpostQuestionDetail(ctx, owner) {
6745
+ if (owner.lane.host.kind !== "outpost") {
6746
+ throw new LanesError("outpost_question_owner_invalid", `Question ${owner.question.question_id} does not have an Outpost owner`, { question_id: owner.question.question_id, lane_id: owner.lane.lane_id });
6747
+ }
6748
+ const outpostId = owner.lane.host.id;
6749
+ let result;
6750
+ try {
6751
+ result = await ctx.dispatchOutpostExec({
6752
+ outpostId,
6753
+ command: buildOutpostQuestionShowCommand(owner.question.question_id),
6754
+ timeoutSeconds: CAPACITY_PROBE_TIMEOUT_MS / 1000,
6755
+ waitMs: CAPACITY_PROBE_TIMEOUT_MS,
6756
+ });
6757
+ }
6758
+ catch {
6759
+ throw new LanesError("outpost_question_show_unavailable", `Could not read question ${owner.question.question_id} from Outpost ${outpostId}`, { outpost_id: outpostId, question_id: owner.question.question_id });
6760
+ }
6761
+ if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
6762
+ throw new LanesError("outpost_question_show_unavailable", `Outpost ${outpostId} did not return question ${owner.question.question_id}`, { outpost_id: outpostId, question_id: owner.question.question_id });
6763
+ }
6764
+ let raw;
6765
+ try {
6766
+ raw = JSON.parse(result.stdout);
6767
+ }
6768
+ catch {
6769
+ throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned invalid question-show JSON`, { outpost_id: outpostId, question_id: owner.question.question_id });
6770
+ }
6771
+ if (raw === null ||
6772
+ typeof raw !== "object" ||
6773
+ Array.isArray(raw) ||
6774
+ raw.ok !== true ||
6775
+ raw.action !== "questions.show" ||
6776
+ !Object.prototype.hasOwnProperty.call(raw, "body")) {
6777
+ throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned a malformed question-show response`, { outpost_id: outpostId, question_id: owner.question.question_id });
6778
+ }
6779
+ const response = raw;
6780
+ let question;
6781
+ try {
6782
+ question = parseQuestionRecord(response.question);
6783
+ }
6784
+ catch {
6785
+ throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, question_id: owner.question.question_id });
6786
+ }
6787
+ if (question.question_id !== owner.question.question_id ||
6788
+ question.lane_id !== owner.lane.lane_id ||
6789
+ !sameQuestionCompany(question.company, owner.lane.company)) {
6790
+ throw new LanesError("outpost_question_company_mismatch", `Outpost ${outpostId} returned question ${owner.question.question_id} for a different lane or company`, { outpost_id: outpostId, question_id: owner.question.question_id });
6791
+ }
6792
+ const audience = parseOutpostQuestionAudience(response.audience, outpostId, 0);
6793
+ const body = parseOutpostQuestionBody(response.body, outpostId, question.question_id);
6794
+ return { question, audience, body };
6795
+ }
6796
+ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
6797
+ const hostedLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === "outpost" &&
6798
+ (!companyFilter || questionCompanyRefMatches(lane.company, companyFilter)));
6799
+ const lanesByOutpost = new Map();
6800
+ for (const lane of hostedLanes) {
6801
+ if (lane.host.kind !== "outpost")
6802
+ continue;
6803
+ const lanes = lanesByOutpost.get(lane.host.id) ?? [];
6804
+ lanes.push(lane);
6805
+ lanesByOutpost.set(lane.host.id, lanes);
6806
+ }
6807
+ const questions = [];
6808
+ const errors = [];
6809
+ const eligibleGroups = [];
6810
+ for (const [outpostId, lanes] of [...lanesByOutpost.entries()].sort(([a], [b]) => a.localeCompare(b))) {
6811
+ for (const group of groupQuestionLanesByCompany(lanes)) {
6812
+ if (companyFilter && !questionCompanyRefMatches(group.company, companyFilter))
6813
+ continue;
6814
+ if (group.company === null) {
6815
+ errors.push({
6816
+ host: `outpost:${outpostId}`,
6817
+ error: "outpost_question_company_unavailable",
6818
+ });
6819
+ continue;
6820
+ }
6821
+ if (await ctx.laneOutpostQuestionVisibilityEnabled(group.company)) {
6822
+ eligibleGroups.push({ outpostId, group });
6823
+ }
6824
+ }
6825
+ }
6826
+ if (eligibleGroups.length === 0) {
6827
+ return { questions, errors: [...new Map(errors.map((entry) => [entry.host, entry])).values()] };
6828
+ }
6829
+ let knownOutpostIds;
6830
+ try {
6831
+ knownOutpostIds = await ctx.listOutpostIds();
6832
+ }
6833
+ catch {
6834
+ return {
6835
+ questions,
6836
+ errors: [...new Map([
6837
+ ...errors,
6838
+ ...[...new Set(eligibleGroups.map(({ outpostId }) => outpostId))].map((outpostId) => ({
6839
+ host: `outpost:${outpostId}`,
6840
+ error: "outpost_list_unavailable",
6841
+ })),
6842
+ ].map((entry) => [entry.host, entry])).values()],
6843
+ };
6844
+ }
6845
+ const normalizedOutpostIds = knownOutpostIds.map((id) => typeof id === "string" ? id.trim() : "");
6846
+ if (knownOutpostIds.length > MAX_PLACEMENT_OUTPOSTS ||
6847
+ normalizedOutpostIds.some((id) => id === "") ||
6848
+ new Set(normalizedOutpostIds).size !== normalizedOutpostIds.length) {
6849
+ throw new LanesError("outpost_list_invalid", "Outpost list is malformed or exceeds the placement limit", { count: knownOutpostIds.length, maximum: MAX_PLACEMENT_OUTPOSTS });
6850
+ }
6851
+ const knownOutpostIdSet = new Set(normalizedOutpostIds);
6852
+ const loadedGroups = await mapWithConcurrency(eligibleGroups, OUTPOST_QUESTION_LOOKUP_CONCURRENCY, async ({ outpostId, group }) => {
6853
+ if (!knownOutpostIdSet.has(outpostId)) {
6854
+ return {
6855
+ questions: [],
6856
+ errors: [{ host: `outpost:${outpostId}`, error: "unknown_outpost" }],
6857
+ };
6858
+ }
6859
+ try {
6860
+ const remoteQuestions = await readOutpostQuestionRecords(ctx, outpostId, group.company);
6861
+ const localLanesById = new Map(group.lanes.map((lane) => [lane.lane_id, lane]));
6862
+ const groupQuestions = [];
6863
+ const seenQuestionIds = new Set();
6864
+ for (const { question, audience } of remoteQuestions) {
6865
+ const lane = localLanesById.get(question.lane_id);
6866
+ if (!lane)
6867
+ continue;
6868
+ if (!sameQuestionCompany(question.company, lane.company)) {
6869
+ throw new LanesError("outpost_question_company_mismatch", `Outpost ${outpostId} question ${question.question_id} does not match its primary lane company`, { outpost_id: outpostId, question_id: question.question_id });
6870
+ }
6871
+ if (seenQuestionIds.has(question.question_id)) {
6872
+ throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned a duplicate question id`, { outpost_id: outpostId, question_id: question.question_id });
6873
+ }
6874
+ seenQuestionIds.add(question.question_id);
6875
+ groupQuestions.push({
6876
+ question,
6877
+ lane,
6878
+ host: `outpost:${outpostId}`,
6879
+ audience,
6880
+ });
6881
+ }
6882
+ return { questions: groupQuestions, errors: [] };
6883
+ }
6884
+ catch (err) {
6885
+ return {
6886
+ questions: [],
6887
+ errors: [{
6888
+ host: `outpost:${outpostId}`,
6889
+ error: isLanesError(err) ? err.error : "outpost_question_list_unavailable",
6890
+ }],
6891
+ };
6892
+ }
6893
+ });
6894
+ questions.push(...loadedGroups.flatMap(({ questions: groupQuestions }) => groupQuestions));
6895
+ errors.push(...loadedGroups.flatMap(({ errors: groupErrors }) => groupErrors));
6896
+ return {
6897
+ questions,
6898
+ errors: [...new Map(errors.map((entry) => [entry.host, entry])).values()],
6899
+ };
6900
+ }
6585
6901
  async function runQuestionsList(ctx, opts) {
6586
6902
  const hqRoot = ctx.hqRoot();
6587
6903
  const status = opts.status ? parseQuestionStatus(opts.status) : undefined;
@@ -6589,7 +6905,8 @@ async function runQuestionsList(ctx, opts) {
6589
6905
  let company;
6590
6906
  if (opts.company)
6591
6907
  company = await ctx.resolveCompany(opts.company);
6592
- const questions = loadAllQuestions(hqRoot)
6908
+ const localLanes = opts.allHosts ? new Map(loadAllLanes(hqRoot).map((lane) => [lane.lane_id, lane])) : undefined;
6909
+ const localQuestions = loadAllQuestions(hqRoot)
6593
6910
  .filter((q) => {
6594
6911
  if (status && q.status !== status)
6595
6912
  return false;
@@ -6599,26 +6916,116 @@ async function runQuestionsList(ctx, opts) {
6599
6916
  return false;
6600
6917
  if (opts.story && q.story_id !== opts.story)
6601
6918
  return false;
6602
- if (company && !companyRefMatches(q.company, company)) {
6919
+ if (company && !(opts.allHosts
6920
+ ? questionCompanyRefMatches(q.company, company)
6921
+ : companyRefMatches(q.company, company))) {
6603
6922
  return false;
6604
6923
  }
6924
+ if (company && localLanes) {
6925
+ const lane = localLanes.get(q.lane_id);
6926
+ if (!lane || !questionCompanyRefMatches(lane.company, company) || !sameQuestionCompany(q.company, lane.company)) {
6927
+ return false;
6928
+ }
6929
+ }
6605
6930
  if (forFilter) {
6606
6931
  const audience = resolveAudience(hqRoot, q, ctx);
6607
6932
  if (!audienceMatches(audience, forFilter))
6608
6933
  return false;
6609
6934
  }
6610
6935
  return true;
6611
- })
6612
- .map((q) => ({
6613
- ...q,
6614
- audience: resolveAudience(hqRoot, q, ctx),
6615
- ...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
6616
- }));
6617
- printJson({ ok: true, action: "questions.list", questions });
6936
+ });
6937
+ const outpostLoad = opts.allHosts
6938
+ ? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company)
6939
+ : { questions: [], errors: [] };
6940
+ const remoteQuestionIds = new Set(outpostLoad.questions.map(({ question }) => question.question_id));
6941
+ const questions = [
6942
+ ...localQuestions
6943
+ .filter((q) => !remoteQuestionIds.has(q.question_id))
6944
+ .map((q) => {
6945
+ const row = {
6946
+ ...q,
6947
+ audience: resolveAudience(hqRoot, q, ctx),
6948
+ ...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
6949
+ };
6950
+ if (!opts.allHosts)
6951
+ return row;
6952
+ const lane = localLanes?.get(q.lane_id);
6953
+ const host = lane?.host.kind === "outpost" ? `outpost:${lane.host.id}` : "local";
6954
+ return { ...row, host };
6955
+ }),
6956
+ ...outpostLoad.questions
6957
+ .filter(({ question: q, lane, audience }) => {
6958
+ if (status && q.status !== status)
6959
+ return false;
6960
+ if (opts.from && q.lane_id !== opts.from && q.origin.id !== opts.from)
6961
+ return false;
6962
+ if (opts.project && q.project_id !== opts.project)
6963
+ return false;
6964
+ if (opts.story && q.story_id !== opts.story)
6965
+ return false;
6966
+ if (company && (!questionCompanyRefMatches(q.company, company) ||
6967
+ !questionCompanyRefMatches(lane.company, company) ||
6968
+ !sameQuestionCompany(q.company, lane.company))) {
6969
+ return false;
6970
+ }
6971
+ if (forFilter) {
6972
+ if (!audienceMatches(audience, forFilter))
6973
+ return false;
6974
+ }
6975
+ return true;
6976
+ })
6977
+ .map(({ question: q, host, audience }) => ({
6978
+ ...q,
6979
+ host,
6980
+ audience,
6981
+ ...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
6982
+ })),
6983
+ ];
6984
+ printJson({
6985
+ ok: true,
6986
+ action: "questions.list",
6987
+ questions,
6988
+ ...(outpostLoad.errors.length > 0 ? { errors: outpostLoad.errors } : {}),
6989
+ });
6618
6990
  }
6619
6991
  async function runQuestionsShow(ctx, questionId) {
6620
6992
  const hqRoot = ctx.hqRoot();
6621
- const question = readQuestion(hqRoot, questionId);
6993
+ let question;
6994
+ try {
6995
+ question = readQuestion(hqRoot, questionId);
6996
+ }
6997
+ catch (err) {
6998
+ if (!isLanesError(err) || err.error !== "question_not_found")
6999
+ throw err;
7000
+ const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
7001
+ if (lookup.errors.length > 0) {
7002
+ throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
7003
+ question_id: questionId,
7004
+ hosts: lookup.errors.map(({ host, error }) => ({ host, error })),
7005
+ });
7006
+ }
7007
+ const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
7008
+ if (matches.length === 0) {
7009
+ throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
7010
+ }
7011
+ if (matches.length > 1) {
7012
+ throw new LanesError("outpost_question_ambiguous", `Question ${questionId} is present on more than one Outpost`, { question_id: questionId, owner_count: matches.length });
7013
+ }
7014
+ const owner = matches[0];
7015
+ if (!owner) {
7016
+ throw new LanesError("outpost_question_ambiguous", `Question ${questionId} has no unique Outpost owner`, { question_id: questionId });
7017
+ }
7018
+ const detail = await readOutpostQuestionDetail(ctx, owner);
7019
+ printJson({
7020
+ ok: true,
7021
+ action: "questions.show",
7022
+ question: detail.question,
7023
+ audience: detail.audience,
7024
+ body: detail.body,
7025
+ host: owner.host,
7026
+ });
7027
+ return;
7028
+ }
6622
7029
  const body = readQuestionBody(hqRoot, question);
6623
7030
  printJson({
6624
7031
  ok: true,
@@ -6630,11 +7037,56 @@ async function runQuestionsShow(ctx, questionId) {
6630
7037
  }
6631
7038
  async function runQuestionsAnswer(ctx, questionId, opts) {
6632
7039
  const hqRoot = ctx.hqRoot();
6633
- const question = readQuestion(hqRoot, questionId);
7040
+ const company = opts.company ? await ctx.resolveCompany(opts.company) : undefined;
7041
+ let question;
7042
+ try {
7043
+ question = readQuestion(hqRoot, questionId);
7044
+ }
7045
+ catch (err) {
7046
+ if (!isLanesError(err) || err.error !== "question_not_found")
7047
+ throw err;
7048
+ const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company);
7049
+ if (lookup.errors.length > 0) {
7050
+ throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
7051
+ question_id: questionId,
7052
+ hosts: lookup.errors.map(({ host, error }) => ({ host, error })),
7053
+ });
7054
+ }
7055
+ const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
7056
+ if (matches.length === 0) {
7057
+ throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
7058
+ }
7059
+ if (matches.length > 1) {
7060
+ throw new LanesError("outpost_question_ambiguous", `Question ${questionId} is present on more than one Outpost`, { question_id: questionId, owner_count: matches.length });
7061
+ }
7062
+ const owner = matches[0];
7063
+ if (!owner) {
7064
+ throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
7065
+ }
7066
+ if (!sameQuestionCompany(owner.question.company, owner.lane.company)) {
7067
+ throw new LanesError("outpost_question_company_mismatch", `Question ${questionId} does not match its primary lane company`, { question_id: questionId, lane_id: owner.lane.lane_id });
7068
+ }
7069
+ throwIfQuestionNotPending(owner.question);
7070
+ await yieldEventLoop();
7071
+ await dispatchOutpostQuestionAnswer(ctx, owner.lane, questionId, {
7072
+ ...opts,
7073
+ company: questionListCompanyArg(owner.lane.company),
7074
+ });
7075
+ return;
7076
+ }
6634
7077
  await yieldEventLoop();
6635
7078
  const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
7079
+ if (company &&
7080
+ (!questionCompanyRefMatches(question.company, company) ||
7081
+ !questionCompanyRefMatches(askingLane.company, company) ||
7082
+ !sameQuestionCompany(question.company, askingLane.company))) {
7083
+ throw new LanesError("question_company_mismatch", `Question ${questionId} does not belong to the requested company`, { question_id: questionId, lane_id: askingLane.lane_id });
7084
+ }
6636
7085
  if (askingLane.host.kind === "outpost" && !laneIsHostedHere(ctx, askingLane)) {
6637
- await dispatchOutpostQuestionAnswer(ctx, askingLane, questionId, opts);
7086
+ await dispatchOutpostQuestionAnswer(ctx, askingLane, questionId, {
7087
+ ...opts,
7088
+ ...(askingLane.company ? { company: questionListCompanyArg(askingLane.company) } : {}),
7089
+ });
6638
7090
  return;
6639
7091
  }
6640
7092
  throwIfQuestionNotPending(question);
@@ -6771,7 +7223,7 @@ function deliverAnswerToAskingLane(ctx, question, choice, at) {
6771
7223
  }
6772
7224
  assertLaneRunsHereForOperatorMutation(ctx, lane, "deliver an answer to");
6773
7225
  appendDropboxMessage(hqRoot, laneId, answerDropboxMessage(question, choice, at, mintMessageId()));
6774
- return { status: "dropped" };
7226
+ return { status: "awaiting_next_tool_call" };
6775
7227
  }
6776
7228
  catch (err) {
6777
7229
  writeAnswerDeliveryFailed(hqRoot, laneId, question.question_id, err, at);
@@ -7587,6 +8039,7 @@ export function registerLanesCommand(parent, deps = {}) {
7587
8039
  questions
7588
8040
  .command("list")
7589
8041
  .description("List questions")
8042
+ .option("--all-hosts", "Include questions from Outposts that own this primary's lanes")
7590
8043
  .option("--for <ref>", "Who must decide (ref | me | user)")
7591
8044
  .option("--from <lane-id>", "Which lane asked")
7592
8045
  .option("--company <slug|uid>", "Filter by company")
@@ -7610,6 +8063,7 @@ export function registerLanesCommand(parent, deps = {}) {
7610
8063
  .option("--choice <label|N>", "1-based index or option label")
7611
8064
  .option("--notes <text>", "Free-text notes stored with the body")
7612
8065
  .option("--freeform <text>", "Supply a free-form answer for a question with options")
8066
+ .option("--company <slug|uid>", "Require the question lane to match this company")
7613
8067
  .action(wrap(async (questionId, opts) => {
7614
8068
  await runQuestionsAnswer(ctx, questionId, opts);
7615
8069
  }));
@@ -1,5 +1,7 @@
1
1
  import { loadCachedTokens } from "@indigoai-us/hq-cloud";
2
2
  import { createFlagClient, } from "@indigoai-us/hq-flags-client";
3
+ import { resolveProcessFlagCompanyContext } from "../flag-registry.js";
4
+ import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
3
5
  export const HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG = "hook-timeout-debug-context";
4
6
  export const HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_FALLBACK = false;
5
7
  export const HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_TIMEOUT_MS = 150;
@@ -20,12 +22,11 @@ function combineSignals(first, second) {
20
22
  */
21
23
  export async function hookTimeoutDebugContextEnabled(dependencies = {}, callerSignal) {
22
24
  const env = dependencies.env ?? process.env;
23
- const endpoint = env.HQ_FLAGS_API_URL?.trim() || "";
24
- const companyUid = env.HQ_COMPANY_UID?.trim() || "";
25
- if (!endpoint || !/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
25
+ const endpoint = resolveFlagRegistryEndpoint(undefined, env);
26
+ const { companyUid, companySlug } = resolveProcessFlagCompanyContext(env);
27
+ if (!companyUid) {
26
28
  return HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_FALLBACK;
27
29
  }
28
- const companySlug = env.HQ_COMPANY_SLUG?.trim() || "";
29
30
  const deadline = AbortSignal.timeout(HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_TIMEOUT_MS);
30
31
  const signal = callerSignal ? AbortSignal.any([callerSignal, deadline]) : deadline;
31
32
  const doFetch = dependencies.fetch ?? globalThis.fetch;
@@ -1,5 +1,6 @@
1
1
  import { loadCachedTokens } from "@indigoai-us/hq-cloud";
2
2
  import { createFlagClient, } from "@indigoai-us/hq-flags-client";
3
+ import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
3
4
  export const CLIENT_HEALTH_REASON_CODES_FLAG = "client-health-sync-reason-codes";
4
5
  export const CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK = false;
5
6
  export const CLIENT_HEALTH_REASON_CODES_FLAG_TIMEOUT_MS = 150;
@@ -32,9 +33,9 @@ export async function clientHealthReasonCodesEnabled(dependencies = {}, callerSi
32
33
  /** Resolve the same default-off gate for one explicitly identified company. */
33
34
  export async function clientHealthReasonCodesEnabledForCompany(company, dependencies = {}, callerSignal) {
34
35
  const env = dependencies.env ?? process.env;
35
- const endpoint = env.HQ_FLAGS_API_URL?.trim() || "";
36
+ const endpoint = resolveFlagRegistryEndpoint(undefined, env);
36
37
  const companyUid = company.companyUid.trim();
37
- if (!endpoint || !/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
38
+ if (!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
38
39
  return CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK;
39
40
  }
40
41
  const companySlug = company.companySlug?.trim() || "";