@indigoai-us/hq-cli 5.125.0 → 5.127.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +329 -0
  2. package/assets/bot-workers/setup/skills/joining-a-team.md +125 -0
  3. package/assets/bot-workers/setup/skills/your-business.md +270 -0
  4. package/assets/bot-workers/setup/worker.yaml +189 -58
  5. package/assets/lanes-hooks/hq-lanes-drain.json +15 -0
  6. package/assets/lanes-hooks/hq-lanes-drain.mjs +254 -0
  7. package/assets/lanes-hooks/hq-lanes-drain.sh +21 -0
  8. package/assets/lanes-reconcile/hq-lanes-reconcile.service +11 -0
  9. package/assets/lanes-reconcile/hq-lanes-reconcile.timer +12 -0
  10. package/dist/command-catalog.generated.d.ts +643 -6
  11. package/dist/command-catalog.generated.js +836 -6
  12. package/dist/command-registration-plan.d.ts +7 -0
  13. package/dist/command-registration-plan.js +11 -1
  14. package/dist/commands/agents.d.ts +12 -0
  15. package/dist/commands/agents.js +46 -2
  16. package/dist/commands/bot.d.ts +10 -0
  17. package/dist/commands/bot.js +6 -0
  18. package/dist/commands/lanes.d.ts +114 -0
  19. package/dist/commands/lanes.js +2969 -0
  20. package/dist/commands/mesh.js +287 -24
  21. package/dist/commands/onboard-create-identity.d.ts +26 -0
  22. package/dist/commands/onboard-create-identity.js +23 -0
  23. package/dist/commands/onboard.js +13 -4
  24. package/dist/lib/bot/progress.d.ts +6 -0
  25. package/dist/lib/bot/progress.js +16 -0
  26. package/dist/lib/bot/runtime/grok.d.ts +30 -7
  27. package/dist/lib/bot/runtime/grok.js +86 -8
  28. package/dist/lib/bot/runtime/index.js +15 -8
  29. package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
  30. package/dist/lib/bot/runtime/messages-stream.js +49 -3
  31. package/dist/lib/lanes/accounts.d.ts +103 -0
  32. package/dist/lib/lanes/accounts.js +318 -0
  33. package/dist/lib/lanes/compaction.d.ts +96 -0
  34. package/dist/lib/lanes/compaction.js +197 -0
  35. package/dist/lib/lanes/config.d.ts +71 -0
  36. package/dist/lib/lanes/config.js +408 -0
  37. package/dist/lib/lanes/delivery.d.ts +151 -0
  38. package/dist/lib/lanes/delivery.js +354 -0
  39. package/dist/lib/lanes/dropbox.d.ts +96 -0
  40. package/dist/lib/lanes/dropbox.js +408 -0
  41. package/dist/lib/lanes/effort.d.ts +50 -0
  42. package/dist/lib/lanes/effort.js +109 -0
  43. package/dist/lib/lanes/errors.d.ts +39 -0
  44. package/dist/lib/lanes/errors.js +72 -0
  45. package/dist/lib/lanes/hooks-install.d.ts +81 -0
  46. package/dist/lib/lanes/hooks-install.js +326 -0
  47. package/dist/lib/lanes/hq-root.d.ts +19 -0
  48. package/dist/lib/lanes/hq-root.js +28 -0
  49. package/dist/lib/lanes/index.d.ts +49 -0
  50. package/dist/lib/lanes/index.js +27 -0
  51. package/dist/lib/lanes/liveness.d.ts +95 -0
  52. package/dist/lib/lanes/liveness.js +332 -0
  53. package/dist/lib/lanes/machine-identity.d.ts +34 -0
  54. package/dist/lib/lanes/machine-identity.js +92 -0
  55. package/dist/lib/lanes/mesh-events.d.ts +109 -0
  56. package/dist/lib/lanes/mesh-events.js +275 -0
  57. package/dist/lib/lanes/outpost-dispatch.d.ts +65 -0
  58. package/dist/lib/lanes/outpost-dispatch.js +90 -0
  59. package/dist/lib/lanes/questions.d.ts +75 -0
  60. package/dist/lib/lanes/questions.js +243 -0
  61. package/dist/lib/lanes/quota.d.ts +95 -0
  62. package/dist/lib/lanes/quota.js +320 -0
  63. package/dist/lib/lanes/reconcile-install.d.ts +69 -0
  64. package/dist/lib/lanes/reconcile-install.js +221 -0
  65. package/dist/lib/lanes/reconcile.d.ts +102 -0
  66. package/dist/lib/lanes/reconcile.js +985 -0
  67. package/dist/lib/lanes/registry.d.ts +116 -0
  68. package/dist/lib/lanes/registry.js +521 -0
  69. package/dist/lib/lanes/spawn.d.ts +274 -0
  70. package/dist/lib/lanes/spawn.js +1363 -0
  71. package/dist/lib/lanes/store.d.ts +68 -0
  72. package/dist/lib/lanes/store.js +745 -0
  73. package/dist/lib/lanes/throttle.d.ts +89 -0
  74. package/dist/lib/lanes/throttle.js +297 -0
  75. package/dist/lib/lanes/types.d.ts +535 -0
  76. package/dist/lib/lanes/types.js +350 -0
  77. package/dist/lib/lanes/watcher-main.d.ts +9 -0
  78. package/dist/lib/lanes/watcher-main.js +22 -0
  79. package/dist/lib/lanes/watcher.d.ts +216 -0
  80. package/dist/lib/lanes/watcher.js +859 -0
  81. package/dist/lib/mesh/api.d.ts +71 -7
  82. package/dist/lib/mesh/api.js +167 -28
  83. package/dist/lib/mesh/live/backfill-held.d.ts +18 -7
  84. package/dist/lib/mesh/live/backfill-held.js +66 -21
  85. package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
  86. package/dist/lib/mesh/live/daemon/config.js +75 -0
  87. package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
  88. package/dist/lib/mesh/live/daemon/index.js +3 -3
  89. package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
  90. package/dist/lib/mesh/live/daemon/install.js +444 -2
  91. package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
  92. package/dist/lib/mesh/live/daemon/paths.js +2 -0
  93. package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
  94. package/dist/lib/mesh/live/daemon/run.js +111 -54
  95. package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
  96. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +3 -0
  97. package/dist/lib/mesh/live/daemon/transcript-watch.js +31 -8
  98. package/dist/lib/scan-packages/index.d.ts +2 -0
  99. package/dist/lib/scan-packages/index.js +18 -13
  100. package/dist/lib/work-context/config.d.ts +20 -0
  101. package/dist/lib/work-context/config.js +26 -5
  102. package/dist/lib/work-context/reconcile.js +28 -7
  103. package/dist/lib/work-context/state.d.ts +12 -0
  104. package/dist/lib/work-context/state.js +131 -3
  105. package/dist/lib/work-context/windows-acl.d.ts +123 -0
  106. package/dist/lib/work-context/windows-acl.js +425 -0
  107. package/dist/utils/pack-contributions.js +5 -4
  108. package/package.json +3 -1
@@ -0,0 +1,354 @@
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 * as fs from "node:fs";
21
+ import * as path from "node:path";
22
+ import { LanesError } from "./errors.js";
23
+ import { parseLaneProvider, } from "./types.js";
24
+ /** Which providers can continue an exited worker from a recorded session id. */
25
+ export const PROVIDER_SESSION_RESUME = {
26
+ claude: true,
27
+ codex: true,
28
+ grok: false,
29
+ };
30
+ export function providerSupportsSessionResume(provider) {
31
+ return PROVIDER_SESSION_RESUME[parseLaneProvider(provider)];
32
+ }
33
+ /**
34
+ * States an operator message may resume from, given a session id.
35
+ *
36
+ * `done`: the worker finished its brief, wrote a done envelope and exited on
37
+ * its own. Nothing about the lane says the operator is finished with it; a
38
+ * follow-up is the normal next step, and the session is intact.
39
+ *
40
+ * `blocked`: the worker exited after writing a blocked envelope that names
41
+ * what it needs. An operator message is usually that missing input. The state
42
+ * is not terminal (spec §6.7) and nothing stopped the lane deliberately.
43
+ *
44
+ * Refused by `hq lanes message`:
45
+ * `awaiting_input` has its own path, `hq lanes questions answer`, and the
46
+ * reconcile pass resumes it once every question is answered. `killed` and
47
+ * `cancelled` were stopped on purpose by the operator; a message must not
48
+ * silently restart them. `failed` means the run ended without an envelope or
49
+ * never started; the operator has not seen why. `interrupted` exited with
50
+ * partial output and no envelope, so the lane's state was never closed
51
+ * cleanly; only the explicit `hq lanes resume` opt-in continues it
52
+ * (RESUME_COMMAND_STATES). `queued` and `running` take the drop box, not a
53
+ * resume.
54
+ */
55
+ export const RESUMABLE_LANE_STATES = ["done", "blocked"];
56
+ export function isResumableLaneState(state) {
57
+ return RESUMABLE_LANE_STATES.includes(state);
58
+ }
59
+ /**
60
+ * States `hq lanes resume` continues from. `interrupted` is the one a plain
61
+ * message refuses: the run ended without closing its state, and continuing
62
+ * it is a decision the operator makes by name. `done` and `blocked` are here
63
+ * so the one command covers every exited-with-a-session case.
64
+ */
65
+ export const RESUME_COMMAND_STATES = ["interrupted", "done", "blocked"];
66
+ export function isResumeCommandState(state) {
67
+ return RESUME_COMMAND_STATES.includes(state);
68
+ }
69
+ export const LANE_DELIVERY_MECHANISMS = ["dropbox", "resume", "none"];
70
+ /**
71
+ * `dropbox`: a process is (or is about to be) alive to drain the inbox.
72
+ * `resume`: the process has exited, the provider can resume, the state allows
73
+ * it, and a session id was recorded from the worker's own result.
74
+ * `none`: nothing can deliver; the caller must refuse and write nothing.
75
+ */
76
+ export function laneDeliveryMechanism(lane) {
77
+ if (lane.state === "queued" || lane.state === "running")
78
+ return "dropbox";
79
+ if (providerSupportsSessionResume(lane.provider) &&
80
+ isResumableLaneState(lane.state) &&
81
+ hasRecordedSessionId(lane)) {
82
+ return "resume";
83
+ }
84
+ return "none";
85
+ }
86
+ export function hasRecordedSessionId(lane) {
87
+ return (typeof lane.provider_session_id === "string" &&
88
+ lane.provider_session_id.trim().length > 0);
89
+ }
90
+ export function deliveryRefusalReason(lane) {
91
+ const state = lane.state;
92
+ switch (state) {
93
+ case "queued":
94
+ case "running":
95
+ return "";
96
+ case "awaiting_input":
97
+ return "Lane is awaiting_input. The worker has exited after writing an ask envelope. Use hq lanes questions answer <question-id>.";
98
+ case "done":
99
+ case "blocked": {
100
+ if (!providerSupportsSessionResume(lane.provider)) {
101
+ return `Lane is ${state}. Provider ${lane.provider} cannot resume an exited worker; there is no delivery path.`;
102
+ }
103
+ if (!hasRecordedSessionId(lane)) {
104
+ return `Lane is ${state}. No provider session id was recorded from the worker's result, so the session cannot be resumed.`;
105
+ }
106
+ return "";
107
+ }
108
+ case "killed":
109
+ case "cancelled":
110
+ return `Lane is ${state}. The operator stopped it on purpose; a message does not restart it. Resuming from ${state} needs an explicit opt-in, which does not exist yet.`;
111
+ case "interrupted":
112
+ return `Lane is ${state}. The run did not close cleanly and the operator has not decided what to do with it. Resuming from ${state} needs an explicit opt-in: hq lanes resume <lane-id> [--text <message>].`;
113
+ case "failed":
114
+ return `Lane is ${state}. The run did not close cleanly and the operator has not decided what to do with it. Resuming from ${state} needs an explicit opt-in, which does not exist yet.`;
115
+ default: {
116
+ const _exhaustive = state;
117
+ return _exhaustive;
118
+ }
119
+ }
120
+ }
121
+ /** Reasons a resumable provider's exit recorded no session id. Never a fabricated value. */
122
+ export const SESSION_ID_ABSENT_REASONS = [
123
+ "worker_out_missing",
124
+ "no_result_json",
125
+ "no_session_id_field",
126
+ "no_thread_started_event",
127
+ ];
128
+ function nonNegativeInt(value) {
129
+ return typeof value === "number" && Number.isInteger(value) && value >= 0
130
+ ? value
131
+ : null;
132
+ }
133
+ /**
134
+ * Parse the `claude -p --output-format json` result. stdout is appended
135
+ * across runs of the same lane, so the last `type: "result"` object is this
136
+ * run's. Only `session_id` and the two cache counters are read out; the
137
+ * prompt, the response body and everything else in the payload stay on disk.
138
+ */
139
+ export function parseClaudeResultOutput(text) {
140
+ const lines = text.split("\n");
141
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
142
+ const line = (lines[i] ?? "").trim();
143
+ if (!line.startsWith("{"))
144
+ continue;
145
+ let parsed;
146
+ try {
147
+ parsed = JSON.parse(line);
148
+ }
149
+ catch {
150
+ continue;
151
+ }
152
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
153
+ continue;
154
+ const rec = parsed;
155
+ if (rec.type !== "result")
156
+ continue;
157
+ const usage = rec.usage && typeof rec.usage === "object" && !Array.isArray(rec.usage)
158
+ ? rec.usage
159
+ : {};
160
+ const sessionId = typeof rec.session_id === "string" && rec.session_id.trim().length > 0
161
+ ? rec.session_id.trim()
162
+ : null;
163
+ return {
164
+ session_id: sessionId,
165
+ session_id_absent: sessionId === null ? "no_session_id_field" : null,
166
+ cache_read_input_tokens: nonNegativeInt(usage.cache_read_input_tokens),
167
+ cache_creation_input_tokens: nonNegativeInt(usage.cache_creation_input_tokens),
168
+ };
169
+ }
170
+ return {
171
+ session_id: null,
172
+ session_id_absent: "no_result_json",
173
+ cache_read_input_tokens: null,
174
+ cache_creation_input_tokens: null,
175
+ };
176
+ }
177
+ /**
178
+ * Parse the `codex exec --json` event stream. The first event of a run is
179
+ * `{"type":"thread.started","thread_id":"<uuid>"}`; that id is what
180
+ * `codex exec resume <id>` takes, and a resumed run emits the same id again.
181
+ * Usage comes from the last `turn.completed` event: `cached_input_tokens`
182
+ * is the cache read and `cache_write_input_tokens` the cache creation.
183
+ * Nothing else on the stream is read out.
184
+ */
185
+ export function parseCodexEventOutput(text) {
186
+ let threadId = null;
187
+ let usage = {};
188
+ let sawEvent = false;
189
+ for (const raw of text.split("\n")) {
190
+ const line = raw.trim();
191
+ if (!line.startsWith("{"))
192
+ continue;
193
+ let parsed;
194
+ try {
195
+ parsed = JSON.parse(line);
196
+ }
197
+ catch {
198
+ continue;
199
+ }
200
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
201
+ continue;
202
+ const rec = parsed;
203
+ if (typeof rec.type !== "string")
204
+ continue;
205
+ sawEvent = true;
206
+ if (rec.type === "thread.started" && threadId === null) {
207
+ threadId =
208
+ typeof rec.thread_id === "string" && rec.thread_id.trim().length > 0
209
+ ? rec.thread_id.trim()
210
+ : null;
211
+ }
212
+ else if (rec.type === "turn.completed") {
213
+ usage =
214
+ rec.usage && typeof rec.usage === "object" && !Array.isArray(rec.usage)
215
+ ? rec.usage
216
+ : {};
217
+ }
218
+ }
219
+ return {
220
+ session_id: threadId,
221
+ session_id_absent: threadId !== null ? null : sawEvent ? "no_thread_started_event" : "no_result_json",
222
+ cache_read_input_tokens: nonNegativeInt(usage.cached_input_tokens),
223
+ cache_creation_input_tokens: nonNegativeInt(usage.cache_write_input_tokens),
224
+ };
225
+ }
226
+ /** Per-provider stdout parser; null for a provider whose exit carries nothing to record. */
227
+ export function parseWorkerRunOutput(provider, text) {
228
+ switch (parseLaneProvider(provider)) {
229
+ case "claude":
230
+ return parseClaudeResultOutput(text);
231
+ case "codex":
232
+ return parseCodexEventOutput(text);
233
+ case "grok":
234
+ return null;
235
+ default:
236
+ return null;
237
+ }
238
+ }
239
+ /**
240
+ * Read this run's result from the worker's own stdout. stdout is opened in
241
+ * append mode across runs, so only bytes past `workerOutOffset` (recorded in
242
+ * spawn.json at launch) are this run's. Returns null for a provider whose
243
+ * exit carries nothing to record, so those lanes keep behaving exactly as
244
+ * before.
245
+ */
246
+ export function readWorkerRunResult(runDir, provider,
247
+ /** Bytes of worker.out that belong to earlier runs; parsing starts after them. */
248
+ workerOutOffset = 0) {
249
+ if (!providerSupportsSessionResume(provider))
250
+ return null;
251
+ const outPath = path.join(runDir, "logs", "worker.out");
252
+ let text;
253
+ try {
254
+ const bytes = fs.readFileSync(outPath);
255
+ text = bytes.subarray(Math.min(workerOutOffset, bytes.length)).toString("utf8");
256
+ }
257
+ catch (err) {
258
+ const code = err.code;
259
+ if (code === "ENOENT") {
260
+ return {
261
+ session_id: null,
262
+ session_id_absent: "worker_out_missing",
263
+ cache_read_input_tokens: null,
264
+ cache_creation_input_tokens: null,
265
+ };
266
+ }
267
+ process.stderr.write(`hq lanes: failed to read ${outPath}: ${err instanceof Error ? err.message : String(err)}\n`);
268
+ throw err;
269
+ }
270
+ return parseWorkerRunOutput(provider, text);
271
+ }
272
+ export const LAUNCH_TRIGGERS = ["admit", "answered_questions", "message", "resume"];
273
+ /**
274
+ * Refusal for `hq lanes resume`, in precedence order: wrong state, provider
275
+ * without a resume path, no recorded session. Null when the resume can go.
276
+ */
277
+ export function resumeCommandRefusal(lane) {
278
+ if (!isResumeCommandState(lane.state)) {
279
+ return {
280
+ error: "lane_not_resumable",
281
+ message: `Lane is ${lane.state}. hq lanes resume continues a lane in ${RESUME_COMMAND_STATES.join(", ")}; ${lane.state === "awaiting_input"
282
+ ? "use hq lanes questions answer <question-id>."
283
+ : lane.state === "queued" || lane.state === "running"
284
+ ? "use hq lanes message for a live lane."
285
+ : "a lane in this state is not resumed."}`,
286
+ };
287
+ }
288
+ if (!providerSupportsSessionResume(lane.provider)) {
289
+ return {
290
+ error: "resume_unsupported",
291
+ message: `Lane is ${lane.state}. Provider ${lane.provider} cannot resume an exited worker; there is no delivery path.`,
292
+ };
293
+ }
294
+ if (!hasRecordedSessionId(lane)) {
295
+ return {
296
+ error: "no_provider_session",
297
+ message: `Lane is ${lane.state}. No provider session id was recorded from the worker's result, so the session cannot be resumed.`,
298
+ };
299
+ }
300
+ return null;
301
+ }
302
+ /**
303
+ * Decide fresh-vs-resume for a spawn. Admission always starts fresh with the
304
+ * brief. Answered questions resume the recorded session when the provider
305
+ * can, else start fresh with the answers file (today's behaviour). An operator
306
+ * message can only be a resume, and only when `laneDeliveryMechanism` says so.
307
+ * `hq lanes resume` is also always a resume, gated by `resumeCommandRefusal`.
308
+ */
309
+ export function planLaneLaunch(lane, trigger) {
310
+ switch (trigger.kind) {
311
+ case "admit":
312
+ return { mode: "fresh" };
313
+ case "answered_questions":
314
+ if (providerSupportsSessionResume(lane.provider) && hasRecordedSessionId(lane)) {
315
+ return { mode: "resume", session_id: lane.provider_session_id };
316
+ }
317
+ return { mode: "fresh" };
318
+ case "message": {
319
+ if (laneDeliveryMechanism(lane) !== "resume" || !hasRecordedSessionId(lane)) {
320
+ throw new LanesError("lane_not_accepting_messages", deliveryRefusalReason(lane) || `Lane is ${lane.state} and cannot be resumed`, { state: lane.state, provider: lane.provider });
321
+ }
322
+ return { mode: "resume", session_id: lane.provider_session_id };
323
+ }
324
+ case "resume": {
325
+ const refusal = resumeCommandRefusal(lane);
326
+ if (refusal !== null || !hasRecordedSessionId(lane)) {
327
+ throw new LanesError(refusal?.error ?? "no_provider_session", refusal?.message ?? `Lane is ${lane.state} and has no recorded provider session`, { state: lane.state, provider: lane.provider });
328
+ }
329
+ return { mode: "resume", session_id: lane.provider_session_id };
330
+ }
331
+ default: {
332
+ const _exhaustive = trigger;
333
+ return _exhaustive;
334
+ }
335
+ }
336
+ }
337
+ /** Lane states a launch may claim `running` from, per trigger. */
338
+ export function claimableStatesFor(trigger) {
339
+ switch (trigger.kind) {
340
+ case "admit":
341
+ return ["queued"];
342
+ case "answered_questions":
343
+ return ["awaiting_input"];
344
+ case "message":
345
+ return RESUMABLE_LANE_STATES;
346
+ case "resume":
347
+ return RESUME_COMMAND_STATES;
348
+ default: {
349
+ const _exhaustive = trigger;
350
+ return _exhaustive;
351
+ }
352
+ }
353
+ }
354
+ //# sourceMappingURL=delivery.js.map
@@ -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