@indigoai-us/hq-cli 5.345.127 → 5.345.129

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 (77) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/assets/scaffold/core/scripts/handoff-knowledge-commit.sh +123 -0
  3. package/assets/scaffold/core/scripts/hq-detach.sh +169 -0
  4. package/dist/command-catalog.generated.d.ts +40 -2
  5. package/dist/command-catalog.generated.js +48 -2
  6. package/dist/commands/auth.js +19 -2
  7. package/dist/commands/core-bucket-c-handoff-knowledge-fast.d.ts +2 -0
  8. package/dist/commands/core-bucket-c-handoff-knowledge-fast.js +10 -0
  9. package/dist/commands/core-bucket-c-handoff-knowledge-native.d.ts +46 -0
  10. package/dist/commands/core-bucket-c-handoff-knowledge-native.js +284 -0
  11. package/dist/commands/core-bucket-c-hq-detach-fast.d.ts +2 -0
  12. package/dist/commands/core-bucket-c-hq-detach-fast.js +10 -0
  13. package/dist/commands/core-bucket-c-hq-detach-native.d.ts +43 -0
  14. package/dist/commands/core-bucket-c-hq-detach-native.js +241 -0
  15. package/dist/commands/core-bucket-c-work-mesh-offer-native.js +8 -2
  16. package/dist/commands/core.js +12 -0
  17. package/dist/commands/daemon.js +2 -0
  18. package/dist/commands/lanes-accounts.js +3 -1
  19. package/dist/commands/lanes-shared.d.ts +7 -0
  20. package/dist/commands/lanes.js +139 -17
  21. package/dist/commands/mesh.js +32 -19
  22. package/dist/commands/skill.js +9 -4
  23. package/dist/fast-scaffold.js +28 -0
  24. package/dist/lib/bot/inbox-poll-backoff-flag.d.ts +3 -5
  25. package/dist/lib/bot/inbox-poll-backoff-flag.js +20 -12
  26. package/dist/lib/bot/inbox-wake.d.ts +18 -0
  27. package/dist/lib/bot/inbox-wake.js +125 -0
  28. package/dist/lib/bot/run.d.ts +8 -0
  29. package/dist/lib/bot/run.js +51 -7
  30. package/dist/lib/bot/runtime/messages-stream.js +31 -3
  31. package/dist/lib/capacity/lane-login.js +1 -0
  32. package/dist/lib/daemon/catalog.d.ts +4 -0
  33. package/dist/lib/daemon/catalog.js +6 -3
  34. package/dist/lib/daemon/flag-monitor.d.ts +4 -1
  35. package/dist/lib/daemon/flag-monitor.js +12 -6
  36. package/dist/lib/daemon/flag.d.ts +2 -0
  37. package/dist/lib/daemon/flag.js +11 -5
  38. package/dist/lib/daemon/hq-child-bootstrap.mjs +47 -7
  39. package/dist/lib/daemon/kill-switch-flags.d.ts +5 -0
  40. package/dist/lib/daemon/kill-switch-flags.js +5 -0
  41. package/dist/lib/daemon/lanes-reconcile-loader.d.ts +7 -0
  42. package/dist/lib/daemon/lanes-reconcile-loader.js +24 -0
  43. package/dist/lib/daemon/lanes-reconcile-watcher.d.ts +17 -0
  44. package/dist/lib/daemon/lanes-reconcile-watcher.js +80 -0
  45. package/dist/lib/daemon/remote-control.d.ts +8 -5
  46. package/dist/lib/daemon/remote-control.js +8 -7
  47. package/dist/lib/daemon/run.d.ts +4 -1
  48. package/dist/lib/daemon/run.js +133 -20
  49. package/dist/lib/doctor/json-output.d.ts +15 -3
  50. package/dist/lib/doctor/json-output.js +13 -1
  51. package/dist/lib/lanes/accounts.js +2 -2
  52. package/dist/lib/lanes/daemon-reconcile.d.ts +3 -0
  53. package/dist/lib/lanes/daemon-reconcile.js +8 -0
  54. package/dist/lib/lanes/host-capacity.d.ts +4 -0
  55. package/dist/lib/lanes/host-capacity.js +10 -1
  56. package/dist/lib/lanes/index.d.ts +2 -2
  57. package/dist/lib/lanes/index.js +1 -1
  58. package/dist/lib/lanes/loop.d.ts +8 -0
  59. package/dist/lib/lanes/loop.js +35 -0
  60. package/dist/lib/lanes/quota-notice.d.ts +11 -11
  61. package/dist/lib/lanes/quota-notice.js +20 -29
  62. package/dist/lib/lanes/quota.d.ts +69 -50
  63. package/dist/lib/lanes/quota.js +146 -127
  64. package/dist/lib/lanes/reconcile-result.d.ts +10 -0
  65. package/dist/lib/lanes/reconcile-result.js +11 -0
  66. package/dist/lib/lanes/reconcile.d.ts +14 -0
  67. package/dist/lib/lanes/reconcile.js +77 -23
  68. package/dist/lib/lanes/watcher.d.ts +8 -0
  69. package/dist/lib/lanes/watcher.js +62 -20
  70. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -1
  71. package/dist/lib/mesh/live/daemon/work-subscribe.js +8 -1
  72. package/dist/lib/mesh/live/paths.d.ts +2 -0
  73. package/dist/lib/mesh/live/paths.js +2 -0
  74. package/dist/lib/mesh/story-status.d.ts +2 -2
  75. package/dist/lib/mesh/story-status.js +2 -1
  76. package/dist/utils/resident-package-preload.js +18 -4
  77. package/package.json +1 -1
@@ -437,6 +437,13 @@ export interface LaneAccountsHandlerDeps {
437
437
  readClaudeAccountBalanceSignalsEnabled(ctx: Ctx): Promise<boolean>;
438
438
  reconcileHostFromCtx(ctx: Ctx): ReconcileHost;
439
439
  resolveCallerPath(callerCwd: string, filePath: string): string;
440
+ /** Admit queued lanes in-process after capacity changes; returns admitted lanes. */
441
+ admitQueuedFromCtx(ctx: Ctx): Promise<Array<{
442
+ id: string;
443
+ from?: string;
444
+ to?: string;
445
+ reason?: string;
446
+ }>>;
440
447
  }
441
448
  export interface LaneQuestionsHandlerDeps {
442
449
  CAPACITY_PROBE_TIMEOUT_MS: number;
@@ -24,6 +24,7 @@ export { resolveAnswerChoice } from "./lanes-questions.js";
24
24
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
25
25
  import { registerLaneKpisCommand } from "./lanes-kpis.js";
26
26
  import { registerLaneKeepAliveAuditCommand } from "./lanes-keep-alive-audit.js";
27
+ import { finishReconcilePass } from "../lib/lanes/reconcile-result.js";
27
28
  import { registerLaneAutopilotReportCommand } from "./lanes-autopilot.js";
28
29
  import { registerLaneChecksCommand } from "./lanes-checks.js";
29
30
  import { registerLaneDropboxRepairCommand } from "./lanes-dropbox-repair.js";
@@ -41,11 +42,12 @@ import { runResidentCommandWithPackageLease } from "../utils/resident-package-pr
41
42
  import { codexEnvEvidence, grokEnvEvidence } from "../lib/doctor/platform.js";
42
43
  import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff, readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
43
44
  import { readLaneStatusRowFields } from "../lib/lanes/status-fields.js";
45
+ import { registerDaemonLanesReconcileHandler } from "../lib/lanes/daemon-reconcile.js";
44
46
  import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
45
47
  import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
46
48
  import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_STATUS_ROW_FIELDS_FLAG, LANE_REAP_STOP_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, LANE_WORKER_CONTINUITY_FLAG, LANE_CONDUCT_LOOP_FLAG, LANE_ENVELOPE_REPAIR_GUIDANCE_FLAG, } from "../lib/lanes/flag-keys.js";
47
49
  import { findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
48
- import { enqueueLoopEnvelope, interruptLoopQueue, interruptLoopQueueForStory, loopControllerIsAlive, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
50
+ import { commitIdleLoopStop, enqueueLoopEnvelope, interruptLoopQueue, interruptLoopQueueForStory, loopControllerIsAlive, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
49
51
  import { runLaneLoopController, startLaneLoopController } from "./lanes-loop.js";
50
52
  import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
51
53
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
@@ -76,6 +78,7 @@ import { listLaneReviews } from "../lib/lanes/reviews.js";
76
78
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
77
79
  import { remoteControlSelectionConfig, remoteControlSessionHome } from "../lib/lanes/remote-control-lane.js";
78
80
  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, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, isLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, isFailedQuotaKeepAliveLane, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
81
+ import { admitQueuedNow } from "../lib/lanes/reconcile.js";
79
82
  import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
80
83
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
81
84
  import { laneCompanyUidSource, resolveLaneCompany } from "../lib/lanes/lane-company.js";
@@ -490,6 +493,82 @@ function livenessDepsFromCtx(ctx) {
490
493
  hostname: ctx.hostname,
491
494
  };
492
495
  }
496
+ /**
497
+ * Admit queued lanes in-process after a command frees or adds capacity, so
498
+ * the queue drains without `hq daemon`. Best effort: an admission failure is
499
+ * reported on stderr and never fails the command that freed the slot.
500
+ */
501
+ async function admitQueuedFromCtx(ctx) {
502
+ try {
503
+ const result = await admitQueuedNow(ctx.hqRoot(), {
504
+ now: ctx.now,
505
+ host: reconcileHostFromCtx(ctx),
506
+ dryRun: false,
507
+ spawnLaneDeps: {
508
+ env: ctx.env,
509
+ existsSync: ctx.existsSync,
510
+ spawnProcess: ctx.spawnProcess,
511
+ proveDetached: ctx.proveDetached,
512
+ killProcess: ctx.killProcess,
513
+ now: ctx.now,
514
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
515
+ runCompaction: ctx.runCompaction,
516
+ locateTranscript: ctx.locateTranscript,
517
+ },
518
+ throttleDeps: ctx.throttleDeps,
519
+ livenessDeps: livenessDepsFromCtx(ctx),
520
+ presenceConnected: ctx.presenceConnected,
521
+ mesh: ctx.mesh,
522
+ env: ctx.env,
523
+ });
524
+ for (const error of result.errors) {
525
+ process.stderr.write(`hq lanes: queue admission: ${error.message}\n`);
526
+ }
527
+ return result.admitted;
528
+ }
529
+ catch (err) {
530
+ process.stderr.write(`hq lanes: queue admission failed: ${err instanceof Error ? err.message : String(err)}\n`);
531
+ return [];
532
+ }
533
+ }
534
+ /**
535
+ * End a queued loop lane whose queue holds only a stop envelope and whose
536
+ * controller is not running. Without this the lane waits for admission just
537
+ * to read the stop, and counts as busy for its worker until then.
538
+ */
539
+ async function finalizeIdleLoopStop(ctx, lane) {
540
+ if (lane.loop_mode !== true || lane.state !== "queued")
541
+ return undefined;
542
+ const hqRoot = ctx.hqRoot();
543
+ const runDirPath = runDir(hqRoot, lane.lane_id);
544
+ if (loopControllerIsAlive(runDirPath))
545
+ return undefined;
546
+ if (!commitIdleLoopStop(runDirPath, lane.lane_id, ctx.now))
547
+ return undefined;
548
+ let stopped;
549
+ try {
550
+ stopped = casUpdateLane(hqRoot, lane.lane_id, (current) => {
551
+ if (current.state !== "queued") {
552
+ throw new LanesError("lane_state_changed", `Lane ${lane.lane_id} left the queue while its stop was committed`, { lane_id: lane.lane_id, state: current.state });
553
+ }
554
+ return {
555
+ ...current,
556
+ state: "done",
557
+ queued_reason: undefined,
558
+ queued_details: undefined,
559
+ admission_reserved_at: undefined,
560
+ terminal_reason: "loop_stopped",
561
+ };
562
+ });
563
+ }
564
+ catch (err) {
565
+ if (err instanceof LanesError && err.error === "lane_state_changed")
566
+ return undefined;
567
+ throw err;
568
+ }
569
+ await emitLaneTerminalStateSoft(ctx, stopped);
570
+ return stopped;
571
+ }
493
572
  function reconcileHostFromCtx(ctx) {
494
573
  return resolveAdmissionHost({
495
574
  hostname: ctx.hostname,
@@ -3522,6 +3601,17 @@ async function runCreate(ctx, opts) {
3522
3601
  throw new LanesError("lane_title_requires_remote_control", "--title is only supported with --remote-control", {});
3523
3602
  }
3524
3603
  const continuityEnabled = opts.remoteControl !== true && opts.worker !== undefined && await readLanesRegistryFlagFailOpen(LANE_WORKER_CONTINUITY_FLAG, { company: company ?? meshCompany, env: ctx.env, defaultValue: true });
3604
+ // A lane that waits only on the admitter (a queued loop lane holding just a
3605
+ // stop, or a queued lane with free capacity) is not busy: finish it here.
3606
+ const preCreateAdmitted = [];
3607
+ if (continuityEnabled) {
3608
+ const workerId = resolvedWorker?.id ?? opts.worker;
3609
+ const waiting = loadAllLanes(hqRoot).filter((lane) => lane.worker === workerId && lane.provider === provider && lane.state === "queued");
3610
+ for (const lane of waiting)
3611
+ await finalizeIdleLoopStop(ctx, lane);
3612
+ if (waiting.length > 0)
3613
+ preCreateAdmitted.push(...await admitQueuedFromCtx(ctx));
3614
+ }
3525
3615
  let workerContinuity = continuityEnabled
3526
3616
  ? findWorkerContinuityLane(loadAllLanes(hqRoot), {
3527
3617
  senior,
@@ -4337,10 +4427,19 @@ async function runCreate(ctx, opts) {
4337
4427
  : undefined,
4338
4428
  });
4339
4429
  }
4430
+ // Admit queued lanes (older ones first) now that this create has settled.
4431
+ // Outpost lanes are admitted on their Outpost.
4432
+ const admitted = [...preCreateAdmitted];
4433
+ if (!outpostId) {
4434
+ admitted.push(...await admitQueuedFromCtx(ctx));
4435
+ if (admitted.some((entry) => entry.id === started.lane_id))
4436
+ started = readLane(hqRoot, started.lane_id);
4437
+ }
4340
4438
  if (!opts.quiet)
4341
4439
  printJson({
4342
4440
  ok: true,
4343
4441
  action: "create",
4442
+ admitted,
4344
4443
  lane_id: started.lane_id,
4345
4444
  state: started.state,
4346
4445
  queued_reason: started.queued_reason ?? null,
@@ -7410,7 +7509,7 @@ async function prepareAccountMove(ctx, lane, input) {
7410
7509
  });
7411
7510
  const block = quotaBlockFor(loadQuotaState(hqRoot), lane.provider, target, model, now);
7412
7511
  if (block.blocked) {
7413
- throw new LanesError("account_quota_held", `Account ${target} for ${lane.provider} is held${block.quota_reset_at ? ` until ${block.quota_reset_at}` : ""}${block.marker ? ` (${block.marker})` : ""}. Clear the hold with: hq lanes accounts clear-quota ${lane.provider} ${target}`, { provider: lane.provider, account: target, quota_reset_at: block.quota_reset_at ?? null, marker: block.marker ?? null, model: model ?? null, clear_command: `hq lanes accounts clear-quota ${lane.provider} ${target}` });
7512
+ throw new LanesError("account_quota_held", `Account ${target} for ${lane.provider} cannot run model ${model ?? "(default)"}: the account's model needs credits${block.marker ? ` (${block.marker})` : ""}. Once credits are added, clear the marker with: hq lanes accounts clear-quota ${lane.provider} ${target}`, { provider: lane.provider, account: target, quota_reset_at: block.quota_reset_at ?? null, marker: block.marker ?? null, model: model ?? null, clear_command: `hq lanes accounts clear-quota ${lane.provider} ${target}` });
7414
7513
  }
7415
7514
  if (model !== undefined) {
7416
7515
  validateLaneModel(lane.provider, model, targetHome, {
@@ -8705,8 +8804,18 @@ async function runStop(ctx, laneId, reason, quiet = false) {
8705
8804
  const queued = enqueueLoopEnvelope({ runDir: runDirPath, laneId: lane.lane_id, envelope: { kind: "stop" }, now: ctx.now });
8706
8805
  if (!controllerAlive && lane.state !== "queued")
8707
8806
  startLaneLoopController(ctx, lane.lane_id);
8807
+ const finalized = !controllerAlive ? await finalizeIdleLoopStop(ctx, readLane(hqRoot, lane.lane_id)) : undefined;
8808
+ const admitted = await admitQueuedFromCtx(ctx);
8708
8809
  if (!quiet)
8709
- printJson({ ok: true, action: "stop", lane_id: lane.lane_id, terminal: false, queued_stop: queued });
8810
+ printJson({
8811
+ ok: true,
8812
+ action: "stop",
8813
+ lane_id: lane.lane_id,
8814
+ terminal: finalized !== undefined,
8815
+ queued_stop: queued,
8816
+ ...(finalized ? { lane: finalized } : {}),
8817
+ admitted,
8818
+ });
8710
8819
  return;
8711
8820
  }
8712
8821
  if (isTerminalLaneState(lane.state)) {
@@ -8744,6 +8853,7 @@ async function runStop(ctx, laneId, reason, quiet = false) {
8744
8853
  }
8745
8854
  await emitLaneTerminalStateSoft(ctx, updated);
8746
8855
  const cancelled = cancelPendingQuestionsForLane(hqRoot, laneId, at);
8856
+ const admitted = await admitQueuedFromCtx(ctx);
8747
8857
  if (!quiet)
8748
8858
  printJson({
8749
8859
  ok: true,
@@ -8752,6 +8862,7 @@ async function runStop(ctx, laneId, reason, quiet = false) {
8752
8862
  terminal: true,
8753
8863
  resumable: false,
8754
8864
  cancelled_questions: cancelled.map((q) => q.question_id),
8865
+ admitted,
8755
8866
  });
8756
8867
  }
8757
8868
  /** Stderr notice that `stop` ends a lane for good, naming the resumable path. */
@@ -9429,21 +9540,21 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
9429
9540
  summary.errors.push({ code: error instanceof LanesError ? error.error : "lead_autopilot_failed", message });
9430
9541
  process.stderr.write(`hq lanes autopilot: ${message}\n`);
9431
9542
  }
9432
- printJson({
9433
- ...summary,
9434
- ...(autopilotRegistrySync ? { autopilot_registry_sync: autopilotRegistrySync } : {}),
9435
- ...(autopilot ? { autopilot } : {}),
9436
- ...(remoteLost.length > 0 ? { remote_lost: remoteLost } : {}),
9437
- ...(mirrorRefresh.unverifiedRemote.size > 0
9438
- ? {
9439
- unverified_remote_lanes: [...mirrorRefresh.unverifiedRemote.keys()]
9440
- .filter((id) => !terminalizedRemoteIds.has(id)),
9441
- }
9442
- : {}),
9443
- });
9444
- if (!summary.ok) {
9445
- process.exit(1);
9543
+ if (!opts.inProcess) {
9544
+ printJson({
9545
+ ...summary,
9546
+ ...(autopilotRegistrySync ? { autopilot_registry_sync: autopilotRegistrySync } : {}),
9547
+ ...(autopilot ? { autopilot } : {}),
9548
+ ...(remoteLost.length > 0 ? { remote_lost: remoteLost } : {}),
9549
+ ...(mirrorRefresh.unverifiedRemote.size > 0
9550
+ ? {
9551
+ unverified_remote_lanes: [...mirrorRefresh.unverifiedRemote.keys()]
9552
+ .filter((id) => !terminalizedRemoteIds.has(id)),
9553
+ }
9554
+ : {}),
9555
+ });
9446
9556
  }
9557
+ finishReconcilePass(summary, opts.inProcess ? "daemon" : "cli");
9447
9558
  }
9448
9559
  async function runReconcileInstall(ctx, opts) {
9449
9560
  // `hq daemon` runs lanes-reconcile on this machine; a separate timer would run it twice.
@@ -9540,6 +9651,7 @@ export function registerLanesCommand(parent, deps = {}) {
9540
9651
  readClaudeAccountBalanceSignalsEnabled,
9541
9652
  reconcileHostFromCtx,
9542
9653
  resolveCallerPath,
9654
+ admitQueuedFromCtx,
9543
9655
  });
9544
9656
  const questionHandlers = createLaneQuestionsHandlers({
9545
9657
  CAPACITY_PROBE_TIMEOUT_MS,
@@ -9647,6 +9759,16 @@ export function registerLanesCommand(parent, deps = {}) {
9647
9759
  withResumeHireAdmission,
9648
9760
  };
9649
9761
  const messageHandlers = createLaneMessageHandlers(messageHandlerDeps);
9762
+ const daemonReconcileCtx = { ...ctx, env: { ...ctx.env } };
9763
+ registerDaemonLanesReconcileHandler(async () => {
9764
+ const hqRoot = daemonReconcileCtx.hqRoot();
9765
+ daemonReconcileCtx.env.HQ_ROOT = hqRoot;
9766
+ daemonReconcileCtx.cwd = () => hqRoot;
9767
+ daemonReconcileCtx.callerCwd = () => hqRoot;
9768
+ await runReconcile(daemonReconcileCtx, { inProcess: true }, async (lane) => {
9769
+ await messageHandlers.recoverQueuedMessages(daemonReconcileCtx, lane.lane_id);
9770
+ });
9771
+ });
9650
9772
  monitorLaneMessageApis.set(parent, (laneId, payload) => messageHandlers.deliverMonitorLaneMessage(ctx, laneId, payload));
9651
9773
  const resumeHandlers = createLaneResumeHandlers({
9652
9774
  onLaneWorkerStarted: deps.onLaneWorkerStarted,
@@ -307,16 +307,21 @@ function parseProjectReposJson(raw, source) {
307
307
  throw err;
308
308
  }
309
309
  }
310
- function projectStoriesFromJson(value, source) {
310
+ function projectStoriesFromJson(value, source, options = {}) {
311
311
  const plan = Array.isArray(value) ? null : record(value);
312
- const rawStories = Array.isArray(value)
313
- ? value
314
- : Array.isArray(plan?.userStories)
315
- ? plan.userStories
316
- : Array.isArray(plan?.stories)
317
- ? plan.stories
318
- : null;
319
- if (!rawStories) {
312
+ if (options.requireUserStories && (!plan || !Array.isArray(plan.userStories))) {
313
+ fail(`${source} must be a PRD object with a userStories array.`);
314
+ }
315
+ const rawStories = options.requireUserStories
316
+ ? plan?.userStories ?? null
317
+ : Array.isArray(value)
318
+ ? value
319
+ : Array.isArray(plan?.userStories)
320
+ ? plan.userStories
321
+ : Array.isArray(plan?.stories)
322
+ ? plan.stories
323
+ : null;
324
+ if (!Array.isArray(rawStories)) {
320
325
  fail(`${source} must be a JSON story array or an object with userStories.`);
321
326
  }
322
327
  const rejectedStories = [];
@@ -398,7 +403,7 @@ function failProjectStoryStates(rejected, json) {
398
403
  }
399
404
  function parseProjectPlan(raw, source, options = {}) {
400
405
  try {
401
- const plan = projectStoriesFromJson(JSON.parse(raw), source);
406
+ const plan = projectStoriesFromJson(JSON.parse(raw), source, options);
402
407
  if (plan.rejectedStories.length && !options.allowRejectedStories) {
403
408
  throw new RejectedProjectStoryStatesError(plan.rejectedStories);
404
409
  }
@@ -431,7 +436,7 @@ async function runProjectSet(projectId, opts) {
431
436
  fail("Pass only one of --repos-json or --repo.");
432
437
  }
433
438
  const source = opts.storiesFile
434
- ? parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile)
439
+ ? parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile, { requireUserStories: true })
435
440
  : opts.storiesJson
436
441
  ? parseProjectPlan(opts.storiesJson, "--stories-json")
437
442
  : undefined;
@@ -588,12 +593,17 @@ async function runProjectMemberAdd(projectId, personUid, opts) {
588
593
  async function runProjectEnsure(projectId, opts) {
589
594
  if (!opts.storiesFile?.trim())
590
595
  fail("`--stories-file <prd.json>` is required.");
591
- const source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile, { allowRejectedStories: true });
592
- const { token, company } = await withCompany(opts);
593
- if (source.rejectedStories.length && source.stories.length === 0) {
594
- failProjectStoryStates(source.rejectedStories, Boolean(opts.json));
596
+ let source;
597
+ try {
598
+ source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile, { requireUserStories: true });
599
+ }
600
+ catch (err) {
601
+ if (!(err instanceof RejectedProjectStoryStatesError) || !opts.json)
602
+ throw err;
603
+ failProjectStoryStates(err.rejected, true);
595
604
  return;
596
605
  }
606
+ const { token, company } = await withCompany(opts);
597
607
  let current;
598
608
  try {
599
609
  current = await getProjectView(token, company.companyUid, projectId);
@@ -738,7 +748,10 @@ async function runProjectEnsure(projectId, opts) {
738
748
  async function runProjectReconcile(projectId, opts) {
739
749
  if (!opts.storiesFile?.trim())
740
750
  fail("`--stories-file <prd.json>` is required.");
741
- const source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile, { allowRejectedStories: true });
751
+ const source = parseProjectPlan(readFileSync(opts.storiesFile, "utf8"), opts.storiesFile, {
752
+ requireUserStories: true,
753
+ allowRejectedStories: true,
754
+ });
742
755
  const { token, company } = await withCompany(opts);
743
756
  let current;
744
757
  try {
@@ -852,7 +865,7 @@ async function runPrdSync(opts) {
852
865
  return;
853
866
  }
854
867
  try {
855
- const source = parseProjectPlan(readFileSync(file, "utf8"), file);
868
+ const source = parseProjectPlan(readFileSync(file, "utf8"), file, { requireUserStories: true });
856
869
  await ensureMissingBoardStories({
857
870
  token,
858
871
  company: {
@@ -1091,7 +1104,7 @@ async function runBindProject(target, opts) {
1091
1104
  }
1092
1105
  if (!current.companyUid)
1093
1106
  fail(`Session ${sessionId} has no company. Bind a company first.`);
1094
- const plan = parseProjectPlan(readFileSync(prdPath, "utf8"), prdPath);
1107
+ const plan = parseProjectPlan(readFileSync(prdPath, "utf8"), prdPath, { requireUserStories: true });
1095
1108
  const { token, company } = await withCompany({ company: opts.company ?? current.companySlug });
1096
1109
  await ensureProjectBoard({
1097
1110
  token,
@@ -1234,7 +1247,7 @@ async function runContextReconcile(opts) {
1234
1247
  validateMembership,
1235
1248
  ensureProjectBoard: token
1236
1249
  ? async ({ companyUid, projectId, prdPath }) => {
1237
- const plan = parseProjectPlan(readFileSync(prdPath, "utf8"), prdPath);
1250
+ const plan = parseProjectPlan(readFileSync(prdPath, "utf8"), prdPath, { requireUserStories: true });
1238
1251
  await ensureProjectBoard({
1239
1252
  token,
1240
1253
  company: { companyUid, companySlug: companyUid },
@@ -190,16 +190,21 @@ function displayNameFromSlug(slug) {
190
190
  .join(" ");
191
191
  }
192
192
  export function makeSkillTemplate(input) {
193
- const name = input.name?.trim() || displayNameFromSlug(input.slug);
193
+ // Frontmatter `name` is the slash-command name in Claude Code and Codex, so it
194
+ // must be the slug. A spaced display title there breaks command entry in the
195
+ // Claude desktop app. The display title goes in frontmatter `title` (which
196
+ // HQ Pro shows in the console) and in the heading.
197
+ const title = input.name?.trim() || displayNameFromSlug(input.slug);
194
198
  const description = input.description?.trim() ?? "";
195
199
  return [
196
200
  "---",
197
- `name: ${JSON.stringify(name)}`,
201
+ `name: ${JSON.stringify(input.slug)}`,
202
+ `title: ${JSON.stringify(title)}`,
198
203
  `description: ${JSON.stringify(description)}`,
199
204
  "tags: []",
200
205
  "---",
201
206
  "",
202
- `# ${name}`,
207
+ `# ${title}`,
203
208
  "",
204
209
  "Describe when and how to use this skill.",
205
210
  "",
@@ -466,7 +471,7 @@ export function registerSkillCommand(program, deps = {}) {
466
471
  skill
467
472
  .command("create <slug>")
468
473
  .description("Register, stamp, surface, and sync a company skill")
469
- .option("--name <name>", "Display name when creating a new template")
474
+ .option("--name <name>", "Display title for a new template, written as frontmatter title (frontmatter name is always the slug)")
470
475
  .option("--description <text>", "Description when creating a new template")
471
476
  .option("--no-sync", "Register locally without uploading the stamped file")
472
477
  .option("--surface-only", "Refresh local skill discovery without registration or sync")
@@ -160,6 +160,30 @@ export function isDefaultOnFastFamily(flagKey) {
160
160
  }
161
161
  /** The relocated scripts that qualify for the entrypoint fast path. */
162
162
  export const FAST_SCAFFOLD_SCRIPTS = [
163
+ {
164
+ name: "hq-detach",
165
+ kind: "native",
166
+ modulePath: "./commands/core-bucket-c-hq-detach-fast.js",
167
+ exportName: "runHqDetachFast",
168
+ flagKey: "hq-core-native-fast-route",
169
+ familyFlagKey: "core-native-bucket-c",
170
+ asset: "core/scripts/hq-detach.sh",
171
+ root: "cwd",
172
+ shellCwd: "caller",
173
+ shellHqRoot: "none",
174
+ },
175
+ {
176
+ name: "handoff-knowledge-commit",
177
+ kind: "native",
178
+ modulePath: "./commands/core-bucket-c-handoff-knowledge-fast.js",
179
+ exportName: "runHandoffKnowledgeCommitFast",
180
+ flagKey: "hq-core-native-fast-route",
181
+ familyFlagKey: "core-native-bucket-c",
182
+ asset: "core/scripts/handoff-knowledge-commit.sh",
183
+ root: "cwd",
184
+ shellCwd: "caller",
185
+ shellHqRoot: "none",
186
+ },
163
187
  {
164
188
  name: "work-mesh-project-registration-offer",
165
189
  kind: "native",
@@ -897,6 +921,10 @@ export function isFastCoreRequest(argv) {
897
921
  }
898
922
  /** Metadata-only help can render without importing the selected command graph. */
899
923
  export function isCommandHelpRequest(argv) {
924
+ // `core` owns the argument parser even when global options appear before
925
+ // its subcommand; its child commands pass --help through to their handlers.
926
+ if (argv[2] === "core" || argv[2] === "run")
927
+ return false;
900
928
  const args = argv.slice(2);
901
929
  const delimiter = args.indexOf("--");
902
930
  const beforeDelimiter = delimiter === -1 ? args : args.slice(0, delimiter);
@@ -2,6 +2,8 @@ import { type FlagClient, type FlagClientOptions } from "@indigoai-us/hq-flags-c
2
2
  import { type BotConfig } from "./config.js";
3
3
  export declare const INBOX_POLL_BACKOFF_FLAG = "agents.inbox-poll-backoff-local-cli";
4
4
  export declare const INBOX_POLL_BACKOFF_FLAG_FALLBACK = false;
5
+ export declare const INBOX_MQTT_WAKE_FLAG = "agents.bot-inbox-mqtt-wake";
6
+ export declare const INBOX_MQTT_WAKE_FLAG_FALLBACK = true;
5
7
  export declare const INBOX_POLL_BACKOFF_FLAG_TIMEOUT_MS = 2000;
6
8
  export declare const INBOX_POLL_BACKOFF_FLAG_CACHE_MS: number;
7
9
  type InboxPollBackoffFlagClient = Pick<FlagClient, "ready" | "isEnabled" | "close">;
@@ -14,11 +16,7 @@ export interface InboxPollBackoffFlagReaderDependencies {
14
16
  reportFailure?: (error: unknown) => void;
15
17
  }
16
18
  export declare function safeErrorClass(error: unknown): string;
17
- /**
18
- * Create a per-bot reader. The flag client is opened at most once per three
19
- * minutes; a missing identity, failed request, or malformed snapshot keeps
20
- * idle polling at its existing two-second interval.
21
- */
22
19
  export declare function createInboxPollBackoffFlagReader(config: Pick<BotConfig, "kind" | "workerId" | "companies" | "companySlug" | "hqRoot">, dependencies?: InboxPollBackoffFlagReaderDependencies): () => Promise<boolean>;
20
+ export declare function createInboxMqttWakeFlagReader(config: Pick<BotConfig, "kind" | "workerId" | "companies" | "companySlug" | "hqRoot">, dependencies?: InboxPollBackoffFlagReaderDependencies): () => Promise<boolean>;
23
21
  export {};
24
22
  //# sourceMappingURL=inbox-poll-backoff-flag.d.ts.map
@@ -5,6 +5,8 @@ import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
5
5
  import { effectiveBotCompanies, effectiveBotKind } from "./config.js";
6
6
  export const INBOX_POLL_BACKOFF_FLAG = "agents.inbox-poll-backoff-local-cli";
7
7
  export const INBOX_POLL_BACKOFF_FLAG_FALLBACK = false;
8
+ export const INBOX_MQTT_WAKE_FLAG = "agents.bot-inbox-mqtt-wake";
9
+ export const INBOX_MQTT_WAKE_FLAG_FALLBACK = true;
8
10
  // One read every three minutes on a fresh connection. From a home network the
9
11
  // TCP and TLS handshakes plus the resolve call take about 250-400 ms, so a
10
12
  // 150 ms budget timed out on every read and kept the gate off.
@@ -15,8 +17,8 @@ export function safeErrorClass(error) {
15
17
  const name = error instanceof Error ? error.name : "UnknownError";
16
18
  return /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(name) ? name : "UnknownError";
17
19
  }
18
- function reportFlagFailure(error) {
19
- process.stderr.write(`HQ local bot inbox poll backoff flag lookup failed (${safeErrorClass(error)}); using default off.\n`);
20
+ function reportFlagFailure(error, fallback) {
21
+ process.stderr.write(`HQ local bot inbox flag lookup failed (${safeErrorClass(error)}); using default ${fallback ? "on" : "off"}.\n`);
20
22
  }
21
23
  function companyContexts(config, env) {
22
24
  if (effectiveBotKind(config) !== "company")
@@ -44,9 +46,9 @@ function companyContexts(config, env) {
44
46
  /**
45
47
  * Create a per-bot reader. The flag client is opened at most once per three
46
48
  * minutes; a missing identity, failed request, or malformed snapshot keeps
47
- * idle polling at its existing two-second interval.
49
+ * registered fallback.
48
50
  */
49
- export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
51
+ function createInboxFlagReader(config, flagKey, fallback, dependencies = {}) {
50
52
  const env = dependencies.env ?? process.env;
51
53
  const contexts = companyContexts(config, env);
52
54
  const endpoint = resolveFlagRegistryEndpoint(undefined, env);
@@ -54,15 +56,15 @@ export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
54
56
  const createClient = dependencies.createClient ?? createFlagClient;
55
57
  const getToken = dependencies.getToken ?? (() => loadCachedTokens()?.idToken ?? "");
56
58
  const doFetch = dependencies.fetch ?? globalThis.fetch;
57
- const report = dependencies.reportFailure ?? reportFlagFailure;
58
- let cachedValue = INBOX_POLL_BACKOFF_FLAG_FALLBACK;
59
+ const report = dependencies.reportFailure ?? ((error) => reportFlagFailure(error, fallback));
60
+ let cachedValue = fallback;
59
61
  let lastReadAt = Number.NEGATIVE_INFINITY;
60
62
  let pending = null;
61
63
  const readContext = async (context) => {
62
64
  let client;
63
65
  let failed = false;
64
66
  let failureReported = false;
65
- let enabled = INBOX_POLL_BACKOFF_FLAG_FALLBACK;
67
+ let enabled = fallback;
66
68
  const reportFailureOnce = (error) => {
67
69
  failed = true;
68
70
  if (failureReported)
@@ -84,7 +86,7 @@ export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
84
86
  });
85
87
  await client.ready();
86
88
  if (!failed) {
87
- enabled = resolveFlagGate(client, INBOX_POLL_BACKOFF_FLAG, { fallback: INBOX_POLL_BACKOFF_FLAG_FALLBACK }, () => INBOX_POLL_BACKOFF_FLAG_FALLBACK);
89
+ enabled = resolveFlagGate(client, flagKey, { fallback }, () => fallback);
88
90
  }
89
91
  }
90
92
  catch (error) {
@@ -98,12 +100,12 @@ export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
98
100
  reportFailureOnce(error);
99
101
  }
100
102
  }
101
- return failed ? INBOX_POLL_BACKOFF_FLAG_FALLBACK : enabled;
103
+ return failed ? fallback : enabled;
102
104
  };
103
105
  const read = async () => {
104
106
  if (!contexts || contexts.length === 0) {
105
- report(new Error("local bot inbox poll backoff flag has no resolvable company identity"));
106
- return INBOX_POLL_BACKOFF_FLAG_FALLBACK;
107
+ report(new Error("local bot inbox flag has no resolvable company identity"));
108
+ return fallback;
107
109
  }
108
110
  const values = await Promise.all(contexts.map(readContext));
109
111
  return values.every(Boolean);
@@ -123,7 +125,7 @@ export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
123
125
  })
124
126
  .catch((error) => {
125
127
  report(error);
126
- cachedValue = INBOX_POLL_BACKOFF_FLAG_FALLBACK;
128
+ cachedValue = fallback;
127
129
  return cachedValue;
128
130
  })
129
131
  .finally(() => {
@@ -132,4 +134,10 @@ export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
132
134
  return pending;
133
135
  };
134
136
  }
137
+ export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
138
+ return createInboxFlagReader(config, INBOX_POLL_BACKOFF_FLAG, INBOX_POLL_BACKOFF_FLAG_FALLBACK, dependencies);
139
+ }
140
+ export function createInboxMqttWakeFlagReader(config, dependencies = {}) {
141
+ return createInboxFlagReader(config, INBOX_MQTT_WAKE_FLAG, INBOX_MQTT_WAKE_FLAG_FALLBACK, dependencies);
142
+ }
135
143
  //# sourceMappingURL=inbox-poll-backoff-flag.js.map
@@ -0,0 +1,18 @@
1
+ import * as fs from "node:fs";
2
+ export { AGENT_INBOX_PUSH_STATE_BASENAME } from "../mesh/live/paths.js";
3
+ type WakeEvent = "wake" | "timeout" | "aborted";
4
+ type Watcher = Pick<fs.FSWatcher, "close"> & {
5
+ on?: (event: "error", listener: (error: Error) => void) => unknown;
6
+ };
7
+ export interface InboxWakeWatcher {
8
+ wait(delayMs: number, signal?: AbortSignal): Promise<WakeEvent>;
9
+ isLiveFor?(agentUid: string): boolean;
10
+ close(): void;
11
+ }
12
+ /** Watches the content-free Work Mesh inbox sequence, with a bounded poll fallback. */
13
+ export declare function createInboxWakeWatcher(root: string, options?: {
14
+ watch?: (root: string, listener: (event: string, filename: string | Buffer | null) => void) => Watcher;
15
+ onError?: (error: unknown) => void;
16
+ now?: () => number;
17
+ }): InboxWakeWatcher;
18
+ //# sourceMappingURL=inbox-wake.d.ts.map