@indigoai-us/hq-cli 5.125.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 (108) hide show
  1. package/CHANGELOG.md +329 -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/assets/lanes-hooks/hq-lanes-drain.json +15 -0
  6. package/assets/lanes-hooks/hq-lanes-drain.mjs +254 -0
  7. package/assets/lanes-hooks/hq-lanes-drain.sh +21 -0
  8. package/assets/lanes-reconcile/hq-lanes-reconcile.service +11 -0
  9. package/assets/lanes-reconcile/hq-lanes-reconcile.timer +12 -0
  10. package/dist/command-catalog.generated.d.ts +643 -6
  11. package/dist/command-catalog.generated.js +836 -6
  12. package/dist/command-registration-plan.d.ts +7 -0
  13. package/dist/command-registration-plan.js +11 -1
  14. package/dist/commands/agents.d.ts +12 -0
  15. package/dist/commands/agents.js +46 -2
  16. package/dist/commands/bot.d.ts +10 -0
  17. package/dist/commands/bot.js +6 -0
  18. package/dist/commands/lanes.d.ts +114 -0
  19. package/dist/commands/lanes.js +2969 -0
  20. package/dist/commands/mesh.js +287 -24
  21. package/dist/commands/onboard-create-identity.d.ts +26 -0
  22. package/dist/commands/onboard-create-identity.js +23 -0
  23. package/dist/commands/onboard.js +13 -4
  24. package/dist/lib/bot/progress.d.ts +6 -0
  25. package/dist/lib/bot/progress.js +16 -0
  26. package/dist/lib/bot/runtime/grok.d.ts +30 -7
  27. package/dist/lib/bot/runtime/grok.js +86 -8
  28. package/dist/lib/bot/runtime/index.js +15 -8
  29. package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
  30. package/dist/lib/bot/runtime/messages-stream.js +49 -3
  31. package/dist/lib/lanes/accounts.d.ts +103 -0
  32. package/dist/lib/lanes/accounts.js +318 -0
  33. package/dist/lib/lanes/compaction.d.ts +96 -0
  34. package/dist/lib/lanes/compaction.js +197 -0
  35. package/dist/lib/lanes/config.d.ts +71 -0
  36. package/dist/lib/lanes/config.js +408 -0
  37. package/dist/lib/lanes/delivery.d.ts +151 -0
  38. package/dist/lib/lanes/delivery.js +354 -0
  39. package/dist/lib/lanes/dropbox.d.ts +96 -0
  40. package/dist/lib/lanes/dropbox.js +408 -0
  41. package/dist/lib/lanes/effort.d.ts +50 -0
  42. package/dist/lib/lanes/effort.js +109 -0
  43. package/dist/lib/lanes/errors.d.ts +39 -0
  44. package/dist/lib/lanes/errors.js +72 -0
  45. package/dist/lib/lanes/hooks-install.d.ts +81 -0
  46. package/dist/lib/lanes/hooks-install.js +326 -0
  47. package/dist/lib/lanes/hq-root.d.ts +19 -0
  48. package/dist/lib/lanes/hq-root.js +28 -0
  49. package/dist/lib/lanes/index.d.ts +49 -0
  50. package/dist/lib/lanes/index.js +27 -0
  51. package/dist/lib/lanes/liveness.d.ts +95 -0
  52. package/dist/lib/lanes/liveness.js +332 -0
  53. package/dist/lib/lanes/machine-identity.d.ts +34 -0
  54. package/dist/lib/lanes/machine-identity.js +92 -0
  55. package/dist/lib/lanes/mesh-events.d.ts +109 -0
  56. package/dist/lib/lanes/mesh-events.js +275 -0
  57. package/dist/lib/lanes/outpost-dispatch.d.ts +65 -0
  58. package/dist/lib/lanes/outpost-dispatch.js +90 -0
  59. package/dist/lib/lanes/questions.d.ts +75 -0
  60. package/dist/lib/lanes/questions.js +243 -0
  61. package/dist/lib/lanes/quota.d.ts +95 -0
  62. package/dist/lib/lanes/quota.js +320 -0
  63. package/dist/lib/lanes/reconcile-install.d.ts +69 -0
  64. package/dist/lib/lanes/reconcile-install.js +221 -0
  65. package/dist/lib/lanes/reconcile.d.ts +102 -0
  66. package/dist/lib/lanes/reconcile.js +985 -0
  67. package/dist/lib/lanes/registry.d.ts +116 -0
  68. package/dist/lib/lanes/registry.js +521 -0
  69. package/dist/lib/lanes/spawn.d.ts +274 -0
  70. package/dist/lib/lanes/spawn.js +1363 -0
  71. package/dist/lib/lanes/store.d.ts +68 -0
  72. package/dist/lib/lanes/store.js +745 -0
  73. package/dist/lib/lanes/throttle.d.ts +89 -0
  74. package/dist/lib/lanes/throttle.js +297 -0
  75. package/dist/lib/lanes/types.d.ts +535 -0
  76. package/dist/lib/lanes/types.js +350 -0
  77. package/dist/lib/lanes/watcher-main.d.ts +9 -0
  78. package/dist/lib/lanes/watcher-main.js +22 -0
  79. package/dist/lib/lanes/watcher.d.ts +216 -0
  80. package/dist/lib/lanes/watcher.js +859 -0
  81. package/dist/lib/mesh/api.d.ts +71 -7
  82. package/dist/lib/mesh/api.js +167 -28
  83. package/dist/lib/mesh/live/backfill-held.d.ts +18 -7
  84. package/dist/lib/mesh/live/backfill-held.js +66 -21
  85. package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
  86. package/dist/lib/mesh/live/daemon/config.js +75 -0
  87. package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
  88. package/dist/lib/mesh/live/daemon/index.js +3 -3
  89. package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
  90. package/dist/lib/mesh/live/daemon/install.js +444 -2
  91. package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
  92. package/dist/lib/mesh/live/daemon/paths.js +2 -0
  93. package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
  94. package/dist/lib/mesh/live/daemon/run.js +111 -54
  95. package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
  96. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +3 -0
  97. package/dist/lib/mesh/live/daemon/transcript-watch.js +31 -8
  98. package/dist/lib/scan-packages/index.d.ts +2 -0
  99. package/dist/lib/scan-packages/index.js +18 -13
  100. package/dist/lib/work-context/config.d.ts +20 -0
  101. package/dist/lib/work-context/config.js +26 -5
  102. package/dist/lib/work-context/reconcile.js +28 -7
  103. package/dist/lib/work-context/state.d.ts +12 -0
  104. package/dist/lib/work-context/state.js +131 -3
  105. package/dist/lib/work-context/windows-acl.d.ts +123 -0
  106. package/dist/lib/work-context/windows-acl.js +425 -0
  107. package/dist/utils/pack-contributions.js +5 -4
  108. package/package.json +3 -1
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Load and validate personal/lanes/config.json (spec §5.5).
3
+ *
4
+ * A missing file is the documented defaults, not an error. A `providers` key
5
+ * outside {claude, codex, grok} is rejected with the accepted list. Account
6
+ * homes are canonicalised with realpath; two account ids for one provider
7
+ * that resolve to the same home are refused (identity-keyed-cap rule).
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+ import { parseConfigEffort } from "./effort.js";
13
+ import { LanesConfigError } from "./errors.js";
14
+ import { CONFIG_SCHEMA_VERSION, LANE_PROVIDERS, isLaneProvider, parseMajorSchema, } from "./types.js";
15
+ export const LANES_CONFIG_REL = path.join("personal", "lanes", "config.json");
16
+ export const DEFAULT_MAX_QUEUED = 3;
17
+ export const DEFAULT_CPU_BUSY_PAUSE = 0.85;
18
+ export const DEFAULT_CPU_BUSY_RESUME = 0.7;
19
+ export const DEFAULT_MIN_AVAILABLE_MEM_MB = 1024;
20
+ /**
21
+ * Owner directive 2026-09-22: "if it's been an hour since last msg, always
22
+ * run compact". Override per provider with `compact_after_ms`.
23
+ */
24
+ export const DEFAULT_COMPACT_AFTER_MS = 60 * 60 * 1000;
25
+ /**
26
+ * Watcher `lane_progress` throttle: at most one thread event per five minutes
27
+ * per lane, and only when the tick saw new activity. Override per provider
28
+ * with `progress_event_min_interval_ms`.
29
+ */
30
+ export const DEFAULT_PROGRESS_EVENT_MIN_INTERVAL_MS = 5 * 60 * 1000;
31
+ /** Spec §9: last event within 10 minutes → live. */
32
+ export const DEFAULT_LIVE_WITHIN_MS = 10 * 60 * 1000;
33
+ /** Spec §9: turn_start with no turn_end after 2 hours → lost. */
34
+ export const DEFAULT_TURN_START_LOST_MS = 2 * 60 * 60 * 1000;
35
+ /** Spec §9: no event for 12 hours (and no session_end) → lost. */
36
+ export const DEFAULT_SILENT_LOST_MS = 12 * 60 * 60 * 1000;
37
+ export const DEFAULT_LIVENESS_WINDOWS = {
38
+ live_within_ms: DEFAULT_LIVE_WITHIN_MS,
39
+ turn_start_lost_ms: DEFAULT_TURN_START_LOST_MS,
40
+ silent_lost_ms: DEFAULT_SILENT_LOST_MS,
41
+ };
42
+ export const DEFAULT_PROVIDER_MAX_RUNNING = {
43
+ claude: 2,
44
+ codex: 4,
45
+ grok: 4,
46
+ };
47
+ export const DEFAULT_ACCOUNT_HOMES = {
48
+ claude: "~/.claude",
49
+ codex: "~/.codex",
50
+ grok: "~/.grok",
51
+ };
52
+ function defaultDeps() {
53
+ return {
54
+ homedir: () => os.homedir(),
55
+ realpathSync: (p) => fs.realpathSync(p),
56
+ existsSync: (p) => fs.existsSync(p),
57
+ readFileSync: (p, encoding) => fs.readFileSync(p, encoding),
58
+ };
59
+ }
60
+ export function expandHome(home, homedir) {
61
+ if (home === "~")
62
+ return homedir;
63
+ if (home.startsWith("~/") || home.startsWith("~\\")) {
64
+ return path.join(homedir, home.slice(2));
65
+ }
66
+ return home;
67
+ }
68
+ /**
69
+ * Canonical principal for an account home: realpath when the path exists,
70
+ * otherwise resolve existing ancestors and rejoin the missing tail so two
71
+ * not-yet-created spellings of the same directory still collide.
72
+ */
73
+ export function canonicalizeHome(home, deps = defaultDeps()) {
74
+ const expanded = path.resolve(expandHome(home, deps.homedir()));
75
+ try {
76
+ return deps.realpathSync(expanded);
77
+ }
78
+ catch (err) {
79
+ const code = err.code;
80
+ if (code !== "ENOENT")
81
+ throw err;
82
+ }
83
+ let cur = expanded;
84
+ const missing = [];
85
+ for (let i = 0; i < 64; i += 1) {
86
+ if (deps.existsSync(cur)) {
87
+ try {
88
+ return path.join(deps.realpathSync(cur), ...missing.reverse());
89
+ }
90
+ catch (err) {
91
+ const code = err.code;
92
+ if (code !== "ENOENT")
93
+ throw err;
94
+ }
95
+ }
96
+ const parent = path.dirname(cur);
97
+ if (parent === cur)
98
+ break;
99
+ missing.push(path.basename(cur));
100
+ cur = parent;
101
+ }
102
+ return expanded;
103
+ }
104
+ function isPlainObject(value) {
105
+ return value !== null && typeof value === "object" && !Array.isArray(value);
106
+ }
107
+ function positiveInt(value, field, fallback) {
108
+ if (value === undefined && fallback !== undefined)
109
+ return fallback;
110
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
111
+ throw new LanesConfigError("invalid_config", `${field} must be a positive integer`, { field, value });
112
+ }
113
+ return value;
114
+ }
115
+ function unitInterval(value, field, fallback) {
116
+ if (value === undefined)
117
+ return fallback;
118
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
119
+ throw new LanesConfigError("invalid_config", `${field} must be a number in [0, 1]`, { field, value });
120
+ }
121
+ return value;
122
+ }
123
+ function parseAccount(provider, id, raw, deps) {
124
+ if (!isPlainObject(raw)) {
125
+ throw new LanesConfigError("invalid_config", `providers.${provider}.accounts.${id} must be an object`, { provider, account: id });
126
+ }
127
+ if (typeof raw.home !== "string" || raw.home.length === 0) {
128
+ throw new LanesConfigError("invalid_config", `providers.${provider}.accounts.${id}.home must be a non-empty string`, { provider, account: id });
129
+ }
130
+ if (typeof raw.enabled !== "boolean") {
131
+ throw new LanesConfigError("invalid_config", `providers.${provider}.accounts.${id}.enabled must be a boolean`, { provider, account: id });
132
+ }
133
+ const account = {
134
+ home: canonicalizeHome(raw.home, deps),
135
+ enabled: raw.enabled,
136
+ };
137
+ if (raw.disabled_reason !== undefined) {
138
+ if (typeof raw.disabled_reason !== "string") {
139
+ throw new LanesConfigError("invalid_config", `providers.${provider}.accounts.${id}.disabled_reason must be a string`, { provider, account: id });
140
+ }
141
+ account.disabled_reason = raw.disabled_reason;
142
+ }
143
+ if (raw.max_running !== undefined) {
144
+ account.max_running = positiveInt(raw.max_running, `providers.${provider}.accounts.${id}.max_running`);
145
+ }
146
+ if (raw.model !== undefined) {
147
+ account.model = nonEmptyModel(raw.model, `providers.${provider}.accounts.${id}.model`);
148
+ }
149
+ if (raw.effort !== undefined) {
150
+ account.effort = parseConfigEffort(provider, raw.effort, `providers.${provider}.accounts.${id}.effort`);
151
+ }
152
+ return account;
153
+ }
154
+ function nonEmptyModel(value, field) {
155
+ if (typeof value !== "string" || value.trim().length === 0) {
156
+ throw new LanesConfigError("invalid_config", `${field} must be a non-empty string`, { field, value });
157
+ }
158
+ return value.trim();
159
+ }
160
+ function rejectDuplicateHomes(provider, accounts) {
161
+ const seen = new Map();
162
+ for (const [id, account] of Object.entries(accounts)) {
163
+ const prior = seen.get(account.home);
164
+ if (prior !== undefined) {
165
+ throw new LanesConfigError("duplicate_account_home", `providers.${provider} accounts ${JSON.stringify(prior)} and ${JSON.stringify(id)} resolve to the same home ${account.home}`, {
166
+ provider,
167
+ accounts: [prior, id],
168
+ home: account.home,
169
+ });
170
+ }
171
+ seen.set(account.home, id);
172
+ }
173
+ }
174
+ function defaultAccountMap(provider, deps) {
175
+ return {
176
+ a: {
177
+ home: canonicalizeHome(DEFAULT_ACCOUNT_HOMES[provider], deps),
178
+ enabled: true,
179
+ },
180
+ };
181
+ }
182
+ function parseProvider(provider, raw, deps) {
183
+ if (!isPlainObject(raw)) {
184
+ throw new LanesConfigError("invalid_config", `providers.${provider} must be an object`, { provider });
185
+ }
186
+ const accountsRaw = raw.accounts;
187
+ const accounts = {};
188
+ if (accountsRaw === undefined) {
189
+ Object.assign(accounts, defaultAccountMap(provider, deps));
190
+ }
191
+ else {
192
+ if (!isPlainObject(accountsRaw)) {
193
+ throw new LanesConfigError("invalid_config", `providers.${provider}.accounts must be an object`, { provider });
194
+ }
195
+ for (const [id, value] of Object.entries(accountsRaw)) {
196
+ if (id.length === 0) {
197
+ throw new LanesConfigError("invalid_config", `providers.${provider} account ids must be non-empty strings`, { provider });
198
+ }
199
+ accounts[id] = parseAccount(provider, id, value, deps);
200
+ }
201
+ }
202
+ rejectDuplicateHomes(provider, accounts);
203
+ // default_account is accepted so existing files parse, and ignored for
204
+ // selection. An omitted field is not an error.
205
+ let defaultAccount;
206
+ if (raw.default_account !== undefined) {
207
+ if (typeof raw.default_account !== "string" || raw.default_account.length === 0) {
208
+ throw new LanesConfigError("invalid_config", `providers.${provider}.default_account must be a non-empty string`, { provider, field: "default_account" });
209
+ }
210
+ defaultAccount = raw.default_account;
211
+ if (Object.keys(accounts).length > 0 && accounts[defaultAccount] === undefined) {
212
+ throw new LanesConfigError("invalid_config", `providers.${provider}.default_account ${JSON.stringify(defaultAccount)} is not a configured account`, { provider, default_account: defaultAccount, accounts: Object.keys(accounts) });
213
+ }
214
+ }
215
+ const parsed = {
216
+ max_running: positiveInt(raw.max_running, `providers.${provider}.max_running`, DEFAULT_PROVIDER_MAX_RUNNING[provider]),
217
+ max_queued: positiveInt(raw.max_queued, `providers.${provider}.max_queued`, DEFAULT_MAX_QUEUED),
218
+ accounts,
219
+ };
220
+ if (defaultAccount !== undefined)
221
+ parsed.default_account = defaultAccount;
222
+ if (raw.model !== undefined) {
223
+ parsed.model = nonEmptyModel(raw.model, `providers.${provider}.model`);
224
+ }
225
+ if (raw.effort !== undefined) {
226
+ parsed.effort = parseConfigEffort(provider, raw.effort, `providers.${provider}.effort`);
227
+ }
228
+ if (raw.compact_after_ms !== undefined) {
229
+ parsed.compact_after_ms = positiveInt(raw.compact_after_ms, `providers.${provider}.compact_after_ms`);
230
+ }
231
+ if (raw.progress_event_min_interval_ms !== undefined) {
232
+ parsed.progress_event_min_interval_ms = positiveInt(raw.progress_event_min_interval_ms, `providers.${provider}.progress_event_min_interval_ms`);
233
+ }
234
+ return parsed;
235
+ }
236
+ function parseThrottle(raw) {
237
+ const obj = raw === undefined ? {} : raw;
238
+ if (!isPlainObject(obj)) {
239
+ throw new LanesConfigError("invalid_config", "throttle must be an object");
240
+ }
241
+ const pause = unitInterval(obj.cpu_busy_pause, "throttle.cpu_busy_pause", DEFAULT_CPU_BUSY_PAUSE);
242
+ const resume = unitInterval(obj.cpu_busy_resume, "throttle.cpu_busy_resume", DEFAULT_CPU_BUSY_RESUME);
243
+ if (resume > pause) {
244
+ throw new LanesConfigError("invalid_config", "throttle.cpu_busy_resume must be <= throttle.cpu_busy_pause", { cpu_busy_pause: pause, cpu_busy_resume: resume });
245
+ }
246
+ let minMem = DEFAULT_MIN_AVAILABLE_MEM_MB;
247
+ if (obj.min_available_mem_mb !== undefined) {
248
+ if (typeof obj.min_available_mem_mb !== "number" ||
249
+ !Number.isFinite(obj.min_available_mem_mb) ||
250
+ obj.min_available_mem_mb < 0) {
251
+ throw new LanesConfigError("invalid_config", "throttle.min_available_mem_mb must be a non-negative number", { value: obj.min_available_mem_mb });
252
+ }
253
+ minMem = obj.min_available_mem_mb;
254
+ }
255
+ return {
256
+ cpu_busy_pause: pause,
257
+ cpu_busy_resume: resume,
258
+ min_available_mem_mb: minMem,
259
+ };
260
+ }
261
+ function parseLiveness(raw) {
262
+ const obj = raw === undefined ? {} : raw;
263
+ if (!isPlainObject(obj)) {
264
+ throw new LanesConfigError("invalid_config", "liveness must be an object");
265
+ }
266
+ return {
267
+ live_within_ms: positiveInt(obj.live_within_ms, "liveness.live_within_ms", DEFAULT_LIVE_WITHIN_MS),
268
+ turn_start_lost_ms: positiveInt(obj.turn_start_lost_ms, "liveness.turn_start_lost_ms", DEFAULT_TURN_START_LOST_MS),
269
+ silent_lost_ms: positiveInt(obj.silent_lost_ms, "liveness.silent_lost_ms", DEFAULT_SILENT_LOST_MS),
270
+ };
271
+ }
272
+ export function documentedDefaultConfig(deps = defaultDeps()) {
273
+ const providers = {};
274
+ for (const provider of LANE_PROVIDERS) {
275
+ providers[provider] = {
276
+ max_running: DEFAULT_PROVIDER_MAX_RUNNING[provider],
277
+ max_queued: DEFAULT_MAX_QUEUED,
278
+ accounts: defaultAccountMap(provider, deps),
279
+ };
280
+ }
281
+ return {
282
+ schema_version: CONFIG_SCHEMA_VERSION,
283
+ providers,
284
+ throttle: {
285
+ cpu_busy_pause: DEFAULT_CPU_BUSY_PAUSE,
286
+ cpu_busy_resume: DEFAULT_CPU_BUSY_RESUME,
287
+ min_available_mem_mb: DEFAULT_MIN_AVAILABLE_MEM_MB,
288
+ },
289
+ liveness: { ...DEFAULT_LIVENESS_WINDOWS },
290
+ };
291
+ }
292
+ export function parseLanesConfig(raw, deps = defaultDeps()) {
293
+ if (!isPlainObject(raw)) {
294
+ throw new LanesConfigError("invalid_config", "lanes config must be a JSON object");
295
+ }
296
+ if (typeof raw.schema_version === "string") {
297
+ parseMajorSchema(raw.schema_version, "hq-lanes-config", 1);
298
+ }
299
+ const providersRaw = raw.providers;
300
+ if (providersRaw !== undefined && !isPlainObject(providersRaw)) {
301
+ throw new LanesConfigError("invalid_config", "providers must be an object");
302
+ }
303
+ if (isPlainObject(providersRaw)) {
304
+ const unknown = Object.keys(providersRaw).filter((key) => !isLaneProvider(key));
305
+ if (unknown.length > 0) {
306
+ throw new LanesConfigError("unknown_provider", `Unknown provider(s) ${unknown.join(", ")}. Accepted: ${LANE_PROVIDERS.join(", ")}`, { accepted: [...LANE_PROVIDERS], value: unknown });
307
+ }
308
+ }
309
+ // Spec §5.5: a missing *file* yields the documented defaults, and the
310
+ // enable switch — not omission — is how an operator turns an account off.
311
+ // The provider set is fixed at {claude, codex, grok} (§5), so a present
312
+ // config that names only `grok` still receives the documented claude and
313
+ // codex defaults (including real-home `~/.claude` / `~/.codex`). Omitting
314
+ // a provider key is not a disable.
315
+ const providers = {};
316
+ for (const provider of LANE_PROVIDERS) {
317
+ const entry = isPlainObject(providersRaw) ? providersRaw[provider] : undefined;
318
+ providers[provider] =
319
+ entry === undefined
320
+ ? documentedDefaultConfig(deps).providers[provider]
321
+ : parseProvider(provider, entry, deps);
322
+ }
323
+ return {
324
+ schema_version: typeof raw.schema_version === "string"
325
+ ? raw.schema_version
326
+ : CONFIG_SCHEMA_VERSION,
327
+ providers,
328
+ throttle: parseThrottle(raw.throttle),
329
+ liveness: parseLiveness(raw.liveness),
330
+ };
331
+ }
332
+ export function lanesConfigPath(hqRoot) {
333
+ return path.join(hqRoot, LANES_CONFIG_REL);
334
+ }
335
+ /**
336
+ * Load operator config. Missing file → documented defaults. Unreadable or
337
+ * invalid JSON is an error (never a silent default).
338
+ */
339
+ export function loadLanesConfig(hqRoot, deps = defaultDeps()) {
340
+ const filePath = lanesConfigPath(hqRoot);
341
+ if (!deps.existsSync(filePath)) {
342
+ return documentedDefaultConfig(deps);
343
+ }
344
+ let text;
345
+ try {
346
+ text = deps.readFileSync(filePath, "utf8");
347
+ }
348
+ catch (err) {
349
+ throw new LanesConfigError("config_unreadable", `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`, { path: filePath });
350
+ }
351
+ let parsed;
352
+ try {
353
+ parsed = JSON.parse(text);
354
+ }
355
+ catch (err) {
356
+ throw new LanesConfigError("invalid_config", `Invalid JSON in ${filePath}: ${err instanceof Error ? err.message : String(err)}`, { path: filePath });
357
+ }
358
+ return parseLanesConfig(parsed, deps);
359
+ }
360
+ function defaultWriteDeps() {
361
+ return {
362
+ mkdirSync: fs.mkdirSync.bind(fs),
363
+ writeFileSync: fs.writeFileSync.bind(fs),
364
+ renameSync: fs.renameSync.bind(fs),
365
+ unlinkSync: fs.unlinkSync.bind(fs),
366
+ };
367
+ }
368
+ /** Persist operator config. Homes are stored as already-canonicalised paths. */
369
+ export function writeLanesConfig(hqRoot, config, deps = defaultWriteDeps()) {
370
+ const filePath = lanesConfigPath(hqRoot);
371
+ const dir = path.dirname(filePath);
372
+ deps.mkdirSync(dir, { recursive: true });
373
+ const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
374
+ try {
375
+ deps.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}\n`, {
376
+ encoding: "utf8",
377
+ });
378
+ deps.renameSync(tmp, filePath);
379
+ }
380
+ catch (err) {
381
+ try {
382
+ deps.unlinkSync(tmp);
383
+ }
384
+ catch (unlinkErr) {
385
+ const code = unlinkErr.code;
386
+ if (code !== "ENOENT") {
387
+ process.stderr.write(`lanes config: unlink tmp failed for ${tmp}: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}\n`);
388
+ }
389
+ }
390
+ throw err;
391
+ }
392
+ }
393
+ /** Resume-time compaction threshold for a provider, defaulting to one hour. */
394
+ export function providerCompactAfterMs(provider) {
395
+ return provider.compact_after_ms ?? DEFAULT_COMPACT_AFTER_MS;
396
+ }
397
+ /** Minimum gap between two `lane_progress` events, defaulting to five minutes. */
398
+ export function providerProgressEventMinIntervalMs(provider) {
399
+ return provider.progress_event_min_interval_ms ?? DEFAULT_PROGRESS_EVENT_MIN_INTERVAL_MS;
400
+ }
401
+ /** Account running cap, defaulting to the provider ceiling when unset. */
402
+ export function accountMaxRunning(provider, accountId) {
403
+ const account = provider.accounts[accountId];
404
+ if (account?.max_running !== undefined)
405
+ return account.max_running;
406
+ return provider.max_running;
407
+ }
408
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Follow-up delivery decision, per provider and per lane state, in one place.
3
+ *
4
+ * Two mechanisms exist. A live provider process drains the drop box on its
5
+ * next `PreToolUse` event (dropbox.ts). A Claude process is print-and-exit,
6
+ * so after it exits the only way to deliver is `claude -p --resume <sid>`,
7
+ * which continues the recorded session and keeps its prompt cache. Codex
8
+ * (0.155.1) has `codex exec resume <thread-id>`; the id is the `thread_id`
9
+ * of the `thread.started` event on the `--json` stdout stream, and the same
10
+ * id comes back on every resumed turn. Grok's CLI (1.0.34) has
11
+ * `--resume <id>` and its `--single --output-format json` result carries a
12
+ * `sessionId`, but no live run has shown `--single --resume` continue a
13
+ * session (the grok account was out of balance when this was written), so
14
+ * an exited grok lane keeps no delivery path until that is proven.
15
+ *
16
+ * Every dispatch site (`hq lanes message`, spawn, reconcile resume) asks this
17
+ * module rather than testing provider or state inline. A guard bound at one
18
+ * site and not another is the defect class this branch has already shipped.
19
+ */
20
+ import { type LaneProvider, type LaneRecord, type LaneState } from "./types.js";
21
+ /** Which providers can continue an exited worker from a recorded session id. */
22
+ export declare const PROVIDER_SESSION_RESUME: Record<LaneProvider, boolean>;
23
+ export declare function providerSupportsSessionResume(provider: LaneProvider): boolean;
24
+ /**
25
+ * States an operator message may resume from, given a session id.
26
+ *
27
+ * `done`: the worker finished its brief, wrote a done envelope and exited on
28
+ * its own. Nothing about the lane says the operator is finished with it; a
29
+ * follow-up is the normal next step, and the session is intact.
30
+ *
31
+ * `blocked`: the worker exited after writing a blocked envelope that names
32
+ * what it needs. An operator message is usually that missing input. The state
33
+ * is not terminal (spec §6.7) and nothing stopped the lane deliberately.
34
+ *
35
+ * Refused by `hq lanes message`:
36
+ * `awaiting_input` has its own path, `hq lanes questions answer`, and the
37
+ * reconcile pass resumes it once every question is answered. `killed` and
38
+ * `cancelled` were stopped on purpose by the operator; a message must not
39
+ * silently restart them. `failed` means the run ended without an envelope or
40
+ * never started; the operator has not seen why. `interrupted` exited with
41
+ * partial output and no envelope, so the lane's state was never closed
42
+ * cleanly; only the explicit `hq lanes resume` opt-in continues it
43
+ * (RESUME_COMMAND_STATES). `queued` and `running` take the drop box, not a
44
+ * resume.
45
+ */
46
+ export declare const RESUMABLE_LANE_STATES: readonly ["done", "blocked"];
47
+ export type ResumableLaneState = (typeof RESUMABLE_LANE_STATES)[number];
48
+ export declare function isResumableLaneState(state: LaneState): state is ResumableLaneState;
49
+ /**
50
+ * States `hq lanes resume` continues from. `interrupted` is the one a plain
51
+ * message refuses: the run ended without closing its state, and continuing
52
+ * it is a decision the operator makes by name. `done` and `blocked` are here
53
+ * so the one command covers every exited-with-a-session case.
54
+ */
55
+ export declare const RESUME_COMMAND_STATES: readonly ["interrupted", "done", "blocked"];
56
+ export type ResumeCommandState = (typeof RESUME_COMMAND_STATES)[number];
57
+ export declare function isResumeCommandState(state: LaneState): state is ResumeCommandState;
58
+ export declare const LANE_DELIVERY_MECHANISMS: readonly ["dropbox", "resume", "none"];
59
+ export type LaneDeliveryMechanism = (typeof LANE_DELIVERY_MECHANISMS)[number];
60
+ export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id">;
61
+ /**
62
+ * `dropbox`: a process is (or is about to be) alive to drain the inbox.
63
+ * `resume`: the process has exited, the provider can resume, the state allows
64
+ * it, and a session id was recorded from the worker's own result.
65
+ * `none`: nothing can deliver; the caller must refuse and write nothing.
66
+ */
67
+ export declare function laneDeliveryMechanism(lane: DeliveryLane): LaneDeliveryMechanism;
68
+ export declare function hasRecordedSessionId(lane: Pick<LaneRecord, "provider_session_id">): lane is Pick<LaneRecord, "provider_session_id"> & {
69
+ provider_session_id: string;
70
+ };
71
+ export declare function deliveryRefusalReason(lane: DeliveryLane): string;
72
+ /** Reasons a resumable provider's exit recorded no session id. Never a fabricated value. */
73
+ export declare const SESSION_ID_ABSENT_REASONS: readonly ["worker_out_missing", "no_result_json", "no_session_id_field", "no_thread_started_event"];
74
+ export type SessionIdAbsentReason = (typeof SESSION_ID_ABSENT_REASONS)[number];
75
+ export interface WorkerRunResult {
76
+ session_id: string | null;
77
+ session_id_absent: SessionIdAbsentReason | null;
78
+ cache_read_input_tokens: number | null;
79
+ cache_creation_input_tokens: number | null;
80
+ }
81
+ /**
82
+ * Parse the `claude -p --output-format json` result. stdout is appended
83
+ * across runs of the same lane, so the last `type: "result"` object is this
84
+ * run's. Only `session_id` and the two cache counters are read out; the
85
+ * prompt, the response body and everything else in the payload stay on disk.
86
+ */
87
+ export declare function parseClaudeResultOutput(text: string): WorkerRunResult;
88
+ /**
89
+ * Parse the `codex exec --json` event stream. The first event of a run is
90
+ * `{"type":"thread.started","thread_id":"<uuid>"}`; that id is what
91
+ * `codex exec resume <id>` takes, and a resumed run emits the same id again.
92
+ * Usage comes from the last `turn.completed` event: `cached_input_tokens`
93
+ * is the cache read and `cache_write_input_tokens` the cache creation.
94
+ * Nothing else on the stream is read out.
95
+ */
96
+ export declare function parseCodexEventOutput(text: string): WorkerRunResult;
97
+ /** Per-provider stdout parser; null for a provider whose exit carries nothing to record. */
98
+ export declare function parseWorkerRunOutput(provider: LaneProvider, text: string): WorkerRunResult | null;
99
+ /**
100
+ * Read this run's result from the worker's own stdout. stdout is opened in
101
+ * append mode across runs, so only bytes past `workerOutOffset` (recorded in
102
+ * spawn.json at launch) are this run's. Returns null for a provider whose
103
+ * exit carries nothing to record, so those lanes keep behaving exactly as
104
+ * before.
105
+ */
106
+ export declare function readWorkerRunResult(runDir: string, provider: LaneProvider,
107
+ /** Bytes of worker.out that belong to earlier runs; parsing starts after them. */
108
+ workerOutOffset?: number): WorkerRunResult | null;
109
+ export declare const LAUNCH_TRIGGERS: readonly ["admit", "answered_questions", "message", "resume"];
110
+ export type LaunchTriggerKind = (typeof LAUNCH_TRIGGERS)[number];
111
+ export type LaunchTrigger = {
112
+ kind: "admit";
113
+ } | {
114
+ kind: "answered_questions";
115
+ } | {
116
+ kind: "message";
117
+ text: string;
118
+ message_id: string;
119
+ } | {
120
+ /** `hq lanes resume`: the operator's explicit opt-in to continue an exited lane. */
121
+ kind: "resume";
122
+ /** Lane state at the moment of the command; recorded as `last_resume.resumed_from`. */
123
+ from: ResumeCommandState;
124
+ text?: string;
125
+ message_id?: string;
126
+ };
127
+ export type LaunchPlan = {
128
+ mode: "fresh";
129
+ } | {
130
+ mode: "resume";
131
+ session_id: string;
132
+ };
133
+ /**
134
+ * Refusal for `hq lanes resume`, in precedence order: wrong state, provider
135
+ * without a resume path, no recorded session. Null when the resume can go.
136
+ */
137
+ export declare function resumeCommandRefusal(lane: DeliveryLane): {
138
+ error: "lane_not_resumable" | "resume_unsupported" | "no_provider_session";
139
+ message: string;
140
+ } | null;
141
+ /**
142
+ * Decide fresh-vs-resume for a spawn. Admission always starts fresh with the
143
+ * brief. Answered questions resume the recorded session when the provider
144
+ * can, else start fresh with the answers file (today's behaviour). An operator
145
+ * message can only be a resume, and only when `laneDeliveryMechanism` says so.
146
+ * `hq lanes resume` is also always a resume, gated by `resumeCommandRefusal`.
147
+ */
148
+ export declare function planLaneLaunch(lane: DeliveryLane, trigger: LaunchTrigger): LaunchPlan;
149
+ /** Lane states a launch may claim `running` from, per trigger. */
150
+ export declare function claimableStatesFor(trigger: LaunchTrigger): readonly LaneState[];
151
+ //# sourceMappingURL=delivery.d.ts.map