@indigoai-us/hq-cli 5.166.0 → 5.168.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 (40) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/command-catalog.generated.d.ts +88 -1
  3. package/dist/command-catalog.generated.js +112 -1
  4. package/dist/commands/daemon.d.ts +2 -0
  5. package/dist/commands/daemon.js +79 -22
  6. package/dist/commands/lanes.js +269 -28
  7. package/dist/commands/mesh.js +13 -19
  8. package/dist/commands/outpost-pruner.d.ts +3 -0
  9. package/dist/commands/outpost-pruner.js +125 -0
  10. package/dist/lib/daemon/catalog.d.ts +11 -1
  11. package/dist/lib/daemon/catalog.js +31 -6
  12. package/dist/lib/daemon/install.d.ts +7 -0
  13. package/dist/lib/daemon/install.js +18 -2
  14. package/dist/lib/daemon/legacy.js +6 -1
  15. package/dist/lib/daemon/machine-type.d.ts +4 -1
  16. package/dist/lib/daemon/machine-type.js +29 -9
  17. package/dist/lib/daemon/migrate.js +2 -2
  18. package/dist/lib/daemon/remote-control.d.ts +32 -9
  19. package/dist/lib/daemon/remote-control.js +55 -11
  20. package/dist/lib/daemon/run.d.ts +6 -0
  21. package/dist/lib/daemon/run.js +43 -2
  22. package/dist/lib/daemon/tools.d.ts +52 -0
  23. package/dist/lib/daemon/tools.js +95 -0
  24. package/dist/lib/daemon/updater.d.ts +23 -15
  25. package/dist/lib/daemon/updater.js +97 -60
  26. package/dist/lib/lanes/index.d.ts +1 -1
  27. package/dist/lib/lanes/index.js +1 -1
  28. package/dist/lib/lanes/outpost-dispatch.d.ts +1 -1
  29. package/dist/lib/lanes/outpost-pruner-install.d.ts +37 -0
  30. package/dist/lib/lanes/outpost-pruner-install.js +226 -0
  31. package/dist/lib/lanes/outpost-worktree-pruner-flag.d.ts +19 -0
  32. package/dist/lib/lanes/outpost-worktree-pruner-flag.js +76 -0
  33. package/dist/lib/lanes/outpost-worktree-pruner.d.ts +60 -0
  34. package/dist/lib/lanes/outpost-worktree-pruner.js +1135 -0
  35. package/dist/lib/lanes/store.d.ts +6 -0
  36. package/dist/lib/lanes/store.js +66 -1
  37. package/dist/lib/mesh/live/session-events-client.js +3 -2
  38. package/dist/lib/mesh/project-board.d.ts +7 -0
  39. package/dist/lib/mesh/project-board.js +29 -1
  40. package/package.json +1 -1
@@ -10,6 +10,7 @@ import { randomBytes } from "node:crypto";
10
10
  import * as fs from "node:fs";
11
11
  import * as os from "node:os";
12
12
  import * as path from "node:path";
13
+ import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
13
14
  import { ensureCognitoToken } from "../utils/cognito-session.js";
14
15
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
15
16
  import { resolveProcessFlagGate } from "../lib/flag-registry.js";
@@ -23,7 +24,7 @@ import { vaultApiFetch } from "../utils/vault-api.js";
23
24
  import { openMeshTransport } from "../lib/mesh/client.js";
24
25
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
25
26
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
26
- import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
27
+ import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, withLaneAdmissionLock, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
27
28
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
28
29
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
29
30
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
@@ -1119,7 +1120,7 @@ async function assessLaneAdmission(ctx, lane, options) {
1119
1120
  return admission;
1120
1121
  }
1121
1122
  function assertLaneRunsHereForAdmission(ctx, lane) {
1122
- if (laneRunsHere(lane, reconcileHostFromCtx(ctx)))
1123
+ if (laneRunsHereForOperatorMutation(ctx, lane))
1123
1124
  return;
1124
1125
  throw new LanesError("lane_remote", `Lane ${lane.lane_id} is hosted remotely; run the admission command on its Outpost`, { lane_id: lane.lane_id, host: lane.host });
1125
1126
  }
@@ -2382,6 +2383,16 @@ function laneIsHostedHere(ctx, lane) {
2382
2383
  }
2383
2384
  return identity.kind === "id" && identity.id === lane.host.id;
2384
2385
  }
2386
+ function laneRunsHereForOperatorMutation(ctx, lane) {
2387
+ if (lane.host.kind === "outpost")
2388
+ return laneIsHostedHere(ctx, lane);
2389
+ return laneRunsHere(lane, reconcileHostFromCtx(ctx));
2390
+ }
2391
+ function assertLaneRunsHereForOperatorMutation(ctx, lane, action) {
2392
+ if (laneRunsHereForOperatorMutation(ctx, lane))
2393
+ return;
2394
+ throw new LanesError("lane_remote", `Cannot ${action} lane ${lane.lane_id} from this host; its state is owned by ${lane.host.kind}:${lane.host.id}`, { lane_id: lane.lane_id, host: lane.host, action });
2395
+ }
2385
2396
  async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
2386
2397
  if (lane.host.kind !== "outpost") {
2387
2398
  throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch to`, { lane_id: lane.lane_id });
@@ -2896,8 +2907,7 @@ async function runMove(ctx, laneId, opts) {
2896
2907
  moved = await dispatchRemoteLaneMove(ctx, source, choice.host, choice.account);
2897
2908
  }
2898
2909
  else {
2899
- const currentHost = laneHostFromCtx(ctx);
2900
- throw new LanesError("lane_move_wrong_host", `Move lane ${laneId} from its current host (${source.host.kind}:${source.host.id})`, { lane_id: laneId, lane_host: source.host, current_host: currentHost });
2910
+ throw new LanesError("lane_remote", `Cannot move lane ${laneId} from this host; its state is owned by ${source.host.kind}:${source.host.id}`, { lane_id: laneId, host: source.host, action: "move" });
2901
2911
  }
2902
2912
  printJson({
2903
2913
  ok: true,
@@ -2912,12 +2922,8 @@ async function runMove(ctx, laneId, opts) {
2912
2922
  async function commitLaneMove(ctx, laneId, target, account) {
2913
2923
  assertPlacementEnabled(ctx);
2914
2924
  const hqRoot = ctx.hqRoot();
2915
- const sourceHost = laneHostFromCtx(ctx);
2916
- const admissionHost = reconcileHostFromCtx(ctx);
2917
2925
  const before = readLane(hqRoot, laneId);
2918
- if (!laneRunsHere(before, admissionHost)) {
2919
- throw new LanesError("lane_move_wrong_host", `Move lane ${laneId} from its current host (${before.host.kind}:${before.host.id})`, { lane_id: laneId, lane_host: before.host, current_host: sourceHost });
2920
- }
2926
+ assertLaneRunsHereForOperatorMutation(ctx, before, "move-target");
2921
2927
  if (before.host.kind === target.kind && before.host.id === target.id) {
2922
2928
  throw new LanesError("lane_move_same_host", `Lane ${laneId} is already on ${target.kind}:${target.id}`, {
2923
2929
  lane_id: laneId,
@@ -2971,12 +2977,7 @@ async function commitLaneMove(ctx, laneId, target, account) {
2971
2977
  const timeout = target.kind === "outpost" ? readLaneTimeout(hqRoot, laneId) : undefined;
2972
2978
  const moved = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
2973
2979
  const current = readLane(hqRoot, laneId);
2974
- if (!laneRunsHere(current, admissionHost)) {
2975
- throw new LanesError("lane_move_conflict", `Lane ${laneId} changed hosts before the move`, {
2976
- lane_id: laneId,
2977
- host: current.host,
2978
- });
2979
- }
2980
+ assertLaneRunsHereForOperatorMutation(ctx, current, "move-target");
2980
2981
  if (!laneCanMoveBeforeStart(current)) {
2981
2982
  throw new LanesError(current.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} started before the move could commit`, { lane_id: laneId, state: current.state });
2982
2983
  }
@@ -3216,17 +3217,15 @@ async function runAdmit(ctx, laneId, opts) {
3216
3217
  state: lane.state,
3217
3218
  });
3218
3219
  }
3220
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "admit");
3219
3221
  const host = reconcileHostFromCtx(ctx);
3220
- if (!laneRunsHere(lane, host)) {
3221
- printJson({ ok: true, action: "admit", lane_id: laneId, admitted: false, blocked_by: "remote", lane });
3222
- return;
3223
- }
3224
3222
  if (lane.pickup_validation_pending === true) {
3225
3223
  printJson({ ok: true, action: "admit", lane_id: laneId, admitted: false, blocked_by: "pickup_validation", lane });
3226
3224
  return;
3227
3225
  }
3228
3226
  await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
3229
3227
  lane = readLane(hqRoot, laneId);
3228
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "admit");
3230
3229
  if (lane.state !== "queued") {
3231
3230
  throw new LanesError("lane_not_queued", `Lane ${laneId} changed to ${lane.state} before admission`, {
3232
3231
  lane_id: laneId,
@@ -3572,6 +3571,7 @@ async function runMessage(ctx, laneId, opts) {
3572
3571
  await dispatchOutpostLaneCommand(ctx, lane, "message", opts);
3573
3572
  return;
3574
3573
  }
3574
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "message");
3575
3575
  lane = laneWithRecoveredProviderSession(hqRoot, lane);
3576
3576
  // An account move is refused on its own terms first: a running lane and a
3577
3577
  // provider without session resume get the move's named errors, not the
@@ -3847,6 +3847,7 @@ async function runResume(ctx, laneId, opts) {
3847
3847
  await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
3848
3848
  return;
3849
3849
  }
3850
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "resume");
3850
3851
  lane = laneWithRecoveredProviderSession(hqRoot, lane);
3851
3852
  if (opts.model !== undefined && opts.account === undefined) {
3852
3853
  throw new LanesError("model_requires_account", "--model on hq lanes resume is only accepted together with --account", {});
@@ -4223,6 +4224,7 @@ async function runStoryList(ctx, laneId) {
4223
4224
  async function runStoryAdd(ctx, laneId, storyId, opts) {
4224
4225
  const hqRoot = ctx.hqRoot();
4225
4226
  let lane = readLane(hqRoot, laneId);
4227
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
4226
4228
  const projectId = opts.project?.trim() || lane.project_id;
4227
4229
  if (!projectId)
4228
4230
  throw new LanesError("missing_project", "--project is required", {});
@@ -4232,7 +4234,10 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
4232
4234
  const at = ctx.now().toISOString();
4233
4235
  lane = await ensureStoryThread(ctx, lane, projectId);
4234
4236
  const before = lane;
4235
- const next = casUpdateLane(hqRoot, laneId, (current) => addCarriedStory(current, projectId, normalizedStoryId, at));
4237
+ const next = casUpdateLane(hqRoot, laneId, (current) => {
4238
+ assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
4239
+ return addCarriedStory(current, projectId, normalizedStoryId, at);
4240
+ });
4236
4241
  await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress");
4237
4242
  const finalLane = readLane(hqRoot, laneId);
4238
4243
  printJson({
@@ -4249,6 +4254,7 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
4249
4254
  async function runStoryStart(ctx, laneId, storyId, opts) {
4250
4255
  const hqRoot = ctx.hqRoot();
4251
4256
  let lane = readLane(hqRoot, laneId);
4257
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
4252
4258
  const target = storyCommandTarget(lane, storyId, opts.project);
4253
4259
  if (target.story.status === "done") {
4254
4260
  throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be started again`, {
@@ -4260,7 +4266,10 @@ async function runStoryStart(ctx, laneId, storyId, opts) {
4260
4266
  const at = ctx.now().toISOString();
4261
4267
  lane = await ensureStoryThread(ctx, lane, target.projectId);
4262
4268
  const before = lane;
4263
- const next = casUpdateLane(hqRoot, laneId, (current) => updateCarriedStory(current, target.projectId, target.storyId, "in_progress", at));
4269
+ const next = casUpdateLane(hqRoot, laneId, (current) => {
4270
+ assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
4271
+ return updateCarriedStory(current, target.projectId, target.storyId, "in_progress", at);
4272
+ });
4264
4273
  await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_started", "in_progress");
4265
4274
  const finalLane = readLane(hqRoot, laneId);
4266
4275
  printJson({ ok: true, action: "story.start", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "in_progress", story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
@@ -4268,6 +4277,7 @@ async function runStoryStart(ctx, laneId, storyId, opts) {
4268
4277
  async function runStoryBlock(ctx, laneId, storyId, opts) {
4269
4278
  const hqRoot = ctx.hqRoot();
4270
4279
  let lane = readLane(hqRoot, laneId);
4280
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
4271
4281
  const target = storyCommandTarget(lane, storyId, opts.project);
4272
4282
  if (target.story.status === "done") {
4273
4283
  throw new LanesError("story_already_done", `Story ${target.projectId}/${target.storyId} is done and cannot be blocked again`, {
@@ -4282,7 +4292,10 @@ async function runStoryBlock(ctx, laneId, storyId, opts) {
4282
4292
  const at = ctx.now().toISOString();
4283
4293
  lane = await ensureStoryThread(ctx, lane, target.projectId);
4284
4294
  const before = lane;
4285
- const next = casUpdateLane(hqRoot, laneId, (current) => updateCarriedStory(current, target.projectId, target.storyId, "blocked", at, { blocked_reason: reason }));
4295
+ const next = casUpdateLane(hqRoot, laneId, (current) => {
4296
+ assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
4297
+ return updateCarriedStory(current, target.projectId, target.storyId, "blocked", at, { blocked_reason: reason });
4298
+ });
4286
4299
  await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_blocked", "blocked", { blocked_reason: reason });
4287
4300
  const finalLane = readLane(hqRoot, laneId);
4288
4301
  printJson({ ok: true, action: "story.block", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "blocked", blocked_reason: reason, story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
@@ -4290,6 +4303,7 @@ async function runStoryBlock(ctx, laneId, storyId, opts) {
4290
4303
  async function runStoryDone(ctx, laneId, storyId, opts) {
4291
4304
  const hqRoot = ctx.hqRoot();
4292
4305
  let lane = readLane(hqRoot, laneId);
4306
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "update story status for");
4293
4307
  if (!opts.verified) {
4294
4308
  throw new LanesError("senior_verification_required", "Only a senior may mark a story done; pass --verified after reviewing or merging it", { lane_id: laneId });
4295
4309
  }
@@ -4311,10 +4325,13 @@ async function runStoryDone(ctx, laneId, storyId, opts) {
4311
4325
  const at = ctx.now().toISOString();
4312
4326
  lane = await ensureStoryThread(ctx, lane, target.projectId);
4313
4327
  const before = lane;
4314
- const next = casUpdateLane(hqRoot, laneId, (current) => updateCarriedStory(current, target.projectId, target.storyId, "done", at, {
4315
- verified_at: at,
4316
- verified_by: current.senior,
4317
- }));
4328
+ const next = casUpdateLane(hqRoot, laneId, (current) => {
4329
+ assertLaneRunsHereForOperatorMutation(ctx, current, "update story status for");
4330
+ return updateCarriedStory(current, target.projectId, target.storyId, "done", at, {
4331
+ verified_at: at,
4332
+ verified_by: current.senior,
4333
+ });
4334
+ });
4318
4335
  await commitStoryTransition(ctx, before, next, target.projectId, target.storyId, "lane_done", "done", { summary: "Senior verification complete" });
4319
4336
  const finalLane = readLane(hqRoot, laneId);
4320
4337
  printJson({ ok: true, action: "story.done", lane_id: laneId, project_id: target.projectId, story_id: target.storyId, status: "done", verified: true, story_warning: storyWarning(finalLane) ?? null, lane: finalLane });
@@ -4354,6 +4371,7 @@ async function runReparent(ctx, laneId, opts) {
4354
4371
  const nextSenior = parseSeniorRef(opts.senior);
4355
4372
  assertSeniorExists(hqRoot, nextSenior, ctx);
4356
4373
  const lane = readLane(hqRoot, laneId);
4374
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "reparent");
4357
4375
  if (isTerminalLaneState(lane.state)) {
4358
4376
  throw new LanesError("lane_terminal", `Cannot re-parent a lane in terminal state ${lane.state}`, { lane_id: laneId, state: lane.state });
4359
4377
  }
@@ -4378,6 +4396,7 @@ async function runReparent(ctx, laneId, opts) {
4378
4396
  continue;
4379
4397
  if (!walkHitsLane(hqRoot, other.senior, laneId, ctx))
4380
4398
  continue;
4399
+ assertLaneRunsHereForOperatorMutation(ctx, other, "reparent descendant of");
4381
4400
  const oldDescendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx);
4382
4401
  const descendantDepth = derivedLaneDepth(hqRoot, other.senior, ctx, override);
4383
4402
  if (descendantDepth > MAX_LANE_DEPTH) {
@@ -4395,6 +4414,7 @@ async function runReparent(ctx, laneId, opts) {
4395
4414
  }
4396
4415
  const at = ctx.now().toISOString();
4397
4416
  const updated = casUpdateLane(hqRoot, laneId, (current) => {
4417
+ assertLaneRunsHereForOperatorMutation(ctx, current, "reparent");
4398
4418
  const already = current.senior_history.some((entry) => entry.at === at && entry.reason === (opts.reason ?? "reparent"));
4399
4419
  if (already && current.depth === newDepth)
4400
4420
  return current;
@@ -4413,6 +4433,7 @@ async function runReparent(ctx, laneId, opts) {
4413
4433
  });
4414
4434
  for (const descendant of descendants) {
4415
4435
  casUpdateLane(hqRoot, descendant.laneId, (current) => {
4436
+ assertLaneRunsHereForOperatorMutation(ctx, current, "reparent descendant of");
4416
4437
  if (current.depth === descendant.depth)
4417
4438
  return current;
4418
4439
  return { ...current, depth: descendant.depth };
@@ -4487,14 +4508,226 @@ async function runLogs(ctx, laneId, opts) {
4487
4508
  export function stopCommittedState(state) {
4488
4509
  return state === "queued" ? "cancelled" : "killed";
4489
4510
  }
4511
+ function parseConfirmedOutpostStopLane(raw, laneId, outpostId, commandId) {
4512
+ let remoteLane;
4513
+ try {
4514
+ remoteLane = parseLaneRecord(raw);
4515
+ }
4516
+ catch (err) {
4517
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned an invalid lane snapshot for stop ${laneId}`, {
4518
+ lane_id: laneId,
4519
+ outpost_id: outpostId,
4520
+ command_id: commandId,
4521
+ cause: err instanceof Error ? err.message : String(err),
4522
+ });
4523
+ }
4524
+ if (remoteLane.lane_id !== laneId ||
4525
+ remoteLane.host.kind !== "outpost" ||
4526
+ remoteLane.host.id !== outpostId ||
4527
+ !isTerminalLaneState(remoteLane.state)) {
4528
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return its terminal lane ${laneId}`, {
4529
+ lane_id: laneId,
4530
+ outpost_id: outpostId,
4531
+ command_id: commandId,
4532
+ owner_lane_id: remoteLane.lane_id,
4533
+ owner_host: remoteLane.host,
4534
+ owner_state: remoteLane.state,
4535
+ });
4536
+ }
4537
+ return remoteLane;
4538
+ }
4539
+ function parseRemoteCancelledQuestionIds(value, laneId, outpostId, commandId) {
4540
+ if (!Array.isArray(value) ||
4541
+ value.some((id) => typeof id !== "string" || id.trim() === "") ||
4542
+ new Set(value).size !== value.length) {
4543
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned invalid cancelled question ids for lane ${laneId}`, { lane_id: laneId, outpost_id: outpostId, command_id: commandId });
4544
+ }
4545
+ return value;
4546
+ }
4547
+ async function readOutpostTerminalLane(ctx, laneId, outpostId, stopCommandId) {
4548
+ let dispatched;
4549
+ try {
4550
+ dispatched = await ctx.dispatchOutpostExec({
4551
+ outpostId,
4552
+ command: buildOutpostLaneCommand({ action: "show", laneId }),
4553
+ timeoutSeconds: 120,
4554
+ waitMs: 120_000,
4555
+ });
4556
+ }
4557
+ catch (err) {
4558
+ throw new LanesError("outpost_delivery_unconfirmed", `Could not read terminal lane ${laneId} from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, {
4559
+ lane_id: laneId,
4560
+ outpost_id: outpostId,
4561
+ command_id: stopCommandId,
4562
+ sync_command_id: null,
4563
+ });
4564
+ }
4565
+ const commandId = dispatched.commandId?.trim();
4566
+ if (dispatched.done !== true ||
4567
+ !commandId ||
4568
+ dispatched.status !== "Success" ||
4569
+ dispatched.exitCode !== 0) {
4570
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not confirm a lane read after stop ${laneId}`, {
4571
+ lane_id: laneId,
4572
+ outpost_id: outpostId,
4573
+ command_id: stopCommandId,
4574
+ sync_command_id: commandId || null,
4575
+ status: dispatched.status,
4576
+ done: dispatched.done ?? false,
4577
+ exit_code: dispatched.exitCode,
4578
+ });
4579
+ }
4580
+ let remote;
4581
+ try {
4582
+ remote = parseRemoteJson(dispatched.stdout, "Outpost lane show");
4583
+ }
4584
+ catch (err) {
4585
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned an invalid lane read after stop ${laneId}`, {
4586
+ lane_id: laneId,
4587
+ outpost_id: outpostId,
4588
+ command_id: stopCommandId,
4589
+ sync_command_id: commandId,
4590
+ cause: err instanceof Error ? err.message : String(err),
4591
+ });
4592
+ }
4593
+ if (remote?.ok !== true || remote.action !== "show") {
4594
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return lane ${laneId} after stop`, {
4595
+ lane_id: laneId,
4596
+ outpost_id: outpostId,
4597
+ command_id: stopCommandId,
4598
+ sync_command_id: commandId,
4599
+ });
4600
+ }
4601
+ return {
4602
+ lane: parseConfirmedOutpostStopLane(remote.lane, laneId, outpostId, commandId),
4603
+ commandId,
4604
+ };
4605
+ }
4606
+ async function dispatchOutpostLaneStop(ctx, lane) {
4607
+ if (lane.host.kind !== "outpost") {
4608
+ throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch stop to`, { lane_id: lane.lane_id });
4609
+ }
4610
+ const outpostId = lane.host.id;
4611
+ let dispatched;
4612
+ try {
4613
+ dispatched = await ctx.dispatchOutpostExec({
4614
+ outpostId,
4615
+ command: buildOutpostLaneCommand({ action: "stop", laneId: lane.lane_id }),
4616
+ timeoutSeconds: 120,
4617
+ waitMs: 120_000,
4618
+ });
4619
+ }
4620
+ catch (err) {
4621
+ throw new LanesError("outpost_dispatch_failed", `Could not dispatch stop for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
4622
+ }
4623
+ const commandId = dispatched.commandId?.trim();
4624
+ if (dispatched.done !== true || !commandId) {
4625
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return a completed stop command for lane ${lane.lane_id}`, {
4626
+ lane_id: lane.lane_id,
4627
+ outpost_id: outpostId,
4628
+ command_id: commandId || null,
4629
+ status: dispatched.status,
4630
+ done: dispatched.done ?? false,
4631
+ exit_code: dispatched.exitCode,
4632
+ });
4633
+ }
4634
+ const remote = parseRemoteJson(dispatched.stdout, "Outpost lane stop");
4635
+ let state;
4636
+ let alreadyTerminal = false;
4637
+ let ownerLane;
4638
+ let cancelledQuestions;
4639
+ let syncCommandId;
4640
+ if (remote?.error === "already_terminal") {
4641
+ if (remote.lane_id !== lane.lane_id ||
4642
+ !isTerminalLaneState(remote.state) ||
4643
+ (dispatched.status !== "Success" && dispatched.status !== "Failed") ||
4644
+ dispatched.exitCode === null ||
4645
+ dispatched.exitCode === 0) {
4646
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned an invalid already-terminal result for lane ${lane.lane_id}`, { lane_id: lane.lane_id, outpost_id: outpostId, command_id: commandId });
4647
+ }
4648
+ state = remote.state;
4649
+ alreadyTerminal = true;
4650
+ const owner = await readOutpostTerminalLane(ctx, lane.lane_id, outpostId, commandId);
4651
+ if (owner.lane.state !== state) {
4652
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} returned a different terminal state for lane ${lane.lane_id}`, {
4653
+ lane_id: lane.lane_id,
4654
+ outpost_id: outpostId,
4655
+ command_id: commandId,
4656
+ sync_command_id: owner.commandId,
4657
+ stop_state: state,
4658
+ owner_state: owner.lane.state,
4659
+ });
4660
+ }
4661
+ ownerLane = owner.lane;
4662
+ cancelledQuestions = [];
4663
+ syncCommandId = owner.commandId;
4664
+ }
4665
+ else if (remote?.error !== undefined) {
4666
+ throw new LanesError("outpost_remote_command_failed", typeof remote.message === "string"
4667
+ ? remote.message
4668
+ : `Outpost ${outpostId} rejected the stop command for lane ${lane.lane_id}`, {
4669
+ lane_id: lane.lane_id,
4670
+ outpost_id: outpostId,
4671
+ command_id: commandId,
4672
+ remote_error: remote.error,
4673
+ });
4674
+ }
4675
+ else {
4676
+ const remoteLane = remote?.lane;
4677
+ const remoteLaneRecord = remoteLane !== null &&
4678
+ typeof remoteLane === "object" &&
4679
+ !Array.isArray(remoteLane)
4680
+ ? remoteLane
4681
+ : null;
4682
+ if (dispatched.status !== "Success" ||
4683
+ dispatched.exitCode !== 0 ||
4684
+ remote?.ok !== true ||
4685
+ remote.action !== "stop" ||
4686
+ remoteLaneRecord?.lane_id !== lane.lane_id ||
4687
+ (remoteLaneRecord.state !== "cancelled" && remoteLaneRecord.state !== "killed")) {
4688
+ throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without confirming the stop for lane ${lane.lane_id}`, {
4689
+ lane_id: lane.lane_id,
4690
+ outpost_id: outpostId,
4691
+ command_id: commandId,
4692
+ status: dispatched.status,
4693
+ exit_code: dispatched.exitCode,
4694
+ });
4695
+ }
4696
+ ownerLane = parseConfirmedOutpostStopLane(remoteLaneRecord, lane.lane_id, outpostId, commandId);
4697
+ state = ownerLane.state;
4698
+ cancelledQuestions = parseRemoteCancelledQuestionIds(remote.cancelled_questions, lane.lane_id, outpostId, commandId);
4699
+ }
4700
+ const updated = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, ownerLane);
4701
+ printJson({
4702
+ ok: true,
4703
+ action: "stop",
4704
+ lane: updated,
4705
+ cancelled_questions: cancelledQuestions,
4706
+ outpost_id: outpostId,
4707
+ outpost_command_id: commandId,
4708
+ ...(syncCommandId ? { outpost_sync_command_id: syncCommandId } : {}),
4709
+ outpost_result: {
4710
+ state,
4711
+ already_terminal: alreadyTerminal,
4712
+ status: dispatched.status,
4713
+ exit_code: dispatched.exitCode,
4714
+ },
4715
+ });
4716
+ }
4490
4717
  async function runStop(ctx, laneId) {
4491
4718
  const hqRoot = ctx.hqRoot();
4492
4719
  const lane = readLane(hqRoot, laneId);
4720
+ if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
4721
+ await dispatchOutpostLaneStop(ctx, lane);
4722
+ return;
4723
+ }
4724
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "stop");
4493
4725
  if (isTerminalLaneState(lane.state)) {
4494
4726
  throw new LanesError("already_terminal", `Lane ${laneId} is already ${lane.state}`, { lane_id: laneId, state: lane.state });
4495
4727
  }
4496
4728
  const at = ctx.now().toISOString();
4497
4729
  const updated = casUpdateLane(hqRoot, laneId, (current) => {
4730
+ assertLaneRunsHereForOperatorMutation(ctx, current, "stop");
4498
4731
  if (isTerminalLaneState(current.state)) {
4499
4732
  throw new LanesError("already_terminal", `Lane ${laneId} is already ${current.state}`, { lane_id: laneId, state: current.state });
4500
4733
  }
@@ -4531,6 +4764,7 @@ async function runAsk(ctx, opts) {
4531
4764
  throw new LanesError("missing_question", "--question is required", {});
4532
4765
  }
4533
4766
  const lane = readLane(hqRoot, laneId);
4767
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "ask");
4534
4768
  const optionList = Array.isArray(opts.option)
4535
4769
  ? opts.option
4536
4770
  : opts.option
@@ -4659,6 +4893,10 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
4659
4893
  const question = readQuestion(hqRoot, questionId);
4660
4894
  throwIfQuestionNotPending(question);
4661
4895
  await yieldEventLoop();
4896
+ const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
4897
+ if (laneAcceptsDropboxDelivery(askingLane)) {
4898
+ assertLaneRunsHereForOperatorMutation(ctx, askingLane, "deliver an answer to");
4899
+ }
4662
4900
  const body = requireQuestionBody(hqRoot, question);
4663
4901
  // A trimmed-empty --freeform is absent: it must not win over --choice and
4664
4902
  // must not become a freeform answer of "".
@@ -4700,7 +4938,7 @@ async function runQuestionsAnswer(ctx, questionId, opts) {
4700
4938
  },
4701
4939
  };
4702
4940
  });
4703
- const delivery = deliverAnswerToAskingLane(hqRoot, updated, choice, at);
4941
+ const delivery = deliverAnswerToAskingLane(ctx, updated, choice, at);
4704
4942
  printJson({
4705
4943
  ok: true,
4706
4944
  action: "questions.answer",
@@ -4729,13 +4967,15 @@ function writeAnswerDeliveryFailed(hqRoot, laneId, questionId, err, at) {
4729
4967
  * Record is already durable. Delivery is best-effort onto the asking lane's
4730
4968
  * drop box (spec §6.5). Target is question.lane_id, never a project/story lookup.
4731
4969
  */
4732
- function deliverAnswerToAskingLane(hqRoot, question, choice, at) {
4970
+ function deliverAnswerToAskingLane(ctx, question, choice, at) {
4971
+ const hqRoot = ctx.hqRoot();
4733
4972
  const laneId = laneIdForAnswerDelivery(question);
4734
4973
  try {
4735
4974
  const lane = readLane(hqRoot, laneId);
4736
4975
  if (!laneAcceptsDropboxDelivery(lane)) {
4737
4976
  return { status: "recorded_only" };
4738
4977
  }
4978
+ assertLaneRunsHereForOperatorMutation(ctx, lane, "deliver an answer to");
4739
4979
  appendDropboxMessage(hqRoot, laneId, answerDropboxMessage(question, choice, at, mintMessageId()));
4740
4980
  return { status: "dropped" };
4741
4981
  }
@@ -5146,6 +5386,7 @@ export function registerLanesCommand(parent, deps = {}) {
5146
5386
  const lanes = parent
5147
5387
  .command("lanes", { hidden: true })
5148
5388
  .description("Create, supervise, and steer worker lanes; routed decision queue");
5389
+ registerOutpostPrunerCommand(lanes);
5149
5390
  lanes
5150
5391
  .command("create")
5151
5392
  .description("Register a lane, run admission, and return immediately")
@@ -14,7 +14,7 @@ import chalk from "chalk";
14
14
  import { loadCachedTokens, isMachineIdentity } from "../utils/cognito-session.js";
15
15
  import * as readline from "node:readline/promises";
16
16
  import { STORY_STATUSES, appendThreadEvent, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
17
- import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard } from "../lib/mesh/project-board.js";
17
+ import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, formatStoryStatusChanges, mergeProjectStories, } from "../lib/mesh/project-board.js";
18
18
  import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.js";
19
19
  import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
20
20
  import { clearDefaultCompany, isDefaultCompanyDisabled, readDeviceConfig, recordMigrationCapabilitySnapshot, setRepairHeldWithDefault, setDefaultCompany, } from "../lib/work-context/config.js";
@@ -307,23 +307,6 @@ function httpStatus(err) {
307
307
  }
308
308
  return undefined;
309
309
  }
310
- function mergeProjectStories(current, planned) {
311
- const currentById = new Map(current.map((story) => [story.id, story]));
312
- const plannedIds = new Set(planned.map((story) => story.id));
313
- const merged = planned.map((story) => {
314
- const live = currentById.get(story.id);
315
- if (!live)
316
- return story;
317
- const status = live.status || story.status;
318
- return {
319
- ...story,
320
- ...(status ? { status } : {}),
321
- passes: status === "done" ? true : typeof live.passes === "boolean" ? live.passes : story.passes,
322
- ...(live.assigneeUid ? { assigneeUid: live.assigneeUid } : {}),
323
- };
324
- });
325
- return [...merged, ...current.filter((story) => !plannedIds.has(story.id))];
326
- }
327
310
  function mergeProjectRepos(current, planned) {
328
311
  const plannedPaths = new Set(planned.map((repo) => repo.path));
329
312
  return [...planned, ...current.filter((repo) => !plannedPaths.has(repo.path))];
@@ -386,6 +369,10 @@ async function runProjectSet(projectId, opts) {
386
369
  registration = ensured.registration;
387
370
  }
388
371
  else if (opts.name !== undefined || opts.description !== undefined || source !== undefined || explicitRepos !== undefined) {
372
+ const stories = source
373
+ ? mergeProjectStories(current.stories, source.stories, { resetStatus: Boolean(opts.resetStatus) })
374
+ : current.stories;
375
+ const statusChangeLine = source ? formatStoryStatusChanges(current.stories, stories) : undefined;
389
376
  try {
390
377
  view = await putProjectView(token, company.companyUid, projectId, {
391
378
  name: opts.name?.trim() || source?.name || current.name,
@@ -394,7 +381,7 @@ async function runProjectSet(projectId, opts) {
394
381
  : source?.description !== undefined
395
382
  ? { description: source.description }
396
383
  : { description: current.description }),
397
- stories: source?.stories ?? current.stories,
384
+ stories,
398
385
  repos: explicitRepos ?? source?.repos ?? current.repos,
399
386
  ...(current.version !== undefined ? { expectedVersion: current.version } : {}),
400
387
  });
@@ -409,6 +396,12 @@ async function runProjectSet(projectId, opts) {
409
396
  }
410
397
  throw err;
411
398
  }
399
+ if (statusChangeLine) {
400
+ if (opts.json)
401
+ console.error(statusChangeLine);
402
+ else
403
+ console.log(statusChangeLine);
404
+ }
412
405
  }
413
406
  else {
414
407
  view = current;
@@ -1816,6 +1809,7 @@ export function registerMeshCommand(program) {
1816
1809
  .option("--repo <path[@branch]>", "Repository binding (repeatable)", collectRepeatable)
1817
1810
  .option("--channel <channelId>", "Bind the project to an existing company or project channel")
1818
1811
  .option("--create", "Create the Board view when the GET is a 404")
1812
+ .option("--reset-status", "Overwrite live story status from the PRD (prints a one-line summary of changes)")
1819
1813
  .option("--json", "Print machine-readable JSON")
1820
1814
  .action((projectId, opts) => wrap(() => runProjectSet(projectId, opts))());
1821
1815
  project
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerOutpostPrunerCommand(lanes: Command): void;
3
+ //# sourceMappingURL=outpost-pruner.d.ts.map