@indigoai-us/hq-cli 5.253.0 → 5.255.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 +26 -0
- package/dist/command-catalog.generated.d.ts +2 -2
- package/dist/command-catalog.generated.js +2 -2
- package/dist/commands/db-sql.js +7 -21
- package/dist/commands/lanes-checks.js +1 -31
- package/dist/commands/lanes-message.js +3 -0
- package/dist/commands/lanes-resume.js +3 -0
- package/dist/commands/lanes-shared.d.ts +27 -0
- package/dist/commands/lanes-watch.d.ts +7 -0
- package/dist/commands/lanes-watch.js +215 -0
- package/dist/commands/lanes.d.ts +1 -1
- package/dist/commands/lanes.js +36 -211
- package/dist/lib/db/remote-sql.d.ts +2 -2
- package/dist/lib/db/remote-sql.js +3 -3
- package/dist/lib/lanes/checked-company-path.d.ts +3 -0
- package/dist/lib/lanes/checked-company-path.js +39 -0
- package/dist/lib/lanes/keep-alive.d.ts +3 -1
- package/dist/lib/lanes/keep-alive.js +56 -1
- package/dist/lib/lanes/manager-status-gate.d.ts +34 -0
- package/dist/lib/lanes/manager-status-gate.js +168 -0
- package/dist/lib/lanes/reconcile.d.ts +2 -0
- package/dist/lib/lanes/reconcile.js +45 -9
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +9 -0
- package/dist/lib/lanes/types.d.ts +21 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.d.ts +3 -0
- package/dist/lib/lanes/watcher.js +92 -6
- package/dist/lib/mesh/live/daemon/transcript-watch.js +25 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.255.0] — 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Internal: moved watch, monitor-check, and wait handlers to src/commands/lanes-watch.ts; behavior is unchanged.
|
|
10
|
+
|
|
11
|
+
## [5.254.0] — 2026-09-28
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- The `lanes.manager-status-gate` flag checks a manager lane's declared
|
|
16
|
+
status file after each provider turn. It queues at most one corrective
|
|
17
|
+
resume if the file was not updated. An invalid or out-of-boundary mapping
|
|
18
|
+
disables the gate for that lane and lets the lane start.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- The end-of-session work report is no longer dropped when the background
|
|
23
|
+
transcript watcher polls late or adopts a session after a restart. A
|
|
24
|
+
synthesized `turn_end` now carries the transcript time, capped at now, and
|
|
25
|
+
is never earlier than the last event sent for the session.
|
|
26
|
+
- `hq db sql --remote` is hidden because remote SQL is not available in this
|
|
27
|
+
CLI build. Existing scripts get a clear error, and local SQL remains
|
|
28
|
+
available without `--remote`.
|
|
29
|
+
|
|
5
30
|
## [5.253.0] — 2026-09-27
|
|
6
31
|
|
|
7
32
|
### Added
|
|
@@ -11,6 +36,7 @@
|
|
|
11
36
|
not own with a plain message. `--as` is unchanged and cannot be combined with `--handle`.
|
|
12
37
|
|
|
13
38
|
## [5.252.0] — 2026-09-27
|
|
39
|
+
|
|
14
40
|
### Added
|
|
15
41
|
|
|
16
42
|
- `hq report publish` takes a snapshot of the finished page and publishes it as the report's preview image, using a browser already installed on the computer. A page that names its own preview image keeps it. If no browser is found the report publishes without a preview.
|
|
@@ -1270,7 +1270,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1270
1270
|
readonly subcommands: readonly [];
|
|
1271
1271
|
}, {
|
|
1272
1272
|
readonly name: "sql";
|
|
1273
|
-
readonly description: "Run SQL against the company vault DB (local
|
|
1273
|
+
readonly description: "Run SQL against the company vault DB (local; read-only by default)";
|
|
1274
1274
|
readonly aliases: readonly [];
|
|
1275
1275
|
readonly hidden: false;
|
|
1276
1276
|
readonly usage: "[options] [sql...]";
|
|
@@ -1287,7 +1287,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1287
1287
|
readonly description: "Allow write/DDL statements (discouraged for ad-hoc DDL)";
|
|
1288
1288
|
}, {
|
|
1289
1289
|
readonly flags: "--remote";
|
|
1290
|
-
readonly description: "
|
|
1290
|
+
readonly description: "Remote SQL is not available in this CLI build yet; local queries work without --remote";
|
|
1291
1291
|
}, {
|
|
1292
1292
|
readonly flags: "--db-path <path>";
|
|
1293
1293
|
readonly description: "Dangerous: open a non-canonical DB path (denied unless --allow-cross-company-path)";
|
|
@@ -1619,7 +1619,7 @@ export const COMMAND_CATALOG = [
|
|
|
1619
1619
|
},
|
|
1620
1620
|
{
|
|
1621
1621
|
"name": "sql",
|
|
1622
|
-
"description": "Run SQL against the company vault DB (local
|
|
1622
|
+
"description": "Run SQL against the company vault DB (local; read-only by default)",
|
|
1623
1623
|
"aliases": [],
|
|
1624
1624
|
"hidden": false,
|
|
1625
1625
|
"usage": "[options] [sql...]",
|
|
@@ -1641,7 +1641,7 @@ export const COMMAND_CATALOG = [
|
|
|
1641
1641
|
},
|
|
1642
1642
|
{
|
|
1643
1643
|
"flags": "--remote",
|
|
1644
|
-
"description": "
|
|
1644
|
+
"description": "Remote SQL is not available in this CLI build yet; local queries work without --remote"
|
|
1645
1645
|
},
|
|
1646
1646
|
{
|
|
1647
1647
|
"flags": "--db-path <path>",
|
package/dist/commands/db-sql.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Default is read-only. Prefer `hq db migrate` for schema changes.
|
|
5
5
|
* Never prints remote connection strings.
|
|
6
6
|
*/
|
|
7
|
+
import { Option } from "commander";
|
|
7
8
|
import chalk from "chalk";
|
|
8
9
|
import fs from "node:fs";
|
|
9
|
-
import { runRemoteSql } from "../lib/db/remote-sql.js";
|
|
10
10
|
import { formatSqlResult, runLocalSql } from "../lib/db/sql.js";
|
|
11
11
|
function readSqlArg(sqlArgs, stdinFlag) {
|
|
12
12
|
if (stdinFlag || sqlArgs.length === 0) {
|
|
@@ -19,10 +19,10 @@ function readSqlArg(sqlArgs, stdinFlag) {
|
|
|
19
19
|
}
|
|
20
20
|
export function registerDbSqlCommand(db) {
|
|
21
21
|
db.command("sql")
|
|
22
|
-
.description("Run SQL against the company vault DB (local
|
|
22
|
+
.description("Run SQL against the company vault DB (local; read-only by default)")
|
|
23
23
|
.requiredOption("--company <slug>", "Company slug (tenant scope; required)")
|
|
24
24
|
.option("--write", "Allow write/DDL statements (discouraged for ad-hoc DDL)", false)
|
|
25
|
-
.
|
|
25
|
+
.addOption(new Option("--remote", "Remote SQL is not available in this CLI build yet; local queries work without --remote").hideHelp())
|
|
26
26
|
.option("--db-path <path>", "Dangerous: open a non-canonical DB path (denied unless --allow-cross-company-path)")
|
|
27
27
|
.option("--allow-cross-company-path", "Dangerous: permit --db-path outside this company's canonical local DB", false)
|
|
28
28
|
.option("--format <fmt>", "Output format: jsonl | table", "jsonl")
|
|
@@ -37,27 +37,13 @@ export function registerDbSqlCommand(db) {
|
|
|
37
37
|
process.exitCode = 1;
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
|
-
const sql = readSqlArg(sqlParts, !!opts.stdin).trim();
|
|
41
|
-
const format = opts.format === "table" ? "table" : "jsonl";
|
|
42
40
|
if (opts.remote) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
company,
|
|
46
|
-
sql,
|
|
47
|
-
getConnectionConfig: async () => {
|
|
48
|
-
throw new Error("no remote binding");
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
console.log(formatSqlResult({
|
|
52
|
-
columns: remoteResult.rows[0]
|
|
53
|
-
? Object.keys(remoteResult.rows[0])
|
|
54
|
-
: [],
|
|
55
|
-
rows: remoteResult.rows,
|
|
56
|
-
changes: 0,
|
|
57
|
-
readonly: true,
|
|
58
|
-
}, format));
|
|
41
|
+
console.error(chalk.red("Error: Remote SQL is not available in this CLI build yet. Local queries work without --remote."));
|
|
42
|
+
process.exitCode = 1;
|
|
59
43
|
return;
|
|
60
44
|
}
|
|
45
|
+
const sql = readSqlArg(sqlParts, !!opts.stdin).trim();
|
|
46
|
+
const format = opts.format === "table" ? "table" : "jsonl";
|
|
61
47
|
const result = runLocalSql({
|
|
62
48
|
company,
|
|
63
49
|
sql,
|
|
@@ -9,6 +9,7 @@ import { createLocalTrackerStore } from "../lib/lanes/tracker-store.js";
|
|
|
9
9
|
import { sessionLiveness } from "../lib/lanes/liveness.js";
|
|
10
10
|
import { findOrphanLanes, parseOrphanMinAge } from "../lib/lanes/orphans.js";
|
|
11
11
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
12
|
+
import { checkCompanyPath } from "../lib/lanes/checked-company-path.js";
|
|
12
13
|
import { LANES_TRACKER_FLAG } from "./lanes-tracker.js";
|
|
13
14
|
function fail(error, message, details = {}) {
|
|
14
15
|
throw new LanesError(error, message, details);
|
|
@@ -56,37 +57,6 @@ function readJsonObject(file, missing = "error") {
|
|
|
56
57
|
return fail("tracker_checks_input_invalid", "Checks input must be a JSON object", { path: file });
|
|
57
58
|
return parsed;
|
|
58
59
|
}
|
|
59
|
-
function isWithin(root, candidate) {
|
|
60
|
-
const relative = path.relative(root, candidate);
|
|
61
|
-
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
62
|
-
}
|
|
63
|
-
function checkCompanyPath(root, company, file, detailsPath) {
|
|
64
|
-
if (path.isAbsolute(file))
|
|
65
|
-
return fail("tracker_checks_mapping_invalid", "Mapped path must be relative to the HQ root", { path: detailsPath });
|
|
66
|
-
const absolute = path.resolve(root, file);
|
|
67
|
-
const rootReal = fs.realpathSync(root);
|
|
68
|
-
if (!isWithin(rootReal, absolute))
|
|
69
|
-
return fail("tracker_checks_mapping_invalid", "Mapped path escapes the HQ root", { path: detailsPath });
|
|
70
|
-
const relative = path.relative(rootReal, absolute).split(path.sep);
|
|
71
|
-
if (relative[0] === "companies" && relative[1] !== company) {
|
|
72
|
-
return fail("tracker_checks_mapping_invalid", "Mapped path crosses the active company boundary", { path: detailsPath });
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
const real = fs.realpathSync(absolute);
|
|
76
|
-
if (!isWithin(rootReal, real))
|
|
77
|
-
return fail("tracker_checks_mapping_invalid", "Mapped path resolves outside the HQ root", { path: detailsPath });
|
|
78
|
-
const realRelative = path.relative(rootReal, real).split(path.sep);
|
|
79
|
-
if (realRelative[0] === "companies" && realRelative[1] !== company) {
|
|
80
|
-
return fail("tracker_checks_mapping_invalid", "Mapped path resolves into another company", { path: detailsPath });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch (err) {
|
|
84
|
-
if (err.code !== "ENOENT") {
|
|
85
|
-
return fail("tracker_checks_mapping_unreadable", `Mapped path could not be checked: ${err instanceof Error ? err.message : String(err)}`, { path: absolute });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return absolute;
|
|
89
|
-
}
|
|
90
60
|
function projectFromPrdReference(root, company, raw, registryPath) {
|
|
91
61
|
if (typeof raw !== "string" || raw.trim() === "") {
|
|
92
62
|
return fail("tracker_checks_mapping_invalid", "Team mapping needs a PRD path", { path: registryPath });
|
|
@@ -10,6 +10,7 @@ import { isPidAlive, runWatcherLoop, WATCHER_OUT_MAX_BYTES, watcherOutLogPath, w
|
|
|
10
10
|
import { laneStories, laneStory, storyWarning } from "../lib/lanes/stories.js";
|
|
11
11
|
import { laneRunDir, probePidRecord, readPidRecord, readProcStarttime, spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
12
12
|
import { keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
|
|
13
|
+
import { managerStatusGateEnabledForLane } from "../lib/lanes/manager-status-gate.js";
|
|
13
14
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
14
15
|
import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
|
|
15
16
|
export function createLaneMessageHandlers(deps) {
|
|
@@ -285,6 +286,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
285
286
|
markAccepted();
|
|
286
287
|
}
|
|
287
288
|
else {
|
|
289
|
+
const managerStatusGateEnabled = await managerStatusGateEnabledForLane(hqRoot, lane, { env: ctx.env });
|
|
288
290
|
const dispatched = await dispatchLaneDelivery(lane, {
|
|
289
291
|
dropbox: () => {
|
|
290
292
|
// Live (or about-to-be-live) process: the PreToolUse drain delivers it.
|
|
@@ -341,6 +343,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
341
343
|
}
|
|
342
344
|
const started = spawnLaneWorker(hqRoot, lane, {
|
|
343
345
|
env: ctx.env,
|
|
346
|
+
managerStatusGateEnabled,
|
|
344
347
|
modelDeps: {
|
|
345
348
|
readFileSync: ctx.readFileSync,
|
|
346
349
|
probe: ctx.probeProvider,
|
|
@@ -5,6 +5,7 @@ import { parseCliEffort } from "../lib/lanes/effort.js";
|
|
|
5
5
|
import { parseOutpostHireGrantContext } from "../lib/lanes/hire-grant-dispatch.js";
|
|
6
6
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
7
7
|
import { laneStories, laneStory, storyWarning } from "../lib/lanes/stories.js";
|
|
8
|
+
import { managerStatusGateEnabledForLane } from "../lib/lanes/manager-status-gate.js";
|
|
8
9
|
import { spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
9
10
|
import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, hasKeepAliveProgress, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
|
|
10
11
|
export function createLaneResumeHandlers(deps) {
|
|
@@ -281,6 +282,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
281
282
|
resumed = lane;
|
|
282
283
|
}
|
|
283
284
|
else {
|
|
285
|
+
const managerStatusGateEnabled = await managerStatusGateEnabledForLane(hqRoot, lane, { env: ctx.env });
|
|
284
286
|
resumed = await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
285
287
|
const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
|
|
286
288
|
if (admission.action === "refuse" && admission.error)
|
|
@@ -327,6 +329,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
327
329
|
}
|
|
328
330
|
return spawnLaneWorker(hqRoot, lane, {
|
|
329
331
|
env: ctx.env,
|
|
332
|
+
managerStatusGateEnabled,
|
|
330
333
|
modelDeps: {
|
|
331
334
|
readFileSync: ctx.readFileSync,
|
|
332
335
|
probe: ctx.probeProvider,
|
|
@@ -7,6 +7,7 @@ import type { WatcherLoopDeps } from "../lib/lanes/watcher.js";
|
|
|
7
7
|
import type { RunCompactionFn } from "../lib/lanes/compaction.js";
|
|
8
8
|
import type { LaneMesh, LaneMeshEvent } from "../lib/lanes/mesh-events.js";
|
|
9
9
|
import type { BalanceHostGap, BalanceHostSnapshot } from "../lib/lanes/claude-balance.js";
|
|
10
|
+
import type { HostCapacitySnapshot } from "../lib/lanes/host-capacity.js";
|
|
10
11
|
import { type ResolvedEffort } from "../lib/lanes/effort.js";
|
|
11
12
|
import type { SessionCopyResult } from "../lib/lanes/move-account.js";
|
|
12
13
|
import type { WorkerProfile } from "../lib/lanes/profile.js";
|
|
@@ -223,6 +224,32 @@ export interface Ctx {
|
|
|
223
224
|
laneTrackerEnabled: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
224
225
|
trackerActor: () => string | null;
|
|
225
226
|
}
|
|
227
|
+
export interface LaneWaitCommandOptions {
|
|
228
|
+
any?: string[];
|
|
229
|
+
for?: string[];
|
|
230
|
+
timeout?: string;
|
|
231
|
+
json?: boolean;
|
|
232
|
+
}
|
|
233
|
+
export interface MonitorCheckOptions {
|
|
234
|
+
session?: string;
|
|
235
|
+
engine?: string;
|
|
236
|
+
json?: boolean;
|
|
237
|
+
reminder?: boolean;
|
|
238
|
+
}
|
|
239
|
+
export interface LaneWatchOptions {
|
|
240
|
+
intervalMs?: string;
|
|
241
|
+
json?: boolean;
|
|
242
|
+
}
|
|
243
|
+
export interface LaneWatchHandlerDeps {
|
|
244
|
+
laneHostFromCtx(ctx: Ctx): LaneRecord["host"];
|
|
245
|
+
loadAllLanes(hqRoot: string): LaneRecord[];
|
|
246
|
+
loadClaudeBalanceHostSnapshots(ctx: Ctx, hqRoot: string, config: LanesConfig): Promise<{
|
|
247
|
+
snapshots: BalanceHostSnapshot[];
|
|
248
|
+
unavailableHosts: BalanceHostGap[];
|
|
249
|
+
}>;
|
|
250
|
+
printJson(value: unknown): void;
|
|
251
|
+
readOutpostCapacity(ctx: Ctx, outpostId: string): Promise<HostCapacitySnapshot>;
|
|
252
|
+
}
|
|
226
253
|
export interface PrimaryOwnedOutpostQuestion {
|
|
227
254
|
question: QuestionRecord;
|
|
228
255
|
lane: LaneRecord;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Ctx, MonitorCheckOptions, LaneWaitCommandOptions, LaneWatchHandlerDeps, LaneWatchOptions } from "./lanes-shared.js";
|
|
2
|
+
export declare function createLaneWatchHandlers(deps: LaneWatchHandlerDeps): {
|
|
3
|
+
runWatch: (ctx: Ctx, laneIds: string[], opts: LaneWatchOptions) => Promise<number>;
|
|
4
|
+
runMonitorCheck: (ctx: Ctx, opts: MonitorCheckOptions) => Promise<number>;
|
|
5
|
+
runWait: (ctx: Ctx, opts: LaneWaitCommandOptions) => Promise<number>;
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=lanes-watch.d.ts.map
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { accountStatus, coveredLaneIds, loadLanesConfig, MAX_WATCHED_LANES, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, monitorReminderText, runLaneWatch, } from "../lib/lanes/index.js";
|
|
2
|
+
import { LEAD_LANE_KEEP_ALIVE_FLAG } from "../lib/lanes/keep-alive.js";
|
|
3
|
+
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
4
|
+
import { isLanesError, LanesError } from "../lib/lanes/errors.js";
|
|
5
|
+
import { LANE_WAIT_INVALID_INPUT_EXIT_CODE, LANE_WAIT_TIMEOUT_EXIT_CODE, runLaneWait, } from "../lib/lanes/wait.js";
|
|
6
|
+
const MONITOR_ACTIVE_LANE_STATES = new Set([
|
|
7
|
+
"running",
|
|
8
|
+
"queued",
|
|
9
|
+
"awaiting_input",
|
|
10
|
+
"blocked",
|
|
11
|
+
]);
|
|
12
|
+
function resolveMonitorEngine(requested, env) {
|
|
13
|
+
const value = requested?.trim().toLowerCase() ||
|
|
14
|
+
env.HQ_CONDUCT_ENGINE?.trim().toLowerCase() ||
|
|
15
|
+
"claude";
|
|
16
|
+
if (!MONITOR_ENGINES.includes(value)) {
|
|
17
|
+
throw new LanesError("monitor_invalid_engine", `Unknown monitor-check engine ${JSON.stringify(value)}. Accepted: ${MONITOR_ENGINES.join(", ")}`, { accepted: [...MONITOR_ENGINES], value });
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function monitorCallsForLaneIds(laneIds) {
|
|
22
|
+
const calls = [];
|
|
23
|
+
for (let offset = 0; offset < laneIds.length; offset += MAX_WATCHED_LANES) {
|
|
24
|
+
const chunk = laneIds.slice(offset, offset + MAX_WATCHED_LANES);
|
|
25
|
+
calls.push({
|
|
26
|
+
command: `hq lanes watch ${chunk.join(" ")}`,
|
|
27
|
+
timeout_ms: MONITOR_MAX_TIMEOUT_MS,
|
|
28
|
+
persistent: true,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return calls;
|
|
32
|
+
}
|
|
33
|
+
function laneWaitHumanLine(result) {
|
|
34
|
+
const details = Object.entries(result.change)
|
|
35
|
+
.filter(([key]) => key !== "type")
|
|
36
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
37
|
+
.join(" ");
|
|
38
|
+
return `lane=${result.lane_id} change=${result.change.type}${details ? ` ${details}` : ""} at=${result.at}`;
|
|
39
|
+
}
|
|
40
|
+
export function createLaneWatchHandlers(deps) {
|
|
41
|
+
const { laneHostFromCtx, loadAllLanes, loadClaudeBalanceHostSnapshots, printJson, readOutpostCapacity, } = deps;
|
|
42
|
+
async function runWatch(ctx, laneIds, opts) {
|
|
43
|
+
const rawInterval = opts.intervalMs === undefined ? undefined : Number(opts.intervalMs);
|
|
44
|
+
const hqRoot = ctx.hqRoot();
|
|
45
|
+
await runLaneWatch(hqRoot, laneIds, {
|
|
46
|
+
intervalMs: rawInterval,
|
|
47
|
+
sleep: ctx.sleep,
|
|
48
|
+
keepAliveEnabled: (lane) => readLanesRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, {
|
|
49
|
+
company: lane.company,
|
|
50
|
+
env: ctx.env,
|
|
51
|
+
}),
|
|
52
|
+
readAccountStatuses: async (lanes) => {
|
|
53
|
+
if (lanes.length === 0)
|
|
54
|
+
return [];
|
|
55
|
+
const config = loadLanesConfig(hqRoot);
|
|
56
|
+
const currentHost = laneHostFromCtx(ctx);
|
|
57
|
+
const currentHostname = ctx.hostname().trim();
|
|
58
|
+
const isCurrentHost = (host) => (host.kind === currentHost.kind && host.id === currentHost.id) ||
|
|
59
|
+
(host.kind === "local" && host.id === currentHostname);
|
|
60
|
+
const remoteOutpostIds = [...new Set(lanes
|
|
61
|
+
.filter((lane) => lane.host.kind === "outpost" && !isCurrentHost(lane.host))
|
|
62
|
+
.map((lane) => lane.host.id))];
|
|
63
|
+
const remoteOutposts = await Promise.all(remoteOutpostIds.map(async (id) => [id, await readOutpostCapacity(ctx, id)]));
|
|
64
|
+
const outpostSnapshots = new Map(remoteOutposts);
|
|
65
|
+
const remoteLocalLanes = lanes.filter((lane) => lane.host.kind === "local" && !isCurrentHost(lane.host));
|
|
66
|
+
const remoteLocalHosts = remoteLocalLanes.length > 0
|
|
67
|
+
? await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config)
|
|
68
|
+
: undefined;
|
|
69
|
+
const localStatusCache = new Map();
|
|
70
|
+
return lanes.map((lane) => {
|
|
71
|
+
if (config.providers[lane.provider].accounts[lane.account] === undefined) {
|
|
72
|
+
throw new LanesError("watch_account_unconfigured", `Lane ${lane.lane_id} records unconfigured ${lane.provider} account ${lane.account}`, { lane_id: lane.lane_id, provider: lane.provider, account: lane.account, host: lane.host });
|
|
73
|
+
}
|
|
74
|
+
let loggedIn;
|
|
75
|
+
if (isCurrentHost(lane.host)) {
|
|
76
|
+
const key = JSON.stringify([lane.provider, lane.account, currentHost.kind, currentHost.id]);
|
|
77
|
+
if (!localStatusCache.has(key)) {
|
|
78
|
+
localStatusCache.set(key, accountStatus(lane.provider, lane.account, config, {
|
|
79
|
+
existsSync: ctx.existsSync,
|
|
80
|
+
probe: ctx.probeProvider,
|
|
81
|
+
env: ctx.env,
|
|
82
|
+
hostname: currentHostname,
|
|
83
|
+
}).logged_in);
|
|
84
|
+
}
|
|
85
|
+
loggedIn = localStatusCache.get(key);
|
|
86
|
+
}
|
|
87
|
+
else if (lane.host.kind === "outpost") {
|
|
88
|
+
loggedIn = outpostSnapshots.get(lane.host.id)?.providers[lane.provider].accounts[lane.account]?.logged_in;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const snapshot = remoteLocalHosts?.snapshots.find((candidate) => candidate.capacity.host.kind === lane.host.kind &&
|
|
92
|
+
candidate.capacity.host.id === lane.host.id);
|
|
93
|
+
if (!snapshot) {
|
|
94
|
+
const gap = remoteLocalHosts?.unavailableHosts.find((candidate) => candidate.host.kind === lane.host.kind && candidate.host.id === lane.host.id);
|
|
95
|
+
throw new LanesError("watch_account_host_unavailable", `Cannot check ${lane.provider} account ${lane.account} on ${lane.host.kind}:${lane.host.id}${gap ? `: ${gap.reason}` : ""}`, { lane_id: lane.lane_id, provider: lane.provider, account: lane.account, host: lane.host });
|
|
96
|
+
}
|
|
97
|
+
loggedIn = snapshot.capacity.providers[lane.provider].accounts[lane.account]?.logged_in;
|
|
98
|
+
}
|
|
99
|
+
if (loggedIn === undefined) {
|
|
100
|
+
throw new LanesError("watch_account_unconfigured", `Lane ${lane.lane_id} records unconfigured ${lane.provider} account ${lane.account} on ${lane.host.kind}:${lane.host.id}`, { lane_id: lane.lane_id, provider: lane.provider, account: lane.account, host: lane.host });
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
lane_id: lane.lane_id,
|
|
104
|
+
provider: lane.provider,
|
|
105
|
+
account: lane.account,
|
|
106
|
+
host: lane.host,
|
|
107
|
+
logged_in: loggedIn,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
writeLine: (line) => console.log(opts.json ? JSON.stringify({ event: line }) : line),
|
|
112
|
+
writeAccountEvent: (event) => console.log(opts.json ? JSON.stringify(event) : `account_logged_out provider=${event.provider} account=${event.account} host=${event.host.kind}:${event.host.id} lane_ids=${event.lane_ids.join(",")}`),
|
|
113
|
+
writeError: (line) => process.stderr.write(`${line}\n`),
|
|
114
|
+
});
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
async function runMonitorCheck(ctx, opts) {
|
|
118
|
+
if (opts.reminder) {
|
|
119
|
+
console.log(monitorReminderText());
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
const sessionId = opts.session?.trim();
|
|
123
|
+
if (!sessionId) {
|
|
124
|
+
throw new LanesError("monitor_session_required", "hq lanes monitor-check requires --session <id> unless --reminder is used");
|
|
125
|
+
}
|
|
126
|
+
const engine = resolveMonitorEngine(opts.engine, ctx.env);
|
|
127
|
+
const activeLanes = loadAllLanes(ctx.hqRoot()).filter((lane) => lane.senior.kind === "session" &&
|
|
128
|
+
lane.senior.id === sessionId &&
|
|
129
|
+
MONITOR_ACTIVE_LANE_STATES.has(lane.state));
|
|
130
|
+
if (engine !== "claude") {
|
|
131
|
+
if (opts.json) {
|
|
132
|
+
printJson({
|
|
133
|
+
ok: true,
|
|
134
|
+
action: "monitor-check",
|
|
135
|
+
session_id: sessionId,
|
|
136
|
+
engine,
|
|
137
|
+
active_lane_ids: activeLanes.map((lane) => lane.lane_id),
|
|
138
|
+
covered_lane_ids: [],
|
|
139
|
+
uncovered_lane_ids: [],
|
|
140
|
+
skipped: "engine_has_no_claude_monitor",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
if (activeLanes.length === 0) {
|
|
146
|
+
if (opts.json) {
|
|
147
|
+
printJson({
|
|
148
|
+
ok: true,
|
|
149
|
+
action: "monitor-check",
|
|
150
|
+
session_id: sessionId,
|
|
151
|
+
engine,
|
|
152
|
+
active_lane_ids: [],
|
|
153
|
+
covered_lane_ids: [],
|
|
154
|
+
uncovered_lane_ids: [],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
const evidence = await ctx.monitorEvidence(sessionId);
|
|
160
|
+
if (evidence.sessionId !== sessionId) {
|
|
161
|
+
throw new LanesError("monitor_evidence_session_mismatch", `Monitor evidence was for ${JSON.stringify(evidence.sessionId)}, expected ${JSON.stringify(sessionId)}`);
|
|
162
|
+
}
|
|
163
|
+
const covered = coveredLaneIds(evidence, ctx.now());
|
|
164
|
+
const activeIds = activeLanes.map((lane) => lane.lane_id);
|
|
165
|
+
const uncovered = activeIds.filter((laneId) => !covered.has(laneId));
|
|
166
|
+
const monitorCalls = monitorCallsForLaneIds(uncovered);
|
|
167
|
+
if (opts.json) {
|
|
168
|
+
printJson({
|
|
169
|
+
ok: uncovered.length === 0,
|
|
170
|
+
action: "monitor-check",
|
|
171
|
+
session_id: sessionId,
|
|
172
|
+
engine,
|
|
173
|
+
active_lane_ids: activeIds,
|
|
174
|
+
covered_lane_ids: activeIds.filter((laneId) => covered.has(laneId)),
|
|
175
|
+
uncovered_lane_ids: uncovered,
|
|
176
|
+
monitor_call: monitorCalls.length === 1 ? monitorCalls[0] : null,
|
|
177
|
+
monitor_calls: monitorCalls,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
else if (uncovered.length > 0) {
|
|
181
|
+
for (const laneId of uncovered)
|
|
182
|
+
console.log(`uncovered lane: ${laneId}`);
|
|
183
|
+
for (const monitorCall of monitorCalls) {
|
|
184
|
+
console.log(`Monitor(command=${JSON.stringify(monitorCall.command)}, timeout_ms=${monitorCall.timeout_ms}, persistent=true)`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return uncovered.length > 0 ? MONITOR_UNCOVERED_EXIT_CODE : 0;
|
|
188
|
+
}
|
|
189
|
+
async function runWait(ctx, opts) {
|
|
190
|
+
const timeoutSeconds = opts.timeout === undefined ? Number.NaN : Number(opts.timeout);
|
|
191
|
+
try {
|
|
192
|
+
const result = await runLaneWait(ctx.hqRoot(), opts.any ?? [], opts.for ?? [], timeoutSeconds * 1000, { now: ctx.now, sleep: ctx.sleep });
|
|
193
|
+
if (result) {
|
|
194
|
+
console.log(opts.json ? JSON.stringify(result) : laneWaitHumanLine(result));
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
console.log(opts.json
|
|
198
|
+
? JSON.stringify({ ok: false, error: "wait_timeout" })
|
|
199
|
+
: "wait_timeout");
|
|
200
|
+
return LANE_WAIT_TIMEOUT_EXIT_CODE;
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
if (!isLanesError(err) || err.error !== "wait_invalid_input")
|
|
204
|
+
throw err;
|
|
205
|
+
const output = { ok: false, error: err.error, message: err.message };
|
|
206
|
+
if (opts.json)
|
|
207
|
+
console.log(JSON.stringify(output));
|
|
208
|
+
else
|
|
209
|
+
process.stderr.write(`${err.message}\n`);
|
|
210
|
+
return LANE_WAIT_INVALID_INPUT_EXIT_CODE;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { runWatch, runMonitorCheck, runWait };
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=lanes-watch.js.map
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface LanesCommandDeps {
|
|
|
58
58
|
/** Test seam for worker PID identity checks. */
|
|
59
59
|
readStarttime?: (pid: number) => number | null;
|
|
60
60
|
/** Test seams for synchronous dead-worker classification and repair. */
|
|
61
|
-
watcherLoopDeps?: Pick<WatcherLoopDeps, "sleep" | "spawnRepair">;
|
|
61
|
+
watcherLoopDeps?: Pick<WatcherLoopDeps, "sleep" | "spawnRepair" | "readManagerStatusGateFlag">;
|
|
62
62
|
stopSleepMs?: (ms: number) => void;
|
|
63
63
|
livenessDeps?: LivenessDeps;
|
|
64
64
|
presenceConnected?: boolean;
|