@indigoai-us/hq-cli 5.345.160 → 5.345.162
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 +36 -0
- package/assets/scaffold/core/scripts/session-journal.sh +116 -24
- package/dist/command-catalog.generated.d.ts +262 -5
- package/dist/command-catalog.generated.js +322 -5
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-inbox.js +6 -2
- package/dist/commands/auth.js +6 -5
- package/dist/commands/core-session-helper-runtime.d.ts +1 -0
- package/dist/commands/core-session-journal-native.d.ts +1 -0
- package/dist/commands/core-session-journal-native.js +158 -34
- package/dist/commands/feedback.d.ts +12 -0
- package/dist/commands/feedback.js +52 -0
- package/dist/commands/files-browse.d.ts +8 -0
- package/dist/commands/files-browse.js +83 -1
- package/dist/commands/install-global.d.ts +2 -0
- package/dist/commands/install-global.js +24 -1
- package/dist/commands/integrations-connect.js +1 -0
- package/dist/commands/integrations-manage.d.ts +2 -0
- package/dist/commands/integrations-manage.js +6 -4
- package/dist/commands/lanes-link.js +5 -3
- package/dist/commands/lanes-loop.js +7 -7
- package/dist/commands/lanes-questions.js +54 -4
- package/dist/commands/lanes-session-start.d.ts +12 -0
- package/dist/commands/lanes-session-start.js +93 -12
- package/dist/commands/lanes.js +2 -1
- package/dist/commands/meetings.d.ts +11 -0
- package/dist/commands/meetings.js +41 -11
- package/dist/commands/sync-held.d.ts +87 -0
- package/dist/commands/sync-held.js +191 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3830 -2812
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +21 -0
- package/dist/lanes-session-start-argv.d.ts +7 -0
- package/dist/lanes-session-start-argv.js +9 -0
- package/dist/lib/agent-kit/inbox-state.d.ts +5 -4
- package/dist/lib/agent-kit/inbox-state.js +24 -3
- package/dist/lib/bot/log.d.ts +3 -2
- package/dist/lib/bot/progress.d.ts +23 -1
- package/dist/lib/bot/progress.js +43 -5
- package/dist/lib/bot/run.js +2 -0
- package/dist/lib/bot/runtime/claude.d.ts +12 -0
- package/dist/lib/bot/runtime/claude.js +20 -0
- package/dist/lib/bot/runtime/index.d.ts +10 -1
- package/dist/lib/bot/runtime/index.js +14 -2
- package/dist/lib/bot/runtime/types.d.ts +8 -0
- package/dist/lib/doctor/checks/ps1-shims.d.ts +21 -0
- package/dist/lib/doctor/checks/ps1-shims.js +82 -0
- package/dist/lib/doctor/fix/apply.d.ts +13 -1
- package/dist/lib/doctor/fix/apply.js +38 -3
- package/dist/lib/doctor/fix/remediation.d.ts +9 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/doctor/registry.js +2 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/links.d.ts +2 -1
- package/dist/lib/lanes/links.js +235 -17
- package/dist/lib/lanes/loop.d.ts +17 -0
- package/dist/lib/lanes/loop.js +235 -5
- package/dist/lib/lanes/questions.d.ts +1 -0
- package/dist/lib/lanes/registry-flag.d.ts +24 -0
- package/dist/lib/lanes/registry-flag.js +72 -6
- package/dist/lib/lanes/store.d.ts +4 -2
- package/dist/lib/lanes/store.js +8 -8
- package/dist/lib/windows/ps1-shims.d.ts +60 -0
- package/dist/lib/windows/ps1-shims.js +152 -0
- package/dist/utils/self-update.d.ts +2 -0
- package/dist/utils/self-update.js +8 -0
- package/dist/utils/staged-npm-install.js +13 -2
- package/dist/utils/token-keychain.d.ts +11 -8
- package/dist/utils/token-keychain.js +36 -15
- package/package.json +8 -8
- package/scripts/install-hook-engine.mjs +12 -0
|
@@ -20,6 +20,8 @@ export interface LinkOptions {
|
|
|
20
20
|
timeout?: number;
|
|
21
21
|
event?: string;
|
|
22
22
|
input?: string;
|
|
23
|
+
ackMode?: string;
|
|
24
|
+
claims?: string;
|
|
23
25
|
now?: () => Date;
|
|
24
26
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
25
27
|
}
|
|
@@ -31,7 +33,6 @@ export declare function resolveLinkSessionId(root: string, explicit: string | un
|
|
|
31
33
|
/** Cheap hook guard used before remote flag lookup or queue access. */
|
|
32
34
|
export declare function linkedSessionIdFromHookInput(input: string): string;
|
|
33
35
|
export declare function isLinkedSession(root: string, sid: string): boolean;
|
|
34
|
-
/** Claims pending files by renaming the whole queue, matching conduct-inbox's no-loss drain. */
|
|
35
36
|
export declare function drainLinkQueue(inbox: string): string;
|
|
36
37
|
export declare function runLaneLink(subcommand: string, options: LinkOptions): Promise<LinkResult>;
|
|
37
38
|
/** Apply the default-on feature gate contract at the command boundary. */
|
package/dist/lib/lanes/links.js
CHANGED
|
@@ -2,6 +2,8 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { appendDropboxMessageAtPath } from "./dropbox.js";
|
|
5
|
+
import { withLaneRecordFileLock } from "./store.js";
|
|
6
|
+
import { LanesError } from "./errors.js";
|
|
5
7
|
export const LANE_SESSION_LINKS_FLAG = "lanes.session-links";
|
|
6
8
|
const ID_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
7
9
|
export class LaneLinkError extends Error {
|
|
@@ -132,6 +134,17 @@ function resolvePointer(root, sid, role, options) {
|
|
|
132
134
|
function queueDir(root, link, child) {
|
|
133
135
|
return path.join(rootDir(root), link, child ? "children" : "up", ...(child ? [child] : []), "inbox");
|
|
134
136
|
}
|
|
137
|
+
function withLinkQueueLock(inbox, fn) {
|
|
138
|
+
try {
|
|
139
|
+
return withLaneRecordFileLock(path.join(inbox, "queue"), fn, { reclaimByAge: false });
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error instanceof LanesError && error.error === "store_lock_timeout") {
|
|
143
|
+
throw new LaneLinkError("queue_busy", "Conductor queue busy; instructions retained");
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
135
148
|
function sendQueue(root, link, child, text) {
|
|
136
149
|
const inbox = queueDir(root, link, child);
|
|
137
150
|
const pending = path.join(inbox, "pending");
|
|
@@ -139,10 +152,12 @@ function sendQueue(root, link, child, text) {
|
|
|
139
152
|
// Reuse hq monitor's session-inbox append primitive; the pending directory is
|
|
140
153
|
// still claimed as a whole below, keeping conduct-inbox's no-loss handoff.
|
|
141
154
|
const cleanText = text.replaceAll("\0", "").replaceAll("\u000b", "").replaceAll("\u000c", "").replaceAll("\r", "");
|
|
142
|
-
|
|
155
|
+
withLinkQueueLock(inbox, () => {
|
|
156
|
+
appendDropboxMessageAtPath(pending, { at: new Date().toISOString(), text: cleanText });
|
|
157
|
+
});
|
|
143
158
|
}
|
|
144
159
|
/** Claims pending files by renaming the whole queue, matching conduct-inbox's no-loss drain. */
|
|
145
|
-
|
|
160
|
+
function drainLinkQueueUnlocked(inbox) {
|
|
146
161
|
const pending = path.join(inbox, "pending");
|
|
147
162
|
const claimedRoot = path.join(inbox, "claimed");
|
|
148
163
|
// Recover claims interrupted before the delivery result was committed. This is
|
|
@@ -190,16 +205,197 @@ export function drainLinkQueue(inbox) {
|
|
|
190
205
|
fs.writeFileSync(path.join(claim, ".consumed"), "delivered\n", { mode: 0o600 });
|
|
191
206
|
return messages;
|
|
192
207
|
}
|
|
208
|
+
export function drainLinkQueue(inbox) {
|
|
209
|
+
return withLinkQueueLock(inbox, () => drainLinkQueueUnlocked(inbox));
|
|
210
|
+
}
|
|
211
|
+
/** Claims remain replayable until the recipient explicitly accepts instructions.
|
|
212
|
+
* This is at-least-once transport, not proof of a native turn or execution.
|
|
213
|
+
*/
|
|
214
|
+
function claimLinkQueueUnlocked(inbox) {
|
|
215
|
+
const pending = path.join(inbox, "pending");
|
|
216
|
+
const claimed = path.join(inbox, "unaccepted");
|
|
217
|
+
fs.mkdirSync(claimed, { recursive: true });
|
|
218
|
+
if (fs.existsSync(path.join(pending, "inbox.jsonl"))) {
|
|
219
|
+
// Allocate while holding the queue lock. Wall time alone can repeat or
|
|
220
|
+
// move backward, and pid/random suffixes do not preserve claim order.
|
|
221
|
+
let serial = 0n;
|
|
222
|
+
for (const previous of fs.readdirSync(claimed)) {
|
|
223
|
+
const prefix = /^(\d{1,16})-/.exec(previous)?.[1];
|
|
224
|
+
if (prefix && BigInt(prefix) > serial)
|
|
225
|
+
serial = BigInt(prefix);
|
|
226
|
+
}
|
|
227
|
+
const wall = Date.now();
|
|
228
|
+
if (!Number.isSafeInteger(wall) || wall < 0)
|
|
229
|
+
throw new LaneLinkError("claim_clock_invalid", "Cannot allocate a safe receipt serial");
|
|
230
|
+
serial = BigInt(wall) > serial ? BigInt(wall) : serial + 1n;
|
|
231
|
+
if (serial > 9999999999999999n)
|
|
232
|
+
throw new LaneLinkError("claim_serial_exhausted", "Receipt serial exhausted; instructions retained");
|
|
233
|
+
const id = `${serial.toString().padStart(16, "0")}-${process.pid}-${randomBytes(8).toString("hex")}`;
|
|
234
|
+
const dir = path.join(claimed, id);
|
|
235
|
+
fs.mkdirSync(dir);
|
|
236
|
+
// The shared queue lock joins the producer's open/write/close before this
|
|
237
|
+
// immutable snapshot is read. Keep the producer directory stable too.
|
|
238
|
+
try {
|
|
239
|
+
fs.renameSync(path.join(pending, "inbox.jsonl"), path.join(dir, "inbox.jsonl"));
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (error.code !== "ENOENT")
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const claims = [];
|
|
247
|
+
const messages = [];
|
|
248
|
+
const ordered = fs.readdirSync(claimed).sort((a, b) => {
|
|
249
|
+
const left = /^(\d{1,16})-/.exec(a)?.[1];
|
|
250
|
+
const right = /^(\d{1,16})-/.exec(b)?.[1];
|
|
251
|
+
if (left && right && BigInt(left) !== BigInt(right))
|
|
252
|
+
return BigInt(left) < BigInt(right) ? -1 : 1;
|
|
253
|
+
return a.localeCompare(b);
|
|
254
|
+
});
|
|
255
|
+
for (const id of ordered) {
|
|
256
|
+
if (!safeId(id))
|
|
257
|
+
continue;
|
|
258
|
+
const dir = path.join(claimed, id);
|
|
259
|
+
if (fs.existsSync(path.join(dir, ".consumed")))
|
|
260
|
+
continue;
|
|
261
|
+
let text;
|
|
262
|
+
try {
|
|
263
|
+
text = fs.readFileSync(path.join(dir, "inbox.jsonl"), "utf8");
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
if (error.code === "ENOENT")
|
|
267
|
+
continue;
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
const rows = [];
|
|
271
|
+
const invalidRows = [];
|
|
272
|
+
text.split(/\r?\n/).forEach((line, index) => {
|
|
273
|
+
if (!line)
|
|
274
|
+
return;
|
|
275
|
+
try {
|
|
276
|
+
const row = JSON.parse(line);
|
|
277
|
+
if (!row || typeof row !== "object" || typeof row.text !== "string")
|
|
278
|
+
throw new Error("invalid row");
|
|
279
|
+
rows.push(row.text);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
invalidRows.push(index + 1);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
if (invalidRows.length) {
|
|
286
|
+
// The immutable original is the quarantine evidence. Record positions,
|
|
287
|
+
// never malformed contents, and retain both even after valid rows' ACK.
|
|
288
|
+
const audit = path.join(dir, "invalid-rows.json");
|
|
289
|
+
const expectedAudit = JSON.stringify({ version: 1, rows: invalidRows }) + "\n";
|
|
290
|
+
let previousAudit;
|
|
291
|
+
try {
|
|
292
|
+
previousAudit = fs.readFileSync(audit, "utf8");
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (error.code !== "ENOENT")
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
if (previousAudit !== expectedAudit) {
|
|
299
|
+
const temporary = `${audit}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
300
|
+
try {
|
|
301
|
+
const fd = fs.openSync(temporary, "wx", 0o600);
|
|
302
|
+
try {
|
|
303
|
+
fs.writeFileSync(fd, expectedAudit);
|
|
304
|
+
fs.fsyncSync(fd);
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
fs.closeSync(fd);
|
|
308
|
+
}
|
|
309
|
+
fs.renameSync(temporary, audit);
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
fs.rmSync(temporary, { force: true });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Retry all durability barriers even if a previous attempt wrote the
|
|
316
|
+
// audit but failed before publishing its directory entry durably.
|
|
317
|
+
for (const file of [path.join(dir, "inbox.jsonl"), audit]) {
|
|
318
|
+
const fd = fs.openSync(file, "r");
|
|
319
|
+
try {
|
|
320
|
+
fs.fsyncSync(fd);
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
fs.closeSync(fd);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
for (const directory of [dir, claimed]) {
|
|
327
|
+
try {
|
|
328
|
+
const fd = fs.openSync(directory, "r");
|
|
329
|
+
try {
|
|
330
|
+
fs.fsyncSync(fd);
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
fs.closeSync(fd);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
// Same known unsupported directory-flush codes as lanes store.
|
|
338
|
+
// File flush errors and unknown directory errors remain fatal.
|
|
339
|
+
if (!["EISDIR", "EINVAL", "ENOTSUP"].includes(error.code ?? ""))
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (!rows.length)
|
|
345
|
+
continue;
|
|
346
|
+
claims.push(id);
|
|
347
|
+
messages.push(...rows);
|
|
348
|
+
}
|
|
349
|
+
return { messages: messages.join("\n"), claims };
|
|
350
|
+
}
|
|
351
|
+
function claimLinkQueue(inbox) {
|
|
352
|
+
return withLinkQueueLock(inbox, () => claimLinkQueueUnlocked(inbox));
|
|
353
|
+
}
|
|
354
|
+
function acknowledgeLinkClaims(inbox, claims) {
|
|
355
|
+
return withLinkQueueLock(inbox, () => acknowledgeLinkClaimsUnlocked(inbox, claims));
|
|
356
|
+
}
|
|
357
|
+
function acknowledgeLinkClaimsUnlocked(inbox, claims) {
|
|
358
|
+
if (!Array.isArray(claims) || !claims.length || claims.some(id => typeof id !== "string" || !safeId(id))) {
|
|
359
|
+
throw new LaneLinkError("claim_invalid", "An exact nonempty claim receipt is required");
|
|
360
|
+
}
|
|
361
|
+
// Validate the entire receipt before changing any claim. Never resolve paths
|
|
362
|
+
// outside the current session's authoritative queue.
|
|
363
|
+
for (const id of claims) {
|
|
364
|
+
if (!fs.existsSync(path.join(inbox, "unaccepted", id, "inbox.jsonl"))) {
|
|
365
|
+
throw new LaneLinkError("claim_not_found", "Claim does not belong to this queue");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const id of claims) {
|
|
369
|
+
fs.writeFileSync(path.join(inbox, "unaccepted", id, ".consumed"), "recipient-accepted\n", { mode: 0o600 });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function countQuarantined(inbox) {
|
|
373
|
+
const claims = path.join(inbox, "unaccepted");
|
|
374
|
+
if (!fs.existsSync(claims))
|
|
375
|
+
return 0;
|
|
376
|
+
return fs.readdirSync(claims).filter(id => safeId(id) && fs.existsSync(path.join(claims, id, "invalid-rows.json"))).length;
|
|
377
|
+
}
|
|
193
378
|
function countPending(inbox) {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
379
|
+
const paths = [path.join(inbox, "pending", "inbox.jsonl")];
|
|
380
|
+
for (const name of ["claimed", "unaccepted"]) {
|
|
381
|
+
const claimed = path.join(inbox, name);
|
|
382
|
+
if (fs.existsSync(claimed))
|
|
383
|
+
for (const id of fs.readdirSync(claimed)) {
|
|
384
|
+
if (!fs.existsSync(path.join(claimed, id, ".consumed")))
|
|
385
|
+
paths.push(path.join(claimed, id, "inbox.jsonl"));
|
|
386
|
+
}
|
|
197
387
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
388
|
+
let count = 0;
|
|
389
|
+
for (const file of paths) {
|
|
390
|
+
try {
|
|
391
|
+
count += fs.readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean).length;
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (error.code !== "ENOENT")
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
202
397
|
}
|
|
398
|
+
return count;
|
|
203
399
|
}
|
|
204
400
|
function readText(options) {
|
|
205
401
|
if (options.textFile) {
|
|
@@ -244,7 +440,7 @@ function listChildren(root, dir) {
|
|
|
244
440
|
if (!fs.existsSync(metaPath))
|
|
245
441
|
return [];
|
|
246
442
|
const unread = countPending(path.join(cdir, "inbox"));
|
|
247
|
-
return [{ ...readJson(metaPath), undelivered: unread }];
|
|
443
|
+
return [{ ...readJson(metaPath), undelivered: unread, quarantined_claims: countQuarantined(path.join(cdir, "inbox")) }];
|
|
248
444
|
});
|
|
249
445
|
}
|
|
250
446
|
async function waitForReport(dir, timeoutSeconds, sleep) {
|
|
@@ -288,6 +484,7 @@ export async function runLaneLink(subcommand, options) {
|
|
|
288
484
|
const root = options.hqRoot;
|
|
289
485
|
let sid = "";
|
|
290
486
|
let childContext = false;
|
|
487
|
+
let stopHookActive = false;
|
|
291
488
|
if (subcommand === "_deliver") {
|
|
292
489
|
let payload = {};
|
|
293
490
|
try {
|
|
@@ -299,6 +496,7 @@ export async function runLaneLink(subcommand, options) {
|
|
|
299
496
|
const value = payload.session_id ?? payload.sessionId;
|
|
300
497
|
sid = typeof value === "string" && safeId(value) ? value : "";
|
|
301
498
|
childContext = isChildHookPayload(payload);
|
|
499
|
+
stopHookActive = payload.stop_hook_active === true || payload.stopHookActive === true;
|
|
302
500
|
}
|
|
303
501
|
const requireSid = () => sid || (sid = resolveLinkSessionId(root, options.sessionId, env));
|
|
304
502
|
const links = rootDir(root);
|
|
@@ -307,21 +505,41 @@ export async function runLaneLink(subcommand, options) {
|
|
|
307
505
|
// drain the root's queue into the subagent, so leave the message queued.
|
|
308
506
|
if (!sid || childContext || !["PostToolUse", "Stop"].includes(options.event ?? ""))
|
|
309
507
|
return {};
|
|
310
|
-
if (options.event === "Stop" && env.HQ_STOP_DECISION_DELIVERABLE === "0")
|
|
508
|
+
if (options.event === "Stop" && (stopHookActive || env.HQ_STOP_DECISION_DELIVERABLE === "0"))
|
|
311
509
|
return {};
|
|
312
510
|
const pointer = readPointer(root, sid);
|
|
313
511
|
if (!pointer)
|
|
314
512
|
return {};
|
|
315
513
|
const target = hookMessage(root, pointer);
|
|
316
|
-
|
|
514
|
+
if (options.ackMode !== undefined && options.ackMode !== "explicit")
|
|
515
|
+
throw new LaneLinkError("ack_mode_invalid", "Unsupported acknowledgement mode");
|
|
516
|
+
const claim = options.ackMode === "explicit" ? claimLinkQueue(target.inbox) : undefined;
|
|
517
|
+
const messages = claim ? claim.messages : drainLinkQueue(target.inbox);
|
|
317
518
|
if (!messages)
|
|
318
519
|
return {};
|
|
319
|
-
const
|
|
520
|
+
const receipt = claim ? { _hqLinkDelivery: { version: 1, sessionId: sid, link: pointer.link, child: pointer.name ?? null, claims: claim.claims } } : {};
|
|
521
|
+
const acceptance = claim ? `\n\n[conduct receipt] Stable instruction receipt IDs: ${claim.claims.join(",")}. Treat replayed IDs as the same instruction, not new execution authority. After accepting these instructions, acknowledge receipt once with: hq lanes link ack --session-id ${sid} --link ${pointer.link}${pointer.name ? ` --child ${pointer.name}` : ""} --claims ${claim.claims.join(",")}. Receipt acceptance does not approve actions, answer owner pickers, or mean work is completed.` : "";
|
|
522
|
+
const body = (target.role === "conductor"
|
|
320
523
|
? `[conduct] Sessions you are conducting reported while you were working.\nEach report names the session it came from. Relay what the operator needs to know, and answer a session with: hq lanes link send --child <name> --text '...'\n\n${messages}`
|
|
321
|
-
: `[conduct] The session conducting you sent this while you were working.\nTreat it as a new instruction from the operator, taking precedence over your current task where the two conflict. When you have something to tell the conductor - a result, a blocker, a question - send it with: hq lanes link report --state <working|blocked|idle|done> --text '...'\n\n${messages}
|
|
524
|
+
: `[conduct] The session conducting you sent this while you were working.\nTreat it as a new instruction from the operator, taking precedence over your current task where the two conflict. When you have something to tell the conductor - a result, a blocker, a question - send it with: hq lanes link report --state <working|blocked|idle|done> --text '...'\n\n${messages}`) + acceptance;
|
|
322
525
|
return options.event === "PostToolUse"
|
|
323
|
-
? { output: { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: body } } }
|
|
324
|
-
: { output: { decision: "block", reason: body } };
|
|
526
|
+
? { output: { ...receipt, hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: body } } }
|
|
527
|
+
: { output: { ...receipt, decision: "block", reason: body } };
|
|
528
|
+
}
|
|
529
|
+
if (subcommand === "capabilities") {
|
|
530
|
+
return { output: { protocol: "conduct-recipient-acceptance/1", explicitClaim: true, recipientAck: true, idleWake: false } };
|
|
531
|
+
}
|
|
532
|
+
if (subcommand === "ack") {
|
|
533
|
+
const currentSid = requireSid();
|
|
534
|
+
const pointer = readPointer(root, currentSid);
|
|
535
|
+
if (!pointer)
|
|
536
|
+
throw new LaneLinkError("session_unlinked", "Session is not linked");
|
|
537
|
+
if (!options.link || options.link !== pointer.link || (options.child ?? null) !== (pointer.name ?? null)) {
|
|
538
|
+
throw new LaneLinkError("claim_owner_invalid", "Receipt does not match the current link/child binding");
|
|
539
|
+
}
|
|
540
|
+
const claims = options.claims?.split(",");
|
|
541
|
+
acknowledgeLinkClaims(hookMessage(root, pointer).inbox, claims);
|
|
542
|
+
return { output: { acknowledged: true, acceptance: "recipient_instruction_only" } };
|
|
325
543
|
}
|
|
326
544
|
if (subcommand === "open") {
|
|
327
545
|
const sessionId = requireSid();
|
|
@@ -438,7 +656,7 @@ export async function runLaneLink(subcommand, options) {
|
|
|
438
656
|
const dir = linkPath(root, link);
|
|
439
657
|
const meta = readJson(path.join(dir, "meta.json"));
|
|
440
658
|
const unread = countPending(path.join(dir, "up", "inbox"));
|
|
441
|
-
return { output: { ...meta, unread_reports: unread, children: listChildren(root, dir) } };
|
|
659
|
+
return { output: { ...meta, unread_reports: unread, quarantined_reports: countQuarantined(path.join(dir, "up", "inbox")), children: listChildren(root, dir) } };
|
|
442
660
|
}
|
|
443
661
|
if (subcommand === "leave") {
|
|
444
662
|
const sessionId = requireSid();
|
package/dist/lib/lanes/loop.d.ts
CHANGED
|
@@ -48,6 +48,10 @@ export interface LoopStatus {
|
|
|
48
48
|
stall_counts: Record<string, number>;
|
|
49
49
|
pid?: number;
|
|
50
50
|
updated_at: string;
|
|
51
|
+
handoff_hold?: {
|
|
52
|
+
envelope_id: string;
|
|
53
|
+
question_id: string;
|
|
54
|
+
};
|
|
51
55
|
}
|
|
52
56
|
export interface LoopResult {
|
|
53
57
|
schema_version: typeof LOOP_SCHEMA;
|
|
@@ -61,6 +65,7 @@ export interface LoopResult {
|
|
|
61
65
|
resumed?: boolean;
|
|
62
66
|
fresh_fallback?: boolean;
|
|
63
67
|
question_id?: string;
|
|
68
|
+
hold_reason?: "handoff_needs_repair";
|
|
64
69
|
decision?: {
|
|
65
70
|
question_id: string;
|
|
66
71
|
choice: string;
|
|
@@ -112,6 +117,7 @@ export interface LoopRunnerOptions {
|
|
|
112
117
|
attempt: number;
|
|
113
118
|
error: string;
|
|
114
119
|
questionId: string;
|
|
120
|
+
reason?: "handoff_needs_repair";
|
|
115
121
|
}): Promise<void> | void;
|
|
116
122
|
defaultDeadlineSeconds?: number;
|
|
117
123
|
now?: () => Date;
|
|
@@ -131,8 +137,19 @@ interface StoredEnvelope {
|
|
|
131
137
|
queued_at: string;
|
|
132
138
|
deadline_budget_seconds?: number;
|
|
133
139
|
stall_attempts?: number;
|
|
140
|
+
accepted_handoff?: {
|
|
141
|
+
output: string;
|
|
142
|
+
sha256: string;
|
|
143
|
+
};
|
|
144
|
+
handoff_retry?: boolean;
|
|
134
145
|
envelope: LoopEnvelope;
|
|
135
146
|
}
|
|
147
|
+
export declare const HANDOFF_HOLD_CHOICES: readonly ["Use the repaired handoff without running the phase again", "Retry the phase; I understand external effects may repeat", "Keep this phase held"];
|
|
148
|
+
export declare function loopHandoffHoldIdentity(runDir: string): {
|
|
149
|
+
envelope_id: string;
|
|
150
|
+
question_id: string;
|
|
151
|
+
unresolved: boolean;
|
|
152
|
+
} | undefined;
|
|
136
153
|
/** Validate both hq-cli phase envelopes and the conduct protocol §8 shape. */
|
|
137
154
|
export declare function validateLoopEnvelope(value: unknown): LoopEnvelope;
|
|
138
155
|
export declare function validateSection8Handoff(value: unknown, envelope: Section8PhaseEnvelope): Section8Handoff;
|