@indigoai-us/hq-cli 5.228.2 → 5.228.4

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.228.4] — 2026-09-27
6
+
7
+ ### Fixed
8
+
9
+ - Account history now distinguishes automatic moves caused by a quota hold, an
10
+ unavailable or logged-out account, or uneven load.
11
+
12
+ ## [5.228.3] — 2026-09-27
13
+
14
+ ### Fixed
15
+
16
+ - Work Mesh tracker rows use the local record fields. Missing blocker
17
+ `opened_at` values use `createdAt`, and invalid mesh rows identify Work Mesh
18
+ as their source.
19
+ - KPI and OKR records retain their status when read from Work Mesh, including in
20
+ `kpi list`, `okr list`, and `team --json` output.
21
+
5
22
  ## [5.228.2] — 2026-09-27
6
23
 
7
24
  ## [5.228.1] — 2026-09-27
@@ -435,7 +435,17 @@ function readTeamInputs(root, company, projects, lanes) {
435
435
  export function pathForTrackerRecord(projectDirectory, kind, id) {
436
436
  return path.join(projectDirectory, "tracker", kind, `${id}.json`);
437
437
  }
438
- function recordInvalid(kind, record, projectDirectory, field) {
438
+ function recordInvalid(kind, record, projectDirectory, field, source = "local") {
439
+ if (source === "Work Mesh") {
440
+ return fail("tracker_checks_record_invalid", `Work Mesh tracker ${kind} record ${record.id} has invalid ${field}`, {
441
+ source,
442
+ record_id: record.id,
443
+ kind,
444
+ company: record.company,
445
+ project_id: record.project_id,
446
+ field,
447
+ });
448
+ }
439
449
  return fail("tracker_checks_record_invalid", `Tracker ${kind} record has invalid ${field}`, {
440
450
  kind,
441
451
  id: record.id,
@@ -443,10 +453,18 @@ function recordInvalid(kind, record, projectDirectory, field) {
443
453
  path: pathForTrackerRecord(projectDirectory, kind, record.id),
444
454
  });
445
455
  }
446
- function requiredRecordDate(kind, record, projectDirectory, field) {
456
+ function requiredRecordDate(kind, record, projectDirectory, field, source = "local") {
447
457
  const value = record[field];
448
458
  if (typeof value !== "string" || !Number.isFinite(Date.parse(value)))
449
- return recordInvalid(kind, record, projectDirectory, field);
459
+ return recordInvalid(kind, record, projectDirectory, field, source);
460
+ return value;
461
+ }
462
+ function requiredRecordTimestamp(kind, record, projectDirectory, field, value, source) {
463
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
464
+ if (source === "Work Mesh")
465
+ return recordInvalid(kind, record, projectDirectory, field, source);
466
+ return parseTimestamp(value, field, pathForTrackerRecord(projectDirectory, kind, record.id));
467
+ }
450
468
  return value;
451
469
  }
452
470
  export function listTrackerRecords(projectDirectory, kind) {
@@ -509,6 +527,7 @@ async function readTrackerCheckRecords(root, company, projects, deps) {
509
527
  const kpis = [];
510
528
  const directives = [];
511
529
  const useMesh = await deps.trackerMeshEnabled(company);
530
+ const source = useMesh ? "Work Mesh" : "local";
512
531
  for (const project of projects) {
513
532
  const absoluteDirectory = path.join(root, "companies", company.slug, "projects", project.projectId);
514
533
  const store = useMesh
@@ -522,40 +541,40 @@ async function readTrackerCheckRecords(root, company, projects, deps) {
522
541
  for (const record of records.blocker) {
523
542
  if (record.company !== company.slug || record.project_id !== project.projectId)
524
543
  continue;
525
- const openedAt = requiredRecordDate("blocker", record, absoluteDirectory, "opened_at");
544
+ const openedAt = requiredRecordDate("blocker", record, absoluteDirectory, "opened_at", source);
526
545
  const resolvedAt = record.resolved_at;
527
546
  if (resolvedAt !== null && (typeof resolvedAt !== "string" || !Number.isFinite(Date.parse(resolvedAt))))
528
- recordInvalid("blocker", record, absoluteDirectory, "resolved_at");
547
+ recordInvalid("blocker", record, absoluteDirectory, "resolved_at", source);
529
548
  const nextStep = record.next_step;
530
549
  if (nextStep !== undefined && nextStep !== null && typeof nextStep !== "string")
531
- recordInvalid("blocker", record, absoluteDirectory, "next_step");
550
+ recordInvalid("blocker", record, absoluteDirectory, "next_step", source);
532
551
  blockers.push({ id: record.id, opened_at: openedAt, resolved_at: resolvedAt, next_step: typeof nextStep === "string" ? nextStep : null });
533
552
  }
534
553
  for (const record of records.kpi) {
535
554
  if (record.company !== company.slug || record.project_id !== project.projectId)
536
555
  continue;
537
- const createdAt = requiredRecordDate("kpi", record, absoluteDirectory, "created_at");
556
+ const createdAt = requiredRecordDate("kpi", record, absoluteDirectory, "created_at", source);
538
557
  if (!Array.isArray(record.values))
539
- recordInvalid("kpi", record, absoluteDirectory, "values");
558
+ recordInvalid("kpi", record, absoluteDirectory, "values", source);
540
559
  const values = record.values.map((value) => {
541
560
  if (!isObject(value))
542
- recordInvalid("kpi", record, absoluteDirectory, "values");
543
- return { at: parseTimestamp(value.at, "values.at", pathForTrackerRecord(absoluteDirectory, "kpi", record.id)) };
561
+ recordInvalid("kpi", record, absoluteDirectory, "values", source);
562
+ return { at: requiredRecordTimestamp("kpi", record, absoluteDirectory, "values[].at", value.at, source) };
544
563
  });
545
564
  kpis.push({ id: record.id, created_at: createdAt, values });
546
565
  }
547
566
  for (const record of records.directive) {
548
567
  if (record.company !== company.slug || record.project_id !== project.projectId)
549
568
  continue;
550
- const createdAt = requiredRecordDate("directive", record, absoluteDirectory, "created_at");
569
+ const createdAt = requiredRecordDate("directive", record, absoluteDirectory, "created_at", source);
551
570
  if (!Array.isArray(record.teams) || record.teams.some((team) => typeof team !== "string"))
552
- recordInvalid("directive", record, absoluteDirectory, "teams");
571
+ recordInvalid("directive", record, absoluteDirectory, "teams", source);
553
572
  if (!Array.isArray(record.acknowledgements))
554
- recordInvalid("directive", record, absoluteDirectory, "acknowledgements");
573
+ recordInvalid("directive", record, absoluteDirectory, "acknowledgements", source);
555
574
  const acknowledgements = record.acknowledgements.map((ack) => {
556
575
  if (!isObject(ack) || typeof ack.team !== "string" || typeof ack.lane_id !== "string")
557
- recordInvalid("directive", record, absoluteDirectory, "acknowledgements");
558
- return { team: ack.team, lane_id: ack.lane_id, at: parseTimestamp(ack.at, "acknowledgements.at", pathForTrackerRecord(absoluteDirectory, "directive", record.id)) };
576
+ recordInvalid("directive", record, absoluteDirectory, "acknowledgements", source);
577
+ return { team: ack.team, lane_id: ack.lane_id, at: requiredRecordTimestamp("directive", record, absoluteDirectory, "acknowledgements[].at", ack.at, source) };
559
578
  });
560
579
  directives.push({ id: record.id, created_at: createdAt, teams: record.teams, acknowledgements });
561
580
  }
@@ -5635,9 +5635,20 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
5635
5635
  ...(model !== undefined ? { model } : {}),
5636
5636
  unavailableHosts: balanceHosts.unavailableHosts,
5637
5637
  });
5638
+ const historyReason = lane.account === undefined
5639
+ ? "load_balance"
5640
+ : currentConfig?.enabled !== true
5641
+ ? "account_unavailable"
5642
+ : currentLogin === false
5643
+ ? "recorded_account_logged_out"
5644
+ : currentLogin !== true
5645
+ ? "account_unavailable"
5646
+ : quotaBlockFor(loadQuotaState(hqRoot), "claude", lane.account, model, ctx.now()).blocked
5647
+ ? "usage_limit"
5648
+ : "load_balance";
5638
5649
  return {
5639
5650
  selection: { ...selected, selected_at: ctx.now().toISOString() },
5640
- ...(repairUnavailableAccount ? { historyReason: "recorded_account_logged_out" } : {}),
5651
+ historyReason,
5641
5652
  ...(preservedModel !== undefined ? { preservedModel } : {}),
5642
5653
  };
5643
5654
  }
@@ -6269,6 +6280,7 @@ async function applyClaudeBalanceMove(ctx, suggestion) {
6269
6280
  const moved = await prepareAccountMove(ctx, lane, {
6270
6281
  account: suggestion.to,
6271
6282
  cliModel: undefined,
6283
+ accountHistoryReason: automaticBalanceMoveHistoryReason(ctx, lane),
6272
6284
  deferTurnStart: true,
6273
6285
  });
6274
6286
  return { lane_id: moved.lane.lane_id, host: moved.lane.host, from: moved.from ?? suggestion.from, to: moved.to, state: moved.lane.state };
@@ -6324,6 +6336,7 @@ async function runClaudeBalanceMoveTarget(ctx, laneId, opts) {
6324
6336
  const moved = await prepareAccountMove(ctx, lane, {
6325
6337
  account,
6326
6338
  cliModel: undefined,
6339
+ accountHistoryReason: automaticBalanceMoveHistoryReason(ctx, lane),
6327
6340
  deferTurnStart: true,
6328
6341
  });
6329
6342
  printJson({
@@ -6335,6 +6348,32 @@ async function runClaudeBalanceMoveTarget(ctx, laneId, opts) {
6335
6348
  account: moved.lane.account,
6336
6349
  });
6337
6350
  }
6351
+ function automaticBalanceMoveHistoryReason(ctx, lane) {
6352
+ const hqRoot = ctx.hqRoot();
6353
+ const config = loadLanesConfig(hqRoot);
6354
+ const accountId = lane.account;
6355
+ if (accountId === undefined)
6356
+ return "load_balance";
6357
+ const source = config.providers[lane.provider].accounts[accountId];
6358
+ if (source?.enabled !== true)
6359
+ return "account_unavailable";
6360
+ const login = accountStatus(lane.provider, accountId, config, {
6361
+ existsSync: ctx.existsSync,
6362
+ hostname: ctx.hostname(),
6363
+ probe: ctx.probeProvider,
6364
+ env: ctx.env,
6365
+ }).logged_in;
6366
+ if (login === false)
6367
+ return "recorded_account_logged_out";
6368
+ if (login !== true)
6369
+ return "account_unavailable";
6370
+ const provider = config.providers[lane.provider];
6371
+ const model = lane.model ?? source.model ?? provider.model;
6372
+ if (quotaBlockFor(loadQuotaState(hqRoot), lane.provider, accountId, model, ctx.now()).blocked) {
6373
+ return "usage_limit";
6374
+ }
6375
+ return "load_balance";
6376
+ }
6338
6377
  function laneWithRecoveredProviderSession(hqRoot, lane) {
6339
6378
  const manifest = readSpawnManifest(laneRunDir(hqRoot, lane.lane_id));
6340
6379
  const sessionId = (typeof lane.provider_session_id === "string" && lane.provider_session_id.trim()
@@ -189,7 +189,7 @@ function selectSpawnAccount(input) {
189
189
  const operatorMovePending = lane.pending_resume !== undefined &&
190
190
  lane.account_history?.at(-1)?.reason === "operator_move";
191
191
  if (current === undefined || !current.enabled) {
192
- reason = repairRecordedAccount ? "recorded_account_logged_out" : "account_unavailable";
192
+ reason = "account_unavailable";
193
193
  refusal = new AccountResolutionError(current === undefined ? "unknown_account" : "account_disabled", `Account ${JSON.stringify(lane.account)} is unavailable for ${lane.provider}`, { provider: lane.provider, account: lane.account });
194
194
  }
195
195
  else {
@@ -244,7 +244,11 @@ function selectSpawnAccount(input) {
244
244
  account: lane.account,
245
245
  });
246
246
  }
247
- if (reason === "recorded_account_logged_out" && lane.provider === "claude") {
247
+ const recoverRecordedClaudeModel = lane.provider === "claude" &&
248
+ reason !== undefined &&
249
+ (reason === "recorded_account_logged_out" ||
250
+ (reason === "account_unavailable" && repairRecordedAccount && recordedModel !== undefined));
251
+ if (recoverRecordedClaudeModel) {
248
252
  const lanes = listLaneIds(hqRoot)
249
253
  .map((laneId) => readLane(hqRoot, laneId))
250
254
  .filter((record) => record.host.kind === lane.host.kind && record.host.id === lane.host.id);
@@ -268,7 +272,8 @@ function selectSpawnAccount(input) {
268
272
  });
269
273
  return {
270
274
  account: resolveAccount(lane.provider, selection.account, config, context),
271
- changeReason: "recorded_account_logged_out",
275
+ changeReason: reason,
276
+ ...(recordedModel !== undefined ? { preservedModel: recordedModel } : {}),
272
277
  };
273
278
  }
274
279
  const allowedAccounts = Object.entries(config.providers[lane.provider].accounts)
@@ -1726,7 +1731,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1726
1731
  process.stderr.write(`hq lanes: account bind failed for ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}\n`);
1727
1732
  throw err;
1728
1733
  }
1729
- const preservedAccountModel = selection.changeReason === "recorded_account_logged_out" ? recordedModel : undefined;
1734
+ const preservedAccountModel = selection.preservedModel;
1730
1735
  if (preservedAccountModel !== undefined)
1731
1736
  pinnedModel = preservedAccountModel;
1732
1737
  const resolved = selection.account;
@@ -3,6 +3,12 @@ import { meshJson, requireToken } from "../mesh/client.js";
3
3
  import { LanesError } from "./errors.js";
4
4
  const ROOT = "/v1/work-mesh/tracker/records";
5
5
  const MAX_RECORD_PAGES = 20;
6
+ const RECORD_FIELDS = {
7
+ blocker: ["story_id", "what", "waiting_on", "next_step", "opened_at", "resolved_at", "resolution", "import_key", "source_path", "source_item_id"],
8
+ kpi: ["name", "definition", "unit", "target", "direction", "status", "approved_by", "approved_at", "values", "import_key", "source_path", "source_item_id"],
9
+ okr: ["objective", "period", "key_results", "status", "approved_by", "approved_at", "import_key", "source_path", "source_item_id"],
10
+ directive: ["text", "date", "source_url", "teams", "policy_path", "acknowledgements", "import_key", "source_path", "source_item_id"],
11
+ };
6
12
  function fail(error, message, details = {}) {
7
13
  throw new LanesError(error, message, details);
8
14
  }
@@ -50,23 +56,30 @@ function fromMesh(value) {
50
56
  }
51
57
  function recordFromMeta(raw, company, projectId, expectedKind) {
52
58
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
53
- return fail("tracker_mesh_response_invalid", "Work Mesh returned a malformed tracker record", { project_id: projectId, kind: expectedKind });
59
+ return fail("tracker_mesh_response_invalid", "Work Mesh returned a malformed tracker record", {
60
+ source: "Work Mesh", kind: expectedKind, company: company.slug, project_id: projectId,
61
+ });
54
62
  }
55
63
  const meta = raw;
56
64
  if (meta.companyUid !== company.uid || meta.projectId !== projectId || meta.kind !== expectedKind || typeof meta.threadId !== "string" || !meta.threadId) {
57
- return fail("tracker_mesh_response_invalid", "Work Mesh tracker record identity did not match the requested company, project, and kind", { project_id: projectId, kind: expectedKind });
65
+ return fail("tracker_mesh_response_invalid", "Work Mesh tracker record identity did not match the requested company, project, and kind", {
66
+ source: "Work Mesh",
67
+ ...(typeof meta.threadId === "string" && meta.threadId ? { record_id: meta.threadId } : {}),
68
+ kind: expectedKind,
69
+ company: company.slug,
70
+ project_id: projectId,
71
+ });
58
72
  }
59
73
  const fields = fromMesh(meta);
60
74
  const createdAt = typeof meta.createdAt === "string" ? meta.createdAt : "";
61
75
  const updatedAt = typeof meta.lastActivityAt === "string" ? meta.lastActivityAt : createdAt;
62
76
  const version = meta.version;
63
77
  if (!Number.isSafeInteger(version) || version < 1 || !Number.isFinite(Date.parse(createdAt)) || !Number.isFinite(Date.parse(updatedAt))) {
64
- return fail("tracker_mesh_response_invalid", "Work Mesh tracker record has invalid version or timestamps", { record_id: meta.threadId, kind: expectedKind });
78
+ return fail("tracker_mesh_response_invalid", "Work Mesh tracker record has invalid version or timestamps", {
79
+ source: "Work Mesh", record_id: meta.threadId, kind: expectedKind, company: company.slug, project_id: projectId,
80
+ });
65
81
  }
66
- const { company_uid: _companyUid, project_id: _projectId, thread_id: _threadId, thread_status: threadStatus, ...rest } = fields;
67
82
  const record = {
68
- ...rest,
69
- ...(typeof threadStatus === "string" && rest.status === undefined ? { status: threadStatus } : {}),
70
83
  schema_version: "hq-lanes-tracker-record.v1",
71
84
  kind: expectedKind,
72
85
  id: meta.threadId,
@@ -76,6 +89,16 @@ function recordFromMeta(raw, company, projectId, expectedKind) {
76
89
  created_at: createdAt,
77
90
  updated_at: updatedAt,
78
91
  };
92
+ for (const field of RECORD_FIELDS[expectedKind]) {
93
+ if (Object.hasOwn(fields, field))
94
+ record[field] = fields[field];
95
+ }
96
+ // hq-pro stores KPI and OKR status in the shared threadStatus metadata field.
97
+ if ((expectedKind === "kpi" || expectedKind === "okr") && !Object.hasOwn(record, "status") && Object.hasOwn(fields, "thread_status")) {
98
+ record.status = fields.thread_status;
99
+ }
100
+ if (expectedKind === "blocker" && !Object.hasOwn(record, "opened_at"))
101
+ record.opened_at = createdAt;
79
102
  return record;
80
103
  }
81
104
  function identityMatches(record, kind, id, company, projectId) {
@@ -523,7 +523,7 @@ export interface AccountHistoryEntry {
523
523
  from: string | null;
524
524
  to: string;
525
525
  at: string;
526
- reason: "first_assignment" | "model_change" | "usage_limit" | "account_unavailable" | "recorded_account_logged_out" | "operator_move";
526
+ reason: "first_assignment" | "model_change" | "usage_limit" | "account_unavailable" | "recorded_account_logged_out" | "load_balance" | "operator_move";
527
527
  }
528
528
  export interface PendingResume {
529
529
  /** Fresh answered-question launches need no previous session id. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.228.2",
3
+ "version": "5.228.4",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {