@indigoai-us/hq-cli 5.345.135 → 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 (63) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/assets/scaffold/core/scripts/worktree-gc.sh +98 -8
  3. package/dist/command-catalog.generated.d.ts +17 -6
  4. package/dist/command-catalog.generated.js +23 -7
  5. package/dist/commands/core-checkpoint-native.d.ts +10 -0
  6. package/dist/commands/core-checkpoint-native.js +60 -15
  7. package/dist/commands/daemon.js +3 -1
  8. package/dist/commands/integrations-core.d.ts +22 -0
  9. package/dist/commands/integrations-core.js +25 -0
  10. package/dist/commands/integrations-manage.d.ts +24 -1
  11. package/dist/commands/integrations-manage.js +92 -17
  12. package/dist/commands/integrations.d.ts +11 -0
  13. package/dist/commands/integrations.js +100 -19
  14. package/dist/commands/lanes-loop.d.ts +9 -0
  15. package/dist/commands/lanes-loop.js +9 -1
  16. package/dist/commands/lanes.js +69 -54
  17. package/dist/commands/meetings.d.ts +2 -0
  18. package/dist/commands/meetings.js +57 -14
  19. package/dist/commands/mesh.d.ts +19 -1
  20. package/dist/commands/mesh.js +158 -70
  21. package/dist/commands/reindex.js +80 -5
  22. package/dist/commands/storage-offload.d.ts +93 -28
  23. package/dist/commands/storage-offload.js +290 -34
  24. package/dist/commands/storage.js +60 -7
  25. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2550 -1748
  26. package/dist/hq-cloud-sync-runner/package.json +1 -1
  27. package/dist/index.js +4 -0
  28. package/dist/lib/capacity/route.js +24 -0
  29. package/dist/lib/core-utils/session-company.d.ts +11 -0
  30. package/dist/lib/core-utils/session-company.js +34 -0
  31. package/dist/lib/core-utils/worktree-resume.js +44 -4
  32. package/dist/lib/core-utils/worktree-session-owner.d.ts +27 -0
  33. package/dist/lib/core-utils/worktree-session-owner.js +157 -0
  34. package/dist/lib/daemon/catalog.d.ts +2 -0
  35. package/dist/lib/daemon/catalog.js +3 -3
  36. package/dist/lib/daemon/hq-child-bootstrap-stage.d.ts +5 -0
  37. package/dist/lib/daemon/hq-child-bootstrap-stage.js +3 -11
  38. package/dist/lib/daemon/server.js +10 -1
  39. package/dist/lib/daemon/supervisor.js +6 -0
  40. package/dist/lib/daemon/sync-runner-bundle.d.ts +5 -0
  41. package/dist/lib/daemon/sync-runner-bundle.js +4 -12
  42. package/dist/lib/doctor/checks/claude-wiring.js +2 -1
  43. package/dist/lib/doctor/fix/apply.d.ts +20 -0
  44. package/dist/lib/doctor/fix/apply.js +63 -3
  45. package/dist/lib/doctor/fix/remediation.d.ts +1 -1
  46. package/dist/lib/doctor/fix/remediation.js +16 -0
  47. package/dist/lib/doctor/shared.d.ts +11 -1
  48. package/dist/lib/doctor/shared.js +16 -2
  49. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  50. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  51. package/dist/lib/lanes/monitor.js +14 -2
  52. package/dist/lib/mesh/live/daemon/install.d.ts +12 -0
  53. package/dist/lib/mesh/live/daemon/install.js +22 -0
  54. package/dist/lib/mesh/live/flush.js +106 -3
  55. package/dist/lib/mesh/live/session-events-client.d.ts +9 -0
  56. package/dist/lib/mesh/live/session-events-client.js +15 -0
  57. package/dist/lib/work-context/state.d.ts +10 -0
  58. package/dist/lib/work-context/state.js +5 -0
  59. package/dist/utils/fsync-directory.d.ts +17 -0
  60. package/dist/utils/fsync-directory.js +27 -0
  61. package/dist/utils/windows-hide-default.d.ts +42 -0
  62. package/dist/utils/windows-hide-default.js +98 -0
  63. package/package.json +2 -2
@@ -17,7 +17,7 @@ import chalk from "chalk";
17
17
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
18
18
  import { getCompanyUid } from "../utils/vault-api.js";
19
19
  import { redactSensitiveUrl, redactUrlsInText } from "../utils/redact-url.js";
20
- import { IntegrationsCliError, bareProvider, printHealthNotice, fetchConnections, fetchAdminSurface, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
20
+ import { IntegrationsCliError, bareProvider, printHealthNotice, fetchConnections, fetchAdminSurface, fetchAuditPage, AUDIT_PAGE_MAX, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
21
21
  import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, purgeConnection, setReadSafe, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
22
22
  const WRITE_POLICIES = ["auto-allow", "confirm", "deny"];
23
23
  const PERMISSIONS = ["read", "write", "admin"];
@@ -520,30 +520,36 @@ export function registerManageCommands(integrations) {
520
520
  .option("--company <slug>", "Company slug, e.g. indigo")
521
521
  .option("--provider <slug>", "Only this app")
522
522
  .option("--limit <n>", "How many events to show", "20")
523
+ .option("--since <iso>", "Only events at or after this ISO timestamp, e.g. 2026-10-01T00:00:00Z")
523
524
  .option("--json", "Machine-readable output")
524
525
  .action(async (opts) => {
525
- const token = await ensureCognitoIdToken();
526
- const companyUid = await getCompanyUid(token, opts.company);
527
- const surface = await fetchAdminSurface(token, companyUid);
528
526
  const limit = Number(opts.limit);
529
527
  if (!Number.isFinite(limit) || limit < 1) {
530
528
  throw new IntegrationsCliError("--limit must be a positive number.", { expected: true });
531
529
  }
532
- // Normalize BOTH sides: `--provider` is routinely copied from JSON
533
- // output, which carries the qualified `factory:linear` form, while every
534
- // audit row is stored bare. Comparing them raw silently reports "no
535
- // activity" for an app that has plenty.
536
- const want = opts.provider ? bareProvider(opts.provider.trim()).toLowerCase() : undefined;
537
- const rows = surface.audit
538
- .filter((row) => !want || bareProvider(row.provider ?? "").toLowerCase() === want)
539
- .slice(0, limit);
530
+ let sinceMs;
531
+ if (opts.since !== undefined) {
532
+ sinceMs = Date.parse(opts.since);
533
+ if (Number.isNaN(sinceMs)) {
534
+ throw new IntegrationsCliError("--since must be an ISO timestamp, e.g. 2026-10-01T00:00:00Z.", { expected: true });
535
+ }
536
+ }
537
+ const token = await ensureCognitoIdToken();
538
+ const companyUid = await getCompanyUid(token, opts.company);
539
+ const result = await collectAuditRows(token, companyUid, {
540
+ limit: Math.floor(limit),
541
+ provider: opts.provider,
542
+ sinceMs,
543
+ });
544
+ const { rows } = result;
540
545
  if (opts.json) {
541
546
  printJson(rows);
547
+ if (result.pageCapHit)
548
+ console.error(auditPageCapNote());
542
549
  return;
543
550
  }
544
551
  if (rows.length === 0) {
545
552
  console.log("No recorded activity yet.");
546
- return;
547
553
  }
548
554
  for (const row of rows) {
549
555
  const who = row.memberOrAgentName ?? row.memberOrAgent;
@@ -551,10 +557,21 @@ export function registerManageCommands(integrations) {
551
557
  if (row.reason)
552
558
  console.log(chalk.dim(` ${redactUrlsInText(row.reason)}`));
553
559
  }
554
- // hq-pro caps its own feed at 50 events; say so rather than letting a
555
- // truncated list read as "that is everything that ever happened".
556
- if (surface.audit.length > rows.length) {
557
- console.log(chalk.dim(`\nShowing ${rows.length} of ${surface.audit.length} recent events.`));
560
+ if (!result.paged) {
561
+ // An older hq-pro ignores view=audit and returns its surface feed,
562
+ // capped at 50 events; say so rather than letting a truncated list
563
+ // read as "that is everything that ever happened".
564
+ if (result.feedSize > rows.length && rows.length > 0) {
565
+ console.log(chalk.dim(`\nShowing ${rows.length} of ${result.feedSize} recent events.`));
566
+ }
567
+ console.log(chalk.dim("This server returns only the latest 50 events."));
568
+ return;
569
+ }
570
+ if (result.pageCapHit) {
571
+ console.log(chalk.dim(`\n${auditPageCapNote()}`));
572
+ }
573
+ else if (result.more && rows.length > 0) {
574
+ console.log(chalk.dim(`\nShowing ${rows.length} events. Raise --limit or use --since to see more.`));
558
575
  }
559
576
  });
560
577
  integrations
@@ -757,4 +774,62 @@ export function removeGrant(current, toolName, principal) {
757
774
  }
758
775
  return removed ? next : null;
759
776
  }
777
+ /** Upper bound on audit pages one `hq integrations audit` run reads. */
778
+ export const AUDIT_MAX_PAGES = 200;
779
+ function auditPageCapNote() {
780
+ return `Stopped after reading ${AUDIT_MAX_PAGES} pages of activity; narrow the search with --since or --provider.`;
781
+ }
782
+ /**
783
+ * Page the audit feed (newest first) until `limit` rows match, the cursor runs
784
+ * out, a row is older than `sinceMs`, or AUDIT_MAX_PAGES pages were read.
785
+ * `provider` is matched client-side on the bare slug, so the qualified
786
+ * `factory:linear` form copied from JSON output matches bare stored rows.
787
+ */
788
+ export async function collectAuditRows(token, companyUid, opts) {
789
+ const want = opts.provider ? bareProvider(opts.provider.trim()).toLowerCase() : undefined;
790
+ const matches = (row) => !want || bareProvider(row.provider ?? "").toLowerCase() === want;
791
+ const tooOld = (row) => {
792
+ if (opts.sinceMs === undefined)
793
+ return false;
794
+ const at = Date.parse(row.timestamp);
795
+ return !Number.isNaN(at) && at < opts.sinceMs;
796
+ };
797
+ const rows = [];
798
+ let cursor;
799
+ for (let page = 0; page < AUDIT_MAX_PAGES; page += 1) {
800
+ const remaining = opts.limit - rows.length;
801
+ // With a provider filter most rows may not match, so read full pages.
802
+ const pageLimit = want ? AUDIT_PAGE_MAX : Math.min(AUDIT_PAGE_MAX, remaining);
803
+ const res = await fetchAuditPage(token, companyUid, { limit: pageLimit, cursor });
804
+ if (!res.paged) {
805
+ if (page > 0)
806
+ return { rows, paged: true, feedSize: 0, more: false, pageCapHit: false };
807
+ const fallback = res.audit.filter((row) => !tooOld(row) && matches(row));
808
+ return {
809
+ rows: fallback.slice(0, opts.limit),
810
+ paged: false,
811
+ feedSize: res.audit.length,
812
+ more: fallback.length > opts.limit,
813
+ pageCapHit: false,
814
+ };
815
+ }
816
+ for (let i = 0; i < res.audit.length; i += 1) {
817
+ const row = res.audit[i];
818
+ if (tooOld(row))
819
+ return { rows, paged: true, feedSize: 0, more: false, pageCapHit: false };
820
+ if (!matches(row))
821
+ continue;
822
+ if (rows.length >= opts.limit) {
823
+ return { rows, paged: true, feedSize: 0, more: true, pageCapHit: false };
824
+ }
825
+ rows.push(row);
826
+ }
827
+ if (!res.nextCursor)
828
+ return { rows, paged: true, feedSize: 0, more: false, pageCapHit: false };
829
+ if (rows.length >= opts.limit)
830
+ return { rows, paged: true, feedSize: 0, more: true, pageCapHit: false };
831
+ cursor = res.nextCursor;
832
+ }
833
+ return { rows, paged: true, feedSize: 0, more: true, pageCapHit: true };
834
+ }
760
835
  //# sourceMappingURL=integrations-manage.js.map
@@ -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,
@@ -3355,7 +3391,7 @@ async function runCreate(ctx, opts) {
3355
3391
  const requestedHireGrant = opts.hireGrant === undefined
3356
3392
  ? undefined
3357
3393
  : parseHireGrant(opts.hireGrant);
3358
- const requestedPlacement = opts.placement !== undefined
3394
+ let requestedPlacement = opts.placement !== undefined
3359
3395
  ? parsePlacementPolicy(opts.placement)
3360
3396
  : undefined;
3361
3397
  const defaultPlacement = opts.defaultPlacement !== undefined
@@ -3575,6 +3611,13 @@ async function runCreate(ctx, opts) {
3575
3611
  placementGate ??= readLanePlacementEnabled(ctx, resolvePlacementCompany());
3576
3612
  return placementGate;
3577
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
+ }
3578
3621
  if (requestedPlacement !== undefined || defaultPlacement !== undefined) {
3579
3622
  await assertPlacementEnabled(ctx, resolvePlacementCompany(), placementEnabled);
3580
3623
  }
@@ -7159,22 +7202,7 @@ async function runAdmit(ctx, laneId, opts) {
7159
7202
  });
7160
7203
  return;
7161
7204
  }
7162
- const started = spawnLaneWorker(hqRoot, lane, {
7163
- env: ctx.env,
7164
- modelDeps: {
7165
- readFileSync: ctx.readFileSync,
7166
- probe: ctx.probeProvider,
7167
- env: ctx.env,
7168
- },
7169
- existsSync: ctx.existsSync,
7170
- spawnProcess: ctx.spawnProcess,
7171
- proveDetached: ctx.proveDetached,
7172
- killProcess: ctx.killProcess,
7173
- now: ctx.now,
7174
- resolveMachineIdentity: ctx.resolveMachineIdentity,
7175
- runCompaction: ctx.runCompaction,
7176
- locateTranscript: ctx.locateTranscript,
7177
- });
7205
+ const started = startAdmittedLane(ctx, hqRoot, lane);
7178
7206
  const admitted = opts.force && started.state === "running"
7179
7207
  ? casUpdateLane(hqRoot, laneId, (current) => ({ ...current, admitted_reason: "operator_force" }))
7180
7208
  : started;
@@ -8811,7 +8839,7 @@ async function runStop(ctx, laneId, reason, quiet = false) {
8811
8839
  }
8812
8840
  const controllerAlive = loopControllerIsAlive(runDirPath);
8813
8841
  if (!controllerAlive && lane.state === "running") {
8814
- 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 });
8815
8843
  }
8816
8844
  const queued = enqueueLoopEnvelope({ runDir: runDirPath, laneId: lane.lane_id, envelope: { kind: "stop" }, now: ctx.now });
8817
8845
  if (!controllerAlive && lane.state !== "queued")
@@ -8887,6 +8915,13 @@ function stopTerminalWarning(laneId) {
8887
8915
  * session so `hq lanes resume` continues it. Keep-alive resumes only
8888
8916
  * `exited_with_partial_output` interruptions, so it leaves this lane alone.
8889
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
+ }
8890
8925
  async function runInterrupt(ctx, laneId, reason, options = {}) {
8891
8926
  const operatorReason = reason?.trim();
8892
8927
  if (reason !== undefined && !operatorReason) {
@@ -8894,6 +8929,11 @@ async function runInterrupt(ctx, laneId, reason, options = {}) {
8894
8929
  }
8895
8930
  if (operatorReason !== undefined)
8896
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);
8897
8937
  const hqRoot = ctx.hqRoot();
8898
8938
  const lane = readLane(hqRoot, laneId);
8899
8939
  if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
@@ -8918,7 +8958,7 @@ async function runInterrupt(ctx, laneId, reason, options = {}) {
8918
8958
  if (!reason)
8919
8959
  reason = "story-scoped loop phase interrupt";
8920
8960
  const remaining = scoped;
8921
- await runInterrupt(ctx, laneId, reason, { preserveLoopQueue: true, quiet: true });
8961
+ await runInterrupt(ctx, laneId, reason, { preserveLoopQueue: true, quiet: true, terminalReason: options.terminalReason });
8922
8962
  if (!options.quiet)
8923
8963
  printJson({ ok: true, action: "interrupt", lane_id: lane.lane_id, terminal: false, resumable: true, ...remaining });
8924
8964
  return;
@@ -8953,13 +8993,11 @@ async function runInterrupt(ctx, laneId, reason, options = {}) {
8953
8993
  queued_reason: undefined,
8954
8994
  queued_details: undefined,
8955
8995
  interrupted_at: at,
8956
- terminal_reason: "operator_interrupt",
8957
- ...(operatorReason === undefined ? {} : {
8958
- stop_history: [
8959
- ...(current.stop_history ?? []),
8960
- { at, reason: operatorReason },
8961
- ],
8962
- }),
8996
+ terminal_reason: terminalReason,
8997
+ stop_history: [
8998
+ ...(current.stop_history ?? []),
8999
+ { at, reason: historyReason },
9000
+ ],
8963
9001
  };
8964
9002
  });
8965
9003
  stopLaneProcesses(hqRoot, laneId, {
@@ -9315,31 +9353,7 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
9315
9353
  runCompaction: ctx.runCompaction,
9316
9354
  locateTranscript: ctx.locateTranscript,
9317
9355
  },
9318
- spawn: (root, candidate) => {
9319
- if (candidate.loop_mode !== true || candidate.state !== "queued") {
9320
- return spawnLaneWorker(root, candidate, {
9321
- env: ctx.env,
9322
- modelDeps: { readFileSync: ctx.readFileSync, probe: ctx.probeProvider, env: ctx.env },
9323
- existsSync: ctx.existsSync,
9324
- spawnProcess: ctx.spawnProcess,
9325
- proveDetached: ctx.proveDetached,
9326
- killProcess: ctx.killProcess,
9327
- now: ctx.now,
9328
- resolveMachineIdentity: ctx.resolveMachineIdentity,
9329
- runCompaction: ctx.runCompaction,
9330
- locateTranscript: ctx.locateTranscript,
9331
- });
9332
- }
9333
- const admitted = casUpdateLane(root, candidate.lane_id, (current) => ({
9334
- ...current,
9335
- state: "running",
9336
- queued_reason: undefined,
9337
- queued_details: undefined,
9338
- admission_reserved_at: undefined,
9339
- }));
9340
- startLaneLoopController(ctx, candidate.lane_id, true);
9341
- return admitted;
9342
- },
9356
+ spawn: (root, candidate) => startAdmittedLane(ctx, root, candidate),
9343
9357
  throttleDeps: ctx.throttleDeps,
9344
9358
  livenessDeps: livenessDepsFromCtx(ctx),
9345
9359
  presenceConnected: ctx.presenceConnected,
@@ -9701,7 +9715,7 @@ export function registerLanesCommand(parent, deps = {}) {
9701
9715
  return deps.restartLoopController(handlerCtx, laneId);
9702
9716
  const current = readLane(handlerCtx.hqRoot(), laneId);
9703
9717
  if (current.state === "running") {
9704
- 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 });
9705
9719
  }
9706
9720
  const runDirPath = runDir(handlerCtx.hqRoot(), laneId);
9707
9721
  if (loopControllerIsAlive(runDirPath)) {
@@ -9991,7 +10005,7 @@ export function registerLanesCommand(parent, deps = {}) {
9991
10005
  const controllerAlive = loopControllerIsAlive(runDirPath);
9992
10006
  const result = enqueueLoopEnvelope({ runDir: runDirPath, laneId: lane.lane_id, envelope, now: ctx.now });
9993
10007
  if (!controllerAlive && lane.state === "running") {
9994
- 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 });
9995
10009
  }
9996
10010
  const shouldRestart = !controllerAlive && lane.state !== "queued";
9997
10011
  const controllerPid = shouldRestart ? startLaneLoopController(ctx, lane.lane_id) : undefined;
@@ -10386,6 +10400,7 @@ export function registerLanesCommand(parent, deps = {}) {
10386
10400
  }));
10387
10401
  lanes
10388
10402
  .command("stop")
10403
+ .alias("cancel")
10389
10404
  .description("Cancel a queued lane or kill a running one (terminal; use interrupt to keep it resumable)")
10390
10405
  .argument("<lane-id>", "Lane id")
10391
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