@indigoai-us/hq-cli 5.345.147 → 5.345.148

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 (122) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/assets/bot-workers/setup/worker.yaml +11 -0
  3. package/dist/command-catalog.generated.d.ts +297 -3
  4. package/dist/command-catalog.generated.js +368 -3
  5. package/dist/commands/agent-inbox.js +2 -1
  6. package/dist/commands/cloud.js +31 -1
  7. package/dist/commands/core-refresh-vault-access-native.d.ts +17 -0
  8. package/dist/commands/core-refresh-vault-access-native.js +214 -19
  9. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
  10. package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
  11. package/dist/commands/core.js +6 -0
  12. package/dist/commands/daemon.d.ts +7 -0
  13. package/dist/commands/daemon.js +37 -3
  14. package/dist/commands/dm.js +9 -2
  15. package/dist/commands/doctor.d.ts +11 -1
  16. package/dist/commands/doctor.js +56 -1
  17. package/dist/commands/install-global.d.ts +7 -0
  18. package/dist/commands/install-global.js +129 -35
  19. package/dist/commands/lanes-shared.d.ts +3 -1
  20. package/dist/commands/lanes.d.ts +4 -4
  21. package/dist/commands/lanes.js +380 -25
  22. package/dist/commands/pack-install.d.ts +2 -2
  23. package/dist/commands/pack-install.js +133 -35
  24. package/dist/commands/sync-manifest.d.ts +2 -9
  25. package/dist/commands/sync-manifest.js +4 -15
  26. package/dist/hook-engine-path.d.ts +4 -0
  27. package/dist/hook-engine-path.js +36 -0
  28. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2783 -2354
  29. package/dist/hq-cloud-sync-runner/package.json +1 -1
  30. package/dist/index.js +6 -0
  31. package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
  32. package/dist/lib/agent-kit/inbox-redact.js +361 -0
  33. package/dist/lib/agent-kit/inbox-state.js +11 -2
  34. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  35. package/dist/lib/agent-kit/run/inbox.js +36 -7
  36. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
  37. package/dist/lib/anywhere-runtime-flag-key.js +9 -0
  38. package/dist/lib/company-skill-wrapper.js +6 -4
  39. package/dist/lib/daemon/install.d.ts +47 -1
  40. package/dist/lib/daemon/install.js +181 -15
  41. package/dist/lib/daemon/lock.d.ts +32 -0
  42. package/dist/lib/daemon/lock.js +55 -2
  43. package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
  44. package/dist/lib/doctor/checks/anywhere.d.ts +3 -0
  45. package/dist/lib/doctor/checks/anywhere.js +23 -8
  46. package/dist/lib/doctor/checks/cli-path.d.ts +11 -0
  47. package/dist/lib/doctor/checks/cli-path.js +45 -0
  48. package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
  49. package/dist/lib/doctor/checks/hook-engine.js +16 -0
  50. package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
  51. package/dist/lib/doctor/checks/sync-health.js +38 -2
  52. package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
  53. package/dist/lib/doctor/checks/temp-homes.js +54 -0
  54. package/dist/lib/doctor/fix/apply.d.ts +90 -1
  55. package/dist/lib/doctor/fix/apply.js +176 -10
  56. package/dist/lib/doctor/fix/json-output.d.ts +4 -1
  57. package/dist/lib/doctor/fix/json-output.js +6 -0
  58. package/dist/lib/doctor/fix/remediation.d.ts +7 -1
  59. package/dist/lib/doctor/fix/remediation.js +23 -1
  60. package/dist/lib/doctor/registry.js +10 -0
  61. package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
  62. package/dist/lib/doctor/temp-home-sweeper.js +418 -0
  63. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  64. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  65. package/dist/lib/flag-registry.js +9 -1
  66. package/dist/lib/flag-snapshot-file.js +8 -3
  67. package/dist/lib/hook-engine/resolve.d.ts +31 -0
  68. package/dist/lib/hook-engine/resolve.js +134 -0
  69. package/dist/lib/lanes/admission-host.d.ts +1 -0
  70. package/dist/lib/lanes/admission-host.js +8 -1
  71. package/dist/lib/lanes/admission.d.ts +4 -0
  72. package/dist/lib/lanes/admission.js +6 -2
  73. package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
  74. package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
  75. package/dist/lib/lanes/index.d.ts +2 -2
  76. package/dist/lib/lanes/index.js +1 -1
  77. package/dist/lib/lanes/keep-alive.js +4 -1
  78. package/dist/lib/lanes/local-host-identity.d.ts +40 -0
  79. package/dist/lib/lanes/local-host-identity.js +314 -0
  80. package/dist/lib/lanes/reaper.d.ts +1 -1
  81. package/dist/lib/lanes/reaper.js +7 -0
  82. package/dist/lib/lanes/reconcile.d.ts +4 -0
  83. package/dist/lib/lanes/reconcile.js +16 -8
  84. package/dist/lib/lanes/spawn.d.ts +20 -0
  85. package/dist/lib/lanes/spawn.js +48 -2
  86. package/dist/lib/lanes/status-fields.js +6 -1
  87. package/dist/lib/lanes/store.js +8 -1
  88. package/dist/lib/lanes/throttle.d.ts +49 -6
  89. package/dist/lib/lanes/throttle.js +154 -17
  90. package/dist/lib/lanes/types.d.ts +22 -1
  91. package/dist/lib/lanes/types.js +27 -3
  92. package/dist/lib/lanes/watcher.js +27 -2
  93. package/dist/lib/lanes/worker-continuity.d.ts +2 -0
  94. package/dist/lib/lanes/worker-continuity.js +18 -11
  95. package/dist/lib/manifest-disabled.d.ts +13 -0
  96. package/dist/lib/manifest-disabled.js +19 -0
  97. package/dist/lib/service-manager/launchd.d.ts +1 -0
  98. package/dist/lib/service-manager/launchd.js +1 -1
  99. package/dist/lib/windows/user-path.d.ts +48 -0
  100. package/dist/lib/windows/user-path.js +114 -0
  101. package/dist/lib/work-context/auto-bind.d.ts +1 -1
  102. package/dist/lib/work-context/auto-bind.js +22 -3
  103. package/dist/lib/work-context/lane-bind.d.ts +31 -0
  104. package/dist/lib/work-context/lane-bind.js +101 -0
  105. package/dist/lib/work-context/organize.js +1 -0
  106. package/dist/lib/work-context/prd-sync.js +7 -0
  107. package/dist/lib/work-context/project.d.ts +36 -10
  108. package/dist/lib/work-context/project.js +130 -41
  109. package/dist/lib/work-context/reconcile.d.ts +6 -0
  110. package/dist/lib/work-context/reconcile.js +25 -5
  111. package/dist/lib/work-context/state.d.ts +6 -0
  112. package/dist/lib/work-context/state.js +29 -1
  113. package/dist/utils/copy-tree.d.ts +61 -0
  114. package/dist/utils/copy-tree.js +249 -0
  115. package/dist/utils/environmental-error.js +7 -3
  116. package/dist/utils/npm-invocation.d.ts +27 -0
  117. package/dist/utils/npm-invocation.js +54 -0
  118. package/dist/utils/safe-link.d.ts +50 -0
  119. package/dist/utils/safe-link.js +300 -0
  120. package/package.json +17 -5
  121. package/scripts/hook-engine-manifest.mjs +22 -0
  122. package/scripts/install-hook-engine.mjs +10 -0
@@ -47,6 +47,7 @@ import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFoc
47
47
  import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
48
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";
49
49
  import { applyContinuitySettings, findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
50
+ import { localHostIdsReferToSameMachine, observeLocalHostIdentity, resolveLocalHostIdentity } from "../lib/lanes/local-host-identity.js";
50
51
  import { commitIdleLoopStop, enqueueLoopEnvelope, interruptLoopQueue, interruptLoopQueueForStory, loopControllerIsAlive, loopLaneIsIdle, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
51
52
  import { LOOP_CONTROLLER_RECOVERY_REASON, LOOP_STALL_RESTART_REASON, runLaneLoopController, startLaneLoopController } from "./lanes-loop.js";
52
53
  import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
@@ -92,7 +93,9 @@ import { LanesError } from "../lib/lanes/errors.js";
92
93
  import { LedgerCorrupt, capacityOptionsFromEnv, readCollectorMark, readNow, rebuildNow, } from "../lib/capacity/ledger.js";
93
94
  import { pickAccount } from "../lib/capacity/route.js";
94
95
  import { capacityViewAge } from "../lib/capacity/freshness.js";
95
- import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
96
+ import { applyTerminalToLane, classifyLaneExit, readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
97
+ import { isSubagentLane, parseLaneEnvelope, parseLaneRunner } from "../lib/lanes/types.js";
98
+ import { nextEnvelopePath } from "../lib/lanes/spawn.js";
96
99
  import { loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
97
100
  import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
98
101
  import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
@@ -101,7 +104,7 @@ import { blockerFreshnessFingerprint, resolveBlockerFreshnessForLane, } from "..
101
104
  import { resolveKrFreshnessForShow } from "../lib/lanes/kr-freshness.js";
102
105
  import { ensureSessionHome } from "../lib/lanes/session-relink.js";
103
106
  import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
104
- import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, spawnEnvelopeRepairRequest, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
107
+ import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, tempHomeRule, spawnLaneWorker, spawnEnvelopeRepairRequest, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
105
108
  import { handOffInstallHere } from "../lib/daemon/handoff.js";
106
109
  export { spawnLaneWorker };
107
110
  export function openPullRequestsForProject(hqRoot, metadata) {
@@ -609,6 +612,7 @@ async function finalizeIdleLoopStop(ctx, lane) {
609
612
  function reconcileHostFromCtx(ctx) {
610
613
  return resolveAdmissionHost({
611
614
  hostname: ctx.hostname,
615
+ homedir: ctx.homedir,
612
616
  readOutpostIdentity: ctx.readOutpostIdentity,
613
617
  });
614
618
  }
@@ -1409,6 +1413,50 @@ function outpostIdentityUnknownError(message, expectedId, details = {}) {
1409
1413
  fix: `Run ${command} from a registered host, or write ${JSON.stringify(`${expected}\n`)} to ${OUTPOST_ID_PATH}.`,
1410
1414
  });
1411
1415
  }
1416
+ function localStableHost(ctx) {
1417
+ const hostname = ctx.hostname().trim();
1418
+ if (!hostname) {
1419
+ throw new LanesError("host_identity_unknown", "Cannot resolve this host for lane placement", {});
1420
+ }
1421
+ const identity = resolveLocalHostIdentity({ homedir: ctx.homedir(), hostname });
1422
+ return { kind: "local", id: identity.id };
1423
+ }
1424
+ /** Read the stable id when one exists. Does not create or update `~/.hq/lane-host.json`. */
1425
+ function recordedLocalHost(ctx) {
1426
+ const hostname = ctx.hostname().trim();
1427
+ if (!hostname) {
1428
+ throw new LanesError("host_identity_unknown", "Cannot resolve this host for lane placement", {});
1429
+ }
1430
+ const observed = observeLocalHostIdentity({ homedir: ctx.homedir(), hostname });
1431
+ if (observed)
1432
+ return { kind: "local", id: observed.id };
1433
+ return { kind: "local", id: hostname };
1434
+ }
1435
+ function observedLocalMatch(ctx, stableId) {
1436
+ const hostname = ctx.hostname().trim();
1437
+ const observed = observeLocalHostIdentity({ homedir: ctx.homedir(), hostname });
1438
+ return {
1439
+ hostname,
1440
+ machineId: observed?.id ?? stableId,
1441
+ ...(observed ? { aliases: observed.aliases } : {}),
1442
+ };
1443
+ }
1444
+ function laneCountsOnHost(lane, host, ctx) {
1445
+ if (lane.host.kind !== host.kind)
1446
+ return false;
1447
+ if (host.kind !== "local")
1448
+ return lane.host.id === host.id;
1449
+ if (lane.host.id === host.id)
1450
+ return true;
1451
+ return localHostIdsReferToSameMachine(lane.host.id, observedLocalMatch(ctx, host.id));
1452
+ }
1453
+ function localLaneContinuesHere(ctx, previous, current) {
1454
+ if (sameLaneHost(previous, current))
1455
+ return true;
1456
+ if (previous.kind !== "local" || current.kind !== "local")
1457
+ return false;
1458
+ return localHostIdsReferToSameMachine(previous.id, observedLocalMatch(ctx, current.id));
1459
+ }
1412
1460
  function laneHostFromCtx(ctx, expectedId) {
1413
1461
  const identity = ctx.readOutpostIdentity();
1414
1462
  if (identity.kind === "unknown") {
@@ -1416,11 +1464,7 @@ function laneHostFromCtx(ctx, expectedId) {
1416
1464
  }
1417
1465
  if (identity.kind === "id")
1418
1466
  return { kind: "outpost", id: identity.id };
1419
- const hostname = ctx.hostname().trim();
1420
- if (!hostname) {
1421
- throw new LanesError("host_identity_unknown", "Cannot resolve this host for lane placement", {});
1422
- }
1423
- return { kind: "local", id: hostname };
1467
+ return recordedLocalHost(ctx);
1424
1468
  }
1425
1469
  async function readOutpostCapacity(ctx, outpostId) {
1426
1470
  return readOutpostCapacityUncached(ctx, outpostId);
@@ -2118,14 +2162,14 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config, options = {})
2118
2162
  });
2119
2163
  const snapshots = [{
2120
2164
  capacity: local,
2121
- lanes: localLanes.filter((lane) => lane.host.kind === localHost.kind && lane.host.id === localHost.id),
2165
+ lanes: localLanes.filter((lane) => laneCountsOnHost(lane, localHost, ctx)),
2122
2166
  laneListAvailable: true,
2123
2167
  }];
2124
2168
  if (localHost.kind === "outpost") {
2125
2169
  const hostname = ctx.hostname().trim();
2126
2170
  const actualLocalHost = { kind: "local", id: hostname };
2127
2171
  const actualLocalLanes = hostname
2128
- ? localLanes.filter((lane) => lane.host.kind === "local" && lane.host.id === hostname)
2172
+ ? localLanes.filter((lane) => laneCountsOnHost(lane, actualLocalHost, ctx))
2129
2173
  : [];
2130
2174
  if (hostname) {
2131
2175
  snapshots.push({
@@ -2759,6 +2803,7 @@ async function applyThrottle(hqRoot, config, ctx, current, provider, pinnedAccou
2759
2803
  last_busy: state.last_busy ?? null,
2760
2804
  busy_source: state.busy_source,
2761
2805
  sampled_at: state.sampled_at,
2806
+ ...(state.memory_hold !== undefined ? { memory_hold: state.memory_hold } : {}),
2762
2807
  };
2763
2808
  if (current.action === "queue")
2764
2809
  return { action: "queue", reason: "throttle", throttle };
@@ -2913,7 +2958,7 @@ export function checkpointInstruction(worker) {
2913
2958
  ...names.map((name) => `- ${name}`),
2914
2959
  ].join("\n");
2915
2960
  }
2916
- export function composeWorkerBrief(worker, taskBrief) {
2961
+ export function composeWorkerBrief(worker, taskBrief, hqRoot) {
2917
2962
  const yaml = worker.workerYaml;
2918
2963
  const name = yaml.worker?.name ?? worker.id;
2919
2964
  const description = yaml.worker?.description ?? worker.description ?? "";
@@ -2968,13 +3013,15 @@ export function composeWorkerBrief(worker, taskBrief) {
2968
3013
  "",
2969
3014
  FOREGROUND_ONLY_RULE,
2970
3015
  "",
3016
+ tempHomeRule(hqRoot),
3017
+ "",
2971
3018
  ].join("\n");
2972
3019
  }
2973
3020
  /**
2974
- * A lane with no worker still carries the standing authorization paragraph
2975
- * and the foreground-only rule.
3021
+ * A lane with no worker still carries the standing authorization paragraph,
3022
+ * the foreground-only rule and the temp-home rule.
2976
3023
  */
2977
- export function composeGeneralBrief(taskBrief) {
3024
+ export function composeGeneralBrief(taskBrief, hqRoot) {
2978
3025
  return [
2979
3026
  taskBrief.replace(/\s+$/, ""),
2980
3027
  "",
@@ -2983,6 +3030,8 @@ export function composeGeneralBrief(taskBrief) {
2983
3030
  "",
2984
3031
  FOREGROUND_ONLY_RULE,
2985
3032
  "",
3033
+ tempHomeRule(hqRoot),
3034
+ "",
2986
3035
  ].join("\n");
2987
3036
  }
2988
3037
  function parseMaxRuntimeSeconds(raw, workerId) {
@@ -3387,6 +3436,9 @@ function mintMessageId() {
3387
3436
  return `msg_${Date.now().toString(36)}_${randomBytes(3).toString("hex")}`;
3388
3437
  }
3389
3438
  async function runCreate(ctx, opts) {
3439
+ if (opts.runner !== undefined && parseLaneRunner(opts.runner) === "subagent") {
3440
+ return runCreateSubagent(ctx, opts);
3441
+ }
3390
3442
  const hqRoot = ctx.hqRoot();
3391
3443
  const requestedHireGrant = opts.hireGrant === undefined
3392
3444
  ? undefined
@@ -3673,6 +3725,7 @@ async function runCreate(ctx, opts) {
3673
3725
  provider,
3674
3726
  company: company ?? meshCompany,
3675
3727
  freshThread: opts.freshThread,
3728
+ runsHere: (lane) => laneRunsHereForOperatorMutation(ctx, lane),
3676
3729
  })
3677
3730
  : { resumable: false };
3678
3731
  const profileDefaultsApply = profile !== undefined && profile.meta.provider === provider;
@@ -3757,14 +3810,21 @@ async function runCreate(ctx, opts) {
3757
3810
  if (placementToApply !== undefined) {
3758
3811
  const currentHost = laneHostFromCtx(ctx);
3759
3812
  if (placementToApply.kind === "local") {
3760
- placementBalanceHostOverride = currentHost;
3761
- placementHostOverride = { kind: "local", id: ctx.hostname() };
3813
+ if (currentHost.kind === "local") {
3814
+ const stable = localStableHost(ctx);
3815
+ placementBalanceHostOverride = stable;
3816
+ placementHostOverride = stable;
3817
+ }
3818
+ else {
3819
+ placementBalanceHostOverride = currentHost;
3820
+ placementHostOverride = localStableHost(ctx);
3821
+ }
3762
3822
  outpostId = undefined;
3763
3823
  }
3764
3824
  else if (placementToApply.kind === "outpost") {
3765
3825
  if (currentHost.kind === "outpost" && currentHost.id === placementToApply.id) {
3766
3826
  placementBalanceHostOverride = currentHost;
3767
- placementHostOverride = { kind: "local", id: ctx.hostname() };
3827
+ placementHostOverride = localStableHost(ctx);
3768
3828
  outpostId = undefined;
3769
3829
  }
3770
3830
  else {
@@ -3803,7 +3863,7 @@ async function runCreate(ctx, opts) {
3803
3863
  if (!localHostname) {
3804
3864
  throw new LanesError("host_identity_unknown", "Cannot resolve this host for automatic lane placement", {});
3805
3865
  }
3806
- const localHost = { kind: "local", id: localHostname };
3866
+ const localHost = localStableHost(ctx);
3807
3867
  let chosenHostIsCaller = chosenHost.id === currentHost.id ||
3808
3868
  sanitizeHostComponent(chosenHost.id) === sanitizeHostComponent(localHostname);
3809
3869
  if (!chosenHostIsCaller && chosenHost.kind === "outpost") {
@@ -4021,12 +4081,18 @@ async function runCreate(ctx, opts) {
4021
4081
  const allowedAccounts = validatedAccounts.size > 0 ? [...validatedAccounts] : profileAccounts;
4022
4082
  let accountSelection;
4023
4083
  if (provider === "claude") {
4084
+ const persistedLocalHost = outpostId === undefined && placementHostOverride === undefined
4085
+ ? localStableHost(ctx)
4086
+ : undefined;
4087
+ // Balance against this process. On an Outpost, laneHostFromCtx is that
4088
+ // Outpost id. The stable local id is only what a lane stores when it
4089
+ // spawns here without --outpost.
4024
4090
  const targetHost = placementBalanceHostOverride ?? placementHostOverride ?? (outpostId !== undefined
4025
4091
  ? { kind: "outpost", id: outpostId }
4026
4092
  : laneHostFromCtx(ctx));
4027
4093
  const actualTargetHost = placementHostOverride ?? (outpostId !== undefined
4028
4094
  ? { kind: "outpost", id: outpostId }
4029
- : { kind: "local", id: ctx.hostname() });
4095
+ : persistedLocalHost);
4030
4096
  balanceTargetHost = targetHost;
4031
4097
  balanceHostLoad = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
4032
4098
  const selected = selectClaudeAccount({
@@ -4059,7 +4125,7 @@ async function runCreate(ctx, opts) {
4059
4125
  const persist = async () => {
4060
4126
  const host = placementHostOverride ?? (outpostId !== undefined
4061
4127
  ? { kind: "outpost", id: outpostId }
4062
- : { kind: "local", id: ctx.hostname() });
4128
+ : localStableHost(ctx));
4063
4129
  if (opts.loop === true && host.kind === "outpost") {
4064
4130
  throw new LanesError("loop_lane_outpost_unsupported", "Loop lanes currently require the local host", { outpost_id: host.id });
4065
4131
  }
@@ -4075,9 +4141,10 @@ async function runCreate(ctx, opts) {
4075
4141
  provider,
4076
4142
  company: company ?? meshCompany,
4077
4143
  freshThread: opts.freshThread,
4144
+ runsHere: (lane) => laneRunsHereForOperatorMutation(ctx, lane),
4078
4145
  });
4079
4146
  if (workerContinuity.resumable && workerContinuity.previous &&
4080
- !sameLaneHost(workerContinuity.previous.host, host)) {
4147
+ !localLaneContinuesHere(ctx, workerContinuity.previous.host, host)) {
4081
4148
  workerContinuity = { previous: workerContinuity.previous, resumable: false, fallbackReason: "host_mismatch" };
4082
4149
  }
4083
4150
  }
@@ -4087,7 +4154,7 @@ async function runCreate(ctx, opts) {
4087
4154
  balanceHostLoad !== undefined && balanceTargetHost !== undefined) {
4088
4155
  const localHost = laneHostFromCtx(ctx);
4089
4156
  if (localHost.kind === balanceTargetHost.kind && localHost.id === balanceTargetHost.id) {
4090
- const localLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === localHost.kind && lane.host.id === localHost.id);
4157
+ const localLanes = loadAllLanes(hqRoot).filter((lane) => laneCountsOnHost(lane, localHost, ctx));
4091
4158
  const refreshedLocal = {
4092
4159
  capacity: buildHostCapacitySnapshot({
4093
4160
  host: localHost,
@@ -4107,7 +4174,7 @@ async function runCreate(ctx, opts) {
4107
4174
  : host);
4108
4175
  const actualTargetHostForPersist = placementHostOverride ?? (outpostId !== undefined
4109
4176
  ? { kind: "outpost", id: outpostId }
4110
- : { kind: "local", id: ctx.hostname() });
4177
+ : localStableHost(ctx));
4111
4178
  const selected = selectClaudeAccount({
4112
4179
  hosts,
4113
4180
  targetHost: balanceTargetHost,
@@ -4412,8 +4479,8 @@ async function runCreate(ctx, opts) {
4412
4479
  const briefText = `${workerContinuity.resumable && workerContinuity.previous
4413
4480
  ? workerContinuityPrompt(workerContinuity.previous, previousResult?.envelope.summary, previousResult?.ref)
4414
4481
  : ""}${resolvedWorker
4415
- ? composeWorkerBrief(resolvedWorker, taskBrief)
4416
- : composeGeneralBrief(taskBrief)}`;
4482
+ ? composeWorkerBrief(resolvedWorker, taskBrief, hqRoot)
4483
+ : composeGeneralBrief(taskBrief, hqRoot)}`;
4417
4484
  fs.writeFileSync(path.join(dest, "brief.md"), briefText);
4418
4485
  if (workerContinuity.resumable && lane.pending_resume) {
4419
4486
  const newBriefPath = path.join(dest, "brief.md");
@@ -7158,6 +7225,7 @@ async function runAdmit(ctx, laneId, opts) {
7158
7225
  last_busy: state.last_busy ?? null,
7159
7226
  busy_source: state.busy_source,
7160
7227
  sampled_at: state.sampled_at,
7228
+ ...(state.memory_hold !== undefined ? { memory_hold: state.memory_hold } : {}),
7161
7229
  };
7162
7230
  }
7163
7231
  else if (hasEarlierEligibleQueuedLane(lanes, lane, config, host, (row) => row.worker_profile === undefined
@@ -8810,6 +8878,278 @@ async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false, behavio
8810
8878
  },
8811
8879
  });
8812
8880
  }
8881
+ /**
8882
+ * Register a lane that an in-session subagent runs. The record carries the
8883
+ * same attribution as a process lane (company, project, story, worker, brief,
8884
+ * senior, model) so it shows on the lanes board, but admission and capacity
8885
+ * are skipped and no provider process or watcher starts. The lane is
8886
+ * `running` from creation; `hq lanes progress` reports lines and the result.
8887
+ */
8888
+ async function runCreateSubagent(ctx, opts) {
8889
+ const unsupported = [
8890
+ ["keepAlive", "--keep-alive"],
8891
+ ["loop", "--loop"],
8892
+ ["remoteControl", "--remote-control"],
8893
+ ["title", "--title"],
8894
+ ["account", "--account"],
8895
+ ["outpost", "--outpost"],
8896
+ ["placement", "--placement"],
8897
+ ["defaultPlacement", "--default-placement"],
8898
+ ["freshThread", "--fresh-thread"],
8899
+ ["hireGrant", "--hire-grant"],
8900
+ ["timeout", "--timeout"],
8901
+ ["allowExtraUsage", "--allow-extra-usage"],
8902
+ ];
8903
+ for (const [key, flag] of unsupported) {
8904
+ if (opts[key] !== undefined && opts[key] !== false) {
8905
+ throw new LanesError("subagent_runner_option_unsupported", `${flag} is not supported with --runner subagent; the subagent runs inside its senior session`, { option: flag });
8906
+ }
8907
+ }
8908
+ const hqRoot = ctx.hqRoot();
8909
+ const project = opts.project?.trim();
8910
+ const story = opts.story?.trim();
8911
+ const noStoryReason = opts.noStory?.trim();
8912
+ const additionalStories = (opts.additionalStory ?? []).map((value) => value.trim());
8913
+ const briefFile = opts.briefFile?.trim();
8914
+ if (!project)
8915
+ throw new LanesError("missing_project", "--project is required", {});
8916
+ if (opts.story !== undefined && opts.noStory !== undefined) {
8917
+ throw new LanesError("story_selection_conflict", "Pass either --story or --no-story, not both", {});
8918
+ }
8919
+ if (opts.noStory !== undefined && !noStoryReason) {
8920
+ throw new LanesError("missing_no_story_reason", "--no-story requires a non-empty reason", {});
8921
+ }
8922
+ if (!story && !noStoryReason)
8923
+ throw new LanesError("missing_story", "--story is required", {});
8924
+ if (noStoryReason && additionalStories.length > 0) {
8925
+ throw new LanesError("story_selection_conflict", "--additional-story requires --story and cannot be used with --no-story", {});
8926
+ }
8927
+ if (additionalStories.some((value) => value.length === 0)) {
8928
+ throw new LanesError("missing_story", "--additional-story values must be non-empty", {});
8929
+ }
8930
+ const storyIds = story ? [story, ...additionalStories] : [];
8931
+ if (new Set(storyIds).size !== storyIds.length) {
8932
+ throw new LanesError("duplicate_story", "A lane cannot carry the same project/story more than once", {
8933
+ project_id: project,
8934
+ stories: storyIds,
8935
+ });
8936
+ }
8937
+ if (!briefFile)
8938
+ throw new LanesError("missing_brief_file", "--brief-file is required", {});
8939
+ if (opts.focusOverride !== undefined) {
8940
+ if (!opts.focusOverride.trim()) {
8941
+ throw new LanesError("focus_override_reason_required", "--focus-override requires a non-empty reason", {});
8942
+ }
8943
+ assertNoSecretText("focus override reason", opts.focusOverride.trim());
8944
+ }
8945
+ const provider = opts.provider ? parseLaneProvider(opts.provider) : "claude";
8946
+ if (provider !== "claude") {
8947
+ throw new LanesError("subagent_runner_requires_claude", "--runner subagent records a Claude subagent; use --provider claude or omit it", { provider });
8948
+ }
8949
+ if (ctx.env.HQ_LANE_ID?.trim()) {
8950
+ throw new LanesError("subagent_runner_requires_session", "A subagent lane belongs to the session that runs the subagent; a lane worker cannot register one", { caller_lane_id: ctx.env.HQ_LANE_ID.trim() });
8951
+ }
8952
+ const senior = opts.senior ? parseSeniorRef(opts.senior) : defaultSenior(ctx);
8953
+ if (senior.kind === "lane") {
8954
+ throw new LanesError("subagent_runner_requires_session", "A subagent lane's senior must be the session running the subagent (session:<id>) or user", { senior });
8955
+ }
8956
+ assertSeniorExists(hqRoot, senior, ctx);
8957
+ let company = opts.company?.trim() ? await ctx.resolveCompany(opts.company.trim()) : null;
8958
+ const seniorCompany = companyOfSenior(hqRoot, senior, ctx);
8959
+ if (company === null && seniorCompany.binding === "company") {
8960
+ const seniorCompanyIdentifier = seniorCompany.slug ?? (seniorCompany.uid
8961
+ ? canonicalCompanySlugForUid(hqRoot, seniorCompany.uid, senior)
8962
+ : undefined);
8963
+ if (seniorCompanyIdentifier)
8964
+ company = await ctx.resolveCompany(seniorCompanyIdentifier);
8965
+ }
8966
+ let meshCompany = null;
8967
+ if (opts.meshCompany?.trim()) {
8968
+ meshCompany = await ctx.resolveCompany(opts.meshCompany.trim());
8969
+ if (company && (company.uid !== meshCompany.uid || company.slug !== meshCompany.slug)) {
8970
+ throw new LanesError("mesh_company_mismatch", "--mesh-company must match --company when both are provided", {
8971
+ company: company.slug,
8972
+ mesh_company: meshCompany.slug,
8973
+ });
8974
+ }
8975
+ }
8976
+ const depthCap = laneDepthCap(hqRoot, company?.slug);
8977
+ const laneDepth = derivedLaneDepth(hqRoot, senior, ctx);
8978
+ if (laneDepth > depthCap.max) {
8979
+ throw maxLaneDepthError(laneDepth, seniorLaneChain(hqRoot, senior, ctx), `Creating lane at depth ${laneDepth} is refused`, depthCap);
8980
+ }
8981
+ const resolvedWorker = opts.worker !== undefined
8982
+ ? resolveLaneWorker(hqRoot, opts.worker, company?.slug)
8983
+ : undefined;
8984
+ const cliModel = parseCliModel(opts.model);
8985
+ const cliEffort = parseCliEffort(provider, opts.effort);
8986
+ const priority = Number.parseInt(opts.priority ?? "1", 10);
8987
+ if (!Number.isInteger(priority)) {
8988
+ throw new LanesError("invalid_priority", "--priority must be an integer", { value: opts.priority });
8989
+ }
8990
+ const briefAbs = resolveCallerPath(ctx.callerCwd(), briefFile);
8991
+ if (!ctx.existsSync(briefAbs)) {
8992
+ throw new LanesError("brief_not_found", `Brief file not found: ${briefAbs}`, { path: briefAbs });
8993
+ }
8994
+ const taskBrief = fs.readFileSync(briefAbs, "utf8");
8995
+ const createdAt = ctx.now().toISOString();
8996
+ const lane = createLane(hqRoot, {
8997
+ company,
8998
+ ...(company === null && meshCompany ? { mesh_company: meshCompany } : {}),
8999
+ project_id: project,
9000
+ story_id: story ?? "",
9001
+ ...(noStoryReason ? { no_story_reason: noStoryReason } : {}),
9002
+ stories: storyIds.map((storyId) => ({
9003
+ project_id: project,
9004
+ story_id: storyId,
9005
+ status: "in_progress",
9006
+ added_at: createdAt,
9007
+ updated_at: createdAt,
9008
+ })),
9009
+ depth: laneDepth,
9010
+ senior,
9011
+ hire_grant: 0,
9012
+ senior_history: [],
9013
+ state: "running",
9014
+ priority,
9015
+ ...(resolvedWorker ? { worker: resolvedWorker.id, worker_path: resolvedWorker.path } : {}),
9016
+ ...(resolvedWorker?.pack ? { worker_pack: resolvedWorker.pack } : {}),
9017
+ keep_alive: false,
9018
+ provider,
9019
+ runner: "subagent",
9020
+ provider_source: opts.provider ? "cli" : "default",
9021
+ ...(cliModel !== undefined ? { model: cliModel } : {}),
9022
+ model_source: cliModel !== undefined ? "cli" : "none",
9023
+ ...(cliEffort !== undefined ? { effort: cliEffort } : {}),
9024
+ effort_source: cliEffort !== undefined ? "cli" : "none",
9025
+ host: { kind: "local", id: ctx.hostname() },
9026
+ attribution_history: [],
9027
+ created_at: createdAt,
9028
+ updated_at: createdAt,
9029
+ });
9030
+ const dest = runDir(hqRoot, lane.lane_id);
9031
+ ensureDir(path.join(dest, "logs"));
9032
+ ensureDir(path.join(dest, "envelopes"));
9033
+ ensureDir(path.join(dest, "questions"));
9034
+ fs.writeFileSync(path.join(dest, "brief.md"), taskBrief);
9035
+ if (!opts.quiet)
9036
+ printJson({
9037
+ ok: true,
9038
+ action: "create",
9039
+ admitted: [],
9040
+ lane_id: lane.lane_id,
9041
+ state: lane.state,
9042
+ runner: "subagent",
9043
+ queued_reason: null,
9044
+ priority: lane.priority,
9045
+ provider: lane.provider,
9046
+ account: null,
9047
+ model: lane.model ?? null,
9048
+ effort: lane.effort ?? null,
9049
+ effort_source: lane.effort_source ?? null,
9050
+ host: lane.host,
9051
+ company: lane.company,
9052
+ project_id: lane.project_id,
9053
+ story_id: lane.story_id,
9054
+ stories: laneStories(lane),
9055
+ thread_id: null,
9056
+ senior: lane.senior,
9057
+ depth: lane.depth,
9058
+ timeout_secs: null,
9059
+ lane,
9060
+ });
9061
+ return lane;
9062
+ }
9063
+ /** Longest progress line kept; `hq lanes list` clips its last line further. */
9064
+ const SUBAGENT_PROGRESS_LINE_MAX = 500;
9065
+ /**
9066
+ * Report on a subagent lane: append a progress line, record the final
9067
+ * envelope (state follows its decision, as for a process lane), or mark it
9068
+ * failed when the subagent ended without one.
9069
+ */
9070
+ async function runProgress(ctx, laneId, opts) {
9071
+ const hqRoot = ctx.hqRoot();
9072
+ const line = opts.line?.replace(/\s+/g, " ").trim();
9073
+ const failedReason = opts.failed?.replace(/\s+/g, " ").trim();
9074
+ if (opts.line !== undefined && !line)
9075
+ throw new LanesError("progress_line_empty", "--line must not be empty", {});
9076
+ if (opts.failed !== undefined && !failedReason)
9077
+ throw new LanesError("progress_failed_reason_empty", "--failed requires a non-empty reason", {});
9078
+ if (opts.envelope !== undefined && opts.failed !== undefined) {
9079
+ throw new LanesError("progress_result_conflict", "Pass either --envelope or --failed, not both", {});
9080
+ }
9081
+ if (line === undefined && opts.envelope === undefined && failedReason === undefined) {
9082
+ throw new LanesError("progress_nothing_to_report", "Pass --line, --envelope, or --failed", {});
9083
+ }
9084
+ const lane = readLane(hqRoot, laneId);
9085
+ if (!isSubagentLane(lane)) {
9086
+ throw new LanesError("lane_not_subagent", `Lane ${lane.lane_id} runs as an HQ process; its watcher reports progress`, { lane_id: lane.lane_id });
9087
+ }
9088
+ if (isTerminalLaneState(lane.state)) {
9089
+ throw new LanesError("already_terminal", `Lane ${lane.lane_id} is already ${lane.state}`, { lane_id: lane.lane_id, state: lane.state });
9090
+ }
9091
+ const dest = runDir(hqRoot, lane.lane_id);
9092
+ const appendLine = (text) => {
9093
+ const clipped = text.length > SUBAGENT_PROGRESS_LINE_MAX ? `${text.slice(0, SUBAGENT_PROGRESS_LINE_MAX - 1)}…` : text;
9094
+ ensureDir(path.join(dest, "logs"));
9095
+ fs.appendFileSync(path.join(dest, "logs", "last-message.txt"), `${clipped}\n`);
9096
+ };
9097
+ if (line !== undefined) {
9098
+ assertNoSecretText("progress line", line);
9099
+ appendLine(line);
9100
+ }
9101
+ if (failedReason !== undefined)
9102
+ assertNoSecretText("failure reason", failedReason);
9103
+ const at = ctx.now().toISOString();
9104
+ let updated = lane;
9105
+ let envelopeRef;
9106
+ if (opts.envelope !== undefined) {
9107
+ const envelopeAbs = resolveCallerPath(ctx.callerCwd(), opts.envelope);
9108
+ let text;
9109
+ try {
9110
+ text = fs.readFileSync(envelopeAbs, "utf8");
9111
+ }
9112
+ catch (err) {
9113
+ throw new LanesError("envelope_not_found", `Envelope file not readable: ${envelopeAbs}`, {
9114
+ path: envelopeAbs,
9115
+ error: err instanceof Error ? err.message : String(err),
9116
+ });
9117
+ }
9118
+ let envelope;
9119
+ try {
9120
+ envelope = parseLaneEnvelope(JSON.parse(text), { storyBound: lane.no_story_reason === undefined });
9121
+ }
9122
+ catch (err) {
9123
+ throw new LanesError("envelope_invalid", `Envelope is not a valid hq-lane-envelope.v1: ${err instanceof Error ? err.message : String(err)}`, {
9124
+ path: envelopeAbs,
9125
+ });
9126
+ }
9127
+ ensureDir(path.join(dest, "envelopes"));
9128
+ const target = nextEnvelopePath(dest);
9129
+ fs.writeFileSync(target, text.endsWith("\n") ? text : `${text}\n`);
9130
+ envelopeRef = path.posix.join("envelopes", path.basename(target));
9131
+ if (envelope.summary)
9132
+ appendLine(envelope.summary.replace(/\s+/g, " ").trim());
9133
+ updated = applyTerminalToLane(hqRoot, lane.lane_id, classifyLaneExit({ envelope, hasPartialOutput: false }), {
9134
+ envelope,
9135
+ envelopeRef,
9136
+ at,
9137
+ });
9138
+ }
9139
+ else if (failedReason !== undefined) {
9140
+ appendLine(failedReason);
9141
+ updated = applyTerminalToLane(hqRoot, lane.lane_id, { state: "failed", reason: "subagent_failed" }, { at });
9142
+ }
9143
+ printJson({
9144
+ ok: true,
9145
+ action: "progress",
9146
+ lane_id: updated.lane_id,
9147
+ state: updated.state,
9148
+ ...(line !== undefined ? { line } : {}),
9149
+ ...(envelopeRef !== undefined ? { envelope_ref: envelopeRef } : {}),
9150
+ ...(updated.terminal_reason ? { terminal_reason: updated.terminal_reason } : {}),
9151
+ });
9152
+ }
8813
9153
  async function runStop(ctx, laneId, reason, quiet = false) {
8814
9154
  const operatorReason = reason?.trim();
8815
9155
  if (reason !== undefined && !operatorReason) {
@@ -9914,7 +10254,11 @@ export function registerLanesCommand(parent, deps = {}) {
9914
10254
  registerLaneAutopilotReportCommand(lanes, {
9915
10255
  hqRoot: ctx.hqRoot,
9916
10256
  now: ctx.now,
9917
- currentHost: () => resolveAdmissionHost({ hostname: ctx.hostname, readOutpostIdentity: ctx.readOutpostIdentity }),
10257
+ currentHost: () => resolveAdmissionHost({
10258
+ hostname: ctx.hostname,
10259
+ homedir: ctx.homedir,
10260
+ readOutpostIdentity: ctx.readOutpostIdentity,
10261
+ }),
9918
10262
  }, wrap);
9919
10263
  const noStoryOption = new Option("--no-story <reason>", "Create a lane without a story and record why");
9920
10264
  noStoryOption.attributeName = () => "noStory";
@@ -9933,6 +10277,7 @@ export function registerLanesCommand(parent, deps = {}) {
9933
10277
  .option("--fresh-thread", "Skip worker continuity and start a cold provider session")
9934
10278
  .option("--loop", "Keep one worker lane registered to process queued phase envelopes")
9935
10279
  .option("--provider <name>", "claude | codex | grok")
10280
+ .option("--runner <kind>", "process (default) | subagent: register a lane an in-session Claude subagent runs; skips admission and starts no process")
9936
10281
  .option("--remote-control", "Open a Claude lane in Remote Control using account a for login and the selected account for inference")
9937
10282
  .option("--title <text>", "Human-readable Claude Remote Control session title; required with --remote-control")
9938
10283
  .option("--account <id>", "Provider account id, or 'auto' to pick the account with the most room (all accounts are one pool across companies)")
@@ -9950,6 +10295,16 @@ export function registerLanesCommand(parent, deps = {}) {
9950
10295
  .action(wrap(async (opts) => {
9951
10296
  await withCliFeatureUsed("lane_create", () => runCreate(ctx, opts));
9952
10297
  }));
10298
+ lanes
10299
+ .command("progress")
10300
+ .description("Report on a subagent lane: a progress line, the final envelope, or a failure")
10301
+ .argument("<lane-id>", "Lane id")
10302
+ .option("--line <text>", "Progress line shown as the lane's last line")
10303
+ .option("--envelope <path>", "Final hq-lane-envelope.v1 file; the lane state follows its decision")
10304
+ .option("--failed <reason>", "Mark the lane failed when the subagent ended without an envelope")
10305
+ .action(wrap(async (laneId, opts) => {
10306
+ await runProgress(ctx, laneId, opts);
10307
+ }));
9953
10308
  lanes
9954
10309
  .command("approve-target", { hidden: true })
9955
10310
  .argument("<lane-id>", "Lane id")
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * Flow:
24
24
  * 1. Classify source → transport
25
- * 2. Fetch payload to tmpdir (npm pack | git clone | rsync)
25
+ * 2. Fetch payload to tmpdir (npm pack | git clone | local copy)
26
26
  * 3. Parse + validate package.yaml (10 checks from spec)
27
27
  * 4. Evaluate `conditional` predicate — skip if exits non-zero
28
28
  * 5. Confirm hooks if `contributes.hooks` non-empty (unless --allow-hooks)
@@ -438,7 +438,7 @@ export interface InstallPackOptions {
438
438
  * Route this function's human output to stderr (and silence scan-packages
439
439
  * stdout) so a caller emitting machine-readable JSON keeps stdout clean.
440
440
  * The fetch tools are already quiet/stderr-only (`npm pack --silent`,
441
- * `rsync -a`, `git clone` progress -> stderr), so this is sufficient.
441
+ * `git clone` progress -> stderr), so this is sufficient.
442
442
  */
443
443
  quiet?: boolean;
444
444
  /**