@indigoai-us/hq-cli 5.276.0 → 5.278.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -0
- package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
- package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
- package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
- package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
- package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
- package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
- package/dist/command-catalog.generated.d.ts +239 -1
- package/dist/command-catalog.generated.js +302 -1
- package/dist/commands/core-delegate.d.ts +6 -0
- package/dist/commands/core-delegate.js +987 -0
- package/dist/commands/core.js +10 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.js +173 -1
- package/dist/commands/dm.d.ts +2 -0
- package/dist/commands/dm.js +11 -4
- package/dist/commands/lanes-accounts.js +44 -2
- package/dist/commands/lanes-message.js +69 -12
- package/dist/commands/lanes-resume.js +2 -0
- package/dist/commands/lanes-shared.d.ts +3 -0
- package/dist/commands/lanes-shared.js +12 -2
- package/dist/commands/lanes.d.ts +5 -1
- package/dist/commands/lanes.js +399 -47
- package/dist/commands/secrets.d.ts +16 -1
- package/dist/commands/secrets.js +77 -48
- package/dist/commands/sync-mode.d.ts +11 -0
- package/dist/commands/sync-mode.js +30 -0
- package/dist/fast-scaffold.d.ts +17 -0
- package/dist/fast-scaffold.js +91 -0
- package/dist/index.js +17 -1
- package/dist/lanes-drain-hook.js +154 -7
- package/dist/lib/agent-kit/run/inbox.js +3 -2
- package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
- package/dist/lib/checkpoint-ontology/apply.js +111 -3
- package/dist/lib/checkpoint-ontology/plan.d.ts +23 -0
- package/dist/lib/checkpoint-ontology/plan.js +23 -0
- package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
- package/dist/lib/checkpoint-ontology/tree.js +34 -0
- package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
- package/dist/lib/core-utils/jq-json-serializer.js +10 -0
- package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
- package/dist/lib/core-utils/secret-patterns.js +33 -0
- package/dist/lib/daemon/catalog.d.ts +18 -2
- package/dist/lib/daemon/catalog.js +18 -2
- package/dist/lib/daemon/config.d.ts +6 -0
- package/dist/lib/daemon/config.js +15 -2
- package/dist/lib/daemon/requests.d.ts +4 -0
- package/dist/lib/daemon/requests.js +2 -0
- package/dist/lib/daemon/run.js +94 -8
- package/dist/lib/daemon/supervisor.d.ts +8 -0
- package/dist/lib/daemon/supervisor.js +101 -1
- package/dist/lib/daemon/sync-runner.d.ts +58 -0
- package/dist/lib/daemon/sync-runner.js +277 -0
- package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
- package/dist/lib/doctor/__testing__/run-doctor.js +52 -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/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +46 -8
- package/dist/lib/lanes/config.d.ts +2 -1
- package/dist/lib/lanes/config.js +31 -0
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
- package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +3 -0
- package/dist/lib/lanes/keep-alive.js +3 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
- package/dist/lib/lanes/outpost-dispatch.js +11 -2
- package/dist/lib/lanes/reconcile.d.ts +9 -0
- package/dist/lib/lanes/reconcile.js +127 -2
- package/dist/lib/lanes/runaway-tool-guard.d.ts +38 -0
- package/dist/lib/lanes/runaway-tool-guard.js +284 -0
- package/dist/lib/lanes/spawn.js +17 -10
- package/dist/lib/lanes/store.js +45 -1
- package/dist/lib/lanes/types.d.ts +37 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.d.ts +2 -0
- package/dist/lib/lanes/watcher.js +13 -2
- package/dist/secrets-env-fast-path.d.ts +5 -0
- package/dist/secrets-env-fast-path.js +16 -0
- package/dist/secrets-env-fast-route.d.ts +18 -0
- package/dist/secrets-env-fast-route.js +54 -0
- package/package.json +2 -2
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { readProcStarttime } from "./spawn.js";
|
|
6
|
+
export const RUNAWAY_TOOL_GUARD_FLAG = "lanes.runaway-tool-guard";
|
|
7
|
+
const SEARCH_COMMANDS = new Set(["rg", "grep", "ugrep", "ag", "find"]);
|
|
8
|
+
const VALUE_OPTIONS = new Set([
|
|
9
|
+
"-A", "--after-context", "-B", "--before-context", "-C", "--context",
|
|
10
|
+
"-e", "--regexp", "-f", "--file", "-g", "--glob", "-t", "--type",
|
|
11
|
+
"-T", "--type-not", "-M", "--max-count", "-m", "--max-count",
|
|
12
|
+
"-j", "--threads", "--ignore-file", "--include", "--exclude",
|
|
13
|
+
"--exclude-dir", "--exclude-from", "--encoding", "--sort",
|
|
14
|
+
]);
|
|
15
|
+
function commandName(command) {
|
|
16
|
+
return path.basename(command).toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
function hasNoIgnoreFlag(command, args) {
|
|
19
|
+
if (command === "rg" || command === "ag") {
|
|
20
|
+
return args.some((arg) => ["--no-ignore", "--no-ignore-vcs", "--unrestricted", "--skip-vcs-ignores"].includes(arg) ||
|
|
21
|
+
/^-[A-Za-z]*u[A-Za-z]*$/.test(arg));
|
|
22
|
+
}
|
|
23
|
+
if (command === "grep" || command === "ugrep") {
|
|
24
|
+
return args.some((arg) => ["--recursive", "--dereference-recursive"].includes(arg) ||
|
|
25
|
+
/^-[A-Za-z]*[rR][A-Za-z]*$/.test(arg));
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function findDepth(args) {
|
|
30
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
31
|
+
if (args[index] !== "-maxdepth")
|
|
32
|
+
continue;
|
|
33
|
+
const value = Number(args[index + 1]);
|
|
34
|
+
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function positionalArgs(command, args) {
|
|
39
|
+
if (command === "find") {
|
|
40
|
+
let index = 0;
|
|
41
|
+
while (index < args.length) {
|
|
42
|
+
const arg = args[index];
|
|
43
|
+
if (["-H", "-L", "-P"].includes(arg) || /^-O\d+$/.test(arg)) {
|
|
44
|
+
index += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (arg === "-D") {
|
|
48
|
+
index += 2;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (/^-D.+/.test(arg)) {
|
|
52
|
+
index += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
const paths = [];
|
|
58
|
+
for (; index < args.length; index += 1) {
|
|
59
|
+
const arg = args[index];
|
|
60
|
+
if (arg.startsWith("-"))
|
|
61
|
+
break;
|
|
62
|
+
paths.push(arg);
|
|
63
|
+
}
|
|
64
|
+
return paths;
|
|
65
|
+
}
|
|
66
|
+
const positionals = [];
|
|
67
|
+
let explicitPattern = false;
|
|
68
|
+
let afterTerminator = false;
|
|
69
|
+
let listFiles = false;
|
|
70
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
71
|
+
const arg = args[index];
|
|
72
|
+
if (afterTerminator) {
|
|
73
|
+
positionals.push(arg);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg === "--") {
|
|
77
|
+
afterTerminator = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (arg === "--files") {
|
|
81
|
+
listFiles = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (VALUE_OPTIONS.has(arg)) {
|
|
85
|
+
if (["-e", "--regexp", "-f", "--file"].includes(arg))
|
|
86
|
+
explicitPattern = true;
|
|
87
|
+
index += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (arg.startsWith("--")) {
|
|
91
|
+
if (arg.startsWith("--regexp=") || arg.startsWith("--file="))
|
|
92
|
+
explicitPattern = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (arg.startsWith("-") && arg.length > 1) {
|
|
96
|
+
if (/^-e.+$/.test(arg) || /^-f.+$/.test(arg))
|
|
97
|
+
explicitPattern = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
positionals.push(arg);
|
|
101
|
+
}
|
|
102
|
+
if (listFiles || explicitPattern)
|
|
103
|
+
return positionals;
|
|
104
|
+
return positionals.slice(1);
|
|
105
|
+
}
|
|
106
|
+
function canonicalPath(candidate) {
|
|
107
|
+
try {
|
|
108
|
+
return fs.realpathSync(candidate);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return path.resolve(candidate);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function searchesBroadRoot(hqRoot, processInfo, command, args) {
|
|
115
|
+
const roots = new Set([
|
|
116
|
+
canonicalPath(hqRoot),
|
|
117
|
+
canonicalPath(path.join(hqRoot, "workspace")),
|
|
118
|
+
canonicalPath(os.homedir()),
|
|
119
|
+
path.parse(path.resolve("/")).root,
|
|
120
|
+
]);
|
|
121
|
+
const operands = positionalArgs(command, args);
|
|
122
|
+
const candidates = operands.length > 0
|
|
123
|
+
? operands.map((operand) => path.resolve(processInfo.cwd || ".", operand))
|
|
124
|
+
: [processInfo.cwd || "."];
|
|
125
|
+
return candidates.some((candidate) => roots.has(canonicalPath(candidate)));
|
|
126
|
+
}
|
|
127
|
+
function isUnboundedSearch(hqRoot, processInfo, findMaxdepthExempt) {
|
|
128
|
+
const command = commandName(processInfo.command);
|
|
129
|
+
if (!SEARCH_COMMANDS.has(command))
|
|
130
|
+
return false;
|
|
131
|
+
const args = processInfo.argv.slice(1);
|
|
132
|
+
if (command === "find") {
|
|
133
|
+
if (args.includes("-prune"))
|
|
134
|
+
return false;
|
|
135
|
+
const depth = findDepth(args);
|
|
136
|
+
if (depth !== null && depth <= findMaxdepthExempt)
|
|
137
|
+
return false;
|
|
138
|
+
return searchesBroadRoot(hqRoot, processInfo, command, args);
|
|
139
|
+
}
|
|
140
|
+
if (!hasNoIgnoreFlag(command, args))
|
|
141
|
+
return false;
|
|
142
|
+
return searchesBroadRoot(hqRoot, processInfo, command, args);
|
|
143
|
+
}
|
|
144
|
+
function laneAncestor(processInfo, processByPid, laneByPid, currentUid) {
|
|
145
|
+
let parentPid = processInfo.ppid;
|
|
146
|
+
const visited = new Set([processInfo.pid]);
|
|
147
|
+
for (let depth = 0; depth < 128; depth += 1) {
|
|
148
|
+
const laneId = laneByPid.get(parentPid);
|
|
149
|
+
if (laneId)
|
|
150
|
+
return laneId;
|
|
151
|
+
if (parentPid <= 1 || visited.has(parentPid))
|
|
152
|
+
return null;
|
|
153
|
+
visited.add(parentPid);
|
|
154
|
+
const parent = processByPid.get(parentPid);
|
|
155
|
+
if (!parent || parent.uid !== currentUid)
|
|
156
|
+
return null;
|
|
157
|
+
parentPid = parent.ppid;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
function errorName(error) {
|
|
162
|
+
return error instanceof Error ? error.name : "UnknownError";
|
|
163
|
+
}
|
|
164
|
+
export function runRunawayToolGuard(input) {
|
|
165
|
+
const processByPid = new Map();
|
|
166
|
+
for (const processInfo of input.processes) {
|
|
167
|
+
if (Number.isSafeInteger(processInfo.pid) && processInfo.pid > 1) {
|
|
168
|
+
processByPid.set(processInfo.pid, processInfo);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const laneByPid = new Map();
|
|
172
|
+
const ambiguousPids = new Set();
|
|
173
|
+
for (const lane of input.lanes) {
|
|
174
|
+
const agent = processByPid.get(lane.agentPid);
|
|
175
|
+
if (!agent ||
|
|
176
|
+
!Number.isSafeInteger(lane.agentStarttime) ||
|
|
177
|
+
agent.starttime !== lane.agentStarttime ||
|
|
178
|
+
agent.uid !== input.currentUid ||
|
|
179
|
+
agent.pid === agent.ppid)
|
|
180
|
+
continue;
|
|
181
|
+
if (laneByPid.has(lane.agentPid))
|
|
182
|
+
ambiguousPids.add(lane.agentPid);
|
|
183
|
+
else
|
|
184
|
+
laneByPid.set(lane.agentPid, lane.laneId);
|
|
185
|
+
}
|
|
186
|
+
for (const pid of ambiguousPids)
|
|
187
|
+
laneByPid.delete(pid);
|
|
188
|
+
let stopped = 0;
|
|
189
|
+
for (const processInfo of input.processes) {
|
|
190
|
+
if (processInfo.uid !== input.currentUid ||
|
|
191
|
+
!Number.isFinite(processInfo.elapsedSeconds) ||
|
|
192
|
+
processInfo.elapsedSeconds <= input.config.max_age_seconds ||
|
|
193
|
+
processInfo.starttime === null ||
|
|
194
|
+
!isUnboundedSearch(input.hqRoot, processInfo, input.config.find_maxdepth_exempt))
|
|
195
|
+
continue;
|
|
196
|
+
const laneId = laneAncestor(processInfo, processByPid, laneByPid, input.currentUid);
|
|
197
|
+
if (!laneId || !input.isCurrentProcess(processInfo.pid, processInfo.starttime))
|
|
198
|
+
continue;
|
|
199
|
+
try {
|
|
200
|
+
input.signal(processInfo.pid, "SIGTERM");
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error.code !== "ESRCH") {
|
|
204
|
+
input.reportError("signal", errorName(error));
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const command = commandName(processInfo.command);
|
|
209
|
+
const durationSeconds = Math.floor(processInfo.elapsedSeconds);
|
|
210
|
+
stopped += 1;
|
|
211
|
+
try {
|
|
212
|
+
input.recordEvent(laneId, {
|
|
213
|
+
event: "runaway_search_stopped",
|
|
214
|
+
at: input.now.toISOString(),
|
|
215
|
+
pid: processInfo.pid,
|
|
216
|
+
command,
|
|
217
|
+
duration_seconds: durationSeconds,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
input.reportError("record", errorName(error));
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
input.notifyLane(laneId, `runaway-search-guard stopped ${command} after ${durationSeconds}s. Narrow the search to the smallest repo path that holds the answer and keep ignore rules on.`);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
input.reportError("notify", errorName(error));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { stopped };
|
|
231
|
+
}
|
|
232
|
+
/** Read the host process table once. argv and cwd are read only for search tools. */
|
|
233
|
+
export function listHostProcesses() {
|
|
234
|
+
if (process.platform !== "linux")
|
|
235
|
+
return [];
|
|
236
|
+
let output;
|
|
237
|
+
try {
|
|
238
|
+
output = execFileSync("ps", ["-eo", "pid=,ppid=,etimes=,uid=,comm="], {
|
|
239
|
+
encoding: "utf8",
|
|
240
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
241
|
+
timeout: 2_000,
|
|
242
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
243
|
+
windowsHide: true,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
throw new Error("ps process-table read failed");
|
|
248
|
+
}
|
|
249
|
+
const processes = [];
|
|
250
|
+
for (const line of output.split("\n")) {
|
|
251
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(.+)$/);
|
|
252
|
+
if (!match)
|
|
253
|
+
continue;
|
|
254
|
+
const pid = Number(match[1]);
|
|
255
|
+
const ppid = Number(match[2]);
|
|
256
|
+
const elapsedSeconds = Number(match[3]);
|
|
257
|
+
const uid = Number(match[4]);
|
|
258
|
+
const command = match[5].trim();
|
|
259
|
+
if (![pid, ppid, elapsedSeconds, uid].every(Number.isSafeInteger) || !command)
|
|
260
|
+
continue;
|
|
261
|
+
let argv = [];
|
|
262
|
+
let cwd = "";
|
|
263
|
+
let starttime = null;
|
|
264
|
+
if (SEARCH_COMMANDS.has(commandName(command))) {
|
|
265
|
+
try {
|
|
266
|
+
const commandLine = fs.readFileSync(`/proc/${pid}/cmdline`);
|
|
267
|
+
argv = commandLine.toString("utf8").split("\0");
|
|
268
|
+
if (argv.at(-1) === "")
|
|
269
|
+
argv.pop();
|
|
270
|
+
cwd = fs.readlinkSync(`/proc/${pid}/cwd`);
|
|
271
|
+
starttime = readProcStarttime(pid);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
argv = [];
|
|
275
|
+
cwd = "";
|
|
276
|
+
starttime = null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
processes.push({ pid, ppid, elapsedSeconds, uid, command, argv, cwd, starttime });
|
|
280
|
+
}
|
|
281
|
+
return processes;
|
|
282
|
+
}
|
|
283
|
+
export { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG } from "./config.js";
|
|
284
|
+
//# sourceMappingURL=runaway-tool-guard.js.map
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -1726,9 +1726,11 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1726
1726
|
const recordedModel = repairRecordedAccount && existing.model_source === "account"
|
|
1727
1727
|
? existing.model
|
|
1728
1728
|
: undefined;
|
|
1729
|
-
|
|
1729
|
+
// `hq lanes set` on a running lane leaves the change pending until this spawn.
|
|
1730
|
+
const pendingSettings = existing.pending_settings;
|
|
1731
|
+
let pinnedModel = pendingSettings?.model ?? (existing.model_source === undefined || existing.model_source === "cli" || existing.model_source === "preserved"
|
|
1730
1732
|
? existing.model
|
|
1731
|
-
: undefined;
|
|
1733
|
+
: undefined);
|
|
1732
1734
|
const accountDeps = {
|
|
1733
1735
|
probe: modelDeps.probe ?? defaultProbe,
|
|
1734
1736
|
env,
|
|
@@ -1860,13 +1862,15 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
1860
1862
|
providerModel: config.providers[lane.provider].model,
|
|
1861
1863
|
});
|
|
1862
1864
|
const pendingResumeEffort = pendingResume !== undefined
|
|
1863
|
-
?
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1865
|
+
? pendingSettings?.effort !== undefined
|
|
1866
|
+
? { effort: pendingSettings.effort, source: "cli" }
|
|
1867
|
+
: {
|
|
1868
|
+
...(existing.effort !== undefined ? { effort: existing.effort } : {}),
|
|
1869
|
+
source: existing.effort_source ?? "none",
|
|
1870
|
+
}
|
|
1867
1871
|
: undefined;
|
|
1868
1872
|
const effort = launch.effort ?? pendingResumeEffort ?? resolveLaneEffort({
|
|
1869
|
-
cliEffort: existing.effort_source === "cli" ? existing.effort : undefined,
|
|
1873
|
+
cliEffort: pendingSettings?.effort ?? (existing.effort_source === "cli" ? existing.effort : undefined),
|
|
1870
1874
|
profileEffort,
|
|
1871
1875
|
accountEffort: accountCfg?.effort,
|
|
1872
1876
|
providerEffort: config.providers[lane.provider].effort,
|
|
@@ -2026,15 +2030,18 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2026
2030
|
terminal_details: undefined,
|
|
2027
2031
|
// An account move's reservation ends with the claim.
|
|
2028
2032
|
pending_resume: undefined,
|
|
2033
|
+
pending_settings: undefined,
|
|
2029
2034
|
account: resolved.id,
|
|
2030
2035
|
provider_home_at_spawn: resolved.home,
|
|
2031
2036
|
...(model !== undefined ? { model } : {}),
|
|
2032
2037
|
...(model !== undefined
|
|
2033
2038
|
? {
|
|
2034
2039
|
model_source: pinnedModel !== undefined
|
|
2035
|
-
? preservedAccountModel
|
|
2036
|
-
? "
|
|
2037
|
-
: "
|
|
2040
|
+
? preservedAccountModel === undefined && pendingSettings?.model !== undefined
|
|
2041
|
+
? "cli"
|
|
2042
|
+
: preservedAccountModel !== undefined || existing.model_source === "preserved"
|
|
2043
|
+
? "preserved"
|
|
2044
|
+
: "cli"
|
|
2038
2045
|
: profileModel !== undefined
|
|
2039
2046
|
? "profile"
|
|
2040
2047
|
: accountCfg?.model !== undefined
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -475,6 +475,7 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
475
475
|
"keep_alive_next_eligible_at",
|
|
476
476
|
"keep_alive_exhausted_at",
|
|
477
477
|
"interrupted_at",
|
|
478
|
+
"last_run_usage_at",
|
|
478
479
|
]) {
|
|
479
480
|
if (raw[field] !== undefined)
|
|
480
481
|
requireTimestamp(raw[field], field, filePath);
|
|
@@ -694,6 +695,40 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
694
695
|
requireTimestamp(entry.at, `${field}.at`, filePath);
|
|
695
696
|
}
|
|
696
697
|
}
|
|
698
|
+
if (raw.settings_history !== undefined) {
|
|
699
|
+
if (!Array.isArray(raw.settings_history)) {
|
|
700
|
+
throw new LaneSchemaError("settings_history must be an array when present", {
|
|
701
|
+
path: filePath,
|
|
702
|
+
field: "settings_history",
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
for (const [index, entry] of raw.settings_history.entries()) {
|
|
706
|
+
const field = `settings_history[${index}]`;
|
|
707
|
+
if (!isPlainObject(entry) ||
|
|
708
|
+
Object.keys(entry).some((key) => !["at", "actor", "model", "effort", "reason"].includes(key)) ||
|
|
709
|
+
(entry.model === undefined && entry.effort === undefined)) {
|
|
710
|
+
throw new LaneSchemaError(`${field} must be a settings history entry`, { path: filePath, field });
|
|
711
|
+
}
|
|
712
|
+
requireTimestamp(entry.at, `${field}.at`, filePath);
|
|
713
|
+
if (typeof entry.actor !== "string" || entry.actor.trim() === "") {
|
|
714
|
+
throw new LaneSchemaError(`${field}.actor must be a non-empty string`, { path: filePath, field });
|
|
715
|
+
}
|
|
716
|
+
if (entry.reason !== undefined && (typeof entry.reason !== "string" || entry.reason.trim() === "")) {
|
|
717
|
+
throw new LaneSchemaError(`${field}.reason must be a non-empty string when present`, { path: filePath, field });
|
|
718
|
+
}
|
|
719
|
+
for (const key of ["model", "effort"]) {
|
|
720
|
+
const change = entry[key];
|
|
721
|
+
if (change === undefined)
|
|
722
|
+
continue;
|
|
723
|
+
if (!isPlainObject(change) ||
|
|
724
|
+
Object.keys(change).some((k) => k !== "from" && k !== "to") ||
|
|
725
|
+
typeof change.to !== "string" || change.to.trim() === "" ||
|
|
726
|
+
(change.from !== null && typeof change.from !== "string")) {
|
|
727
|
+
throw new LaneSchemaError(`${field}.${key} must be a {from, to} change`, { path: filePath, field });
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
697
732
|
if (raw.worker_profile_repin_history !== undefined) {
|
|
698
733
|
if (!Array.isArray(raw.worker_profile_repin_history)) {
|
|
699
734
|
throw new LaneSchemaError("worker_profile_repin_history must be an array when present", {
|
|
@@ -751,7 +786,7 @@ function parseLaneAccountSelection(value, filePath) {
|
|
|
751
786
|
const selection = value;
|
|
752
787
|
if (typeof selection.account !== "string" || selection.account.trim() === "")
|
|
753
788
|
throw failure("account");
|
|
754
|
-
if (selection.signal !== "usage" && selection.signal !== "lane_count" && selection.signal !== "explicit")
|
|
789
|
+
if (selection.signal !== "usage" && selection.signal !== "recent_usage" && selection.signal !== "lane_count" && selection.signal !== "explicit")
|
|
755
790
|
throw failure("signal");
|
|
756
791
|
if (selection.reason !== "least_loaded" && selection.reason !== "explicit")
|
|
757
792
|
throw failure("reason");
|
|
@@ -773,6 +808,10 @@ function parseLaneAccountSelection(value, filePath) {
|
|
|
773
808
|
throw failure(`considered.${index}`);
|
|
774
809
|
if (row.ineligible_reason !== undefined && typeof row.ineligible_reason !== "string")
|
|
775
810
|
throw failure(`considered.${index}.ineligible_reason`);
|
|
811
|
+
if (row.recent_usage_at !== undefined && row.recent_usage_at !== null &&
|
|
812
|
+
(typeof row.recent_usage_at !== "string" || !Number.isFinite(Date.parse(row.recent_usage_at)))) {
|
|
813
|
+
throw failure(`considered.${index}.recent_usage_at`);
|
|
814
|
+
}
|
|
776
815
|
if (row.usage !== null) {
|
|
777
816
|
if (!isPlainObject(row.usage))
|
|
778
817
|
throw failure(`considered.${index}.usage`);
|
|
@@ -1015,6 +1054,11 @@ export function mergeLaneRecords(live, sidecars) {
|
|
|
1015
1054
|
failed_resume_history: unionHistory(all.map((record) => record.failed_resume_history ?? []), entryIdentity, base.failed_resume_history ?? []),
|
|
1016
1055
|
}
|
|
1017
1056
|
: {}),
|
|
1057
|
+
...(all.some((record) => record.settings_history !== undefined)
|
|
1058
|
+
? {
|
|
1059
|
+
settings_history: unionHistory(all.map((record) => record.settings_history ?? []), entryIdentity, base.settings_history ?? []),
|
|
1060
|
+
}
|
|
1061
|
+
: {}),
|
|
1018
1062
|
...(all.some((record) => record.stop_history !== undefined)
|
|
1019
1063
|
? {
|
|
1020
1064
|
stop_history: unionHistory(all.map((record) => record.stop_history ?? []), entryIdentity, base.stop_history ?? []),
|
|
@@ -253,12 +253,14 @@ export interface LaneAccountSelectionConsidered {
|
|
|
253
253
|
weekly_percent?: number;
|
|
254
254
|
sampled_at?: string;
|
|
255
255
|
} | null;
|
|
256
|
+
/** Latest completed run in the recent-usage window, when lane snapshots are complete. */
|
|
257
|
+
recent_usage_at?: string | null;
|
|
256
258
|
hosts: LaneAccountSelectionHostCount[];
|
|
257
259
|
}
|
|
258
260
|
/** Decision evidence saved with a Claude lane when account balancing is active. */
|
|
259
261
|
export interface LaneAccountSelection {
|
|
260
262
|
account: string;
|
|
261
|
-
signal: "usage" | "lane_count" | "explicit";
|
|
263
|
+
signal: "usage" | "recent_usage" | "lane_count" | "explicit";
|
|
262
264
|
reason: "least_loaded" | "explicit";
|
|
263
265
|
target_host: LaneHost;
|
|
264
266
|
selected_at: string;
|
|
@@ -287,6 +289,20 @@ export interface LaneStopHistoryEntry {
|
|
|
287
289
|
at: string;
|
|
288
290
|
reason: string;
|
|
289
291
|
}
|
|
292
|
+
/** One `hq lanes set` change; it takes effect when the lane next resumes. */
|
|
293
|
+
export interface LaneSettingsHistoryEntry {
|
|
294
|
+
at: string;
|
|
295
|
+
actor: string;
|
|
296
|
+
model?: {
|
|
297
|
+
from: string | null;
|
|
298
|
+
to: string;
|
|
299
|
+
};
|
|
300
|
+
effort?: {
|
|
301
|
+
from: string | null;
|
|
302
|
+
to: string;
|
|
303
|
+
};
|
|
304
|
+
reason?: string;
|
|
305
|
+
}
|
|
290
306
|
/** Evidence for a senior-authorized Outpost worker-profile content re-pin. */
|
|
291
307
|
export interface WorkerProfileRepinHistoryEntry {
|
|
292
308
|
old_hash: string | null;
|
|
@@ -564,6 +580,17 @@ export interface LaneRecord {
|
|
|
564
580
|
terminal_reason?: string;
|
|
565
581
|
/** Reasons supplied to explicit stop commands, retained independently of optional project ledgers. */
|
|
566
582
|
stop_history?: LaneStopHistoryEntry[];
|
|
583
|
+
/** Model and effort changes made with `hq lanes set`. */
|
|
584
|
+
settings_history?: LaneSettingsHistoryEntry[];
|
|
585
|
+
/**
|
|
586
|
+
* Model or effort from `hq lanes set` on a running lane. The running turn
|
|
587
|
+
* keeps its launched values (quota classification reads `model`); the next
|
|
588
|
+
* spawn applies these and clears them.
|
|
589
|
+
*/
|
|
590
|
+
pending_settings?: {
|
|
591
|
+
model?: string;
|
|
592
|
+
effort?: string;
|
|
593
|
+
};
|
|
567
594
|
/** Classification time when this lane entered interrupted; stable across later metadata writes. */
|
|
568
595
|
interrupted_at?: string;
|
|
569
596
|
/**
|
|
@@ -598,6 +625,8 @@ export interface LaneRecord {
|
|
|
598
625
|
last_resume?: LaneResumeRecord;
|
|
599
626
|
/** Cache counters from the last exited run's result. Nothing else from the payload. */
|
|
600
627
|
last_run_usage?: LaneRunUsage;
|
|
628
|
+
/** Completion time paired with last_run_usage; written by the watcher. */
|
|
629
|
+
last_run_usage_at?: string;
|
|
601
630
|
/**
|
|
602
631
|
* Spec §4: one repair turn per expected envelope slot when the final output
|
|
603
632
|
* is invalid or missing. Written before repair spawn and updated on repair
|
|
@@ -862,6 +891,12 @@ export interface TrackerChecksConfig {
|
|
|
862
891
|
kpi_value_max_age_ms: number;
|
|
863
892
|
directive_ack_timeout_ms: number;
|
|
864
893
|
}
|
|
894
|
+
export interface RunawaySearchGuardConfig {
|
|
895
|
+
/** Wall-time threshold before an unbounded descendant search is stopped. */
|
|
896
|
+
max_age_seconds: number;
|
|
897
|
+
/** `find` searches at this depth or less are left alone. */
|
|
898
|
+
find_maxdepth_exempt: number;
|
|
899
|
+
}
|
|
865
900
|
export interface LanesConfig {
|
|
866
901
|
schema_version: typeof CONFIG_SCHEMA_VERSION | string;
|
|
867
902
|
providers: Record<LaneProvider, ProviderConfig>;
|
|
@@ -870,6 +905,7 @@ export interface LanesConfig {
|
|
|
870
905
|
tracker: {
|
|
871
906
|
checks: TrackerChecksConfig;
|
|
872
907
|
};
|
|
908
|
+
runaway_search_guard?: RunawaySearchGuardConfig;
|
|
873
909
|
/** Seconds an unanswered question waits at a session audience before advancing. */
|
|
874
910
|
session_question_timeout_secs: number;
|
|
875
911
|
/** Age in hours after which an open manager blocker must be re-verified. */
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -230,9 +230,9 @@ export const LANE_FIELD_WRITERS = {
|
|
|
230
230
|
last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
|
|
231
231
|
last_message_at: ["command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
|
|
232
232
|
overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
|
|
233
|
-
admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command"], interrupted_at: ["watcher", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
|
|
233
|
+
admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command"], settings_history: ["command"], pending_settings: ["command", "spawn"], interrupted_at: ["command", "watcher", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
|
|
234
234
|
quota_reset_at: ["watcher", "reconcile"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
|
|
235
|
-
provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], last_resume: ["spawn"], last_run_usage: ["watcher"], envelope_repair: ["watcher", "reconcile"], envelope_validation_errors: ["watcher"], last_progress_event_at: ["watcher"],
|
|
235
|
+
provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], last_resume: ["spawn"], last_run_usage: ["watcher"], last_run_usage_at: ["watcher"], envelope_repair: ["watcher", "reconcile"], envelope_validation_errors: ["watcher"], last_progress_event_at: ["watcher"],
|
|
236
236
|
};
|
|
237
237
|
function isPlainObject(value) {
|
|
238
238
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -130,6 +130,8 @@ export declare function watcherEventsPath(hqRoot: string, laneId: string): strin
|
|
|
130
130
|
export declare function watcherOutLogPath(hqRoot: string, laneId: string): string;
|
|
131
131
|
export declare function appendCappedLog(filePath: string, line: string, maxBytes?: number): void;
|
|
132
132
|
export declare function writeWatcherDecision(filePath: string, record: Record<string, unknown>, maxBytes?: number): void;
|
|
133
|
+
/** Append a decision to the shared event stream without a read-modify-write. */
|
|
134
|
+
export declare function appendWatcherEvent(filePath: string, record: Record<string, unknown>, appendLine?: (target: string, line: string) => void): void;
|
|
133
135
|
export declare function watcherLastSignalPath(hqRoot: string, laneId: string): string;
|
|
134
136
|
export declare function appendWatcherSignal(hqRoot: string, laneId: string, signal: WatcherSignal): void;
|
|
135
137
|
/**
|
|
@@ -223,6 +223,16 @@ export function writeWatcherDecision(filePath, record, maxBytes = WATCHER_OUT_MA
|
|
|
223
223
|
}
|
|
224
224
|
appendCappedLog(filePath, JSON.stringify(compact), maxBytes);
|
|
225
225
|
}
|
|
226
|
+
/** Append a decision to the shared event stream without a read-modify-write. */
|
|
227
|
+
export function appendWatcherEvent(filePath, record, appendLine = (target, line) => fs.appendFileSync(target, line, "utf8")) {
|
|
228
|
+
const compact = {};
|
|
229
|
+
for (const [key, value] of Object.entries(record)) {
|
|
230
|
+
if (value !== undefined)
|
|
231
|
+
compact[key] = value;
|
|
232
|
+
}
|
|
233
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
234
|
+
appendLine(filePath, `${JSON.stringify(compact)}\n`);
|
|
235
|
+
}
|
|
226
236
|
export function watcherLastSignalPath(hqRoot, laneId) {
|
|
227
237
|
return path.join(laneRunDir(hqRoot, laneId), "watcher", "last-signal.json");
|
|
228
238
|
}
|
|
@@ -245,7 +255,7 @@ export function notifySlotFreed(_input) {
|
|
|
245
255
|
* or unparseable id is recorded as absent with its reason, never invented.
|
|
246
256
|
* Non-resumable providers return null and the record is left as it was.
|
|
247
257
|
*/
|
|
248
|
-
function runResultFields(hqRoot, lane) {
|
|
258
|
+
function runResultFields(hqRoot, lane, at) {
|
|
249
259
|
const runDir = laneRunDir(hqRoot, lane.lane_id);
|
|
250
260
|
const result = readWorkerRunResult(runDir, lane.provider, readSpawnManifest(runDir).worker_out_offset ?? 0);
|
|
251
261
|
if (result === null)
|
|
@@ -255,6 +265,7 @@ function runResultFields(hqRoot, lane) {
|
|
|
255
265
|
cache_read_input_tokens: result.cache_read_input_tokens,
|
|
256
266
|
cache_creation_input_tokens: result.cache_creation_input_tokens,
|
|
257
267
|
},
|
|
268
|
+
last_run_usage_at: at,
|
|
258
269
|
};
|
|
259
270
|
if (result.session_id !== null) {
|
|
260
271
|
fields.provider_session_id = result.session_id;
|
|
@@ -366,7 +377,7 @@ export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, op
|
|
|
366
377
|
}
|
|
367
378
|
const next = {
|
|
368
379
|
...current,
|
|
369
|
-
...runResultFields(hqRoot, current),
|
|
380
|
+
...runResultFields(hqRoot, current, at),
|
|
370
381
|
state: effectiveClassification.state,
|
|
371
382
|
queued_reason: undefined,
|
|
372
383
|
stalled: undefined,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ParsedSecretsEnvFastRequest } from "./fast-scaffold.js";
|
|
2
|
+
import type { SecretsEnvFastResult } from "./secrets-env-fast-route.js";
|
|
3
|
+
/** Run the shared secret action without importing the full CLI command graph. */
|
|
4
|
+
export declare function runSecretsEnvFastPath(request: ParsedSecretsEnvFastRequest): Promise<SecretsEnvFastResult>;
|
|
5
|
+
//# sourceMappingURL=secrets-env-fast-path.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Run the shared secret action without importing the full CLI command graph. */
|
|
2
|
+
export async function runSecretsEnvFastPath(request) {
|
|
3
|
+
if (process.env.HQ_AGENT_DIR?.trim()) {
|
|
4
|
+
const [os, { agentKitPaths }, { adoptAgentIdentityEnv }] = await Promise.all([
|
|
5
|
+
import("node:os"),
|
|
6
|
+
import("./lib/agent-kit/paths.js"),
|
|
7
|
+
import("./lib/agent-kit/adopt-identity.js"),
|
|
8
|
+
]);
|
|
9
|
+
adoptAgentIdentityEnv(agentKitPaths(os.homedir(), process.env), process.env);
|
|
10
|
+
}
|
|
11
|
+
const { runSecretsEnvCommand } = await import("./commands/secrets.js");
|
|
12
|
+
return runSecretsEnvCommand(request.options, request.scope, {
|
|
13
|
+
fallbackOnAuthFailure: true,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=secrets-env-fast-path.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Default-off hq-flags gate for the `secrets env` entry fast path. */
|
|
2
|
+
import type { ParsedSecretsEnvFastRequest } from "./fast-scaffold.js";
|
|
3
|
+
export declare const CLI_SECRETS_ENV_FAST_PATH_FLAG = "cli.secrets-env-fast-path";
|
|
4
|
+
export type SecretsEnvFastResult = "handled" | "fallback";
|
|
5
|
+
export interface SecretsEnvFastDispatchDeps {
|
|
6
|
+
isEnabled: () => Promise<boolean>;
|
|
7
|
+
runFast: () => Promise<SecretsEnvFastResult>;
|
|
8
|
+
runCurrent: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/** Keep the existing command graph in control when the gate is off or declines. */
|
|
11
|
+
export declare function dispatchSecretsEnvFastRequest(_request: ParsedSecretsEnvFastRequest, deps: SecretsEnvFastDispatchDeps): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Fast routes only use an explicit local override or the latest company flag
|
|
14
|
+
* snapshot. A missing company, stale snapshot, or unreadable cache leaves the
|
|
15
|
+
* current CLI path in control; it never performs a registry network request.
|
|
16
|
+
*/
|
|
17
|
+
export declare function secretsEnvFastRouteEnabled(request: ParsedSecretsEnvFastRequest, env?: NodeJS.ProcessEnv): Promise<boolean>;
|
|
18
|
+
//# sourceMappingURL=secrets-env-fast-route.d.ts.map
|