@indigoai-us/hq-cli 5.345.134 → 5.345.136

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 (100) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/assets/scaffold/core/scripts/worktree-gc.sh +98 -8
  3. package/dist/command-catalog.generated.d.ts +62 -6
  4. package/dist/command-catalog.generated.js +79 -7
  5. package/dist/command-registration-plan.d.ts +6 -0
  6. package/dist/command-registration-plan.js +1 -0
  7. package/dist/commands/core-bucket-c-handoff-knowledge-native.d.ts +3 -1
  8. package/dist/commands/core-bucket-c-handoff-knowledge-native.js +8 -4
  9. package/dist/commands/core-checkpoint-native.d.ts +10 -0
  10. package/dist/commands/core-checkpoint-native.js +60 -15
  11. package/dist/commands/core-handoff.d.ts +2 -0
  12. package/dist/commands/core-handoff.js +3 -1
  13. package/dist/commands/core-pipeline-workflow-runner-native.js +7 -5
  14. package/dist/commands/daemon.js +3 -1
  15. package/dist/commands/fingerprint.d.ts +10 -0
  16. package/dist/commands/fingerprint.js +191 -0
  17. package/dist/commands/integrations-core.d.ts +22 -0
  18. package/dist/commands/integrations-core.js +25 -0
  19. package/dist/commands/integrations-manage.d.ts +24 -1
  20. package/dist/commands/integrations-manage.js +92 -17
  21. package/dist/commands/integrations.d.ts +11 -0
  22. package/dist/commands/integrations.js +100 -19
  23. package/dist/commands/lanes-loop.d.ts +9 -0
  24. package/dist/commands/lanes-loop.js +9 -1
  25. package/dist/commands/lanes.js +78 -55
  26. package/dist/commands/meetings.d.ts +2 -0
  27. package/dist/commands/meetings.js +57 -14
  28. package/dist/commands/mesh.d.ts +19 -1
  29. package/dist/commands/mesh.js +158 -70
  30. package/dist/commands/reindex.js +80 -5
  31. package/dist/commands/storage-offload.d.ts +93 -28
  32. package/dist/commands/storage-offload.js +290 -34
  33. package/dist/commands/storage.js +60 -7
  34. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2550 -1748
  35. package/dist/hq-cloud-sync-runner/package.json +1 -1
  36. package/dist/index.js +4 -0
  37. package/dist/lib/capacity/route.js +24 -0
  38. package/dist/lib/core-utils/session-company.d.ts +11 -0
  39. package/dist/lib/core-utils/session-company.js +34 -0
  40. package/dist/lib/core-utils/worktree-resume.js +44 -4
  41. package/dist/lib/core-utils/worktree-session-owner.d.ts +27 -0
  42. package/dist/lib/core-utils/worktree-session-owner.js +157 -0
  43. package/dist/lib/daemon/catalog.d.ts +2 -0
  44. package/dist/lib/daemon/catalog.js +3 -3
  45. package/dist/lib/daemon/hq-child-bootstrap-stage.d.ts +5 -0
  46. package/dist/lib/daemon/hq-child-bootstrap-stage.js +3 -11
  47. package/dist/lib/daemon/server.js +10 -1
  48. package/dist/lib/daemon/supervisor.js +6 -0
  49. package/dist/lib/daemon/sync-runner-bundle.d.ts +5 -0
  50. package/dist/lib/daemon/sync-runner-bundle.js +4 -12
  51. package/dist/lib/doctor/checks/claude-wiring.js +2 -1
  52. package/dist/lib/doctor/fix/apply.d.ts +20 -0
  53. package/dist/lib/doctor/fix/apply.js +63 -3
  54. package/dist/lib/doctor/fix/remediation.d.ts +1 -1
  55. package/dist/lib/doctor/fix/remediation.js +16 -0
  56. package/dist/lib/doctor/shared.d.ts +11 -1
  57. package/dist/lib/doctor/shared.js +16 -2
  58. package/dist/lib/fingerprint/fixtures.d.ts +19 -0
  59. package/dist/lib/fingerprint/fixtures.js +61 -0
  60. package/dist/lib/fingerprint/format.d.ts +50 -0
  61. package/dist/lib/fingerprint/format.js +105 -0
  62. package/dist/lib/fingerprint/offline.d.ts +16 -0
  63. package/dist/lib/fingerprint/offline.js +44 -0
  64. package/dist/lib/fingerprint/registry-client.d.ts +74 -0
  65. package/dist/lib/fingerprint/registry-client.js +141 -0
  66. package/dist/lib/fingerprint/resolve-local.d.ts +14 -0
  67. package/dist/lib/fingerprint/resolve-local.js +67 -0
  68. package/dist/lib/fingerprint/self-core.d.ts +139 -0
  69. package/dist/lib/fingerprint/self-core.js +371 -0
  70. package/dist/lib/fingerprint/self.d.ts +44 -0
  71. package/dist/lib/fingerprint/self.js +191 -0
  72. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  73. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  74. package/dist/lib/lanes/monitor.js +14 -2
  75. package/dist/lib/lanes/spawn.d.ts +19 -1
  76. package/dist/lib/lanes/spawn.js +50 -6
  77. package/dist/lib/lanes/story-ledger.d.ts +1 -0
  78. package/dist/lib/lanes/story-ledger.js +3 -1
  79. package/dist/lib/lanes/types.d.ts +12 -0
  80. package/dist/lib/lanes/types.js +12 -1
  81. package/dist/lib/mesh/live/daemon/install.d.ts +12 -0
  82. package/dist/lib/mesh/live/daemon/install.js +22 -0
  83. package/dist/lib/mesh/live/flush.js +106 -3
  84. package/dist/lib/mesh/live/session-events-client.d.ts +9 -0
  85. package/dist/lib/mesh/live/session-events-client.js +15 -0
  86. package/dist/lib/work-context/state.d.ts +10 -0
  87. package/dist/lib/work-context/state.js +5 -0
  88. package/dist/utils/cli-network-lifecycle.d.ts +2 -0
  89. package/dist/utils/cli-network-lifecycle.js +5 -1
  90. package/dist/utils/cognito-session.d.ts +2 -21
  91. package/dist/utils/cognito-session.js +3 -84
  92. package/dist/utils/fsync-directory.d.ts +17 -0
  93. package/dist/utils/fsync-directory.js +27 -0
  94. package/dist/utils/person-token-cache.d.ts +32 -0
  95. package/dist/utils/person-token-cache.js +95 -0
  96. package/dist/utils/vault-api.d.ts +21 -1
  97. package/dist/utils/vault-api.js +54 -5
  98. package/dist/utils/windows-hide-default.d.ts +42 -0
  99. package/dist/utils/windows-hide-default.js +98 -0
  100. package/package.json +2 -2
@@ -21,6 +21,7 @@
21
21
  * hq integrations show [app] One app in full.
22
22
  * hq integrations tools --provider <p> What the connected app can do.
23
23
  * hq integrations call <tool> --provider <p> --args '<json>'
24
+ * hq integrations call <tool> --provider <p> --args-file args.json
24
25
  * hq integrations pending Calls awaiting approval.
25
26
  * hq integrations pending --history Recently decided calls, including status.
26
27
  * hq integrations approve|reject <queueId> Decide a queued call.
@@ -51,6 +52,16 @@
51
52
  */
52
53
  import { Command } from "commander";
53
54
  import { type PendingApproval } from "./integrations-api.js";
55
+ /**
56
+ * Resolve `hq integrations call` tool arguments from `--args <json>`,
57
+ * `--args -` (stdin), or `--args-file <path>` (`-` also reads stdin). A file or
58
+ * stdin avoids shell quoting: zsh breaks inline JSON that contains apostrophes
59
+ * or parentheses. The result must be a JSON object.
60
+ */
61
+ export declare function resolveCallArgs(opts: {
62
+ args?: string;
63
+ argsFile?: string;
64
+ }, stdin?: NodeJS.ReadableStream): Promise<Record<string, unknown>>;
54
65
  export { IntegrationsCliError, callGateway, fetchConnections, fetchUsableApps, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
55
66
  export type { AdminConnection, UsableApp, UsableAppsResult } from "./integrations-core.js";
56
67
  /** Wait for a decided queue item without resending the approve request. */
@@ -21,6 +21,7 @@
21
21
  * hq integrations show [app] One app in full.
22
22
  * hq integrations tools --provider <p> What the connected app can do.
23
23
  * hq integrations call <tool> --provider <p> --args '<json>'
24
+ * hq integrations call <tool> --provider <p> --args-file args.json
24
25
  * hq integrations pending Calls awaiting approval.
25
26
  * hq integrations pending --history Recently decided calls, including status.
26
27
  * hq integrations approve|reject <queueId> Decide a queued call.
@@ -50,6 +51,7 @@
50
51
  * `integrations-manage.ts`.
51
52
  */
52
53
  import { randomUUID } from "node:crypto";
54
+ import { readFile } from "node:fs/promises";
53
55
  import chalk from "chalk";
54
56
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
55
57
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
@@ -60,6 +62,69 @@ import { registerImportCommands } from "./integrations-import.js";
60
62
  import { registerManageCommands } from "./integrations-manage.js";
61
63
  import { registerSlackCommands } from "./integrations-slack.js";
62
64
  import { fetchPendingApprovals } from "./integrations-api.js";
65
+ const CALL_ARGS_EXAMPLE = `--args '{"assignee":"me"}' or --args-file args.json`;
66
+ function readStream(stream) {
67
+ return new Promise((resolve, reject) => {
68
+ let data = "";
69
+ stream.setEncoding("utf8");
70
+ stream.on("data", (chunk) => {
71
+ data += chunk;
72
+ });
73
+ stream.on("end", () => resolve(data));
74
+ stream.on("error", reject);
75
+ });
76
+ }
77
+ /**
78
+ * Resolve `hq integrations call` tool arguments from `--args <json>`,
79
+ * `--args -` (stdin), or `--args-file <path>` (`-` also reads stdin). A file or
80
+ * stdin avoids shell quoting: zsh breaks inline JSON that contains apostrophes
81
+ * or parentheses. The result must be a JSON object.
82
+ */
83
+ export async function resolveCallArgs(opts, stdin = process.stdin) {
84
+ if (opts.args !== undefined && opts.argsFile !== undefined) {
85
+ throw new IntegrationsCliError("Use either --args or --args-file, not both.", { expected: true });
86
+ }
87
+ let text;
88
+ let source;
89
+ const fromStdin = opts.args === "-" || opts.argsFile === "-";
90
+ if (fromStdin) {
91
+ if (stdin.isTTY) {
92
+ throw new IntegrationsCliError("Reading tool arguments from stdin needs piped input, e.g. `hq integrations call <tool> --args - < args.json`.", { expected: true });
93
+ }
94
+ text = await readStream(stdin);
95
+ source = "stdin";
96
+ }
97
+ else if (opts.argsFile !== undefined) {
98
+ source = `--args-file ${opts.argsFile}`;
99
+ try {
100
+ text = await readFile(opts.argsFile, "utf8");
101
+ }
102
+ catch (err) {
103
+ const reason = err instanceof Error ? err.message : String(err);
104
+ throw new IntegrationsCliError(`Could not read --args-file ${opts.argsFile}: ${reason}`, { expected: true });
105
+ }
106
+ }
107
+ else {
108
+ text = opts.args ?? "{}";
109
+ source = "--args";
110
+ }
111
+ let raw;
112
+ try {
113
+ raw = JSON.parse(text);
114
+ }
115
+ catch (err) {
116
+ const reason = err instanceof Error ? err.message : String(err);
117
+ throw new IntegrationsCliError(source === "--args"
118
+ ? `--args must be a JSON object, e.g. ${CALL_ARGS_EXAMPLE}`
119
+ : `Tool arguments from ${source} are not valid JSON (${reason}). They must be a JSON object, e.g. {"assignee":"me"}.`, { expected: true });
120
+ }
121
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
122
+ throw new IntegrationsCliError(source === "--args"
123
+ ? `--args must be a JSON object, e.g. ${CALL_ARGS_EXAMPLE}`
124
+ : `Tool arguments from ${source} must be a JSON object, e.g. {"assignee":"me"}.`, { expected: true });
125
+ }
126
+ return raw;
127
+ }
63
128
  /** Routing ids hq sets on a built-in tool call; never accepted from --args. */
64
129
  const BUILT_IN_ROUTING_ARGS = ["companyUid", "connectionId", "idempotencyKey"];
65
130
  /**
@@ -382,7 +447,7 @@ export function registerIntegrationsCommand(program) {
382
447
  const selector = sharedProvider
383
448
  ? `--connection ${connection.id}`
384
449
  : `--provider ${bareProvider(connection.provider)}`;
385
- console.log(chalk.dim(`\n${tools.length} tools. Describe inputs: hq integrations tools ${selector} --describe <tool>\nCall one with: hq integrations call <tool> ${selector} --args '<json>'`));
450
+ console.log(chalk.dim(`\n${tools.length} tools. Describe inputs: hq integrations tools ${selector} --describe <tool>\nCall one with: hq integrations call <tool> ${selector} --args '<json>'\nIf the JSON has apostrophes or parentheses, put it in a file: --args-file args.json (or --args - to read stdin)`));
386
451
  });
387
452
  integrations
388
453
  .command("call <tool>")
@@ -390,21 +455,12 @@ export function registerIntegrationsCommand(program) {
390
455
  .option("--provider <slug>", "Connected app (e.g. linear)")
391
456
  .option("--connection <id>", "Connection id (acct_…)")
392
457
  .option("--company <slug>", "Company slug, e.g. indigo")
393
- .option("--args <json>", "Tool arguments as JSON", "{}")
458
+ .option("--args <json>", "Tool arguments as a JSON object (default {}); use - to read the JSON from stdin")
459
+ .option("--args-file <path>", "Read tool arguments from a JSON file (use - for stdin); avoids shell quoting of apostrophes and parentheses")
394
460
  .option("--idempotency-key <key>", "Stable key so retries execute at most once (default: generated)")
395
461
  .option("--json", "Machine-readable output")
396
462
  .action(async (tool, opts) => {
397
- let parsedArgs;
398
- try {
399
- const raw = JSON.parse(opts.args);
400
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
401
- throw new Error("not an object");
402
- }
403
- parsedArgs = raw;
404
- }
405
- catch {
406
- throw new IntegrationsCliError(`--args must be a JSON object, e.g. --args '{"assignee":"me"}'`, { expected: true });
407
- }
463
+ const parsedArgs = await resolveCallArgs(opts);
408
464
  const token = await ensureCognitoIdToken();
409
465
  const companyUid = await getCompanyUid(token, opts.company);
410
466
  const connection = selectConnection(await fetchConnections(token, companyUid), opts);
@@ -417,7 +473,7 @@ export function registerIntegrationsCommand(program) {
417
473
  // arg with a routing name would redirect the call; refuse it.
418
474
  const reserved = BUILT_IN_ROUTING_ARGS.filter((key) => key in parsedArgs);
419
475
  if (reserved.length) {
420
- throw new IntegrationsCliError(`--args cannot include ${reserved.join(", ")}: it is set by hq from the selected connection.`, { expected: true });
476
+ throw new IntegrationsCliError(`Tool arguments cannot include ${reserved.join(", ")}: it is set by hq from the selected connection.`, { expected: true });
421
477
  }
422
478
  gatewayParams = {
423
479
  companyUid,
@@ -525,16 +581,41 @@ export function registerIntegrationsCommand(program) {
525
581
  process.exitCode = 1;
526
582
  return;
527
583
  }
584
+ if (decision === "reject") {
585
+ if (opts.json)
586
+ printJson(body);
587
+ else
588
+ console.log(chalk.yellow(`Rejected (status: ${body.status ?? "rejected"}).`));
589
+ return;
590
+ }
591
+ // A synchronous approve runs the call inline. The gateway answers a
592
+ // provider-side tool error with { status: "rejected", outcome:
593
+ // "failed", rejectionReason: "provider_tool_error" }; an older
594
+ // gateway reports "resumed" with an isError result. Classify both as
595
+ // failures, the same way the polling path does.
596
+ const result = body.result === undefined ? undefined : unwrapGatewayResult(body.result);
597
+ const toolError = body.result !== undefined &&
598
+ (gatewayResultIsError(result) || gatewayResultIsError(body.result));
599
+ const failed = approvalOutcomeFailed(body) || toolError;
600
+ if (failed)
601
+ process.exitCode = 1;
528
602
  if (opts.json) {
529
603
  printJson(body);
530
604
  return;
531
605
  }
532
- console.log(decision === "approve"
533
- ? chalk.green(`Approved — the call ran (status: ${body.status ?? "resumed"}).`)
534
- : chalk.yellow(`Rejected (status: ${body.status ?? "rejected"}).`));
535
- if (decision === "approve" && body.result !== undefined) {
536
- printJson(unwrapGatewayResult(body.result));
606
+ if (failed) {
607
+ const reason = body.rejectionReason ??
608
+ (toolError ? "provider_tool_error" : body.outcome ?? body.status ?? "failed");
609
+ console.log(chalk.red(`The approved call failed at the provider (${reason}).`));
537
610
  }
611
+ else if (body.status === "resumed" || body.status === "completed" || body.outcome === "ran") {
612
+ console.log(chalk.green(`Approved — the call ran (status: ${body.status ?? body.outcome}).`));
613
+ }
614
+ else {
615
+ console.log(chalk.yellow(`Approval ended with status ${body.status ?? "unknown"}.`));
616
+ }
617
+ if (result !== undefined)
618
+ printJson(result);
538
619
  });
539
620
  }
540
621
  registerConnectCommands(integrations);
@@ -18,9 +18,18 @@ export interface LoopControllerDeps {
18
18
  resumeLane(laneId: string, options: LaneResumeOptions): Promise<unknown>;
19
19
  interruptLane(laneId: string, reason: string, options: {
20
20
  preserveLoopQueue: boolean;
21
+ terminalReason?: string;
21
22
  }): Promise<void>;
22
23
  emitTerminal(lane: LaneRecord): Promise<void>;
23
24
  }
25
+ /**
26
+ * terminal_reason values for interrupts HQ issues itself. An operator
27
+ * interrupt (`hq lanes interrupt`) records `operator_interrupt`; these keep a
28
+ * system recovery from looking like an operator action in the lane record.
29
+ */
30
+ export declare const LOOP_CONTROLLER_RECOVERY_REASON = "loop_controller_recovery";
31
+ export declare const LOOP_STALL_RESTART_REASON = "loop_stall_restart";
32
+ export declare const LOOP_PHASE_DEADLINE_REASON = "loop_phase_deadline";
24
33
  export declare function startLaneLoopController(ctx: Ctx, laneId: string, initialAdmission?: boolean): number;
25
34
  export declare function runLaneLoopController(ctx: Ctx, laneId: string, deps: LoopControllerDeps): Promise<void>;
26
35
  //# sourceMappingURL=lanes-loop.d.ts.map
@@ -52,6 +52,14 @@ function casLoopLaneState(hqRoot, laneId, from, to) {
52
52
  }
53
53
  }
54
54
  }
55
+ /**
56
+ * terminal_reason values for interrupts HQ issues itself. An operator
57
+ * interrupt (`hq lanes interrupt`) records `operator_interrupt`; these keep a
58
+ * system recovery from looking like an operator action in the lane record.
59
+ */
60
+ export const LOOP_CONTROLLER_RECOVERY_REASON = "loop_controller_recovery";
61
+ export const LOOP_STALL_RESTART_REASON = "loop_stall_restart";
62
+ export const LOOP_PHASE_DEADLINE_REASON = "loop_phase_deadline";
55
63
  export function startLaneLoopController(ctx, laneId, initialAdmission = false) {
56
64
  const entry = process.argv[1];
57
65
  if (!entry)
@@ -202,7 +210,7 @@ export async function runLaneLoopController(ctx, laneId, deps) {
202
210
  }
203
211
  let interruptPromise;
204
212
  const abortPhase = () => {
205
- interruptPromise = deps.interruptLane(laneId, "loop phase deadline exceeded", { preserveLoopQueue: true }).catch((error) => {
213
+ interruptPromise = deps.interruptLane(laneId, "loop phase deadline exceeded", { preserveLoopQueue: true, terminalReason: LOOP_PHASE_DEADLINE_REASON }).catch((error) => {
206
214
  process.stderr.write(`hq lanes: could not interrupt stalled loop phase ${laneId}: ${error instanceof Error ? error.message : String(error)}\n`);
207
215
  });
208
216
  };
@@ -48,7 +48,7 @@ import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } fr
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
50
  import { commitIdleLoopStop, enqueueLoopEnvelope, interruptLoopQueue, interruptLoopQueueForStory, loopControllerIsAlive, loopLaneIsIdle, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
51
- import { runLaneLoopController, startLaneLoopController } from "./lanes-loop.js";
51
+ import { LOOP_CONTROLLER_RECOVERY_REASON, LOOP_STALL_RESTART_REASON, runLaneLoopController, startLaneLoopController } from "./lanes-loop.js";
52
52
  import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
53
53
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
54
54
  import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
@@ -494,6 +494,41 @@ function livenessDepsFromCtx(ctx) {
494
494
  hostname: ctx.hostname,
495
495
  };
496
496
  }
497
+ /**
498
+ * Start a lane that admission just let through. A queued loop lane is marked
499
+ * running and gets its loop controller, which works through the queued phase
500
+ * envelopes; every other lane spawns its worker directly. Every admission path
501
+ * (reconcile, in-process queue drain, hq lanes admit) goes through here so a
502
+ * loop lane never runs its brief without a controller.
503
+ */
504
+ function startAdmittedLane(ctx, root, candidate) {
505
+ if (candidate.loop_mode !== true || candidate.state !== "queued") {
506
+ const admissionHost = reconcileHostFromCtx(ctx);
507
+ return spawnLaneWorker(root, candidate, {
508
+ env: ctx.env,
509
+ hostname: admissionHost.hostname,
510
+ admissionHost,
511
+ modelDeps: { readFileSync: ctx.readFileSync, probe: ctx.probeProvider, env: ctx.env },
512
+ existsSync: ctx.existsSync,
513
+ spawnProcess: ctx.spawnProcess,
514
+ proveDetached: ctx.proveDetached,
515
+ killProcess: ctx.killProcess,
516
+ now: ctx.now,
517
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
518
+ runCompaction: ctx.runCompaction,
519
+ locateTranscript: ctx.locateTranscript,
520
+ });
521
+ }
522
+ const admitted = casUpdateLane(root, candidate.lane_id, (current) => ({
523
+ ...current,
524
+ state: "running",
525
+ queued_reason: undefined,
526
+ queued_details: undefined,
527
+ admission_reserved_at: undefined,
528
+ }));
529
+ startLaneLoopController(ctx, candidate.lane_id, true);
530
+ return admitted;
531
+ }
497
532
  /**
498
533
  * Admit queued lanes in-process after a command frees or adds capacity, so
499
534
  * the queue drains without `hq daemon`. Best effort: an admission failure is
@@ -516,6 +551,7 @@ async function admitQueuedFromCtx(ctx) {
516
551
  runCompaction: ctx.runCompaction,
517
552
  locateTranscript: ctx.locateTranscript,
518
553
  },
554
+ spawn: (root, candidate) => startAdmittedLane(ctx, root, candidate),
519
555
  throttleDeps: ctx.throttleDeps,
520
556
  livenessDeps: livenessDepsFromCtx(ctx),
521
557
  presenceConnected: ctx.presenceConnected,
@@ -2432,11 +2468,18 @@ function autopilotWorkerStarter(ctx) {
2432
2468
  return { lane_id: started.lane_id };
2433
2469
  };
2434
2470
  }
2471
+ /**
2472
+ * The senior for a lane created without `--senior`. Inside a lane worker
2473
+ * (HQ_LANE_ID set) the worker's own session wins, because HQ_PARENT_SESSION_ID
2474
+ * there names the session that started the worker, one level too high.
2475
+ */
2435
2476
  function defaultSenior(ctx) {
2477
+ const sessionId = sessionIdFromEnv(ctx.env);
2478
+ if (ctx.env.HQ_LANE_ID?.trim() && sessionId)
2479
+ return { kind: "session", id: sessionId };
2436
2480
  const parent = ctx.env.HQ_PARENT_SESSION_ID?.trim();
2437
2481
  if (parent)
2438
2482
  return { kind: "session", id: parent };
2439
- const sessionId = sessionIdFromEnv(ctx.env);
2440
2483
  if (sessionId)
2441
2484
  return { kind: "session", id: sessionId };
2442
2485
  return { kind: "user" };
@@ -3348,7 +3391,7 @@ async function runCreate(ctx, opts) {
3348
3391
  const requestedHireGrant = opts.hireGrant === undefined
3349
3392
  ? undefined
3350
3393
  : parseHireGrant(opts.hireGrant);
3351
- const requestedPlacement = opts.placement !== undefined
3394
+ let requestedPlacement = opts.placement !== undefined
3352
3395
  ? parsePlacementPolicy(opts.placement)
3353
3396
  : undefined;
3354
3397
  const defaultPlacement = opts.defaultPlacement !== undefined
@@ -3568,6 +3611,13 @@ async function runCreate(ctx, opts) {
3568
3611
  placementGate ??= readLanePlacementEnabled(ctx, resolvePlacementCompany());
3569
3612
  return placementGate;
3570
3613
  };
3614
+ if (requestedPlacement?.kind === "local" &&
3615
+ defaultPlacement === undefined &&
3616
+ !(await placementEnabled())) {
3617
+ // With placement off every lane already runs here, so --placement local
3618
+ // asks for the default. Treat it as omitted instead of refusing.
3619
+ requestedPlacement = undefined;
3620
+ }
3571
3621
  if (requestedPlacement !== undefined || defaultPlacement !== undefined) {
3572
3622
  await assertPlacementEnabled(ctx, resolvePlacementCompany(), placementEnabled);
3573
3623
  }
@@ -6305,6 +6355,7 @@ async function runShow(ctx, laneId, hasPendingQuestionForLane, resolveLaneTarget
6305
6355
  ...(showRefresh.remoteRefresh === undefined ? {} : { remote_refresh: showRefresh.remoteRefresh }),
6306
6356
  provider_session_id: lane.provider_session_id ?? null,
6307
6357
  provider_session_id_absent: lane.provider_session_id_absent ?? null,
6358
+ fingerprint: lane.fingerprint ?? null,
6308
6359
  envelope_repair_mode: lane.envelope_repair?.mode ?? null,
6309
6360
  envelope_validation_errors: lane.envelope_validation_errors ?? [],
6310
6361
  last_resume: lane.last_resume ?? null,
@@ -7151,22 +7202,7 @@ async function runAdmit(ctx, laneId, opts) {
7151
7202
  });
7152
7203
  return;
7153
7204
  }
7154
- const started = spawnLaneWorker(hqRoot, lane, {
7155
- env: ctx.env,
7156
- modelDeps: {
7157
- readFileSync: ctx.readFileSync,
7158
- probe: ctx.probeProvider,
7159
- env: ctx.env,
7160
- },
7161
- existsSync: ctx.existsSync,
7162
- spawnProcess: ctx.spawnProcess,
7163
- proveDetached: ctx.proveDetached,
7164
- killProcess: ctx.killProcess,
7165
- now: ctx.now,
7166
- resolveMachineIdentity: ctx.resolveMachineIdentity,
7167
- runCompaction: ctx.runCompaction,
7168
- locateTranscript: ctx.locateTranscript,
7169
- });
7205
+ const started = startAdmittedLane(ctx, hqRoot, lane);
7170
7206
  const admitted = opts.force && started.state === "running"
7171
7207
  ? casUpdateLane(hqRoot, laneId, (current) => ({ ...current, admitted_reason: "operator_force" }))
7172
7208
  : started;
@@ -8803,7 +8839,7 @@ async function runStop(ctx, laneId, reason, quiet = false) {
8803
8839
  }
8804
8840
  const controllerAlive = loopControllerIsAlive(runDirPath);
8805
8841
  if (!controllerAlive && lane.state === "running") {
8806
- await runInterrupt(ctx, lane.lane_id, "loop controller exited; recovering queued work before stop", { preserveLoopQueue: true, quiet: true });
8842
+ await runInterrupt(ctx, lane.lane_id, "loop controller exited; recovering queued work before stop", { preserveLoopQueue: true, quiet: true, terminalReason: LOOP_CONTROLLER_RECOVERY_REASON });
8807
8843
  }
8808
8844
  const queued = enqueueLoopEnvelope({ runDir: runDirPath, laneId: lane.lane_id, envelope: { kind: "stop" }, now: ctx.now });
8809
8845
  if (!controllerAlive && lane.state !== "queued")
@@ -8879,6 +8915,13 @@ function stopTerminalWarning(laneId) {
8879
8915
  * session so `hq lanes resume` continues it. Keep-alive resumes only
8880
8916
  * `exited_with_partial_output` interruptions, so it leaves this lane alone.
8881
8917
  */
8918
+ /** Stop-history text for an operator interrupt given without --reason. */
8919
+ function defaultOperatorInterruptReason(ctx) {
8920
+ const sessionId = sessionIdFromEnv(ctx.env);
8921
+ const laneId = ctx.env.HQ_LANE_ID?.trim();
8922
+ const caller = laneId ? ` by lane ${laneId}` : sessionId ? ` by session ${sessionId}` : "";
8923
+ return `hq lanes interrupt${caller} (no reason given)`;
8924
+ }
8882
8925
  async function runInterrupt(ctx, laneId, reason, options = {}) {
8883
8926
  const operatorReason = reason?.trim();
8884
8927
  if (reason !== undefined && !operatorReason) {
@@ -8886,6 +8929,11 @@ async function runInterrupt(ctx, laneId, reason, options = {}) {
8886
8929
  }
8887
8930
  if (operatorReason !== undefined)
8888
8931
  assertNoSecretText("interrupt reason", operatorReason);
8932
+ const terminalReason = options.terminalReason ?? "operator_interrupt";
8933
+ // Every interrupt leaves a stop-history entry, so an operator interrupt
8934
+ // given without --reason is still distinguishable from a system one.
8935
+ const historyReason = operatorReason ??
8936
+ (terminalReason === "operator_interrupt" ? defaultOperatorInterruptReason(ctx) : terminalReason);
8889
8937
  const hqRoot = ctx.hqRoot();
8890
8938
  const lane = readLane(hqRoot, laneId);
8891
8939
  if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
@@ -8910,7 +8958,7 @@ async function runInterrupt(ctx, laneId, reason, options = {}) {
8910
8958
  if (!reason)
8911
8959
  reason = "story-scoped loop phase interrupt";
8912
8960
  const remaining = scoped;
8913
- await runInterrupt(ctx, laneId, reason, { preserveLoopQueue: true, quiet: true });
8961
+ await runInterrupt(ctx, laneId, reason, { preserveLoopQueue: true, quiet: true, terminalReason: options.terminalReason });
8914
8962
  if (!options.quiet)
8915
8963
  printJson({ ok: true, action: "interrupt", lane_id: lane.lane_id, terminal: false, resumable: true, ...remaining });
8916
8964
  return;
@@ -8945,13 +8993,11 @@ async function runInterrupt(ctx, laneId, reason, options = {}) {
8945
8993
  queued_reason: undefined,
8946
8994
  queued_details: undefined,
8947
8995
  interrupted_at: at,
8948
- terminal_reason: "operator_interrupt",
8949
- ...(operatorReason === undefined ? {} : {
8950
- stop_history: [
8951
- ...(current.stop_history ?? []),
8952
- { at, reason: operatorReason },
8953
- ],
8954
- }),
8996
+ terminal_reason: terminalReason,
8997
+ stop_history: [
8998
+ ...(current.stop_history ?? []),
8999
+ { at, reason: historyReason },
9000
+ ],
8955
9001
  };
8956
9002
  });
8957
9003
  stopLaneProcesses(hqRoot, laneId, {
@@ -9307,31 +9353,7 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
9307
9353
  runCompaction: ctx.runCompaction,
9308
9354
  locateTranscript: ctx.locateTranscript,
9309
9355
  },
9310
- spawn: (root, candidate) => {
9311
- if (candidate.loop_mode !== true || candidate.state !== "queued") {
9312
- return spawnLaneWorker(root, candidate, {
9313
- env: ctx.env,
9314
- modelDeps: { readFileSync: ctx.readFileSync, probe: ctx.probeProvider, env: ctx.env },
9315
- existsSync: ctx.existsSync,
9316
- spawnProcess: ctx.spawnProcess,
9317
- proveDetached: ctx.proveDetached,
9318
- killProcess: ctx.killProcess,
9319
- now: ctx.now,
9320
- resolveMachineIdentity: ctx.resolveMachineIdentity,
9321
- runCompaction: ctx.runCompaction,
9322
- locateTranscript: ctx.locateTranscript,
9323
- });
9324
- }
9325
- const admitted = casUpdateLane(root, candidate.lane_id, (current) => ({
9326
- ...current,
9327
- state: "running",
9328
- queued_reason: undefined,
9329
- queued_details: undefined,
9330
- admission_reserved_at: undefined,
9331
- }));
9332
- startLaneLoopController(ctx, candidate.lane_id, true);
9333
- return admitted;
9334
- },
9356
+ spawn: (root, candidate) => startAdmittedLane(ctx, root, candidate),
9335
9357
  throttleDeps: ctx.throttleDeps,
9336
9358
  livenessDeps: livenessDepsFromCtx(ctx),
9337
9359
  presenceConnected: ctx.presenceConnected,
@@ -9693,7 +9715,7 @@ export function registerLanesCommand(parent, deps = {}) {
9693
9715
  return deps.restartLoopController(handlerCtx, laneId);
9694
9716
  const current = readLane(handlerCtx.hqRoot(), laneId);
9695
9717
  if (current.state === "running") {
9696
- await runInterrupt(handlerCtx, laneId, "loop stall question answered; restarting controller", { preserveLoopQueue: true, quiet: true });
9718
+ await runInterrupt(handlerCtx, laneId, "loop stall question answered; restarting controller", { preserveLoopQueue: true, quiet: true, terminalReason: LOOP_STALL_RESTART_REASON });
9697
9719
  }
9698
9720
  const runDirPath = runDir(handlerCtx.hqRoot(), laneId);
9699
9721
  if (loopControllerIsAlive(runDirPath)) {
@@ -9983,7 +10005,7 @@ export function registerLanesCommand(parent, deps = {}) {
9983
10005
  const controllerAlive = loopControllerIsAlive(runDirPath);
9984
10006
  const result = enqueueLoopEnvelope({ runDir: runDirPath, laneId: lane.lane_id, envelope, now: ctx.now });
9985
10007
  if (!controllerAlive && lane.state === "running") {
9986
- await runInterrupt(ctx, lane.lane_id, "loop controller exited; recovering the queued phase", { preserveLoopQueue: true, quiet: true });
10008
+ await runInterrupt(ctx, lane.lane_id, "loop controller exited; recovering the queued phase", { preserveLoopQueue: true, quiet: true, terminalReason: LOOP_CONTROLLER_RECOVERY_REASON });
9987
10009
  }
9988
10010
  const shouldRestart = !controllerAlive && lane.state !== "queued";
9989
10011
  const controllerPid = shouldRestart ? startLaneLoopController(ctx, lane.lane_id) : undefined;
@@ -10378,6 +10400,7 @@ export function registerLanesCommand(parent, deps = {}) {
10378
10400
  }));
10379
10401
  lanes
10380
10402
  .command("stop")
10403
+ .alias("cancel")
10381
10404
  .description("Cancel a queued lane or kill a running one (terminal; use interrupt to keep it resumable)")
10382
10405
  .argument("<lane-id>", "Lane id")
10383
10406
  .option("--reason <text>", "Record the reason in the lane's terminal history")
@@ -1,3 +1,5 @@
1
1
  import { Command } from "commander";
2
+ /** Upper bound on list pages one short-id lookup reads (per cursor chain). */
3
+ export declare const SHORT_ID_MAX_PAGES = 50;
2
4
  export declare function registerMeetingsCommand(program: Command): void;
3
5
  //# sourceMappingURL=meetings.d.ts.map
@@ -251,26 +251,69 @@ function renderSignals(signals) {
251
251
  }
252
252
  console.log();
253
253
  }
254
+ /** The server caps one `/v1/meetings` page at this many rows. */
255
+ const MEETINGS_PAGE_SIZE = "100";
256
+ /** Upper bound on list pages one short-id lookup reads (per cursor chain). */
257
+ export const SHORT_ID_MAX_PAGES = 50;
258
+ /**
259
+ * Resolve the 8-character id `hq meetings list` prints to a full meeting id.
260
+ *
261
+ * `/v1/meetings` is paged: the server caps a page at 100 rows (it ignores a
262
+ * larger `limit`) and may end a page early, even empty, with
263
+ * `truncated: true` when it runs out of time. Reading only the first page
264
+ * missed any meeting past it, so follow `nextToken` (and `nativeNextToken`
265
+ * for HQ Meet calls) until the prefix is found or the list ends. A full
266
+ * 8-character prefix stops at the first page with a match; a shorter prefix
267
+ * reads every page so ambiguity is still reported.
268
+ */
254
269
  async function resolveShortId(token, prefix, query) {
255
270
  if (prefix.includes("-") && prefix.length > 8)
256
271
  return prefix;
257
- const res = await vaultApiFetch({
258
- token,
259
- path: "/v1/meetings",
260
- query: { ...query, limit: "500" },
261
- });
262
- if (!res.ok) {
263
- throw new Error(`Could not resolve meeting ID "${prefix}" — failed to list meetings (${res.status}). Pass the full meeting id.`);
272
+ const stopOnFirstMatch = prefix.length >= 8;
273
+ const matches = new Map();
274
+ let capped = false;
275
+ const fetchPage = async (extra) => {
276
+ const res = await vaultApiFetch({
277
+ token,
278
+ path: "/v1/meetings",
279
+ query: { ...query, limit: MEETINGS_PAGE_SIZE, ...extra },
280
+ });
281
+ if (!res.ok) {
282
+ throw new Error(`Could not resolve meeting ID "${prefix}" — failed to list meetings (${res.status}). Pass the full meeting id.`);
283
+ }
284
+ const data = (await res.json());
285
+ for (const m of data.meetings ?? []) {
286
+ if (typeof m.meetingId === "string" && m.meetingId.startsWith(prefix))
287
+ matches.set(m.meetingId, m);
288
+ }
289
+ return data;
290
+ };
291
+ const first = await fetchPage({});
292
+ let nextToken = first.nextToken;
293
+ for (let page = 1; nextToken && !(stopOnFirstMatch && matches.size > 0); page += 1) {
294
+ if (page >= SHORT_ID_MAX_PAGES) {
295
+ capped = true;
296
+ break;
297
+ }
298
+ nextToken = (await fetchPage({ nextToken })).nextToken;
299
+ }
300
+ let nativeCursor = first.nativeNextToken;
301
+ for (let page = 1; nativeCursor && !(stopOnFirstMatch && matches.size > 0); page += 1) {
302
+ if (page >= SHORT_ID_MAX_PAGES) {
303
+ capped = true;
304
+ break;
305
+ }
306
+ nativeCursor = (await fetchPage({ nativeCursor })).nativeNextToken;
264
307
  }
265
- const data = (await res.json());
266
- const matches = data.meetings.filter((m) => m.meetingId.startsWith(prefix));
267
- if (matches.length === 1)
268
- return matches[0].meetingId;
269
- if (matches.length > 1) {
270
- console.error(chalk.red(`Ambiguous ID prefix "${prefix}" — matches ${matches.length} meetings. Use a longer prefix.`));
308
+ if (matches.size === 1)
309
+ return [...matches.keys()][0];
310
+ if (matches.size > 1) {
311
+ console.error(chalk.red(`Ambiguous ID prefix "${prefix}" — matches ${matches.size} meetings. Use a longer prefix.`));
271
312
  process.exit(1);
272
313
  }
273
- console.error(chalk.red(`No meeting matches ID "${prefix}". It may be older than the meetings shown by \`hq meetings list\`, still processing, or attributed to a different company. Pass the full meeting id, add --company <slug>, or widen the list with \`hq meetings list --limit <n>\`.`));
314
+ console.error(chalk.red(capped
315
+ ? `No meeting matches ID "${prefix}" in the newest ${SHORT_ID_MAX_PAGES * Number(MEETINGS_PAGE_SIZE)} meetings. Pass the full meeting id (shown by \`hq meetings list --json\`).`
316
+ : `No meeting matches ID "${prefix}". It may still be processing or attributed to a different company. Pass the full meeting id, or add --company <slug>.`));
274
317
  process.exit(1);
275
318
  }
276
319
  async function handleApiError(res, json = false) {
@@ -6,10 +6,27 @@
6
6
  * Distinct from `hq doctor` (hook guardrails).
7
7
  */
8
8
  import { Command } from "commander";
9
- import { type MeshCompany, type MeshThread } from "../lib/mesh/api.js";
9
+ import { type MeshCompany, type MeshProjectRepo, type MeshProjectStoryInput, type MeshThread } from "../lib/mesh/api.js";
10
+ import { UnknownProjectStoryStateError } from "../lib/mesh/story-status.js";
10
11
  import { type DaemonInstallPlatform, type InstallPaths } from "../lib/mesh/live/daemon/install.js";
11
12
  import { type RunCommand } from "../lib/service-manager/launchd.js";
12
13
  export declare function formatCheckLines(threads: MeshThread[], company: MeshCompany, projectId?: string): string[];
14
+ /** 400 PROJECT_VIEW_INVALID "Unknown story ..." from a story PATCH. */
15
+ export declare function isUnknownBoardStory(error: unknown): boolean;
16
+ /** The local PRD for this project when it lists `storyId`; null otherwise. */
17
+ export declare function localPrdWithStory(company: {
18
+ companySlug?: string;
19
+ }, projectId: string, storyId: string, hqRoot?: string | undefined): {
20
+ prdPath: string;
21
+ source: ProjectPlanSource;
22
+ } | null;
23
+ type ProjectPlanSource = {
24
+ name?: string;
25
+ description?: string;
26
+ stories: MeshProjectStoryInput[];
27
+ repos?: MeshProjectRepo[];
28
+ rejectedStories: UnknownProjectStoryStateError[];
29
+ };
13
30
  /** Test seam for the usage-attribution question asked by `default set`. */
14
31
  export interface DefaultCompanyPromptDeps {
15
32
  /** True when stdin and stdout are a terminal (a person can answer). */
@@ -61,4 +78,5 @@ export declare function runDaemonStatus(opts: {
61
78
  run?: RunCommand;
62
79
  uid?: number;
63
80
  }): Promise<void>;
81
+ export {};
64
82
  //# sourceMappingURL=mesh.d.ts.map