@indigoai-us/hq-cli 5.345.62 → 5.345.63
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 +4 -0
- package/dist/command-catalog.generated.d.ts +736 -0
- package/dist/command-catalog.generated.js +907 -0
- package/dist/commands/lanes-link.d.ts +3 -0
- package/dist/commands/lanes-link.js +83 -0
- package/dist/commands/lanes-loop.js +5 -6
- package/dist/commands/lanes-questions.js +27 -3
- package/dist/commands/lanes.js +26 -4
- package/dist/lib/lanes/links.d.ts +39 -0
- package/dist/lib/lanes/links.js +450 -0
- package/dist/lib/lanes/loop.d.ts +7 -2
- package/dist/lib/lanes/loop.js +169 -57
- package/dist/utils/version-gate.js +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
2
|
+
import { readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
|
|
3
|
+
import { isLinkedSession, linkedSessionIdFromHookInput, LANE_SESSION_LINKS_FLAG, LaneLinkError, runLaneLinkWithFlag } from "../lib/lanes/links.js";
|
|
4
|
+
function writeJson(value) {
|
|
5
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
6
|
+
}
|
|
7
|
+
function addLinkOptions(command) {
|
|
8
|
+
return command
|
|
9
|
+
.option("--json", "Print JSON output (the default)")
|
|
10
|
+
.option("--session-id <id>", "Act as this session")
|
|
11
|
+
.option("--link <id>", "Conduct link id")
|
|
12
|
+
.option("--child <name|all>", "Child name or all children")
|
|
13
|
+
.option("--name <name>", "Child name")
|
|
14
|
+
.option("--engine <engine>", "Session engine")
|
|
15
|
+
.option("--title <title>", "Session title")
|
|
16
|
+
.option("--host-session <id>", "Host session id")
|
|
17
|
+
.option("--text <message>", "Message text")
|
|
18
|
+
.option("--text-file <path>", "Read message text from a file")
|
|
19
|
+
.option("--state <state>", "Child state")
|
|
20
|
+
.option("--note <note>", "State note")
|
|
21
|
+
.option("--timeout <seconds>", "Wait timeout in seconds", (value) => Number(value));
|
|
22
|
+
}
|
|
23
|
+
async function readHookInput() {
|
|
24
|
+
const chunks = [];
|
|
25
|
+
for await (const chunk of process.stdin)
|
|
26
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
27
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
28
|
+
}
|
|
29
|
+
function commandError(error) {
|
|
30
|
+
const code = error instanceof LaneLinkError ? error.code : "internal";
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
process.stderr.write(`hq lanes link: ${message}\n`);
|
|
33
|
+
writeJson({ error: code, message });
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
export function registerLanesLinkCommand(lanes) {
|
|
37
|
+
const link = lanes.command("link").description("Link live sessions to a conductor through session inboxes");
|
|
38
|
+
const commands = ["open", "join", "send", "report", "status", "inbox", "read", "wait", "list", "leave", "close"];
|
|
39
|
+
for (const subcommand of commands) {
|
|
40
|
+
addLinkOptions(link.command(subcommand)).action(async (options) => {
|
|
41
|
+
try {
|
|
42
|
+
const root = resolveLanesHqRoot(process.env);
|
|
43
|
+
const enabled = await readLanesRegistryFlagFailOpen(LANE_SESSION_LINKS_FLAG, {
|
|
44
|
+
env: process.env,
|
|
45
|
+
defaultValue: true,
|
|
46
|
+
});
|
|
47
|
+
const result = await runLaneLinkWithFlag(subcommand, { ...options, hqRoot: root, env: process.env }, enabled);
|
|
48
|
+
writeJson(result.output ?? {});
|
|
49
|
+
if (result.exitCode !== undefined)
|
|
50
|
+
process.exitCode = result.exitCode;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
commandError(error);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const deliver = link.command("_deliver", { hidden: true })
|
|
58
|
+
.description("Internal linked-session hook delivery")
|
|
59
|
+
.requiredOption("--event <event>", "Hook event");
|
|
60
|
+
deliver.action(async (options) => {
|
|
61
|
+
// Hook commands must not update/re-exec the CLI inside the provider's time budget.
|
|
62
|
+
process.env.HQ_NO_UPDATE_CHECK = "1";
|
|
63
|
+
try {
|
|
64
|
+
const root = resolveLanesHqRoot(process.env);
|
|
65
|
+
const input = await readHookInput();
|
|
66
|
+
const sid = linkedSessionIdFromHookInput(input);
|
|
67
|
+
if (!sid || !isLinkedSession(root, sid))
|
|
68
|
+
return;
|
|
69
|
+
const enabled = await readLanesRegistryFlagFailOpen(LANE_SESSION_LINKS_FLAG, {
|
|
70
|
+
env: process.env,
|
|
71
|
+
defaultValue: true,
|
|
72
|
+
});
|
|
73
|
+
const result = await runLaneLinkWithFlag("_deliver", { hqRoot: root, env: process.env, event: options.event, input }, enabled);
|
|
74
|
+
if (result.output)
|
|
75
|
+
writeJson(result.output);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
process.stderr.write(`hq lanes link _deliver: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
79
|
+
// Hook failures are fail-open; queued messages remain available if delivery failed.
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=lanes-link.js.map
|
|
@@ -17,12 +17,11 @@ export function startLaneLoopController(ctx, laneId, initialAdmission = false) {
|
|
|
17
17
|
const entry = process.argv[1];
|
|
18
18
|
if (!entry)
|
|
19
19
|
throw new LanesError("loop_controller_entry_missing", "Cannot resolve the hq CLI entry point for the loop controller", { lane_id: laneId });
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
stdio: "ignore",
|
|
24
|
-
|
|
25
|
-
});
|
|
20
|
+
const args = [entry, "lanes", "_loop-worker", laneId];
|
|
21
|
+
const env = { ...ctx.env, HQ_LANE_LOOP_CONTROLLER: "1", ...(initialAdmission ? { HQ_LANE_LOOP_INITIAL: "1" } : {}) };
|
|
22
|
+
const child = ctx.spawnProcess
|
|
23
|
+
? ctx.spawnProcess(process.execPath, args, { cwd: ctx.hqRoot(), detached: true, windowsHide: true, stdio: ["ignore", 1, 2], env })
|
|
24
|
+
: spawn(process.execPath, args, { cwd: ctx.hqRoot(), detached: true, stdio: "ignore", env });
|
|
26
25
|
child.unref();
|
|
27
26
|
if (child.pid === undefined)
|
|
28
27
|
throw new LanesError("loop_controller_spawn_failed", "Loop controller did not start", { lane_id: laneId });
|
|
@@ -5,7 +5,7 @@ import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
|
5
5
|
import { appendCappedHistory, createQuestion, isLanesError, listSessionLiveness, parseLivenessState, parseQuestionRecord, parseQuestionStatus, readDaemonPresenceConnected, readLane, readQuestion, wakeAnsweredLaneNow, sessionLiveness, appendDropboxMessage, answerDropboxMessage, laneAcceptsDropboxDelivery, laneIdForAnswerDelivery, writeQuestionBody } from "../lib/lanes/index.js";
|
|
6
6
|
import { resolveQuestionCompanyWithFallback } from "../lib/lanes/questions.js";
|
|
7
7
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
8
|
-
import { applyLoopStallAnswer } from "../lib/lanes/loop.js";
|
|
8
|
+
import { applyLoopStallAnswer, findLoopStallResult } from "../lib/lanes/loop.js";
|
|
9
9
|
export function resolveAnswerChoice(choice, options, freeform) {
|
|
10
10
|
const trimmed = choice.trim();
|
|
11
11
|
if (/^[0-9]+$/.test(trimmed)) {
|
|
@@ -801,7 +801,6 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
801
801
|
});
|
|
802
802
|
return;
|
|
803
803
|
}
|
|
804
|
-
throwIfQuestionNotPending(question);
|
|
805
804
|
if (askingLane.state === "awaiting_input") {
|
|
806
805
|
assertLaneRunsHereForOperatorMutation(ctx, askingLane, "wake after answering");
|
|
807
806
|
}
|
|
@@ -819,11 +818,27 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
819
818
|
throw new LanesError("missing_choice", "--choice or --freeform is required", {});
|
|
820
819
|
}
|
|
821
820
|
const choice = freeform ?? resolveAnswerChoice(opts.choice, body.options, false);
|
|
821
|
+
const isLoopStallQuestion = body.loop_stall?.lane_id === askingLane.lane_id && askingLane.loop_mode === true;
|
|
822
|
+
const incomingAnswerKind = freeform ? "freeform" : "option";
|
|
823
|
+
const incomingNotesMatch = opts.notes
|
|
824
|
+
? question.answer?.notes_ref !== undefined && body.notes === opts.notes
|
|
825
|
+
: question.answer?.notes_ref === undefined;
|
|
826
|
+
const retryingSameLoopAnswer = isLoopStallQuestion && question.status === "answered" && question.answer?.choice === choice &&
|
|
827
|
+
question.answer.answer_kind === incomingAnswerKind && incomingNotesMatch &&
|
|
828
|
+
deps.sameQuestionAnswer(question.answer, { ...question.answer });
|
|
829
|
+
if (question.status !== "pending" && !retryingSameLoopAnswer)
|
|
830
|
+
throwIfQuestionNotPending(question);
|
|
831
|
+
const loopStallResult = isLoopStallQuestion
|
|
832
|
+
? findLoopStallResult(path.join(hqRoot, "workspace", "lanes-runs", askingLane.lane_id), questionId)
|
|
833
|
+
: undefined;
|
|
822
834
|
const at = ctx.now().toISOString();
|
|
823
835
|
const answeredBy = sessionIdFromEnv(ctx.env)
|
|
824
836
|
? `session:${sessionIdFromEnv(ctx.env)}`
|
|
825
837
|
: "user";
|
|
826
838
|
const updated = casUpdateQuestion(hqRoot, questionId, (current) => {
|
|
839
|
+
if (retryingSameLoopAnswer && current.status === "answered" && current.answer?.choice === choice &&
|
|
840
|
+
deps.sameQuestionAnswer(current.answer, question.answer))
|
|
841
|
+
return current;
|
|
827
842
|
throwIfQuestionNotPending(current);
|
|
828
843
|
assertQuestionCompanyAccess(current, askingLane, requestedCompany, hqRoot);
|
|
829
844
|
const recordedCompany = current.company ?? questionCompany;
|
|
@@ -856,7 +871,12 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
856
871
|
},
|
|
857
872
|
};
|
|
858
873
|
});
|
|
859
|
-
if (
|
|
874
|
+
if (isLoopStallQuestion) {
|
|
875
|
+
if (!loopStallResult && !retryingSameLoopAnswer) {
|
|
876
|
+
process.stderr.write(`hq lanes: loop phase for question ${questionId} was already re-enqueued; recording answer as superseded\n`);
|
|
877
|
+
printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: "superseded" });
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
860
880
|
const resolved = applyLoopStallAnswer({
|
|
861
881
|
runDir: path.join(hqRoot, "workspace", "lanes-runs", askingLane.lane_id),
|
|
862
882
|
questionId,
|
|
@@ -866,6 +886,10 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
866
886
|
});
|
|
867
887
|
if (!resolved)
|
|
868
888
|
throw new LanesError("loop_stall_result_missing", `Loop stall result for question ${questionId} is missing`, { question_id: questionId, lane_id: askingLane.lane_id });
|
|
889
|
+
if (resolved.action === "already_applied") {
|
|
890
|
+
printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: resolved.action });
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
869
893
|
const controllerPid = await deps.restartLoopController(ctx, askingLane.lane_id);
|
|
870
894
|
printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: resolved.action, controller_pid: controllerPid });
|
|
871
895
|
return;
|
package/dist/commands/lanes.js
CHANGED
|
@@ -34,6 +34,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
34
34
|
import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
35
35
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
36
36
|
import { registerLaneSessionStartCommand } from "./lanes-session-start.js";
|
|
37
|
+
import { registerLanesLinkCommand } from "./lanes-link.js";
|
|
37
38
|
import { laneDepthCap } from "../lib/lanes/depth-cap.js";
|
|
38
39
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
39
40
|
import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
|
|
@@ -9431,10 +9432,30 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9431
9432
|
}
|
|
9432
9433
|
const runDirPath = runDir(handlerCtx.hqRoot(), laneId);
|
|
9433
9434
|
if (loopControllerIsAlive(runDirPath)) {
|
|
9434
|
-
|
|
9435
|
-
if (
|
|
9436
|
-
|
|
9437
|
-
|
|
9435
|
+
let status = loopStatus(runDirPath, laneId);
|
|
9436
|
+
if (status.state === "parked") {
|
|
9437
|
+
const deadline = Date.now() + 10_000;
|
|
9438
|
+
while (Date.now() < deadline) {
|
|
9439
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
9440
|
+
status = loopStatus(runDirPath, laneId);
|
|
9441
|
+
if (status.state !== "parked") {
|
|
9442
|
+
if (status.pid === undefined)
|
|
9443
|
+
throw new LanesError("loop_controller_pid_missing", `Loop controller for ${laneId} left parked without a recorded pid`, { lane_id: laneId });
|
|
9444
|
+
return status.pid;
|
|
9445
|
+
}
|
|
9446
|
+
if (!loopControllerIsAlive(runDirPath))
|
|
9447
|
+
break;
|
|
9448
|
+
}
|
|
9449
|
+
if (loopControllerIsAlive(runDirPath) && loopStatus(runDirPath, laneId).state === "parked") {
|
|
9450
|
+
throw new LanesError("loop_controller_winding_down", `Loop controller for ${laneId} stayed parked while winding down`, { lane_id: laneId });
|
|
9451
|
+
}
|
|
9452
|
+
}
|
|
9453
|
+
else {
|
|
9454
|
+
const pid = status.pid;
|
|
9455
|
+
if (pid === undefined)
|
|
9456
|
+
throw new LanesError("loop_controller_pid_missing", `Loop controller for ${laneId} is alive without a recorded pid`, { lane_id: laneId });
|
|
9457
|
+
return pid;
|
|
9458
|
+
}
|
|
9438
9459
|
}
|
|
9439
9460
|
return startLaneLoopController(handlerCtx, laneId);
|
|
9440
9461
|
},
|
|
@@ -9534,6 +9555,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9534
9555
|
.command("lanes")
|
|
9535
9556
|
.description("Create, supervise, and steer worker lanes; routed decision queue");
|
|
9536
9557
|
registerLaneSessionStartCommand(lanes);
|
|
9558
|
+
registerLanesLinkCommand(lanes);
|
|
9537
9559
|
registerLaneChecksCommand(lanes, {
|
|
9538
9560
|
hqRoot: ctx.hqRoot,
|
|
9539
9561
|
resolveCompany: ctx.resolveCompany,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const LANE_SESSION_LINKS_FLAG = "lanes.session-links";
|
|
2
|
+
export declare class LaneLinkError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
constructor(code: string, message: string);
|
|
5
|
+
}
|
|
6
|
+
export interface LinkOptions {
|
|
7
|
+
hqRoot: string;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
link?: string;
|
|
11
|
+
child?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
engine?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
hostSession?: string;
|
|
16
|
+
text?: string;
|
|
17
|
+
textFile?: string;
|
|
18
|
+
state?: string;
|
|
19
|
+
note?: string;
|
|
20
|
+
timeout?: number;
|
|
21
|
+
event?: string;
|
|
22
|
+
input?: string;
|
|
23
|
+
now?: () => Date;
|
|
24
|
+
sleep?: (milliseconds: number) => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export interface LinkResult {
|
|
27
|
+
output?: Record<string, unknown>;
|
|
28
|
+
exitCode?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function resolveLinkSessionId(root: string, explicit: string | undefined, env?: NodeJS.ProcessEnv): string;
|
|
31
|
+
/** Cheap hook guard used before remote flag lookup or queue access. */
|
|
32
|
+
export declare function linkedSessionIdFromHookInput(input: string): string;
|
|
33
|
+
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
|
+
export declare function drainLinkQueue(inbox: string): string;
|
|
36
|
+
export declare function runLaneLink(subcommand: string, options: LinkOptions): Promise<LinkResult>;
|
|
37
|
+
/** Apply the default-on feature gate contract at the command boundary. */
|
|
38
|
+
export declare function runLaneLinkWithFlag(subcommand: string, options: LinkOptions, enabled: boolean): Promise<LinkResult>;
|
|
39
|
+
//# sourceMappingURL=links.d.ts.map
|