@indigoai-us/hq-cli 5.236.0 → 5.237.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.
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.237.0] — 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Lane-store creates and updates record their hq-cli version. User-invoked
|
|
10
|
+
writes from older CLIs are refused, while background writers preserve newer
|
|
11
|
+
stamps and record Outpost owner versions separately. `hq lanes checks` warns
|
|
12
|
+
about newer stamps, and `HQ_LANES_ALLOW_OLDER_WRITE=1` provides a one-command
|
|
13
|
+
override with a warning.
|
|
14
|
+
|
|
5
15
|
## [5.236.0] — 2026-09-27
|
|
6
16
|
|
|
7
17
|
### Changed
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { CLI_VERSION } from "../cli-version.js";
|
|
3
4
|
import { loadLanesConfig } from "../lib/lanes/config.js";
|
|
4
5
|
import { LanesError, isLanesError } from "../lib/lanes/errors.js";
|
|
5
6
|
import { evaluateTrackerChecks } from "../lib/lanes/tracker-checks.js";
|
|
6
|
-
import { laneRecordPath, listLaneIds, listQuestionIds, questionRecordPath, readLane, readQuestion } from "../lib/lanes/store.js";
|
|
7
|
+
import { LANE_STORE_UPGRADE_COMMAND, isLaneStoreVersionNewer, laneRecordPath, listLaneIds, listQuestionIds, questionRecordPath, readLane, readQuestion } from "../lib/lanes/store.js";
|
|
7
8
|
import { createLocalTrackerStore } from "../lib/lanes/tracker-store.js";
|
|
8
9
|
import { LANES_TRACKER_FLAG } from "./lanes-tracker.js";
|
|
9
10
|
function fail(error, message, details = {}) {
|
|
@@ -575,6 +576,25 @@ export async function resolveTrackerCompany(deps, requested) {
|
|
|
575
576
|
async function checkReport(root, company, projects, directiveProjects, projectFilter, now) {
|
|
576
577
|
const selectedProjects = [...projects.values()];
|
|
577
578
|
const laneRecords = readScopedLanes(root, company, selectedProjects);
|
|
579
|
+
const warnings = laneRecords.flatMap((lane) => {
|
|
580
|
+
const storedVersion = [lane.written_by_version, lane.owner_written_by_version]
|
|
581
|
+
.filter((version) => version !== undefined)
|
|
582
|
+
.reduce((newest, candidate) => {
|
|
583
|
+
if (newest === undefined || isLaneStoreVersionNewer(candidate, newest))
|
|
584
|
+
return candidate;
|
|
585
|
+
return newest;
|
|
586
|
+
}, undefined);
|
|
587
|
+
return storedVersion !== undefined && isLaneStoreVersionNewer(storedVersion, CLI_VERSION)
|
|
588
|
+
? [{
|
|
589
|
+
code: "lane_store_newer_cli",
|
|
590
|
+
lane_id: lane.lane_id,
|
|
591
|
+
stored_version: storedVersion,
|
|
592
|
+
running_version: CLI_VERSION,
|
|
593
|
+
upgrade_command: LANE_STORE_UPGRADE_COMMAND,
|
|
594
|
+
message: `Lane ${lane.lane_id} has a local or owner-host copy last written by hq-cli ${storedVersion}, newer than running hq-cli ${CLI_VERSION}. Upgrade before writing.`
|
|
595
|
+
}]
|
|
596
|
+
: [];
|
|
597
|
+
});
|
|
578
598
|
const teams = readTeamInputs(root, company, selectedProjects, laneRecords);
|
|
579
599
|
const questionRecords = readCompanyQuestions(root, company, projectFilter);
|
|
580
600
|
const trackerRecords = await readTrackerCheckRecords(root, company, selectedProjects);
|
|
@@ -586,6 +606,7 @@ async function checkReport(root, company, projects, directiveProjects, projectFi
|
|
|
586
606
|
ok: true,
|
|
587
607
|
company: company.slug,
|
|
588
608
|
generated_at: now.toISOString(),
|
|
609
|
+
...(warnings.length === 0 ? {} : { warnings }),
|
|
589
610
|
checks: evaluateTrackerChecks({
|
|
590
611
|
now,
|
|
591
612
|
thresholds: loadLanesConfig(root).tracker.checks,
|
|
@@ -52,6 +52,9 @@ export declare function tryReadLane(hqRoot: string, laneId: string, deps?: Store
|
|
|
52
52
|
export declare function readLane(hqRoot: string, laneId: string, deps?: StoreDeps): LaneRecord;
|
|
53
53
|
export declare function tryReadQuestion(hqRoot: string, questionId: string, deps?: StoreDeps): QuestionRecord | null;
|
|
54
54
|
export declare function readQuestion(hqRoot: string, questionId: string, deps?: StoreDeps): QuestionRecord;
|
|
55
|
+
/** Semver precedence includes prerelease identifiers (for example, rc.2 > rc.1). */
|
|
56
|
+
export declare function isLaneStoreVersionNewer(storedVersion: string, runningVersion: string): boolean;
|
|
57
|
+
export declare const LANE_STORE_UPGRADE_COMMAND = "npm install -g @indigoai-us/hq-cli@latest";
|
|
55
58
|
export declare function createLane(hqRoot: string, init: Omit<LaneRecord, "schema_version" | "lane_id" | "version" | "created_at" | "updated_at"> & Partial<Pick<LaneRecord, "schema_version" | "lane_id" | "version" | "created_at" | "updated_at">>, deps?: StoreDeps): LaneRecord;
|
|
56
59
|
/** Hierarchy lock: separate from record writes so callers can CAS-write while holding it. */
|
|
57
60
|
export declare function withLaneHireLocks<T>(hqRoot: string, laneIds: readonly string[], fn: () => T, deps?: StoreDeps): T;
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -10,6 +10,8 @@ import { randomBytes } from "node:crypto";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import semver from "semver";
|
|
14
|
+
import { CLI_VERSION } from "../../cli-version.js";
|
|
13
15
|
import { generateUlid } from "../mesh/live/ulid.js";
|
|
14
16
|
import { resolveLanesHqRoot } from "./hq-root.js";
|
|
15
17
|
import { LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesError, isLaneVersionConflict, } from "./errors.js";
|
|
@@ -391,6 +393,26 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
391
393
|
const schema_version = requireString(raw.schema_version, "schema_version");
|
|
392
394
|
parseMajorSchema(schema_version, "hq-lane", 1);
|
|
393
395
|
requireString(raw.lane_id, "lane_id");
|
|
396
|
+
if (raw.written_by_version !== undefined) {
|
|
397
|
+
const writtenByVersion = requireString(raw.written_by_version, "written_by_version");
|
|
398
|
+
if (!semver.valid(writtenByVersion)) {
|
|
399
|
+
throw new LaneSchemaError("written_by_version must be a valid semver version", {
|
|
400
|
+
path: filePath,
|
|
401
|
+
field: "written_by_version",
|
|
402
|
+
value: writtenByVersion,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (raw.owner_written_by_version !== undefined) {
|
|
407
|
+
const ownerWrittenByVersion = requireString(raw.owner_written_by_version, "owner_written_by_version");
|
|
408
|
+
if (!semver.valid(ownerWrittenByVersion)) {
|
|
409
|
+
throw new LaneSchemaError("owner_written_by_version must be a valid semver version", {
|
|
410
|
+
path: filePath,
|
|
411
|
+
field: "owner_written_by_version",
|
|
412
|
+
value: ownerWrittenByVersion,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
394
416
|
requireInt(raw.version, "version");
|
|
395
417
|
requireInt(raw.depth, "depth");
|
|
396
418
|
requireInt(raw.priority, "priority");
|
|
@@ -950,6 +972,52 @@ export function readQuestion(hqRoot, questionId, deps = defaultStoreDeps()) {
|
|
|
950
972
|
throw new LaneNotFoundError("question", questionId);
|
|
951
973
|
return record;
|
|
952
974
|
}
|
|
975
|
+
/** Semver precedence includes prerelease identifiers (for example, rc.2 > rc.1). */
|
|
976
|
+
export function isLaneStoreVersionNewer(storedVersion, runningVersion) {
|
|
977
|
+
if (!semver.valid(storedVersion)) {
|
|
978
|
+
throw new LanesError("lane_store_version_invalid", "Lane store writer version is not valid semver", {
|
|
979
|
+
stored_version: storedVersion,
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
if (!semver.valid(runningVersion)) {
|
|
983
|
+
throw new LanesError("lane_store_version_invalid", "Running hq-cli version is not valid semver", {
|
|
984
|
+
running_version: runningVersion,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
return semver.gt(storedVersion, runningVersion);
|
|
988
|
+
}
|
|
989
|
+
const OLDER_LANE_STORE_WRITE_OVERRIDE = "HQ_LANES_ALLOW_OLDER_WRITE";
|
|
990
|
+
export const LANE_STORE_UPGRADE_COMMAND = "npm install -g @indigoai-us/hq-cli@latest";
|
|
991
|
+
const warnedBackgroundWriterLaneIds = new Set();
|
|
992
|
+
function assertLaneStoreWriteAllowed(lane, laneId, writer) {
|
|
993
|
+
const storedVersion = lane.written_by_version;
|
|
994
|
+
if (storedVersion === undefined || !isLaneStoreVersionNewer(storedVersion, CLI_VERSION))
|
|
995
|
+
return;
|
|
996
|
+
const warning = `Lane ${laneId} was last written by hq-cli ${storedVersion}, newer than running hq-cli ${CLI_VERSION}. Upgrade with \`${LANE_STORE_UPGRADE_COMMAND}\` before writing.`;
|
|
997
|
+
if (writer === "command" || writer === "creator") {
|
|
998
|
+
if (process.env[OLDER_LANE_STORE_WRITE_OVERRIDE] === "1") {
|
|
999
|
+
console.warn(`${warning} Proceeding because ${OLDER_LANE_STORE_WRITE_OVERRIDE}=1 was set for this command.`);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
throw new LanesError("lane_store_newer_cli", `${warning} For a one-command override, set ${OLDER_LANE_STORE_WRITE_OVERRIDE}=1.`, {
|
|
1003
|
+
lane_id: laneId,
|
|
1004
|
+
stored_version: storedVersion,
|
|
1005
|
+
running_version: CLI_VERSION,
|
|
1006
|
+
upgrade_command: LANE_STORE_UPGRADE_COMMAND,
|
|
1007
|
+
override: `${OLDER_LANE_STORE_WRITE_OVERRIDE}=1`,
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
if (!warnedBackgroundWriterLaneIds.has(laneId)) {
|
|
1011
|
+
warnedBackgroundWriterLaneIds.add(laneId);
|
|
1012
|
+
console.warn(`${warning} Continuing background ${writer} writes and preserving the newer stamp.`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
function newestLaneStoreVersion(storedVersion) {
|
|
1016
|
+
if (storedVersion !== undefined && isLaneStoreVersionNewer(storedVersion, CLI_VERSION)) {
|
|
1017
|
+
return storedVersion;
|
|
1018
|
+
}
|
|
1019
|
+
return CLI_VERSION;
|
|
1020
|
+
}
|
|
953
1021
|
function recordsEquivalentAsideFromClock(current, updated) {
|
|
954
1022
|
if (updated === current)
|
|
955
1023
|
return true;
|
|
@@ -961,7 +1029,7 @@ function recordsEquivalentAsideFromClock(current, updated) {
|
|
|
961
1029
|
};
|
|
962
1030
|
return JSON.stringify(strip(current)) === JSON.stringify(strip(updated));
|
|
963
1031
|
}
|
|
964
|
-
function casWrite(filePath, expectedVersion, parse, merge, updater, deps) {
|
|
1032
|
+
function casWrite(filePath, expectedVersion, parse, merge, updater, deps, options = {}) {
|
|
965
1033
|
return withRecordLock(filePath, deps, () => {
|
|
966
1034
|
const current = readMergedRecord(filePath, parse, merge, deps);
|
|
967
1035
|
if (current === null) {
|
|
@@ -980,9 +1048,11 @@ function casWrite(filePath, expectedVersion, parse, merge, updater, deps) {
|
|
|
980
1048
|
if (recordsEquivalentAsideFromClock(current, updated)) {
|
|
981
1049
|
return current;
|
|
982
1050
|
}
|
|
1051
|
+
options.beforeWrite?.(current);
|
|
983
1052
|
const next = {
|
|
984
1053
|
...current,
|
|
985
1054
|
...updated,
|
|
1055
|
+
...options.stamp?.(current),
|
|
986
1056
|
version: current.version + 1,
|
|
987
1057
|
updated_at: deps.now().toISOString(),
|
|
988
1058
|
};
|
|
@@ -996,6 +1066,7 @@ export function createLane(hqRoot, init, deps = defaultStoreDeps()) {
|
|
|
996
1066
|
const filePath = laneRecordPath(hqRoot, lane_id);
|
|
997
1067
|
const record = {
|
|
998
1068
|
...init,
|
|
1069
|
+
written_by_version: CLI_VERSION,
|
|
999
1070
|
hire_grant: init.hire_grant ?? 0,
|
|
1000
1071
|
schema_version: init.schema_version ?? LANE_SCHEMA_VERSION,
|
|
1001
1072
|
lane_id,
|
|
@@ -1060,7 +1131,10 @@ export function updateLane(hqRoot, laneId, expectedVersion, writer, updater, dep
|
|
|
1060
1131
|
}
|
|
1061
1132
|
}
|
|
1062
1133
|
return updated;
|
|
1063
|
-
}, deps
|
|
1134
|
+
}, deps, {
|
|
1135
|
+
beforeWrite: (current) => assertLaneStoreWriteAllowed(current, laneId, writer),
|
|
1136
|
+
stamp: (current) => ({ written_by_version: newestLaneStoreVersion(current.written_by_version) }),
|
|
1137
|
+
});
|
|
1064
1138
|
if (laneStoryLedgerEnabled() && before && laneLedgerRelevantChange(before, updated)) {
|
|
1065
1139
|
return syncStoryLedgerForLane(hqRoot, updated, deps);
|
|
1066
1140
|
}
|
|
@@ -1182,7 +1256,12 @@ export function syncLaneRecordFromOwner(hqRoot, laneId, outpostId, rawOwnerLane,
|
|
|
1182
1256
|
current.outpost_command_id !== undefined
|
|
1183
1257
|
? { ...ownerLane, outpost_command_id: current.outpost_command_id }
|
|
1184
1258
|
: ownerLane;
|
|
1185
|
-
const
|
|
1259
|
+
const merged = mergeLaneRecords(ownerSnapshot, [current]);
|
|
1260
|
+
const synced = {
|
|
1261
|
+
...merged,
|
|
1262
|
+
written_by_version: newestLaneStoreVersion(current.written_by_version),
|
|
1263
|
+
owner_written_by_version: ownerLane.written_by_version,
|
|
1264
|
+
};
|
|
1186
1265
|
parseLaneRecord(synced, filePath);
|
|
1187
1266
|
atomicWriteJson(filePath, synced, deps);
|
|
1188
1267
|
return synced;
|
|
@@ -318,6 +318,10 @@ export interface LaneStory {
|
|
|
318
318
|
export interface LaneRecord {
|
|
319
319
|
schema_version: typeof LANE_SCHEMA_VERSION | string;
|
|
320
320
|
lane_id: string;
|
|
321
|
+
/** CLI release that most recently wrote this lane record. Absent on legacy rows. */
|
|
322
|
+
written_by_version?: string;
|
|
323
|
+
/** CLI release that most recently wrote the owner-host copy synced to primary. */
|
|
324
|
+
owner_written_by_version?: string;
|
|
321
325
|
/** `null` is a core/personal lane (spec §6.2 / §10). Not an empty slug. */
|
|
322
326
|
company: LaneCompanyRef | null;
|
|
323
327
|
/** Explicit Work Mesh attribution for a personal lane; never inferred from cwd. */
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -216,7 +216,7 @@ export const REQUIRED_LANE_RECORD_FIELDS = [
|
|
|
216
216
|
export const LANE_WRITERS = ["creator", "command", "watcher", "reconcile", "spawn", "story-ledger"];
|
|
217
217
|
/** Field ownership at the durable-record mutation boundary (spec §10.1). */
|
|
218
218
|
export const LANE_FIELD_WRITERS = {
|
|
219
|
-
schema_version: ["creator"], lane_id: ["creator"], company: ["creator"],
|
|
219
|
+
schema_version: ["creator"], lane_id: ["creator"], written_by_version: [], owner_written_by_version: [], company: ["creator"],
|
|
220
220
|
project_id: ["command"], story_id: ["command"], no_story_reason: ["creator", "command"], ledger_sync_error: ["story-ledger"], pending_question_ids: ["story-ledger"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator", "command"],
|
|
221
221
|
depth: ["command"], senior: ["command"], senior_history: ["command"],
|
|
222
222
|
hire_grant: ["creator", "command"],
|