@indigoai-us/hq-cli 5.186.0 → 5.188.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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"],
@@ -75,14 +75,27 @@ export function extractLastAssistantText(jsonl) {
75
75
  const type = typeof obj.type === "string" ? obj.type : "";
76
76
  const role = typeof obj.role === "string" ? obj.role : "";
77
77
  const payload = obj.payload;
78
+ const item = obj.item;
79
+ const message = obj.message;
78
80
  let text = "";
79
81
  if (type === "assistant" || role === "assistant") {
80
82
  text = textFromUnknown(obj.message ?? obj.content);
81
83
  }
84
+ else if (message && (message.role === "assistant" || message.type === "assistant")) {
85
+ text = textFromUnknown(message.content ?? message.text ?? message);
86
+ }
82
87
  else if (payload &&
83
88
  (payload.type === "agent_message" || payload.type === "assistant")) {
84
89
  text = textFromUnknown(payload.message ?? payload.content ?? payload.text);
85
90
  }
91
+ else if (type === "item.completed" &&
92
+ item &&
93
+ item.type === "agent_message") {
94
+ text = textFromUnknown(item.text ?? item.content ?? item.message);
95
+ }
96
+ else if (type === "result" && typeof obj.result === "string") {
97
+ text = obj.result;
98
+ }
86
99
  const trimmed = text.trim();
87
100
  if (trimmed)
88
101
  last = trimmed;
@@ -4,6 +4,15 @@ export declare function mergeProjectStories(current: MeshProjectStoryInput[], pl
4
4
  resetStatus?: boolean;
5
5
  }): MeshProjectStoryInput[];
6
6
  export declare function boardStoryStatus(story: MeshProjectStoryInput): string;
7
+ /**
8
+ * Keep only prd-sync task_status events whose Board status actually differs.
9
+ * Missing Board rows stay (ensure will add them). Same-status replays of
10
+ * already-done stories must not be sent again (US-051b).
11
+ */
12
+ export declare function prdSyncEventsThatChangeBoard<T extends {
13
+ taskId: string;
14
+ status: string;
15
+ }>(events: readonly T[], boardStories: readonly MeshProjectStoryInput[]): T[];
7
16
  /** One-line summary of live→next status diffs for existing story ids. */
8
17
  export declare function formatStoryStatusChanges(current: MeshProjectStoryInput[], next: MeshProjectStoryInput[]): string | undefined;
9
18
  /**
@@ -34,6 +34,15 @@ export function boardStoryStatus(story) {
34
34
  return story.status;
35
35
  return story.passes === true ? "done" : "queued";
36
36
  }
37
+ /**
38
+ * Keep only prd-sync task_status events whose Board status actually differs.
39
+ * Missing Board rows stay (ensure will add them). Same-status replays of
40
+ * already-done stories must not be sent again (US-051b).
41
+ */
42
+ export function prdSyncEventsThatChangeBoard(events, boardStories) {
43
+ const byId = new Map(boardStories.map((story) => [story.id, boardStoryStatus(story)]));
44
+ return events.filter((event) => byId.get(event.taskId) !== event.status);
45
+ }
37
46
  /** One-line summary of live→next status diffs for existing story ids. */
38
47
  export function formatStoryStatusChanges(current, next) {
39
48
  const currentById = new Map(current.map((story) => [story.id, story]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.186.0",
3
+ "version": "5.188.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {