@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;
@@ -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 synced = mergeLaneRecords(ownerSnapshot, [current]);
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. */
@@ -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"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.236.0",
3
+ "version": "5.237.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {