@indigoai-us/hq-cli 5.126.0 → 5.127.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +211 -0
  2. package/assets/lanes-hooks/hq-lanes-drain.json +15 -0
  3. package/assets/lanes-hooks/hq-lanes-drain.mjs +254 -0
  4. package/assets/lanes-hooks/hq-lanes-drain.sh +21 -0
  5. package/assets/lanes-reconcile/hq-lanes-reconcile.service +11 -0
  6. package/assets/lanes-reconcile/hq-lanes-reconcile.timer +12 -0
  7. package/dist/command-catalog.generated.d.ts +245 -11
  8. package/dist/command-catalog.generated.js +323 -11
  9. package/dist/command-registration-plan.d.ts +1 -0
  10. package/dist/command-registration-plan.js +11 -2
  11. package/dist/commands/agents.d.ts +12 -0
  12. package/dist/commands/agents.js +46 -2
  13. package/dist/commands/lanes.d.ts +55 -16
  14. package/dist/commands/lanes.js +1187 -250
  15. package/dist/commands/mesh.js +254 -19
  16. package/dist/lib/lanes/accounts.d.ts +67 -7
  17. package/dist/lib/lanes/accounts.js +233 -43
  18. package/dist/lib/lanes/compaction.d.ts +96 -0
  19. package/dist/lib/lanes/compaction.js +197 -0
  20. package/dist/lib/lanes/config.d.ts +15 -0
  21. package/dist/lib/lanes/config.js +65 -8
  22. package/dist/lib/lanes/delivery.d.ts +151 -0
  23. package/dist/lib/lanes/delivery.js +354 -0
  24. package/dist/lib/lanes/dropbox.d.ts +96 -0
  25. package/dist/lib/lanes/dropbox.js +408 -0
  26. package/dist/lib/lanes/effort.d.ts +50 -0
  27. package/dist/lib/lanes/effort.js +109 -0
  28. package/dist/lib/lanes/errors.d.ts +2 -2
  29. package/dist/lib/lanes/hooks-install.d.ts +81 -0
  30. package/dist/lib/lanes/hooks-install.js +326 -0
  31. package/dist/lib/lanes/hq-root.d.ts +19 -0
  32. package/dist/lib/lanes/hq-root.js +28 -0
  33. package/dist/lib/lanes/index.d.ts +35 -5
  34. package/dist/lib/lanes/index.js +19 -4
  35. package/dist/lib/lanes/liveness.d.ts +7 -1
  36. package/dist/lib/lanes/liveness.js +64 -5
  37. package/dist/lib/lanes/machine-identity.d.ts +34 -0
  38. package/dist/lib/lanes/machine-identity.js +92 -0
  39. package/dist/lib/lanes/mesh-events.d.ts +109 -0
  40. package/dist/lib/lanes/mesh-events.js +275 -0
  41. package/dist/lib/lanes/outpost-dispatch.d.ts +65 -0
  42. package/dist/lib/lanes/outpost-dispatch.js +90 -0
  43. package/dist/lib/lanes/questions.d.ts +75 -0
  44. package/dist/lib/lanes/questions.js +243 -0
  45. package/dist/lib/lanes/quota.d.ts +95 -0
  46. package/dist/lib/lanes/quota.js +320 -0
  47. package/dist/lib/lanes/reconcile-install.d.ts +69 -0
  48. package/dist/lib/lanes/reconcile-install.js +221 -0
  49. package/dist/lib/lanes/reconcile.d.ts +102 -0
  50. package/dist/lib/lanes/reconcile.js +985 -0
  51. package/dist/lib/lanes/registry.d.ts +18 -5
  52. package/dist/lib/lanes/registry.js +293 -19
  53. package/dist/lib/lanes/spawn.d.ts +274 -0
  54. package/dist/lib/lanes/spawn.js +1363 -0
  55. package/dist/lib/lanes/store.d.ts +7 -3
  56. package/dist/lib/lanes/store.js +150 -32
  57. package/dist/lib/lanes/types.d.ts +346 -4
  58. package/dist/lib/lanes/types.js +138 -5
  59. package/dist/lib/lanes/watcher-main.d.ts +9 -0
  60. package/dist/lib/lanes/watcher-main.js +22 -0
  61. package/dist/lib/lanes/watcher.d.ts +216 -0
  62. package/dist/lib/lanes/watcher.js +859 -0
  63. package/dist/lib/mesh/api.d.ts +71 -7
  64. package/dist/lib/mesh/api.js +167 -28
  65. package/dist/lib/mesh/live/backfill-held.d.ts +18 -7
  66. package/dist/lib/mesh/live/backfill-held.js +66 -21
  67. package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
  68. package/dist/lib/mesh/live/daemon/run.js +111 -54
  69. package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
  70. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +3 -0
  71. package/dist/lib/mesh/live/daemon/transcript-watch.js +31 -8
  72. package/dist/lib/scan-packages/index.d.ts +2 -0
  73. package/dist/lib/scan-packages/index.js +1 -1
  74. package/dist/lib/work-context/state.d.ts +12 -0
  75. package/dist/lib/work-context/state.js +131 -3
  76. package/package.json +3 -1
@@ -7,7 +7,7 @@
7
7
  * compare-and-set on `version`. Histories are capped at 50 at the append site.
8
8
  */
9
9
  import * as fs from "node:fs";
10
- import { type AttributionHistoryEntry, type LaneRecord, type QuestionRecord, type RoutingHistoryEntry, type SeniorHistoryEntry } from "./types.js";
10
+ import { type AttributionHistoryEntry, type LaneRecord, type LaneWriter, type QuestionRecord, type RoutingHistoryEntry, type SeniorHistoryEntry } from "./types.js";
11
11
  export declare const LANES_STORE_REL: string;
12
12
  export declare const LANE_RECORDS_DIR = "lanes";
13
13
  export declare const QUESTION_RECORDS_DIR = "questions";
@@ -27,7 +27,7 @@ export interface StoreDeps {
27
27
  fsyncSync: typeof fs.fsyncSync;
28
28
  chmodSync: typeof fs.chmodSync;
29
29
  }
30
- export declare function resolveStoreHqRoot(hqRoot?: string): string;
30
+ export declare function resolveStoreHqRoot(hqRoot?: string, env?: NodeJS.ProcessEnv, cwd?: string): string;
31
31
  export declare function lanesStoreRoot(hqRoot?: string): string;
32
32
  export declare function laneRecordPath(hqRoot: string, laneId: string): string;
33
33
  export declare function questionRecordPath(hqRoot: string, questionId: string): string;
@@ -53,7 +53,11 @@ export declare function readLane(hqRoot: string, laneId: string, deps?: StoreDep
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
55
  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
- export declare function updateLane(hqRoot: string, laneId: string, expectedVersion: number, updater: (current: LaneRecord) => LaneRecord, deps?: StoreDeps): LaneRecord;
56
+ export declare function updateLane(hqRoot: string, laneId: string, expectedVersion: number, writer: LaneWriter, updater: (current: LaneRecord) => LaneRecord, deps?: StoreDeps): LaneRecord;
57
+ /** Compare-and-set with conflict retry. Callers that race a concurrent writer re-read. */
58
+ export declare function retryUpdateLane(hqRoot: string, laneId: string, writer: LaneWriter, updater: (current: LaneRecord) => LaneRecord, deps?: StoreDeps, attempts?: number): LaneRecord;
59
+ /** Compare-and-set with conflict retry for question records (spec §6.2). */
60
+ export declare function retryUpdateQuestion(hqRoot: string, questionId: string, updater: (current: QuestionRecord) => QuestionRecord, deps?: StoreDeps, attempts?: number): QuestionRecord;
57
61
  export declare function appendLaneSeniorHistory(hqRoot: string, laneId: string, expectedVersion: number, entry: SeniorHistoryEntry, deps?: StoreDeps): LaneRecord;
58
62
  export declare function appendLaneAttribution(hqRoot: string, laneId: string, expectedVersion: number, entry: AttributionHistoryEntry, deps?: StoreDeps): LaneRecord;
59
63
  export declare function createQuestion(hqRoot: string, init: Omit<QuestionRecord, "schema_version" | "question_id" | "version"> & Partial<Pick<QuestionRecord, "schema_version" | "question_id" | "version">>, deps?: StoreDeps): QuestionRecord;
@@ -11,9 +11,9 @@ import * as fs from "node:fs";
11
11
  import * as os from "node:os";
12
12
  import * as path from "node:path";
13
13
  import { generateUlid } from "../mesh/live/ulid.js";
14
- import { findHqRoot } from "../../utils/manifest.js";
15
- import { LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesError, } from "./errors.js";
16
- import { HISTORY_CAP, HOST_KINDS, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isLaneState, isQuestionStatus, isSeniorKind, parseMajorSchema, } from "./types.js";
14
+ import { resolveLanesHqRoot } from "./hq-root.js";
15
+ import { LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesError, isLaneVersionConflict, } from "./errors.js";
16
+ import { HISTORY_CAP, HOST_KINDS, LANE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, SENIOR_KINDS, isHostKind, isLaneProvider, isLaneState, isQuestionStatus, isSeniorKind, parseMajorSchema, REQUIRED_LANE_RECORD_FIELDS, LANE_FIELD_WRITERS, LANE_WRITERS, } from "./types.js";
17
17
  export const LANES_STORE_REL = path.join("workspace", "lanes");
18
18
  export const LANE_RECORDS_DIR = "lanes";
19
19
  export const QUESTION_RECORDS_DIR = "questions";
@@ -38,8 +38,8 @@ function defaultStoreDeps() {
38
38
  chmodSync: fs.chmodSync.bind(fs),
39
39
  };
40
40
  }
41
- export function resolveStoreHqRoot(hqRoot) {
42
- return hqRoot ?? findHqRoot();
41
+ export function resolveStoreHqRoot(hqRoot, env = process.env, cwd) {
42
+ return hqRoot ?? resolveLanesHqRoot({ env, cwd });
43
43
  }
44
44
  export function lanesStoreRoot(hqRoot) {
45
45
  return path.join(resolveStoreHqRoot(hqRoot), LANES_STORE_REL);
@@ -250,12 +250,35 @@ function requireInt(value, field) {
250
250
  }
251
251
  return value;
252
252
  }
253
+ /**
254
+ * Company on a lane/question record is either a slug+uid pair or JSON `null`.
255
+ * Absence (omitted key) is normalised to `null`. Empty strings stay a schema
256
+ * error so a no-company lane cannot be confused with a broken company object.
257
+ */
258
+ function parseOptionalCompanyField(raw, filePath) {
259
+ if (raw.company === undefined) {
260
+ raw.company = null;
261
+ return;
262
+ }
263
+ if (raw.company === null)
264
+ return;
265
+ if (!isPlainObject(raw.company)) {
266
+ throw new LaneSchemaError("company must be an object", { path: filePath });
267
+ }
268
+ requireString(raw.company.slug, "company.slug");
269
+ requireString(raw.company.uid, "company.uid");
270
+ }
253
271
  export function parseLaneRecord(raw, filePath) {
254
272
  if (!isPlainObject(raw)) {
255
273
  throw new LaneSchemaError("Lane record must be a JSON object", {
256
274
  path: filePath,
257
275
  });
258
276
  }
277
+ for (const field of REQUIRED_LANE_RECORD_FIELDS) {
278
+ if (!(field in raw)) {
279
+ throw new LaneSchemaError(`Missing required lane record field ${field}`, { path: filePath, field });
280
+ }
281
+ }
259
282
  const schema_version = requireString(raw.schema_version, "schema_version");
260
283
  parseMajorSchema(schema_version, "hq-lane", 1);
261
284
  requireString(raw.lane_id, "lane_id");
@@ -270,11 +293,7 @@ export function parseLaneRecord(raw, filePath) {
270
293
  if (!isLaneProvider(raw.provider)) {
271
294
  throw new LaneSchemaError(`Unknown provider ${JSON.stringify(raw.provider)}`, { path: filePath, value: raw.provider });
272
295
  }
273
- if (!isPlainObject(raw.company)) {
274
- throw new LaneSchemaError("company must be an object", { path: filePath });
275
- }
276
- requireString(raw.company.slug, "company.slug");
277
- requireString(raw.company.uid, "company.uid");
296
+ parseOptionalCompanyField(raw, filePath);
278
297
  if (!isPlainObject(raw.senior)) {
279
298
  throw new LaneSchemaError("senior must be an object", { path: filePath });
280
299
  }
@@ -330,6 +349,7 @@ export function parseQuestionRecord(raw, filePath) {
330
349
  requireString(raw.lane_id, "lane_id");
331
350
  requireInt(raw.version, "version");
332
351
  requireInt(raw.hop, "hop");
352
+ parseOptionalCompanyField(raw, filePath);
333
353
  if (!isPlainObject(raw.body)) {
334
354
  throw new LaneSchemaError("body must be an object", { path: filePath });
335
355
  }
@@ -338,6 +358,13 @@ export function parseQuestionRecord(raw, filePath) {
338
358
  path: filePath,
339
359
  });
340
360
  }
361
+ // Optional on the record, but merge-on-read spreads it; a non-array from a
362
+ // synced sidecar must fail here, inside the per-sidecar skip, not later.
363
+ if (raw.answer_history !== undefined && !Array.isArray(raw.answer_history)) {
364
+ throw new LaneSchemaError("answer_history must be an array when present", {
365
+ path: filePath,
366
+ });
367
+ }
341
368
  return raw;
342
369
  }
343
370
  /**
@@ -378,9 +405,13 @@ function canonicalJson(value) {
378
405
  return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(obj[key])}`).join(",")}}`;
379
406
  }
380
407
  function historyTimestamp(entry) {
381
- if (!isPlainObject(entry) || typeof entry.at !== "string")
408
+ if (!isPlainObject(entry))
382
409
  return "";
383
- return entry.at;
410
+ if (typeof entry.at === "string")
411
+ return entry.at;
412
+ if (typeof entry.answered_at === "string")
413
+ return entry.answered_at;
414
+ return "";
384
415
  }
385
416
  /**
386
417
  * Attribution identity is the message that caused the re-point (spec §2).
@@ -395,6 +426,12 @@ function attributionIdentity(entry) {
395
426
  function entryIdentity(entry) {
396
427
  return canonicalJson(entry);
397
428
  }
429
+ function questionAnswerHistory(record) {
430
+ return [
431
+ ...(record.answer_history ?? []),
432
+ ...(record.answer ? [record.answer] : []),
433
+ ];
434
+ }
398
435
  function unionHistory(lists, identity, preferred) {
399
436
  const map = new Map();
400
437
  for (const entry of preferred) {
@@ -438,6 +475,7 @@ export function mergeQuestionRecords(live, sidecars) {
438
475
  const base = higherVersionRecord(live, sidecars);
439
476
  return {
440
477
  ...base,
478
+ answer_history: unionHistory(all.map(questionAnswerHistory), entryIdentity, questionAnswerHistory(base)),
441
479
  routing_history: unionHistory(all.map((record) => record.routing_history), entryIdentity, base.routing_history),
442
480
  };
443
481
  }
@@ -487,6 +525,17 @@ export function readQuestion(hqRoot, questionId, deps = defaultStoreDeps()) {
487
525
  throw new LaneNotFoundError("question", questionId);
488
526
  return record;
489
527
  }
528
+ function recordsEquivalentAsideFromClock(current, updated) {
529
+ if (updated === current)
530
+ return true;
531
+ const strip = (record) => {
532
+ const copy = { ...record };
533
+ delete copy.version;
534
+ delete copy.updated_at;
535
+ return copy;
536
+ };
537
+ return JSON.stringify(strip(current)) === JSON.stringify(strip(updated));
538
+ }
490
539
  function casWrite(filePath, expectedVersion, parse, merge, updater, deps) {
491
540
  return withRecordLock(filePath, deps, () => {
492
541
  const current = readMergedRecord(filePath, parse, merge, deps);
@@ -503,6 +552,9 @@ function casWrite(filePath, expectedVersion, parse, merge, updater, deps) {
503
552
  });
504
553
  }
505
554
  const updated = updater(current);
555
+ if (recordsEquivalentAsideFromClock(current, updated)) {
556
+ return current;
557
+ }
506
558
  const next = {
507
559
  ...current,
508
560
  ...updated,
@@ -536,17 +588,76 @@ export function createLane(hqRoot, init, deps = defaultStoreDeps()) {
536
588
  return record;
537
589
  });
538
590
  }
539
- export function updateLane(hqRoot, laneId, expectedVersion, updater, deps = defaultStoreDeps()) {
540
- return casWrite(laneRecordPath(hqRoot, laneId), expectedVersion, parseLaneRecord, mergeLaneRecords, updater, deps);
591
+ /** Value equality over JSON-shaped record fields; the clone breaks reference identity. */
592
+ function fieldUnchanged(before, after) {
593
+ if (Object.is(before, after))
594
+ return true;
595
+ if (before === undefined || after === undefined)
596
+ return false;
597
+ return JSON.stringify(before) === JSON.stringify(after);
598
+ }
599
+ export function updateLane(hqRoot, laneId, expectedVersion, writer, updater, deps = defaultStoreDeps()) {
600
+ if (!LANE_WRITERS.includes(writer)) {
601
+ throw new LanesError("lane_writer_unknown", `Unknown lane writer ${String(writer)}`, { writer });
602
+ }
603
+ return casWrite(laneRecordPath(hqRoot, laneId), expectedVersion, parseLaneRecord, mergeLaneRecords, (current) => {
604
+ // `current` is the pre-update snapshot and stays untouched: the updater
605
+ // receives a deep clone, so mutating its argument in place cannot reach
606
+ // the record the ownership check compares against.
607
+ const updated = updater(structuredClone(current));
608
+ for (const key of Object.keys(updated)) {
609
+ if (key === "version" || key === "updated_at" || fieldUnchanged(current[key], updated[key]))
610
+ continue;
611
+ const allowedWriters = LANE_FIELD_WRITERS[key];
612
+ // Unknown forward-version fields are preserved, never assigned an
613
+ // ownership rule by a v1 reader.
614
+ if (allowedWriters && !allowedWriters.includes(writer)) {
615
+ throw new LanesError("lane_writer_unauthorized", `Writer ${writer} cannot write lane field ${key}`, { writer, field: key });
616
+ }
617
+ }
618
+ return updated;
619
+ }, deps);
620
+ }
621
+ /** Compare-and-set with conflict retry. Callers that race a concurrent writer re-read. */
622
+ export function retryUpdateLane(hqRoot, laneId, writer, updater, deps = defaultStoreDeps(), attempts = 5) {
623
+ let last;
624
+ for (let i = 0; i < attempts; i += 1) {
625
+ try {
626
+ const current = readLane(hqRoot, laneId, deps);
627
+ return updateLane(hqRoot, laneId, current.version, writer, updater, deps);
628
+ }
629
+ catch (err) {
630
+ last = err;
631
+ if (!isLaneVersionConflict(err) || i === attempts - 1)
632
+ throw err;
633
+ }
634
+ }
635
+ throw last;
636
+ }
637
+ /** Compare-and-set with conflict retry for question records (spec §6.2). */
638
+ export function retryUpdateQuestion(hqRoot, questionId, updater, deps = defaultStoreDeps(), attempts = 5) {
639
+ let last;
640
+ for (let i = 0; i < attempts; i += 1) {
641
+ try {
642
+ const current = readQuestion(hqRoot, questionId, deps);
643
+ return updateQuestion(hqRoot, questionId, current.version, updater, deps);
644
+ }
645
+ catch (err) {
646
+ last = err;
647
+ if (!isLaneVersionConflict(err) || i === attempts - 1)
648
+ throw err;
649
+ }
650
+ }
651
+ throw last;
541
652
  }
542
653
  export function appendLaneSeniorHistory(hqRoot, laneId, expectedVersion, entry, deps = defaultStoreDeps()) {
543
- return updateLane(hqRoot, laneId, expectedVersion, (current) => ({
654
+ return updateLane(hqRoot, laneId, expectedVersion, "command", (current) => ({
544
655
  ...current,
545
656
  senior_history: appendCappedHistory(current.senior_history, entry),
546
657
  }), deps);
547
658
  }
548
659
  export function appendLaneAttribution(hqRoot, laneId, expectedVersion, entry, deps = defaultStoreDeps()) {
549
- return updateLane(hqRoot, laneId, expectedVersion, (current) => ({
660
+ return updateLane(hqRoot, laneId, expectedVersion, "command", (current) => ({
550
661
  ...current,
551
662
  attribution_history: appendCappedHistory(current.attribution_history, entry),
552
663
  }), deps);
@@ -600,28 +711,35 @@ export function appendQuestionRouting(hqRoot, questionId, expectedVersion, entry
600
711
  routing_history: appendCappedHistory(current.routing_history, entry),
601
712
  }), deps);
602
713
  }
603
- export function listLaneIds(hqRoot, deps = defaultStoreDeps()) {
604
- const dir = path.join(lanesStoreRoot(hqRoot), LANE_RECORDS_DIR);
605
- if (!deps.existsSync(dir))
606
- return [];
607
- return deps
608
- .readdirSync(dir)
714
+ function jsonRecordIds(names) {
715
+ return names
609
716
  .filter((name) => typeof name === "string" &&
610
717
  name.endsWith(".json") &&
611
718
  !name.startsWith(".") &&
612
719
  !name.includes(".conflict-"))
613
720
  .map((name) => name.slice(0, -".json".length));
614
721
  }
722
+ /**
723
+ * Absent directory → empty list. A path that exists but cannot be listed
724
+ * (ENOTDIR, EACCES, …) is unknown, not empty — throw `store_unreadable`.
725
+ */
726
+ function listRecordIds(dir, label, deps) {
727
+ let names;
728
+ try {
729
+ names = deps.readdirSync(dir);
730
+ }
731
+ catch (err) {
732
+ const code = err.code;
733
+ if (code === "ENOENT")
734
+ return [];
735
+ throw new LanesError("store_unreadable", `Failed to read ${label} directory ${dir}: ${err instanceof Error ? err.message : String(err)}`, { path: dir });
736
+ }
737
+ return jsonRecordIds(names);
738
+ }
739
+ export function listLaneIds(hqRoot, deps = defaultStoreDeps()) {
740
+ return listRecordIds(path.join(lanesStoreRoot(hqRoot), LANE_RECORDS_DIR), "lane", deps);
741
+ }
615
742
  export function listQuestionIds(hqRoot, deps = defaultStoreDeps()) {
616
- const dir = path.join(lanesStoreRoot(hqRoot), QUESTION_RECORDS_DIR);
617
- if (!deps.existsSync(dir))
618
- return [];
619
- return deps
620
- .readdirSync(dir)
621
- .filter((name) => typeof name === "string" &&
622
- name.endsWith(".json") &&
623
- !name.startsWith(".") &&
624
- !name.includes(".conflict-"))
625
- .map((name) => name.slice(0, -".json".length));
743
+ return listRecordIds(path.join(lanesStoreRoot(hqRoot), QUESTION_RECORDS_DIR), "question", deps);
626
744
  }
627
745
  //# sourceMappingURL=store.js.map