@indigoai-us/hq-cli 5.126.0 → 5.128.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 +218 -0
- 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 +245 -11
- package/dist/command-catalog.generated.js +323 -11
- package/dist/command-registration-plan.d.ts +1 -0
- package/dist/command-registration-plan.js +11 -2
- package/dist/commands/agents.d.ts +12 -0
- package/dist/commands/agents.js +46 -2
- package/dist/commands/lanes.d.ts +55 -16
- package/dist/commands/lanes.js +1187 -250
- package/dist/commands/mesh.js +254 -19
- package/dist/lib/core-utils/sentry-report.d.ts +2 -0
- package/dist/lib/core-utils/sentry-report.js +47 -7
- package/dist/lib/lanes/accounts.d.ts +67 -7
- package/dist/lib/lanes/accounts.js +233 -43
- package/dist/lib/lanes/compaction.d.ts +96 -0
- package/dist/lib/lanes/compaction.js +197 -0
- package/dist/lib/lanes/config.d.ts +15 -0
- package/dist/lib/lanes/config.js +65 -8
- 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 +2 -2
- 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 +35 -5
- package/dist/lib/lanes/index.js +19 -4
- package/dist/lib/lanes/liveness.d.ts +7 -1
- package/dist/lib/lanes/liveness.js +64 -5
- 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 +18 -5
- package/dist/lib/lanes/registry.js +293 -19
- package/dist/lib/lanes/spawn.d.ts +274 -0
- package/dist/lib/lanes/spawn.js +1363 -0
- package/dist/lib/lanes/store.d.ts +7 -3
- package/dist/lib/lanes/store.js +150 -32
- package/dist/lib/lanes/types.d.ts +346 -4
- package/dist/lib/lanes/types.js +138 -5
- 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/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 +1 -1
- package/dist/lib/work-context/state.d.ts +12 -0
- package/dist/lib/work-context/state.js +131 -3
- package/package.json +3 -1
|
@@ -0,0 +1,96 @@
|
|
|
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 * as fs from "node:fs";
|
|
11
|
+
import { type LaneRecord, type LaneState, type QuestionRecord } from "./types.js";
|
|
12
|
+
export declare const DROPBOX_DIR = "dropbox";
|
|
13
|
+
export declare const INBOX_NAME = "inbox.jsonl";
|
|
14
|
+
export declare const DRAINING_DIR = "draining";
|
|
15
|
+
export declare const DELIVERED_DIR = "delivered";
|
|
16
|
+
export declare const INFLIGHT_DIR = "inflight";
|
|
17
|
+
export interface DropboxMessage {
|
|
18
|
+
at?: string;
|
|
19
|
+
message_id?: string;
|
|
20
|
+
kind?: string;
|
|
21
|
+
question_id?: string;
|
|
22
|
+
choice?: string;
|
|
23
|
+
text: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface DrainDeps {
|
|
27
|
+
mkdirSync: typeof fs.mkdirSync;
|
|
28
|
+
renameSync: typeof fs.renameSync;
|
|
29
|
+
readFileSync: typeof fs.readFileSync;
|
|
30
|
+
writeFileSync: typeof fs.writeFileSync;
|
|
31
|
+
unlinkSync: typeof fs.unlinkSync;
|
|
32
|
+
existsSync: typeof fs.existsSync;
|
|
33
|
+
readdirSync: typeof fs.readdirSync;
|
|
34
|
+
rmdirSync: typeof fs.rmdirSync;
|
|
35
|
+
kill: (pid: number, signal?: NodeJS.Signals | 0) => void;
|
|
36
|
+
pid: number;
|
|
37
|
+
unique: () => string;
|
|
38
|
+
readStarttime?: (pid: number) => number | null;
|
|
39
|
+
/** Test seam: after observing inbox bytes, before the consume rename. */
|
|
40
|
+
afterObserveInbox?: () => void;
|
|
41
|
+
/** Test seam: runs after this process has claimed and read its bytes. */
|
|
42
|
+
afterRead?: () => void;
|
|
43
|
+
}
|
|
44
|
+
export declare function mintClaimId(): string;
|
|
45
|
+
export declare function assertSafeLaneId(laneId: string): void;
|
|
46
|
+
export declare function dropboxDir(hqRoot: string, laneId: string): string;
|
|
47
|
+
export declare function dropboxInboxPath(hqRoot: string, laneId: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Worker tool-call liveness (watcher.ts reads it). A Claude worker run with
|
|
50
|
+
* `--output-format json` writes nothing to worker.out until it exits, so the
|
|
51
|
+
* only thing it does observably while working is call tools. Every tool call
|
|
52
|
+
* runs this drain hook, and the hook records the call here before it touches
|
|
53
|
+
* the drop box. The file's mtime is the signal; the body is for humans.
|
|
54
|
+
*/
|
|
55
|
+
export declare const TOOL_ACTIVITY_NAME = "tool-activity.json";
|
|
56
|
+
export declare function toolActivityPath(hqRoot: string, laneId: string): string;
|
|
57
|
+
export interface ToolActivityRecord {
|
|
58
|
+
at: string;
|
|
59
|
+
hook_pid: number;
|
|
60
|
+
hook_event: "PreToolUse";
|
|
61
|
+
}
|
|
62
|
+
export declare function recordToolActivity(hqRoot: string, laneId: string, at?: string): ToolActivityRecord;
|
|
63
|
+
export declare function readToolActivity(hqRoot: string, laneId: string): ToolActivityRecord | null;
|
|
64
|
+
export declare function appendDropboxMessage(hqRoot: string, laneId: string, message: DropboxMessage): DropboxMessage;
|
|
65
|
+
/**
|
|
66
|
+
* Drain this lane's inbox exactly once. Concurrent callers: one rename wins;
|
|
67
|
+
* the loser sees ENOENT and returns whatever it separately claimed (usually
|
|
68
|
+
* nothing). Leftovers in draining/ and dead inflight dirs are recovered.
|
|
69
|
+
*/
|
|
70
|
+
export declare function drainDropbox(hqRoot: string, laneId: string, depsInput?: Partial<DrainDeps>): DropboxMessage[];
|
|
71
|
+
export declare function formatOperatorInstruction(messages: DropboxMessage[]): string;
|
|
72
|
+
export declare function hookAdditionalContextJson(messages: DropboxMessage[]): string;
|
|
73
|
+
export interface DrainHookResult {
|
|
74
|
+
exitCode: number;
|
|
75
|
+
stdout: string;
|
|
76
|
+
stderr: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Hook protocol. HQ_LANE_ID absent or empty → exit 0, empty stdout, empty stderr.
|
|
80
|
+
* That no-op is load-bearing: this hook is registered in the grok home and runs
|
|
81
|
+
* on every session on the box.
|
|
82
|
+
*/
|
|
83
|
+
export declare function runDrainHook(env: NodeJS.ProcessEnv): DrainHookResult;
|
|
84
|
+
/** Spec §6.5: only a still-running lane gets drop-box delivery. */
|
|
85
|
+
export declare function laneAcceptsDropboxDelivery(lane: Pick<LaneRecord, "state">): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Operator `hq lanes message` delivery. Queued is accepted because the
|
|
88
|
+
* worker that will spawn drains the inbox on its first tool event.
|
|
89
|
+
* `laneAcceptsDropboxDelivery` refuses queued, so it is the wrong guard here.
|
|
90
|
+
*/
|
|
91
|
+
export declare function laneAcceptsOperatorMessage(lane: Pick<LaneRecord, "state">): boolean;
|
|
92
|
+
export declare function operatorMessageRefusalReason(state: LaneState): string;
|
|
93
|
+
/** Delivery target is the lane_id on the question record. Never a project/story lookup. */
|
|
94
|
+
export declare function laneIdForAnswerDelivery(question: Pick<QuestionRecord, "lane_id">): string;
|
|
95
|
+
export declare function answerDropboxMessage(question: Pick<QuestionRecord, "question_id" | "lane_id">, choice: string, at: string, messageId: string): DropboxMessage;
|
|
96
|
+
//# sourceMappingURL=dropbox.d.ts.map
|
|
@@ -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
|
|
@@ -31,9 +31,9 @@ export declare class LanesConfigError extends LanesError {
|
|
|
31
31
|
constructor(error: string, message: string, details?: Record<string, unknown>);
|
|
32
32
|
}
|
|
33
33
|
export declare class AccountResolutionError extends LanesError {
|
|
34
|
-
constructor(error: "unknown_account" | "account_disabled" | "no_enabled_account", message: string, details?: Record<string, unknown>);
|
|
34
|
+
constructor(error: "unknown_account" | "account_disabled" | "no_enabled_account" | "account_model_unavailable" | "account_quota_exhausted", message: string, details?: Record<string, unknown>);
|
|
35
35
|
}
|
|
36
36
|
export declare class WorkerResolutionError extends LanesError {
|
|
37
|
-
constructor(error: "worker_not_found" | "worker_tenancy_unresolved" | "worker_not_available" | "worker_not_active" | "
|
|
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
38
|
}
|
|
39
39
|
//# sourceMappingURL=errors.d.ts.map
|