@indigoai-us/hq-cli 5.345.48 → 5.345.49
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 +10 -0
- package/README.md +2 -1
- package/dist/command-catalog.generated.d.ts +8 -0
- package/dist/command-catalog.generated.js +10 -0
- package/dist/commands/anywhere-runtime-flag.js +5 -3
- package/dist/commands/feedback.d.ts +10 -0
- package/dist/commands/feedback.js +45 -0
- package/dist/commands/lanes-message.js +2 -2
- package/dist/commands/lanes.js +2 -2
- package/dist/commands/mcp-serve.js +17 -4
- package/dist/lib/anywhere-person-setting.d.ts +1 -0
- package/dist/lib/anywhere-person-setting.js +12 -0
- package/dist/lib/anywhere-runtime-fast-flag.js +5 -3
- package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -1
- package/dist/lib/anywhere-runtime-flag-key.js +6 -1
- package/dist/lib/daemon/flag.d.ts +1 -1
- package/dist/lib/daemon/flag.js +20 -9
- package/dist/lib/daemon/server.d.ts +1 -1
- package/dist/lib/lanes/reconcile.js +6 -4
- package/dist/lib/lanes/registry-flag.d.ts +11 -1
- package/dist/lib/lanes/registry-flag.js +68 -29
- package/dist/lib/lanes/watcher.js +63 -2
- package/dist/utils/resident-package-preload.js +4 -1
- package/dist/utils/self-update.js +6 -5
- package/dist/utils/version-gate.d.ts +11 -4
- package/dist/utils/version-gate.js +132 -41
- package/docs/anywhere-runtime.md +5 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.49] — 2026-10-08
|
|
6
|
+
- Persist terminal lane envelopes and story metadata when a final report arrives after the lane has already stopped.
|
|
7
|
+
|
|
8
|
+
- Keep changelog guard coverage valid after release fragments move into CHANGELOG.md.
|
|
9
|
+
- The CLI now updates the pnpm-managed `hq` install that PATH resolves first, so `hq update` converges when another pnpm home shadows the running process.
|
|
10
|
+
- Preserve an explicit unknown state when the lanes flag registry cannot be read. The four message-delivery and session-wake gates now fail open on unknown; all other lanes gates retain their fail-closed behavior.
|
|
11
|
+
- `hq feedback bug` can include a fail-off, hq-flags-gated agent or human reporter mark derived from runtime context.
|
|
12
|
+
- HQ Anywhere remains enabled when its flag registry is unavailable, while the local kill switch still turns it off.
|
|
13
|
+
- Windows native-resolve cleanup accepts taskkill exit 128 only after confirming the target process has exited.
|
|
14
|
+
|
|
5
15
|
## [5.345.48] — 2026-10-08
|
|
6
16
|
- Add default-off multi-company session locks with ordered `hq-session add/remove company` updates and scope-capability enforcement.
|
|
7
17
|
- Give a sync runner that reports stopping 60 seconds to persist pending pushes before SIGKILL. The daemon shutdown deadline includes that grace; other services remain capped at 15 seconds.
|
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@ hq --help
|
|
|
9
9
|
|
|
10
10
|
The global HQ runtime installs hooks for supported agent runtimes. When the
|
|
11
11
|
`hq-anywhere-runtime` flag is enabled, the hooks use hqd and a per-user folder
|
|
12
|
-
registry. The flag
|
|
12
|
+
registry. The flag defaults on when its registry row cannot be read, and an
|
|
13
|
+
explicit false turns it off. See [HQ Anywhere runtime](docs/anywhere-runtime.md)
|
|
13
14
|
for install and link commands.
|
|
14
15
|
|
|
15
16
|
## Live meeting transcripts
|
|
@@ -3673,6 +3673,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3673
3673
|
readonly flags: "--company <slug>";
|
|
3674
3674
|
readonly description: "Company slug to associate with the report";
|
|
3675
3675
|
readonly hidden: false;
|
|
3676
|
+
}, {
|
|
3677
|
+
readonly flags: "--reporter <kind>";
|
|
3678
|
+
readonly description: "Mark this bug report as agent-filed (only 'agent' is accepted)";
|
|
3679
|
+
readonly hidden: false;
|
|
3676
3680
|
}, {
|
|
3677
3681
|
readonly flags: "--screenshot <path>";
|
|
3678
3682
|
readonly description: "Attach a screenshot (repeatable, up to 5; .png/.jpg/.jpeg/.webp/.gif)";
|
|
@@ -3706,6 +3710,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3706
3710
|
readonly flags: "--company <slug>";
|
|
3707
3711
|
readonly description: "Company slug to associate with the report";
|
|
3708
3712
|
readonly hidden: false;
|
|
3713
|
+
}, {
|
|
3714
|
+
readonly flags: "--reporter <kind>";
|
|
3715
|
+
readonly description: "Mark this bug report as agent-filed (only 'agent' is accepted)";
|
|
3716
|
+
readonly hidden: false;
|
|
3709
3717
|
}, {
|
|
3710
3718
|
readonly flags: "--screenshot <path>";
|
|
3711
3719
|
readonly description: "Attach a screenshot (repeatable, up to 5; .png/.jpg/.jpeg/.webp/.gif)";
|
|
@@ -4442,6 +4442,11 @@ export const COMMAND_CATALOG = [
|
|
|
4442
4442
|
"description": "Company slug to associate with the report",
|
|
4443
4443
|
"hidden": false
|
|
4444
4444
|
},
|
|
4445
|
+
{
|
|
4446
|
+
"flags": "--reporter <kind>",
|
|
4447
|
+
"description": "Mark this bug report as agent-filed (only 'agent' is accepted)",
|
|
4448
|
+
"hidden": false
|
|
4449
|
+
},
|
|
4445
4450
|
{
|
|
4446
4451
|
"flags": "--screenshot <path>",
|
|
4447
4452
|
"description": "Attach a screenshot (repeatable, up to 5; .png/.jpg/.jpeg/.webp/.gif)",
|
|
@@ -4482,6 +4487,11 @@ export const COMMAND_CATALOG = [
|
|
|
4482
4487
|
"description": "Company slug to associate with the report",
|
|
4483
4488
|
"hidden": false
|
|
4484
4489
|
},
|
|
4490
|
+
{
|
|
4491
|
+
"flags": "--reporter <kind>",
|
|
4492
|
+
"description": "Mark this bug report as agent-filed (only 'agent' is accepted)",
|
|
4493
|
+
"hidden": false
|
|
4494
|
+
},
|
|
4485
4495
|
{
|
|
4486
4496
|
"flags": "--screenshot <path>",
|
|
4487
4497
|
"description": "Attach a screenshot (repeatable, up to 5; .png/.jpg/.jpeg/.webp/.gif)",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveFlagGate, resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
2
|
-
import { HQ_ANYWHERE_RUNTIME_FLAG } from "../lib/anywhere-runtime-flag-key.js";
|
|
2
|
+
import { HQ_ANYWHERE_RUNTIME_FLAG, hqAnywhereRuntimeLocallyDisabled } from "../lib/anywhere-runtime-flag-key.js";
|
|
3
3
|
export { HQ_ANYWHERE_RUNTIME_FLAG };
|
|
4
4
|
export class AnywhereRuntimeDisabledError extends Error {
|
|
5
5
|
expected = true;
|
|
@@ -9,8 +9,10 @@ export class AnywhereRuntimeDisabledError extends Error {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
export function anywhereRuntimeEnabled(env = process.env) {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
if (hqAnywhereRuntimeLocallyDisabled(env))
|
|
13
|
+
return false;
|
|
14
|
+
const fallback = () => true;
|
|
15
|
+
const lookup = { fallback: true };
|
|
14
16
|
return env === process.env
|
|
15
17
|
? resolveProcessFlagGate(HQ_ANYWHERE_RUNTIME_FLAG, lookup, fallback)
|
|
16
18
|
: resolveFlagGate(undefined, HQ_ANYWHERE_RUNTIME_FLAG, lookup, fallback, env);
|
|
@@ -4,6 +4,8 @@ import { collectFeedbackLogs, feedbackLogsEnabled } from "../utils/feedback-logs
|
|
|
4
4
|
import { type LogBundleRef } from "../utils/feedback-log-bundle.js";
|
|
5
5
|
export declare const FEEDBACK_LATEST_VERSION_FLAG = "feedback.latest-version";
|
|
6
6
|
export declare const FEEDBACK_LATEST_VERSION_FLAG_FALLBACK = false;
|
|
7
|
+
export declare const FEEDBACK_REPORTER_KIND_FLAG = "feedback.reporter-kind";
|
|
8
|
+
export declare const FEEDBACK_REPORTER_KIND_FLAG_FALLBACK = true;
|
|
7
9
|
export declare const BODY_MAX_BYTES: number;
|
|
8
10
|
/** Validate fields that must be valid before feedback attachments are uploaded. */
|
|
9
11
|
export declare function validateFeedbackContent(title: string, body: string): void;
|
|
@@ -44,7 +46,15 @@ export interface FeedbackSubmitOptions {
|
|
|
44
46
|
diagnostics?: DiagnosticsBlob;
|
|
45
47
|
/** Precomputed setting so environment lookup happens before attachments upload. */
|
|
46
48
|
logsEnabled?: boolean;
|
|
49
|
+
/** Optional one-way override: callers can mark human sessions as agent. */
|
|
50
|
+
reporterOverride?: "agent";
|
|
47
51
|
}
|
|
52
|
+
export interface FeedbackReporterMark {
|
|
53
|
+
reporter_kind: "agent" | "human";
|
|
54
|
+
reporter_source: string;
|
|
55
|
+
}
|
|
56
|
+
/** Resolve a safe reporter classification without returning any env values. */
|
|
57
|
+
export declare function resolveFeedbackReporter(env?: NodeJS.ProcessEnv, isTTY?: boolean, override?: string): FeedbackReporterMark;
|
|
48
58
|
export declare function readBodyFile(bodyFile: string, stdin?: NodeJS.ReadableStream): Promise<string>;
|
|
49
59
|
/**
|
|
50
60
|
* Attach redacted `~/.hq` log tails to `diagnostics` if, and only if, they fit
|
|
@@ -10,6 +10,8 @@ import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
|
10
10
|
import { cachedLatestVersion } from "../utils/version-check.js";
|
|
11
11
|
export const FEEDBACK_LATEST_VERSION_FLAG = "feedback.latest-version";
|
|
12
12
|
export const FEEDBACK_LATEST_VERSION_FLAG_FALLBACK = false;
|
|
13
|
+
export const FEEDBACK_REPORTER_KIND_FLAG = "feedback.reporter-kind";
|
|
14
|
+
export const FEEDBACK_REPORTER_KIND_FLAG_FALLBACK = true;
|
|
13
15
|
export const BODY_MAX_BYTES = 64 * 1024;
|
|
14
16
|
/** Validate fields that must be valid before feedback attachments are uploaded. */
|
|
15
17
|
export function validateFeedbackContent(title, body) {
|
|
@@ -46,6 +48,32 @@ function validateFeedbackRequestSize(requestBody) {
|
|
|
46
48
|
* after attaching, so this reserve only needs to be roughly right.
|
|
47
49
|
*/
|
|
48
50
|
const LOGS_ENVELOPE_RESERVE_BYTES = 1024;
|
|
51
|
+
const AGENT_RUNTIME_MARKERS = [
|
|
52
|
+
"HQ_LANE_ID",
|
|
53
|
+
"HQ_POLICY_WORKER_DIR",
|
|
54
|
+
"HQ_AGENT_DIR",
|
|
55
|
+
"CODEX_THREAD_ID",
|
|
56
|
+
"CODEX_SESSION_ID",
|
|
57
|
+
"CLAUDE_CODE_SESSION_ID",
|
|
58
|
+
"CLAUDE_SESSION_ID",
|
|
59
|
+
"HQ_SESSION_ID",
|
|
60
|
+
"GROK_SESSION_ID",
|
|
61
|
+
];
|
|
62
|
+
/** Resolve a safe reporter classification without returning any env values. */
|
|
63
|
+
export function resolveFeedbackReporter(env = process.env, isTTY = process.stdin.isTTY === true, override) {
|
|
64
|
+
for (const marker of AGENT_RUNTIME_MARKERS) {
|
|
65
|
+
if (env[marker]?.trim()) {
|
|
66
|
+
return { reporter_kind: "agent", reporter_source: marker };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (override === "agent") {
|
|
70
|
+
return { reporter_kind: "agent", reporter_source: "CLI_REPORTER_OVERRIDE" };
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
reporter_kind: "human",
|
|
74
|
+
reporter_source: isTTY ? "stdin_isTTY" : "no_agent_runtime_marker",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
49
77
|
function buildFeedbackRequestBody(opts) {
|
|
50
78
|
const requestBody = {
|
|
51
79
|
type: opts.type,
|
|
@@ -53,6 +81,10 @@ function buildFeedbackRequestBody(opts) {
|
|
|
53
81
|
body: opts.body,
|
|
54
82
|
diagnostics: opts.diagnostics,
|
|
55
83
|
};
|
|
84
|
+
if (opts.reporterMark) {
|
|
85
|
+
requestBody.reporter_kind = opts.reporterMark.reporter_kind;
|
|
86
|
+
requestBody.reporter_source = opts.reporterMark.reporter_source;
|
|
87
|
+
}
|
|
56
88
|
if (opts.company) {
|
|
57
89
|
requestBody.company = opts.company;
|
|
58
90
|
}
|
|
@@ -124,6 +156,9 @@ export async function submitFeedback(opts) {
|
|
|
124
156
|
// which invokes the same validator before uploading any attachments.
|
|
125
157
|
validateFeedbackContent(opts.title, opts.body);
|
|
126
158
|
const diagnostics = opts.diagnostics ?? collectDiagnostics();
|
|
159
|
+
const reporterMark = opts.type === "bug" && resolveProcessFlagGate(FEEDBACK_REPORTER_KIND_FLAG, { fallback: FEEDBACK_REPORTER_KIND_FLAG_FALLBACK }, () => FEEDBACK_REPORTER_KIND_FLAG_FALLBACK)
|
|
160
|
+
? resolveFeedbackReporter(process.env, process.stdin.isTTY === true, opts.reporterOverride)
|
|
161
|
+
: undefined;
|
|
127
162
|
const requestBody = buildFeedbackRequestBody({
|
|
128
163
|
type: opts.type,
|
|
129
164
|
title: opts.title,
|
|
@@ -132,6 +167,7 @@ export async function submitFeedback(opts) {
|
|
|
132
167
|
diagnostics,
|
|
133
168
|
screenshots: opts.screenshots,
|
|
134
169
|
logBundle: opts.logBundle,
|
|
170
|
+
reporterMark,
|
|
135
171
|
});
|
|
136
172
|
// Direct callers can supply attachment references; ensure the complete base
|
|
137
173
|
// envelope still fits before adding optional inline logs or making a request.
|
|
@@ -172,12 +208,16 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
172
208
|
.requiredOption("--title <text>", "Short title for the report")
|
|
173
209
|
.requiredOption("--body-file <path>", "Path to a markdown file with the body; use - to read from stdin")
|
|
174
210
|
.option("--company <slug>", "Company slug to associate with the report")
|
|
211
|
+
.option("--reporter <kind>", "Mark this bug report as agent-filed (only 'agent' is accepted)")
|
|
175
212
|
.option("--screenshot <path>", `Attach a screenshot (repeatable, up to ${MAX_SCREENSHOTS}; .png/.jpg/.jpeg/.webp/.gif)`, (value, prev) => [...prev, value], [])
|
|
176
213
|
// Commander turns `--no-logs` into a boolean `logs` that defaults to true,
|
|
177
214
|
// so the default is "attach logs" and the flag is the opt-out.
|
|
178
215
|
.option("--no-logs", "Do not attach redacted tails of your ~/.hq log files (also: HQ_FEEDBACK_LOGS=0)")
|
|
179
216
|
.action(async (opts) => {
|
|
180
217
|
try {
|
|
218
|
+
if (opts.reporter !== undefined && (opts.reporter !== "agent" || type !== "bug")) {
|
|
219
|
+
throw new Error("Invalid --reporter value: only bug reports may use 'agent'.");
|
|
220
|
+
}
|
|
181
221
|
const token = await ensureCognitoToken({ interactive: false });
|
|
182
222
|
const body = await readBodyFile(opts.bodyFile);
|
|
183
223
|
validateFeedbackContent(opts.title, body);
|
|
@@ -192,6 +232,9 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
192
232
|
diagnostics.latestVersion = latestVersion;
|
|
193
233
|
}
|
|
194
234
|
const logsEnabled = opts.logs !== false && feedbackLogsEnabled();
|
|
235
|
+
const reporterMark = type === "bug" && resolveProcessFlagGate(FEEDBACK_REPORTER_KIND_FLAG, { fallback: FEEDBACK_REPORTER_KIND_FLAG_FALLBACK }, () => FEEDBACK_REPORTER_KIND_FLAG_FALLBACK)
|
|
236
|
+
? resolveFeedbackReporter(process.env, process.stdin.isTTY === true, opts.reporter)
|
|
237
|
+
: undefined;
|
|
195
238
|
// Validate the serialized server request envelope before screenshots
|
|
196
239
|
// or the log bundle can create remote objects. Attachment references
|
|
197
240
|
// are added and checked again inside submitFeedback after upload.
|
|
@@ -201,6 +244,7 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
201
244
|
body,
|
|
202
245
|
company: opts.company,
|
|
203
246
|
diagnostics,
|
|
247
|
+
reporterMark,
|
|
204
248
|
}));
|
|
205
249
|
// vaultApiFetch can reject an hqk_ token or an invalid API base URL
|
|
206
250
|
// locally before fetch. Check the mandatory POST, and the screenshot
|
|
@@ -236,6 +280,7 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
236
280
|
includeLogs: opts.logs !== false,
|
|
237
281
|
logBundle,
|
|
238
282
|
diagnostics,
|
|
283
|
+
reporterOverride: opts.reporter === "agent" ? "agent" : undefined,
|
|
239
284
|
logsEnabled,
|
|
240
285
|
});
|
|
241
286
|
console.log(`Submitted: ${result.id}`);
|
|
@@ -13,7 +13,7 @@ import { artifactIdentityKeys, rollbackStorySwitchAttribution, storySwitchNotice
|
|
|
13
13
|
import { laneRunDir, probePidRecord, readPidRecord, readProcStarttime, spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
14
14
|
import { findRetiredLaneEntry, keepAliveExclusion, keepAliveSupersededBy, LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
|
|
15
15
|
import { isTerminalLaneState } from "../lib/lanes/types.js";
|
|
16
|
-
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
16
|
+
import { readLanesRegistryFlag, readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
|
|
17
17
|
import { appendLaneFocusOverride, focusGuardRefusal, makeFocusOverride } from "../lib/lanes/focus-guard.js";
|
|
18
18
|
import { appendProjectFocusOverride, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
|
|
19
19
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
@@ -192,7 +192,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
192
192
|
: "superseded";
|
|
193
193
|
}
|
|
194
194
|
if (exclusion === "superseded" && currentLane.state !== "done" && currentLane.state !== "interrupted") {
|
|
195
|
-
const runningSupersededMessageDeliveryEnabled = await
|
|
195
|
+
const runningSupersededMessageDeliveryEnabled = await readLanesRegistryFlagFailOpen(LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, { company: companyForLane(currentLane), env: ctx.env });
|
|
196
196
|
if (runningSupersededMessageDeliveryEnabled) {
|
|
197
197
|
const latestLane = readLane(ctx.hqRoot(), currentLane.lane_id);
|
|
198
198
|
if (latestLane.state === "killed" || latestLane.state === "cancelled") {
|
package/dist/commands/lanes.js
CHANGED
|
@@ -36,7 +36,7 @@ import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
|
36
36
|
import { laneDepthCap } from "../lib/lanes/depth-cap.js";
|
|
37
37
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
38
38
|
import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
|
|
39
|
-
import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "../lib/lanes/registry-flag.js";
|
|
39
|
+
import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff, readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
|
|
40
40
|
import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
|
|
41
41
|
import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
|
|
42
42
|
import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, } from "../lib/lanes/flag-keys.js";
|
|
@@ -1898,7 +1898,7 @@ async function resolveMessageLaneTarget(ctx, laneId) {
|
|
|
1898
1898
|
const localOverride = readLanesFlagLocalOverride(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, ctx.env);
|
|
1899
1899
|
return localOverride ?? "check-company";
|
|
1900
1900
|
}
|
|
1901
|
-
return
|
|
1901
|
+
return readLanesRegistryFlagFailOpen(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, {
|
|
1902
1902
|
company,
|
|
1903
1903
|
env: ctx.env,
|
|
1904
1904
|
});
|
|
@@ -16,12 +16,16 @@ import * as os from "node:os";
|
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import { CLI_VERSION } from "../cli-version.js";
|
|
18
18
|
import { HQ_ANYWHERE_RUNTIME_FLAG } from "../lib/anywhere-runtime-flag-key.js";
|
|
19
|
-
import { anywhereRuntimeAllowed } from "../lib/anywhere-person-setting.js";
|
|
20
19
|
import { createLineSplitter, HqdClientError, hqdRequest, hqdSocketPath } from "../lib/daemon/protocol.js";
|
|
21
20
|
import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload } from "../utils/package-use-lease.js";
|
|
22
21
|
import { preloadResidentPackageModules, reportResidentPackagePreloadFailure, RESIDENT_PACKAGE_MODULES } from "../utils/resident-package-preload.js";
|
|
23
22
|
export const MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
24
23
|
const UNREACHABLE_CODES = new Set(["ENOENT", "ECONNREFUSED", "ECONNRESET", "EPIPE", "ETIMEDOUT", "ESOCKET", "EACCES"]);
|
|
24
|
+
function hasPersonTokenCache(env, home) {
|
|
25
|
+
// Mirrors personTokenCacheFile() in utils/cognito-session.ts; duplicated so this path does not load that module.
|
|
26
|
+
const stateDir = env.HQ_STATE_DIR?.trim() || path.join(home, ".hq");
|
|
27
|
+
return fs.existsSync(path.join(stateDir, "cognito-tokens.json"));
|
|
28
|
+
}
|
|
25
29
|
/** HQ root for prompts: HQ_ROOT, else the ~/.hq/root pointer written by `hq install --global`. */
|
|
26
30
|
export function readHqRoot(env, home) {
|
|
27
31
|
const fromEnv = env.HQ_ROOT?.trim();
|
|
@@ -54,9 +58,18 @@ export async function runMcpServe(opts = {}) {
|
|
|
54
58
|
const cwd = opts.cwd ?? process.cwd();
|
|
55
59
|
const socketPath = hqdSocketPath(home, env);
|
|
56
60
|
const timeoutMs = opts.timeoutMs ?? 120_000;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
const runtimeAllowed = opts.runtimeAllowed ?? (async () => {
|
|
62
|
+
// A person token is required to read the opt-in setting. Deny a fresh
|
|
63
|
+
// install before loading the hq-flags and Cognito readers.
|
|
64
|
+
if (!hasPersonTokenCache(env, home))
|
|
65
|
+
return false;
|
|
66
|
+
const { anywhereRuntimeAllowed } = await import("../lib/anywhere-person-setting.js");
|
|
67
|
+
return anywhereRuntimeAllowed(env, {
|
|
68
|
+
home,
|
|
69
|
+
onError: (name) => process.stderr.write(`HQ Anywhere person setting unavailable (${name}); access remains off.\n`),
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
if (!(await runtimeAllowed())) {
|
|
60
73
|
process.stderr.write(`HQ Anywhere is not enabled. The ${HQ_ANYWHERE_RUNTIME_FLAG} flag and your HQ Anywhere person setting must both be on.\n`);
|
|
61
74
|
process.exitCode = 1;
|
|
62
75
|
return;
|
|
@@ -6,6 +6,7 @@ export interface ReadHqAnywherePersonSettingOptions {
|
|
|
6
6
|
fetchImpl?: typeof fetch;
|
|
7
7
|
apiBaseUrl?: string;
|
|
8
8
|
env?: NodeJS.ProcessEnv;
|
|
9
|
+
home?: string;
|
|
9
10
|
onError?: (errorName: string) => void;
|
|
10
11
|
}
|
|
11
12
|
/** Returns null when the person preference cannot be read; callers must fail closed. */
|
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
1
4
|
import { anywhereRuntimeEnabledFromSnapshot } from "./anywhere-runtime-fast-flag.js";
|
|
5
|
+
function hasPersonTokenCache(env, home) {
|
|
6
|
+
// Mirrors personTokenCacheFile() in utils/cognito-session.ts; duplicated so this path does not load that module.
|
|
7
|
+
const stateDir = env.HQ_STATE_DIR?.trim() || path.join(home, ".hq");
|
|
8
|
+
return fs.existsSync(path.join(stateDir, "cognito-tokens.json"));
|
|
9
|
+
}
|
|
2
10
|
/** Returns null when the person preference cannot be read; callers must fail closed. */
|
|
3
11
|
export async function readHqAnywherePersonSetting(options = {}) {
|
|
4
12
|
const env = options.env ?? process.env;
|
|
5
13
|
try {
|
|
14
|
+
// Avoid loading the Cognito SDK on the common fresh-install path. There is
|
|
15
|
+
// no person token to refresh or use until a cache exists.
|
|
16
|
+
if (!options.getPersonToken && !hasPersonTokenCache(env, options.home ?? env.HOME ?? os.homedir()))
|
|
17
|
+
return null;
|
|
6
18
|
const getPersonToken = options.getPersonToken ?? (async () => {
|
|
7
19
|
const { ensureCognitoToken } = await import("../utils/cognito-session.js");
|
|
8
20
|
return ensureCognitoToken({ interactive: false, tokenSource: "person" });
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { readLocalOverride, resolveFlag } from "@indigoai-us/hq-flags-client";
|
|
2
2
|
import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
|
|
3
3
|
import { readFlagSnapshotFile, readPersonFlagSnapshotFile } from "./flag-snapshot-file.js";
|
|
4
|
-
import { HQ_ANYWHERE_RUNTIME_FLAG } from "./anywhere-runtime-flag-key.js";
|
|
4
|
+
import { HQ_ANYWHERE_RUNTIME_FLAG, hqAnywhereRuntimeLocallyDisabled } from "./anywhere-runtime-flag-key.js";
|
|
5
5
|
/** Resolve the Anywhere gate from hq-flags without loading the CLI's large worker bundle. */
|
|
6
6
|
export function anywhereRuntimeEnabledFromSnapshot(env = process.env) {
|
|
7
|
+
if (hqAnywhereRuntimeLocallyDisabled(env))
|
|
8
|
+
return false;
|
|
7
9
|
const local = readLocalOverride(HQ_ANYWHERE_RUNTIME_FLAG, env);
|
|
8
10
|
if (local !== null)
|
|
9
11
|
return local;
|
|
@@ -17,13 +19,13 @@ export function anywhereRuntimeEnabledFromSnapshot(env = process.env) {
|
|
|
17
19
|
const snapshot = persisted
|
|
18
20
|
? { version: 0, flags: persisted.flags }
|
|
19
21
|
: null;
|
|
20
|
-
//
|
|
22
|
+
// The flag is a fail-safe kill switch: unknown registry state keeps HQ Anywhere enabled.
|
|
21
23
|
return resolveFlag({
|
|
22
24
|
flagKey: HQ_ANYWHERE_RUNTIME_FLAG,
|
|
23
25
|
env,
|
|
24
26
|
snapshot,
|
|
25
27
|
companyIdentifiers: [company.companyUid, company.companySlug].filter((value) => Boolean(value)),
|
|
26
|
-
lookup: { fallback:
|
|
28
|
+
lookup: { fallback: true },
|
|
27
29
|
}).value;
|
|
28
30
|
}
|
|
29
31
|
//# sourceMappingURL=anywhere-runtime-fast-flag.js.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
/** hq-flags key for the staged HQ Anywhere runtime. The registry default is
|
|
1
|
+
/** hq-flags key for the staged HQ Anywhere runtime. The registry default is on. */
|
|
2
2
|
export declare const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
|
|
3
|
+
/** The HQ Anywhere environment kill switch stays compatible with managed provider wrappers. */
|
|
4
|
+
export declare function hqAnywhereRuntimeLocallyDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
3
5
|
//# sourceMappingURL=anywhere-runtime-flag-key.d.ts.map
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
/** hq-flags key for the staged HQ Anywhere runtime. The registry default is
|
|
1
|
+
/** hq-flags key for the staged HQ Anywhere runtime. The registry default is on. */
|
|
2
2
|
export const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
|
|
3
|
+
/** The HQ Anywhere environment kill switch stays compatible with managed provider wrappers. */
|
|
4
|
+
export function hqAnywhereRuntimeLocallyDisabled(env = process.env) {
|
|
5
|
+
const value = env.HQ_FLAG_HQ_ANYWHERE_RUNTIME;
|
|
6
|
+
return value === "false" || value === "0";
|
|
7
|
+
}
|
|
3
8
|
//# sourceMappingURL=anywhere-runtime-flag-key.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Feature flags read by the daemon. The daemon runs for weeks, so each read
|
|
3
3
|
* asks the registry again instead of holding a per-process answer, and it
|
|
4
|
-
* reads as
|
|
4
|
+
* reads as each flag's fallback unless the registry or a local override decides it.
|
|
5
5
|
*/
|
|
6
6
|
import { type FlagClientOptions, type FlagResolution } from "@indigoai-us/hq-flags-client";
|
|
7
7
|
/** Outposts hand running services to the next daemon on a CLI update instead of restarting them all. */
|
package/dist/lib/daemon/flag.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Feature flags read by the daemon. The daemon runs for weeks, so each read
|
|
3
3
|
* asks the registry again instead of holding a per-process answer, and it
|
|
4
|
-
* reads as
|
|
4
|
+
* reads as each flag's fallback unless the registry or a local override decides it.
|
|
5
5
|
*/
|
|
6
6
|
import { createFlagClient, readLocalOverride, } from "@indigoai-us/hq-flags-client";
|
|
7
7
|
import { ensureCognitoIdToken } from "../../utils/cognito-session.js";
|
|
8
8
|
import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
|
|
9
9
|
import { resolveProcessFlagCompanyContext } from "../flag-registry.js";
|
|
10
|
+
import { HQ_ANYWHERE_RUNTIME_FLAG, hqAnywhereRuntimeLocallyDisabled } from "../anywhere-runtime-flag-key.js";
|
|
10
11
|
/** Outposts hand running services to the next daemon on a CLI update instead of restarting them all. */
|
|
11
12
|
export const SUPERVISOR_HANDOFF_FLAG = "daemon.supervisor-handoff";
|
|
12
13
|
/** Self-updates build the new CLI beside the running one and swap it in, so `hq` never disappears mid-update. */
|
|
@@ -14,6 +15,9 @@ export const STAGED_SELF_UPDATE_FLAG = "cli.staged-self-update";
|
|
|
14
15
|
const READ_TIMEOUT_MS = 5_000;
|
|
15
16
|
export async function readDaemonFlag(flagKey, options = {}) {
|
|
16
17
|
const env = options.env ?? process.env;
|
|
18
|
+
if (flagKey === HQ_ANYWHERE_RUNTIME_FLAG && hqAnywhereRuntimeLocallyDisabled(env))
|
|
19
|
+
return false;
|
|
20
|
+
const fallback = flagKey === HQ_ANYWHERE_RUNTIME_FLAG;
|
|
17
21
|
const override = readLocalOverride(flagKey, env);
|
|
18
22
|
const localOverrideMode = options.localOverrideMode ?? "respect";
|
|
19
23
|
if (override !== null && (localOverrideMode === "respect" || (localOverrideMode === "off-only" && !override)))
|
|
@@ -28,10 +32,10 @@ export async function readDaemonFlag(flagKey, options = {}) {
|
|
|
28
32
|
let client;
|
|
29
33
|
try {
|
|
30
34
|
const company = resolveProcessFlagCompanyContext(env);
|
|
31
|
-
// Signed out reads as
|
|
35
|
+
// Signed out reads as the flag fallback without reaching the registry.
|
|
32
36
|
const token = await (options.getToken ?? (() => ensureCognitoIdToken({ interactive: false })))();
|
|
33
37
|
if (typeof token !== "string" || token.trim() === "")
|
|
34
|
-
return
|
|
38
|
+
return fallback;
|
|
35
39
|
client = (options.createClient ?? createFlagClient)({
|
|
36
40
|
endpoint: resolveFlagRegistryEndpoint(undefined, env),
|
|
37
41
|
getToken: () => token,
|
|
@@ -42,20 +46,27 @@ export async function readDaemonFlag(flagKey, options = {}) {
|
|
|
42
46
|
...(company.companySlug ? { companyIdentifiers: [company.companySlug] } : {}),
|
|
43
47
|
onError,
|
|
44
48
|
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
try {
|
|
50
|
+
await client.ready();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
onError(error);
|
|
54
|
+
}
|
|
55
|
+
const resolution = client.explain(flagKey, { fallback });
|
|
56
|
+
if (failed && options.throwOnError && !fallback) {
|
|
48
57
|
throw new Error("hq-flags registry read failed", { cause: reportedError });
|
|
49
58
|
}
|
|
50
|
-
|
|
59
|
+
if (resolution.source === "snapshot")
|
|
60
|
+
return resolution.value === true;
|
|
61
|
+
return fallback && resolution.value === true;
|
|
51
62
|
}
|
|
52
63
|
catch (error) {
|
|
53
64
|
if (!failed)
|
|
54
65
|
onError(error);
|
|
55
|
-
if (options.throwOnError && failed) {
|
|
66
|
+
if (options.throwOnError && failed && !fallback) {
|
|
56
67
|
throw new Error("hq-flags registry read failed", { cause: reportedError ?? error });
|
|
57
68
|
}
|
|
58
|
-
return
|
|
69
|
+
return fallback;
|
|
59
70
|
}
|
|
60
71
|
finally {
|
|
61
72
|
try {
|
|
@@ -39,7 +39,7 @@ export interface HqdServerOptions {
|
|
|
39
39
|
env?: NodeJS.ProcessEnv;
|
|
40
40
|
/** Work-mesh root override for mesh.presence (tests). */
|
|
41
41
|
workMeshRoot?: string;
|
|
42
|
-
/** Test seam for the hq-anywhere-runtime
|
|
42
|
+
/** Test seam for the hq-anywhere-runtime fail-safe gate. */
|
|
43
43
|
mcpEnabled?: () => boolean | Promise<boolean>;
|
|
44
44
|
/** Test seam for the live hq-flags read used by the daemon's bounded runtime monitor. */
|
|
45
45
|
runtimeFlagReader?: (options: ReadDaemonFlagOptions) => Promise<boolean>;
|
|
@@ -22,7 +22,7 @@ import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDa
|
|
|
22
22
|
import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, nextEnvelopePath, readPidRecord, readProcStarttime, readSpawnManifest, markSpawnFailure, stopLaneProcesses, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
|
|
23
23
|
import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, listQuestionIds, updateLane, updateQuestion, } from "./store.js";
|
|
24
24
|
import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyObservation, recordCpuBusyObservation, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
25
|
-
import { readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "./registry-flag.js";
|
|
25
|
+
import { readLanesRegistryFlag, readLanesRegistryFlagFailOff, readLanesRegistryFlagFailOpen } from "./registry-flag.js";
|
|
26
26
|
import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_KEEP_ALIVE_DUE_WAKE_PRECISE_FLAG, LANE_LEAD_ADMISSION_PRIORITY_FLAG, LANE_QUOTA_ACCOUNT_FAILOVER_FLAG, } from "./flag-keys.js";
|
|
27
27
|
import { checkLaneDiskSpace, configuredCodexRolloutHomes, laneDiskAccount, retainCodexRollouts } from "./disk-guard.js";
|
|
28
28
|
import { laneCompanyUidSource, resolveLaneCompany } from "./lane-company.js";
|
|
@@ -1216,6 +1216,7 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
1216
1216
|
}
|
|
1217
1217
|
}
|
|
1218
1218
|
const readRegistryFlag = deps.readRegistryFlag ?? readLanesRegistryFlag;
|
|
1219
|
+
const readRegistryFlagFailOpen = deps.readRegistryFlag ?? readLanesRegistryFlagFailOpen;
|
|
1219
1220
|
const storyCloseCompanies = new Map();
|
|
1220
1221
|
for (const lane of lanes) {
|
|
1221
1222
|
if (lane.depth < 1 || lane.state !== "interrupted" || lane.keep_alive === true ||
|
|
@@ -1303,10 +1304,10 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
1303
1304
|
readRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, options),
|
|
1304
1305
|
readRegistryFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, options),
|
|
1305
1306
|
hasBlockedKeepAliveLane
|
|
1306
|
-
?
|
|
1307
|
+
? readRegistryFlagFailOpen(LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, options)
|
|
1307
1308
|
: Promise.resolve(false),
|
|
1308
1309
|
readRegistryFlag(LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, options),
|
|
1309
|
-
|
|
1310
|
+
readRegistryFlagFailOpen(LANE_KEEP_ALIVE_DUE_WAKE_PRECISE_FLAG, options),
|
|
1310
1311
|
]);
|
|
1311
1312
|
keepAliveFlagByCompany.set(key, keepAliveEnabled);
|
|
1312
1313
|
blockedResumeFlagByCompany.set(key, blockedResumeEnabled);
|
|
@@ -3585,6 +3586,7 @@ async function runTerminalKeepAliveCandidatePass(hqRoot, lanes, summary, deps, n
|
|
|
3585
3586
|
if (company === null)
|
|
3586
3587
|
return summary;
|
|
3587
3588
|
const readFlag = deps.readRegistryFlag ?? readLanesRegistryFlag;
|
|
3589
|
+
const readFlagFailOpen = deps.readRegistryFlag ?? readLanesRegistryFlagFailOpen;
|
|
3588
3590
|
const options = { company, env: deps.env ?? process.env };
|
|
3589
3591
|
let keepAliveEnabled;
|
|
3590
3592
|
let lineageAware;
|
|
@@ -3595,7 +3597,7 @@ async function runTerminalKeepAliveCandidatePass(hqRoot, lanes, summary, deps, n
|
|
|
3595
3597
|
readFlag(LEAD_LANE_KEEP_ALIVE_FLAG, options),
|
|
3596
3598
|
readFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, options),
|
|
3597
3599
|
readFlag(LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, options),
|
|
3598
|
-
|
|
3600
|
+
readFlagFailOpen(LANE_KEEP_ALIVE_DUE_WAKE_PRECISE_FLAG, options),
|
|
3599
3601
|
]);
|
|
3600
3602
|
}
|
|
3601
3603
|
catch (err) {
|
|
@@ -19,12 +19,22 @@ export interface ReadLanesFlagOptions {
|
|
|
19
19
|
reportFailure?: (error: unknown) => void;
|
|
20
20
|
}
|
|
21
21
|
export declare function readLanesFlagLocalOverride(flagKey: string, env?: NodeJS.ProcessEnv): boolean | null;
|
|
22
|
+
export type LanesRegistryFlagResult = {
|
|
23
|
+
state: "resolved";
|
|
24
|
+
value: boolean;
|
|
25
|
+
} | {
|
|
26
|
+
state: "unknown";
|
|
27
|
+
};
|
|
22
28
|
/**
|
|
23
29
|
* Read a lanes flag once for each company identity in this CLI process. The
|
|
24
|
-
* cache contains only an in-flight or settled
|
|
30
|
+
* cache contains only an in-flight or settled decision result; it is never
|
|
25
31
|
* written to disk. A local override is checked on every call and wins first.
|
|
26
32
|
*/
|
|
27
33
|
export declare function readLanesRegistryFlag(flagKey: string, options?: ReadLanesFlagOptions): Promise<boolean>;
|
|
34
|
+
/** Read a lanes flag while preserving registry failure as an explicit unknown state. */
|
|
35
|
+
export declare function readLanesRegistryFlagResult(flagKey: string, options?: ReadLanesFlagOptions): Promise<LanesRegistryFlagResult>;
|
|
36
|
+
/** Delivery and wake call sites opt in to availability when the registry is unreadable. */
|
|
37
|
+
export declare function readLanesRegistryFlagFailOpen(flagKey: string, options?: ReadLanesFlagOptions): Promise<boolean>;
|
|
28
38
|
/**
|
|
29
39
|
* Flag readers normally report their own failures and return the safe default.
|
|
30
40
|
* Keep enforcement callers safe if an injected or unexpected reader rejects.
|
|
@@ -8,6 +8,7 @@ export function readLanesFlagLocalOverride(flagKey, env = process.env) {
|
|
|
8
8
|
}
|
|
9
9
|
const registryReads = new Map();
|
|
10
10
|
const reportedThrownFlagReads = new Set();
|
|
11
|
+
const reportedRegistryFailureFallbacks = new Set();
|
|
11
12
|
class MissingFlagTokenError extends Error {
|
|
12
13
|
constructor() {
|
|
13
14
|
super("No cached HQ identity token is available");
|
|
@@ -17,13 +18,13 @@ class MissingFlagTokenError extends Error {
|
|
|
17
18
|
function errorClass(error) {
|
|
18
19
|
return error instanceof Error ? error.name : "UnknownError";
|
|
19
20
|
}
|
|
20
|
-
function reportReadFailure(flagKey, error) {
|
|
21
|
+
function reportReadFailure(flagKey, error, fallback) {
|
|
21
22
|
const suffix = flagKey.replace(/^lanes\./, "").replaceAll(/[^a-zA-Z0-9]+/g, "_");
|
|
22
23
|
process.stderr.write(`${JSON.stringify({
|
|
23
24
|
event: `lanes.${suffix}.flag_read_failed`,
|
|
24
25
|
flag: flagKey,
|
|
25
26
|
error: errorClass(error),
|
|
26
|
-
fallback
|
|
27
|
+
fallback,
|
|
27
28
|
})}\n`);
|
|
28
29
|
}
|
|
29
30
|
function reportInvalidCompanyUid(flagKey, source) {
|
|
@@ -46,27 +47,42 @@ function cacheKey(flagKey, company, endpoint) {
|
|
|
46
47
|
}
|
|
47
48
|
/**
|
|
48
49
|
* Read a lanes flag once for each company identity in this CLI process. The
|
|
49
|
-
* cache contains only an in-flight or settled
|
|
50
|
+
* cache contains only an in-flight or settled decision result; it is never
|
|
50
51
|
* written to disk. A local override is checked on every call and wins first.
|
|
51
52
|
*/
|
|
52
53
|
export async function readLanesRegistryFlag(flagKey, options = {}) {
|
|
54
|
+
const result = await readLanesRegistryFlagResult(flagKey, options);
|
|
55
|
+
return result.state === "resolved" ? result.value : false;
|
|
56
|
+
}
|
|
57
|
+
/** Read a lanes flag while preserving registry failure as an explicit unknown state. */
|
|
58
|
+
export async function readLanesRegistryFlagResult(flagKey, options = {}) {
|
|
59
|
+
const outcome = await readRegistryFlagOutcome(flagKey, options, false);
|
|
60
|
+
return outcome.result;
|
|
61
|
+
}
|
|
62
|
+
async function readRegistryFlagOutcome(flagKey, options, failOpen) {
|
|
53
63
|
const env = options.env ?? process.env;
|
|
54
64
|
const localOverride = readLanesFlagLocalOverride(flagKey, env);
|
|
55
65
|
if (localOverride !== null)
|
|
56
|
-
return localOverride;
|
|
66
|
+
return { result: { state: "resolved", value: localOverride } };
|
|
57
67
|
const companyUid = options.company?.uid?.trim();
|
|
58
68
|
if (companyUid && !companyUid.startsWith("cmp_")) {
|
|
59
69
|
reportInvalidCompanyUid(flagKey, options.companyUidSource ?? "company.uid");
|
|
60
|
-
return false;
|
|
70
|
+
return { result: { state: "resolved", value: false } };
|
|
61
71
|
}
|
|
62
72
|
const endpoint = resolveFlagRegistryEndpoint(undefined, env);
|
|
63
73
|
const key = cacheKey(flagKey, options.company, endpoint);
|
|
64
74
|
const cached = registryReads.get(key);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
const pending = cached ?? readRegistryValue(flagKey, endpoint, options);
|
|
76
|
+
if (!cached)
|
|
77
|
+
registryReads.set(key, pending);
|
|
78
|
+
const outcome = await pending;
|
|
79
|
+
reportRegistryFailure(flagKey, key, outcome, options, failOpen);
|
|
80
|
+
return outcome;
|
|
81
|
+
}
|
|
82
|
+
/** Delivery and wake call sites opt in to availability when the registry is unreadable. */
|
|
83
|
+
export async function readLanesRegistryFlagFailOpen(flagKey, options = {}) {
|
|
84
|
+
const outcome = await readRegistryFlagOutcome(flagKey, options, true);
|
|
85
|
+
return outcome.result.state === "unknown" ? true : outcome.result.value;
|
|
70
86
|
}
|
|
71
87
|
/**
|
|
72
88
|
* Flag readers normally report their own failures and return the safe default.
|
|
@@ -97,15 +113,10 @@ async function readRegistryValue(flagKey, endpoint, options) {
|
|
|
97
113
|
const companyUid = company?.uid?.trim();
|
|
98
114
|
const companySlug = company?.slug?.trim();
|
|
99
115
|
let client;
|
|
100
|
-
let
|
|
101
|
-
let
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
failed = true;
|
|
105
|
-
if (reported)
|
|
106
|
-
return;
|
|
107
|
-
reported = true;
|
|
108
|
-
(options.reportFailure ?? ((cause) => reportReadFailure(flagKey, cause)))(error);
|
|
116
|
+
let resolution;
|
|
117
|
+
let failure;
|
|
118
|
+
const rememberFailure = (error) => {
|
|
119
|
+
failure ??= { error };
|
|
109
120
|
};
|
|
110
121
|
try {
|
|
111
122
|
const acquireToken = options.getToken ?? (async () => {
|
|
@@ -135,30 +146,58 @@ async function readRegistryValue(flagKey, endpoint, options) {
|
|
|
135
146
|
requestTimeoutMs: LANES_FLAG_READ_TIMEOUT_MS,
|
|
136
147
|
...(companyUid ? { companyUid } : {}),
|
|
137
148
|
...(companySlug ? { companyIdentifiers: [companySlug] } : {}),
|
|
138
|
-
onError:
|
|
149
|
+
onError: rememberFailure,
|
|
139
150
|
});
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
151
|
+
try {
|
|
152
|
+
await client.ready();
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
rememberFailure(error);
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
resolution = client.explain(flagKey, { fallback: false });
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
rememberFailure(error);
|
|
146
162
|
}
|
|
147
163
|
}
|
|
148
164
|
catch (error) {
|
|
149
|
-
|
|
165
|
+
rememberFailure(error);
|
|
150
166
|
}
|
|
151
167
|
finally {
|
|
152
168
|
try {
|
|
153
169
|
client?.close();
|
|
154
170
|
}
|
|
155
171
|
catch (error) {
|
|
156
|
-
|
|
172
|
+
rememberFailure(error);
|
|
157
173
|
}
|
|
158
174
|
}
|
|
159
|
-
|
|
175
|
+
if (resolution?.source === "snapshot" || resolution?.source === "local-override") {
|
|
176
|
+
return { result: { state: "resolved", value: resolution.value }, ...(failure ? { failure } : {}) };
|
|
177
|
+
}
|
|
178
|
+
if (failure)
|
|
179
|
+
return { result: { state: "unknown" }, failure };
|
|
180
|
+
return { result: { state: "resolved", value: resolution?.value ?? false } };
|
|
181
|
+
}
|
|
182
|
+
function reportRegistryFailure(flagKey, key, outcome, options, failOpen) {
|
|
183
|
+
if (!outcome.failure)
|
|
184
|
+
return;
|
|
185
|
+
const fallback = outcome.result.state === "resolved"
|
|
186
|
+
? outcome.result.value
|
|
187
|
+
: failOpen;
|
|
188
|
+
const reportKey = `${key}:${fallback}`;
|
|
189
|
+
if (reportedRegistryFailureFallbacks.has(reportKey))
|
|
190
|
+
return;
|
|
191
|
+
reportedRegistryFailureFallbacks.add(reportKey);
|
|
192
|
+
const reportFailure = options.reportFailure;
|
|
193
|
+
if (reportFailure) {
|
|
194
|
+
reportFailure(outcome.failure.error);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
reportReadFailure(flagKey, outcome.failure.error, fallback);
|
|
160
198
|
}
|
|
161
199
|
export function _resetLanesRegistryFlagCacheForTests() {
|
|
162
200
|
registryReads.clear();
|
|
201
|
+
reportedRegistryFailureFallbacks.clear();
|
|
163
202
|
}
|
|
164
203
|
//# sourceMappingURL=registry-flag.js.map
|
|
@@ -398,8 +398,69 @@ function applyTerminalToLaneWithResult(hqRoot, laneId, classification, opts = {}
|
|
|
398
398
|
current.state === "interrupted" ||
|
|
399
399
|
current.state === "cancelled" ||
|
|
400
400
|
current.state === "killed";
|
|
401
|
-
if (terminal)
|
|
402
|
-
|
|
401
|
+
if (terminal) {
|
|
402
|
+
// A terminal state may have been recorded before the worker's final
|
|
403
|
+
// report arrived. The report is durable output from work that already
|
|
404
|
+
// ran, so preserve it without reopening the lane or rechecking admission.
|
|
405
|
+
if (opts.envelope === undefined || opts.envelope === null)
|
|
406
|
+
return current;
|
|
407
|
+
const approvalGate = enforceRegistryApprovalGate(current, opts.envelope);
|
|
408
|
+
effectiveEnvelope = approvalGate.envelope;
|
|
409
|
+
effectiveClassification = approvalGate.violation
|
|
410
|
+
? { state: "blocked", reason: "approval_gate_violated" }
|
|
411
|
+
: terminalClassification;
|
|
412
|
+
const envelope = effectiveEnvelope;
|
|
413
|
+
const storySwitch = applyStorySwitchEnvelope(current, envelope, at, envelopeRef);
|
|
414
|
+
const next = {
|
|
415
|
+
...current,
|
|
416
|
+
last_envelope: {
|
|
417
|
+
ref: envelopeRef,
|
|
418
|
+
decision: envelope.decision,
|
|
419
|
+
at,
|
|
420
|
+
artifacts: envelope.artifacts,
|
|
421
|
+
...(envelope.blocked_reason !== undefined ? { blocked_reason: envelope.blocked_reason } : {}),
|
|
422
|
+
...(envelope.next_check_at !== undefined ? { next_check_at: envelope.next_check_at } : {}),
|
|
423
|
+
},
|
|
424
|
+
...(envelope.decision !== "blocked" ? { keep_alive_blocked_resume_streak: 0 } : {}),
|
|
425
|
+
envelope_validation_errors: opts.envelopeValidationErrors,
|
|
426
|
+
};
|
|
427
|
+
if (!approvalGate.violation && approvalGate.approvedIdsToConsume.length > 0) {
|
|
428
|
+
next.registry_worker_approval_history = approvalGate.approvedIdsToConsume.reduce((history, approvalId) => appendCappedHistory(history, {
|
|
429
|
+
event: "consumed",
|
|
430
|
+
approval_id: approvalId,
|
|
431
|
+
envelope_ref: envelopeRef,
|
|
432
|
+
at,
|
|
433
|
+
}), current.registry_worker_approval_history ?? []);
|
|
434
|
+
}
|
|
435
|
+
if (current.state === "cancelled" || current.state === "killed") {
|
|
436
|
+
return JSON.stringify(next) !== JSON.stringify(current) ? next : current;
|
|
437
|
+
}
|
|
438
|
+
const storyStatus = storySwitch.suppressCompletion
|
|
439
|
+
? "in_progress"
|
|
440
|
+
: terminalEnvelopeStoryStatus(current, envelope.decision, envelope.story_status);
|
|
441
|
+
if (storyStatus !== undefined) {
|
|
442
|
+
next.story_status = storySwitch.suppressCompletion ? "in_progress" : envelope.story_status;
|
|
443
|
+
next.story_complete = storySwitch.suppressCompletion ? false : envelope.story_complete;
|
|
444
|
+
next.story_complete_reason = storySwitch.suppressCompletion ? undefined : envelope.story_complete_reason;
|
|
445
|
+
next.stories = laneStories(current).map((story) => story.project_id === current.project_id && story.story_id === current.story_id
|
|
446
|
+
? {
|
|
447
|
+
...story,
|
|
448
|
+
status: storyStatus,
|
|
449
|
+
updated_at: at,
|
|
450
|
+
...(envelope.decision === "blocked"
|
|
451
|
+
? { blocked_reason: envelope.blocked_reason ?? "" }
|
|
452
|
+
: { blocked_reason: undefined }),
|
|
453
|
+
}
|
|
454
|
+
: story);
|
|
455
|
+
}
|
|
456
|
+
if (storySwitch.clearPending)
|
|
457
|
+
next.story_switch_pending = undefined;
|
|
458
|
+
if (storySwitch.storySwitchUnacknowledged !== undefined) {
|
|
459
|
+
next.story_switch_unacknowledged = storySwitch.storySwitchUnacknowledged;
|
|
460
|
+
next.attribution_history = storySwitch.attributionHistory ?? current.attribution_history;
|
|
461
|
+
}
|
|
462
|
+
return JSON.stringify(next) !== JSON.stringify(current) ? next : current;
|
|
463
|
+
}
|
|
403
464
|
if (current.state === "awaiting_input" || current.state === "blocked") {
|
|
404
465
|
if (current.state === effectiveClassification.state)
|
|
405
466
|
return current;
|
|
@@ -40,7 +40,7 @@ export const RESIDENT_PACKAGE_MODULES = {
|
|
|
40
40
|
"@indigoai-us/hq-cloud",
|
|
41
41
|
],
|
|
42
42
|
"bot-logs-follow": [],
|
|
43
|
-
mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud", "../sentry.js", "../lib/doctor/client-health-reason-codes-flag.js", "@indigoai-us/hq-flags-client"],
|
|
43
|
+
mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js", "../lib/anywhere-person-setting.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud", "../sentry.js", "../lib/doctor/client-health-reason-codes-flag.js", "@indigoai-us/hq-flags-client"],
|
|
44
44
|
childWrapper: ["../main.js"],
|
|
45
45
|
coreJobRun: ["../lib/flag-registry.js"],
|
|
46
46
|
"daemon-call": ["../lib/daemon/protocol.js"],
|
|
@@ -76,6 +76,7 @@ export const RESIDENT_PACKAGE_MODULES = {
|
|
|
76
76
|
"../utils/plan-gate-error.js",
|
|
77
77
|
"open",
|
|
78
78
|
"../commands/mcp-serve.js",
|
|
79
|
+
"../lib/anywhere-person-setting.js",
|
|
79
80
|
"../lib/daemon/mcp-oauth.js",
|
|
80
81
|
"../lib/mcp/tools.js",
|
|
81
82
|
"../lib/mcp/prompts.js",
|
|
@@ -187,6 +188,7 @@ const LOADERS = {
|
|
|
187
188
|
mcp: [
|
|
188
189
|
() => import("../lib/mcp/tools.js"),
|
|
189
190
|
() => import("../lib/mcp/prompts.js"),
|
|
191
|
+
() => import("../lib/anywhere-person-setting.js"),
|
|
190
192
|
() => import("../utils/cognito-session.js"),
|
|
191
193
|
() => import("@indigoai-us/hq-cloud"),
|
|
192
194
|
async () => {
|
|
@@ -347,6 +349,7 @@ const LOADERS = {
|
|
|
347
349
|
() => import("../utils/plan-gate-error.js"),
|
|
348
350
|
() => import("open"),
|
|
349
351
|
() => import("../commands/mcp-serve.js"),
|
|
352
|
+
() => import("../lib/anywhere-person-setting.js"),
|
|
350
353
|
() => import("../lib/daemon/mcp-oauth.js"),
|
|
351
354
|
() => import("../lib/mcp/tools.js"),
|
|
352
355
|
() => import("../lib/mcp/prompts.js"),
|
|
@@ -65,7 +65,7 @@ import chalk from "chalk";
|
|
|
65
65
|
import { Sentry } from "../sentry.js";
|
|
66
66
|
import { isCliTelemetryDisabled } from "./cli-telemetry.js";
|
|
67
67
|
import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
|
|
68
|
-
import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput,
|
|
68
|
+
import { buildBunInstallArgv, buildPnpmInstallArgv, buildPrefixedInstallArgv, buildSpawnPlan, canWriteNpmInstall, captureNonWritableNpmPrefixNotice, claimNonWritableNpmPrefixNotice, checkUpdateConvergence, derivePnpmHome, detachUpdateSupervisor, isBunManagedPackageDir, isLocalDependencyInstall, isPnpmManagedPackageDir, nonWritablePrefixNote, openInstallOutput, resolvePnpmPathInstall, pnpmUpdateEnv, pnpmInstalledVersion, performPnpmUpdate, resolveRunningInstall, runUpdateCommand, runSupervisedUpdateCommand, UPDATE_RECURSION_GUARD_ENV, } from "./version-gate.js";
|
|
69
69
|
import { canStageInstall, stagedNpmInstall } from "./staged-install.js";
|
|
70
70
|
import { acquireUpdateLock as acquireSharedUpdateLock } from "./update-lock.js";
|
|
71
71
|
import { markLatestIneffective } from "./version-check.js";
|
|
@@ -926,17 +926,18 @@ async function attemptUpdateAndReexec(argv, flavor, known, deps, env) {
|
|
|
926
926
|
// actually running would stay exactly as stale as it started while the CLI
|
|
927
927
|
// reported success and re-exec'd into the same build. That is the shape of
|
|
928
928
|
// the 2026-09-02 gate loop — see isLocalDependencyInstall in version-gate.ts.
|
|
929
|
-
|
|
929
|
+
let install = (deps.resolveInstall ?? resolveRunningInstall)();
|
|
930
930
|
if (isLocalDependencyInstall(install)) {
|
|
931
931
|
console.error(chalk.dim(`hq-cli ${latest} is available, but this copy is a local dependency (${install.packageRoot}) — update the project that owns it.`));
|
|
932
932
|
return { action: "skipped", latest };
|
|
933
933
|
}
|
|
934
934
|
if (install.manager === "pnpm") {
|
|
935
|
-
const
|
|
936
|
-
if (
|
|
937
|
-
console.error(chalk.yellow(`⚠ hq self-update refused: ${
|
|
935
|
+
const pathTarget = resolvePnpmPathInstall(install);
|
|
936
|
+
if (pathTarget.refusal) {
|
|
937
|
+
console.error(chalk.yellow(`⚠ hq self-update refused: ${pathTarget.refusal}`));
|
|
938
938
|
return { action: "skipped", latest };
|
|
939
939
|
}
|
|
940
|
+
install = pathTarget.install;
|
|
940
941
|
}
|
|
941
942
|
const releaseLock = flavor.lock ? (deps.acquireLock ?? acquireUpdateLock)() : () => { };
|
|
942
943
|
if (!releaseLock)
|
|
@@ -194,6 +194,8 @@ export declare function isPrefixWritable(prefix: string, access?: (target: strin
|
|
|
194
194
|
* existing ancestor: npm can create a child only if that ancestor is writable.
|
|
195
195
|
*/
|
|
196
196
|
export declare function canWriteNpmInstall(prefix: string, packageRoot: string, access?: (target: string, mode: number) => void, platform?: NodeJS.Platform): boolean;
|
|
197
|
+
/** Check whether pnpm can replace its global package and PATH shim. */
|
|
198
|
+
export declare function canWritePnpmInstall(install: RunningInstall, access?: (target: string, mode: number) => void): boolean;
|
|
197
199
|
/** Claim the one local warning/report for this installed version and prefix. */
|
|
198
200
|
export declare function claimNonWritableNpmPrefixNotice(install: Pick<RunningInstall, "manager" | "prefix" | "packageRoot">, fromVersion: string, homeDir?: string, kind?: "user" | "report"): boolean;
|
|
199
201
|
/** Emit one path-free warning event for a handled non-writable npm prefix. */
|
|
@@ -223,10 +225,12 @@ export declare function derivePnpmHome(install: RunningInstall): string | null;
|
|
|
223
225
|
* Environment for the pnpm self-update spawn. pnpm requires its configured
|
|
224
226
|
* global bin directory to be on PATH; pnpm 12.5.1 reports
|
|
225
227
|
* `ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH` otherwise. Minimal-environment parents
|
|
226
|
-
* may also lack PNPM_HOME, so derive it from the
|
|
228
|
+
* may also lack PNPM_HOME, so derive it from the selected install when needed.
|
|
227
229
|
*
|
|
228
|
-
* A no-op for npm/bun installs.
|
|
229
|
-
*
|
|
230
|
+
* A no-op for npm/bun installs. The selected install's home owns its global
|
|
231
|
+
* bin directory, so it takes precedence over an inherited PNPM_HOME after the
|
|
232
|
+
* updater retargets the install. Returns `undefined` only when the inherited
|
|
233
|
+
* environment already has the required values.
|
|
230
234
|
*/
|
|
231
235
|
export declare function pnpmUpdateEnv(install: RunningInstall, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv | undefined;
|
|
232
236
|
export declare function buildPrefixedInstallArgv(prefix: string): string[];
|
|
@@ -436,7 +440,10 @@ declare function probeCliVersion(bin: string): string | null;
|
|
|
436
440
|
* shims, so writing one home cannot change a different home that appears first
|
|
437
441
|
* on PATH.
|
|
438
442
|
*/
|
|
439
|
-
export declare function
|
|
443
|
+
export declare function resolvePnpmPathInstall(install: RunningInstall, canWrite?: (target: RunningInstall) => boolean): {
|
|
444
|
+
install: RunningInstall;
|
|
445
|
+
refusal: string | null;
|
|
446
|
+
};
|
|
440
447
|
/**
|
|
441
448
|
* Read-your-writes convergence check, run after an install reports success.
|
|
442
449
|
*
|
|
@@ -101,11 +101,11 @@ function findRunningPackageRoot() {
|
|
|
101
101
|
return findCliPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
|
|
102
102
|
}
|
|
103
103
|
/** Resolve an on-PATH executable back to the installed hq-cli package it loads. */
|
|
104
|
-
function
|
|
104
|
+
function resolvePackageRootForExecutable(bin) {
|
|
105
105
|
try {
|
|
106
106
|
const packageRoot = findCliPackageRoot(path.dirname(realpathSync(bin)));
|
|
107
107
|
if (packageRoot)
|
|
108
|
-
return packageRoot;
|
|
108
|
+
return { packageRoot, candidates: [packageRoot] };
|
|
109
109
|
}
|
|
110
110
|
catch {
|
|
111
111
|
// A missing or inaccessible executable may still be recoverable from the
|
|
@@ -115,25 +115,26 @@ function findPackageRootForExecutable(bin) {
|
|
|
115
115
|
// resolves symlinks, but a generated shim is a regular file, so walking up
|
|
116
116
|
// from its real path never reaches the package it invokes. Look up only the
|
|
117
117
|
// hq-cli package under the matching PNPM_HOME/global layout instead.
|
|
118
|
-
const
|
|
118
|
+
const shimBinDir = path.dirname(bin);
|
|
119
119
|
const configuredHome = process.env.PNPM_HOME;
|
|
120
|
-
const inferredHome = path.basename(
|
|
121
|
-
? path.dirname(
|
|
122
|
-
:
|
|
120
|
+
const inferredHome = path.basename(shimBinDir).toLowerCase() === "bin"
|
|
121
|
+
? path.dirname(shimBinDir)
|
|
122
|
+
: shimBinDir;
|
|
123
123
|
const candidateHomes = [configuredHome, inferredHome].filter((home, index, homes) => Boolean(home) && homes.findIndex((candidate) => candidate === home) === index);
|
|
124
|
-
const home = candidateHomes.find((candidate) => samePath(
|
|
125
|
-
samePath(
|
|
124
|
+
const home = candidateHomes.find((candidate) => samePath(shimBinDir, candidate) ||
|
|
125
|
+
samePath(shimBinDir, path.join(candidate, "bin")));
|
|
126
126
|
if (!home)
|
|
127
|
-
return null;
|
|
127
|
+
return { packageRoot: null, candidates: [] };
|
|
128
128
|
const globalRoot = path.join(home, "global");
|
|
129
129
|
let storeVersions;
|
|
130
130
|
try {
|
|
131
131
|
storeVersions = readdirSync(globalRoot);
|
|
132
132
|
}
|
|
133
133
|
catch {
|
|
134
|
-
return null;
|
|
134
|
+
return { packageRoot: null, candidates: [] };
|
|
135
135
|
}
|
|
136
136
|
const packageSegments = CLI_NAME.split("/");
|
|
137
|
+
const candidates = new Set();
|
|
137
138
|
for (const storeVersion of storeVersions) {
|
|
138
139
|
const storeRoot = path.join(globalRoot, storeVersion);
|
|
139
140
|
const packageRoots = [
|
|
@@ -156,11 +157,38 @@ function findPackageRootForExecutable(bin) {
|
|
|
156
157
|
if (packageRoot &&
|
|
157
158
|
samePath(packageRoot, candidate) &&
|
|
158
159
|
isPnpmManagedPackageDir(packageRoot)) {
|
|
159
|
-
|
|
160
|
+
candidates.add(packageRoot);
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
|
-
|
|
164
|
+
const roots = [...candidates];
|
|
165
|
+
if (roots.length <= 1)
|
|
166
|
+
return { packageRoot: roots[0] ?? null, candidates: roots };
|
|
167
|
+
// pnpm's generated shim identifies the package entry point it will execute.
|
|
168
|
+
// Use that target to select among v11 hash directories instead of letting
|
|
169
|
+
// directory enumeration order choose the global project and virtual store.
|
|
170
|
+
let shimContents;
|
|
171
|
+
try {
|
|
172
|
+
shimContents = readFileSync(bin, "utf8").replace(/\\/g, "/");
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return { packageRoot: null, candidates: roots };
|
|
176
|
+
}
|
|
177
|
+
const selected = roots.filter((root) => {
|
|
178
|
+
const entryPoint = installedHqEntryPoint({ manager: "pnpm", prefix: null, packageRoot: root });
|
|
179
|
+
if (!entryPoint)
|
|
180
|
+
return false;
|
|
181
|
+
const absolute = entryPoint.replace(/\\/g, "/");
|
|
182
|
+
const relative = path.relative(shimBinDir, entryPoint).replace(/\\/g, "/");
|
|
183
|
+
return shimContents.includes(absolute) || shimContents.includes(relative);
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
packageRoot: selected.length === 1 ? selected[0] : null,
|
|
187
|
+
candidates: roots,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function findPackageRootForExecutable(bin) {
|
|
191
|
+
return resolvePackageRootForExecutable(bin).packageRoot;
|
|
164
192
|
}
|
|
165
193
|
function samePath(left, right) {
|
|
166
194
|
const canonical = (value) => {
|
|
@@ -507,6 +535,39 @@ export function canWriteNpmInstall(prefix, packageRoot, access = (target, mode)
|
|
|
507
535
|
}
|
|
508
536
|
});
|
|
509
537
|
}
|
|
538
|
+
/** Check whether pnpm can replace its global package and PATH shim. */
|
|
539
|
+
export function canWritePnpmInstall(install, access = (target, mode) => accessSync(target, mode)) {
|
|
540
|
+
const home = derivePnpmHome(install);
|
|
541
|
+
if (!home || !install.packageRoot)
|
|
542
|
+
return false;
|
|
543
|
+
const mode = fsConstants.W_OK | fsConstants.X_OK;
|
|
544
|
+
const dirs = pnpmInstallDirs(install);
|
|
545
|
+
const targets = [
|
|
546
|
+
home,
|
|
547
|
+
path.join(home, "global"),
|
|
548
|
+
path.join(home, "store"),
|
|
549
|
+
...(dirs ? [dirs.globalDir, dirs.storeDir, dirs.virtualStoreDir] : []),
|
|
550
|
+
path.dirname(install.packageRoot),
|
|
551
|
+
];
|
|
552
|
+
return targets.every((target) => {
|
|
553
|
+
let candidate = path.resolve(target);
|
|
554
|
+
while (true) {
|
|
555
|
+
try {
|
|
556
|
+
access(candidate, mode);
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
if (error?.code !== "ENOENT")
|
|
561
|
+
return false;
|
|
562
|
+
// pnpm creates missing targets under the nearest existing writable ancestor.
|
|
563
|
+
const parent = path.dirname(candidate);
|
|
564
|
+
if (parent === candidate)
|
|
565
|
+
return false;
|
|
566
|
+
candidate = parent;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
510
571
|
const nonWritablePrefixNotices = new Set();
|
|
511
572
|
/** Claim the one local warning/report for this installed version and prefix. */
|
|
512
573
|
export function claimNonWritableNpmPrefixNotice(install, fromVersion, homeDir = os.homedir(), kind = "user") {
|
|
@@ -601,18 +662,22 @@ export function derivePnpmHome(install) {
|
|
|
601
662
|
* Environment for the pnpm self-update spawn. pnpm requires its configured
|
|
602
663
|
* global bin directory to be on PATH; pnpm 12.5.1 reports
|
|
603
664
|
* `ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH` otherwise. Minimal-environment parents
|
|
604
|
-
* may also lack PNPM_HOME, so derive it from the
|
|
665
|
+
* may also lack PNPM_HOME, so derive it from the selected install when needed.
|
|
605
666
|
*
|
|
606
|
-
* A no-op for npm/bun installs.
|
|
607
|
-
*
|
|
667
|
+
* A no-op for npm/bun installs. The selected install's home owns its global
|
|
668
|
+
* bin directory, so it takes precedence over an inherited PNPM_HOME after the
|
|
669
|
+
* updater retargets the install. Returns `undefined` only when the inherited
|
|
670
|
+
* environment already has the required values.
|
|
608
671
|
*/
|
|
609
672
|
export function pnpmUpdateEnv(install, base = process.env) {
|
|
610
673
|
if (install.manager !== "pnpm")
|
|
611
674
|
return undefined;
|
|
612
|
-
const home =
|
|
675
|
+
const home = derivePnpmHome(install) ?? base.PNPM_HOME;
|
|
613
676
|
if (!home)
|
|
614
677
|
return undefined;
|
|
615
|
-
|
|
678
|
+
// pnpm's own `bin -g` command reports PNPM_HOME itself as the global bin
|
|
679
|
+
// directory; it must be that directory, not a guessed `<home>/bin` child.
|
|
680
|
+
const binDir = home;
|
|
616
681
|
const pathKey = Object.keys(base).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
617
682
|
const currentPath = base[pathKey] ?? "";
|
|
618
683
|
const normalizedBinDir = path.resolve(binDir);
|
|
@@ -633,7 +698,7 @@ export function pnpmUpdateEnv(install, base = process.env) {
|
|
|
633
698
|
COREPACK_DEFAULT_TO_LATEST: "0",
|
|
634
699
|
COREPACK_ENABLE_AUTO_PIN: "0",
|
|
635
700
|
};
|
|
636
|
-
if (
|
|
701
|
+
if (base.PNPM_HOME !== home)
|
|
637
702
|
result.PNPM_HOME = home;
|
|
638
703
|
if (!alreadyOnPath)
|
|
639
704
|
result[pathKey] = [currentPath, binDir].filter(Boolean).join(path.delimiter);
|
|
@@ -1401,36 +1466,62 @@ function probeCliVersion(bin) {
|
|
|
1401
1466
|
* shims, so writing one home cannot change a different home that appears first
|
|
1402
1467
|
* on PATH.
|
|
1403
1468
|
*/
|
|
1404
|
-
export function
|
|
1469
|
+
export function resolvePnpmPathInstall(install, canWrite = canWritePnpmInstall) {
|
|
1405
1470
|
if (install.manager !== "pnpm" || !install.packageRoot)
|
|
1406
|
-
return null;
|
|
1471
|
+
return { install, refusal: null };
|
|
1407
1472
|
const bin = resolveHqOnPath();
|
|
1408
1473
|
if (!bin)
|
|
1409
|
-
return null;
|
|
1410
|
-
const pathPackageRoot = findPackageRootForExecutable(bin);
|
|
1411
|
-
if (!pathPackageRoot ||
|
|
1412
|
-
globalInstallManagerForPath(bin, pathPackageRoot) !== "pnpm") {
|
|
1413
|
-
return null;
|
|
1414
|
-
}
|
|
1415
|
-
// The fallback resolver scans every hash in a PNPM_HOME because generated
|
|
1416
|
-
// shims do not expose their target. Multiple installs in the SAME home can
|
|
1417
|
-
// therefore produce an arbitrary package root; only a different home means
|
|
1418
|
-
// that updating this install cannot change which global shim wins PATH.
|
|
1474
|
+
return { install, refusal: null };
|
|
1419
1475
|
const runningHome = derivePnpmHome(install);
|
|
1476
|
+
const packageRootLookup = resolvePackageRootForExecutable(bin);
|
|
1477
|
+
const pathPackageRoot = packageRootLookup.packageRoot;
|
|
1478
|
+
if (!pathPackageRoot) {
|
|
1479
|
+
if (packageRootLookup.candidates.length > 1) {
|
|
1480
|
+
return {
|
|
1481
|
+
install,
|
|
1482
|
+
refusal: `could not identify which pnpm package PATH binary ${bin} resolves to; matching global packages: ${packageRootLookup.candidates.join(", ")}`,
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
const binDirectory = path.dirname(bin);
|
|
1486
|
+
const binHome = path.basename(binDirectory).toLowerCase() === "bin"
|
|
1487
|
+
? path.dirname(binDirectory)
|
|
1488
|
+
: binDirectory;
|
|
1489
|
+
if (runningHome && binHome && samePath(runningHome, binHome)) {
|
|
1490
|
+
if (canWrite(install))
|
|
1491
|
+
return { install, refusal: null };
|
|
1492
|
+
return {
|
|
1493
|
+
install,
|
|
1494
|
+
refusal: `cannot update the pnpm install resolved by PATH at ${bin}: its pnpm home is not writable`,
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
return { install, refusal: `could not identify the pnpm-managed install resolved by PATH at ${bin}` };
|
|
1498
|
+
}
|
|
1499
|
+
const pathManager = globalInstallManagerForPath(bin, pathPackageRoot);
|
|
1500
|
+
if (pathManager !== "pnpm") {
|
|
1501
|
+
if (pathManager === "npm" || pathManager === "bun") {
|
|
1502
|
+
// Updating the running pnpm install still makes progress when a known
|
|
1503
|
+
// npm/Bun install shadows it; refusing would guarantee no convergence.
|
|
1504
|
+
return { install, refusal: null };
|
|
1505
|
+
}
|
|
1506
|
+
return { install, refusal: `PATH resolves ${bin} to an install that is not verified as pnpm-managed` };
|
|
1507
|
+
}
|
|
1508
|
+
// The resolver uses the generated shim's target when a pnpm v11 home has
|
|
1509
|
+
// multiple package hashes. Its selected package root must drive the global
|
|
1510
|
+
// directory and virtual store used by the update.
|
|
1420
1511
|
const pathHome = derivePnpmHome({ manager: "pnpm", prefix: null, packageRoot: pathPackageRoot });
|
|
1421
|
-
if (!runningHome || !pathHome
|
|
1422
|
-
return
|
|
1423
|
-
let resolvedBin = bin;
|
|
1424
|
-
try {
|
|
1425
|
-
resolvedBin = realpathSync(bin);
|
|
1512
|
+
if (!runningHome || !pathHome) {
|
|
1513
|
+
return { install, refusal: `could not identify the PNPM_HOME for PATH binary ${bin}` };
|
|
1426
1514
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1515
|
+
const targetInstall = samePath(runningHome, pathHome)
|
|
1516
|
+
? install
|
|
1517
|
+
: { manager: "pnpm", prefix: null, packageRoot: pathPackageRoot };
|
|
1518
|
+
if (!canWrite(targetInstall)) {
|
|
1519
|
+
return {
|
|
1520
|
+
install,
|
|
1521
|
+
refusal: `cannot update the pnpm install resolved by PATH at ${bin}: its pnpm home is not writable`,
|
|
1522
|
+
};
|
|
1429
1523
|
}
|
|
1430
|
-
|
|
1431
|
-
return (`PATH resolves ${resolvedBin} at version ${version} (manager: pnpm), ` +
|
|
1432
|
-
`but this process is running from a different pnpm install at ${install.packageRoot}. ` +
|
|
1433
|
-
"Refusing to update one pnpm home while another hq-cli install wins PATH resolution.");
|
|
1524
|
+
return { install: targetInstall, refusal: null };
|
|
1434
1525
|
}
|
|
1435
1526
|
/**
|
|
1436
1527
|
* Read-your-writes convergence check, run after an install reports success.
|
package/docs/anywhere-runtime.md
CHANGED
|
@@ -5,7 +5,9 @@ local hqd daemon on macOS, Linux, and Windows. On Windows, hqd uses a bundled
|
|
|
5
5
|
PowerShell pipe host that applies and verifies a user-only named-pipe DACL
|
|
6
6
|
before becoming ready. A per-user
|
|
7
7
|
registry links repositories to HQ companies. The `hq-anywhere-runtime` flag
|
|
8
|
-
is
|
|
8
|
+
is an operator kill switch that defaults on when its registry row is absent,
|
|
9
|
+
archived, or unreadable. An explicit registry false or the local
|
|
10
|
+
`HQ_FLAG_HQ_ANYWHERE_RUNTIME=false|0` override turns it off. Runtime access also
|
|
9
11
|
requires the current person to enable the `hq-anywhere` setting in HQ. If that
|
|
10
12
|
setting cannot be read, runtime access stays off.
|
|
11
13
|
|
|
@@ -34,8 +36,8 @@ stored in a repository or synced HQ folder.
|
|
|
34
36
|
Windows requires Windows PowerShell 5.1 or PowerShell 7 for the bundled pipe
|
|
35
37
|
host. If it is unavailable, the pipe cannot be created with a verified
|
|
36
38
|
current-user-only DACL, or another process already owns the pipe name, hqd
|
|
37
|
-
refuses to start. The
|
|
38
|
-
|
|
39
|
+
refuses to start. The flag's missing-row fallback is on across platforms; an
|
|
40
|
+
explicit false remains the operator kill switch.
|
|
39
41
|
|
|
40
42
|
On macOS and Linux, hqd checks the socket directory and socket path with
|
|
41
43
|
`lstat` before serving. It refuses symlinked, shared, or foreign-owned paths.
|