@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.
- package/CHANGELOG.md +329 -0
- package/assets/bot-workers/setup/skills/joining-a-team.md +125 -0
- package/assets/bot-workers/setup/skills/your-business.md +270 -0
- package/assets/bot-workers/setup/worker.yaml +189 -58
- package/assets/lanes-hooks/hq-lanes-drain.json +15 -0
- package/assets/lanes-hooks/hq-lanes-drain.mjs +254 -0
- package/assets/lanes-hooks/hq-lanes-drain.sh +21 -0
- package/assets/lanes-reconcile/hq-lanes-reconcile.service +11 -0
- package/assets/lanes-reconcile/hq-lanes-reconcile.timer +12 -0
- package/dist/command-catalog.generated.d.ts +643 -6
- package/dist/command-catalog.generated.js +836 -6
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +11 -1
- package/dist/commands/agents.d.ts +12 -0
- package/dist/commands/agents.js +46 -2
- package/dist/commands/bot.d.ts +10 -0
- package/dist/commands/bot.js +6 -0
- package/dist/commands/lanes.d.ts +114 -0
- package/dist/commands/lanes.js +2969 -0
- package/dist/commands/mesh.js +287 -24
- package/dist/commands/onboard-create-identity.d.ts +26 -0
- package/dist/commands/onboard-create-identity.js +23 -0
- package/dist/commands/onboard.js +13 -4
- package/dist/lib/bot/progress.d.ts +6 -0
- package/dist/lib/bot/progress.js +16 -0
- package/dist/lib/bot/runtime/grok.d.ts +30 -7
- package/dist/lib/bot/runtime/grok.js +86 -8
- package/dist/lib/bot/runtime/index.js +15 -8
- package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
- package/dist/lib/bot/runtime/messages-stream.js +49 -3
- package/dist/lib/lanes/accounts.d.ts +103 -0
- package/dist/lib/lanes/accounts.js +318 -0
- package/dist/lib/lanes/compaction.d.ts +96 -0
- package/dist/lib/lanes/compaction.js +197 -0
- package/dist/lib/lanes/config.d.ts +71 -0
- package/dist/lib/lanes/config.js +408 -0
- package/dist/lib/lanes/delivery.d.ts +151 -0
- package/dist/lib/lanes/delivery.js +354 -0
- package/dist/lib/lanes/dropbox.d.ts +96 -0
- package/dist/lib/lanes/dropbox.js +408 -0
- package/dist/lib/lanes/effort.d.ts +50 -0
- package/dist/lib/lanes/effort.js +109 -0
- package/dist/lib/lanes/errors.d.ts +39 -0
- package/dist/lib/lanes/errors.js +72 -0
- package/dist/lib/lanes/hooks-install.d.ts +81 -0
- package/dist/lib/lanes/hooks-install.js +326 -0
- package/dist/lib/lanes/hq-root.d.ts +19 -0
- package/dist/lib/lanes/hq-root.js +28 -0
- package/dist/lib/lanes/index.d.ts +49 -0
- package/dist/lib/lanes/index.js +27 -0
- package/dist/lib/lanes/liveness.d.ts +95 -0
- package/dist/lib/lanes/liveness.js +332 -0
- package/dist/lib/lanes/machine-identity.d.ts +34 -0
- package/dist/lib/lanes/machine-identity.js +92 -0
- package/dist/lib/lanes/mesh-events.d.ts +109 -0
- package/dist/lib/lanes/mesh-events.js +275 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +65 -0
- package/dist/lib/lanes/outpost-dispatch.js +90 -0
- package/dist/lib/lanes/questions.d.ts +75 -0
- package/dist/lib/lanes/questions.js +243 -0
- package/dist/lib/lanes/quota.d.ts +95 -0
- package/dist/lib/lanes/quota.js +320 -0
- package/dist/lib/lanes/reconcile-install.d.ts +69 -0
- package/dist/lib/lanes/reconcile-install.js +221 -0
- package/dist/lib/lanes/reconcile.d.ts +102 -0
- package/dist/lib/lanes/reconcile.js +985 -0
- package/dist/lib/lanes/registry.d.ts +116 -0
- package/dist/lib/lanes/registry.js +521 -0
- package/dist/lib/lanes/spawn.d.ts +274 -0
- package/dist/lib/lanes/spawn.js +1363 -0
- package/dist/lib/lanes/store.d.ts +68 -0
- package/dist/lib/lanes/store.js +745 -0
- package/dist/lib/lanes/throttle.d.ts +89 -0
- package/dist/lib/lanes/throttle.js +297 -0
- package/dist/lib/lanes/types.d.ts +535 -0
- package/dist/lib/lanes/types.js +350 -0
- package/dist/lib/lanes/watcher-main.d.ts +9 -0
- package/dist/lib/lanes/watcher-main.js +22 -0
- package/dist/lib/lanes/watcher.d.ts +216 -0
- package/dist/lib/lanes/watcher.js +859 -0
- package/dist/lib/mesh/api.d.ts +71 -7
- package/dist/lib/mesh/api.js +167 -28
- package/dist/lib/mesh/live/backfill-held.d.ts +18 -7
- package/dist/lib/mesh/live/backfill-held.js +66 -21
- package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
- package/dist/lib/mesh/live/daemon/config.js +75 -0
- package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
- package/dist/lib/mesh/live/daemon/index.js +3 -3
- package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
- package/dist/lib/mesh/live/daemon/install.js +444 -2
- package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/paths.js +2 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/run.js +111 -54
- package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.js +31 -8
- package/dist/lib/scan-packages/index.d.ts +2 -0
- package/dist/lib/scan-packages/index.js +18 -13
- package/dist/lib/work-context/config.d.ts +20 -0
- package/dist/lib/work-context/config.js +26 -5
- package/dist/lib/work-context/reconcile.js +28 -7
- package/dist/lib/work-context/state.d.ts +12 -0
- package/dist/lib/work-context/state.js +131 -3
- package/dist/lib/work-context/windows-acl.d.ts +123 -0
- package/dist/lib/work-context/windows-acl.js +425 -0
- package/dist/utils/pack-contributions.js +5 -4
- 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
|