@indigoai-us/hq-cli 5.294.0 → 5.295.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 +25 -0
- package/dist/command-catalog.generated.d.ts +12 -9
- package/dist/command-catalog.generated.js +15 -12
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +76 -1
- package/dist/commands/doctor.js +0 -74
- package/dist/commands/lanes-message.js +23 -5
- package/dist/commands/lanes-resume.js +0 -3
- package/dist/commands/lanes-shared.d.ts +1 -2
- package/dist/commands/lanes.d.ts +1 -9
- package/dist/commands/lanes.js +177 -172
- package/dist/fast-scaffold.d.ts +0 -5
- package/dist/fast-scaffold.js +0 -10
- package/dist/index.js +1 -10
- package/dist/lib/core-utils/sentry-report.d.ts +0 -19
- package/dist/lib/core-utils/sentry-report.js +8 -61
- package/dist/lib/doctor/checks/claude-wiring.js +9 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/manager-status-gate.d.ts +0 -17
- package/dist/lib/lanes/manager-status-gate.js +2 -43
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -0
- package/dist/lib/lanes/outpost-dispatch.js +7 -1
- package/dist/lib/lanes/outpost-hire-cache.d.ts +0 -1
- package/dist/lib/lanes/outpost-hire-cache.js +0 -1
- package/dist/lib/lanes/outpost-snapshot-memo.d.ts +6 -0
- package/dist/lib/lanes/outpost-snapshot-memo.js +14 -0
- package/dist/lib/lanes/reconcile.d.ts +2 -3
- package/dist/lib/lanes/reconcile.js +44 -38
- package/dist/lib/lanes/spawn.d.ts +0 -2
- package/dist/lib/lanes/spawn.js +18 -5
- package/dist/lib/lanes/store.js +22 -0
- package/dist/lib/lanes/types.d.ts +18 -1
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.d.ts +4 -3
- package/dist/lib/lanes/watcher.js +102 -34
- package/dist/lib/mesh/live/daemon/run.js +18 -1
- package/dist/utils/cognito-session.d.ts +6 -0
- package/dist/utils/cognito-session.js +9 -0
- package/dist/utils/self-update.js +31 -0
- package/dist/utils/version-gate.js +22 -3
- package/package.json +1 -1
- package/dist/fast-lanes-list-route.d.ts +0 -8
- package/dist/fast-lanes-list-route.js +0 -32
- package/dist/fast-lanes-list.d.ts +0 -3
- package/dist/fast-lanes-list.js +0 -224
- package/dist/lib/doctor/client-health-sentry-flag.d.ts +0 -23
- package/dist/lib/doctor/client-health-sentry-flag.js +0 -99
- package/dist/lib/doctor/client-health-sentry.d.ts +0 -71
- package/dist/lib/doctor/client-health-sentry.js +0 -286
- package/dist/lib/lanes/outpost-snapshot-cache.d.ts +0 -26
- package/dist/lib/lanes/outpost-snapshot-cache.js +0 -215
- package/dist/lib/lanes/outpost-snapshot-refresh.d.ts +0 -21
- package/dist/lib/lanes/outpost-snapshot-refresh.js +0 -329
package/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.295.0] — 2026-09-30
|
|
6
|
+
- Remove the unused direct client-health Sentry route; the existing server-side reporting path remains unchanged.
|
|
7
|
+
|
|
8
|
+
- Self-update supervisor copy and cleanup failures now log their error class.
|
|
9
|
+
- Report failed and ineffective self-updates to Sentry with bounded context.
|
|
10
|
+
|
|
11
|
+
- hq-cli release notes now use per-branch fragments that are folded into the next version section, reducing conflicts between parallel pull requests.
|
|
12
|
+
- Claude keep-alive resume events now retain a compact account-load selection snapshot, including when the lane stays on its current account.
|
|
13
|
+
- After a successful missing-envelope repair reports unfinished work, the watcher can queue one automatic continue behind the default-off `lanes.envelope-repair-continue` flag. The repair prompt now gives the required unfinished-story envelope fields.
|
|
14
|
+
- Add a private machine-session token-file command for supported machine-auth handoffs.
|
|
15
|
+
- Apply the 5x fast-route speedup gate only when the base p95 is at or above the 200 ms target; retain the p95 latency and base/PR regression gates.
|
|
16
|
+
- The changelog fragment check runs only on pull requests again, so pushes to main no longer fail it.
|
|
17
|
+
- Refresh terminal Outpost lane mirrors before applying `hq lanes list` state filters, so a lane whose owner is still running is listed as running.
|
|
18
|
+
- Remove the unused Outpost snapshot cache flag and its cache-first lane-list path.
|
|
19
|
+
- The mesh daemon logs spool watcher failures and continues flushing on its polling interval instead of exiting on an unhandled watcher error.
|
|
20
|
+
- Make the manager status gate always active and remove its rollout flag lookup.
|
|
21
|
+
- Compare fast-route latency with a PR-build full-graph baseline so the 5x gate remains valid after the base build gains fast routing.
|
|
22
|
+
|
|
5
23
|
## [5.294.0] — 2026-09-30
|
|
24
|
+
- Record automatic keep-alive resumes in each lane's durable, bounded history and expose it in `hq lanes show --json` and `hq lanes list --json`.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
6
27
|
|
|
28
|
+
- Outpost `hq lanes message` now reaches primary-hosted lanes through the
|
|
29
|
+
primary owner command, which checks sender rights and includes the
|
|
30
|
+
destination in JSON output.
|
|
7
31
|
- Split the lane story command handlers into a dedicated module without changing behavior.
|
|
8
32
|
- On a box, `sudo hq daemon migrate --apply` now finds the login user's own systemd user units (hq-mesh-daemon, hq-lanes-reconcile and the rest) and stops them through that user's systemd manager. `sudo hq daemon uninstall --restore-legacy` restarts them the same way. Before, root looked only in its own home and could not reach the login user's manager.
|
|
9
33
|
|
|
10
34
|
## [5.293.0] — 2026-09-30
|
|
11
35
|
- [HQ-CLI-HOOK-TIMEOUT-KIND] Group hook timeout events by normalized kind (`warning` or `exceeded`) across scripts; keep script and platform tags for triage. Existing per-script issues will stop receiving new events, and the lead handles their status.
|
|
36
|
+
- [HQ-CLI-2C] Label unmeasured SessionStart delay as entry Bash startup when BASH_ENV is set; doctor follows the env-prefixed hook command.
|
|
12
37
|
- `hq lanes move` now reports an already-on-target no-op without dispatching or changing the queued lane.
|
|
13
38
|
- Keep bundled scheduled-job reconciliation units on the live scaffold runner, and preserve caller spelling in job-validation paths.
|
|
14
39
|
- On Linux, `hq daemon run` sets XDG_RUNTIME_DIR for its services when the system unit left it unset, so `systemctl --user` from jobs-reconcile no longer fails with "Failed to connect to bus: No medium found".
|
|
@@ -860,6 +860,18 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
860
860
|
readonly arguments: readonly [];
|
|
861
861
|
readonly options: readonly [];
|
|
862
862
|
readonly subcommands: readonly [{
|
|
863
|
+
readonly name: "machine-session";
|
|
864
|
+
readonly description: "Write the existing machine identity session to a private file";
|
|
865
|
+
readonly aliases: readonly [];
|
|
866
|
+
readonly hidden: false;
|
|
867
|
+
readonly usage: "[options]";
|
|
868
|
+
readonly arguments: readonly [];
|
|
869
|
+
readonly options: readonly [{
|
|
870
|
+
readonly flags: "--output <path>";
|
|
871
|
+
readonly description: "path for the private machine session JSON";
|
|
872
|
+
}];
|
|
873
|
+
readonly subcommands: readonly [];
|
|
874
|
+
}, {
|
|
863
875
|
readonly name: "login";
|
|
864
876
|
readonly description: "Sign in to HQ — opens the Cognito Hosted UI and caches tokens locally";
|
|
865
877
|
readonly aliases: readonly [];
|
|
@@ -6130,15 +6142,6 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
6130
6142
|
}, {
|
|
6131
6143
|
readonly flags: "--client-health-report";
|
|
6132
6144
|
readonly description: "Resolve the opt-in hq-flags gate used by the client-health report hook.";
|
|
6133
|
-
}, {
|
|
6134
|
-
readonly flags: "--client-health-sentry";
|
|
6135
|
-
readonly description: "Send the opt-in client-health Sentry report from a doctor JSON document.";
|
|
6136
|
-
}, {
|
|
6137
|
-
readonly flags: "--json-input <file>";
|
|
6138
|
-
readonly description: "Doctor JSON input for the client-health report.";
|
|
6139
|
-
}, {
|
|
6140
|
-
readonly flags: "--client-health-fix-outcome <outcome>";
|
|
6141
|
-
readonly description: "Safe fix outcome label for the client-health report.";
|
|
6142
6145
|
}, {
|
|
6143
6146
|
readonly flags: "--yes";
|
|
6144
6147
|
readonly description: "Skip the interactive --fix confirmation (non-interactive use).";
|
|
@@ -1097,6 +1097,21 @@ export const COMMAND_CATALOG = [
|
|
|
1097
1097
|
"arguments": [],
|
|
1098
1098
|
"options": [],
|
|
1099
1099
|
"subcommands": [
|
|
1100
|
+
{
|
|
1101
|
+
"name": "machine-session",
|
|
1102
|
+
"description": "Write the existing machine identity session to a private file",
|
|
1103
|
+
"aliases": [],
|
|
1104
|
+
"hidden": false,
|
|
1105
|
+
"usage": "[options]",
|
|
1106
|
+
"arguments": [],
|
|
1107
|
+
"options": [
|
|
1108
|
+
{
|
|
1109
|
+
"flags": "--output <path>",
|
|
1110
|
+
"description": "path for the private machine session JSON"
|
|
1111
|
+
}
|
|
1112
|
+
],
|
|
1113
|
+
"subcommands": []
|
|
1114
|
+
},
|
|
1100
1115
|
{
|
|
1101
1116
|
"name": "login",
|
|
1102
1117
|
"description": "Sign in to HQ — opens the Cognito Hosted UI and caches tokens locally",
|
|
@@ -7883,18 +7898,6 @@ export const COMMAND_CATALOG = [
|
|
|
7883
7898
|
"flags": "--client-health-report",
|
|
7884
7899
|
"description": "Resolve the opt-in hq-flags gate used by the client-health report hook."
|
|
7885
7900
|
},
|
|
7886
|
-
{
|
|
7887
|
-
"flags": "--client-health-sentry",
|
|
7888
|
-
"description": "Send the opt-in client-health Sentry report from a doctor JSON document."
|
|
7889
|
-
},
|
|
7890
|
-
{
|
|
7891
|
-
"flags": "--json-input <file>",
|
|
7892
|
-
"description": "Doctor JSON input for the client-health report."
|
|
7893
|
-
},
|
|
7894
|
-
{
|
|
7895
|
-
"flags": "--client-health-fix-outcome <outcome>",
|
|
7896
|
-
"description": "Safe fix outcome label for the client-health report."
|
|
7897
|
-
},
|
|
7898
7901
|
{
|
|
7899
7902
|
"flags": "--yes",
|
|
7900
7903
|
"description": "Skip the interactive --fix confirmation (non-interactive use)."
|
package/dist/commands/auth.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* hq auth logout — clear the cached HQ session
|
|
7
7
|
* hq auth refresh — ensure a valid cached Cognito session (non-interactive)
|
|
8
8
|
* hq auth status — show whether a valid session is cached + expiry
|
|
9
|
+
* hq auth machine-session --output <path> — write machine tokens to a private file
|
|
9
10
|
*
|
|
10
11
|
* Sign-up is owned by the onboarding web app at
|
|
11
12
|
* https://onboarding.hq.computer. `hq auth login` signs an existing account
|
package/dist/commands/auth.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* hq auth logout — clear the cached HQ session
|
|
7
7
|
* hq auth refresh — ensure a valid cached Cognito session (non-interactive)
|
|
8
8
|
* hq auth status — show whether a valid session is cached + expiry
|
|
9
|
+
* hq auth machine-session --output <path> — write machine tokens to a private file
|
|
9
10
|
*
|
|
10
11
|
* Sign-up is owned by the onboarding web app at
|
|
11
12
|
* https://onboarding.hq.computer. `hq auth login` signs an existing account
|
|
@@ -13,9 +14,12 @@
|
|
|
13
14
|
* session is kept valid by `hq auth refresh` / `hq-auth-refresh` and consumed
|
|
14
15
|
* by the deploy + sync skills.
|
|
15
16
|
*/
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import { open, realpath } from "node:fs/promises";
|
|
19
|
+
import path from "node:path";
|
|
16
20
|
import chalk from "chalk";
|
|
17
21
|
import { browserLogin, clearCachedTokens, loadCachedTokens, isExpiring, loadMachineCreds, CognitoAuthError, } from "@indigoai-us/hq-cloud";
|
|
18
|
-
import { DEFAULT_COGNITO, refreshCachedSession, resolveCognitoTokenSource, } from "../utils/cognito-session.js";
|
|
22
|
+
import { DEFAULT_COGNITO, refreshCachedSession, resolveCognitoTokenSource, ensureMachineSessionTokens, personTokenCacheFile, machineTokenCacheFile, } from "../utils/cognito-session.js";
|
|
19
23
|
import { resolveLoginProviderSelection, } from "../utils/login-provider.js";
|
|
20
24
|
import { getLastLoginProviderStatePath, loadLastLoginProvider, storeLastLoginProvider, } from "../lib/auth-login-provider-state.js";
|
|
21
25
|
import { callbackPortBusyGuidance, DEFAULT_CALLBACK_PORT, isCallbackPortBusy, } from "../utils/callback-port-busy.js";
|
|
@@ -128,6 +132,77 @@ export function registerAuthCommands(program) {
|
|
|
128
132
|
const authCmd = program
|
|
129
133
|
.command("auth")
|
|
130
134
|
.description("Manage the local HQ Cognito session");
|
|
135
|
+
authCmd
|
|
136
|
+
.command("machine-session")
|
|
137
|
+
.description("Write the existing machine identity session to a private file")
|
|
138
|
+
.requiredOption("--output <path>", "path for the private machine session JSON")
|
|
139
|
+
.action(async (options) => {
|
|
140
|
+
const requestedPath = path.resolve(options.output);
|
|
141
|
+
let handle;
|
|
142
|
+
try {
|
|
143
|
+
if (!loadMachineCreds()) {
|
|
144
|
+
throw new Error("Machine credentials are unavailable.");
|
|
145
|
+
}
|
|
146
|
+
const outputDirectory = await realpath(path.dirname(requestedPath));
|
|
147
|
+
const outputPath = path.join(outputDirectory, path.basename(requestedPath));
|
|
148
|
+
const protectedPaths = await Promise.all([personTokenCacheFile(), machineTokenCacheFile()].map(async (protectedPath) => {
|
|
149
|
+
const resolved = path.resolve(protectedPath);
|
|
150
|
+
try {
|
|
151
|
+
return path.join(await realpath(path.dirname(resolved)), path.basename(resolved));
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error.code === "ENOENT")
|
|
155
|
+
return resolved;
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}));
|
|
159
|
+
if (protectedPaths.includes(outputPath)) {
|
|
160
|
+
throw new Error("Refusing to overwrite a cached Cognito session.");
|
|
161
|
+
}
|
|
162
|
+
const tokens = await ensureMachineSessionTokens();
|
|
163
|
+
const contents = JSON.stringify({
|
|
164
|
+
accessToken: tokens.accessToken,
|
|
165
|
+
idToken: tokens.idToken,
|
|
166
|
+
expiresAt: tokens.expiresAt,
|
|
167
|
+
}) + "\n";
|
|
168
|
+
try {
|
|
169
|
+
handle = await open(outputPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL |
|
|
170
|
+
(fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error.code !== "EEXIST")
|
|
174
|
+
throw error;
|
|
175
|
+
handle = await open(outputPath, fs.constants.O_WRONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
176
|
+
const metadata = await handle.stat();
|
|
177
|
+
if (!metadata.isFile() ||
|
|
178
|
+
metadata.uid !== process.getuid?.() ||
|
|
179
|
+
(metadata.mode & 0o077) !== 0) {
|
|
180
|
+
throw new Error("Existing output file is not a private file owned by this user.");
|
|
181
|
+
}
|
|
182
|
+
await handle.truncate(0);
|
|
183
|
+
}
|
|
184
|
+
await handle.writeFile(contents, { encoding: "utf8" });
|
|
185
|
+
await handle.close();
|
|
186
|
+
handle = undefined;
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (handle)
|
|
190
|
+
await handle.close().catch(() => undefined);
|
|
191
|
+
const code = error && typeof error === "object" && "code" in error &&
|
|
192
|
+
typeof error.code === "string" && /^[A-Z0-9_]+$/.test(error.code)
|
|
193
|
+
? error.code
|
|
194
|
+
: undefined;
|
|
195
|
+
console.error(code === "EACCES" || code === "ELOOP" || code === "EEXIST"
|
|
196
|
+
? "Could not write machine session file (unsafe or inaccessible path)."
|
|
197
|
+
: error instanceof Error && error.message === "Machine credentials are unavailable."
|
|
198
|
+
? "Machine credentials are unavailable."
|
|
199
|
+
: error instanceof Error && (error.message === "Existing output file is not a private file owned by this user." ||
|
|
200
|
+
error.message === "Refusing to overwrite a cached Cognito session.")
|
|
201
|
+
? error.message
|
|
202
|
+
: "Could not obtain or write machine session.");
|
|
203
|
+
process.exitCode = 1;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
131
206
|
authCmd
|
|
132
207
|
.command("login")
|
|
133
208
|
.description("Sign in to HQ — opens the Cognito Hosted UI and caches tokens locally")
|
package/dist/commands/doctor.js
CHANGED
|
@@ -33,9 +33,6 @@ import { PARITY_FAMILY_ID, PARITY_FAMILY_TITLE, runParityReplay, } from "../lib/
|
|
|
33
33
|
import { applyFixes } from "../lib/doctor/fix/apply.js";
|
|
34
34
|
import { renderDoctorFixJson } from "../lib/doctor/fix/json-output.js";
|
|
35
35
|
import { clientHealthReasonCodesEnabled as isActiveCompanyClientHealthReasonCodesEnabled, clientHealthReasonCodesEnabledForCompany, } from "../lib/doctor/client-health-reason-codes-flag.js";
|
|
36
|
-
import { CLIENT_HEALTH_SENTRY_MAX_INPUT_BYTES, CLIENT_HEALTH_SENTRY_NOT_HANDLED_EXIT_CODE, handleClientHealthSentryReport, } from "../lib/doctor/client-health-sentry.js";
|
|
37
|
-
import { clientHealthSentryDirectEnabled, clientHealthSentryDirectEnabledForCompany, } from "../lib/doctor/client-health-sentry-flag.js";
|
|
38
|
-
import { reportClientHealthEvents } from "../lib/core-utils/sentry-report.js";
|
|
39
36
|
import { isCompanyJournalSlug } from "../lib/doctor/checks/sync-health.js";
|
|
40
37
|
import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
|
|
41
38
|
import * as readline from "node:readline";
|
|
@@ -244,26 +241,6 @@ function createClientHealthReasonCodeCompanyResolver(hqRoot) {
|
|
|
244
241
|
});
|
|
245
242
|
};
|
|
246
243
|
}
|
|
247
|
-
function createClientHealthSentryCompanyResolver(hqRoot) {
|
|
248
|
-
const companies = readCompaniesManifestMap(hqRoot);
|
|
249
|
-
return async (companySlug) => {
|
|
250
|
-
if (companySlug === null)
|
|
251
|
-
return clientHealthSentryDirectEnabled();
|
|
252
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(companySlug))
|
|
253
|
-
return false;
|
|
254
|
-
const manifestUid = companies?.[companySlug]?.cloud_uid;
|
|
255
|
-
const activeSlug = process.env.HQ_COMPANY_SLUG?.trim() || "";
|
|
256
|
-
const activeUid = process.env.HQ_COMPANY_UID?.trim() || "";
|
|
257
|
-
const companyUid = typeof manifestUid === "string" && manifestUid.trim()
|
|
258
|
-
? manifestUid.trim()
|
|
259
|
-
: companySlug === activeSlug
|
|
260
|
-
? activeUid
|
|
261
|
-
: "";
|
|
262
|
-
if (!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid))
|
|
263
|
-
return false;
|
|
264
|
-
return clientHealthSentryDirectEnabledForCompany({ companyUid, companySlug });
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
244
|
/** Register the top-level `hq doctor` command so it appears in `hq --help`. */
|
|
268
245
|
export function registerDoctorCommand(program) {
|
|
269
246
|
program
|
|
@@ -279,9 +256,6 @@ export function registerDoctorCommand(program) {
|
|
|
279
256
|
.option("--only <families>", "Run only these comma-separated check families (e.g. hooks). Unknown names are rejected; --json then records the scope.")
|
|
280
257
|
.option("--fix", "Apply the allowlisted safe repairs (backs up first; read-only without this flag).")
|
|
281
258
|
.addOption(new Option("--client-health-report", "Resolve the opt-in hq-flags gate used by the client-health report hook.").hideHelp())
|
|
282
|
-
.addOption(new Option("--client-health-sentry", "Send the opt-in client-health Sentry report from a doctor JSON document.").hideHelp())
|
|
283
|
-
.addOption(new Option("--json-input <file>", "Doctor JSON input for the client-health report.").hideHelp())
|
|
284
|
-
.addOption(new Option("--client-health-fix-outcome <outcome>", "Safe fix outcome label for the client-health report.").hideHelp())
|
|
285
259
|
.option("--yes", "Skip the interactive --fix confirmation (non-interactive use).")
|
|
286
260
|
.option("--force", "Let --fix run despite uncommitted changes under .claude/, .codex/, or .grok/.")
|
|
287
261
|
.action(async (opts) => {
|
|
@@ -298,54 +272,6 @@ export function registerDoctorCommand(program) {
|
|
|
298
272
|
process.exitCode = 1;
|
|
299
273
|
return;
|
|
300
274
|
}
|
|
301
|
-
if (opts.clientHealthSentry === true) {
|
|
302
|
-
const hqRoot = resolveHqRoot();
|
|
303
|
-
if (!hqRoot || !opts.jsonInput) {
|
|
304
|
-
process.stderr.write("hq doctor --client-health-sentry requires an HQ root and --json-input.\n");
|
|
305
|
-
process.exitCode = 1;
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
try {
|
|
309
|
-
const workspacePath = path.join(hqRoot, "workspace");
|
|
310
|
-
const workspaceStat = fs.lstatSync(workspacePath);
|
|
311
|
-
if (!workspaceStat.isDirectory() || workspaceStat.isSymbolicLink()) {
|
|
312
|
-
throw new Error("workspace is not a local directory");
|
|
313
|
-
}
|
|
314
|
-
const statePath = path.join(hqRoot, "workspace", ".hq-client-health");
|
|
315
|
-
const stateStat = fs.lstatSync(statePath);
|
|
316
|
-
if (!stateStat.isDirectory() || stateStat.isSymbolicLink()) {
|
|
317
|
-
throw new Error("client-health state is not a local directory");
|
|
318
|
-
}
|
|
319
|
-
const stateRoot = fs.realpathSync(statePath);
|
|
320
|
-
const inputPath = fs.realpathSync(opts.jsonInput);
|
|
321
|
-
const relativeInput = path.relative(stateRoot, inputPath);
|
|
322
|
-
if (relativeInput === ".." || relativeInput.startsWith(`..${path.sep}`) || path.isAbsolute(relativeInput)) {
|
|
323
|
-
throw new Error("input path is outside client-health state");
|
|
324
|
-
}
|
|
325
|
-
if (fs.statSync(inputPath).size > CLIENT_HEALTH_SENTRY_MAX_INPUT_BYTES) {
|
|
326
|
-
throw new Error("input exceeds client-health size limit");
|
|
327
|
-
}
|
|
328
|
-
const result = await handleClientHealthSentryReport({
|
|
329
|
-
hqRoot,
|
|
330
|
-
doctorJson: fs.readFileSync(inputPath, "utf8"),
|
|
331
|
-
fixOutcome: opts.clientHealthFixOutcome ?? "not_auto_fixable",
|
|
332
|
-
}, {
|
|
333
|
-
resolveFlagForCompany: createClientHealthSentryCompanyResolver(hqRoot),
|
|
334
|
-
sendEvents: async (events) => (await reportClientHealthEvents(events)).code === 0,
|
|
335
|
-
});
|
|
336
|
-
process.exitCode =
|
|
337
|
-
result === "not_handled"
|
|
338
|
-
? CLIENT_HEALTH_SENTRY_NOT_HANDLED_EXIT_CODE
|
|
339
|
-
: result === "send_failed"
|
|
340
|
-
? 1
|
|
341
|
-
: 0;
|
|
342
|
-
}
|
|
343
|
-
catch {
|
|
344
|
-
process.stderr.write("hq doctor --client-health-sentry could not handle the report.\n");
|
|
345
|
-
process.exitCode = 1;
|
|
346
|
-
}
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
275
|
// `--fix` is the only write path. It resolves the tree, applies the
|
|
350
276
|
// allowlisted repairs behind a backup + confirmation, and returns its own
|
|
351
277
|
// exit code; the read-only report below never runs in this branch.
|
|
@@ -10,12 +10,11 @@ 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, keepAliveSupersededBy, LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
|
|
13
|
-
import { managerStatusGateEnabledForLane } from "../lib/lanes/manager-status-gate.js";
|
|
14
13
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
15
14
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
16
15
|
import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, callerMatchesSessionSenior, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
|
|
17
16
|
export function createLaneMessageHandlers(deps) {
|
|
18
|
-
const { archiveResumeMessage, assertLaneRunsHereForAdmission, assertLaneRunsHereForOperatorMutation, assessLaneAdmission, casUpdateLane, dispatchOutpostLaneCommand, drainHookStatusFor, laneIsHostedHere, laneWithRecoveredProviderSession, loadAllLanes, mintMessageId, persistClaudeAccountSelection, prepareAccountMove, printJson, recordResumeSetup, resolveActiveQuestionCompany, resolveResumeProfile, selectAutomaticClaudeResumeAccount, validateResumeProfileModel, withResumeHireAdmission } = deps;
|
|
17
|
+
const { archiveResumeMessage, assertLaneRunsHereForAdmission, assertLaneRunsHereForOperatorMutation, assessLaneAdmission, casUpdateLane, dispatchOutpostLaneCommand, dispatchPrimaryLaneMessage, drainHookStatusFor, laneIsHostedHere, laneWithRecoveredProviderSession, loadAllLanes, mintMessageId, persistClaudeAccountSelection, prepareAccountMove, printJson, recordResumeSetup, resolveActiveQuestionCompany, resolveResumeProfile, selectAutomaticClaudeResumeAccount, validateResumeProfileModel, withResumeHireAdmission } = deps;
|
|
19
18
|
function currentSuccessorOf(lane, lanes, lineageAware, companyForLane) {
|
|
20
19
|
const validLanes = lanes.filter((candidate) => Number.isFinite(Date.parse(candidate.created_at)));
|
|
21
20
|
const newestFirst = [...validLanes].sort((left, right) => Date.parse(right.created_at) - Date.parse(left.created_at));
|
|
@@ -221,7 +220,27 @@ export function createLaneMessageHandlers(deps) {
|
|
|
221
220
|
if (!text) {
|
|
222
221
|
throw new LanesError("missing_text", "--text is required", {});
|
|
223
222
|
}
|
|
224
|
-
|
|
223
|
+
let resolution;
|
|
224
|
+
try {
|
|
225
|
+
resolution = await deps.resolveCommandLane(ctx, laneId);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
const identity = ctx.readOutpostIdentity();
|
|
229
|
+
const failedOutpostIds = err instanceof LanesError ? err.details.failed_outpost_ids : undefined;
|
|
230
|
+
const probedOutpostIds = err instanceof LanesError ? err.details.probed_outpost_ids : undefined;
|
|
231
|
+
const primaryLookupFailed = Array.isArray(failedOutpostIds) && failedOutpostIds.includes("primary");
|
|
232
|
+
const primaryReportedMissing = Array.isArray(probedOutpostIds) && probedOutpostIds.includes("primary");
|
|
233
|
+
if (!(err instanceof LanesError) ||
|
|
234
|
+
err.error !== "lane_not_found" ||
|
|
235
|
+
identity.kind !== "id" ||
|
|
236
|
+
identity.id === "primary" ||
|
|
237
|
+
primaryLookupFailed ||
|
|
238
|
+
primaryReportedMissing) {
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
await dispatchPrimaryLaneMessage(ctx, laneId, { ...opts, text });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
225
244
|
let lane = resolution.lane;
|
|
226
245
|
if (resolution.remoteOnly || (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane))) {
|
|
227
246
|
const callerLaneId = ctx.env.HQ_LANE_ID?.trim();
|
|
@@ -465,7 +484,6 @@ export function createLaneMessageHandlers(deps) {
|
|
|
465
484
|
markAccepted();
|
|
466
485
|
}
|
|
467
486
|
else {
|
|
468
|
-
const managerStatusGateEnabled = await managerStatusGateEnabledForLane(hqRoot, lane, { env: ctx.env });
|
|
469
487
|
const dispatched = await dispatchLaneDelivery(lane, {
|
|
470
488
|
dropbox: () => {
|
|
471
489
|
// Live (or about-to-be-live) process: the PreToolUse drain delivers it.
|
|
@@ -523,7 +541,6 @@ export function createLaneMessageHandlers(deps) {
|
|
|
523
541
|
}
|
|
524
542
|
const started = spawnLaneWorker(hqRoot, lane, {
|
|
525
543
|
env: ctx.env,
|
|
526
|
-
managerStatusGateEnabled,
|
|
527
544
|
modelDeps: {
|
|
528
545
|
readFileSync: ctx.readFileSync,
|
|
529
546
|
probe: ctx.probeProvider,
|
|
@@ -624,6 +641,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
624
641
|
accepted: true,
|
|
625
642
|
delivered: false,
|
|
626
643
|
delivery: mechanism,
|
|
644
|
+
delivered_to: { host: lane.host.id, lane_id: laneId, mechanism },
|
|
627
645
|
delivery_status: deliveryStatus,
|
|
628
646
|
inbox_path: mechanism === "dropbox" ? dropboxInboxPath(hqRoot, laneId) : null,
|
|
629
647
|
drain_hook: drainHook,
|
|
@@ -5,7 +5,6 @@ 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";
|
|
9
8
|
import { spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
10
9
|
import { accountMoveSummary, accountRequestKeepsCurrentLane, assertCurrentResumeAccountAvailable, hasKeepAliveProgress, recordedResumeEffort, resetKeepAliveProgress, resolveResumeEffort } from "./lanes-shared.js";
|
|
11
10
|
export function createLaneResumeHandlers(deps) {
|
|
@@ -328,7 +327,6 @@ export function createLaneResumeHandlers(deps) {
|
|
|
328
327
|
resumed = lane;
|
|
329
328
|
}
|
|
330
329
|
else {
|
|
331
|
-
const managerStatusGateEnabled = await managerStatusGateEnabledForLane(hqRoot, lane, { env: ctx.env });
|
|
332
330
|
resumed = await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
333
331
|
const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
|
|
334
332
|
if (admission.action === "refuse" && admission.error)
|
|
@@ -376,7 +374,6 @@ export function createLaneResumeHandlers(deps) {
|
|
|
376
374
|
}
|
|
377
375
|
return spawnLaneWorker(hqRoot, lane, {
|
|
378
376
|
env: ctx.env,
|
|
379
|
-
managerStatusGateEnabled,
|
|
380
377
|
modelDeps: {
|
|
381
378
|
readFileSync: ctx.readFileSync,
|
|
382
379
|
probe: ctx.probeProvider,
|
|
@@ -157,6 +157,7 @@ export interface LaneMessageHandlerDeps {
|
|
|
157
157
|
}): Promise<AdmissionResult>;
|
|
158
158
|
casUpdateLane(hqRoot: string, laneId: string, updater: (current: LaneRecord) => LaneRecord): LaneRecord;
|
|
159
159
|
dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "message" | "resume", options: LaneMessageOptions | LaneResumeOptions, behavior?: OutpostLaneDispatchBehavior): Promise<void>;
|
|
160
|
+
dispatchPrimaryLaneMessage(ctx: Ctx, laneId: string, options: LaneMessageOptions): Promise<void>;
|
|
160
161
|
drainHookStatusFor(ctx: Ctx, lane: LaneRecord): {
|
|
161
162
|
registered: boolean;
|
|
162
163
|
kind: string;
|
|
@@ -248,8 +249,6 @@ export interface Ctx {
|
|
|
248
249
|
backfillOutpostIdentity: (expectedId: string) => "created" | "unchanged";
|
|
249
250
|
fetchText: (url: string) => Promise<string>;
|
|
250
251
|
lanePlacementEnabled: (company: LaneCompanyRef | undefined) => Promise<boolean>;
|
|
251
|
-
outpostSnapshotCacheEnabled: () => Promise<boolean>;
|
|
252
|
-
startOutpostLaneSnapshotRefresh: (hqRoot: string) => void;
|
|
253
252
|
storyLedgerEnabled: () => boolean;
|
|
254
253
|
isSsmOwnerDispatch: () => boolean;
|
|
255
254
|
withGrantHireLocks: typeof withLaneHireLocks;
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -14,7 +14,6 @@ import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
|
14
14
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
15
15
|
export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
16
16
|
import { type OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
17
|
-
import { LANE_OUTPOST_SNAPSHOT_CACHE_FLAG } from "../lib/lanes/outpost-hire-cache.js";
|
|
18
17
|
import { emitLaneDecisionEvents } from "../lib/lanes/mesh-events.js";
|
|
19
18
|
import { MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
20
19
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
@@ -62,7 +61,7 @@ export interface LanesCommandDeps {
|
|
|
62
61
|
/** Test seam for worker PID identity checks. */
|
|
63
62
|
readStarttime?: (pid: number) => number | null;
|
|
64
63
|
/** Test seams for synchronous dead-worker classification and repair. */
|
|
65
|
-
watcherLoopDeps?: Pick<WatcherLoopDeps, "sleep" | "spawnRepair"
|
|
64
|
+
watcherLoopDeps?: Pick<WatcherLoopDeps, "sleep" | "spawnRepair">;
|
|
66
65
|
stopSleepMs?: (ms: number) => void;
|
|
67
66
|
livenessDeps?: LivenessDeps;
|
|
68
67
|
presenceConnected?: boolean;
|
|
@@ -88,10 +87,6 @@ export interface LanesCommandDeps {
|
|
|
88
87
|
monitorEvidence?: MonitorEvidenceReader;
|
|
89
88
|
/** Test seam for the default-off outpost placement rollout flag. */
|
|
90
89
|
lanePlacementEnabled?: (company: LaneCompanyRef | undefined) => Promise<boolean>;
|
|
91
|
-
/** Test seam for the default-off shared Outpost snapshot cache. */
|
|
92
|
-
outpostSnapshotCacheEnabled?: () => Promise<boolean>;
|
|
93
|
-
/** Test seam for the detached refresh started by the cache-first lane list. */
|
|
94
|
-
startOutpostLaneSnapshotRefresh?: (hqRoot: string) => void;
|
|
95
90
|
/** Test seam for the default-off story ledger flag. */
|
|
96
91
|
storyLedgerEnabled?: () => boolean;
|
|
97
92
|
/** Test seam for the SSM-only Outpost owner grant path. */
|
|
@@ -103,7 +98,6 @@ export interface LanesCommandDeps {
|
|
|
103
98
|
/** Test seam for the repository-merge-hold registry flag. */
|
|
104
99
|
mergeHoldEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
105
100
|
}
|
|
106
|
-
export { LANE_OUTPOST_SNAPSHOT_CACHE_FLAG };
|
|
107
101
|
export declare function parseSeniorRef(raw: string): SeniorRef;
|
|
108
102
|
export declare function parseQuestionBody(raw: unknown): QuestionBody | null;
|
|
109
103
|
/**
|
|
@@ -148,8 +142,6 @@ export declare function pickupFailureUpdater(reasonRaw: string, details: Record<
|
|
|
148
142
|
*/
|
|
149
143
|
export declare function applyOutpostAccountPickup(hqRoot: string, laneId: string, modelDeps?: KnownModelsDeps): LaneRecord;
|
|
150
144
|
export declare function cancelPendingQuestionsForLane(hqRoot: string, laneId: string, nowIso: string, operatorReason?: string): QuestionRecord[];
|
|
151
|
-
/** Keeps the placement-enabled cache-first path on the full lane command graph. */
|
|
152
|
-
export declare function runFastCachedLanesListFromEntrypoint(): Promise<void>;
|
|
153
145
|
export declare function stopCommittedState(state: LaneRecord["state"]): "cancelled" | "killed";
|
|
154
146
|
/** Stderr notice that `stop` ends a lane for good, naming the resumable path. */
|
|
155
147
|
export declare function stopTerminalWarning(laneId: string): string;
|