@indigoai-us/hq-cli 5.345.55 → 5.345.57
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 +18 -0
- package/assets/scaffold/core/scripts/derive-trigger-facts.sh +10 -1
- package/assets/scaffold/core/scripts/jobs-validate.sh +2 -1
- package/assets/scaffold/core/scripts/session-title.sh +18 -15
- package/dist/command-catalog.generated.d.ts +124 -0
- package/dist/command-catalog.generated.js +155 -0
- package/dist/commands/core-delegate-w1b.js +18 -5
- package/dist/commands/core-derive-trigger-facts-command.js +14 -0
- package/dist/commands/core-derive-trigger-facts-native.d.ts +1 -0
- package/dist/commands/core-derive-trigger-facts-native.js +20 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +15 -2
- package/dist/commands/lanes-checks.d.ts +3 -0
- package/dist/commands/lanes-checks.js +34 -0
- package/dist/commands/lanes-loop.d.ts +12 -0
- package/dist/commands/lanes-loop.js +214 -0
- package/dist/commands/lanes-questions.js +37 -11
- package/dist/commands/lanes-resume.js +19 -1
- package/dist/commands/lanes-session-start.d.ts +2 -7
- package/dist/commands/lanes-session-start.js +9 -29
- package/dist/commands/lanes-shared.d.ts +14 -2
- package/dist/commands/lanes.d.ts +3 -1
- package/dist/commands/lanes.js +376 -35
- package/dist/commands/members.d.ts +4 -0
- package/dist/commands/members.js +23 -2
- package/dist/lanes-drain-hook.js +6 -2
- package/dist/lib/doctor/checks/sync-health.d.ts +2 -0
- package/dist/lib/doctor/checks/sync-health.js +47 -23
- package/dist/lib/lanes/delivery.d.ts +3 -0
- package/dist/lib/lanes/delivery.js +5 -0
- package/dist/lib/lanes/dropbox.d.ts +7 -2
- package/dist/lib/lanes/dropbox.js +157 -10
- package/dist/lib/lanes/flag-keys.d.ts +12 -0
- package/dist/lib/lanes/flag-keys.js +12 -0
- package/dist/lib/lanes/hooks-install.d.ts +14 -11
- package/dist/lib/lanes/hooks-install.js +70 -50
- package/dist/lib/lanes/loop.d.ts +158 -0
- package/dist/lib/lanes/loop.js +743 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +9 -1
- package/dist/lib/lanes/outpost-dispatch.js +21 -7
- package/dist/lib/lanes/reaper.d.ts +58 -0
- package/dist/lib/lanes/reaper.js +403 -0
- package/dist/lib/lanes/reconcile.d.ts +19 -0
- package/dist/lib/lanes/reconcile.js +173 -6
- package/dist/lib/lanes/spawn.d.ts +4 -0
- package/dist/lib/lanes/spawn.js +41 -9
- package/dist/lib/lanes/status-fields.d.ts +12 -0
- package/dist/lib/lanes/status-fields.js +189 -0
- package/dist/lib/lanes/store.js +49 -2
- package/dist/lib/lanes/types.d.ts +18 -0
- package/dist/lib/lanes/types.js +3 -3
- package/dist/lib/lanes/watcher.js +3 -0
- package/dist/lib/lanes/worker-continuity.d.ts +18 -0
- package/dist/lib/lanes/worker-continuity.js +81 -0
- package/dist/lib/lanes/worker-process.d.ts +4 -0
- package/dist/lib/lanes/worker-process.js +77 -0
- package/dist/main.js +9 -3
- package/dist/utils/cli-telemetry.d.ts +4 -1
- package/dist/utils/cli-telemetry.js +8 -0
- package/dist/utils/jq-windows-compat.d.ts +13 -0
- package/dist/utils/jq-windows-compat.js +48 -0
- package/dist/utils/run-bundled-script.d.ts +3 -0
- package/dist/utils/run-bundled-script.js +9 -80
- package/dist/utils/windows-bash.d.ts +5 -0
- package/dist/utils/windows-bash.js +75 -0
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ import { LANES_RUNS_REL } from "../lib/lanes/spawn.js";
|
|
|
13
13
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
14
14
|
import { checkCompanyPath } from "../lib/lanes/checked-company-path.js";
|
|
15
15
|
import { findOrphanedBareIdDropboxes } from "../lib/lanes/orphaned-dropbox.js";
|
|
16
|
+
import { parseReapMinAge, reapLocalLanes } from "../lib/lanes/reaper.js";
|
|
16
17
|
export function mappingIncludesStory(mapping, storyId) {
|
|
17
18
|
return (mapping.storyPrefix === undefined || storyId.startsWith(mapping.storyPrefix))
|
|
18
19
|
&& (mapping.excludePrefix === undefined || !storyId.startsWith(mapping.excludePrefix));
|
|
@@ -905,6 +906,39 @@ async function orphanReport(root, company, now, minAgeMinutes, userIdleHours, de
|
|
|
905
906
|
output("orphans.stop", stopped);
|
|
906
907
|
}
|
|
907
908
|
export function registerLaneChecksCommand(parent, deps, wrap) {
|
|
909
|
+
parent.command("reap")
|
|
910
|
+
// CL-5's SessionStart hook launches this alias detached and does not wait for it.
|
|
911
|
+
.alias("_reap")
|
|
912
|
+
.description("Classify local lanes whose session senior has ended; report only by default")
|
|
913
|
+
.option("--min-age <minutes>", "Minimum lane age in minutes (default: 120)")
|
|
914
|
+
.option("--apply-stale-state", "Remove stale worker and watcher pid markers")
|
|
915
|
+
.option("--stop", "Stop candidate process trees when the hq-flags stop key is enabled")
|
|
916
|
+
.option("--reason <text>", "Required audit reason when --stop is set")
|
|
917
|
+
.option("--json", "Print machine-readable JSON (default)")
|
|
918
|
+
.action(wrap(async (options) => {
|
|
919
|
+
const reason = options.reason?.trim();
|
|
920
|
+
if (options.stop && !reason)
|
|
921
|
+
fail("lane_reap_reason_required", "--stop requires a non-empty --reason", {});
|
|
922
|
+
if (!options.stop && options.reason !== undefined)
|
|
923
|
+
fail("lane_reap_reason_requires_stop", "--reason can only be used with --stop", {});
|
|
924
|
+
if (reason)
|
|
925
|
+
assertNoSecretText("stop reason", reason);
|
|
926
|
+
const minAgeMinutes = parseReapMinAge(options.minAge);
|
|
927
|
+
const report = await reapLocalLanes(deps.hqRoot(), {
|
|
928
|
+
minAgeMinutes,
|
|
929
|
+
stop: options.stop === true,
|
|
930
|
+
applyStaleState: options.applyStaleState === true,
|
|
931
|
+
reason: reason ?? "Orphaned lane: owning session is confirmed ended",
|
|
932
|
+
json: true,
|
|
933
|
+
}, {
|
|
934
|
+
...deps.reaperDeps,
|
|
935
|
+
now: deps.now,
|
|
936
|
+
sessionState: deps.sessionState,
|
|
937
|
+
stopEnabled: deps.stopEnabled,
|
|
938
|
+
stopLane: deps.stopLane,
|
|
939
|
+
});
|
|
940
|
+
console.log(JSON.stringify(report, null, 2));
|
|
941
|
+
}));
|
|
908
942
|
parent.command("checks")
|
|
909
943
|
.description("Evaluate tracker and lane health checks")
|
|
910
944
|
.option("--company <slug>", "Select the company by slug")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Ctx, LaneResumeOptions } from "./lanes-shared.js";
|
|
2
|
+
import type { LaneRecord } from "../lib/lanes/types.js";
|
|
3
|
+
export interface LoopControllerDeps {
|
|
4
|
+
resumeLane(laneId: string, options: LaneResumeOptions): Promise<unknown>;
|
|
5
|
+
interruptLane(laneId: string, reason: string, options: {
|
|
6
|
+
preserveLoopQueue: boolean;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
emitTerminal(lane: LaneRecord): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export declare function startLaneLoopController(ctx: Ctx, laneId: string, initialAdmission?: boolean): number;
|
|
11
|
+
export declare function runLaneLoopController(ctx: Ctx, laneId: string, deps: LoopControllerDeps): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=lanes-loop.d.ts.map
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { LanesError } from "../lib/lanes/errors.js";
|
|
5
|
+
import { readLane, retryUpdateLane } from "../lib/lanes/store.js";
|
|
6
|
+
import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
|
|
7
|
+
import { spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
8
|
+
import { raiseLaneQuestion } from "../lib/lanes/questions.js";
|
|
9
|
+
import { isTerminalLaneState } from "../lib/lanes/types.js";
|
|
10
|
+
import { clearLoopControllerPid, loopAdmissionPath, runLoopLane, setLoopControllerPid } from "../lib/lanes/loop.js";
|
|
11
|
+
import { laneStories } from "../lib/lanes/stories.js";
|
|
12
|
+
const LOOP_RUNS_REL = path.join("workspace", "lanes-runs");
|
|
13
|
+
function loopRunDir(hqRoot, laneId) {
|
|
14
|
+
return path.join(hqRoot, LOOP_RUNS_REL, laneId);
|
|
15
|
+
}
|
|
16
|
+
export function startLaneLoopController(ctx, laneId, initialAdmission = false) {
|
|
17
|
+
const entry = process.argv[1];
|
|
18
|
+
if (!entry)
|
|
19
|
+
throw new LanesError("loop_controller_entry_missing", "Cannot resolve the hq CLI entry point for the loop controller", { lane_id: laneId });
|
|
20
|
+
const child = spawn(process.execPath, [entry, "lanes", "_loop-worker", laneId], {
|
|
21
|
+
cwd: ctx.hqRoot(),
|
|
22
|
+
detached: true,
|
|
23
|
+
stdio: "ignore",
|
|
24
|
+
env: { ...ctx.env, HQ_LANE_LOOP_CONTROLLER: "1", ...(initialAdmission ? { HQ_LANE_LOOP_INITIAL: "1" } : {}) },
|
|
25
|
+
});
|
|
26
|
+
child.unref();
|
|
27
|
+
if (child.pid === undefined)
|
|
28
|
+
throw new LanesError("loop_controller_spawn_failed", "Loop controller did not start", { lane_id: laneId });
|
|
29
|
+
setLoopControllerPid(loopRunDir(ctx.hqRoot(), laneId), laneId, child.pid);
|
|
30
|
+
return child.pid;
|
|
31
|
+
}
|
|
32
|
+
export async function runLaneLoopController(ctx, laneId, deps) {
|
|
33
|
+
const hqRoot = ctx.hqRoot();
|
|
34
|
+
const lane = readLane(hqRoot, laneId);
|
|
35
|
+
if (lane.loop_mode !== true)
|
|
36
|
+
throw new LanesError("lane_not_loop_mode", `Lane ${laneId} is not a loop lane`, { lane_id: laneId });
|
|
37
|
+
const runDir = loopRunDir(hqRoot, laneId);
|
|
38
|
+
let initialAdmission = ctx.env.HQ_LANE_LOOP_INITIAL === "1";
|
|
39
|
+
const stop = new AbortController();
|
|
40
|
+
const onSignal = () => stop.abort();
|
|
41
|
+
process.once("SIGTERM", onSignal);
|
|
42
|
+
process.once("SIGINT", onSignal);
|
|
43
|
+
try {
|
|
44
|
+
const result = await runLoopLane({
|
|
45
|
+
lane,
|
|
46
|
+
runDir,
|
|
47
|
+
signal: stop.signal,
|
|
48
|
+
idlePollMs: 250,
|
|
49
|
+
execute: async ({ envelope, signal, fresh, sessionId }) => {
|
|
50
|
+
let current = readLane(hqRoot, laneId);
|
|
51
|
+
const data = envelope;
|
|
52
|
+
const storyId = typeof data.story_id === "string" && data.story_id.trim() ? data.story_id.trim() : current.story_id;
|
|
53
|
+
if (storyId !== current.story_id) {
|
|
54
|
+
current = retryUpdateLane(hqRoot, laneId, "command", (record) => {
|
|
55
|
+
const stories = laneStories(record);
|
|
56
|
+
if (!stories.some((story) => story.project_id === record.project_id && story.story_id === storyId)) {
|
|
57
|
+
stories.push({ project_id: record.project_id, story_id: storyId, status: "in_progress", added_at: ctx.now().toISOString(), updated_at: ctx.now().toISOString() });
|
|
58
|
+
}
|
|
59
|
+
return { ...record, story_id: storyId, stories };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const worktree = typeof data.worktree === "string" ? path.resolve(data.worktree) : hqRoot;
|
|
63
|
+
if (typeof data.worktree === "string") {
|
|
64
|
+
let isDirectory = false;
|
|
65
|
+
try {
|
|
66
|
+
isDirectory = fs.statSync(worktree).isDirectory();
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code !== "ENOENT" && error.code !== "ENOTDIR")
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
if (!isDirectory)
|
|
73
|
+
throw new Error(`phase worktree is not an accessible directory: ${worktree}`);
|
|
74
|
+
}
|
|
75
|
+
const phaseContext = data.schema === "hq-phase-envelope/v1"
|
|
76
|
+
? [
|
|
77
|
+
`Story id: ${storyId}`,
|
|
78
|
+
`Story title: ${String(data.story_title ?? "")}`,
|
|
79
|
+
`Story description: ${String(data.story_description ?? "")}`,
|
|
80
|
+
`Phase: ${String(data.phase)}`,
|
|
81
|
+
`Working directory (worktree): ${worktree}`,
|
|
82
|
+
`Incoming handoff: ${String(data.incoming_handoff ?? "None: this is the first phase")}`,
|
|
83
|
+
"Acceptance criteria:",
|
|
84
|
+
...(Array.isArray(data.acceptance_criteria) ? data.acceptance_criteria.map((item, index) => `${index}. ${String(item)}`) : []),
|
|
85
|
+
"Constraints:",
|
|
86
|
+
...(Array.isArray(data.constraints) ? data.constraints.map((item) => `- ${String(item)}`) : []),
|
|
87
|
+
].join("\n")
|
|
88
|
+
: `Execute the ${String(data.phase ?? "requested")} phase for story ${storyId}.`;
|
|
89
|
+
const prompt = typeof data.prompt === "string" ? data.prompt :
|
|
90
|
+
`${phaseContext}\n\nPhase envelope:\n${JSON.stringify(envelope, null, 2)}`;
|
|
91
|
+
const briefPath = path.join(runDir, "brief.md");
|
|
92
|
+
fs.writeFileSync(briefPath, [
|
|
93
|
+
"Follow the lane worker instructions and complete this single bounded phase.",
|
|
94
|
+
"Return the standard hq-lane-envelope.v1 envelope when the phase is complete. For a section 8 phase envelope, put a complete hq-phase-handoff/v1 object in artifacts; it must identify the requested story_id and phase and include worker_id, status, summary, files_changed, commits, back_pressure (tests/lint/typecheck/build), and context_for_next.",
|
|
95
|
+
"",
|
|
96
|
+
prompt,
|
|
97
|
+
].join("\n"));
|
|
98
|
+
const admissionPath = loopAdmissionPath(current.state, current.pending_resume !== undefined, initialAdmission);
|
|
99
|
+
if (admissionPath === "initial") {
|
|
100
|
+
initialAdmission = false;
|
|
101
|
+
if (current.state === "running") {
|
|
102
|
+
retryUpdateLane(hqRoot, laneId, "command", (record) => ({
|
|
103
|
+
...record,
|
|
104
|
+
state: "queued",
|
|
105
|
+
queued_reason: undefined,
|
|
106
|
+
queued_details: undefined,
|
|
107
|
+
admission_reserved_at: undefined,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
await spawnLaneWorker(hqRoot, current, {
|
|
111
|
+
env: ctx.env,
|
|
112
|
+
modelDeps: { readFileSync: ctx.readFileSync, probe: ctx.probeProvider, env: ctx.env },
|
|
113
|
+
existsSync: ctx.existsSync,
|
|
114
|
+
spawnProcess: ctx.spawnProcess,
|
|
115
|
+
proveDetached: ctx.proveDetached,
|
|
116
|
+
killProcess: ctx.killProcess,
|
|
117
|
+
now: ctx.now,
|
|
118
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
119
|
+
runCompaction: ctx.runCompaction,
|
|
120
|
+
locateTranscript: ctx.locateTranscript,
|
|
121
|
+
workingDirectory: worktree,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else if (admissionPath === "deferred") {
|
|
125
|
+
retryUpdateLane(hqRoot, laneId, "command", (record) => ({
|
|
126
|
+
...record,
|
|
127
|
+
pending_resume: record.pending_resume ? {
|
|
128
|
+
...record.pending_resume,
|
|
129
|
+
...(fresh ? { session_id: undefined, fresh_thread: true } : sessionId ? { session_id: sessionId, fresh_thread: undefined } : {}),
|
|
130
|
+
working_directory: worktree,
|
|
131
|
+
} : undefined,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
await deps.resumeLane(laneId, {
|
|
136
|
+
text: `Read ${briefPath} and follow it as the complete instruction for this phase.`,
|
|
137
|
+
project: current.project_id,
|
|
138
|
+
story: storyId,
|
|
139
|
+
freshThread: fresh,
|
|
140
|
+
...(sessionId ? { loopSessionId: sessionId } : {}),
|
|
141
|
+
workingDirectory: worktree,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
let interruptPromise;
|
|
145
|
+
const abortPhase = () => {
|
|
146
|
+
interruptPromise = deps.interruptLane(laneId, "loop phase deadline exceeded", { preserveLoopQueue: true }).catch((error) => {
|
|
147
|
+
process.stderr.write(`hq lanes: could not interrupt stalled loop phase ${laneId}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
signal.addEventListener("abort", abortPhase, { once: true });
|
|
151
|
+
try {
|
|
152
|
+
for (;;) {
|
|
153
|
+
if (signal.aborted) {
|
|
154
|
+
await interruptPromise;
|
|
155
|
+
throw signal.reason ?? new Error("phase deadline exceeded");
|
|
156
|
+
}
|
|
157
|
+
const latest = readLane(hqRoot, laneId);
|
|
158
|
+
if (latest.state === "done" || latest.state === "blocked" || latest.state === "awaiting_input" || isTerminalLaneState(latest.state)) {
|
|
159
|
+
const envelopeRecord = readLatestEnvelopeRecord(hqRoot, laneId);
|
|
160
|
+
if (latest.state !== "done" && latest.state !== "blocked" && latest.state !== "awaiting_input") {
|
|
161
|
+
throw new Error(`lane phase ended in ${latest.state}${latest.terminal_reason ? ` (${latest.terminal_reason})` : ""}`);
|
|
162
|
+
}
|
|
163
|
+
if (!envelopeRecord)
|
|
164
|
+
throw new Error(`lane ${laneId} ended without writing a phase envelope`);
|
|
165
|
+
const handoff = envelopeRecord.envelope.artifacts.find((artifact) => typeof artifact === "object" && artifact !== null &&
|
|
166
|
+
artifact.schema === "hq-phase-handoff/v1");
|
|
167
|
+
if (data.schema === "hq-phase-envelope/v1" && !handoff) {
|
|
168
|
+
throw new Error("section 8 phase requires an hq-phase-handoff/v1 object in the lane envelope artifacts");
|
|
169
|
+
}
|
|
170
|
+
return { output: JSON.stringify(handoff ?? envelopeRecord.envelope), ...(latest.provider_session_id ? { sessionId: latest.provider_session_id } : {}) };
|
|
171
|
+
}
|
|
172
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
signal.removeEventListener("abort", abortPhase);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
raiseQuestion: ({ envelope, envelopeId, attempt, error }) => {
|
|
180
|
+
const current = readLane(hqRoot, laneId);
|
|
181
|
+
const data = envelope;
|
|
182
|
+
raiseLaneQuestion({
|
|
183
|
+
hqRoot,
|
|
184
|
+
lane: current,
|
|
185
|
+
raisedVia: "cli",
|
|
186
|
+
at: ctx.now().toISOString(),
|
|
187
|
+
body: {
|
|
188
|
+
question: `Loop phase ${String(data.phase ?? envelopeId)} stalled twice. How should it proceed?`,
|
|
189
|
+
context: `${error}\nThe phase is parked; later envelopes remain queued. Stall attempt: ${attempt}.`,
|
|
190
|
+
options: ["Retry with a longer deadline", "Reroute to another worker", "Drop this phase and block the story"],
|
|
191
|
+
recommended: "Retry with a longer deadline",
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
if (result.state === "stopped") {
|
|
197
|
+
const stopped = retryUpdateLane(hqRoot, laneId, "command", (current) => ({
|
|
198
|
+
...current,
|
|
199
|
+
state: "done",
|
|
200
|
+
queued_reason: undefined,
|
|
201
|
+
queued_details: undefined,
|
|
202
|
+
admission_reserved_at: undefined,
|
|
203
|
+
terminal_reason: "loop_stopped",
|
|
204
|
+
}));
|
|
205
|
+
await deps.emitTerminal(stopped);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
process.removeListener("SIGTERM", onSignal);
|
|
210
|
+
process.removeListener("SIGINT", onSignal);
|
|
211
|
+
clearLoopControllerPid(runDir, laneId, process.pid);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=lanes-loop.js.map
|
|
@@ -291,7 +291,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
291
291
|
const body = parseOutpostQuestionBody(response.body, outpostId, question.question_id);
|
|
292
292
|
return { question, audience, body };
|
|
293
293
|
}
|
|
294
|
-
async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
|
|
294
|
+
async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter, questionId, visibilityFilter) {
|
|
295
295
|
const hostedLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === "outpost" &&
|
|
296
296
|
!laneIsHostedHere(ctx, lane) &&
|
|
297
297
|
(!companyFilter ||
|
|
@@ -356,7 +356,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
356
356
|
};
|
|
357
357
|
}
|
|
358
358
|
try {
|
|
359
|
-
const remoteQuestions = await readOutpostQuestionRecords(ctx, outpostId, group.company);
|
|
359
|
+
const remoteQuestions = await readOutpostQuestionRecords(ctx, outpostId, group.company, questionId, visibilityFilter);
|
|
360
360
|
const localLanesById = new Map(group.lanes.map((lane) => [lane.lane_id, lane]));
|
|
361
361
|
const groupQuestions = [];
|
|
362
362
|
const seenQuestionIds = new Set();
|
|
@@ -595,8 +595,32 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
595
595
|
}
|
|
596
596
|
return true;
|
|
597
597
|
});
|
|
598
|
+
const deliveredLocalQuestions = new Map();
|
|
599
|
+
const currentSessionId = sessionIdFromEnv(ctx.env);
|
|
600
|
+
if (currentSessionId) {
|
|
601
|
+
const deliveredAt = ctx.now().toISOString();
|
|
602
|
+
for (const question of localQuestions) {
|
|
603
|
+
if (question.status !== "pending")
|
|
604
|
+
continue;
|
|
605
|
+
const audience = resolveAudience(hqRoot, question, ctx);
|
|
606
|
+
if (audience.kind !== "session" || audience.id !== currentSessionId)
|
|
607
|
+
continue;
|
|
608
|
+
const delivered = casUpdateQuestion(hqRoot, question.question_id, (current) => current.status !== "pending" ||
|
|
609
|
+
(current.seen_at && current.seen_session_id === currentSessionId)
|
|
610
|
+
? current
|
|
611
|
+
: { ...current, seen_at: deliveredAt, seen_session_id: currentSessionId });
|
|
612
|
+
deliveredLocalQuestions.set(question.question_id, delivered);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
598
615
|
const outpostLoad = opts.allHosts
|
|
599
|
-
? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company
|
|
616
|
+
? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company, undefined, {
|
|
617
|
+
sessionId: currentSessionId ?? undefined,
|
|
618
|
+
for: opts.for,
|
|
619
|
+
from: opts.from,
|
|
620
|
+
project: opts.project,
|
|
621
|
+
story: opts.story,
|
|
622
|
+
status: opts.status,
|
|
623
|
+
})
|
|
600
624
|
: { questions: [], errors: [] };
|
|
601
625
|
for (const owner of outpostLoad.questions) {
|
|
602
626
|
assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
|
|
@@ -607,12 +631,13 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
607
631
|
...localQuestions
|
|
608
632
|
.filter((q) => !remoteQuestionIds.has(q.question_id))
|
|
609
633
|
.map((q) => {
|
|
634
|
+
const visibleQuestion = deliveredLocalQuestions.get(q.question_id) ?? q;
|
|
610
635
|
const row = {
|
|
611
|
-
...
|
|
612
|
-
priority:
|
|
613
|
-
priority_display:
|
|
614
|
-
audience: resolveAudience(hqRoot,
|
|
615
|
-
...(
|
|
636
|
+
...visibleQuestion,
|
|
637
|
+
priority: visibleQuestion.priority ?? null,
|
|
638
|
+
priority_display: visibleQuestion.priority ?? "-",
|
|
639
|
+
audience: resolveAudience(hqRoot, visibleQuestion, ctx),
|
|
640
|
+
...(visibleQuestion.question_kind === "continue" ? { flag: "continue-loop" } : {}),
|
|
616
641
|
};
|
|
617
642
|
if (!opts.allHosts)
|
|
618
643
|
return row;
|
|
@@ -662,7 +687,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
662
687
|
catch (err) {
|
|
663
688
|
if (!isLanesError(err) || err.error !== "question_not_found")
|
|
664
689
|
throw err;
|
|
665
|
-
const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
|
|
690
|
+
const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, undefined, questionId);
|
|
666
691
|
if (lookup.errors.length > 0) {
|
|
667
692
|
throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
|
|
668
693
|
question_id: questionId,
|
|
@@ -717,7 +742,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
717
742
|
if (!isLanesError(err) || err.error !== "question_not_found")
|
|
718
743
|
throw err;
|
|
719
744
|
const company = explicitCompany ?? await resolveActiveQuestionCompany(ctx, "Cannot look up this lane question because the active HQ company could not be resolved; re-run with --company <slug>");
|
|
720
|
-
const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company);
|
|
745
|
+
const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company, questionId);
|
|
721
746
|
const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
|
|
722
747
|
if (matches.length === 0 && lookup.errors.length > 0) {
|
|
723
748
|
throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
|
|
@@ -726,7 +751,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
726
751
|
});
|
|
727
752
|
}
|
|
728
753
|
if (matches.length === 0) {
|
|
729
|
-
const allCompanyLookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
|
|
754
|
+
const allCompanyLookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, undefined, questionId);
|
|
730
755
|
const otherCompanyMatch = allCompanyLookup.questions.find((entry) => entry.question.question_id === questionId);
|
|
731
756
|
if (otherCompanyMatch) {
|
|
732
757
|
assertQuestionCompanyAccess(otherCompanyMatch.question, otherCompanyMatch.lane, company, hqRoot);
|
|
@@ -818,6 +843,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
818
843
|
company: recordedCompany,
|
|
819
844
|
body: nextBody,
|
|
820
845
|
status: "answered",
|
|
846
|
+
delivered_at: null,
|
|
821
847
|
answer: {
|
|
822
848
|
choice,
|
|
823
849
|
answer_kind: freeform ? "freeform" : "option",
|
|
@@ -91,6 +91,9 @@ export function createLaneResumeHandlers(deps) {
|
|
|
91
91
|
const hqRoot = ctx.hqRoot();
|
|
92
92
|
const resolution = await deps.resolveCommandLane(ctx, laneId);
|
|
93
93
|
let lane = resolution.lane;
|
|
94
|
+
if (opts.freshThread === true && lane.loop_mode !== true) {
|
|
95
|
+
throw new LanesError("fresh_thread_loop_only", "--fresh-thread is reserved for loop-lane phases", { lane_id: laneId });
|
|
96
|
+
}
|
|
94
97
|
const failedResumeReason = opts.reason?.trim();
|
|
95
98
|
if (opts.reason !== undefined && opts.fromFailed !== true) {
|
|
96
99
|
throw new LanesError("failed_resume_reason_requires_flag", "--reason on hq lanes resume requires --from-failed", { lane_id: laneId });
|
|
@@ -153,6 +156,9 @@ export function createLaneResumeHandlers(deps) {
|
|
|
153
156
|
throw new LanesError("effort_resume_unsupported", `Provider ${lane.provider} cannot change effort on a resumed session`, { lane_id: laneId, provider: lane.provider });
|
|
154
157
|
}
|
|
155
158
|
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
159
|
+
if (opts.loopSessionId !== undefined && opts.loopSessionId !== lane.provider_session_id) {
|
|
160
|
+
lane = casUpdateLane(hqRoot, laneId, (current) => ({ ...current, provider_session_id: opts.loopSessionId }));
|
|
161
|
+
}
|
|
156
162
|
if (opts.model !== undefined && opts.account === undefined) {
|
|
157
163
|
throw new LanesError("model_requires_account", "--model on hq lanes resume is only accepted together with --account", {});
|
|
158
164
|
}
|
|
@@ -233,7 +239,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
233
239
|
}
|
|
234
240
|
const refusal = queuedProfileRecovery || lane.state === "awaiting_input"
|
|
235
241
|
? null
|
|
236
|
-
: resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true || isFailedQuotaKeepAliveLane(lane) });
|
|
242
|
+
: resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true || isFailedQuotaKeepAliveLane(lane), allowFresh: opts.freshThread === true });
|
|
237
243
|
if (refusal !== null) {
|
|
238
244
|
throw new LanesError(refusal.error, refusal.message, {
|
|
239
245
|
lane_id: laneId,
|
|
@@ -435,6 +441,8 @@ export function createLaneResumeHandlers(deps) {
|
|
|
435
441
|
...(deliveryText !== undefined ? { text: deliveryText } : {}),
|
|
436
442
|
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
437
443
|
compact: opts.compact !== false,
|
|
444
|
+
...(opts.freshThread === true ? { fresh_thread: true } : {}),
|
|
445
|
+
...(opts.workingDirectory ? { working_directory: opts.workingDirectory } : {}),
|
|
438
446
|
},
|
|
439
447
|
});
|
|
440
448
|
lane = move.lane;
|
|
@@ -469,6 +477,8 @@ export function createLaneResumeHandlers(deps) {
|
|
|
469
477
|
...(deliveryText !== undefined ? { text: deliveryText } : {}),
|
|
470
478
|
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
471
479
|
compact: opts.compact !== false,
|
|
480
|
+
...(opts.freshThread === true ? { fresh_thread: true } : {}),
|
|
481
|
+
...(opts.workingDirectory ? { working_directory: opts.workingDirectory } : {}),
|
|
472
482
|
},
|
|
473
483
|
accountSelection: selection,
|
|
474
484
|
});
|
|
@@ -496,6 +506,8 @@ export function createLaneResumeHandlers(deps) {
|
|
|
496
506
|
...(deliveryText !== undefined ? { text: deliveryText } : {}),
|
|
497
507
|
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
498
508
|
compact: opts.compact !== false,
|
|
509
|
+
...(opts.freshThread === true ? { fresh_thread: true } : {}),
|
|
510
|
+
...(opts.workingDirectory ? { working_directory: opts.workingDirectory } : {}),
|
|
499
511
|
},
|
|
500
512
|
});
|
|
501
513
|
lane = move.lane;
|
|
@@ -535,6 +547,8 @@ export function createLaneResumeHandlers(deps) {
|
|
|
535
547
|
...(deliveryText !== undefined ? { text: deliveryText } : {}),
|
|
536
548
|
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
537
549
|
compact: opts.compact !== false,
|
|
550
|
+
...(opts.freshThread === true ? { fresh_thread: true } : {}),
|
|
551
|
+
...(opts.workingDirectory ? { working_directory: opts.workingDirectory } : {}),
|
|
538
552
|
at: quotaHoldAt,
|
|
539
553
|
};
|
|
540
554
|
resumed = markSpawnFailure(hqRoot, laneId, "account_quota_exhausted", {
|
|
@@ -590,6 +604,8 @@ export function createLaneResumeHandlers(deps) {
|
|
|
590
604
|
...(deliveryText !== undefined ? { text: deliveryText } : {}),
|
|
591
605
|
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
592
606
|
compact: opts.compact !== false,
|
|
607
|
+
...(opts.freshThread === true ? { fresh_thread: true } : {}),
|
|
608
|
+
...(opts.workingDirectory ? { working_directory: opts.workingDirectory } : {}),
|
|
593
609
|
at,
|
|
594
610
|
},
|
|
595
611
|
};
|
|
@@ -610,10 +626,12 @@ export function createLaneResumeHandlers(deps) {
|
|
|
610
626
|
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
611
627
|
runCompaction: ctx.runCompaction,
|
|
612
628
|
locateTranscript: ctx.locateTranscript,
|
|
629
|
+
...(opts.workingDirectory ? { workingDirectory: opts.workingDirectory } : {}),
|
|
613
630
|
}, {
|
|
614
631
|
trigger: {
|
|
615
632
|
kind: "resume",
|
|
616
633
|
from: resumedFrom,
|
|
634
|
+
...(opts.freshThread === true ? { fresh: true } : {}),
|
|
617
635
|
...(resumedFrom === "awaiting_input" && pendingQuestions.length === 0 ? { allowAwaitingInput: true } : {}),
|
|
618
636
|
...(opts.fromFailed === true ? { fromFailed: true } : {}),
|
|
619
637
|
...(failedResumeExpectedSenior !== undefined
|
|
@@ -7,16 +7,11 @@ export interface SessionStartOptions {
|
|
|
7
7
|
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
8
8
|
stderr?: Pick<NodeJS.WriteStream, "write">;
|
|
9
9
|
resolveRoot?: (env: NodeJS.ProcessEnv, cwd?: string) => string;
|
|
10
|
-
startSweep?: (root: string, env: NodeJS.ProcessEnv
|
|
11
|
-
}
|
|
12
|
-
interface SessionStartContext {
|
|
13
|
-
sessionId?: string;
|
|
14
|
-
companySlug?: string;
|
|
10
|
+
startSweep?: (root: string, env: NodeJS.ProcessEnv) => void;
|
|
15
11
|
}
|
|
16
12
|
/** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
|
|
17
|
-
export declare function startSessionStartOrphanSweep(root: string, env?: NodeJS.ProcessEnv, spawnDetached?: typeof spawn
|
|
13
|
+
export declare function startSessionStartOrphanSweep(root: string, env?: NodeJS.ProcessEnv, spawnDetached?: typeof spawn): void;
|
|
18
14
|
/** SessionStart-compatible, fail-open conduct bootstrap. */
|
|
19
15
|
export declare function runLaneSessionStart(options?: SessionStartOptions): Promise<void>;
|
|
20
16
|
export declare function registerLaneSessionStartCommand(lanes: Command): void;
|
|
21
|
-
export {};
|
|
22
17
|
//# sourceMappingURL=lanes-session-start.d.ts.map
|
|
@@ -27,22 +27,6 @@ function sessionWasDisabled(root, env, payloadSessionId) {
|
|
|
27
27
|
return /^\s*(?:conduct_mode|conduct_enabled|conduct_default):\s*(?:off|false|no|0)\s*(?:#.*)?$/mi.test(text) ||
|
|
28
28
|
/^\s*conduct_engine:\s*(?:""|''|#.*)?\s*$/mi.test(text);
|
|
29
29
|
}
|
|
30
|
-
function sessionStartContext(root, env, payloadSessionId) {
|
|
31
|
-
const sessionId = payloadSessionId || env.HQ_SESSION_ID;
|
|
32
|
-
let companySlug = "";
|
|
33
|
-
if (sessionId && /^[A-Za-z0-9_-]+$/.test(sessionId)) {
|
|
34
|
-
const meta = path.join(root, "workspace", "sessions", sessionId, "meta.yaml");
|
|
35
|
-
if (fs.existsSync(meta)) {
|
|
36
|
-
const text = fs.readFileSync(meta, "utf8");
|
|
37
|
-
companySlug = /^\s*company_slug:\s*["']?([A-Za-z0-9][A-Za-z0-9-]*)["']?\s*(?:#.*)?$/mi.exec(text)?.[1] ?? "";
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (!companySlug)
|
|
41
|
-
companySlug = env.HQ_COMPANY_SLUG?.trim() ?? "";
|
|
42
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(companySlug))
|
|
43
|
-
companySlug = "";
|
|
44
|
-
return { ...(sessionId ? { sessionId } : {}), ...(companySlug ? { companySlug } : {}) };
|
|
45
|
-
}
|
|
46
30
|
function extractConductorBlock(core) {
|
|
47
31
|
const start = core.indexOf("<!-- inject:start -->");
|
|
48
32
|
const end = core.indexOf("<!-- inject:end -->");
|
|
@@ -52,15 +36,12 @@ function extractConductorBlock(core) {
|
|
|
52
36
|
}
|
|
53
37
|
const ORPHAN_SWEEP_LOCK_TTL_MS = 15 * 60_000;
|
|
54
38
|
/** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
|
|
55
|
-
export function startSessionStartOrphanSweep(root, env = process.env, spawnDetached = spawn
|
|
56
|
-
// The orphan command resolves company membership when --company is absent;
|
|
57
|
-
// skip here if this session has no tenant context rather than risk ambiguity.
|
|
58
|
-
if (!context.companySlug)
|
|
59
|
-
return;
|
|
39
|
+
export function startSessionStartOrphanSweep(root, env = process.env, spawnDetached = spawn) {
|
|
60
40
|
const stateRoot = env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
41
|
+
const host = os.hostname().replace(/[^A-Za-z0-9._-]/g, "_") || "unknown-host";
|
|
42
|
+
const stateDir = path.join(stateRoot, "hq-cli", "lanes-session-start", host);
|
|
43
|
+
const lockPath = path.join(stateDir, "orphan-sweep.lock");
|
|
44
|
+
const reportDir = path.join(stateRoot, "hq-cli", "lanes-orphan-reports", host);
|
|
64
45
|
const ownerToken = randomUUID();
|
|
65
46
|
try {
|
|
66
47
|
fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
@@ -108,17 +89,17 @@ export function startSessionStartOrphanSweep(root, env = process.env, spawnDetac
|
|
|
108
89
|
}
|
|
109
90
|
const wrapper = [
|
|
110
91
|
"const cp=require('node:child_process'),fs=require('node:fs'),path=require('node:path');",
|
|
111
|
-
"const [cli,lock,root,token,report
|
|
92
|
+
"const [cli,lock,root,token,report]=process.argv.slice(1);",
|
|
112
93
|
"const cleanup=()=>{try{const lease=JSON.parse(fs.readFileSync(lock,'utf8'));if(lease.owner_token===token)fs.rmSync(lock,{force:true})}catch{}};",
|
|
113
94
|
"const start=()=>{let lease;try{lease=JSON.parse(fs.readFileSync(lock,'utf8'))}catch{}if(lease?.owner_token!==token){if(Date.now()<Number(lease?.started_at||0)+1000)return setTimeout(start,10);process.exit(0)}",
|
|
114
95
|
"try{fs.writeFileSync(lock,JSON.stringify({...lease,pid:process.pid}))}catch{}",
|
|
115
96
|
"const tmp=report+'.tmp';let output;try{output=fs.createWriteStream(tmp,{flags:'wx',mode:0o600})}catch{cleanup();process.exit(0)}",
|
|
116
|
-
"const child=cp.spawn(process.execPath,[cli,'lanes','
|
|
97
|
+
"const child=cp.spawn(process.execPath,[cli,'lanes','_reap','--json'],{stdio:['ignore','pipe','ignore'],env:{...process.env,HQ_ROOT:root,HQ_NO_UPDATE_CHECK:'1'}});",
|
|
117
98
|
"child.stdout.pipe(output);child.on('error',()=>{output.destroy();try{fs.rmSync(tmp,{force:true})}catch{}cleanup()});",
|
|
118
99
|
"child.on('exit',(code)=>output.end(()=>{try{if(code===0)fs.renameSync(tmp,report);else fs.rmSync(tmp,{force:true})}catch{}cleanup()}));};start();",
|
|
119
100
|
].join("");
|
|
120
101
|
const reportPath = path.join(reportDir, `orphans-${startedAt}-${ownerToken}.json`);
|
|
121
|
-
const launcher = spawnDetached(process.execPath, ["-e", wrapper, cli, lockPath, root, ownerToken, reportPath
|
|
102
|
+
const launcher = spawnDetached(process.execPath, ["-e", wrapper, cli, lockPath, root, ownerToken, reportPath], {
|
|
122
103
|
detached: true,
|
|
123
104
|
stdio: "ignore",
|
|
124
105
|
env: { ...env, HQ_ROOT: root, HQ_NO_UPDATE_CHECK: "1" },
|
|
@@ -185,8 +166,7 @@ export async function runLaneSessionStart(options = {}) {
|
|
|
185
166
|
});
|
|
186
167
|
if (!flagEnabled)
|
|
187
168
|
return;
|
|
188
|
-
|
|
189
|
-
(options.startSweep ?? ((r, e, c) => startSessionStartOrphanSweep(r, e, spawn, c)))(root, env, context);
|
|
169
|
+
(options.startSweep ?? ((r, e) => startSessionStartOrphanSweep(r, e, spawn)))(root, env);
|
|
190
170
|
if (sessionWasDisabled(root, env, payloadSessionId))
|
|
191
171
|
return;
|
|
192
172
|
let enabled = enabledFromSettings(root);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LaneCompanyRef, LanePlacementPolicy, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneHireLocks } from "../lib/lanes/index.js";
|
|
1
|
+
import type { LaneCompanyRef, LanePlacementPolicy, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneAdmissionLock, withLaneHireLocks } from "../lib/lanes/index.js";
|
|
2
2
|
import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
3
3
|
import type { OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
4
4
|
import type { CpuBusySource } from "../lib/lanes/throttle.js";
|
|
@@ -77,6 +77,10 @@ export interface LaneResumeOptions {
|
|
|
77
77
|
effort?: string;
|
|
78
78
|
worker?: string;
|
|
79
79
|
keepAlive?: boolean;
|
|
80
|
+
freshThread?: boolean;
|
|
81
|
+
workingDirectory?: string;
|
|
82
|
+
/** Internal loop runner session; selects this story's saved provider session. */
|
|
83
|
+
loopSessionId?: string;
|
|
80
84
|
meshCompany?: string;
|
|
81
85
|
profileRepinContext?: string;
|
|
82
86
|
grantContext?: string;
|
|
@@ -313,6 +317,7 @@ export interface Ctx {
|
|
|
313
317
|
storyLedgerEnabled: () => boolean;
|
|
314
318
|
isSsmOwnerDispatch: () => boolean;
|
|
315
319
|
withGrantHireLocks: typeof withLaneHireLocks;
|
|
320
|
+
withAdmissionLock: typeof withLaneAdmissionLock;
|
|
316
321
|
mergeHoldEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
317
322
|
mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
318
323
|
}
|
|
@@ -457,7 +462,14 @@ export interface LaneQuestionsHandlerDeps {
|
|
|
457
462
|
questionCompanyForLane(question: QuestionRecord, lane: Pick<LaneRecord, "company" | "mesh_company" | "project_id" | "lane_id">, hqRoot?: string): LaneCompanyRef | null;
|
|
458
463
|
questionCompanyRefMatches(recordCompany: LaneCompanyRef | null | undefined, filter: LaneCompanyRef): boolean;
|
|
459
464
|
questionListCompanyArg(company: LaneCompanyRef | null): string | undefined;
|
|
460
|
-
readOutpostQuestionRecords(ctx: Ctx, outpostId: string, company: LaneCompanyRef | null
|
|
465
|
+
readOutpostQuestionRecords(ctx: Ctx, outpostId: string, company: LaneCompanyRef | null, questionId?: string, visibilityFilter?: {
|
|
466
|
+
sessionId?: string;
|
|
467
|
+
for?: string;
|
|
468
|
+
from?: string;
|
|
469
|
+
project?: string;
|
|
470
|
+
story?: string;
|
|
471
|
+
status?: string;
|
|
472
|
+
}): Promise<OutpostQuestionRow[]>;
|
|
461
473
|
readQuestionBody(hqRoot: string, question: QuestionRecord): QuestionBody | null;
|
|
462
474
|
printJson(value: unknown): void;
|
|
463
475
|
readSessionMeta(hqRoot: string, sessionId: string, ctx: Ctx): Record<string, unknown> | null;
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/
|
|
|
17
17
|
export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
18
18
|
import { type OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
19
19
|
import { emitLaneDecisionEvents } from "../lib/lanes/mesh-events.js";
|
|
20
|
-
import { MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
20
|
+
import { MAX_LANE_DEPTH, withLaneAdmissionLock, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
21
21
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
22
22
|
import { type RunCompactionFn } from "../lib/lanes/compaction.js";
|
|
23
23
|
import type { WatcherLoopDeps } from "../lib/lanes/watcher.js";
|
|
@@ -106,6 +106,8 @@ export interface LanesCommandDeps {
|
|
|
106
106
|
isSsmOwnerDispatch?: () => boolean;
|
|
107
107
|
/** Test seam for changing a lane between pre-lock and locked grant reads. */
|
|
108
108
|
withGrantHireLocks?: typeof withLaneHireLocks;
|
|
109
|
+
/** Test seam proving create admission encloses the worker busy check. */
|
|
110
|
+
withAdmissionLock?: typeof withLaneAdmissionLock;
|
|
109
111
|
/** Test seam for the review-before-merge registry flag. */
|
|
110
112
|
mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
111
113
|
/** Test seam for the repository-merge-hold registry flag. */
|