@indigoai-us/hq-cli 5.187.0 → 5.188.1

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.
@@ -10,7 +10,7 @@ export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
10
10
  export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
11
11
  export type { HostCapacityAccount, HostCapacityProvider, HostCapacitySnapshot, HostPlacementChoice, } from "./host-capacity.js";
12
12
  export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
13
- export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
13
+ export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
14
14
  export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
15
15
  export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
16
16
  export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
@@ -27,7 +27,7 @@ export interface OutpostDispatchResult {
27
27
  stderr: string;
28
28
  }
29
29
  export interface OutpostLaneCommand {
30
- action: "message" | "resume" | "stop" | "show" | "grant";
30
+ action: "message" | "resume" | "stop" | "show" | "grant" | "grant-target";
31
31
  laneId: string;
32
32
  hqBinary?: string;
33
33
  text?: string;
@@ -36,6 +36,7 @@ export interface OutpostLaneCommand {
36
36
  account?: string;
37
37
  model?: string;
38
38
  hire?: string;
39
+ grantContext?: string;
39
40
  compact?: boolean;
40
41
  }
41
42
  export interface OutpostExecPort {
@@ -61,12 +62,21 @@ export declare function parseOutpostPickupPayload(raw: unknown): OutpostPickupPa
61
62
  export declare function buildOutpostPickupCommand(payloadUrl: string): string;
62
63
  /** Bounded, read-only capacity query executed by the target Outpost CLI. */
63
64
  export declare function buildOutpostCapacityCommand(): string;
65
+ /** Read the lane records owned by an Outpost for exact per-account state counts. */
66
+ export declare function buildOutpostLaneListCommand(): string;
64
67
  /** Commit a queued lane move on the host that currently owns its state. */
65
68
  export declare function buildOutpostMoveTargetCommand(input: {
66
69
  laneId: string;
67
70
  host: LaneHost;
68
71
  account: string;
69
72
  }): string;
73
+ /** Move an owned Claude session between accounts while leaving its turn parked. */
74
+ export declare function buildOutpostBalanceMoveCommand(input: {
75
+ laneId: string;
76
+ account: string;
77
+ fromAccount: string;
78
+ expectedState: LaneRecord["state"];
79
+ }): string;
70
80
  /** Idempotently backfill the marker on the selected Outpost. */
71
81
  export declare function buildOutpostIdentityBackfillCommand(expectedId: string): string;
72
82
  /** Build the same `hq outposts exec` remote command used by lane pickup. */
@@ -46,6 +46,10 @@ export function buildOutpostPickupCommand(payloadUrl) {
46
46
  export function buildOutpostCapacityCommand() {
47
47
  return withRemoteHqDir(joinCommandParts(["hq", "lanes", "capacity", "--json"]));
48
48
  }
49
+ /** Read the lane records owned by an Outpost for exact per-account state counts. */
50
+ export function buildOutpostLaneListCommand() {
51
+ return withRemoteHqDir(joinCommandParts(["hq", "lanes", "list", "--json"]));
52
+ }
49
53
  /** Commit a queued lane move on the host that currently owns its state. */
50
54
  export function buildOutpostMoveTargetCommand(input) {
51
55
  return withRemoteHqDir(joinCommandParts([
@@ -61,6 +65,21 @@ export function buildOutpostMoveTargetCommand(input) {
61
65
  input.account,
62
66
  ]));
63
67
  }
68
+ /** Move an owned Claude session between accounts while leaving its turn parked. */
69
+ export function buildOutpostBalanceMoveCommand(input) {
70
+ return withRemoteHqDir(joinCommandParts([
71
+ "hq",
72
+ "lanes",
73
+ "balance-move-target",
74
+ input.laneId,
75
+ "--account",
76
+ input.account,
77
+ "--from-account",
78
+ input.fromAccount,
79
+ "--expected-state",
80
+ input.expectedState,
81
+ ]));
82
+ }
64
83
  /** Idempotently backfill the marker on the selected Outpost. */
65
84
  export function buildOutpostIdentityBackfillCommand(expectedId) {
66
85
  return withRemoteHqDir(joinCommandParts([
@@ -86,6 +105,7 @@ export function buildOutpostLaneCommand(input) {
86
105
  ["--account", input.account],
87
106
  ["--model", input.model],
88
107
  ["--hire", input.hire],
108
+ ["--grant-context", input.grantContext],
89
109
  ]) {
90
110
  if (value !== undefined)
91
111
  args.push(flag, value);
@@ -0,0 +1,12 @@
1
+ export interface SsmRunCommandContextDeps {
2
+ platform?: string;
3
+ parentPid?: number;
4
+ readFileSync?: (path: string, encoding: "utf8") => string;
5
+ }
6
+ /**
7
+ * True only for a Linux process descended from the root-owned SSM Run Command
8
+ * worker chain. Outpost owner-only lane mutations use this to distinguish an
9
+ * SSM dispatch from a normal local CLI invocation.
10
+ */
11
+ export declare function isSsmRunCommandProcess(deps?: SsmRunCommandContextDeps): boolean;
12
+ //# sourceMappingURL=ssm-command-context.d.ts.map
@@ -0,0 +1,52 @@
1
+ import * as fs from "node:fs";
2
+ const SSM_WORKER_CHAIN = [
3
+ "ssm-document-wo",
4
+ "ssm-agent-worke",
5
+ "amazon-ssm-agen",
6
+ ];
7
+ /**
8
+ * True only for a Linux process descended from the root-owned SSM Run Command
9
+ * worker chain. Outpost owner-only lane mutations use this to distinguish an
10
+ * SSM dispatch from a normal local CLI invocation.
11
+ */
12
+ export function isSsmRunCommandProcess(deps = {}) {
13
+ if ((deps.platform ?? process.platform) !== "linux")
14
+ return false;
15
+ const readFileSync = deps.readFileSync ?? ((filePath, encoding) => fs.readFileSync(filePath, encoding));
16
+ let pid = deps.parentPid ?? process.ppid;
17
+ const lineage = [];
18
+ for (let depth = 0; depth < 32 && pid > 1; depth += 1) {
19
+ let status;
20
+ let command;
21
+ try {
22
+ status = readFileSync(`/proc/${pid}/status`, "utf8");
23
+ command = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
24
+ }
25
+ catch (err) {
26
+ const code = err !== null && typeof err === "object" && "code" in err
27
+ ? err.code
28
+ : undefined;
29
+ if (code === "ENOENT" || code === "EACCES" || code === "EPERM")
30
+ return false;
31
+ throw err;
32
+ }
33
+ const parentMatch = /^PPid:\s*(\d+)\s*$/m.exec(status);
34
+ const uidMatch = /^Uid:\s*\d+\s+(\d+)\s+\d+\s+\d+\s*$/m.exec(status);
35
+ if (!parentMatch || !uidMatch || !command)
36
+ return false;
37
+ const parentPid = Number(parentMatch[1]);
38
+ const effectiveUid = Number(uidMatch[1]);
39
+ if (!Number.isSafeInteger(parentPid) || !Number.isSafeInteger(effectiveUid))
40
+ return false;
41
+ lineage.push({ pid, parentPid, command, effectiveUid });
42
+ pid = parentPid;
43
+ }
44
+ for (let start = 0; start <= lineage.length - SSM_WORKER_CHAIN.length; start += 1) {
45
+ const chain = lineage.slice(start, start + SSM_WORKER_CHAIN.length);
46
+ if (chain.every((entry, index) => entry.effectiveUid === 0 && entry.command.startsWith(SSM_WORKER_CHAIN[index]))) {
47
+ return true;
48
+ }
49
+ }
50
+ return false;
51
+ }
52
+ //# sourceMappingURL=ssm-command-context.js.map
@@ -473,6 +473,9 @@ export function parseLaneRecord(raw, filePath) {
473
473
  path: filePath,
474
474
  });
475
475
  }
476
+ if (raw.account_selection !== undefined) {
477
+ parseLaneAccountSelection(raw.account_selection, filePath);
478
+ }
476
479
  if (raw.placement_default !== undefined) {
477
480
  const placement = raw.placement_default;
478
481
  if (!isPlainObject(placement) ||
@@ -494,6 +497,89 @@ export function parseLaneRecord(raw, filePath) {
494
497
  // a parse/rewrite cycle (spec §5.1 field-preservation).
495
498
  return raw;
496
499
  }
500
+ function parseLaneAccountSelection(value, filePath) {
501
+ const failure = (field) => new LaneSchemaError(`Invalid account_selection.${field}`, {
502
+ path: filePath,
503
+ field: `account_selection.${field}`,
504
+ });
505
+ if (!isPlainObject(value))
506
+ throw failure("must be an object");
507
+ const selection = value;
508
+ if (typeof selection.account !== "string" || selection.account.trim() === "")
509
+ throw failure("account");
510
+ if (selection.signal !== "usage" && selection.signal !== "lane_count" && selection.signal !== "explicit")
511
+ throw failure("signal");
512
+ if (selection.reason !== "least_loaded" && selection.reason !== "explicit")
513
+ throw failure("reason");
514
+ if (typeof selection.selected_at !== "string" || !Number.isFinite(Date.parse(selection.selected_at)))
515
+ throw failure("selected_at");
516
+ const parseHost = (host, field) => {
517
+ if (!isPlainObject(host) || !isHostKind(host.kind) || typeof host.id !== "string" || host.id.trim() === "")
518
+ throw failure(field);
519
+ };
520
+ parseHost(selection.target_host, "target_host");
521
+ if (!Array.isArray(selection.considered) || !Array.isArray(selection.skipped_accounts) || !Array.isArray(selection.unavailable_hosts)) {
522
+ throw failure("arrays");
523
+ }
524
+ for (const [index, rowValue] of selection.considered.entries()) {
525
+ if (!isPlainObject(rowValue))
526
+ throw failure(`considered.${index}`);
527
+ const row = rowValue;
528
+ if (typeof row.account !== "string" || row.account.trim() === "" || typeof row.eligible !== "boolean" || typeof row.live_lanes !== "number" || !Number.isInteger(row.live_lanes) || row.live_lanes < 0 || typeof row.counts_complete !== "boolean" || !Array.isArray(row.hosts))
529
+ throw failure(`considered.${index}`);
530
+ if (row.ineligible_reason !== undefined && typeof row.ineligible_reason !== "string")
531
+ throw failure(`considered.${index}.ineligible_reason`);
532
+ if (row.usage !== null) {
533
+ if (!isPlainObject(row.usage))
534
+ throw failure(`considered.${index}.usage`);
535
+ for (const key of ["five_hour_percent", "weekly_percent"]) {
536
+ const percent = row.usage[key];
537
+ if (percent !== undefined && (typeof percent !== "number" || !Number.isFinite(percent) || percent < 0 || percent > 100))
538
+ throw failure(`considered.${index}.usage.${key}`);
539
+ }
540
+ if (row.usage.sampled_at !== undefined && (typeof row.usage.sampled_at !== "string" || !Number.isFinite(Date.parse(row.usage.sampled_at))))
541
+ throw failure(`considered.${index}.usage.sampled_at`);
542
+ }
543
+ for (const [hostIndex, hostValue] of row.hosts.entries()) {
544
+ if (!isPlainObject(hostValue))
545
+ throw failure(`considered.${index}.hosts.${hostIndex}`);
546
+ const host = hostValue;
547
+ parseHost(host.host, `considered.${index}.hosts.${hostIndex}.host`);
548
+ if (typeof host.enabled !== "boolean" || !(host.logged_in === null || typeof host.logged_in === "boolean"))
549
+ throw failure(`considered.${index}.hosts.${hostIndex}.status`);
550
+ if (!(host.live_lanes === null || (typeof host.live_lanes === "number" && Number.isInteger(host.live_lanes) && host.live_lanes >= 0)))
551
+ throw failure(`considered.${index}.hosts.${hostIndex}.live_lanes`);
552
+ if (!isPlainObject(host.lane_counts))
553
+ throw failure(`considered.${index}.hosts.${hostIndex}.lane_counts`);
554
+ for (const key of ["running", "queued", "awaiting_input"]) {
555
+ const count = host.lane_counts[key];
556
+ if (!(count === null || (typeof count === "number" && Number.isInteger(count) && count >= 0)))
557
+ throw failure(`considered.${index}.hosts.${hostIndex}.lane_counts.${key}`);
558
+ }
559
+ if (!["lane_list", "capacity", "capacity_partial", "unavailable"].includes(String(host.count_source)))
560
+ throw failure(`considered.${index}.hosts.${hostIndex}.count_source`);
561
+ }
562
+ }
563
+ for (const [index, rowValue] of selection.skipped_accounts.entries()) {
564
+ if (!isPlainObject(rowValue))
565
+ throw failure(`skipped_accounts.${index}`);
566
+ const row = rowValue;
567
+ if (typeof row.account !== "string" || row.account.trim() === "" || typeof row.reason !== "string" || row.reason.trim() === "")
568
+ throw failure(`skipped_accounts.${index}`);
569
+ parseHost(row.host, `skipped_accounts.${index}.host`);
570
+ }
571
+ for (const [index, rowValue] of selection.unavailable_hosts.entries()) {
572
+ if (!isPlainObject(rowValue))
573
+ throw failure(`unavailable_hosts.${index}`);
574
+ const row = rowValue;
575
+ if (typeof row.reason !== "string" || row.reason.trim() === "")
576
+ throw failure(`unavailable_hosts.${index}`);
577
+ if (isPlainObject(row.host) && row.host.kind === "unknown" && row.host.id === "unknown")
578
+ continue;
579
+ parseHost(row.host, `unavailable_hosts.${index}.host`);
580
+ }
581
+ return selection;
582
+ }
497
583
  export function parseQuestionRecord(raw, filePath) {
498
584
  if (!isPlainObject(raw)) {
499
585
  throw new LaneSchemaError("Question record must be a JSON object", {
@@ -230,6 +230,52 @@ export interface LaneHost {
230
230
  kind: HostKind;
231
231
  id: string;
232
232
  }
233
+ export interface LaneAccountSelectionHostCount {
234
+ host: LaneHost;
235
+ enabled: boolean;
236
+ logged_in: boolean | null;
237
+ live_lanes: number | null;
238
+ lane_counts: {
239
+ running: number | null;
240
+ queued: number | null;
241
+ awaiting_input: number | null;
242
+ };
243
+ count_source: "lane_list" | "capacity" | "capacity_partial" | "unavailable";
244
+ }
245
+ export interface LaneAccountSelectionConsidered {
246
+ account: string;
247
+ eligible: boolean;
248
+ ineligible_reason?: string;
249
+ live_lanes: number;
250
+ counts_complete: boolean;
251
+ usage: {
252
+ five_hour_percent?: number;
253
+ weekly_percent?: number;
254
+ sampled_at?: string;
255
+ } | null;
256
+ hosts: LaneAccountSelectionHostCount[];
257
+ }
258
+ /** Decision evidence saved with a Claude lane when account balancing is active. */
259
+ export interface LaneAccountSelection {
260
+ account: string;
261
+ signal: "usage" | "lane_count" | "explicit";
262
+ reason: "least_loaded" | "explicit";
263
+ target_host: LaneHost;
264
+ selected_at: string;
265
+ considered: LaneAccountSelectionConsidered[];
266
+ skipped_accounts: Array<{
267
+ account: string;
268
+ host: LaneHost;
269
+ reason: string;
270
+ }>;
271
+ unavailable_hosts: Array<{
272
+ host: LaneHost | {
273
+ kind: "unknown";
274
+ id: "unknown";
275
+ };
276
+ reason: string;
277
+ }>;
278
+ }
233
279
  export interface SeniorHistoryEntry {
234
280
  from: SeniorRef;
235
281
  to: SeniorRef;
@@ -309,6 +355,8 @@ export interface LaneRecord {
309
355
  worker_policy_delivery?: "hook" | "prompt";
310
356
  provider: LaneProvider;
311
357
  account?: string;
358
+ /** Recent Claude account-selection evidence for a create or automatic resume move. */
359
+ account_selection?: LaneAccountSelection;
312
360
  /** Eligible accounts for a queue entry created without an explicit account. */
313
361
  admission_accounts?: string[];
314
362
  /**
@@ -224,7 +224,7 @@ export const LANE_FIELD_WRITERS = {
224
224
  pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
225
225
  pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
226
226
  priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
227
- provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
227
+ provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
228
228
  version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
229
229
  last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn"], effort_source: ["creator", "spawn"], fast_mode: ["spawn"],
230
230
  last_message_at: ["command"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.187.0",
3
+ "version": "5.188.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {