@indigoai-us/hq-cli 5.345.160 → 5.345.161
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 +24 -0
- package/assets/scaffold/core/scripts/session-journal.sh +116 -24
- package/dist/command-catalog.generated.d.ts +218 -5
- package/dist/command-catalog.generated.js +269 -5
- package/dist/commands/auth.js +6 -5
- package/dist/commands/core-session-helper-runtime.d.ts +1 -0
- package/dist/commands/core-session-journal-native.d.ts +1 -0
- package/dist/commands/core-session-journal-native.js +158 -34
- package/dist/commands/feedback.d.ts +12 -0
- package/dist/commands/feedback.js +52 -0
- package/dist/commands/files-browse.d.ts +8 -0
- package/dist/commands/files-browse.js +83 -1
- package/dist/commands/install-global.d.ts +2 -0
- package/dist/commands/install-global.js +24 -1
- package/dist/commands/integrations-connect.js +1 -0
- package/dist/commands/integrations-manage.d.ts +2 -0
- package/dist/commands/integrations-manage.js +6 -4
- package/dist/commands/lanes-link.js +5 -3
- package/dist/commands/lanes-session-start.d.ts +12 -0
- package/dist/commands/lanes-session-start.js +93 -12
- package/dist/commands/meetings.d.ts +11 -0
- package/dist/commands/meetings.js +41 -11
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2486 -1794
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +21 -0
- package/dist/lanes-session-start-argv.d.ts +7 -0
- package/dist/lanes-session-start-argv.js +9 -0
- package/dist/lib/bot/log.d.ts +3 -2
- package/dist/lib/bot/progress.d.ts +23 -1
- package/dist/lib/bot/progress.js +43 -5
- package/dist/lib/bot/run.js +2 -0
- package/dist/lib/bot/runtime/claude.d.ts +12 -0
- package/dist/lib/bot/runtime/claude.js +20 -0
- package/dist/lib/bot/runtime/index.d.ts +10 -1
- package/dist/lib/bot/runtime/index.js +14 -2
- package/dist/lib/bot/runtime/types.d.ts +8 -0
- package/dist/lib/doctor/checks/ps1-shims.d.ts +21 -0
- package/dist/lib/doctor/checks/ps1-shims.js +82 -0
- package/dist/lib/doctor/fix/apply.d.ts +13 -1
- package/dist/lib/doctor/fix/apply.js +38 -3
- package/dist/lib/doctor/fix/remediation.d.ts +9 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/doctor/registry.js +2 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/links.d.ts +2 -1
- package/dist/lib/lanes/links.js +235 -17
- package/dist/lib/lanes/registry-flag.d.ts +24 -0
- package/dist/lib/lanes/registry-flag.js +72 -6
- package/dist/lib/lanes/store.d.ts +4 -2
- package/dist/lib/lanes/store.js +8 -8
- package/dist/lib/windows/ps1-shims.d.ts +60 -0
- package/dist/lib/windows/ps1-shims.js +152 -0
- package/dist/utils/self-update.d.ts +2 -0
- package/dist/utils/self-update.js +8 -0
- package/dist/utils/staged-npm-install.js +13 -2
- package/dist/utils/token-keychain.d.ts +11 -8
- package/dist/utils/token-keychain.js +36 -15
- package/package.json +8 -8
- package/scripts/install-hook-engine.mjs +12 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.
|
|
1
|
+
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.99","bundleSha256":"05ece85b87c743511d74111934f420f24a47677e9e90a18dd45e6d3d1d8d0375","formatVersion":1}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { installFastPathStreamErrorHandlers } from "./utils/fast-stream-errors.j
|
|
|
15
15
|
import { installWindowsHideDefault } from "./utils/windows-hide-default.js";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { parseMonitorCheckFastRequest } from "./lanes-monitor-check-argv.js";
|
|
18
|
+
import { isLaneSessionStartFastRequest } from "./lanes-session-start-argv.js";
|
|
18
19
|
import { createRequire } from "node:module";
|
|
19
20
|
import { hookEnginePathArgs, runHookEnginePath } from "./hook-engine-path.js";
|
|
20
21
|
function cygpathMixedPath(value) {
|
|
@@ -156,6 +157,22 @@ async function runMonitorCheckFastWithRecovery(request) {
|
|
|
156
157
|
return;
|
|
157
158
|
await runMainWithInstallTreeRecovery();
|
|
158
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* The SessionStart hook runs `hq lanes session-start` on every new session
|
|
162
|
+
* under a 2 s cap. Load only that command's module, not the command graph.
|
|
163
|
+
*/
|
|
164
|
+
async function runLaneSessionStartFastWithRecovery() {
|
|
165
|
+
const loaded = await recoverFastImport(() => import("./commands/lanes-session-start.js"));
|
|
166
|
+
if (loaded.reexecStatus !== undefined) {
|
|
167
|
+
process.exitCode = loaded.reexecStatus;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (loaded.module) {
|
|
171
|
+
await loaded.module.runLaneSessionStart();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await runMainWithInstallTreeRecovery();
|
|
175
|
+
}
|
|
159
176
|
/**
|
|
160
177
|
* Whether the install follow-up would read the registry or spawn the mesh
|
|
161
178
|
* daemon. False for fast routes' callers, and for commands that are not run
|
|
@@ -206,6 +223,10 @@ function dispatchCliRoutes() {
|
|
|
206
223
|
const helpRequest = !versionRequest && isCommandHelpRequest(process.argv);
|
|
207
224
|
const parsedFastCore = versionRequest || helpRequest ? null : parseFastCore(process.argv);
|
|
208
225
|
const monitorRequest = versionRequest || helpRequest ? null : parseMonitorCheckFastRequest(process.argv);
|
|
226
|
+
if (!versionRequest && !helpRequest && !monitorRequest && isLaneSessionStartFastRequest(process.argv)) {
|
|
227
|
+
void runLaneSessionStartFastWithRecovery();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
209
230
|
const secretsRequest = versionRequest || helpRequest || monitorRequest || parsedFastCore
|
|
210
231
|
? null
|
|
211
232
|
: parseSecretsEnvFastRequest(process.argv);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free argv matcher for the `hq lanes session-start` fast route.
|
|
3
|
+
* Imported statically by src/index.ts, so it must not import anything.
|
|
4
|
+
*/
|
|
5
|
+
/** Exactly `hq lanes session-start` with no options (the SessionStart hook form). */
|
|
6
|
+
export declare function isLaneSessionStartFastRequest(argv: readonly string[]): boolean;
|
|
7
|
+
//# sourceMappingURL=lanes-session-start-argv.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free argv matcher for the `hq lanes session-start` fast route.
|
|
3
|
+
* Imported statically by src/index.ts, so it must not import anything.
|
|
4
|
+
*/
|
|
5
|
+
/** Exactly `hq lanes session-start` with no options (the SessionStart hook form). */
|
|
6
|
+
export function isLaneSessionStartFastRequest(argv) {
|
|
7
|
+
return argv.length === 4 && argv[2] === "lanes" && argv[3] === "session-start";
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=lanes-session-start-argv.js.map
|
package/dist/lib/bot/log.d.ts
CHANGED
|
@@ -6,8 +6,9 @@ export declare function ensureBotLog(dir: string): string;
|
|
|
6
6
|
export declare function rotateBotLogIfNeeded(dir: string, maxBytes?: number): void;
|
|
7
7
|
/** Strip anything that looks like a bearer token, secret, or op:// ref. */
|
|
8
8
|
export declare function redactForLog(message: string): string;
|
|
9
|
-
export
|
|
9
|
+
export type BotLogLevel = "debug" | "info" | "warn" | "error";
|
|
10
|
+
export declare function botLogLine(level: BotLogLevel, message: string, now?: () => Date): string;
|
|
10
11
|
export declare function appendBotLog(dir: string, line: string): void;
|
|
11
|
-
export type BotLogger = (level:
|
|
12
|
+
export type BotLogger = (level: BotLogLevel, message: string) => void;
|
|
12
13
|
export declare function createBotLogger(dir: string, now?: () => Date): BotLogger;
|
|
13
14
|
//# sourceMappingURL=log.d.ts.map
|
|
@@ -54,7 +54,23 @@ export interface ProgressPosterOptions {
|
|
|
54
54
|
* it, anything else (another message, the end of the turn) keeps it.
|
|
55
55
|
*/
|
|
56
56
|
dropPreToolMessages?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Called with each message dropped because a tool call followed it (see
|
|
59
|
+
* `dropPreToolMessages` and `isPreToolAside`), so the bot log can record it.
|
|
60
|
+
*/
|
|
61
|
+
onDropped?: (text: string) => void;
|
|
57
62
|
}
|
|
63
|
+
/** Below this many characters a message right before a tool call is an aside, not a progress note. */
|
|
64
|
+
export declare const PRE_TOOL_ASIDE_MAX_CHARS = 40;
|
|
65
|
+
/**
|
|
66
|
+
* True for a short assistant text that is an aside to itself when a tool call
|
|
67
|
+
* follows it ("Bind first, separately." reached a person on 2026-10-13: the
|
|
68
|
+
* model thinking out loud before a tool_use in the same assistant message).
|
|
69
|
+
* Real progress notes are full sentences addressed to the person ("Reading
|
|
70
|
+
* your agency docs now, then I'll look at the client folders.") and run past
|
|
71
|
+
* the limit; an hq-block is always content the desktop app must draw.
|
|
72
|
+
*/
|
|
73
|
+
export declare function isPreToolAside(text: string): boolean;
|
|
58
74
|
/** Whitespace-insensitive form used to recognise repeated text. */
|
|
59
75
|
export declare function normalizeForCompare(text: string): string;
|
|
60
76
|
/** Matches {"action":"no_reply"} anywhere in a model reply, with or without preamble text. */
|
|
@@ -71,7 +87,13 @@ export declare class ProgressPoster {
|
|
|
71
87
|
private readonly workingNoticeText;
|
|
72
88
|
private readonly buffer;
|
|
73
89
|
private readonly dropPreTool;
|
|
74
|
-
|
|
90
|
+
private readonly onDropped;
|
|
91
|
+
/**
|
|
92
|
+
* Messages waiting to learn whether a tool call follows: every message when
|
|
93
|
+
* dropPreToolMessages is set, short asides (isPreToolAside) otherwise. The
|
|
94
|
+
* runtime streams one content block per event, so "followed by a tool_use
|
|
95
|
+
* in the same assistant message" is "the next event is a tool call".
|
|
96
|
+
*/
|
|
75
97
|
private candidates;
|
|
76
98
|
private pending;
|
|
77
99
|
/** In-between questions, held until the turn ends (see the header). */
|
package/dist/lib/bot/progress.js
CHANGED
|
@@ -27,6 +27,20 @@ export const PROGRESS_COALESCE_MS = 2_000;
|
|
|
27
27
|
export const PROGRESS_MIN_GAP_MS = 4_000;
|
|
28
28
|
export const WORKING_NOTICE_MS = 60_000;
|
|
29
29
|
export const WORKING_NOTICE_TEXT = "Working on it — I'll reply here when it's done.";
|
|
30
|
+
/** Below this many characters a message right before a tool call is an aside, not a progress note. */
|
|
31
|
+
export const PRE_TOOL_ASIDE_MAX_CHARS = 40;
|
|
32
|
+
/**
|
|
33
|
+
* True for a short assistant text that is an aside to itself when a tool call
|
|
34
|
+
* follows it ("Bind first, separately." reached a person on 2026-10-13: the
|
|
35
|
+
* model thinking out loud before a tool_use in the same assistant message).
|
|
36
|
+
* Real progress notes are full sentences addressed to the person ("Reading
|
|
37
|
+
* your agency docs now, then I'll look at the client folders.") and run past
|
|
38
|
+
* the limit; an hq-block is always content the desktop app must draw.
|
|
39
|
+
*/
|
|
40
|
+
export function isPreToolAside(text) {
|
|
41
|
+
const t = text.trim();
|
|
42
|
+
return t.length < PRE_TOOL_ASIDE_MAX_CHARS && !t.includes("```hq-block");
|
|
43
|
+
}
|
|
30
44
|
/** Whitespace-insensitive form used to recognise repeated text. */
|
|
31
45
|
export function normalizeForCompare(text) {
|
|
32
46
|
return text.replace(/\s+/g, "");
|
|
@@ -47,7 +61,13 @@ export class ProgressPoster {
|
|
|
47
61
|
workingNoticeText;
|
|
48
62
|
buffer;
|
|
49
63
|
dropPreTool;
|
|
50
|
-
|
|
64
|
+
onDropped;
|
|
65
|
+
/**
|
|
66
|
+
* Messages waiting to learn whether a tool call follows: every message when
|
|
67
|
+
* dropPreToolMessages is set, short asides (isPreToolAside) otherwise. The
|
|
68
|
+
* runtime streams one content block per event, so "followed by a tool_use
|
|
69
|
+
* in the same assistant message" is "the next event is a tool call".
|
|
70
|
+
*/
|
|
51
71
|
candidates = [];
|
|
52
72
|
pending = [];
|
|
53
73
|
/** In-between questions, held until the turn ends (see the header). */
|
|
@@ -82,6 +102,7 @@ export class ProgressPoster {
|
|
|
82
102
|
this.workingNoticeText = opts.workingNoticeText ?? WORKING_NOTICE_TEXT;
|
|
83
103
|
this.buffer = opts.buffer ?? false;
|
|
84
104
|
this.dropPreTool = opts.dropPreToolMessages ?? false;
|
|
105
|
+
this.onDropped = opts.onDropped ?? (() => undefined);
|
|
85
106
|
}
|
|
86
107
|
/** Arm the one-time "working on it" note. Call when the turn starts. */
|
|
87
108
|
start() {
|
|
@@ -93,6 +114,12 @@ export class ProgressPoster {
|
|
|
93
114
|
this.noticeTimer = null;
|
|
94
115
|
if (this.finished || this.said.length > 0 || this.noticeSent)
|
|
95
116
|
return;
|
|
117
|
+
// A short message is waiting to learn whether a tool call follows: if it
|
|
118
|
+
// was real, it is the model's own "working on it"; look again later.
|
|
119
|
+
if (!this.dropPreTool && this.candidates.length > 0) {
|
|
120
|
+
this.start();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
96
123
|
this.noticeSent = true;
|
|
97
124
|
this.enqueueWith(this.workingNoticeText, this.sendProgress);
|
|
98
125
|
}, this.workingNoticeMs);
|
|
@@ -106,9 +133,9 @@ export class ProgressPoster {
|
|
|
106
133
|
// silence signal, not content, and must never be posted as progress.
|
|
107
134
|
if (containsNoReplySentinel(t))
|
|
108
135
|
return;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
136
|
+
// The previous message was not followed by a tool call: it was real.
|
|
137
|
+
this.promoteCandidates();
|
|
138
|
+
if (this.dropPreTool || isPreToolAside(t)) {
|
|
112
139
|
this.candidates.push(t);
|
|
113
140
|
return;
|
|
114
141
|
}
|
|
@@ -116,9 +143,18 @@ export class ProgressPoster {
|
|
|
116
143
|
}
|
|
117
144
|
/** The model called a tool: whatever it wrote just before was a note to itself. */
|
|
118
145
|
tool() {
|
|
119
|
-
if (this.finished
|
|
146
|
+
if (this.finished)
|
|
120
147
|
return;
|
|
148
|
+
const dropped = this.candidates;
|
|
121
149
|
this.candidates = [];
|
|
150
|
+
for (const t of dropped) {
|
|
151
|
+
try {
|
|
152
|
+
this.onDropped(t);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
/* a log hook must never break the turn */
|
|
156
|
+
}
|
|
157
|
+
}
|
|
122
158
|
}
|
|
123
159
|
promoteCandidates() {
|
|
124
160
|
const kept = this.candidates;
|
|
@@ -199,6 +235,8 @@ export class ProgressPoster {
|
|
|
199
235
|
async abandon() {
|
|
200
236
|
if (this.finished)
|
|
201
237
|
return this.chain;
|
|
238
|
+
// A message the turn ended on (no tool after it) is real.
|
|
239
|
+
this.promoteCandidates();
|
|
202
240
|
this.finished = true;
|
|
203
241
|
this.cancelNotice();
|
|
204
242
|
this.cancelFlush();
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -1118,6 +1118,8 @@ export async function runBot(deps) {
|
|
|
1118
1118
|
// reach them, and it posts no "still working" notice either — the
|
|
1119
1119
|
// desktop app shows that the bot is working.
|
|
1120
1120
|
...(isSetupBot ? { dropPreToolMessages: true } : {}),
|
|
1121
|
+
// Short asides before a tool call never reach the person either (isPreToolAside).
|
|
1122
|
+
onDropped: (text) => log("debug", `dropped a pre-tool aside for ${messageId} (${text.length} chars): ${text}`),
|
|
1121
1123
|
...deps.progress,
|
|
1122
1124
|
...(args.workingNotice === false || silenceable || isSetupBot ? { workingNoticeMs: 0 } : {}),
|
|
1123
1125
|
...(silenceable || args.suppressOutput ? { buffer: true } : {}),
|
|
@@ -27,6 +27,18 @@ export declare const FALLBACK_CLAUDE_PERMISSION_MODE = "auto";
|
|
|
27
27
|
export declare const CLAUDE_EFFORT_ENV = "HQ_BOT_CLAUDE_EFFORT";
|
|
28
28
|
export declare const DEFAULT_CLAUDE_EFFORT = "medium";
|
|
29
29
|
export declare function claudeEffort(env?: NodeJS.ProcessEnv): string;
|
|
30
|
+
/**
|
|
31
|
+
* Per-tool-call safety for a bot turn: Claude Code reads these to bound each
|
|
32
|
+
* Bash tool call (its default and the most a call may ask for). On 2026-10-13
|
|
33
|
+
* a bot-authored command ended in `cat > /dev/null`, waited on stdin that
|
|
34
|
+
* never closed, and the turn showed "working" for over 15 minutes. Set only
|
|
35
|
+
* when the environment does not already set them.
|
|
36
|
+
*/
|
|
37
|
+
export declare const CLAUDE_BASH_DEFAULT_TIMEOUT_ENV = "BASH_DEFAULT_TIMEOUT_MS";
|
|
38
|
+
export declare const CLAUDE_BASH_MAX_TIMEOUT_ENV = "BASH_MAX_TIMEOUT_MS";
|
|
39
|
+
export declare const BOT_BASH_TIMEOUT_MS: number;
|
|
40
|
+
/** Claude's `runtimeEnv` layer: the Bash tool timeouts defaulted (pure). */
|
|
41
|
+
export declare function claudeRuntimeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
30
42
|
export declare function claudePermissionMode(env?: NodeJS.ProcessEnv, autoApprove?: boolean, explicit?: string): string;
|
|
31
43
|
/**
|
|
32
44
|
* True when Claude Code refused to start because bypassPermissions is turned
|
|
@@ -34,6 +34,25 @@ export const DEFAULT_CLAUDE_EFFORT = "medium";
|
|
|
34
34
|
export function claudeEffort(env = process.env) {
|
|
35
35
|
return env[CLAUDE_EFFORT_ENV]?.trim() || DEFAULT_CLAUDE_EFFORT;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Per-tool-call safety for a bot turn: Claude Code reads these to bound each
|
|
39
|
+
* Bash tool call (its default and the most a call may ask for). On 2026-10-13
|
|
40
|
+
* a bot-authored command ended in `cat > /dev/null`, waited on stdin that
|
|
41
|
+
* never closed, and the turn showed "working" for over 15 minutes. Set only
|
|
42
|
+
* when the environment does not already set them.
|
|
43
|
+
*/
|
|
44
|
+
export const CLAUDE_BASH_DEFAULT_TIMEOUT_ENV = "BASH_DEFAULT_TIMEOUT_MS";
|
|
45
|
+
export const CLAUDE_BASH_MAX_TIMEOUT_ENV = "BASH_MAX_TIMEOUT_MS";
|
|
46
|
+
export const BOT_BASH_TIMEOUT_MS = 10 * 60 * 1000;
|
|
47
|
+
/** Claude's `runtimeEnv` layer: the Bash tool timeouts defaulted (pure). */
|
|
48
|
+
export function claudeRuntimeEnv(env) {
|
|
49
|
+
const out = { ...env };
|
|
50
|
+
for (const key of [CLAUDE_BASH_DEFAULT_TIMEOUT_ENV, CLAUDE_BASH_MAX_TIMEOUT_ENV]) {
|
|
51
|
+
if (!out[key]?.trim())
|
|
52
|
+
out[key] = String(BOT_BASH_TIMEOUT_MS);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
37
56
|
export function claudePermissionMode(env = process.env, autoApprove = true, explicit) {
|
|
38
57
|
const raw = env[CLAUDE_PERMISSION_MODE_ENV]?.trim();
|
|
39
58
|
if (raw)
|
|
@@ -193,6 +212,7 @@ export function createClaudeRuntime(env = process.env, opts = {}) {
|
|
|
193
212
|
args.push("--session-id", scratch.newSessionId);
|
|
194
213
|
return args;
|
|
195
214
|
},
|
|
215
|
+
runtimeEnv: claudeRuntimeEnv,
|
|
196
216
|
stdinInput(input) {
|
|
197
217
|
return input.platform === "win32"
|
|
198
218
|
? JSON.stringify({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BotRuntimeId } from "../config.js";
|
|
2
|
-
import { type BotRuntime, type RuntimeTurnInput, type RuntimeTurnResult, type SpawnLike } from "./types.js";
|
|
2
|
+
import { type BotRuntime, type RuntimeTurnInput, type RuntimeTurnResult, type SpawnLike, type SpawnOptions } from "./types.js";
|
|
3
3
|
export * from "./types.js";
|
|
4
4
|
export { createClaudeRuntime, claudePermissionMode } from "./claude.js";
|
|
5
5
|
export { createCodexRuntime, codexPrompt } from "./codex.js";
|
|
@@ -19,6 +19,15 @@ export declare function runtimeFor(id: BotRuntimeId, env?: NodeJS.ProcessEnv): B
|
|
|
19
19
|
export declare function descendantPids(pid: number, listProcesses?: () => string): number[];
|
|
20
20
|
/** After a stop, wait this long for the output pipes to close before settling anyway. */
|
|
21
21
|
export declare const STOP_SETTLE_GRACE_MS = 2000;
|
|
22
|
+
/**
|
|
23
|
+
* The child's stdin: a pipe only when this process has something to write to
|
|
24
|
+
* it (a stdin prompt, which is ended right after it is written, or a
|
|
25
|
+
* protocol), otherwise closed (`ignore` = /dev/null). The prompt normally
|
|
26
|
+
* travels on argv, so the model CLI must never inherit or wait on an open
|
|
27
|
+
* stdin: a bot-authored `cat > /dev/null` on 2026-10-13 blocked a turn for
|
|
28
|
+
* over 15 minutes.
|
|
29
|
+
*/
|
|
30
|
+
export declare function runtimeStdinMode(options: Pick<SpawnOptions, "stdin" | "protocol">): "pipe" | "ignore";
|
|
22
31
|
export declare const defaultSpawn: SpawnLike;
|
|
23
32
|
/** Time limit for the short helper commands a turn may run (`--version`, the bundled model catalog). */
|
|
24
33
|
export declare const AUX_COMMAND_TIMEOUT_MS = 20000;
|
|
@@ -71,6 +71,17 @@ function defaultListProcesses() {
|
|
|
71
71
|
}
|
|
72
72
|
/** After a stop, wait this long for the output pipes to close before settling anyway. */
|
|
73
73
|
export const STOP_SETTLE_GRACE_MS = 2_000;
|
|
74
|
+
/**
|
|
75
|
+
* The child's stdin: a pipe only when this process has something to write to
|
|
76
|
+
* it (a stdin prompt, which is ended right after it is written, or a
|
|
77
|
+
* protocol), otherwise closed (`ignore` = /dev/null). The prompt normally
|
|
78
|
+
* travels on argv, so the model CLI must never inherit or wait on an open
|
|
79
|
+
* stdin: a bot-authored `cat > /dev/null` on 2026-10-13 blocked a turn for
|
|
80
|
+
* over 15 minutes.
|
|
81
|
+
*/
|
|
82
|
+
export function runtimeStdinMode(options) {
|
|
83
|
+
return options.stdin !== undefined || options.protocol ? "pipe" : "ignore";
|
|
84
|
+
}
|
|
74
85
|
export const defaultSpawn = (command, args, options) => new Promise((resolve, reject) => {
|
|
75
86
|
let stdout = "";
|
|
76
87
|
let stderr = "";
|
|
@@ -80,7 +91,7 @@ export const defaultSpawn = (command, args, options) => new Promise((resolve, re
|
|
|
80
91
|
const child = spawn(plan.cmd, plan.args, {
|
|
81
92
|
cwd: options.cwd,
|
|
82
93
|
env: options.env,
|
|
83
|
-
stdio: [options
|
|
94
|
+
stdio: [runtimeStdinMode(options), "pipe", "pipe"],
|
|
84
95
|
...(plan.shell ? { shell: plan.shell } : {}),
|
|
85
96
|
...(plan.windowsVerbatimArguments !== undefined
|
|
86
97
|
? { windowsVerbatimArguments: plan.windowsVerbatimArguments }
|
|
@@ -347,7 +358,8 @@ export async function runRuntimeTurn(runtime, input, deps = {}) {
|
|
|
347
358
|
const args = runtime.buildArgs(remembered ? { ...turnInput, model: remembered.to } : turnInput, { outputFile, newSessionId });
|
|
348
359
|
// Enforce at the model boundary too: old plists and direct `bot run` callers
|
|
349
360
|
// may lack the guard. A bot prompt must never quit the user's desktop app.
|
|
350
|
-
const
|
|
361
|
+
const baseEnv = { ...(input.env ?? deps.env ?? process.env), HQ_NO_ENSURE_HQ_DESKTOP: "1" };
|
|
362
|
+
const env = { ...(runtime.runtimeEnv?.(baseEnv) ?? baseEnv), HQ_NO_ENSURE_HQ_DESKTOP: "1" };
|
|
351
363
|
const binary = runtime.resolveBinary?.(env, input.cwd, turnInput.platform) ?? runtime.binary;
|
|
352
364
|
const stdin = runtime.stdinInput?.(turnInput);
|
|
353
365
|
const protocol = runtime.spawnProtocol?.(turnInput);
|
|
@@ -146,6 +146,14 @@ export interface BotRuntime {
|
|
|
146
146
|
resolveBinary?(env: NodeJS.ProcessEnv, cwd?: string, platform?: NodeJS.Platform): string;
|
|
147
147
|
/** Optional full prompt stream sent through stdin instead of argv. */
|
|
148
148
|
stdinInput?(input: RuntimeTurnInput): string | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* Optional: runtime-specific defaults layered onto the turn's environment
|
|
151
|
+
* (per-tool safety defaults the CLI honours, for example). Applied in
|
|
152
|
+
* `runRuntimeTurn` on top of the env that `turnEnv` in run.ts built from
|
|
153
|
+
* the bot's identity rules; it only adds runtime keys and never changes
|
|
154
|
+
* who the turn acts as. Pure; must not mutate `env`.
|
|
155
|
+
*/
|
|
156
|
+
runtimeEnv?(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
149
157
|
/** Optional bidirectional process protocol for this runtime. */
|
|
150
158
|
spawnProtocol?(input: RuntimeTurnInput): SpawnProtocol | undefined;
|
|
151
159
|
/** Build the argv for one turn. Pure. */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
|
|
2
|
+
export declare const PS1_SHIM_FAMILY_ID = "cli-ps1-shim";
|
|
3
|
+
export declare const PS1_SHIM_CHECK_ID = "cli-ps1-shim.blocked-by-policy";
|
|
4
|
+
export interface Ps1ShimCheckDeps {
|
|
5
|
+
platform: NodeJS.Platform;
|
|
6
|
+
/** The npm prefix holding `hq.cmd`, or null when hq is not an npm global install. */
|
|
7
|
+
binDir: () => string | null;
|
|
8
|
+
/** The npm-generated hq-cli `.ps1` launchers present in the prefix. */
|
|
9
|
+
findShims: (dir: string) => string[];
|
|
10
|
+
/** The effective PowerShell execution policy, or null when it could not be read. */
|
|
11
|
+
readPolicy: () => string | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Windows: npm writes `hq.ps1` next to `hq.cmd`, and PowerShell prefers the
|
|
15
|
+
* `.ps1`. Under a policy that blocks unsigned scripts (the Windows client
|
|
16
|
+
* default) `hq` then fails with UnauthorizedAccess. This check only reads:
|
|
17
|
+
* it never changes the execution policy.
|
|
18
|
+
*/
|
|
19
|
+
export declare const ps1ShimsFamily: CheckFamily;
|
|
20
|
+
export declare function checkPs1Shims(_context: CheckContext, deps?: Ps1ShimCheckDeps): CheckResult[];
|
|
21
|
+
//# sourceMappingURL=ps1-shims.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { npmShimDir } from "../../windows/user-path.js";
|
|
4
|
+
import { findHqCliPs1Shims, policyBlocksUnsignedScripts, readExecutionPolicy } from "../../windows/ps1-shims.js";
|
|
5
|
+
export const PS1_SHIM_FAMILY_ID = "cli-ps1-shim";
|
|
6
|
+
export const PS1_SHIM_CHECK_ID = "cli-ps1-shim.blocked-by-policy";
|
|
7
|
+
const defaultDeps = {
|
|
8
|
+
platform: process.platform,
|
|
9
|
+
binDir: () => npmShimDir(process.argv[1], fs.existsSync),
|
|
10
|
+
findShims: (dir) => findHqCliPs1Shims(dir),
|
|
11
|
+
readPolicy: () => readExecutionPolicy(),
|
|
12
|
+
};
|
|
13
|
+
const REMEDIATION = "Run `hq.cmd doctor --fix` (or `hq.cmd install --global`) to remove the hq .ps1 launchers. " +
|
|
14
|
+
"Until then, type `hq.cmd` instead of `hq` in PowerShell. Your execution policy is not changed.";
|
|
15
|
+
/**
|
|
16
|
+
* Windows: npm writes `hq.ps1` next to `hq.cmd`, and PowerShell prefers the
|
|
17
|
+
* `.ps1`. Under a policy that blocks unsigned scripts (the Windows client
|
|
18
|
+
* default) `hq` then fails with UnauthorizedAccess. This check only reads:
|
|
19
|
+
* it never changes the execution policy.
|
|
20
|
+
*/
|
|
21
|
+
export const ps1ShimsFamily = {
|
|
22
|
+
id: PS1_SHIM_FAMILY_ID,
|
|
23
|
+
title: "hq PowerShell launchers",
|
|
24
|
+
run: (context) => Promise.resolve(checkPs1Shims(context)),
|
|
25
|
+
};
|
|
26
|
+
export function checkPs1Shims(_context, deps = defaultDeps) {
|
|
27
|
+
const dir = deps.platform === "win32" ? deps.binDir() : null;
|
|
28
|
+
if (!dir) {
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
status: "NA",
|
|
32
|
+
checkId: PS1_SHIM_CHECK_ID,
|
|
33
|
+
message: "Not a Windows npm global install, so there are no hq PowerShell launchers to check.",
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
const shims = deps.findShims(dir);
|
|
38
|
+
if (shims.length === 0) {
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
status: "PASS",
|
|
42
|
+
checkId: PS1_SHIM_CHECK_ID,
|
|
43
|
+
target: dir,
|
|
44
|
+
message: `No hq .ps1 launchers in ${dir}; PowerShell runs hq.cmd.`,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
const names = shims.map((file) => path.win32.basename(file)).join(", ");
|
|
49
|
+
const policy = deps.readPolicy();
|
|
50
|
+
if (policy === null) {
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
status: "UNKNOWN",
|
|
54
|
+
checkId: PS1_SHIM_CHECK_ID,
|
|
55
|
+
target: shims[0],
|
|
56
|
+
message: `Found ${names} in ${dir} but could not read the PowerShell execution policy. If \`hq\` fails with UnauthorizedAccess, use \`hq.cmd\`.`,
|
|
57
|
+
remediation: REMEDIATION,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
if (!policyBlocksUnsignedScripts(policy)) {
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
status: "PASS",
|
|
65
|
+
checkId: PS1_SHIM_CHECK_ID,
|
|
66
|
+
target: shims[0],
|
|
67
|
+
message: `${names} can run under the ${policy} execution policy.`,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
status: "WARN",
|
|
74
|
+
checkId: PS1_SHIM_CHECK_ID,
|
|
75
|
+
target: shims[0],
|
|
76
|
+
message: `PowerShell runs ${names} for \`hq\`, and the ${policy} execution policy blocks it, so \`hq\` fails with UnauthorizedAccess.`,
|
|
77
|
+
details: { policy, shims },
|
|
78
|
+
remediation: REMEDIATION,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=ps1-shims.js.map
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
* execute bit, add a hook id to the gate profiles it is missing from,
|
|
18
18
|
* re-register a script present on disk, or pull a company or personal
|
|
19
19
|
* vault whose local tree exists but whose journal is stale — and NEVER
|
|
20
|
-
* rewrites a hook body or deletes a file
|
|
20
|
+
* rewrites a hook body or deletes a file, with one scoped exception: on
|
|
21
|
+
* Windows, `remove-ps1-shims` deletes npm-generated hq-cli `.ps1`
|
|
22
|
+
* launchers (and only files whose content is that shim) from the npm
|
|
23
|
+
* prefix, outside the HQ tree. Classification is owned by
|
|
21
24
|
* {@link deriveRemediation};
|
|
22
25
|
* a content-drift finding is manual-only and simply never appears in the
|
|
23
26
|
* fixable set.
|
|
@@ -38,6 +41,7 @@ import { type SpawnSyncReturns } from "node:child_process";
|
|
|
38
41
|
import type { CheckResult, DoctorStatus } from "../types.js";
|
|
39
42
|
import { type GateProfile } from "../hook-gate-profiles.js";
|
|
40
43
|
import { type FixClass } from "./remediation.js";
|
|
44
|
+
import { type RemovePs1ShimsResult } from "../../windows/ps1-shims.js";
|
|
41
45
|
import { type RefreshSyncOutcome } from "./refresh-sync-outcome.js";
|
|
42
46
|
/** The tree subtrees whose uncommitted changes block a `--fix` run. */
|
|
43
47
|
export declare const HOOK_CONFIG_DIRS: readonly [".claude", ".codex", ".grok"];
|
|
@@ -109,6 +113,12 @@ export interface ApplyFixesOptions {
|
|
|
109
113
|
from: string | null;
|
|
110
114
|
to: string;
|
|
111
115
|
} | null;
|
|
116
|
+
/**
|
|
117
|
+
* Removal used by the `remove-ps1-shims` class: delete npm's hq-cli `.ps1`
|
|
118
|
+
* launchers from the given npm prefix (Windows only; other files and the
|
|
119
|
+
* execution policy are left alone). Tests inject a stub.
|
|
120
|
+
*/
|
|
121
|
+
removePs1Shims?: (dir: string) => RemovePs1ShimsResult;
|
|
112
122
|
}
|
|
113
123
|
/** One company or personal vault the `refresh-sync` class will pull. */
|
|
114
124
|
export interface RefreshSyncTarget {
|
|
@@ -311,6 +321,8 @@ export declare function defaultRewriteBotNodePath(name: string): {
|
|
|
311
321
|
from: string;
|
|
312
322
|
to: string;
|
|
313
323
|
} | null;
|
|
324
|
+
/** Default `remove-ps1-shims` repair: Windows only, a no-op elsewhere. */
|
|
325
|
+
export declare function defaultRemovePs1Shims(dir: string): RemovePs1ShimsResult;
|
|
314
326
|
/**
|
|
315
327
|
* An hq path a bot may be pointed at unattended: an installed package entry
|
|
316
328
|
* or its stable shim, never a worktree or one-off build that happens to be
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
* execute bit, add a hook id to the gate profiles it is missing from,
|
|
18
18
|
* re-register a script present on disk, or pull a company or personal
|
|
19
19
|
* vault whose local tree exists but whose journal is stale — and NEVER
|
|
20
|
-
* rewrites a hook body or deletes a file
|
|
20
|
+
* rewrites a hook body or deletes a file, with one scoped exception: on
|
|
21
|
+
* Windows, `remove-ps1-shims` deletes npm-generated hq-cli `.ps1`
|
|
22
|
+
* launchers (and only files whose content is that shim) from the npm
|
|
23
|
+
* prefix, outside the HQ tree. Classification is owned by
|
|
21
24
|
* {@link deriveRemediation};
|
|
22
25
|
* a content-drift finding is manual-only and simply never appears in the
|
|
23
26
|
* fixable set.
|
|
@@ -44,6 +47,7 @@ import { parseHookGateProfiles, requiredMissingProfiles, } from "../hook-gate-pr
|
|
|
44
47
|
import { createBackup } from "./backup.js";
|
|
45
48
|
import { removeStrayGateEntries } from "../stray-gate-entries.js";
|
|
46
49
|
import { deriveRemediation } from "./remediation.js";
|
|
50
|
+
import { HQ_CLI_BINS, removeHqCliPs1Shims } from "../../windows/ps1-shims.js";
|
|
47
51
|
import { botDir } from "../../bot/paths.js";
|
|
48
52
|
import { readBotConfig } from "../../bot/config.js";
|
|
49
53
|
import { buildBotDaemonPaths, installedDaemonHqBinary, repairBotDaemonIfStale, rewriteBotDaemonNodePath, } from "../../bot/daemon.js";
|
|
@@ -197,6 +201,7 @@ export async function applyFixes(options) {
|
|
|
197
201
|
};
|
|
198
202
|
const rewriteBotNodePath = options.rewriteBotNodePath ?? defaultRewriteBotNodePath;
|
|
199
203
|
const repairBotHqBinary = options.repairBotHqBinary ?? defaultRepairBotHqBinary;
|
|
204
|
+
const removePs1Shims = options.removePs1Shims ?? defaultRemovePs1Shims;
|
|
200
205
|
// 2. Collect findings, then plan each newly unlocked allowlisted repair.
|
|
201
206
|
// Identity is deliberately the check id + target pair: an unsuccessful
|
|
202
207
|
// repair must not be applied repeatedly in one invocation.
|
|
@@ -238,7 +243,7 @@ export async function applyFixes(options) {
|
|
|
238
243
|
const rem = deriveRemediation(result);
|
|
239
244
|
if (!rem?.autoFixable)
|
|
240
245
|
continue;
|
|
241
|
-
const plan = planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath, repairBotHqBinary, refreshManifest);
|
|
246
|
+
const plan = planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath, repairBotHqBinary, refreshManifest, removePs1Shims);
|
|
242
247
|
if (!plan)
|
|
243
248
|
continue;
|
|
244
249
|
const key = JSON.stringify([plan.checkId, plan.target]);
|
|
@@ -683,8 +688,10 @@ export function uncommittedHookConfigChanges(hqRoot) {
|
|
|
683
688
|
}
|
|
684
689
|
// --- Fix planning --------------------------------------------------------------
|
|
685
690
|
/** Build the concrete plan for one auto-fixable finding, or null if unplannable. */
|
|
686
|
-
function planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath = defaultRewriteBotNodePath, repairBotHqBinary = defaultRepairBotHqBinary, refreshManifest = defaultRefreshManifest) {
|
|
691
|
+
function planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath = defaultRewriteBotNodePath, repairBotHqBinary = defaultRepairBotHqBinary, refreshManifest = defaultRefreshManifest, removePs1Shims = defaultRemovePs1Shims) {
|
|
687
692
|
switch (rem.fixClass) {
|
|
693
|
+
case "remove-ps1-shims":
|
|
694
|
+
return planRemovePs1Shims(result, rem, removePs1Shims);
|
|
688
695
|
case "bot-node-path":
|
|
689
696
|
return planBotNodePath(result, rem, rewriteBotNodePath);
|
|
690
697
|
case "bot-hq-binary":
|
|
@@ -753,6 +760,34 @@ function planBotHqBinary(result, rem, repair) {
|
|
|
753
760
|
postStatus: (results) => results.find((entry) => entry.checkId === result.checkId)?.status ?? null,
|
|
754
761
|
};
|
|
755
762
|
}
|
|
763
|
+
/** Remove npm's hq-cli `.ps1` launchers so PowerShell falls back to `hq.cmd`. */
|
|
764
|
+
function planRemovePs1Shims(result, rem, remove) {
|
|
765
|
+
const dir = rem.fixTarget;
|
|
766
|
+
if (!dir)
|
|
767
|
+
return null;
|
|
768
|
+
let failure;
|
|
769
|
+
return {
|
|
770
|
+
checkId: result.checkId,
|
|
771
|
+
fixClass: "remove-ps1-shims",
|
|
772
|
+
target: dir,
|
|
773
|
+
relpath: "",
|
|
774
|
+
preview: Object.keys(HQ_CLI_BINS).map((bin) => ` - ${path.win32.join(dir, `${bin}.ps1`)}`).join("\n") +
|
|
775
|
+
"\n only npm's hq-cli launchers are removed; hq.cmd stays and the execution policy is not changed",
|
|
776
|
+
summary: "remove hq .ps1 launchers",
|
|
777
|
+
apply: () => {
|
|
778
|
+
const outcome = remove(dir);
|
|
779
|
+
failure = outcome.failed.length > 0 ? outcome.failed.map((entry) => entry.reason).join("; ") : undefined;
|
|
780
|
+
},
|
|
781
|
+
postStatus: (results) => results.find((entry) => entry.checkId === result.checkId)?.status ?? null,
|
|
782
|
+
failureReason: () => failure,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
/** Default `remove-ps1-shims` repair: Windows only, a no-op elsewhere. */
|
|
786
|
+
export function defaultRemovePs1Shims(dir) {
|
|
787
|
+
if (process.platform !== "win32")
|
|
788
|
+
return { removed: [], kept: [], failed: [] };
|
|
789
|
+
return removeHqCliPs1Shims(dir);
|
|
790
|
+
}
|
|
756
791
|
/** Real launchctl for the `bot-hq-binary` reload; a failure is a non-ok result. */
|
|
757
792
|
const doctorLaunchctl = (args) => {
|
|
758
793
|
const r = spawnSync("launchctl", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -35,6 +35,14 @@
|
|
|
35
35
|
* and hashes); it never writes the HQ tree. A company
|
|
36
36
|
* scope whose slug did not resolve locally stays
|
|
37
37
|
* manual-only.
|
|
38
|
+
* - `remove-ps1-shims` — Windows only: delete the npm-generated `hq.ps1` /
|
|
39
|
+
* `hq-auth-refresh.ps1` launchers from the npm prefix
|
|
40
|
+
* when the PowerShell execution policy blocks them.
|
|
41
|
+
* `hq.cmd` stays and PowerShell falls back to it. The
|
|
42
|
+
* removal only touches files whose content is npm's
|
|
43
|
+
* hq-cli shim (see `lib/windows/ps1-shims.ts`); the
|
|
44
|
+
* execution policy is never changed. This is the one
|
|
45
|
+
* scoped exception to the no-delete rule below.
|
|
38
46
|
*
|
|
39
47
|
* EVERYTHING else — content drift between platform copies, a missing script, a
|
|
40
48
|
* missing Codex counterpart, an unquoted `$CLAUDE_PROJECT_DIR`, a stale
|
|
@@ -46,7 +54,7 @@
|
|
|
46
54
|
*/
|
|
47
55
|
import type { CheckResult } from "../types.js";
|
|
48
56
|
/** The allowlisted safe repair classes `--fix` is permitted to apply. */
|
|
49
|
-
export type FixClass = "executable-bit" | "gate-profile" | "remove-stray-gate-entries" | "refresh-sync" | "refresh-manifest" | "bot-node-path" | "bot-hq-binary";
|
|
57
|
+
export type FixClass = "executable-bit" | "gate-profile" | "remove-stray-gate-entries" | "refresh-sync" | "refresh-manifest" | "bot-node-path" | "bot-hq-binary" | "remove-ps1-shims";
|
|
50
58
|
/**
|
|
51
59
|
* A finding's structured remediation. `autoFixable`, `action`, and `command`
|
|
52
60
|
* are the `--json` surface (AC1); `fixTarget`/`fixClass` are the internal handle
|