@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
|
+
* Lane drop box: append-only inbox.jsonl plus exactly-once drain (spec §6.5).
|
|
3
|
+
*
|
|
4
|
+
* Consume is `rename(inbox.jsonl → draining/<unique>)` — atomic on this box.
|
|
5
|
+
* Two overlapping drainers cannot both claim the same bytes; a crash after
|
|
6
|
+
* rename leaves the messages in draining/ for the next drain to recover.
|
|
7
|
+
* A read-then-truncate is not this mechanism and loses or duplicates under
|
|
8
|
+
* overlap.
|
|
9
|
+
*/
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { LanesError } from "./errors.js";
|
|
14
|
+
import { LANES_RUNS_REL, readProcStarttime } from "./spawn.js";
|
|
15
|
+
export const DROPBOX_DIR = "dropbox";
|
|
16
|
+
export const INBOX_NAME = "inbox.jsonl";
|
|
17
|
+
export const DRAINING_DIR = "draining";
|
|
18
|
+
export const DELIVERED_DIR = "delivered";
|
|
19
|
+
export const INFLIGHT_DIR = "inflight";
|
|
20
|
+
function defaultDrainDeps() {
|
|
21
|
+
return {
|
|
22
|
+
mkdirSync: fs.mkdirSync.bind(fs),
|
|
23
|
+
renameSync: fs.renameSync.bind(fs),
|
|
24
|
+
readFileSync: fs.readFileSync.bind(fs),
|
|
25
|
+
writeFileSync: fs.writeFileSync.bind(fs),
|
|
26
|
+
unlinkSync: fs.unlinkSync.bind(fs),
|
|
27
|
+
existsSync: fs.existsSync.bind(fs),
|
|
28
|
+
readdirSync: fs.readdirSync.bind(fs),
|
|
29
|
+
rmdirSync: fs.rmdirSync.bind(fs),
|
|
30
|
+
kill: (pid, signal) => {
|
|
31
|
+
process.kill(pid, signal);
|
|
32
|
+
},
|
|
33
|
+
pid: process.pid,
|
|
34
|
+
unique: mintClaimId,
|
|
35
|
+
readStarttime: readProcStarttime,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function mintClaimId() {
|
|
39
|
+
return `${process.pid}.${process.hrtime.bigint()}.${randomBytes(4).toString("hex")}`;
|
|
40
|
+
}
|
|
41
|
+
export function assertSafeLaneId(laneId) {
|
|
42
|
+
if (!laneId || laneId.trim() !== laneId || laneId.length === 0) {
|
|
43
|
+
throw new LanesError("invalid_lane_id", "HQ_LANE_ID is empty or padded", {
|
|
44
|
+
lane_id: laneId,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (laneId.includes("/") ||
|
|
48
|
+
laneId.includes("\\") ||
|
|
49
|
+
laneId.includes("\0") ||
|
|
50
|
+
laneId === "." ||
|
|
51
|
+
laneId === ".." ||
|
|
52
|
+
laneId.includes("..")) {
|
|
53
|
+
throw new LanesError("invalid_lane_id", `HQ_LANE_ID ${JSON.stringify(laneId)} is not a single path segment`, { lane_id: laneId });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function dropboxDir(hqRoot, laneId) {
|
|
57
|
+
assertSafeLaneId(laneId);
|
|
58
|
+
return path.join(hqRoot, LANES_RUNS_REL, laneId, DROPBOX_DIR);
|
|
59
|
+
}
|
|
60
|
+
export function dropboxInboxPath(hqRoot, laneId) {
|
|
61
|
+
return path.join(dropboxDir(hqRoot, laneId), INBOX_NAME);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Worker tool-call liveness (watcher.ts reads it). A Claude worker run with
|
|
65
|
+
* `--output-format json` writes nothing to worker.out until it exits, so the
|
|
66
|
+
* only thing it does observably while working is call tools. Every tool call
|
|
67
|
+
* runs this drain hook, and the hook records the call here before it touches
|
|
68
|
+
* the drop box. The file's mtime is the signal; the body is for humans.
|
|
69
|
+
*/
|
|
70
|
+
export const TOOL_ACTIVITY_NAME = "tool-activity.json";
|
|
71
|
+
export function toolActivityPath(hqRoot, laneId) {
|
|
72
|
+
assertSafeLaneId(laneId);
|
|
73
|
+
return path.join(hqRoot, LANES_RUNS_REL, laneId, "logs", TOOL_ACTIVITY_NAME);
|
|
74
|
+
}
|
|
75
|
+
export function recordToolActivity(hqRoot, laneId, at = new Date().toISOString()) {
|
|
76
|
+
const dest = toolActivityPath(hqRoot, laneId);
|
|
77
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
78
|
+
const record = { at, hook_pid: process.pid, hook_event: "PreToolUse" };
|
|
79
|
+
const tmp = `${dest}.${process.pid}.${mintClaimId()}.tmp`;
|
|
80
|
+
fs.writeFileSync(tmp, `${JSON.stringify(record)}\n`, "utf8");
|
|
81
|
+
fs.renameSync(tmp, dest);
|
|
82
|
+
return record;
|
|
83
|
+
}
|
|
84
|
+
export function readToolActivity(hqRoot, laneId) {
|
|
85
|
+
let text;
|
|
86
|
+
try {
|
|
87
|
+
text = fs.readFileSync(toolActivityPath(hqRoot, laneId), "utf8");
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
const code = err.code;
|
|
91
|
+
if (code === "ENOENT")
|
|
92
|
+
return null;
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(text);
|
|
97
|
+
if (typeof parsed.at !== "string" || typeof parsed.hook_pid !== "number")
|
|
98
|
+
return null;
|
|
99
|
+
return { at: parsed.at, hook_pid: parsed.hook_pid, hook_event: "PreToolUse" };
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
process.stderr.write(`hq lanes: tool activity for ${laneId} is unparseable: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function appendDropboxMessage(hqRoot, laneId, message) {
|
|
107
|
+
const inbox = dropboxInboxPath(hqRoot, laneId);
|
|
108
|
+
fs.mkdirSync(path.dirname(inbox), { recursive: true });
|
|
109
|
+
fs.appendFileSync(inbox, `${JSON.stringify(message)}\n`, "utf8");
|
|
110
|
+
return message;
|
|
111
|
+
}
|
|
112
|
+
function listNames(dir, deps) {
|
|
113
|
+
try {
|
|
114
|
+
return deps.readdirSync(dir).filter((name) => !name.startsWith("."));
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
const code = err.code;
|
|
118
|
+
if (code === "ENOENT")
|
|
119
|
+
return [];
|
|
120
|
+
throw new LanesError("dropbox_unreadable", `Failed to read drop box directory ${dir}: ${err instanceof Error ? err.message : String(err)}`, { path: dir });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function tryRename(src, dest, deps) {
|
|
124
|
+
try {
|
|
125
|
+
deps.renameSync(src, dest);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const code = err.code;
|
|
130
|
+
if (code === "ENOENT")
|
|
131
|
+
return false;
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function pidAlive(pid, deps, starttime) {
|
|
136
|
+
try {
|
|
137
|
+
deps.kill(pid, 0);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
const code = err.code;
|
|
141
|
+
if (code === "ESRCH")
|
|
142
|
+
return false;
|
|
143
|
+
if (code === "EPERM") {
|
|
144
|
+
/* still check starttime below */
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Unknown starttime is not proof of liveness. Recover. Skip steal only
|
|
151
|
+
// when the pid is alive, recorded starttime is non-zero, and it matches.
|
|
152
|
+
if (starttime === undefined || starttime === 0)
|
|
153
|
+
return false;
|
|
154
|
+
const live = (deps.readStarttime ?? readProcStarttime)(pid);
|
|
155
|
+
if (live === null || live === 0)
|
|
156
|
+
return false;
|
|
157
|
+
return live === starttime;
|
|
158
|
+
}
|
|
159
|
+
function parseJsonl(text, filePath) {
|
|
160
|
+
const messages = [];
|
|
161
|
+
const lines = text.split("\n");
|
|
162
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
163
|
+
const line = lines[i];
|
|
164
|
+
if (line === undefined || line.trim() === "")
|
|
165
|
+
continue;
|
|
166
|
+
let parsed;
|
|
167
|
+
try {
|
|
168
|
+
parsed = JSON.parse(line);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
process.stderr.write(`hq-lanes-drain: skipping unparseable drop box line ${i + 1} of ${filePath}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
175
|
+
process.stderr.write(`hq-lanes-drain: skipping non-object drop box line ${i + 1} of ${filePath}\n`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const rec = parsed;
|
|
179
|
+
const textField = rec.text;
|
|
180
|
+
messages.push({
|
|
181
|
+
...rec,
|
|
182
|
+
text: typeof textField === "string"
|
|
183
|
+
? textField
|
|
184
|
+
: JSON.stringify(rec),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return messages;
|
|
188
|
+
}
|
|
189
|
+
function readClaimedFile(filePath, deps) {
|
|
190
|
+
let text;
|
|
191
|
+
try {
|
|
192
|
+
text = deps.readFileSync(filePath, "utf8");
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
throw new LanesError("dropbox_unreadable", `Failed to read drop box file ${filePath}: ${err instanceof Error ? err.message : String(err)}`, { path: filePath });
|
|
196
|
+
}
|
|
197
|
+
return parseJsonl(text, filePath);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Drain this lane's inbox exactly once. Concurrent callers: one rename wins;
|
|
201
|
+
* the loser sees ENOENT and returns whatever it separately claimed (usually
|
|
202
|
+
* nothing). Leftovers in draining/ and dead inflight dirs are recovered.
|
|
203
|
+
*/
|
|
204
|
+
export function drainDropbox(hqRoot, laneId, depsInput = {}) {
|
|
205
|
+
const deps = { ...defaultDrainDeps(), ...depsInput };
|
|
206
|
+
const box = dropboxDir(hqRoot, laneId);
|
|
207
|
+
const inbox = path.join(box, INBOX_NAME);
|
|
208
|
+
const draining = path.join(box, DRAINING_DIR);
|
|
209
|
+
const delivered = path.join(box, DELIVERED_DIR);
|
|
210
|
+
const inflightRoot = path.join(box, INFLIGHT_DIR);
|
|
211
|
+
try {
|
|
212
|
+
deps.mkdirSync(box, { recursive: true });
|
|
213
|
+
deps.mkdirSync(draining, { recursive: true });
|
|
214
|
+
deps.mkdirSync(delivered, { recursive: true });
|
|
215
|
+
deps.mkdirSync(inflightRoot, { recursive: true });
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
throw new LanesError("dropbox_unreadable", `Failed to open drop box ${box}: ${err instanceof Error ? err.message : String(err)}`, { path: box });
|
|
219
|
+
}
|
|
220
|
+
const starttime = (deps.readStarttime ?? readProcStarttime)(deps.pid) ?? 0;
|
|
221
|
+
const inflightDir = path.join(inflightRoot, `${deps.pid}.${starttime}.${deps.unique()}`);
|
|
222
|
+
deps.mkdirSync(inflightDir, { recursive: true });
|
|
223
|
+
if (deps.existsSync(inbox)) {
|
|
224
|
+
deps.afterObserveInbox?.();
|
|
225
|
+
tryRename(inbox, path.join(inflightDir, INBOX_NAME), deps);
|
|
226
|
+
}
|
|
227
|
+
for (const name of listNames(draining, deps)) {
|
|
228
|
+
tryRename(path.join(draining, name), path.join(inflightDir, name), deps);
|
|
229
|
+
}
|
|
230
|
+
for (const name of listNames(inflightRoot, deps)) {
|
|
231
|
+
const other = path.join(inflightRoot, name);
|
|
232
|
+
if (other === inflightDir)
|
|
233
|
+
continue;
|
|
234
|
+
const parts = name.split(".");
|
|
235
|
+
const pidPart = Number(parts[0]);
|
|
236
|
+
const startPart = Number(parts[1]);
|
|
237
|
+
if (Number.isInteger(pidPart) &&
|
|
238
|
+
pidPart !== deps.pid &&
|
|
239
|
+
pidAlive(pidPart, deps, Number.isInteger(startPart) ? startPart : undefined)) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
for (const file of listNames(other, deps)) {
|
|
243
|
+
tryRename(path.join(other, file), path.join(inflightDir, file), deps);
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
deps.rmdirSync(other);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
const code = err.code;
|
|
250
|
+
if (code !== "ENOENT" && code !== "ENOTEMPTY")
|
|
251
|
+
throw err;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const claimed = listNames(inflightDir, deps);
|
|
255
|
+
const messages = [];
|
|
256
|
+
for (const name of claimed) {
|
|
257
|
+
messages.push(...readClaimedFile(path.join(inflightDir, name), deps));
|
|
258
|
+
}
|
|
259
|
+
deps.afterRead?.();
|
|
260
|
+
for (const name of claimed) {
|
|
261
|
+
const src = path.join(inflightDir, name);
|
|
262
|
+
const dest = path.join(delivered, `${deps.unique()}.${name}`);
|
|
263
|
+
if (!tryRename(src, dest, deps)) {
|
|
264
|
+
throw new LanesError("dropbox_unreadable", `Claimed drop box file disappeared before delivery archive: ${src}`, { path: src });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
deps.rmdirSync(inflightDir);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
const code = err.code;
|
|
272
|
+
if (code !== "ENOENT" && code !== "ENOTEMPTY")
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
return messages;
|
|
276
|
+
}
|
|
277
|
+
export function formatOperatorInstruction(messages) {
|
|
278
|
+
const body = messages
|
|
279
|
+
.map((msg, i) => {
|
|
280
|
+
const header = msg.kind === "question_answer"
|
|
281
|
+
? `${i + 1}. Answer to question ${msg.question_id ?? "?"} — choice: ${msg.choice ?? "?"}`
|
|
282
|
+
: `${i + 1}.`;
|
|
283
|
+
return `${header}\n${msg.text}`;
|
|
284
|
+
})
|
|
285
|
+
.join("\n\n");
|
|
286
|
+
return [
|
|
287
|
+
"OPERATOR INSTRUCTION (outranks the lane brief).",
|
|
288
|
+
"The following messages were delivered through this lane's drop box. Treat them as operator instructions that outrank the brief.",
|
|
289
|
+
"",
|
|
290
|
+
body,
|
|
291
|
+
].join("\n");
|
|
292
|
+
}
|
|
293
|
+
export function hookAdditionalContextJson(messages) {
|
|
294
|
+
return `${JSON.stringify({
|
|
295
|
+
hookSpecificOutput: {
|
|
296
|
+
hookEventName: "PreToolUse",
|
|
297
|
+
additionalContext: formatOperatorInstruction(messages),
|
|
298
|
+
},
|
|
299
|
+
})}\n`;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Hook protocol. HQ_LANE_ID absent or empty → exit 0, empty stdout, empty stderr.
|
|
303
|
+
* That no-op is load-bearing: this hook is registered in the grok home and runs
|
|
304
|
+
* on every session on the box.
|
|
305
|
+
*/
|
|
306
|
+
export function runDrainHook(env) {
|
|
307
|
+
const laneId = env.HQ_LANE_ID ?? "";
|
|
308
|
+
if (laneId === "") {
|
|
309
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
310
|
+
}
|
|
311
|
+
const hqRoot = env.HQ_ROOT ?? "";
|
|
312
|
+
if (hqRoot === "") {
|
|
313
|
+
return {
|
|
314
|
+
exitCode: 1,
|
|
315
|
+
stdout: "",
|
|
316
|
+
stderr: "hq-lanes-drain: HQ_ROOT is unset while HQ_LANE_ID is set\n",
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
// Liveness first: a failed drain must still leave proof the worker called
|
|
320
|
+
// a tool. A failure here is reported, never fatal to delivery.
|
|
321
|
+
let stderr = "";
|
|
322
|
+
try {
|
|
323
|
+
recordToolActivity(hqRoot, laneId);
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
stderr += `hq-lanes-drain: failed to record tool activity: ${err instanceof Error ? err.message : String(err)}\n`;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const messages = drainDropbox(hqRoot, laneId);
|
|
330
|
+
if (messages.length === 0) {
|
|
331
|
+
return { exitCode: 0, stdout: "", stderr };
|
|
332
|
+
}
|
|
333
|
+
return { exitCode: 0, stdout: hookAdditionalContextJson(messages), stderr };
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
337
|
+
return {
|
|
338
|
+
exitCode: 1,
|
|
339
|
+
stdout: "",
|
|
340
|
+
stderr: `${stderr}hq-lanes-drain: ${message}\n`,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/** Spec §6.5: only a still-running lane gets drop-box delivery. */
|
|
345
|
+
export function laneAcceptsDropboxDelivery(lane) {
|
|
346
|
+
return lane.state === "running";
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Operator `hq lanes message` delivery. Queued is accepted because the
|
|
350
|
+
* worker that will spawn drains the inbox on its first tool event.
|
|
351
|
+
* `laneAcceptsDropboxDelivery` refuses queued, so it is the wrong guard here.
|
|
352
|
+
*/
|
|
353
|
+
export function laneAcceptsOperatorMessage(lane) {
|
|
354
|
+
const state = lane.state;
|
|
355
|
+
switch (state) {
|
|
356
|
+
case "queued":
|
|
357
|
+
case "running":
|
|
358
|
+
return true;
|
|
359
|
+
case "awaiting_input":
|
|
360
|
+
case "blocked":
|
|
361
|
+
case "interrupted":
|
|
362
|
+
case "done":
|
|
363
|
+
case "failed":
|
|
364
|
+
case "cancelled":
|
|
365
|
+
case "killed":
|
|
366
|
+
return false;
|
|
367
|
+
default: {
|
|
368
|
+
const _exhaustive = state;
|
|
369
|
+
return _exhaustive;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
export function operatorMessageRefusalReason(state) {
|
|
374
|
+
switch (state) {
|
|
375
|
+
case "queued":
|
|
376
|
+
case "running":
|
|
377
|
+
return "";
|
|
378
|
+
case "awaiting_input":
|
|
379
|
+
return "Lane is awaiting_input. The worker has exited after writing an ask envelope. Use hq lanes questions answer <question-id>.";
|
|
380
|
+
case "blocked":
|
|
381
|
+
return "Lane is blocked. The worker has exited and there is no delivery path.";
|
|
382
|
+
case "interrupted":
|
|
383
|
+
case "done":
|
|
384
|
+
case "failed":
|
|
385
|
+
case "cancelled":
|
|
386
|
+
case "killed":
|
|
387
|
+
return `Lane is ${state}. The worker is gone and there is no durable fallback record.`;
|
|
388
|
+
default: {
|
|
389
|
+
const _exhaustive = state;
|
|
390
|
+
return _exhaustive;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/** Delivery target is the lane_id on the question record. Never a project/story lookup. */
|
|
395
|
+
export function laneIdForAnswerDelivery(question) {
|
|
396
|
+
return question.lane_id;
|
|
397
|
+
}
|
|
398
|
+
export function answerDropboxMessage(question, choice, at, messageId) {
|
|
399
|
+
return {
|
|
400
|
+
at,
|
|
401
|
+
message_id: messageId,
|
|
402
|
+
kind: "question_answer",
|
|
403
|
+
question_id: question.question_id,
|
|
404
|
+
choice,
|
|
405
|
+
text: `OPERATOR INSTRUCTION (outranks the lane brief). Question ${question.question_id} was answered with choice: ${choice}. Treat this as an operator decision that outranks the brief.`,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
//# sourceMappingURL=dropbox.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effort (reasoning depth) per lane provider.
|
|
3
|
+
*
|
|
4
|
+
* Every provider CLI here tolerates or ignores a bad value in some way:
|
|
5
|
+
* `claude --effort bogus` warns and runs at the default, and
|
|
6
|
+
* `codex -c model_reasoning_effort=bogus` loads without complaint. hq-cli
|
|
7
|
+
* therefore validates the value against each provider's own set before any
|
|
8
|
+
* spawn, and refuses anything else with `invalid_effort`.
|
|
9
|
+
*
|
|
10
|
+
* Measured 2026-09-22:
|
|
11
|
+
* - claude: `--effort <level>`; the CLI's own warning lists
|
|
12
|
+
* low, medium, high, xhigh, max.
|
|
13
|
+
* - codex 0.155.1: `exec` has no effort flag. The config key
|
|
14
|
+
* `model_reasoning_effort` is set with `-c`. The value set is the
|
|
15
|
+
* binary's ReasoningEffort enum (minimal, low, medium, high, xhigh, max,
|
|
16
|
+
* ultra); per-model support is in ~/.codex/models_cache.json.
|
|
17
|
+
* - grok 1.0.34: `--reasoning-effort <level>` (alias `--effort`); an unknown
|
|
18
|
+
* value is rejected with "use one of: xhigh, high, medium, low".
|
|
19
|
+
*/
|
|
20
|
+
import type { LaneProvider } from "./types.js";
|
|
21
|
+
export declare const PROVIDER_EFFORT_LEVELS: Record<LaneProvider, readonly string[]>;
|
|
22
|
+
export declare const EFFORT_SOURCES: readonly ["cli", "account", "provider", "default_rule", "none"];
|
|
23
|
+
export type EffortSource = (typeof EFFORT_SOURCES)[number];
|
|
24
|
+
export interface ResolvedEffort {
|
|
25
|
+
effort?: string;
|
|
26
|
+
source: EffortSource;
|
|
27
|
+
}
|
|
28
|
+
/** Validate `--effort` for a provider. Undefined in, undefined out. */
|
|
29
|
+
export declare function parseCliEffort(provider: LaneProvider, raw: string | undefined): string | undefined;
|
|
30
|
+
/** Validate an `effort` field from personal/lanes/config.json. */
|
|
31
|
+
export declare function parseConfigEffort(provider: LaneProvider, raw: unknown, field: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Effort applied when nothing sets one. Owner directive 2026-09-22,
|
|
34
|
+
* verbatim: "make sure opus and fable are used on low by default".
|
|
35
|
+
* Keyed on the model already chosen for the lane; it does not pick a model.
|
|
36
|
+
*/
|
|
37
|
+
export declare function defaultEffortForModel(model: string | undefined): string | undefined;
|
|
38
|
+
/** Precedence: `--effort`, then the account, then the provider, then the default rule. */
|
|
39
|
+
export declare function resolveLaneEffort(input: {
|
|
40
|
+
cliEffort?: string;
|
|
41
|
+
accountEffort?: string;
|
|
42
|
+
providerEffort?: string;
|
|
43
|
+
model?: string;
|
|
44
|
+
}): ResolvedEffort;
|
|
45
|
+
/**
|
|
46
|
+
* Add the provider's effort argument. Codex takes it as a config override,
|
|
47
|
+
* which must precede the `--` that ends its options.
|
|
48
|
+
*/
|
|
49
|
+
export declare function appendEffortFlag(provider: LaneProvider, args: string[], effort: string | undefined): string[];
|
|
50
|
+
//# sourceMappingURL=effort.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effort (reasoning depth) per lane provider.
|
|
3
|
+
*
|
|
4
|
+
* Every provider CLI here tolerates or ignores a bad value in some way:
|
|
5
|
+
* `claude --effort bogus` warns and runs at the default, and
|
|
6
|
+
* `codex -c model_reasoning_effort=bogus` loads without complaint. hq-cli
|
|
7
|
+
* therefore validates the value against each provider's own set before any
|
|
8
|
+
* spawn, and refuses anything else with `invalid_effort`.
|
|
9
|
+
*
|
|
10
|
+
* Measured 2026-09-22:
|
|
11
|
+
* - claude: `--effort <level>`; the CLI's own warning lists
|
|
12
|
+
* low, medium, high, xhigh, max.
|
|
13
|
+
* - codex 0.155.1: `exec` has no effort flag. The config key
|
|
14
|
+
* `model_reasoning_effort` is set with `-c`. The value set is the
|
|
15
|
+
* binary's ReasoningEffort enum (minimal, low, medium, high, xhigh, max,
|
|
16
|
+
* ultra); per-model support is in ~/.codex/models_cache.json.
|
|
17
|
+
* - grok 1.0.34: `--reasoning-effort <level>` (alias `--effort`); an unknown
|
|
18
|
+
* value is rejected with "use one of: xhigh, high, medium, low".
|
|
19
|
+
*/
|
|
20
|
+
import { LanesConfigError, LanesError } from "./errors.js";
|
|
21
|
+
export const PROVIDER_EFFORT_LEVELS = {
|
|
22
|
+
claude: ["low", "medium", "high", "xhigh", "max"],
|
|
23
|
+
codex: ["minimal", "low", "medium", "high", "xhigh", "max", "ultra"],
|
|
24
|
+
grok: ["low", "medium", "high", "xhigh"],
|
|
25
|
+
};
|
|
26
|
+
export const EFFORT_SOURCES = [
|
|
27
|
+
"cli",
|
|
28
|
+
"account",
|
|
29
|
+
"provider",
|
|
30
|
+
"default_rule",
|
|
31
|
+
"none",
|
|
32
|
+
];
|
|
33
|
+
function acceptedList(provider) {
|
|
34
|
+
return PROVIDER_EFFORT_LEVELS[provider].join(", ");
|
|
35
|
+
}
|
|
36
|
+
/** Validate `--effort` for a provider. Undefined in, undefined out. */
|
|
37
|
+
export function parseCliEffort(provider, raw) {
|
|
38
|
+
if (raw === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
const value = raw.trim();
|
|
41
|
+
const accepted = PROVIDER_EFFORT_LEVELS[provider];
|
|
42
|
+
if (!accepted.includes(value)) {
|
|
43
|
+
throw new LanesError("invalid_effort", `Invalid --effort ${JSON.stringify(raw)} for ${provider}. Accepted: ${acceptedList(provider)}`, { provider, value: raw, accepted: [...accepted] });
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
/** Validate an `effort` field from personal/lanes/config.json. */
|
|
48
|
+
export function parseConfigEffort(provider, raw, field) {
|
|
49
|
+
const accepted = PROVIDER_EFFORT_LEVELS[provider];
|
|
50
|
+
if (typeof raw !== "string" || !accepted.includes(raw.trim())) {
|
|
51
|
+
throw new LanesConfigError("invalid_effort", `${field} must be one of ${acceptedList(provider)}`, { field, provider, value: raw, accepted: [...accepted] });
|
|
52
|
+
}
|
|
53
|
+
return raw.trim();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Effort applied when nothing sets one. Owner directive 2026-09-22,
|
|
57
|
+
* verbatim: "make sure opus and fable are used on low by default".
|
|
58
|
+
* Keyed on the model already chosen for the lane; it does not pick a model.
|
|
59
|
+
*/
|
|
60
|
+
export function defaultEffortForModel(model) {
|
|
61
|
+
if (model === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (model.startsWith("claude-opus-") || model.startsWith("claude-fable-")) {
|
|
64
|
+
return "low";
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
/** Precedence: `--effort`, then the account, then the provider, then the default rule. */
|
|
69
|
+
export function resolveLaneEffort(input) {
|
|
70
|
+
if (input.cliEffort !== undefined)
|
|
71
|
+
return { effort: input.cliEffort, source: "cli" };
|
|
72
|
+
if (input.accountEffort !== undefined) {
|
|
73
|
+
return { effort: input.accountEffort, source: "account" };
|
|
74
|
+
}
|
|
75
|
+
if (input.providerEffort !== undefined) {
|
|
76
|
+
return { effort: input.providerEffort, source: "provider" };
|
|
77
|
+
}
|
|
78
|
+
const fallback = defaultEffortForModel(input.model);
|
|
79
|
+
if (fallback !== undefined)
|
|
80
|
+
return { effort: fallback, source: "default_rule" };
|
|
81
|
+
return { source: "none" };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Add the provider's effort argument. Codex takes it as a config override,
|
|
85
|
+
* which must precede the `--` that ends its options.
|
|
86
|
+
*/
|
|
87
|
+
export function appendEffortFlag(provider, args, effort) {
|
|
88
|
+
if (effort === undefined)
|
|
89
|
+
return args;
|
|
90
|
+
const value = parseCliEffort(provider, effort);
|
|
91
|
+
switch (provider) {
|
|
92
|
+
case "claude":
|
|
93
|
+
return [...args, "--effort", value];
|
|
94
|
+
case "grok":
|
|
95
|
+
return [...args, "--reasoning-effort", value];
|
|
96
|
+
case "codex": {
|
|
97
|
+
const flag = ["-c", `model_reasoning_effort="${value}"`];
|
|
98
|
+
const end = args.indexOf("--");
|
|
99
|
+
if (end === -1)
|
|
100
|
+
return [...args, ...flag];
|
|
101
|
+
return [...args.slice(0, end), ...flag, ...args.slice(end)];
|
|
102
|
+
}
|
|
103
|
+
default: {
|
|
104
|
+
const _exhaustive = provider;
|
|
105
|
+
return _exhaustive;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=effort.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distinguishable errors for the lanes library. `error` matches the spec's
|
|
3
|
+
* JSON `error` field so the command layer can print `err.toJSON()` as-is.
|
|
4
|
+
*/
|
|
5
|
+
export declare class LanesError extends Error {
|
|
6
|
+
readonly error: string;
|
|
7
|
+
readonly details: Record<string, unknown>;
|
|
8
|
+
constructor(error: string, message: string, details?: Record<string, unknown>);
|
|
9
|
+
toJSON(): Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export declare function isLanesError(err: unknown): err is LanesError;
|
|
12
|
+
/** Compare-and-set miss. Callers retry after re-reading the record. */
|
|
13
|
+
export declare class LaneVersionConflict extends LanesError {
|
|
14
|
+
readonly expectedVersion: number;
|
|
15
|
+
readonly actualVersion: number;
|
|
16
|
+
readonly recordPath: string;
|
|
17
|
+
constructor(opts: {
|
|
18
|
+
path: string;
|
|
19
|
+
expectedVersion: number;
|
|
20
|
+
actualVersion: number;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export declare function isLaneVersionConflict(err: unknown): err is LaneVersionConflict;
|
|
24
|
+
export declare class LaneNotFoundError extends LanesError {
|
|
25
|
+
constructor(kind: "lane" | "question", id: string);
|
|
26
|
+
}
|
|
27
|
+
export declare class LaneSchemaError extends LanesError {
|
|
28
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
29
|
+
}
|
|
30
|
+
export declare class LanesConfigError extends LanesError {
|
|
31
|
+
constructor(error: string, message: string, details?: Record<string, unknown>);
|
|
32
|
+
}
|
|
33
|
+
export declare class AccountResolutionError extends LanesError {
|
|
34
|
+
constructor(error: "unknown_account" | "account_disabled" | "no_enabled_account" | "account_model_unavailable" | "account_quota_exhausted", message: string, details?: Record<string, unknown>);
|
|
35
|
+
}
|
|
36
|
+
export declare class WorkerResolutionError extends LanesError {
|
|
37
|
+
constructor(error: "worker_not_found" | "worker_tenancy_unresolved" | "worker_not_available" | "worker_not_active" | "worker_id_conflict" | "package_workers_unmounted" | "worker_registry_unreadable", message: string, details?: Record<string, unknown>);
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distinguishable errors for the lanes library. `error` matches the spec's
|
|
3
|
+
* JSON `error` field so the command layer can print `err.toJSON()` as-is.
|
|
4
|
+
*/
|
|
5
|
+
export class LanesError extends Error {
|
|
6
|
+
error;
|
|
7
|
+
details;
|
|
8
|
+
constructor(error, message, details = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "LanesError";
|
|
11
|
+
this.error = error;
|
|
12
|
+
this.details = details;
|
|
13
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
14
|
+
}
|
|
15
|
+
toJSON() {
|
|
16
|
+
return { error: this.error, ...this.details };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function isLanesError(err) {
|
|
20
|
+
return err instanceof LanesError;
|
|
21
|
+
}
|
|
22
|
+
/** Compare-and-set miss. Callers retry after re-reading the record. */
|
|
23
|
+
export class LaneVersionConflict extends LanesError {
|
|
24
|
+
expectedVersion;
|
|
25
|
+
actualVersion;
|
|
26
|
+
recordPath;
|
|
27
|
+
constructor(opts) {
|
|
28
|
+
super("version_conflict", `Lane record version conflict at ${opts.path}: expected ${opts.expectedVersion}, found ${opts.actualVersion}`, {
|
|
29
|
+
path: opts.path,
|
|
30
|
+
expected: opts.expectedVersion,
|
|
31
|
+
actual: opts.actualVersion,
|
|
32
|
+
});
|
|
33
|
+
this.name = "LaneVersionConflict";
|
|
34
|
+
this.expectedVersion = opts.expectedVersion;
|
|
35
|
+
this.actualVersion = opts.actualVersion;
|
|
36
|
+
this.recordPath = opts.path;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function isLaneVersionConflict(err) {
|
|
40
|
+
return err instanceof LaneVersionConflict;
|
|
41
|
+
}
|
|
42
|
+
export class LaneNotFoundError extends LanesError {
|
|
43
|
+
constructor(kind, id) {
|
|
44
|
+
super(kind === "lane" ? "lane_not_found" : "question_not_found", `${kind} ${id} not found`, { [`${kind}_id`]: id });
|
|
45
|
+
this.name = "LaneNotFoundError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export class LaneSchemaError extends LanesError {
|
|
49
|
+
constructor(message, details = {}) {
|
|
50
|
+
super("schema_error", message, details);
|
|
51
|
+
this.name = "LaneSchemaError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export class LanesConfigError extends LanesError {
|
|
55
|
+
constructor(error, message, details = {}) {
|
|
56
|
+
super(error, message, details);
|
|
57
|
+
this.name = "LanesConfigError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export class AccountResolutionError extends LanesError {
|
|
61
|
+
constructor(error, message, details = {}) {
|
|
62
|
+
super(error, message, details);
|
|
63
|
+
this.name = "AccountResolutionError";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class WorkerResolutionError extends LanesError {
|
|
67
|
+
constructor(error, message, details = {}) {
|
|
68
|
+
super(error, message, details);
|
|
69
|
+
this.name = "WorkerResolutionError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=errors.js.map
|