@indigoai-us/hq-cli 5.124.0 → 5.126.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 (80) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/assets/bot-workers/setup/skills/joining-a-team.md +125 -0
  3. package/assets/bot-workers/setup/skills/your-business.md +270 -0
  4. package/assets/bot-workers/setup/worker.yaml +189 -58
  5. package/dist/command-catalog.generated.d.ts +414 -5
  6. package/dist/command-catalog.generated.js +531 -5
  7. package/dist/command-registration-plan.d.ts +6 -0
  8. package/dist/command-registration-plan.js +1 -0
  9. package/dist/commands/bot.d.ts +10 -0
  10. package/dist/commands/bot.js +6 -0
  11. package/dist/commands/lanes.d.ts +75 -0
  12. package/dist/commands/lanes.js +2032 -0
  13. package/dist/commands/login.js +49 -6
  14. package/dist/commands/mesh.js +133 -28
  15. package/dist/commands/onboard-create-identity.d.ts +26 -0
  16. package/dist/commands/onboard-create-identity.js +23 -0
  17. package/dist/commands/onboard.js +13 -4
  18. package/dist/lib/bot/progress.d.ts +6 -0
  19. package/dist/lib/bot/progress.js +16 -0
  20. package/dist/lib/bot/runtime/grok.d.ts +30 -7
  21. package/dist/lib/bot/runtime/grok.js +86 -8
  22. package/dist/lib/bot/runtime/index.js +15 -8
  23. package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
  24. package/dist/lib/bot/runtime/messages-stream.js +49 -3
  25. package/dist/lib/doctor/checks/work-context.js +4 -2
  26. package/dist/lib/lanes/accounts.d.ts +43 -0
  27. package/dist/lib/lanes/accounts.js +128 -0
  28. package/dist/lib/lanes/config.d.ts +56 -0
  29. package/dist/lib/lanes/config.js +351 -0
  30. package/dist/lib/lanes/errors.d.ts +39 -0
  31. package/dist/lib/lanes/errors.js +72 -0
  32. package/dist/lib/lanes/index.d.ts +19 -0
  33. package/dist/lib/lanes/index.js +12 -0
  34. package/dist/lib/lanes/liveness.d.ts +89 -0
  35. package/dist/lib/lanes/liveness.js +273 -0
  36. package/dist/lib/lanes/registry.d.ts +103 -0
  37. package/dist/lib/lanes/registry.js +247 -0
  38. package/dist/lib/lanes/store.d.ts +64 -0
  39. package/dist/lib/lanes/store.js +627 -0
  40. package/dist/lib/lanes/throttle.d.ts +89 -0
  41. package/dist/lib/lanes/throttle.js +297 -0
  42. package/dist/lib/lanes/types.d.ts +193 -0
  43. package/dist/lib/lanes/types.js +217 -0
  44. package/dist/lib/mesh/live/backfill-held.d.ts +2 -0
  45. package/dist/lib/mesh/live/backfill-held.js +8 -4
  46. package/dist/lib/mesh/live/daemon/board-refresh.d.ts +3 -0
  47. package/dist/lib/mesh/live/daemon/board-refresh.js +6 -0
  48. package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
  49. package/dist/lib/mesh/live/daemon/config.js +75 -0
  50. package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
  51. package/dist/lib/mesh/live/daemon/doctor.js +21 -1
  52. package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
  53. package/dist/lib/mesh/live/daemon/index.js +3 -3
  54. package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
  55. package/dist/lib/mesh/live/daemon/install.js +444 -2
  56. package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
  57. package/dist/lib/mesh/live/daemon/paths.js +2 -0
  58. package/dist/lib/mesh/live/daemon/run.d.ts +11 -0
  59. package/dist/lib/mesh/live/daemon/run.js +129 -10
  60. package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
  61. package/dist/lib/mesh/live/flush.d.ts +3 -0
  62. package/dist/lib/mesh/live/flush.js +11 -0
  63. package/dist/lib/scan-packages/index.js +17 -12
  64. package/dist/lib/work-context/company.d.ts +8 -0
  65. package/dist/lib/work-context/company.js +62 -18
  66. package/dist/lib/work-context/config.d.ts +55 -0
  67. package/dist/lib/work-context/config.js +333 -67
  68. package/dist/lib/work-context/contract.d.ts +2 -0
  69. package/dist/lib/work-context/default-company.d.ts +43 -0
  70. package/dist/lib/work-context/default-company.js +83 -0
  71. package/dist/lib/work-context/outbox.d.ts +5 -1
  72. package/dist/lib/work-context/outbox.js +5 -0
  73. package/dist/lib/work-context/reconcile.d.ts +4 -0
  74. package/dist/lib/work-context/reconcile.js +86 -9
  75. package/dist/lib/work-context/state.d.ts +2 -0
  76. package/dist/lib/work-context/state.js +14 -0
  77. package/dist/lib/work-context/windows-acl.d.ts +123 -0
  78. package/dist/lib/work-context/windows-acl.js +425 -0
  79. package/dist/utils/pack-contributions.js +5 -4
  80. package/package.json +1 -1
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Session liveness from the work-mesh spool (spec §9).
3
+ *
4
+ * Source: ~/.hq/work-mesh/spool.jsonl, events session_start / turn_start /
5
+ * turn_end / session_end keyed by session id. Nothing latches, including
6
+ * `ended` — a later event for a reused id supersedes session_end.
7
+ *
8
+ * An index is a single compact file (workspace/lanes/sessions/index.json).
9
+ * This module never stats workspace/sessions/.
10
+ */
11
+ import * as fs from "node:fs";
12
+ import * as os from "node:os";
13
+ import * as path from "node:path";
14
+ import { workMeshSpoolPath, workMeshRoot } from "../mesh/live/paths.js";
15
+ import { DEFAULT_LIVE_WITHIN_MS, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, loadLanesConfig, } from "./config.js";
16
+ import { lanesStoreRoot } from "./store.js";
17
+ export const LIVE_WITHIN_MS = DEFAULT_LIVE_WITHIN_MS;
18
+ export const TURN_START_LOST_MS = DEFAULT_TURN_START_LOST_MS;
19
+ export const SILENT_LOST_MS = DEFAULT_SILENT_LOST_MS;
20
+ export const LIVENESS_EVENT_KINDS = [
21
+ "session_start",
22
+ "turn_start",
23
+ "turn_end",
24
+ "session_end",
25
+ ];
26
+ export function defaultLivenessDeps() {
27
+ return {
28
+ now: () => Date.now(),
29
+ homedir: () => os.homedir(),
30
+ env: process.env,
31
+ readFileSync: (file, encoding) => fs.readFileSync(file, encoding),
32
+ writeFileSync: fs.writeFileSync.bind(fs),
33
+ renameSync: fs.renameSync.bind(fs),
34
+ unlinkSync: fs.unlinkSync.bind(fs),
35
+ existsSync: (file) => fs.existsSync(file),
36
+ mkdirSync: fs.mkdirSync.bind(fs),
37
+ statSync: (file) => fs.statSync(file),
38
+ };
39
+ }
40
+ export function defaultSpoolPath(deps = defaultLivenessDeps()) {
41
+ return workMeshSpoolPath(workMeshRoot(deps.homedir(), deps.env));
42
+ }
43
+ export function livenessIndexPath(hqRoot) {
44
+ return path.join(lanesStoreRoot(hqRoot), "sessions", "index.json");
45
+ }
46
+ function isPlainObject(value) {
47
+ return value !== null && typeof value === "object" && !Array.isArray(value);
48
+ }
49
+ /**
50
+ * Classify one session from its last spool event. `ended` is not latched:
51
+ * a later event for the same id is classified from that later event.
52
+ *
53
+ * Mid-turn (`turn_start` younger than 2h) stays `live` even after the 10
54
+ * minute recency window — spec §9's prose treats that as working, not idle.
55
+ * `session_end` as the last event is `ended` even when recent (fast path).
56
+ */
57
+ export function classifySessionLiveness(entry, nowMs, presenceConnected = false, windows = {
58
+ live_within_ms: LIVE_WITHIN_MS,
59
+ turn_start_lost_ms: TURN_START_LOST_MS,
60
+ silent_lost_ms: SILENT_LOST_MS,
61
+ }) {
62
+ const at = Date.parse(entry.lastAt);
63
+ const age = Number.isFinite(at) ? nowMs - at : Number.POSITIVE_INFINITY;
64
+ const kind = entry.lastKind;
65
+ if (kind === "session_end")
66
+ return "ended";
67
+ if (presenceConnected)
68
+ return "live";
69
+ if (kind === "turn_start" && age < windows.turn_start_lost_ms)
70
+ return "live";
71
+ if (age <= windows.live_within_ms)
72
+ return "live";
73
+ if (kind === "turn_start" && age >= windows.turn_start_lost_ms)
74
+ return "lost";
75
+ if (age >= windows.silent_lost_ms)
76
+ return "lost";
77
+ if (kind === "turn_end")
78
+ return "idle";
79
+ return "idle";
80
+ }
81
+ export function ingestSpoolLine(line, sessions, lineNo) {
82
+ const trimmed = line.trim();
83
+ if (!trimmed)
84
+ return;
85
+ let parsed;
86
+ try {
87
+ parsed = JSON.parse(trimmed);
88
+ }
89
+ catch (err) {
90
+ process.stderr.write(`lanes liveness: skipping malformed spool line ${lineNo}: ${err instanceof Error ? err.message : String(err)}\n`);
91
+ return;
92
+ }
93
+ if (!isPlainObject(parsed))
94
+ return;
95
+ const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId : "";
96
+ const kind = typeof parsed.kind === "string" ? parsed.kind : "";
97
+ const at = typeof parsed.at === "string" ? parsed.at : "";
98
+ if (!sessionId || !kind || !at)
99
+ return;
100
+ if (!Number.isFinite(Date.parse(at))) {
101
+ process.stderr.write(`lanes liveness: skipping spool line ${lineNo} with unparseable at\n`);
102
+ return;
103
+ }
104
+ const existing = sessions[sessionId];
105
+ const seq = typeof parsed.seq === "number" ? parsed.seq : undefined;
106
+ if (existing) {
107
+ const existingAt = Date.parse(existing.lastAt);
108
+ const nextAt = Date.parse(at);
109
+ if (nextAt < existingAt)
110
+ return;
111
+ if (nextAt === existingAt &&
112
+ seq !== undefined &&
113
+ existing.lastSeq !== undefined &&
114
+ seq < existing.lastSeq) {
115
+ return;
116
+ }
117
+ }
118
+ const entry = { sessionId, lastKind: kind, lastAt: at };
119
+ if (seq !== undefined)
120
+ entry.lastSeq = seq;
121
+ sessions[sessionId] = entry;
122
+ }
123
+ export function buildLivenessIndexFromSpool(spoolContents, meta) {
124
+ const sessions = {};
125
+ const lines = spoolContents.split("\n");
126
+ for (let i = 0; i < lines.length; i += 1) {
127
+ ingestSpoolLine(lines[i] ?? "", sessions, i + 1);
128
+ }
129
+ return {
130
+ schema_version: "hq-lane-liveness-index.v1",
131
+ built_at: meta.built_at,
132
+ spool_path: meta.spool_path,
133
+ spool_bytes: meta.spool_bytes,
134
+ spool_mtime_ms: meta.spool_mtime_ms,
135
+ sessions,
136
+ };
137
+ }
138
+ function atomicWriteJson(target, value, deps) {
139
+ const dir = path.dirname(target);
140
+ deps.mkdirSync(dir, { recursive: true });
141
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
142
+ try {
143
+ deps.writeFileSync(tmp, `${JSON.stringify(value)}\n`, { encoding: "utf8" });
144
+ deps.renameSync(tmp, target);
145
+ }
146
+ catch (err) {
147
+ try {
148
+ deps.unlinkSync(tmp);
149
+ }
150
+ catch (unlinkErr) {
151
+ const code = unlinkErr.code;
152
+ if (code !== "ENOENT") {
153
+ process.stderr.write(`lanes liveness: unlink tmp failed for ${tmp}: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}\n`);
154
+ }
155
+ }
156
+ throw err;
157
+ }
158
+ }
159
+ export function rebuildLivenessIndex(hqRoot, spoolPath, deps = defaultLivenessDeps()) {
160
+ let contents;
161
+ let st;
162
+ try {
163
+ st = deps.statSync(spoolPath);
164
+ contents = deps.readFileSync(spoolPath, "utf8");
165
+ }
166
+ catch (err) {
167
+ const code = err.code;
168
+ if (code === "ENOENT") {
169
+ const empty = {
170
+ schema_version: "hq-lane-liveness-index.v1",
171
+ built_at: new Date(deps.now()).toISOString(),
172
+ spool_path: spoolPath,
173
+ spool_bytes: 0,
174
+ spool_mtime_ms: 0,
175
+ sessions: {},
176
+ };
177
+ atomicWriteJson(livenessIndexPath(hqRoot), empty, deps);
178
+ return empty;
179
+ }
180
+ throw err;
181
+ }
182
+ const index = buildLivenessIndexFromSpool(contents, {
183
+ spool_path: spoolPath,
184
+ spool_bytes: st.size,
185
+ spool_mtime_ms: Math.floor(st.mtimeMs),
186
+ built_at: new Date(deps.now()).toISOString(),
187
+ });
188
+ atomicWriteJson(livenessIndexPath(hqRoot), index, deps);
189
+ return index;
190
+ }
191
+ export function readLivenessIndex(hqRoot, deps = defaultLivenessDeps()) {
192
+ const filePath = livenessIndexPath(hqRoot);
193
+ if (!deps.existsSync(filePath))
194
+ return null;
195
+ let raw;
196
+ try {
197
+ raw = deps.readFileSync(filePath, "utf8");
198
+ }
199
+ catch (err) {
200
+ process.stderr.write(`lanes liveness: failed to read index ${filePath}: ${err instanceof Error ? err.message : String(err)}\n`);
201
+ throw err;
202
+ }
203
+ try {
204
+ const parsed = JSON.parse(raw);
205
+ if (!isPlainObject(parsed) || parsed.schema_version !== "hq-lane-liveness-index.v1") {
206
+ throw new Error(`unexpected liveness index schema in ${filePath}`);
207
+ }
208
+ if (!isPlainObject(parsed.sessions)) {
209
+ throw new Error(`liveness index sessions map missing in ${filePath}`);
210
+ }
211
+ return parsed;
212
+ }
213
+ catch (err) {
214
+ process.stderr.write(`lanes liveness: invalid index ${filePath}: ${err instanceof Error ? err.message : String(err)}\n`);
215
+ throw err;
216
+ }
217
+ }
218
+ /**
219
+ * Return the compact index, rebuilding from the spool when missing or stale.
220
+ * Never walks workspace/sessions/.
221
+ */
222
+ export function loadLivenessIndex(hqRoot, spoolPath, deps = defaultLivenessDeps()) {
223
+ const spool = spoolPath ?? defaultSpoolPath(deps);
224
+ const existing = (() => {
225
+ try {
226
+ return readLivenessIndex(hqRoot, deps);
227
+ }
228
+ catch (err) {
229
+ process.stderr.write(`lanes liveness: rebuilding after invalid index: ${err instanceof Error ? err.message : String(err)}\n`);
230
+ return null;
231
+ }
232
+ })();
233
+ if (existing) {
234
+ try {
235
+ const st = deps.statSync(spool);
236
+ if (existing.spool_path === spool &&
237
+ existing.spool_bytes === st.size &&
238
+ existing.spool_mtime_ms === Math.floor(st.mtimeMs)) {
239
+ return existing;
240
+ }
241
+ }
242
+ catch (err) {
243
+ const code = err.code;
244
+ if (code !== "ENOENT")
245
+ throw err;
246
+ if (existing.spool_bytes === 0)
247
+ return existing;
248
+ }
249
+ }
250
+ return rebuildLivenessIndex(hqRoot, spool, deps);
251
+ }
252
+ export function sessionLiveness(hqRoot, sessionId, opts = {}) {
253
+ const deps = opts.deps ?? defaultLivenessDeps();
254
+ const index = loadLivenessIndex(hqRoot, opts.spoolPath, deps);
255
+ const entry = index.sessions[sessionId];
256
+ if (!entry)
257
+ return null;
258
+ const windows = opts.windows ?? loadLanesConfig(hqRoot).liveness;
259
+ return classifySessionLiveness(entry, opts.nowMs ?? deps.now(), opts.presenceConnected ?? false, windows);
260
+ }
261
+ export function listSessionLiveness(hqRoot, opts = {}) {
262
+ const deps = opts.deps ?? defaultLivenessDeps();
263
+ const index = loadLivenessIndex(hqRoot, opts.spoolPath, deps);
264
+ const nowMs = opts.nowMs ?? deps.now();
265
+ const windows = opts.windows ?? loadLanesConfig(hqRoot).liveness;
266
+ return Object.values(index.sessions).map((entry) => ({
267
+ sessionId: entry.sessionId,
268
+ state: classifySessionLiveness(entry, nowMs, opts.presenceConnected ?? false, windows),
269
+ lastKind: entry.lastKind,
270
+ lastAt: entry.lastAt,
271
+ }));
272
+ }
273
+ //# sourceMappingURL=liveness.js.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * HQ worker resolution for lanes (spec §5.3).
3
+ *
4
+ * Tenancy derives from the registry entry's `path:` prefix, never from the
5
+ * optional `company:` field. A company path with a missing or contradictory
6
+ * `company:` is `worker_tenancy_unresolved`, never a global worker.
7
+ *
8
+ * The registry does not index personal/workers/ for this command. Naming a
9
+ * personal worker fails with a clear error and does not fall through to a
10
+ * same-named entry in another tree.
11
+ */
12
+ import * as fs from "node:fs";
13
+ export declare const WORKER_REGISTRY_REL: string;
14
+ export interface RegistryWorkerEntry {
15
+ id: string;
16
+ path: string;
17
+ type: string;
18
+ visibility?: string;
19
+ company?: string;
20
+ team?: string;
21
+ status?: string;
22
+ description?: string;
23
+ }
24
+ export type WorkerTenancy = {
25
+ kind: "global";
26
+ } | {
27
+ kind: "company";
28
+ slug: string;
29
+ };
30
+ export interface WorkerYamlSkill {
31
+ id?: string;
32
+ file: string;
33
+ }
34
+ export interface WorkerYaml {
35
+ worker?: {
36
+ id?: string;
37
+ name?: string;
38
+ description?: string;
39
+ type?: string;
40
+ company?: string;
41
+ status?: string;
42
+ };
43
+ instructions?: unknown;
44
+ skills?: WorkerYamlSkill[];
45
+ context?: {
46
+ base?: string[];
47
+ };
48
+ knowledge?: unknown;
49
+ verification?: {
50
+ approval_required?: boolean;
51
+ human_checkpoints?: unknown;
52
+ };
53
+ execution?: {
54
+ max_runtime?: unknown;
55
+ model?: unknown;
56
+ codex_model?: unknown;
57
+ codex_flags?: unknown;
58
+ };
59
+ }
60
+ export interface ResolvedLaneWorker {
61
+ id: string;
62
+ path: string;
63
+ type: string;
64
+ status: string;
65
+ visibility?: string;
66
+ description?: string;
67
+ tenancy: WorkerTenancy;
68
+ workerYaml: WorkerYaml;
69
+ workerDir: string;
70
+ resolvedContextBase: string[];
71
+ skillFiles: string[];
72
+ defaultTimeout: unknown;
73
+ approvalRequired: boolean;
74
+ humanCheckpoints: string[];
75
+ }
76
+ export interface RegistryDeps {
77
+ existsSync: (p: string) => boolean;
78
+ readFileSync: (p: string, encoding: "utf8") => string;
79
+ statSync: (p: string) => fs.Stats;
80
+ }
81
+ export declare function registryPath(hqRoot: string): string;
82
+ export declare function personalWorkerDir(hqRoot: string, workerId: string): string;
83
+ export declare function personalWorkerExists(hqRoot: string, workerId: string, deps?: RegistryDeps): boolean;
84
+ export declare function readWorkerRegistryFile(hqRoot: string, deps?: RegistryDeps): RegistryWorkerEntry[];
85
+ /**
86
+ * Tenancy from path, not from `company:`. A company path whose field is
87
+ * missing or disagrees is unresolved — never global.
88
+ */
89
+ export declare function tenancyFromRegistryPath(entry: RegistryWorkerEntry): WorkerTenancy | {
90
+ kind: "unresolved";
91
+ pathSlug: string;
92
+ field?: string;
93
+ } | {
94
+ kind: "personal";
95
+ };
96
+ /**
97
+ * Resolve context.base against HQ root, then core/, then the worker directory.
98
+ * Entries that resolve nowhere are dropped, not passed on.
99
+ */
100
+ export declare function resolveContextBasePaths(hqRoot: string, workerDir: string, bases: string[] | undefined, deps?: RegistryDeps): string[];
101
+ export declare function resolveSkillFiles(workerDir: string, skills: WorkerYamlSkill[] | undefined, deps?: RegistryDeps): string[];
102
+ export declare function resolveLaneWorker(hqRoot: string, workerId: string, laneCompany: string | undefined, deps?: RegistryDeps): ResolvedLaneWorker;
103
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1,247 @@
1
+ /**
2
+ * HQ worker resolution for lanes (spec §5.3).
3
+ *
4
+ * Tenancy derives from the registry entry's `path:` prefix, never from the
5
+ * optional `company:` field. A company path with a missing or contradictory
6
+ * `company:` is `worker_tenancy_unresolved`, never a global worker.
7
+ *
8
+ * The registry does not index personal/workers/ for this command. Naming a
9
+ * personal worker fails with a clear error and does not fall through to a
10
+ * same-named entry in another tree.
11
+ */
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { parseUserYaml } from "../../utils/user-yaml-error.js";
15
+ import { WorkerResolutionError } from "./errors.js";
16
+ export const WORKER_REGISTRY_REL = path.join("core", "workers", "registry.yaml");
17
+ function defaultRegistryDeps() {
18
+ return {
19
+ existsSync: (p) => fs.existsSync(p),
20
+ readFileSync: (p, encoding) => fs.readFileSync(p, encoding),
21
+ statSync: (p) => fs.statSync(p),
22
+ };
23
+ }
24
+ function isPlainObject(value) {
25
+ return value !== null && typeof value === "object" && !Array.isArray(value);
26
+ }
27
+ export function registryPath(hqRoot) {
28
+ return path.join(hqRoot, WORKER_REGISTRY_REL);
29
+ }
30
+ export function personalWorkerDir(hqRoot, workerId) {
31
+ return path.join(hqRoot, "personal", "workers", workerId);
32
+ }
33
+ function isExistingPath(p, deps) {
34
+ try {
35
+ const st = deps.statSync(p);
36
+ return st.isFile() || st.isDirectory();
37
+ }
38
+ catch (err) {
39
+ const code = err.code;
40
+ if (code === "ENOENT")
41
+ return false;
42
+ throw err;
43
+ }
44
+ }
45
+ export function personalWorkerExists(hqRoot, workerId, deps = defaultRegistryDeps()) {
46
+ const yamlPath = path.join(personalWorkerDir(hqRoot, workerId), "worker.yaml");
47
+ try {
48
+ return deps.statSync(yamlPath).isFile();
49
+ }
50
+ catch (err) {
51
+ const code = err.code;
52
+ if (code === "ENOENT")
53
+ return false;
54
+ throw err;
55
+ }
56
+ }
57
+ export function readWorkerRegistryFile(hqRoot, deps = defaultRegistryDeps()) {
58
+ const filePath = registryPath(hqRoot);
59
+ if (!deps.existsSync(filePath)) {
60
+ throw new WorkerResolutionError("worker_registry_unreadable", `Worker registry missing at ${filePath}`, { path: filePath });
61
+ }
62
+ let text;
63
+ try {
64
+ text = deps.readFileSync(filePath, "utf8");
65
+ }
66
+ catch (err) {
67
+ throw new WorkerResolutionError("worker_registry_unreadable", `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`, { path: filePath });
68
+ }
69
+ const doc = parseUserYaml(text, filePath);
70
+ const workers = doc?.workers;
71
+ if (!Array.isArray(workers)) {
72
+ throw new WorkerResolutionError("worker_registry_unreadable", `Worker registry workers key is missing or not an array at ${filePath}`, { path: filePath });
73
+ }
74
+ const entries = [];
75
+ for (let i = 0; i < workers.length; i += 1) {
76
+ const item = workers[i];
77
+ if (!isPlainObject(item)) {
78
+ process.stderr.write(`lanes registry: skipping malformed worker entry in ${filePath} at index ${i}: not an object (type=${typeof item})\n`);
79
+ continue;
80
+ }
81
+ if (typeof item.id !== "string" ||
82
+ item.id.length === 0 ||
83
+ typeof item.path !== "string" ||
84
+ item.path.length === 0) {
85
+ process.stderr.write(`lanes registry: skipping malformed worker entry in ${filePath} at index ${i}: missing id or path (id=${JSON.stringify(item.id)} path=${JSON.stringify(item.path)})\n`);
86
+ continue;
87
+ }
88
+ entries.push({
89
+ id: item.id,
90
+ path: item.path,
91
+ type: typeof item.type === "string" ? item.type : "",
92
+ visibility: typeof item.visibility === "string" ? item.visibility : undefined,
93
+ company: typeof item.company === "string" ? item.company : undefined,
94
+ team: typeof item.team === "string" ? item.team : undefined,
95
+ status: typeof item.status === "string" ? item.status : undefined,
96
+ description: typeof item.description === "string" ? item.description : undefined,
97
+ });
98
+ }
99
+ return entries;
100
+ }
101
+ const COMPANY_PATH = /^companies\/([^/]+)\//;
102
+ const PERSONAL_PATH = /^(?:personal\/workers\/)/;
103
+ const CORE_PATH = /^(?:core\/workers\/)/;
104
+ /**
105
+ * Tenancy from path, not from `company:`. A company path whose field is
106
+ * missing or disagrees is unresolved — never global.
107
+ */
108
+ export function tenancyFromRegistryPath(entry) {
109
+ const normalized = entry.path.replaceAll("\\", "/");
110
+ if (PERSONAL_PATH.test(normalized)) {
111
+ return { kind: "personal" };
112
+ }
113
+ const companyMatch = COMPANY_PATH.exec(normalized);
114
+ if (companyMatch) {
115
+ const pathSlug = companyMatch[1] ?? "";
116
+ if (!pathSlug) {
117
+ return { kind: "unresolved", pathSlug, field: entry.company };
118
+ }
119
+ const field = entry.company?.trim() ?? "";
120
+ if (!field || field !== pathSlug) {
121
+ return { kind: "unresolved", pathSlug, field: field || undefined };
122
+ }
123
+ return { kind: "company", slug: pathSlug };
124
+ }
125
+ if (CORE_PATH.test(normalized) || normalized.startsWith("core/")) {
126
+ return { kind: "global" };
127
+ }
128
+ // Unknown tree: do not treat as global.
129
+ return { kind: "unresolved", pathSlug: "", field: entry.company };
130
+ }
131
+ function loadWorkerYaml(hqRoot, relativeDir, deps) {
132
+ const dir = path.join(hqRoot, relativeDir);
133
+ const filePath = path.join(dir, "worker.yaml");
134
+ if (!deps.existsSync(filePath)) {
135
+ throw new WorkerResolutionError("worker_not_found", `worker.yaml missing at ${filePath}`, { path: filePath });
136
+ }
137
+ const text = deps.readFileSync(filePath, "utf8");
138
+ const doc = parseUserYaml(text, filePath);
139
+ if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
140
+ return {};
141
+ }
142
+ return doc;
143
+ }
144
+ /**
145
+ * Resolve context.base against HQ root, then core/, then the worker directory.
146
+ * Entries that resolve nowhere are dropped, not passed on.
147
+ */
148
+ export function resolveContextBasePaths(hqRoot, workerDir, bases, deps = defaultRegistryDeps()) {
149
+ if (!bases || bases.length === 0)
150
+ return [];
151
+ const resolved = [];
152
+ for (const entry of bases) {
153
+ if (typeof entry !== "string" || entry.length === 0)
154
+ continue;
155
+ const candidates = [
156
+ path.isAbsolute(entry) ? entry : path.join(hqRoot, entry),
157
+ path.join(hqRoot, "core", entry),
158
+ path.join(workerDir, entry),
159
+ ];
160
+ const hit = candidates.find((candidate) => isExistingPath(candidate, deps));
161
+ if (hit)
162
+ resolved.push(hit);
163
+ }
164
+ return resolved;
165
+ }
166
+ export function resolveSkillFiles(workerDir, skills, deps = defaultRegistryDeps()) {
167
+ if (!skills)
168
+ return [];
169
+ const files = [];
170
+ for (const skill of skills) {
171
+ if (!skill || typeof skill.file !== "string" || skill.file.length === 0) {
172
+ continue;
173
+ }
174
+ const candidate = path.isAbsolute(skill.file)
175
+ ? skill.file
176
+ : path.join(workerDir, skill.file);
177
+ if (isExistingPath(candidate, deps))
178
+ files.push(candidate);
179
+ }
180
+ return files;
181
+ }
182
+ function humanCheckpointsOf(yaml) {
183
+ const raw = yaml.verification?.human_checkpoints;
184
+ if (!Array.isArray(raw))
185
+ return [];
186
+ return raw.filter((item) => typeof item === "string");
187
+ }
188
+ export function resolveLaneWorker(hqRoot, workerId, laneCompany, deps = defaultRegistryDeps()) {
189
+ if (personalWorkerExists(hqRoot, workerId, deps)) {
190
+ throw new WorkerResolutionError("personal_worker_unindexed", `Worker ${JSON.stringify(workerId)} lives under personal/workers/ which the registry does not index; --worker cannot resolve personal workers`, {
191
+ worker: workerId,
192
+ path: path.join("personal", "workers", workerId).replaceAll("\\", "/"),
193
+ });
194
+ }
195
+ const entries = readWorkerRegistryFile(hqRoot, deps);
196
+ const entry = entries.find((item) => item.id === workerId);
197
+ if (!entry) {
198
+ throw new WorkerResolutionError("worker_not_found", `Worker ${JSON.stringify(workerId)} is not in core/workers/registry.yaml`, { worker: workerId });
199
+ }
200
+ const tenancy = tenancyFromRegistryPath(entry);
201
+ if (tenancy.kind === "personal") {
202
+ throw new WorkerResolutionError("personal_worker_unindexed", `Worker ${JSON.stringify(workerId)} is registered under personal/workers/ which lanes do not admit`, { worker: workerId, path: entry.path });
203
+ }
204
+ if (tenancy.kind === "unresolved") {
205
+ throw new WorkerResolutionError("worker_tenancy_unresolved", `Worker ${JSON.stringify(workerId)} has a company path whose company field is missing or contradicts the path`, {
206
+ worker: workerId,
207
+ path: entry.path,
208
+ path_slug: tenancy.pathSlug,
209
+ company_field: tenancy.field ?? null,
210
+ });
211
+ }
212
+ if (tenancy.kind === "company") {
213
+ if (!laneCompany || laneCompany !== tenancy.slug) {
214
+ throw new WorkerResolutionError("worker_not_available", `Worker ${JSON.stringify(workerId)} belongs to company ${tenancy.slug} and is not available to ${laneCompany ?? "(none)"}`, {
215
+ worker: workerId,
216
+ worker_company: tenancy.slug,
217
+ lane_company: laneCompany ?? null,
218
+ });
219
+ }
220
+ }
221
+ const status = entry.status && entry.status.length > 0 ? entry.status : "active";
222
+ if (status !== "active") {
223
+ throw new WorkerResolutionError("worker_not_active", `Worker ${JSON.stringify(workerId)} has status ${JSON.stringify(status)} (need active)`, { worker: workerId, status });
224
+ }
225
+ const relativeDir = entry.path.replace(/\/+$/, "");
226
+ const workerDir = path.join(hqRoot, relativeDir);
227
+ const workerYaml = loadWorkerYaml(hqRoot, relativeDir, deps);
228
+ const resolvedContextBase = resolveContextBasePaths(hqRoot, workerDir, workerYaml.context?.base, deps);
229
+ const skillFiles = resolveSkillFiles(workerDir, workerYaml.skills, deps);
230
+ return {
231
+ id: entry.id,
232
+ path: entry.path,
233
+ type: entry.type,
234
+ status,
235
+ visibility: entry.visibility,
236
+ description: entry.description,
237
+ tenancy,
238
+ workerYaml,
239
+ workerDir,
240
+ resolvedContextBase,
241
+ skillFiles,
242
+ defaultTimeout: workerYaml.execution?.max_runtime,
243
+ approvalRequired: workerYaml.verification?.approval_required === true,
244
+ humanCheckpoints: humanCheckpointsOf(workerYaml),
245
+ };
246
+ }
247
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * File-backed lane / question record store (spec §10, §10.1).
3
+ *
4
+ * Root: workspace/lanes/ under the HQ root. One stable path per record;
5
+ * `status` is the pending/answered discriminator — files are never moved
6
+ * between directories. Every mutation is temp-file-plus-rename and a
7
+ * compare-and-set on `version`. Histories are capped at 50 at the append site.
8
+ */
9
+ import * as fs from "node:fs";
10
+ import { type AttributionHistoryEntry, type LaneRecord, type QuestionRecord, type RoutingHistoryEntry, type SeniorHistoryEntry } from "./types.js";
11
+ export declare const LANES_STORE_REL: string;
12
+ export declare const LANE_RECORDS_DIR = "lanes";
13
+ export declare const QUESTION_RECORDS_DIR = "questions";
14
+ export declare const PENDING_INDEX_DIR: string;
15
+ export interface StoreDeps {
16
+ now: () => Date;
17
+ hostname: () => string;
18
+ mkdirSync: typeof fs.mkdirSync;
19
+ readFileSync: typeof fs.readFileSync;
20
+ writeFileSync: typeof fs.writeFileSync;
21
+ renameSync: typeof fs.renameSync;
22
+ unlinkSync: typeof fs.unlinkSync;
23
+ existsSync: typeof fs.existsSync;
24
+ readdirSync: typeof fs.readdirSync;
25
+ openSync: typeof fs.openSync;
26
+ closeSync: typeof fs.closeSync;
27
+ fsyncSync: typeof fs.fsyncSync;
28
+ chmodSync: typeof fs.chmodSync;
29
+ }
30
+ export declare function resolveStoreHqRoot(hqRoot?: string): string;
31
+ export declare function lanesStoreRoot(hqRoot?: string): string;
32
+ export declare function laneRecordPath(hqRoot: string, laneId: string): string;
33
+ export declare function questionRecordPath(hqRoot: string, questionId: string): string;
34
+ export declare function pendingMarkerPath(hqRoot: string, questionId: string): string;
35
+ export declare function sanitizeHostComponent(hostname: string): string;
36
+ /** Collision-safe across machines: ULID plus a host component. */
37
+ export declare function mintLaneId(deps?: Pick<StoreDeps, "now" | "hostname">): string;
38
+ export declare function mintQuestionId(deps?: Pick<StoreDeps, "now" | "hostname">): string;
39
+ export declare function appendCappedHistory<T>(history: readonly T[], entry: T, cap?: number): T[];
40
+ export declare function parseLaneRecord(raw: unknown, filePath?: string): LaneRecord;
41
+ export declare function parseQuestionRecord(raw: unknown, filePath?: string): QuestionRecord;
42
+ /**
43
+ * HQ Sync sibling-twin grammar from `@indigoai-us/hq-cloud` `buildConflictPath`:
44
+ * `<file>.json.conflict-<ISO-with-dashes>-<machine>.json`
45
+ * (legacy twins; current engines park losers under `.hq/conflict-backups/`,
46
+ * but merge-on-read still has to recover the ones already on disk).
47
+ */
48
+ export declare function isConflictSidecarName(recordBasename: string, name: string): boolean;
49
+ export declare function mergeLaneRecords(live: LaneRecord, sidecars: readonly LaneRecord[]): LaneRecord;
50
+ export declare function mergeQuestionRecords(live: QuestionRecord, sidecars: readonly QuestionRecord[]): QuestionRecord;
51
+ export declare function tryReadLane(hqRoot: string, laneId: string, deps?: StoreDeps): LaneRecord | null;
52
+ export declare function readLane(hqRoot: string, laneId: string, deps?: StoreDeps): LaneRecord;
53
+ export declare function tryReadQuestion(hqRoot: string, questionId: string, deps?: StoreDeps): QuestionRecord | null;
54
+ export declare function readQuestion(hqRoot: string, questionId: string, deps?: StoreDeps): QuestionRecord;
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;
57
+ export declare function appendLaneSeniorHistory(hqRoot: string, laneId: string, expectedVersion: number, entry: SeniorHistoryEntry, deps?: StoreDeps): LaneRecord;
58
+ export declare function appendLaneAttribution(hqRoot: string, laneId: string, expectedVersion: number, entry: AttributionHistoryEntry, deps?: StoreDeps): LaneRecord;
59
+ 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;
60
+ export declare function updateQuestion(hqRoot: string, questionId: string, expectedVersion: number, updater: (current: QuestionRecord) => QuestionRecord, deps?: StoreDeps): QuestionRecord;
61
+ export declare function appendQuestionRouting(hqRoot: string, questionId: string, expectedVersion: number, entry: RoutingHistoryEntry, deps?: StoreDeps): QuestionRecord;
62
+ export declare function listLaneIds(hqRoot: string, deps?: StoreDeps): string[];
63
+ export declare function listQuestionIds(hqRoot: string, deps?: StoreDeps): string[];
64
+ //# sourceMappingURL=store.d.ts.map