@indigoai-us/hq-cli 5.345.155 → 5.345.159
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 +289 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +536 -7
- package/dist/command-catalog.generated.js +676 -24
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/agent-inbox.js +2 -2
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +150 -10
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +13 -1
- package/dist/commands/crm.js +83 -6
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files-browse.d.ts +16 -0
- package/dist/commands/files-browse.js +52 -1
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +52 -1
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/integrations-core.d.ts +13 -1
- package/dist/commands/integrations-core.js +49 -4
- package/dist/commands/integrations.js +4 -2
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +37 -0
- package/dist/commands/secrets.js +185 -16
- package/dist/commands/signals.js +17 -5
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +7 -0
- package/dist/lib/agent-kit/inbox-state.js +5 -1
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +9 -0
- package/dist/lib/doctor/checks/sync-health.js +66 -1
- package/dist/lib/doctor/company-resolution-state.d.ts +30 -0
- package/dist/lib/doctor/company-resolution-state.js +89 -0
- package/dist/lib/doctor/fix/apply.d.ts +36 -6
- package/dist/lib/doctor/fix/apply.js +128 -12
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +1 -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/hook-engine/resolve.js +6 -2
- package/dist/lib/index-render/company-knowledge.d.ts +1 -1
- package/dist/lib/index-render/company-knowledge.js +4 -5
- package/dist/lib/index-render/index.js +44 -3
- package/dist/lib/index-render/projects.d.ts +1 -1
- package/dist/lib/index-render/projects.js +5 -5
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/sentry-before-send.js +1 -0
- package/dist/utils/access-denied-hint.js +6 -0
- package/dist/utils/client-health.d.ts +34 -0
- package/dist/utils/client-health.js +96 -3
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expiry check for the Claude OAuth credential a lane worker will use.
|
|
3
|
+
*
|
|
4
|
+
* `hq lanes accounts list` used to report `logged_in: true` when a
|
|
5
|
+
* credential merely existed (a `.credentials.json` file, or `claude auth
|
|
6
|
+
* status` saying a keychain item is present). An expired access token still
|
|
7
|
+
* passed, and the lane exited seconds after start with an OAuth expiry
|
|
8
|
+
* error. This module reads the same credential the worker reads and reports
|
|
9
|
+
* whether its access token is still valid.
|
|
10
|
+
*
|
|
11
|
+
* The worker's credential is keyed by CLAUDE_CONFIG_DIR:
|
|
12
|
+
* - macOS: the login user's Keychain item `Claude Code-credentials` for the
|
|
13
|
+
* default home (the worker runs with the variable unset), or the
|
|
14
|
+
* hash-suffixed item for any other home; `<home>/.credentials.json` when no
|
|
15
|
+
* Keychain item exists.
|
|
16
|
+
* - elsewhere: `<home>/.credentials.json`.
|
|
17
|
+
*
|
|
18
|
+
* An access token past its expiry is only reported expired when there is no
|
|
19
|
+
* refresh token, or the capacity collector has recorded the refresh token as
|
|
20
|
+
* revoked. Otherwise Claude Code renews the access token at startup, so the
|
|
21
|
+
* account is still usable.
|
|
22
|
+
*
|
|
23
|
+
* Only `claudeAiOauth.expiresAt` and the presence of the tokens are read.
|
|
24
|
+
* Token values are never returned, logged, or copied; the refresh token is
|
|
25
|
+
* only passed to the revoked check, which compares a fingerprint.
|
|
26
|
+
*/
|
|
27
|
+
import { spawnSync } from "node:child_process";
|
|
28
|
+
import * as fs from "node:fs";
|
|
29
|
+
import * as os from "node:os";
|
|
30
|
+
import * as path from "node:path";
|
|
31
|
+
import { CLAUDE_KEYCHAIN_SERVICE, claudeKeychainServiceName, isRefreshTokenRevoked, } from "../capacity/collectors/claude-oauth.js";
|
|
32
|
+
import { resolveLanesHqRoot } from "./hq-root.js";
|
|
33
|
+
/** Revoked check against the capacity collector's marker file. Never throws. */
|
|
34
|
+
export function defaultIsRefreshRevoked(refreshToken) {
|
|
35
|
+
try {
|
|
36
|
+
return isRefreshTokenRevoked(resolveLanesHqRoot(), refreshToken);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function safeUsername() {
|
|
43
|
+
try {
|
|
44
|
+
return os.userInfo().username || null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export const CLAUDE_CREDENTIAL_SECURITY_TIMEOUT_MS = 5_000;
|
|
51
|
+
/** `security find-generic-password` exit status when the item does not exist. */
|
|
52
|
+
const SECURITY_ITEM_NOT_FOUND = 44;
|
|
53
|
+
/** Keychain service the worker reads for this home. */
|
|
54
|
+
export function laneClaudeKeychainService(target) {
|
|
55
|
+
return target.defaultHome ? CLAUDE_KEYCHAIN_SERVICE : claudeKeychainServiceName(target.home);
|
|
56
|
+
}
|
|
57
|
+
/** Classify a credential document. Returns only state and expiry. */
|
|
58
|
+
export function claudeTokenStateFromDocument(text, nowMs, isRefreshRevoked = () => false) {
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(text);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { state: "unknown", expires_at: null };
|
|
65
|
+
}
|
|
66
|
+
if (!isRecord(parsed))
|
|
67
|
+
return { state: "unknown", expires_at: null };
|
|
68
|
+
const oauth = parsed.claudeAiOauth;
|
|
69
|
+
if (!isRecord(oauth))
|
|
70
|
+
return { state: "missing", expires_at: null };
|
|
71
|
+
const token = typeof oauth.accessToken === "string" ? oauth.accessToken : oauth.access_token;
|
|
72
|
+
if (typeof token !== "string" || token.trim() === "")
|
|
73
|
+
return { state: "missing", expires_at: null };
|
|
74
|
+
const raw = oauth.expiresAt;
|
|
75
|
+
if (typeof raw !== "number" || !Number.isFinite(raw))
|
|
76
|
+
return { state: "valid", expires_at: null };
|
|
77
|
+
const ms = raw < 1e12 ? raw * 1000 : raw;
|
|
78
|
+
const date = new Date(ms);
|
|
79
|
+
const expires_at = Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
80
|
+
if (ms > nowMs)
|
|
81
|
+
return { state: "valid", expires_at };
|
|
82
|
+
// Past expiry. Claude Code renews the access token from the refresh token
|
|
83
|
+
// at startup, so this is only a dead sign-in when there is nothing to renew
|
|
84
|
+
// with or the refresh token is known to be revoked.
|
|
85
|
+
const refresh = typeof oauth.refreshToken === "string" ? oauth.refreshToken : oauth.refresh_token;
|
|
86
|
+
if (typeof refresh !== "string" || refresh.trim() === "")
|
|
87
|
+
return { state: "expired", expires_at };
|
|
88
|
+
let revoked = false;
|
|
89
|
+
try {
|
|
90
|
+
revoked = isRefreshRevoked(refresh);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
revoked = false;
|
|
94
|
+
}
|
|
95
|
+
return { state: revoked ? "expired" : "valid", expires_at };
|
|
96
|
+
}
|
|
97
|
+
function isRecord(value) {
|
|
98
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
99
|
+
}
|
|
100
|
+
function defaultSecurity(args) {
|
|
101
|
+
const result = spawnSync("security", [...args], {
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
timeout: CLAUDE_CREDENTIAL_SECURITY_TIMEOUT_MS,
|
|
104
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
105
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
106
|
+
});
|
|
107
|
+
const timedOut = result.error?.code === "ETIMEDOUT";
|
|
108
|
+
return {
|
|
109
|
+
status: result.error ? null : result.status,
|
|
110
|
+
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
111
|
+
timedOut,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function createClaudeCredentialReader(deps = {}) {
|
|
115
|
+
const platform = deps.platform ?? process.platform;
|
|
116
|
+
const now = deps.now ?? (() => Date.now());
|
|
117
|
+
const security = deps.security ?? defaultSecurity;
|
|
118
|
+
const readFile = deps.readFile ?? ((file) => fs.readFileSync(file, "utf8"));
|
|
119
|
+
const isRefreshRevoked = deps.isRefreshRevoked ?? defaultIsRefreshRevoked;
|
|
120
|
+
const fromFile = (home) => {
|
|
121
|
+
let text;
|
|
122
|
+
try {
|
|
123
|
+
text = readFile(path.join(home, ".credentials.json"));
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const code = err?.code;
|
|
127
|
+
return { state: code === "ENOENT" ? "missing" : "unknown", expires_at: null, source: null };
|
|
128
|
+
}
|
|
129
|
+
return { ...claudeTokenStateFromDocument(text, now(), isRefreshRevoked), source: "file" };
|
|
130
|
+
};
|
|
131
|
+
return (target) => {
|
|
132
|
+
if (platform !== "darwin")
|
|
133
|
+
return fromFile(target.home);
|
|
134
|
+
const service = laneClaudeKeychainService(target);
|
|
135
|
+
let read;
|
|
136
|
+
try {
|
|
137
|
+
// Claude Code writes its item under the login user's account name. A
|
|
138
|
+
// second item with the same service and another account (MCP OAuth
|
|
139
|
+
// only) can exist, so ask for the user's item first, as the capacity
|
|
140
|
+
// collector does, and fall back to any item for the service.
|
|
141
|
+
const username = deps.username ?? safeUsername();
|
|
142
|
+
read = username
|
|
143
|
+
? security(["find-generic-password", "-s", service, "-a", username, "-w"])
|
|
144
|
+
: { status: SECURITY_ITEM_NOT_FOUND, stdout: "", timedOut: false };
|
|
145
|
+
if (!read.timedOut && read.status === SECURITY_ITEM_NOT_FOUND) {
|
|
146
|
+
read = security(["find-generic-password", "-s", service, "-w"]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return { state: "unknown", expires_at: null, source: null };
|
|
151
|
+
}
|
|
152
|
+
if (read.timedOut)
|
|
153
|
+
return { state: "unknown", expires_at: null, source: null };
|
|
154
|
+
if (read.status === SECURITY_ITEM_NOT_FOUND)
|
|
155
|
+
return fromFile(target.home);
|
|
156
|
+
if (read.status !== 0)
|
|
157
|
+
return { state: "unknown", expires_at: null, source: null };
|
|
158
|
+
return { ...claudeTokenStateFromDocument(read.stdout.replace(/\n$/, ""), now(), isRefreshRevoked), source: "keychain" };
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** Production reader: real Keychain on macOS, the credentials file elsewhere. */
|
|
162
|
+
export const readLaneClaudeCredential = createClaudeCredentialReader();
|
|
163
|
+
//# sourceMappingURL=claude-credential.js.map
|
|
@@ -79,6 +79,8 @@ export type RunCompactionFn = (input: {
|
|
|
79
79
|
}) => CompactionCommandResult;
|
|
80
80
|
/** Synchronous `/compact`. stdout is discarded; stderr goes to the run dir. */
|
|
81
81
|
export declare const defaultRunCompaction: RunCompactionFn;
|
|
82
|
+
/** Stderr line printed before a resume-time `/compact` starts. */
|
|
83
|
+
export declare function compactionStartNotice(sessionId: string): string;
|
|
82
84
|
export interface CompactLaneSessionInput {
|
|
83
85
|
command: string;
|
|
84
86
|
/** Full argv of `claude -p --resume <sid> "/compact" …`, built by buildProviderCommand. */
|
|
@@ -388,6 +388,12 @@ export const defaultRunCompaction = (input) => {
|
|
|
388
388
|
fs.closeSync(errFd);
|
|
389
389
|
}
|
|
390
390
|
};
|
|
391
|
+
/** Stderr line printed before a resume-time `/compact` starts. */
|
|
392
|
+
export function compactionStartNotice(sessionId) {
|
|
393
|
+
const minutes = Math.round(COMPACTION_TIMEOUT_MS / 60_000);
|
|
394
|
+
return `hq lanes: compacting session ${sessionId} before resume; this can take up to ${minutes} minutes. ` +
|
|
395
|
+
`Re-run with --no-compact to skip it.\n`;
|
|
396
|
+
}
|
|
391
397
|
/**
|
|
392
398
|
* Run `/compact` on the recorded session and report what happened. A failed
|
|
393
399
|
* or ineffective compaction is recorded, not thrown: the resume that needed
|
|
@@ -398,6 +404,9 @@ export function compactLaneSession(input) {
|
|
|
398
404
|
const locate = input.locateTranscript ?? findSessionTranscript;
|
|
399
405
|
const started = input.now();
|
|
400
406
|
const beforeProbe = probeCompactBoundaries(locate(input.accountHome, input.sessionId));
|
|
407
|
+
// `/compact` runs synchronously and can take minutes. Say so before it
|
|
408
|
+
// starts, so a resume that is waiting on it does not look hung.
|
|
409
|
+
process.stderr.write(compactionStartNotice(input.sessionId));
|
|
401
410
|
const result = run({
|
|
402
411
|
command: input.command,
|
|
403
412
|
args: input.args,
|
package/dist/lib/lanes/config.js
CHANGED
|
@@ -186,6 +186,12 @@ function parseAccount(provider, id, raw, deps) {
|
|
|
186
186
|
home: canonicalizeHome(raw.home, deps),
|
|
187
187
|
enabled: raw.enabled,
|
|
188
188
|
};
|
|
189
|
+
if (raw.company !== undefined) {
|
|
190
|
+
if (typeof raw.company !== "string" || raw.company.trim() === "" || raw.company.trim() !== raw.company) {
|
|
191
|
+
throw new LanesConfigError("invalid_config", `providers.${provider}.accounts.${id}.company must be a non-empty company slug or UID`, { provider, account: id });
|
|
192
|
+
}
|
|
193
|
+
account.company = raw.company;
|
|
194
|
+
}
|
|
189
195
|
if (raw.hosts !== undefined) {
|
|
190
196
|
const field = `providers.${provider}.accounts.${id}.hosts`;
|
|
191
197
|
if (!isPlainObject(raw.hosts)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync as nodeSpawnSync } from "node:child_process";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { LanesError } from "./errors.js";
|
|
4
|
+
import { liveNodeBinary } from "./node-binary.js";
|
|
4
5
|
const LANE_RUNNER_ENTRY_NAMES = new Set(["watcher-main", "worker-process", "remote-control-lane"]);
|
|
5
6
|
/**
|
|
6
7
|
* True for the detached lane runner scripts. They are not the hq CLI: running
|
|
@@ -42,7 +43,7 @@ export function spawnEnvelopeRepairViaCurrentCli(hqRoot, lane, input, deps = {})
|
|
|
42
43
|
isLaneRunnerEntry(cliEntry)) {
|
|
43
44
|
throw new LanesError("repair_cli_entry_invalid", "Lane watcher has no valid absolute hq CLI entrypoint for envelope repair", { lane_id: lane.lane_id });
|
|
44
45
|
}
|
|
45
|
-
const result = run(
|
|
46
|
+
const result = run(liveNodeBinary(), [cliEntry, "lanes", "_spawn-envelope-repair"], {
|
|
46
47
|
encoding: "utf8",
|
|
47
48
|
timeout: 120_000,
|
|
48
49
|
input: JSON.stringify(request),
|
|
@@ -25,6 +25,7 @@ import * as path from "node:path";
|
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { parse as parseToml, TomlError } from "smol-toml";
|
|
27
27
|
import { LanesError } from "./errors.js";
|
|
28
|
+
import { liveNodeBinary } from "./node-binary.js";
|
|
28
29
|
import { parseLaneProvider } from "./types.js";
|
|
29
30
|
export const DRAIN_HOOK_BASENAME = "hq-lanes-drain";
|
|
30
31
|
export const DRAIN_HOOK_TIMEOUT_SECONDS = 30;
|
|
@@ -49,7 +50,7 @@ export const PROVIDER_REGISTRATION_KIND = {
|
|
|
49
50
|
};
|
|
50
51
|
export function drainHookCommand(_shPath, provider) {
|
|
51
52
|
const providerEnv = provider === "grok" ? "HQ_LANE_PROVIDER=grok " : "";
|
|
52
|
-
return `${providerEnv}${shellQuote(
|
|
53
|
+
return `${providerEnv}${shellQuote(liveNodeBinary())} ${shellQuote(drainHookEntryPath())}`;
|
|
53
54
|
}
|
|
54
55
|
function legacyDrainHookCommand(shPath) {
|
|
55
56
|
return `bash ${shPath}`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AuthState } from "./accounts.js";
|
|
2
|
+
import type { ClaudeCredentialReader } from "./claude-credential.js";
|
|
2
3
|
import { type QuotaState } from "./quota.js";
|
|
3
4
|
import type { LaneHost, LaneProvider, LaneRecord, LanesConfig } from "./types.js";
|
|
4
5
|
export interface HostCapacityAccount {
|
|
@@ -63,6 +64,7 @@ export declare function buildHostCapacitySnapshot(input: {
|
|
|
63
64
|
hostname?: string;
|
|
64
65
|
now: Date;
|
|
65
66
|
quotaState?: QuotaState;
|
|
67
|
+
readClaudeCredential?: ClaudeCredentialReader;
|
|
66
68
|
}): HostCapacitySnapshot;
|
|
67
69
|
/** Reject malformed remote snapshots before they can influence host selection. */
|
|
68
70
|
export declare function parseHostCapacitySnapshot(raw: unknown): HostCapacitySnapshot;
|
|
@@ -31,6 +31,7 @@ export function buildHostCapacitySnapshot(input) {
|
|
|
31
31
|
...(input.probe !== undefined ? { probe: input.probe } : {}),
|
|
32
32
|
...(input.env !== undefined ? { env: input.env } : {}),
|
|
33
33
|
...(input.hostname !== undefined ? { hostname: input.hostname } : {}),
|
|
34
|
+
...(input.readClaudeCredential !== undefined ? { readClaudeCredential: input.readClaudeCredential } : {}),
|
|
34
35
|
});
|
|
35
36
|
const accountLanes = providerLanes.filter((lane) => lane.account === id);
|
|
36
37
|
const liveLanes = accountLanes.filter((lane) => lane.state === "running" || lane.state === "queued" || lane.state === "awaiting_input");
|
|
@@ -30,7 +30,7 @@ export declare function queueLaneTerminalResume(current: LaneRecord, at: string,
|
|
|
30
30
|
trigger?: "resume" | "watcher";
|
|
31
31
|
}): LaneTerminalResumeResult;
|
|
32
32
|
/** Persist a manager status miss and queue its one bounded follow-up turn. */
|
|
33
|
-
export declare function queueManagerStatusGateResume(hqRoot: string, laneId: string, entry: Omit<ManagerStatusGateHistoryEntry, "event" | "resume_queued" | "resume_skipped_reason">, resumeText: string): LaneRecord;
|
|
33
|
+
export declare function queueManagerStatusGateResume(hqRoot: string, laneId: string, entry: Omit<ManagerStatusGateHistoryEntry, "event" | "resume_queued" | "resume_skipped_reason">, resumeText: string, skip?: (current: LaneRecord) => boolean): LaneRecord;
|
|
34
34
|
export declare const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
35
35
|
export declare const LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT = "You ended blocked with no pending question. Record the event you wait on in waiting_on, re-check each blocker, and continue the next unblocked story.";
|
|
36
36
|
export declare const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
@@ -75,8 +75,10 @@ export function queueLaneTerminalResume(current, at, resumeText, options = {}) {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
/** Persist a manager status miss and queue its one bounded follow-up turn. */
|
|
78
|
-
export function queueManagerStatusGateResume(hqRoot, laneId, entry, resumeText) {
|
|
78
|
+
export function queueManagerStatusGateResume(hqRoot, laneId, entry, resumeText, skip = () => false) {
|
|
79
79
|
return retryUpdateLane(hqRoot, laneId, "watcher", (current) => {
|
|
80
|
+
if (skip(current))
|
|
81
|
+
return current;
|
|
80
82
|
const gate = current.status_gate;
|
|
81
83
|
if (gate === undefined || gate.turn_id !== entry.turn_id)
|
|
82
84
|
return current;
|
package/dist/lib/lanes/links.js
CHANGED
|
@@ -117,6 +117,9 @@ function resolvePointer(root, sid, role, options) {
|
|
|
117
117
|
if (role === "child" && !safeId(child))
|
|
118
118
|
throw new LaneLinkError("child_name_invalid", `Invalid child name: ${child ?? ""}`);
|
|
119
119
|
const link = options.link ?? pointer.link;
|
|
120
|
+
if (role === "conductor" && link !== pointer.link) {
|
|
121
|
+
throw new LaneLinkError("link_role_invalid", "A conductor can only consume or wait on its own link");
|
|
122
|
+
}
|
|
120
123
|
if (role === "child") {
|
|
121
124
|
if (options.link !== undefined && !safeId(options.link))
|
|
122
125
|
throw new LaneLinkError("link_invalid", "Invalid --link");
|
|
@@ -412,15 +415,18 @@ export async function runLaneLink(subcommand, options) {
|
|
|
412
415
|
}
|
|
413
416
|
if (subcommand === "inbox" || subcommand === "read") {
|
|
414
417
|
const role = subcommand === "inbox" ? "child" : "conductor";
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
+
// Preserve explicit-link validation errors before resolving ownership.
|
|
419
|
+
if (role === "conductor" && options.link !== undefined)
|
|
420
|
+
linkPath(root, options.link);
|
|
421
|
+
const resolved = resolvePointer(root, requireSid(), role, options);
|
|
418
422
|
linkPath(root, resolved.link);
|
|
419
423
|
const text = drainLinkQueue(queueDir(root, resolved.link, role === "child" ? resolved.child : undefined));
|
|
420
424
|
return { output: { messages: text } };
|
|
421
425
|
}
|
|
422
426
|
if (subcommand === "wait") {
|
|
423
|
-
|
|
427
|
+
if (options.link !== undefined)
|
|
428
|
+
linkPath(root, options.link);
|
|
429
|
+
const resolved = resolvePointer(root, requireSid(), "conductor", options);
|
|
424
430
|
return await waitForReport(linkPath(root, resolved.link), options.timeout ?? 1800, options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))));
|
|
425
431
|
}
|
|
426
432
|
if (subcommand === "list") {
|
package/dist/lib/lanes/loop.js
CHANGED
|
@@ -1002,6 +1002,21 @@ function appendJournal(runDir, value) {
|
|
|
1002
1002
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1003
1003
|
fs.appendFileSync(file, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
1004
1004
|
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Where a synthetic failed or blocked handoff goes. The phase worker may have
|
|
1007
|
+
* written its own handoff to result_path already (for example one that failed
|
|
1008
|
+
* validation). That file is the only copy of the worker's evidence, so it is
|
|
1009
|
+
* never overwritten: the synthetic handoff is written beside it instead
|
|
1010
|
+
* (`<name>.lane-failure.json`). With no worker file, result_path itself gets
|
|
1011
|
+
* the synthetic handoff so the pipeline driver still sees the phase end.
|
|
1012
|
+
*/
|
|
1013
|
+
function syntheticHandoffPath(resultPath) {
|
|
1014
|
+
if (!fs.existsSync(resultPath))
|
|
1015
|
+
return resultPath;
|
|
1016
|
+
const ext = path.extname(resultPath);
|
|
1017
|
+
const base = ext ? resultPath.slice(0, -ext.length) : resultPath;
|
|
1018
|
+
return `${base}.lane-failure${ext || ".json"}`;
|
|
1019
|
+
}
|
|
1005
1020
|
function writeResultAt(runDir, activeFile, record, result, includeHandoffValidationError = true) {
|
|
1006
1021
|
const localPath = path.join(resultsDir(runDir), resultName(record.envelope_id));
|
|
1007
1022
|
atomicJson(localPath, result);
|
|
@@ -1025,7 +1040,7 @@ function writeResultAt(runDir, activeFile, record, result, includeHandoffValidat
|
|
|
1025
1040
|
back_pressure: { tests: "skip", lint: "skip", typecheck: "skip", build: "skip" },
|
|
1026
1041
|
context_for_next: [result.error ?? result.status, ...(result.question_id ? [`Decision question: ${result.question_id}`] : [])].join("\n"),
|
|
1027
1042
|
};
|
|
1028
|
-
atomicJson(path.resolve(externalPath), failure);
|
|
1043
|
+
atomicJson(syntheticHandoffPath(path.resolve(externalPath)), failure);
|
|
1029
1044
|
}
|
|
1030
1045
|
}
|
|
1031
1046
|
else
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node binary to use for a child process started by a long-lived lane
|
|
3
|
+
* process (loop worker, watcher, drain hook registration).
|
|
4
|
+
*
|
|
5
|
+
* `process.execPath` is fixed when the process starts. A Node upgrade can
|
|
6
|
+
* remove that file while the process keeps running (Homebrew deletes the old
|
|
7
|
+
* Cellar directory), and every later child then fails to start. When the
|
|
8
|
+
* launch-time binary is gone:
|
|
9
|
+
*
|
|
10
|
+
* 1. For a Homebrew Cellar path (`<prefix>/Cellar/<formula>/<version>/bin/node`),
|
|
11
|
+
* use the stable `<prefix>/bin/node` symlink when it exists. That points at
|
|
12
|
+
* the upgraded install of the same prefix and does not depend on PATH or on
|
|
13
|
+
* a version manager shim reading the child's cwd.
|
|
14
|
+
* 2. Otherwise fall back to `node` resolved through the child env PATH.
|
|
15
|
+
*/
|
|
16
|
+
export declare function liveNodeBinary(execPath?: string, exists?: (candidate: string) => boolean): string;
|
|
17
|
+
//# sourceMappingURL=node-binary.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* The Node binary to use for a child process started by a long-lived lane
|
|
5
|
+
* process (loop worker, watcher, drain hook registration).
|
|
6
|
+
*
|
|
7
|
+
* `process.execPath` is fixed when the process starts. A Node upgrade can
|
|
8
|
+
* remove that file while the process keeps running (Homebrew deletes the old
|
|
9
|
+
* Cellar directory), and every later child then fails to start. When the
|
|
10
|
+
* launch-time binary is gone:
|
|
11
|
+
*
|
|
12
|
+
* 1. For a Homebrew Cellar path (`<prefix>/Cellar/<formula>/<version>/bin/node`),
|
|
13
|
+
* use the stable `<prefix>/bin/node` symlink when it exists. That points at
|
|
14
|
+
* the upgraded install of the same prefix and does not depend on PATH or on
|
|
15
|
+
* a version manager shim reading the child's cwd.
|
|
16
|
+
* 2. Otherwise fall back to `node` resolved through the child env PATH.
|
|
17
|
+
*/
|
|
18
|
+
export function liveNodeBinary(execPath = process.execPath, exists = fs.existsSync) {
|
|
19
|
+
if (exists(execPath))
|
|
20
|
+
return execPath;
|
|
21
|
+
const parts = execPath.split(path.sep);
|
|
22
|
+
const cellar = parts.lastIndexOf("Cellar");
|
|
23
|
+
if (cellar > 0) {
|
|
24
|
+
const prefixLink = path.join(parts.slice(0, cellar).join(path.sep) || path.sep, "bin", "node");
|
|
25
|
+
if (exists(prefixLink))
|
|
26
|
+
return prefixLink;
|
|
27
|
+
}
|
|
28
|
+
return "node";
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=node-binary.js.map
|
|
@@ -27,7 +27,7 @@ export interface OutpostDispatchResult {
|
|
|
27
27
|
stderr: string;
|
|
28
28
|
}
|
|
29
29
|
export interface OutpostLaneCommand {
|
|
30
|
-
action: "message" | "resume" | "stop" | "show" | "logs" | "grant" | "grant-target" | "reparent-target" | "approve-target" | "repair-company-target" | "blocked-escalation-target";
|
|
30
|
+
action: "message" | "resume" | "stop" | "show" | "logs" | "grant" | "grant-target" | "reparent-target" | "approve-target" | "repair-company-target" | "set-target" | "blocked-escalation-target";
|
|
31
31
|
laneId: string;
|
|
32
32
|
hqBinary?: string;
|
|
33
33
|
text?: string;
|
|
@@ -46,6 +46,7 @@ export interface OutpostLaneCommand {
|
|
|
46
46
|
profileRepinContext?: string;
|
|
47
47
|
approvalContext?: string;
|
|
48
48
|
senior?: string;
|
|
49
|
+
actor?: string;
|
|
49
50
|
reason?: string;
|
|
50
51
|
company?: string;
|
|
51
52
|
compact?: boolean;
|
|
@@ -225,6 +225,7 @@ export function buildOutpostLaneCommand(input) {
|
|
|
225
225
|
["--approval-context", input.approvalContext],
|
|
226
226
|
["--failed-resume-context", input.failedResumeContext],
|
|
227
227
|
["--senior", input.senior],
|
|
228
|
+
["--actor", input.actor],
|
|
228
229
|
["--reason", input.reason],
|
|
229
230
|
["--company", input.company],
|
|
230
231
|
["--message-id", input.messageId],
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* compare-and-set. The per-lane watcher owns progress/terminal/silence of a
|
|
6
6
|
* live process — this pass owns the transitions no single lane can own.
|
|
7
7
|
*/
|
|
8
|
+
import { accountMatchesCompany } from "./account-company.js";
|
|
8
9
|
import * as fs from "node:fs";
|
|
9
10
|
import * as path from "node:path";
|
|
10
11
|
import { parseUserYaml } from "../../utils/user-yaml-error.js";
|
|
@@ -724,6 +725,8 @@ function availableBalancedAccount(hqRoot, lane, occupying, config, host, profile
|
|
|
724
725
|
for (const account of lane.admission_accounts) {
|
|
725
726
|
if (account === lane.account)
|
|
726
727
|
continue;
|
|
728
|
+
if (!accountMatchesCompany(config.providers[lane.provider].accounts[account], resolveLaneCompany(hqRoot, lane).company, config.providers[lane.provider].accounts))
|
|
729
|
+
continue;
|
|
727
730
|
if (config.providers[lane.provider].accounts[account]?.enabled !== true)
|
|
728
731
|
continue;
|
|
729
732
|
const status = accountStatus(lane.provider, account, config, {
|
|
@@ -754,6 +757,7 @@ function availableBalancedAccount(hqRoot, lane, occupying, config, host, profile
|
|
|
754
757
|
function writeAdmissionAccount(hqRoot, lane, account, dryRun) {
|
|
755
758
|
return casLaneIf(hqRoot, lane.lane_id, (current) => current.state === "queued" &&
|
|
756
759
|
current.account === lane.account &&
|
|
760
|
+
accountMatchesCompany(loadLanesConfig(hqRoot).providers[current.provider].accounts[account], resolveLaneCompany(hqRoot, current).company, loadLanesConfig(hqRoot).providers[current.provider].accounts) &&
|
|
757
761
|
current.admission_accounts?.includes(account) === true, (current) => ({ ...current, account }), dryRun).record;
|
|
758
762
|
}
|
|
759
763
|
function casQuestionIf(hqRoot, questionId, shouldWrite, updater, dryRun) {
|
|
@@ -15,6 +15,7 @@ import { isOwnedPoolForwarder, poolHostsFile, readRemoteControlPoolsConfig } fro
|
|
|
15
15
|
import { accountStatus, isDefaultProviderHome } from "./accounts.js";
|
|
16
16
|
import { effectiveAccountHome } from "./account-home.js";
|
|
17
17
|
import { LanesError } from "./errors.js";
|
|
18
|
+
import { readLaneClaudeCredential } from "./claude-credential.js";
|
|
18
19
|
import { defaultProbe } from "./models.js";
|
|
19
20
|
import { encodeClaudeProjectDir } from "./move-account.js";
|
|
20
21
|
/** Keep every credential and network route tied to the selected pool. */
|
|
@@ -117,6 +118,7 @@ export function prepareRemoteControlLane(input) {
|
|
|
117
118
|
}
|
|
118
119
|
const status = accountStatus("claude", "a", input.config, {
|
|
119
120
|
hostname: input.hostname, existsSync: fs.existsSync, probe: defaultProbe, env: input.env,
|
|
121
|
+
readClaudeCredential: readLaneClaudeCredential,
|
|
120
122
|
});
|
|
121
123
|
if (!status.logged_in) {
|
|
122
124
|
throw new LanesError("remote_control_login_missing", "Claude account a must be logged in for Remote Control", {});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* mutable fields are re-read from the lane record (spec §5.4).
|
|
9
9
|
*/
|
|
10
10
|
import { PROVIDER_HOME_ENV } from "./accounts.js";
|
|
11
|
+
import { type ClaudeCredentialReader } from "./claude-credential.js";
|
|
11
12
|
import { type RunCompactionFn } from "./compaction.js";
|
|
12
13
|
import { type ResolvedEffort } from "./effort.js";
|
|
13
14
|
import { type KnownModelsDeps } from "./models.js";
|
|
@@ -66,6 +67,8 @@ export type ProveDetachedFn = (pid: number) => DetachProof;
|
|
|
66
67
|
export type KillProcessFn = (pid: number, signal?: NodeJS.Signals) => void;
|
|
67
68
|
export interface SpawnLaneDeps {
|
|
68
69
|
env?: NodeJS.ProcessEnv;
|
|
70
|
+
/** Claude credential expiry check; see spawnCredentialReader for the default. */
|
|
71
|
+
readClaudeCredential?: ClaudeCredentialReader;
|
|
69
72
|
/** Builds the lane's fingerprint from the worker env. Defaults to laneStartFingerprint. */
|
|
70
73
|
buildFingerprint?: (hqRoot: string, workerEnv: NodeJS.ProcessEnv) => string | null;
|
|
71
74
|
/** Test seam for the default-off quota account failover registry gate. */
|