@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 +17 -0
- package/dist/commands/lanes-checks.js +34 -15
- package/dist/commands/lanes.js +40 -1
- package/dist/lib/lanes/spawn.js +9 -4
- package/dist/lib/lanes/tracker-mesh.js +29 -6
- package/dist/lib/lanes/types.d.ts +1 -1
- package/package.json +1 -1
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:
|
|
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:
|
|
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
|
}
|
package/dist/commands/lanes.js
CHANGED
|
@@ -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
|
-
|
|
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()
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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", {
|
|
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", {
|
|
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", {
|
|
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. */
|