@indigoai-us/hq-cli 5.293.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 (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +5 -1
  3. package/dist/command-catalog.generated.d.ts +42 -24
  4. package/dist/command-catalog.generated.js +53 -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-questions.js +8 -5
  30. package/dist/commands/lanes-shared.d.ts +17 -0
  31. package/dist/commands/lanes-stories.d.ts +14 -0
  32. package/dist/commands/lanes-stories.js +308 -0
  33. package/dist/commands/lanes.js +39 -309
  34. package/dist/fast-core.js +53 -25
  35. package/dist/fast-scaffold.d.ts +15 -2
  36. package/dist/fast-scaffold.js +167 -7
  37. package/dist/lib/core-utils/worktree-resume.d.ts +2 -0
  38. package/dist/lib/core-utils/worktree-resume.js +16 -0
  39. package/dist/lib/daemon/migrate.d.ts +8 -1
  40. package/dist/lib/daemon/migrate.js +19 -7
  41. package/dist/lib/lanes/autopilot-metrics.d.ts +99 -0
  42. package/dist/lib/lanes/autopilot-metrics.js +301 -0
  43. package/dist/lib/lanes/store.d.ts +2 -0
  44. package/dist/lib/lanes/store.js +32 -3
  45. package/dist/native-fast-route.d.ts +3 -1
  46. package/dist/native-fast-route.js +10 -8
  47. package/dist/utils/contribution-table.d.ts +1 -1
  48. package/dist/utils/fast-stream-errors.d.ts +12 -0
  49. package/dist/utils/fast-stream-errors.js +18 -0
  50. package/dist/utils/scaffold-fast-import-guard.d.ts +2 -2
  51. package/dist/utils/scaffold-fast-import-guard.js +6 -3
  52. 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";
@@ -5295,6 +5297,13 @@ function appendCallerSeniorReason(message, authorization) {
5295
5297
  ? message
5296
5298
  : `${message}; authorization refused: ${callerSeniorReason(authorization)}`;
5297
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
+ }
5298
5307
  function laneGrantTargetHostedHere(ctx, lane) {
5299
5308
  if (lane.host.kind === "local") {
5300
5309
  return laneRunsHere(lane, reconcileHostFromCtx(ctx));
@@ -6447,84 +6456,6 @@ function laneWithRecoveredProviderSession(hqRoot, lane) {
6447
6456
  : {}),
6448
6457
  };
6449
6458
  }
6450
- function storyCommandTarget(lane, storyId, project) {
6451
- const projectId = project?.trim() || lane.project_id;
6452
- const normalizedStoryId = storyId.trim();
6453
- if (!projectId) {
6454
- throw new LanesError("missing_project", "--project is required", {});
6455
- }
6456
- if (!normalizedStoryId) {
6457
- throw new LanesError("missing_story", "<story-id> is required", {});
6458
- }
6459
- const story = laneStory(lane, projectId, normalizedStoryId);
6460
- if (!story) {
6461
- 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 });
6462
- }
6463
- return { projectId, storyId: normalizedStoryId, story };
6464
- }
6465
- function updateCarriedStory(lane, projectId, storyId, status, at, extras = {}) {
6466
- const stories = laneStories(lane).map((story) => story.project_id === projectId && story.story_id === storyId
6467
- ? {
6468
- ...story,
6469
- status,
6470
- updated_at: at,
6471
- blocked_reason: extras.blocked_reason,
6472
- verified_at: extras.verified_at,
6473
- verified_by: extras.verified_by,
6474
- }
6475
- : story);
6476
- return {
6477
- ...lane,
6478
- project_id: projectId,
6479
- story_id: storyId,
6480
- stories,
6481
- };
6482
- }
6483
- function addCarriedStory(lane, projectId, storyId, at) {
6484
- if (laneStory(lane, projectId, storyId)) {
6485
- throw new LanesError("story_already_on_lane", `Story ${projectId}/${storyId} is already carried by lane ${lane.lane_id}`, {
6486
- lane_id: lane.lane_id,
6487
- project_id: projectId,
6488
- story_id: storyId,
6489
- });
6490
- }
6491
- return {
6492
- ...lane,
6493
- ...(lane.story_id.trim() ? {} : { project_id: projectId, story_id: storyId }),
6494
- stories: [
6495
- ...laneStories(lane),
6496
- { project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
6497
- ],
6498
- };
6499
- }
6500
- function callerMayVerifyStory(ctx, lane) {
6501
- const callerLaneId = ctx.env.HQ_LANE_ID?.trim();
6502
- if (callerLaneId) {
6503
- return lane.senior.kind === "lane" && lane.senior.id === callerLaneId;
6504
- }
6505
- if (lane.senior.kind === "user")
6506
- return true;
6507
- if (lane.senior.kind === "lane")
6508
- return false;
6509
- return sessionIdFromEnv(ctx.env) === lane.senior.id;
6510
- }
6511
- async function ensureStoryThread(ctx, lane, projectId, storyId = lane.story_id) {
6512
- const company = lane.mesh_company ?? lane.company;
6513
- if (!company)
6514
- return lane;
6515
- return ensureLaneThreadRecord(ctx, lane, projectId, storyId);
6516
- }
6517
- async function ensureStoryThreadForTransition(ctx, lane, projectId, storyId) {
6518
- try {
6519
- return { lane: await ensureStoryThread(ctx, lane, projectId, storyId) };
6520
- }
6521
- catch (err) {
6522
- const warning = storyNotOnBoardWarning(err);
6523
- if (warning === undefined)
6524
- throw err;
6525
- return { lane, warning };
6526
- }
6527
- }
6528
6459
  async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, storyId = lane.story_id, laneStatus = lane.state) {
6529
6460
  const company = lane.mesh_company ?? lane.company;
6530
6461
  if (!company)
@@ -6546,228 +6477,6 @@ async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, st
6546
6477
  }
6547
6478
  return updated;
6548
6479
  }
6549
- async function emitStoryTransition(ctx, lane, projectId, storyId, event, status, payload = {}, skipStoryStatusPatch = false) {
6550
- const company = lane.mesh_company ?? lane.company;
6551
- if (!company)
6552
- return;
6553
- const ready = await ensureStoryThread(ctx, lane, projectId, storyId);
6554
- if (!ready.thread_id)
6555
- return;
6556
- await emitLaneMeshEvent(ctx, company, ready.thread_id, event, {
6557
- lane_id: ready.lane_id,
6558
- project_id: projectId,
6559
- story_id: storyId,
6560
- ...payload,
6561
- lane_status: lane.state,
6562
- });
6563
- if (!skipStoryStatusPatch && projectId?.trim() && storyId?.trim()) {
6564
- await ctx.mesh.patchStoryStatus(company, projectId, storyId, status === "blocked" ? "in_progress" : status);
6565
- }
6566
- }
6567
- function sameCommittedStory(current, expected, projectId, storyId) {
6568
- const currentStory = laneStory(current, projectId, storyId);
6569
- const expectedStory = laneStory(expected, projectId, storyId);
6570
- return (current.project_id === expected.project_id &&
6571
- current.story_id === expected.story_id &&
6572
- JSON.stringify(currentStory) === JSON.stringify(expectedStory));
6573
- }
6574
- async function commitStoryTransition(ctx, before, next, projectId, storyId, event, status, payload = {}, skipStoryStatusPatch = false) {
6575
- try {
6576
- await emitStoryTransition(ctx, next, projectId, storyId, event, status, payload, skipStoryStatusPatch);
6577
- return undefined;
6578
- }
6579
- catch (err) {
6580
- const warning = storyNotOnBoardWarning(err);
6581
- if (warning !== undefined)
6582
- return warning;
6583
- try {
6584
- const current = readLane(ctx.hqRoot(), next.lane_id);
6585
- if (!sameCommittedStory(current, next, projectId, storyId)) {
6586
- 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 });
6587
- }
6588
- updateLane(ctx.hqRoot(), next.lane_id, current.version, "command", (latest) => {
6589
- if (!sameCommittedStory(latest, next, projectId, storyId)) {
6590
- throw new LanesError("story_transition_rollback_conflict", `Story ${projectId}/${storyId} changed before rollback`, { lane_id: next.lane_id, project_id: projectId, story_id: storyId });
6591
- }
6592
- return {
6593
- ...latest,
6594
- project_id: before.project_id,
6595
- story_id: before.story_id,
6596
- stories: before.stories,
6597
- };
6598
- });
6599
- }
6600
- catch (rollbackErr) {
6601
- process.stderr.write(`hq lanes: story transition rollback failed for ${next.lane_id}/${projectId}/${storyId}: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}\n`);
6602
- }
6603
- throw err;
6604
- }
6605
- }
6606
- async function runStoryList(ctx, laneId) {
6607
- const lane = readLane(ctx.hqRoot(), laneId);
6608
- printJson({
6609
- ok: true,
6610
- action: "story.list",
6611
- lane_id: laneId,
6612
- stories: laneStories(lane),
6613
- active_stories: activeLaneStories(lane),
6614
- story_warning: storyWarning(lane) ?? null,
6615
- });
6616
- }
6617
- async function runStoryAdd(ctx, laneId, storyId, opts) {
6618
- const hqRoot = ctx.hqRoot();
6619
- let lane = readLane(hqRoot, laneId);
6620
- assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
6621
- const projectId = opts.project?.trim() || lane.project_id;
6622
- if (!projectId)
6623
- throw new LanesError("missing_project", "--project is required", {});
6624
- const normalizedStoryId = storyId.trim();
6625
- if (!normalizedStoryId)
6626
- throw new LanesError("missing_story", "<story-id> is required", {});
6627
- if (ctx.storyLedgerEnabled()) {
6628
- validateProjectStories(hqRoot, projectId, [normalizedStoryId], lane.company?.slug);
6629
- }
6630
- const company = lane.mesh_company ?? lane.company;
6631
- const warnings = [];
6632
- let storyKnownAbsent = false;
6633
- if (company) {
6634
- const boardCheck = await storyBoardCheck(ctx, company, projectId, [normalizedStoryId]);
6635
- if (boardCheck.warning !== undefined)
6636
- warnings.push(boardCheck.warning);
6637
- storyKnownAbsent = boardCheck.absentStoryIds.has(normalizedStoryId);
6638
- }
6639
- const at = ctx.now().toISOString();
6640
- const thread = await ensureStoryThreadForTransition(ctx, lane, projectId, normalizedStoryId);
6641
- lane = thread.lane;
6642
- if (thread.warning !== undefined)
6643
- warnings.push(thread.warning);
6644
- const before = lane;
6645
- const next = casUpdateLane(hqRoot, laneId, (current) => {
6646
- assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
6647
- const added = addCarriedStory(current, projectId, normalizedStoryId, at);
6648
- return {
6649
- ...added,
6650
- no_story_reason: undefined,
6651
- };
6652
- });
6653
- const transitionWarning = await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress", {}, storyKnownAbsent);
6654
- if (transitionWarning !== undefined)
6655
- warnings.push(transitionWarning);
6656
- const finalLane = readLane(hqRoot, laneId);
6657
- printJson({
6658
- ok: true,
6659
- action: "story.add",
6660
- lane_id: laneId,
6661
- project_id: projectId,
6662
- story_id: normalizedStoryId,
6663
- status: "in_progress",
6664
- story_warning: combinedStoryWarning(finalLane, warnings),
6665
- lane: finalLane,
6666
- });
6667
- }
6668
- async function runStoryStart(ctx, laneId, storyId, opts) {
6669
- const hqRoot = ctx.hqRoot();
6670
- let lane = readLane(hqRoot, laneId);
6671
- assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
6672
- const target = storyCommandTarget(lane, storyId, opts.project);
6673
- if (target.story.status === "done") {
6674
- throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be started again`, {
6675
- lane_id: laneId,
6676
- project_id: target.projectId,
6677
- story_id: target.storyId,
6678
- });
6679
- }
6680
- const at = ctx.now().toISOString();
6681
- const warnings = [];
6682
- const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
6683
- lane = thread.lane;
6684
- if (thread.warning !== undefined)
6685
- warnings.push(thread.warning);
6686
- const before = lane;
6687
- const next = casUpdateLane(hqRoot, laneId, (current) => {
6688
- assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
6689
- return updateCarriedStory(current, target.projectId, target.storyId, "in_progress", at);
6690
- });
6691
- const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_started", "in_progress");
6692
- if (transitionWarning !== undefined)
6693
- warnings.push(transitionWarning);
6694
- const finalLane = readLane(hqRoot, laneId);
6695
- 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 });
6696
- }
6697
- async function runStoryBlock(ctx, laneId, storyId, opts) {
6698
- const hqRoot = ctx.hqRoot();
6699
- let lane = readLane(hqRoot, laneId);
6700
- assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
6701
- const target = storyCommandTarget(lane, storyId, opts.project);
6702
- if (target.story.status === "done") {
6703
- throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be blocked again`, {
6704
- lane_id: laneId,
6705
- project_id: target.projectId,
6706
- story_id: target.storyId,
6707
- });
6708
- }
6709
- const reason = opts.reason?.trim();
6710
- if (!reason)
6711
- throw new LanesError("missing_block_reason", "--reason is required", {});
6712
- const at = ctx.now().toISOString();
6713
- const warnings = [];
6714
- const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
6715
- lane = thread.lane;
6716
- if (thread.warning !== undefined)
6717
- warnings.push(thread.warning);
6718
- const before = lane;
6719
- const next = casUpdateLane(hqRoot, laneId, (current) => {
6720
- assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
6721
- return updateCarriedStory(current, target.projectId, target.storyId, "blocked", at, { blocked_reason: reason });
6722
- });
6723
- const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_blocked", "blocked", { blocked_reason: reason });
6724
- if (transitionWarning !== undefined)
6725
- warnings.push(transitionWarning);
6726
- const finalLane = readLane(hqRoot, laneId);
6727
- 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 });
6728
- }
6729
- async function runStoryDone(ctx, laneId, storyId, opts) {
6730
- const hqRoot = ctx.hqRoot();
6731
- let lane = readLane(hqRoot, laneId);
6732
- assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
6733
- if (!opts.verified) {
6734
- throw new LanesError("senior_verification_required", "Only a senior may mark a story done; pass --verified after reviewing or merging it", { lane_id: laneId });
6735
- }
6736
- if (!callerMayVerifyStory(ctx, lane)) {
6737
- throw new LanesError("senior_verification_required", `Caller is not the senior for lane ${laneId}`, {
6738
- lane_id: laneId,
6739
- senior: lane.senior,
6740
- });
6741
- }
6742
- const target = storyCommandTarget(lane, storyId, opts.project);
6743
- if (target.story.status !== "review") {
6744
- throw new LanesError("story_not_ready_for_verification", `Story ${target.projectId}/${target.storyId} must be in review before senior verification`, {
6745
- lane_id: laneId,
6746
- project_id: target.projectId,
6747
- story_id: target.storyId,
6748
- status: target.story.status,
6749
- });
6750
- }
6751
- const at = ctx.now().toISOString();
6752
- const warnings = [];
6753
- const thread = await ensureStoryThreadForTransition(ctx, lane, target.projectId, target.storyId);
6754
- lane = thread.lane;
6755
- if (thread.warning !== undefined)
6756
- warnings.push(thread.warning);
6757
- const before = lane;
6758
- const next = casUpdateLane(hqRoot, laneId, (current) => {
6759
- assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
6760
- return updateCarriedStory(current, target.projectId, target.storyId, "done", at, {
6761
- verified_at: at,
6762
- verified_by: current.senior,
6763
- });
6764
- });
6765
- const transitionWarning = await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_done", "done", { summary: "Senior verification complete" });
6766
- if (transitionWarning !== undefined)
6767
- warnings.push(transitionWarning);
6768
- const finalLane = readLane(hqRoot, laneId);
6769
- 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 });
6770
- }
6771
6480
  /**
6772
6481
  * Registration state of the drain hook in the lane's account home, read the
6773
6482
  * way the provider reads it. Null when the lane has no bound account yet
@@ -8052,6 +7761,19 @@ function attachJsonNoop(cmd) {
8052
7761
  }
8053
7762
  export function registerLanesCommand(parent, deps = {}) {
8054
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
+ });
8055
7777
  const accountHandlers = createLaneAccountsHandlers({
8056
7778
  automaticBalanceMoveHistoryReason,
8057
7779
  ensureDir,
@@ -8069,6 +7791,7 @@ export function registerLanesCommand(parent, deps = {}) {
8069
7791
  MAX_PLACEMENT_OUTPOSTS,
8070
7792
  OUTPOST_QUESTION_LOOKUP_CONCURRENCY,
8071
7793
  assertLaneRunsHereForOperatorMutation,
7794
+ assertQuestionAnswerAuthorized,
8072
7795
  assertQuestionCompanyMatchesLane,
8073
7796
  casUpdateQuestion,
8074
7797
  effectiveLaneQuestionCompany,
@@ -8207,6 +7930,11 @@ export function registerLanesCommand(parent, deps = {}) {
8207
7930
  }, wrap);
8208
7931
  registerOutpostPrunerCommand(lanes);
8209
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);
8210
7938
  const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
8211
7939
  noStoryOption.attributeName = () => "noStory";
8212
7940
  lanes
@@ -8418,7 +8146,7 @@ export function registerLanesCommand(parent, deps = {}) {
8418
8146
  .description("List the stories carried by a lane")
8419
8147
  .argument("<lane-id>", "Lane id")
8420
8148
  .action(wrap(async (laneId) => {
8421
- await runStoryList(ctx, laneId);
8149
+ await storyHandlers.runStoryList(ctx, laneId);
8422
8150
  }));
8423
8151
  stories
8424
8152
  .command("add")
@@ -8427,7 +8155,7 @@ export function registerLanesCommand(parent, deps = {}) {
8427
8155
  .argument("<story-id>", "Story id")
8428
8156
  .option("--project <slug>", "Project id; defaults to the lane's current project")
8429
8157
  .action(wrap(async (laneId, storyId, opts) => {
8430
- await runStoryAdd(ctx, laneId, storyId, opts);
8158
+ await storyHandlers.runStoryAdd(ctx, laneId, storyId, opts);
8431
8159
  }));
8432
8160
  stories
8433
8161
  .command("start")
@@ -8436,7 +8164,7 @@ export function registerLanesCommand(parent, deps = {}) {
8436
8164
  .argument("<story-id>", "Story id")
8437
8165
  .option("--project <slug>", "Project id; defaults to the lane's current project")
8438
8166
  .action(wrap(async (laneId, storyId, opts) => {
8439
- await runStoryStart(ctx, laneId, storyId, opts);
8167
+ await storyHandlers.runStoryStart(ctx, laneId, storyId, opts);
8440
8168
  }));
8441
8169
  stories
8442
8170
  .command("block")
@@ -8446,7 +8174,7 @@ export function registerLanesCommand(parent, deps = {}) {
8446
8174
  .requiredOption("--reason <text>", "Why the story is blocked")
8447
8175
  .option("--project <slug>", "Project id; defaults to the lane's current project")
8448
8176
  .action(wrap(async (laneId, storyId, opts) => {
8449
- await runStoryBlock(ctx, laneId, storyId, opts);
8177
+ await storyHandlers.runStoryBlock(ctx, laneId, storyId, opts);
8450
8178
  }));
8451
8179
  stories
8452
8180
  .command("done")
@@ -8456,7 +8184,7 @@ export function registerLanesCommand(parent, deps = {}) {
8456
8184
  .requiredOption("--verified", "Confirm senior review or merge")
8457
8185
  .option("--project <slug>", "Project id; defaults to the lane's current project")
8458
8186
  .action(wrap(async (laneId, storyId, opts) => {
8459
- await runStoryDone(ctx, laneId, storyId, opts);
8187
+ await storyHandlers.runStoryDone(ctx, laneId, storyId, opts);
8460
8188
  }));
8461
8189
  lanes
8462
8190
  .command("message")
@@ -8745,9 +8473,11 @@ export function registerLanesCommand(parent, deps = {}) {
8745
8473
  .action(wrap(async (opts) => {
8746
8474
  await runPickup(ctx, opts);
8747
8475
  }));
8748
- lanes
8749
- .command("autopilot")
8750
- .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")
8751
8481
  .command("run")
8752
8482
  .description("Run one lead autopilot check pass")
8753
8483
  .option("--dry-run", "Report proposed wakes without writing them")
package/dist/fast-core.js CHANGED
@@ -18,43 +18,71 @@ export async function runFastCore(argv) {
18
18
  const parsed = parseFastCore(argv);
19
19
  if (!parsed)
20
20
  return;
21
- // `root: "live"` — resolve the live HQ root and run the script in it, exactly
22
- // as core.ts's runEntry does. Fail soft: if no root can be resolved, run the
23
- // script anyway with the inherited environment (the hook already carries
24
- // CLAUDE_PROJECT_DIR, and each script has its own root fallback) rather than
25
- // crashing a Stop hook on a resolution error.
21
+ const entry = parsed.entry;
22
+ // The full graph's process gate accepts textual true/false overrides. It ignores
23
+ // numeric env values, so preserve that behavior for family flags here too.
24
+ const familyEnabled = entry.kind !== "native" || !entry.familyFlagKey ||
25
+ await (await import("./native-fast-route.js")).fastFlagEnabled(entry.familyFlagKey, process.env, { numericEnvironmentOverride: false });
26
+ const useShell = entry.kind === "native" && Boolean(entry.familyFlagKey) && !familyEnabled;
27
+ const shouldInjectRoot = useShell
28
+ ? entry.shellHqRoot === "live" || (entry.shellHqRoot === "reconcile-default" &&
29
+ !parsed.args.includes("--hq-root") && !parsed.args.includes("--help") && !parsed.args.includes("-h"))
30
+ : entry.kind === "shell"
31
+ ? true
32
+ : entry.root === "live";
33
+ // `root: "live"` — resolve the live HQ root using the same resolver as the
34
+ // full command graph. Existing shell fast entries keep their historical
35
+ // fail-soft behavior; newly routed commands fall back to the full graph so
36
+ // its expected-user-error formatting remains authoritative.
26
37
  let hqRoot;
27
- try {
28
- hqRoot = resolveLiveRoot(parsed.hqRootArg ? { hqRoot: parsed.hqRootArg } : {});
29
- }
30
- catch {
31
- hqRoot = undefined;
38
+ if (shouldInjectRoot) {
39
+ try {
40
+ hqRoot = resolveLiveRoot(parsed.hqRootArg ? { hqRoot: parsed.hqRootArg } : {});
41
+ }
42
+ catch {
43
+ if (entry.kind === "native") {
44
+ const { runCli } = await import("./main.js");
45
+ await runCli();
46
+ return;
47
+ }
48
+ hqRoot = undefined;
49
+ }
32
50
  }
33
- if (parsed.entry.kind === "native") {
34
- const handlerModule = await import(parsed.entry.modulePath);
35
- const handler = handlerModule[parsed.entry.exportName];
51
+ if (entry.kind === "native" && !useShell) {
52
+ const { installFastPathStreamErrorHandlers } = await import("./utils/fast-stream-errors.js");
53
+ installFastPathStreamErrorHandlers();
54
+ const handlerModule = await import(entry.modulePath);
55
+ const handler = handlerModule[entry.exportName];
36
56
  if (typeof handler !== "function") {
37
- throw new Error(`Native fast handler ${parsed.entry.exportName} was not exported`);
57
+ throw new Error(`Native fast handler ${entry.exportName} was not exported`);
38
58
  }
39
59
  const chunks = [];
40
- for await (const chunk of process.stdin)
41
- chunks.push(chunk);
42
- const code = await handler(argv, Buffer.concat(chunks).toString("utf8"), process.cwd(), hqRoot);
60
+ if (entry.readsStdin) {
61
+ for await (const chunk of process.stdin)
62
+ chunks.push(chunk);
63
+ }
64
+ const code = await handler(parsed.args, entry.readsStdin ? Buffer.concat(chunks).toString("utf8") : "", process.cwd(), hqRoot, entry.name.split(" ").at(-1) ?? entry.name, parsed.hqRootArg);
43
65
  // Let Node drain handler output before terminating. The timeout guard's
44
66
  // stderr message must survive the native route's process boundary.
45
67
  process.exitCode = code;
46
68
  return;
47
69
  }
48
- // Keep the CHILD in the caller's cwd, unlike the generic "live" dispatch. The
49
- // relocated scripts read their data tree from the injected root, not cwd, and
50
- // hq-session's Work Mesh registration serializes $PWD — so changing directory
51
- // to the HQ root would mislabel a bind made from inside a project/worktree.
70
+ const asset = entry.asset;
71
+ if (!asset)
72
+ throw new Error(`Fast route ${entry.name} has no shell companion`);
73
+ // Some historical registrations run from the live root, others preserve the
74
+ // caller cwd and only inject HQ_ROOT. Keep those choices explicit per entry.
75
+ const cwd = entry.kind === "native" && entry.shellCwd === "live"
76
+ ? hqRoot ?? process.cwd()
77
+ : process.cwd();
78
+ const injectRoot = entry.kind === "shell" || entry.kind === "native" &&
79
+ (entry.shellHqRoot === "live" || entry.shellHqRoot === "reconcile-default" && shouldInjectRoot);
52
80
  const { code } = runBundledScript({
53
- asset: parsed.entry.asset,
81
+ asset,
54
82
  args: parsed.args,
55
- cwd: process.cwd(),
56
- hqRoot,
57
- env: parsed.entry.name === "hq-session" ? { HQ_HQ_SESSION_NO_CLI: "1" } : undefined,
83
+ cwd,
84
+ ...(injectRoot ? { hqRoot } : {}),
85
+ env: entry.name === "hq-session" ? { HQ_HQ_SESSION_NO_CLI: "1" } : undefined,
58
86
  });
59
87
  // Propagate verbatim: these scripts use their exit code as their interface.
60
88
  process.exit(code);
@@ -15,16 +15,29 @@ export type FastScaffoldScript = {
15
15
  kind: "shell";
16
16
  /** Path under `assets/scaffold/`, e.g. `core/scripts/foo.sh`. */
17
17
  asset: string;
18
+ root?: "live";
18
19
  } | {
19
- /** Direct subcommand name — `hq core <name>`. */
20
+ /** Command path, joined with spaces, e.g. `policy frontmatter`. */
20
21
  name: string;
21
22
  kind: "native";
22
23
  /** Relative ESM module specifier, resolved from fast-core.js. */
23
24
  modulePath: string;
24
25
  /** Named export which accepts the native fast-handler contract. */
25
26
  exportName: string;
26
- /** Default-off registry flag for this native route. */
27
+ /** Existing default-off gate for the whole fast route. */
27
28
  flagKey: string;
29
+ /** Existing family gate that chooses this native handler or its shell companion. */
30
+ familyFlagKey?: string;
31
+ /** Shell rollback companion used while the family gate is off. */
32
+ asset?: string;
33
+ /** Root context used by the native handler. */
34
+ root?: "live" | "cwd" | "passthrough";
35
+ /** Cwd used for a shell rollback companion. Defaults to the caller cwd. */
36
+ shellCwd?: "caller" | "live";
37
+ /** Root injection behavior for a shell rollback companion. */
38
+ shellHqRoot?: "none" | "live" | "reconcile-default";
39
+ /** Whether this handler consumes stdin. Shell companions inherit stdin directly. */
40
+ readsStdin?: boolean;
28
41
  };
29
42
  /** The relocated scripts that qualify for the entrypoint fast path. */
30
43
  export declare const FAST_SCAFFOLD_SCRIPTS: readonly FastScaffoldScript[];