@indigoai-us/hq-cli 5.292.0 → 5.294.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +5 -1
  3. package/dist/command-catalog.generated.d.ts +45 -24
  4. package/dist/command-catalog.generated.js +57 -30
  5. package/dist/commands/core-command-catalog.js +3 -2
  6. package/dist/commands/core-delegate-w1b.d.ts +2 -0
  7. package/dist/commands/core-delegate-w1b.js +28 -2
  8. package/dist/commands/core-delegate.d.ts +3 -1
  9. package/dist/commands/core-delegate.js +35 -5
  10. package/dist/commands/core-handoff-fast.d.ts +18 -0
  11. package/dist/commands/core-handoff-fast.js +103 -0
  12. package/dist/commands/core-handoff-open-steps.d.ts +21 -0
  13. package/dist/commands/core-handoff-open-steps.js +264 -0
  14. package/dist/commands/core-handoff.d.ts +4 -2
  15. package/dist/commands/core-handoff.js +22 -249
  16. package/dist/commands/core-jobs-native.d.ts +2 -0
  17. package/dist/commands/core-jobs-native.js +22 -11
  18. package/dist/commands/core-knowledge-prefs.d.ts +2 -0
  19. package/dist/commands/core-knowledge-prefs.js +13 -1
  20. package/dist/commands/core-ontology-native.d.ts +2 -0
  21. package/dist/commands/core-ontology-native.js +22 -2
  22. package/dist/commands/core-policy.d.ts +2 -0
  23. package/dist/commands/core-policy.js +18 -1
  24. package/dist/commands/core.js +2 -1
  25. package/dist/commands/daemon.d.ts +2 -1
  26. package/dist/commands/daemon.js +22 -7
  27. package/dist/commands/lanes-autopilot.d.ts +11 -0
  28. package/dist/commands/lanes-autopilot.js +336 -0
  29. package/dist/commands/lanes-move.js +54 -1
  30. package/dist/commands/lanes-questions.js +12 -5
  31. package/dist/commands/lanes-shared.d.ts +17 -0
  32. package/dist/commands/lanes-stories.d.ts +14 -0
  33. package/dist/commands/lanes-stories.js +308 -0
  34. package/dist/commands/lanes.d.ts +2 -0
  35. package/dist/commands/lanes.js +158 -314
  36. package/dist/fast-core.js +53 -25
  37. package/dist/fast-scaffold.d.ts +15 -2
  38. package/dist/fast-scaffold.js +167 -7
  39. package/dist/lib/core-utils/sentry-report.js +2 -2
  40. package/dist/lib/core-utils/worktree-resume.d.ts +2 -0
  41. package/dist/lib/core-utils/worktree-resume.js +16 -0
  42. package/dist/lib/daemon/migrate.d.ts +8 -1
  43. package/dist/lib/daemon/migrate.js +19 -7
  44. package/dist/lib/daemon/run.d.ts +11 -0
  45. package/dist/lib/daemon/run.js +17 -1
  46. package/dist/lib/lanes/autopilot-metrics.d.ts +99 -0
  47. package/dist/lib/lanes/autopilot-metrics.js +301 -0
  48. package/dist/lib/lanes/autopilot.d.ts +13 -1
  49. package/dist/lib/lanes/autopilot.js +271 -28
  50. package/dist/lib/lanes/grants.d.ts +1 -1
  51. package/dist/lib/lanes/grants.js +7 -5
  52. package/dist/lib/lanes/index.d.ts +1 -1
  53. package/dist/lib/lanes/keep-alive.d.ts +5 -0
  54. package/dist/lib/lanes/keep-alive.js +9 -4
  55. package/dist/lib/lanes/questions.d.ts +2 -1
  56. package/dist/lib/lanes/questions.js +2 -0
  57. package/dist/lib/lanes/reconcile.js +186 -9
  58. package/dist/lib/lanes/spawn.js +4 -0
  59. package/dist/lib/lanes/store.d.ts +2 -0
  60. package/dist/lib/lanes/store.js +53 -4
  61. package/dist/lib/lanes/types.d.ts +13 -1
  62. package/dist/lib/lanes/types.js +2 -2
  63. package/dist/lib/lanes/watcher.js +3 -1
  64. package/dist/native-fast-route.d.ts +3 -1
  65. package/dist/native-fast-route.js +10 -8
  66. package/dist/utils/contribution-table.d.ts +1 -1
  67. package/dist/utils/fast-stream-errors.d.ts +12 -0
  68. package/dist/utils/fast-stream-errors.js +18 -0
  69. package/dist/utils/scaffold-fast-import-guard.d.ts +2 -2
  70. package/dist/utils/scaffold-fast-import-guard.js +6 -3
  71. package/dist/utils/self-update.d.ts +7 -0
  72. package/dist/utils/self-update.js +5 -1
  73. package/dist/utils/version-gate.d.ts +15 -0
  74. package/dist/utils/version-gate.js +46 -2
  75. package/package.json +1 -1
@@ -17,11 +17,13 @@ import { createLaneMoveHandlers } from "./lanes-move.js";
17
17
  import { createLaneQuestionsHandlers } from "./lanes-questions.js";
18
18
  import { createLaneResumeHandlers } from "./lanes-resume.js";
19
19
  import { createLaneWatchHandlers } from "./lanes-watch.js";
20
+ import { createLaneStoryHandlers } from "./lanes-stories.js";
20
21
  import { callerMatchesSessionSenior, assertPlacementEnabled, defaultLanePlacementEnabled, parsePlacementPolicy, processLanePlacementCompany, readClaudeAccountBalanceSignalsEnabled, readLanePlacementEnabled, } from "./lanes-shared.js";
21
22
  export { CLAUDE_ACCOUNT_BALANCE_SIGNALS_FLAG, LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
22
23
  export { resolveAnswerChoice } from "./lanes-questions.js";
23
24
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
24
25
  import { registerLaneKpisCommand } from "./lanes-kpis.js";
26
+ import { registerLaneAutopilotReportCommand } from "./lanes-autopilot.js";
25
27
  import { registerLaneChecksCommand } from "./lanes-checks.js";
26
28
  import { registerLaneReportCommands } from "./lanes-report.js";
27
29
  import { registerLaneRepoMergeHoldCommand } from "./lanes-hold.js";
@@ -55,7 +57,7 @@ import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
55
57
  import { listLaneReviews } from "../lib/lanes/reviews.js";
56
58
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
57
59
  import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
58
- import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
60
+ import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
59
61
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
60
62
  import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
61
63
  import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
@@ -490,6 +492,67 @@ function mergeOwnerLaneHierarchy(supplied, ownerLanes) {
490
492
  byId.set(lane.lane_id, lane);
491
493
  return [...byId.values()];
492
494
  }
495
+ function hierarchyWithRemoteSenior(hierarchy, remoteSenior) {
496
+ return [
497
+ ...hierarchy.filter((lane) => lane.lane_id !== remoteSenior.lane_id),
498
+ remoteSenior,
499
+ ];
500
+ }
501
+ async function readRemoteHireGrantSenior(ctx, hqRoot, seniorLaneId) {
502
+ if (!listLaneIds(hqRoot).includes(seniorLaneId))
503
+ return undefined;
504
+ const mirror = readLane(hqRoot, seniorLaneId);
505
+ if (mirror.host.kind !== "outpost" ||
506
+ laneRunsHere(mirror, reconcileHostFromCtx(ctx))) {
507
+ return undefined;
508
+ }
509
+ const outpostId = mirror.host.id;
510
+ const unavailable = (reason) => new LanesError("hire_grant_senior_unavailable", `Cannot verify senior lane ${seniorLaneId} from owning Outpost ${outpostId}; refusing hire-grant admission (${reason})`, { senior_lane_id: seniorLaneId, outpost_id: outpostId, reason });
511
+ let ownerRecords;
512
+ try {
513
+ ownerRecords = await readOutpostLaneRecords(ctx, outpostId, {
514
+ laneIds: [seniorLaneId],
515
+ snapshot: true,
516
+ strictSnapshot: true,
517
+ }, {}, { bypassCache: true });
518
+ }
519
+ catch (error) {
520
+ throw unavailable(isLanesError(error) ? error.error : "outpost_lane_list_unavailable");
521
+ }
522
+ const matches = ownerRecords.filter((lane) => lane.lane_id === seniorLaneId);
523
+ const owner = matches[0];
524
+ const ownerIsHostedByOutpost = owner !== undefined && ((owner.host.kind === "outpost" && owner.host.id === outpostId) ||
525
+ (owner.host.kind === "local" && owner.host.id.trim() !== ""));
526
+ if (matches.length !== 1 || owner === undefined || !ownerIsHostedByOutpost) {
527
+ throw unavailable(matches.length === 0
528
+ ? "owner_did_not_return_senior"
529
+ : matches.length > 1
530
+ ? "owner_returned_duplicate_seniors"
531
+ : "owner_senior_identity_mismatch");
532
+ }
533
+ try {
534
+ const mirrorCompany = resolveLaneCompany(hqRoot, mirror).company;
535
+ const ownerCompany = resolveLaneCompany(hqRoot, owner).company;
536
+ if ((ownerCompany?.uid ?? null) !== (mirrorCompany?.uid ?? null) ||
537
+ (ownerCompany?.slug ?? null) !== (mirrorCompany?.slug ?? null)) {
538
+ throw unavailable("owner_company_did_not_match_mirror");
539
+ }
540
+ }
541
+ catch (error) {
542
+ if (isLanesError(error) && error.error === "hire_grant_senior_unavailable")
543
+ throw error;
544
+ throw unavailable("owner_company_identity_unavailable");
545
+ }
546
+ return owner;
547
+ }
548
+ async function hierarchyWithVerifiedRemoteSenior(ctx, hqRoot, lane, hierarchy) {
549
+ if (lane.senior.kind !== "lane" || !lane.senior.id)
550
+ return [...hierarchy];
551
+ const remoteSenior = await readRemoteHireGrantSenior(ctx, hqRoot, lane.senior.id);
552
+ return remoteSenior === undefined
553
+ ? [...hierarchy]
554
+ : mergeOwnerLaneRecords(hierarchy, [remoteSenior]);
555
+ }
493
556
  function mergeOwnerLaneRecords(supplied, ownerLanes) {
494
557
  const byId = new Map(supplied.map((lane) => [lane.lane_id, lane]));
495
558
  for (const lane of ownerLanes)
@@ -911,8 +974,18 @@ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
911
974
  throw new LanesError("hire_grant_context_required", `Resume of Outpost lane ${lane.lane_id} requires the primary senior's hire-grant context`, { lane_id: lane.lane_id, senior_lane_id: lane.senior.id });
912
975
  }
913
976
  await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot), new Set([lane.senior.id]));
977
+ const remoteSenior = grantContext === undefined
978
+ ? await readRemoteHireGrantSenior(ctx, hqRoot, lane.senior.id)
979
+ : undefined;
980
+ const suppliedHierarchy = grantContext?.hierarchy ?? (remoteSenior === undefined
981
+ ? undefined
982
+ : hierarchyWithRemoteSenior(loadAllLanes(hqRoot), remoteSenior));
914
983
  return withHireGrantAdmission(hqRoot, lane.senior.id, (snapshot) => {
915
984
  const current = readLane(hqRoot, lane.lane_id);
985
+ if (remoteSenior !== undefined &&
986
+ (current.senior.kind !== "lane" || current.senior.id !== remoteSenior.lane_id)) {
987
+ throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed while its owning Outpost was being checked`, { lane_id: current.lane_id, expected_senior_lane_id: remoteSenior.lane_id, current_senior: current.senior });
988
+ }
916
989
  if (grantContext !== undefined &&
917
990
  !sameHireGrantSenior(current.senior, grantContext.expected_senior)) {
918
991
  throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed before the Outpost resumed it`, {
@@ -929,7 +1002,7 @@ async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
929
1002
  if (refusal !== null)
930
1003
  throw refusal;
931
1004
  return fn();
932
- }, grantContext?.hierarchy);
1005
+ }, suppliedHierarchy, remoteSenior === undefined ? undefined : new Set([remoteSenior.lane_id]));
933
1006
  }
934
1007
  const CAPACITY_PROBE_TIMEOUT_MS = 15_000;
935
1008
  const MAX_PLACEMENT_OUTPOSTS = 16;
@@ -1205,8 +1278,11 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOpt
1205
1278
  }
1206
1279
  return lanes;
1207
1280
  }
1208
- async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}, dispatchOptions = {}) {
1281
+ async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}, dispatchOptions = {}, options = {}) {
1209
1282
  const query = normalizedLaneListQuery(requestedQuery);
1283
+ if (options.bypassCache === true) {
1284
+ return readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOptions);
1285
+ }
1210
1286
  let invocationMemo = outpostLaneListMemo.get(ctx);
1211
1287
  if (!invocationMemo) {
1212
1288
  invocationMemo = new OutpostSnapshotInvocationMemo();
@@ -2144,6 +2220,10 @@ function assertKnownOutpost(requested, known) {
2144
2220
  */
2145
2221
  export const ASK_BACKOFF_MS = [5_000, 15_000];
2146
2222
  export const ASK_ATTEMPTS = ASK_BACKOFF_MS.length + 1;
2223
+ /** Flip to true in the next release to make --priority mandatory. */
2224
+ export const ASK_PRIORITY_REQUIRED = false;
2225
+ const ASK_PRIORITY_WARNING = "--priority will be required in the next release.";
2226
+ const QUESTION_PRIORITIES = ["P0", "P1", "P2"];
2147
2227
  function withCasRetry(fn, attempts = 5) {
2148
2228
  let last;
2149
2229
  for (let i = 0; i < attempts; i += 1) {
@@ -3980,19 +4060,21 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
3980
4060
  let grantContext;
3981
4061
  let grantHierarchy;
3982
4062
  if (action === "grant") {
3983
- const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
4063
+ const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
3984
4064
  hierarchy: [lane.lane_id],
3985
4065
  snapshot: true,
3986
4066
  }));
4067
+ const hierarchy = await hierarchyWithVerifiedRemoteSenior(ctx, ctx.hqRoot(), lane, ownerHierarchy);
3987
4068
  grantHierarchy = hierarchy;
3988
4069
  grantContext = createOutpostHireGrantContext(lane, hierarchy);
3989
4070
  }
3990
4071
  else if ((action === "resume" && lane.senior.kind === "lane") ||
3991
4072
  (action === "message" && (lane.senior.kind === "lane" || lane.senior.kind === "session"))) {
3992
- const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
4073
+ const ownerHierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
3993
4074
  hierarchy: [lane.lane_id],
3994
4075
  snapshot: true,
3995
4076
  }));
4077
+ const hierarchy = await hierarchyWithVerifiedRemoteSenior(ctx, ctx.hqRoot(), lane, ownerHierarchy);
3996
4078
  grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined, action === "message" ? options.supersededMessageSessionId : undefined);
3997
4079
  }
3998
4080
  let dispatched;
@@ -5215,6 +5297,13 @@ function appendCallerSeniorReason(message, authorization) {
5215
5297
  ? message
5216
5298
  : `${message}; authorization refused: ${callerSeniorReason(authorization)}`;
5217
5299
  }
5300
+ function assertQuestionAnswerAuthorized(ctx, lane) {
5301
+ const authorization = callerSeniorAuthorization(ctx, lane.senior);
5302
+ if (authorization.authorized)
5303
+ return;
5304
+ const reason = callerSeniorReason(authorization);
5305
+ throw new LanesError("question_answer_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(lane.senior)} may answer lane ${lane.lane_id}'s Outpost question`, authorization), { lane_id: lane.lane_id, senior: lane.senior, reason });
5306
+ }
5218
5307
  function laneGrantTargetHostedHere(ctx, lane) {
5219
5308
  if (lane.host.kind === "local") {
5220
5309
  return laneRunsHere(lane, reconcileHostFromCtx(ctx));
@@ -5254,6 +5343,10 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
5254
5343
  }
5255
5344
  await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot), parentLaneIds);
5256
5345
  }
5346
+ const remoteSenior = authorization === "senior" &&
5347
+ initial.senior.kind === "lane" && initial.senior.id
5348
+ ? await readRemoteHireGrantSenior(ctx, hqRoot, initial.senior.id)
5349
+ : undefined;
5257
5350
  const updated = ctx.withGrantHireLocks(hqRoot, lockedLaneIds, () => {
5258
5351
  const current = readLane(hqRoot, laneId);
5259
5352
  const currentCallerAuthorization = authorization === "senior"
@@ -5270,11 +5363,18 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
5270
5363
  (!grantContext || !sameHireGrantSenior(current.senior, grantContext.expected_senior))) {
5271
5364
  throw new LanesError("hire_grant_senior_changed", `Lane ${laneId}'s senior changed before the Outpost applied the grant update`, { lane_id: laneId, current_senior: current.senior });
5272
5365
  }
5273
- const hierarchy = grantContext
5366
+ if (remoteSenior !== undefined &&
5367
+ (current.senior.kind !== "lane" || current.senior.id !== remoteSenior.lane_id)) {
5368
+ throw new LanesError("hire_grant_senior_changed", `Lane ${current.lane_id}'s senior changed while its owning Outpost was being checked`, { lane_id: current.lane_id, expected_senior_lane_id: remoteSenior.lane_id, current_senior: current.senior });
5369
+ }
5370
+ const currentHierarchy = grantContext
5274
5371
  ? authorization === "ssm-owner-dispatch"
5275
5372
  ? mergeOwnerLaneHierarchy(grantContext.hierarchy, loadAllLanes(hqRoot))
5276
5373
  : grantContext.hierarchy
5277
5374
  : loadAllLanes(hqRoot);
5375
+ const hierarchy = remoteSenior === undefined
5376
+ ? currentHierarchy
5377
+ : hierarchyWithRemoteSenior(currentHierarchy, remoteSenior);
5278
5378
  if (current.senior.kind === "lane") {
5279
5379
  if (!current.senior.id) {
5280
5380
  throw new LanesError("hire_grant_senior_invalid", `Lane ${laneId} has a lane senior without an id`, {
@@ -6356,84 +6456,6 @@ function laneWithRecoveredProviderSession(hqRoot, lane) {
6356
6456
  : {}),
6357
6457
  };
6358
6458
  }
6359
- function storyCommandTarget(lane, storyId, project) {
6360
- const projectId = project?.trim() || lane.project_id;
6361
- const normalizedStoryId = storyId.trim();
6362
- if (!projectId) {
6363
- throw new LanesError("missing_project", "--project is required", {});
6364
- }
6365
- if (!normalizedStoryId) {
6366
- throw new LanesError("missing_story", "<story-id> is required", {});
6367
- }
6368
- const story = laneStory(lane, projectId, normalizedStoryId);
6369
- if (!story) {
6370
- throw new LanesError("story_not_on_lane", `Story ${projectId}/${normalizedStoryId} is not carried by lane ${lane.lane_id}`, { lane_id: lane.lane_id, project_id: projectId, story_id: normalizedStoryId });
6371
- }
6372
- return { projectId, storyId: normalizedStoryId, story };
6373
- }
6374
- function updateCarriedStory(lane, projectId, storyId, status, at, extras = {}) {
6375
- const stories = laneStories(lane).map((story) => story.project_id === projectId && story.story_id === storyId
6376
- ? {
6377
- ...story,
6378
- status,
6379
- updated_at: at,
6380
- blocked_reason: extras.blocked_reason,
6381
- verified_at: extras.verified_at,
6382
- verified_by: extras.verified_by,
6383
- }
6384
- : story);
6385
- return {
6386
- ...lane,
6387
- project_id: projectId,
6388
- story_id: storyId,
6389
- stories,
6390
- };
6391
- }
6392
- function addCarriedStory(lane, projectId, storyId, at) {
6393
- if (laneStory(lane, projectId, storyId)) {
6394
- throw new LanesError("story_already_on_lane", `Story ${projectId}/${storyId} is already carried by lane ${lane.lane_id}`, {
6395
- lane_id: lane.lane_id,
6396
- project_id: projectId,
6397
- story_id: storyId,
6398
- });
6399
- }
6400
- return {
6401
- ...lane,
6402
- ...(lane.story_id.trim() ? {} : { project_id: projectId, story_id: storyId }),
6403
- stories: [
6404
- ...laneStories(lane),
6405
- { project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
6406
- ],
6407
- };
6408
- }
6409
- function callerMayVerifyStory(ctx, lane) {
6410
- const callerLaneId = ctx.env.HQ_LANE_ID?.trim();
6411
- if (callerLaneId) {
6412
- return lane.senior.kind === "lane" && lane.senior.id === callerLaneId;
6413
- }
6414
- if (lane.senior.kind === "user")
6415
- return true;
6416
- if (lane.senior.kind === "lane")
6417
- return false;
6418
- return sessionIdFromEnv(ctx.env) === lane.senior.id;
6419
- }
6420
- async function ensureStoryThread(ctx, lane, projectId, storyId = lane.story_id) {
6421
- const company = lane.mesh_company ?? lane.company;
6422
- if (!company)
6423
- return lane;
6424
- return ensureLaneThreadRecord(ctx, lane, projectId, storyId);
6425
- }
6426
- async function ensureStoryThreadForTransition(ctx, lane, projectId, storyId) {
6427
- try {
6428
- return { lane: await ensureStoryThread(ctx, lane, projectId, storyId) };
6429
- }
6430
- catch (err) {
6431
- const warning = storyNotOnBoardWarning(err);
6432
- if (warning === undefined)
6433
- throw err;
6434
- return { lane, warning };
6435
- }
6436
- }
6437
6459
  async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, storyId = lane.story_id, laneStatus = lane.state) {
6438
6460
  const company = lane.mesh_company ?? lane.company;
6439
6461
  if (!company)
@@ -6455,228 +6477,6 @@ async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, st
6455
6477
  }
6456
6478
  return updated;
6457
6479
  }
6458
- async function emitStoryTransition(ctx, lane, projectId, storyId, event, status, payload = {}, skipStoryStatusPatch = false) {
6459
- const company = lane.mesh_company ?? lane.company;
6460
- if (!company)
6461
- return;
6462
- const ready = await ensureStoryThread(ctx, lane, projectId, storyId);
6463
- if (!ready.thread_id)
6464
- return;
6465
- await emitLaneMeshEvent(ctx, company, ready.thread_id, event, {
6466
- lane_id: ready.lane_id,
6467
- project_id: projectId,
6468
- story_id: storyId,
6469
- ...payload,
6470
- lane_status: lane.state,
6471
- });
6472
- if (!skipStoryStatusPatch && projectId?.trim() && storyId?.trim()) {
6473
- await ctx.mesh.patchStoryStatus(company, projectId, storyId, status === "blocked" ? "in_progress" : status);
6474
- }
6475
- }
6476
- function sameCommittedStory(current, expected, projectId, storyId) {
6477
- const currentStory = laneStory(current, projectId, storyId);
6478
- const expectedStory = laneStory(expected, projectId, storyId);
6479
- return (current.project_id === expected.project_id &&
6480
- current.story_id === expected.story_id &&
6481
- JSON.stringify(currentStory) === JSON.stringify(expectedStory));
6482
- }
6483
- async function commitStoryTransition(ctx, before, next, projectId, storyId, event, status, payload = {}, skipStoryStatusPatch = false) {
6484
- try {
6485
- await emitStoryTransition(ctx, next, projectId, storyId, event, status, payload, skipStoryStatusPatch);
6486
- return undefined;
6487
- }
6488
- catch (err) {
6489
- const warning = storyNotOnBoardWarning(err);
6490
- if (warning !== undefined)
6491
- return warning;
6492
- try {
6493
- const current = readLane(ctx.hqRoot(), next.lane_id);
6494
- if (!sameCommittedStory(current, next, projectId, storyId)) {
6495
- throw new LanesError("story_transition_rollback_conflict", `Story ${projectId}/${storyId} changed while mesh publication failed`, { lane_id: next.lane_id, project_id: projectId, story_id: storyId });
6496
- }
6497
- updateLane(ctx.hqRoot(), next.lane_id, current.version, "command", (latest) => {
6498
- if (!sameCommittedStory(latest, next, projectId, storyId)) {
6499
- throw new LanesError("story_transition_rollback_conflict", `Story ${projectId}/${storyId} changed before rollback`, { lane_id: next.lane_id, project_id: projectId, story_id: storyId });
6500
- }
6501
- return {
6502
- ...latest,
6503
- project_id: before.project_id,
6504
- story_id: before.story_id,
6505
- stories: before.stories,
6506
- };
6507
- });
6508
- }
6509
- catch (rollbackErr) {
6510
- process.stderr.write(`hq lanes: story transition rollback failed for ${next.lane_id}/${projectId}/${storyId}: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}\n`);
6511
- }
6512
- throw err;
6513
- }
6514
- }
6515
- async function runStoryList(ctx, laneId) {
6516
- const lane = readLane(ctx.hqRoot(), laneId);
6517
- printJson({
6518
- ok: true,
6519
- action: "story.list",
6520
- lane_id: laneId,
6521
- stories: laneStories(lane),
6522
- active_stories: activeLaneStories(lane),
6523
- story_warning: storyWarning(lane) ?? null,
6524
- });
6525
- }
6526
- async function runStoryAdd(ctx, laneId, storyId, opts) {
6527
- const hqRoot = ctx.hqRoot();
6528
- let lane = readLane(hqRoot, laneId);
6529
- assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
6530
- const projectId = opts.project?.trim() || lane.project_id;
6531
- if (!projectId)
6532
- throw new LanesError("missing_project", "--project is required", {});
6533
- const normalizedStoryId = storyId.trim();
6534
- if (!normalizedStoryId)
6535
- throw new LanesError("missing_story", "<story-id> is required", {});
6536
- if (ctx.storyLedgerEnabled()) {
6537
- validateProjectStories(hqRoot, projectId, [normalizedStoryId], lane.company?.slug);
6538
- }
6539
- const company = lane.mesh_company ?? lane.company;
6540
- const warnings = [];
6541
- let storyKnownAbsent = false;
6542
- if (company) {
6543
- const boardCheck = await storyBoardCheck(ctx, company, projectId, [normalizedStoryId]);
6544
- if (boardCheck.warning !== undefined)
6545
- warnings.push(boardCheck.warning);
6546
- storyKnownAbsent = boardCheck.absentStoryIds.has(normalizedStoryId);
6547
- }
6548
- const at = ctx.now().toISOString();
6549
- const thread = await ensureStoryThreadForTransition(ctx, lane, projectId, normalizedStoryId);
6550
- lane = thread.lane;
6551
- if (thread.warning !== undefined)
6552
- warnings.push(thread.warning);
6553
- const before = lane;
6554
- const next = casUpdateLane(hqRoot, laneId, (current) => {
6555
- assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
6556
- const added = addCarriedStory(current, projectId, normalizedStoryId, at);
6557
- return {
6558
- ...added,
6559
- no_story_reason: undefined,
6560
- };
6561
- });
6562
- const transitionWarning = await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress", {}, storyKnownAbsent);
6563
- if (transitionWarning !== undefined)
6564
- warnings.push(transitionWarning);
6565
- const finalLane = readLane(hqRoot, laneId);
6566
- printJson({
6567
- ok: true,
6568
- action: "story.add",
6569
- lane_id: laneId,
6570
- project_id: projectId,
6571
- story_id: normalizedStoryId,
6572
- status: "in_progress",
6573
- story_warning: combinedStoryWarning(finalLane, warnings),
6574
- lane: finalLane,
6575
- });
6576
- }
6577
- async function runStoryStart(ctx, laneId, storyId, opts) {
6578
- const hqRoot = ctx.hqRoot();
6579
- let lane = readLane(hqRoot, laneId);
6580
- assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
6581
- const target = storyCommandTarget(lane, storyId, opts.project);
6582
- if (target.story.status === "done") {
6583
- throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be started again`, {
6584
- lane_id: laneId,
6585
- project_id: target.projectId,
6586
- story_id: target.storyId,
6587
- });
6588
- }
6589
- const at = ctx.now().toISOString();
6590
- const warnings = [];
6591
- const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
6592
- lane = thread.lane;
6593
- if (thread.warning !== undefined)
6594
- warnings.push(thread.warning);
6595
- const before = lane;
6596
- const next = casUpdateLane(hqRoot, laneId, (current) => {
6597
- assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
6598
- return updateCarriedStory(current, target.projectId, target.storyId, "in_progress", at);
6599
- });
6600
- const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_started", "in_progress");
6601
- if (transitionWarning !== undefined)
6602
- warnings.push(transitionWarning);
6603
- const finalLane = readLane(hqRoot, laneId);
6604
- printJson({ ok: true, action: "story.start", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "in_progress", story_warning: combinedStoryWarning(finalLane, warnings), lane: finalLane });
6605
- }
6606
- async function runStoryBlock(ctx, laneId, storyId, opts) {
6607
- const hqRoot = ctx.hqRoot();
6608
- let lane = readLane(hqRoot, laneId);
6609
- assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
6610
- const target = storyCommandTarget(lane, storyId, opts.project);
6611
- if (target.story.status === "done") {
6612
- throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be blocked again`, {
6613
- lane_id: laneId,
6614
- project_id: target.projectId,
6615
- story_id: target.storyId,
6616
- });
6617
- }
6618
- const reason = opts.reason?.trim();
6619
- if (!reason)
6620
- throw new LanesError("missing_block_reason", "--reason is required", {});
6621
- const at = ctx.now().toISOString();
6622
- const warnings = [];
6623
- const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
6624
- lane = thread.lane;
6625
- if (thread.warning !== undefined)
6626
- warnings.push(thread.warning);
6627
- const before = lane;
6628
- const next = casUpdateLane(hqRoot, laneId, (current) => {
6629
- assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
6630
- return updateCarriedStory(current, target.projectId, target.storyId, "blocked", at, { blocked_reason: reason });
6631
- });
6632
- const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_blocked", "blocked", { blocked_reason: reason });
6633
- if (transitionWarning !== undefined)
6634
- warnings.push(transitionWarning);
6635
- const finalLane = readLane(hqRoot, laneId);
6636
- printJson({ ok: true, action: "story.block", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "blocked", blocked_reason: reason, story_warning: combinedStoryWarning(finalLane, warnings), lane: finalLane });
6637
- }
6638
- async function runStoryDone(ctx, laneId, storyId, opts) {
6639
- const hqRoot = ctx.hqRoot();
6640
- let lane = readLane(hqRoot, laneId);
6641
- assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
6642
- if (!opts.verified) {
6643
- throw new LanesError("senior_verification_required", "Only a senior may mark a story done; pass --verified after reviewing or merging it", { lane_id: laneId });
6644
- }
6645
- if (!callerMayVerifyStory(ctx, lane)) {
6646
- throw new LanesError("senior_verification_required", `Caller is not the senior for lane ${laneId}`, {
6647
- lane_id: laneId,
6648
- senior: lane.senior,
6649
- });
6650
- }
6651
- const target = storyCommandTarget(lane, storyId, opts.project);
6652
- if (target.story.status !== "review") {
6653
- throw new LanesError("story_not_ready_for_verification", `Story ${target.projectId}/${target.storyId} must be in review before senior verification`, {
6654
- lane_id: laneId,
6655
- project_id: target.projectId,
6656
- story_id: target.storyId,
6657
- status: target.story.status,
6658
- });
6659
- }
6660
- const at = ctx.now().toISOString();
6661
- const warnings = [];
6662
- const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
6663
- lane = thread.lane;
6664
- if (thread.warning !== undefined)
6665
- warnings.push(thread.warning);
6666
- const before = lane;
6667
- const next = casUpdateLane(hqRoot, laneId, (current) => {
6668
- assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
6669
- return updateCarriedStory(current, target.projectId, target.storyId, "done", at, {
6670
- verified_at: at,
6671
- verified_by: current.senior,
6672
- });
6673
- });
6674
- const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_done", "done", { summary: "Senior verification complete" });
6675
- if (transitionWarning !== undefined)
6676
- warnings.push(transitionWarning);
6677
- const finalLane = readLane(hqRoot, laneId);
6678
- printJson({ ok: true, action: "story.done", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "done", verified: true, story_warning: combinedStoryWarning(finalLane, warnings), lane: finalLane });
6679
- }
6680
6480
  /**
6681
6481
  * Registration state of the drain hook in the lane's account home, read the
6682
6482
  * way the provider reads it. Null when the lane has no bound account yet
@@ -7443,7 +7243,23 @@ export function interruptNotResumableWarning(laneId) {
7443
7243
  return `hq lanes: warning: lane ${laneId} was interrupted before its provider reported a session id, ` +
7444
7244
  `so hq lanes resume cannot continue it. Start a new lane for the remaining work.\n`;
7445
7245
  }
7246
+ function parseQuestionPriority(value) {
7247
+ if (value === undefined) {
7248
+ if (ASK_PRIORITY_REQUIRED) {
7249
+ throw new LanesError("ask_priority_required", "--priority is required; choose P0, P1, or P2", {
7250
+ allowed: [...QUESTION_PRIORITIES],
7251
+ });
7252
+ }
7253
+ return undefined;
7254
+ }
7255
+ const normalized = value.toUpperCase();
7256
+ if (!QUESTION_PRIORITIES.includes(normalized)) {
7257
+ throw new LanesError("ask_priority_invalid", `Invalid --priority ${JSON.stringify(value)}. Allowed values: P0, P1, P2`, { allowed: [...QUESTION_PRIORITIES], value });
7258
+ }
7259
+ return normalized;
7260
+ }
7446
7261
  async function runAsk(ctx, opts) {
7262
+ const priority = parseQuestionPriority(opts.priority);
7447
7263
  const hqRoot = ctx.hqRoot();
7448
7264
  const laneId = ctx.env.HQ_LANE_ID?.trim();
7449
7265
  if (!laneId) {
@@ -7482,6 +7298,7 @@ async function runAsk(ctx, opts) {
7482
7298
  at,
7483
7299
  activeCompany: questionCompany,
7484
7300
  questionId,
7301
+ priority,
7485
7302
  });
7486
7303
  questionId = withBody.question_id;
7487
7304
  await emitLaneMeshEvent(ctx, lane.mesh_company ?? lane.company, lane.thread_id, "lane_question", {
@@ -7496,12 +7313,16 @@ async function runAsk(ctx, opts) {
7496
7313
  question_id: withBody.question_id,
7497
7314
  });
7498
7315
  }
7316
+ if (priority === undefined) {
7317
+ process.stderr.write(`hq lanes ask: warning: ${ASK_PRIORITY_WARNING}\n`);
7318
+ }
7499
7319
  printJson({
7500
7320
  ok: true,
7501
7321
  action: "ask",
7502
7322
  question: withBody,
7503
7323
  lane_id: lane.lane_id,
7504
7324
  lane_state: lane.state,
7325
+ ...(priority === undefined ? { warnings: [ASK_PRIORITY_WARNING] } : {}),
7505
7326
  });
7506
7327
  return;
7507
7328
  }
@@ -7940,6 +7761,19 @@ function attachJsonNoop(cmd) {
7940
7761
  }
7941
7762
  export function registerLanesCommand(parent, deps = {}) {
7942
7763
  const ctx = resolveDeps(deps);
7764
+ const storyHandlers = createLaneStoryHandlers({
7765
+ assertLaneRunsHereForOperatorMutation,
7766
+ casUpdateLane,
7767
+ combinedStoryWarning,
7768
+ emitLaneMeshEvent,
7769
+ ensureLaneThreadRecord,
7770
+ printJson,
7771
+ readLane,
7772
+ storyBoardCheck,
7773
+ storyNotOnBoardWarning,
7774
+ updateLane,
7775
+ validateProjectStories,
7776
+ });
7943
7777
  const accountHandlers = createLaneAccountsHandlers({
7944
7778
  automaticBalanceMoveHistoryReason,
7945
7779
  ensureDir,
@@ -7957,6 +7791,7 @@ export function registerLanesCommand(parent, deps = {}) {
7957
7791
  MAX_PLACEMENT_OUTPOSTS,
7958
7792
  OUTPOST_QUESTION_LOOKUP_CONCURRENCY,
7959
7793
  assertLaneRunsHereForOperatorMutation,
7794
+ assertQuestionAnswerAuthorized,
7960
7795
  assertQuestionCompanyMatchesLane,
7961
7796
  casUpdateQuestion,
7962
7797
  effectiveLaneQuestionCompany,
@@ -8095,6 +7930,11 @@ export function registerLanesCommand(parent, deps = {}) {
8095
7930
  }, wrap);
8096
7931
  registerOutpostPrunerCommand(lanes);
8097
7932
  registerLaneKpisCommand(lanes, { hqRoot: ctx.hqRoot, now: ctx.now, wrap });
7933
+ registerLaneAutopilotReportCommand(lanes, {
7934
+ hqRoot: ctx.hqRoot,
7935
+ now: ctx.now,
7936
+ currentHost: () => resolveAdmissionHost({ hostname: ctx.hostname, readOutpostIdentity: ctx.readOutpostIdentity }),
7937
+ }, wrap);
8098
7938
  const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
8099
7939
  noStoryOption.attributeName = () => "noStory";
8100
7940
  lanes
@@ -8306,7 +8146,7 @@ export function registerLanesCommand(parent, deps = {}) {
8306
8146
  .description("List the stories carried by a lane")
8307
8147
  .argument("<lane-id>", "Lane id")
8308
8148
  .action(wrap(async (laneId) => {
8309
- await runStoryList(ctx, laneId);
8149
+ await storyHandlers.runStoryList(ctx, laneId);
8310
8150
  }));
8311
8151
  stories
8312
8152
  .command("add")
@@ -8315,7 +8155,7 @@ export function registerLanesCommand(parent, deps = {}) {
8315
8155
  .argument("<story-id>", "Story id")
8316
8156
  .option("--project <slug>", "Project id; defaults to the lane's current project")
8317
8157
  .action(wrap(async (laneId, storyId, opts) => {
8318
- await runStoryAdd(ctx, laneId, storyId, opts);
8158
+ await storyHandlers.runStoryAdd(ctx, laneId, storyId, opts);
8319
8159
  }));
8320
8160
  stories
8321
8161
  .command("start")
@@ -8324,7 +8164,7 @@ export function registerLanesCommand(parent, deps = {}) {
8324
8164
  .argument("<story-id>", "Story id")
8325
8165
  .option("--project <slug>", "Project id; defaults to the lane's current project")
8326
8166
  .action(wrap(async (laneId, storyId, opts) => {
8327
- await runStoryStart(ctx, laneId, storyId, opts);
8167
+ await storyHandlers.runStoryStart(ctx, laneId, storyId, opts);
8328
8168
  }));
8329
8169
  stories
8330
8170
  .command("block")
@@ -8334,7 +8174,7 @@ export function registerLanesCommand(parent, deps = {}) {
8334
8174
  .requiredOption("--reason <text>", "Why the story is blocked")
8335
8175
  .option("--project <slug>", "Project id; defaults to the lane's current project")
8336
8176
  .action(wrap(async (laneId, storyId, opts) => {
8337
- await runStoryBlock(ctx, laneId, storyId, opts);
8177
+ await storyHandlers.runStoryBlock(ctx, laneId, storyId, opts);
8338
8178
  }));
8339
8179
  stories
8340
8180
  .command("done")
@@ -8344,7 +8184,7 @@ export function registerLanesCommand(parent, deps = {}) {
8344
8184
  .requiredOption("--verified", "Confirm senior review or merge")
8345
8185
  .option("--project <slug>", "Project id; defaults to the lane's current project")
8346
8186
  .action(wrap(async (laneId, storyId, opts) => {
8347
- await runStoryDone(ctx, laneId, storyId, opts);
8187
+ await storyHandlers.runStoryDone(ctx, laneId, storyId, opts);
8348
8188
  }));
8349
8189
  lanes
8350
8190
  .command("message")
@@ -8493,6 +8333,8 @@ export function registerLanesCommand(parent, deps = {}) {
8493
8333
  .option("--context <text>", "Context for the audience")
8494
8334
  .option("--option <text>", "Answer choice (repeatable)", collectRepeatable)
8495
8335
  .option("--recommended <text>", "Recommended option")
8336
+ .option("--priority <P0|P1|P2>", "Urgency: P0 blocks now, P1 needed today, P2 can wait")
8337
+ .addHelpText("after", "\nThe audience may take up to 24 hours to answer; keep working meanwhile.\n")
8496
8338
  .action(wrap(async (opts) => {
8497
8339
  await runAsk(ctx, opts);
8498
8340
  }));
@@ -8631,9 +8473,11 @@ export function registerLanesCommand(parent, deps = {}) {
8631
8473
  .action(wrap(async (opts) => {
8632
8474
  await runPickup(ctx, opts);
8633
8475
  }));
8634
- lanes
8635
- .command("autopilot")
8636
- .description("Check registered team leads and wake them for ready work")
8476
+ const autopilot = lanes.commands.find((command) => command.name() === "autopilot");
8477
+ if (!autopilot)
8478
+ throw new Error("Lanes autopilot command group was not registered");
8479
+ autopilot
8480
+ .description("Run lead autopilot checks and report follow-through metrics")
8637
8481
  .command("run")
8638
8482
  .description("Run one lead autopilot check pass")
8639
8483
  .option("--dry-run", "Report proposed wakes without writing them")