@akira-tl/forgerelay 0.10.0 → 0.10.2

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 CHANGED
@@ -4,6 +4,23 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.10.2] - 2026-09-06
8
+
9
+ ### Added
10
+
11
+ - Added first-class Windows PowerShell 5.1 (`powershell.exe`) execution for Agent commands, Hooks, pipe/PTY sessions, shell identity guidance, and packaged Windows acceptance while preserving PowerShell 5.1-specific syntax and quoting semantics.
12
+
13
+ ### Fixed
14
+
15
+ - Hardened the Windows ConPTY lifecycle used by PowerShell runtimes: process-tree interruption no longer passes unsupported Windows signals, leaked ConPTY input/output resources are released after exit, and Windows PowerShell text I/O is normalized to UTF-8 so PTY completion, Unicode output, durable output, and child cleanup complete reliably.
16
+
17
+ ## [0.10.1] - 2026-09-05
18
+
19
+ ### Added
20
+
21
+ - Added native PowerShell 7 (`pwsh`) execution for Agent commands, Hooks, and PTY sessions with one versioned Command Shell Runtime, no-profile invocation, PowerShell-native command semantics, and Windows packaged-shim acceptance in cloud CI.
22
+ - Added an exact-release-tag PowerShell guidance Skill seeded into the ForgeRelay config Skills directory without bundling prompt templates into the npm package or overwriting user edits.
23
+
7
24
  ## [0.10.0] - 2026-09-05
8
25
 
9
26
  ### Added
package/README.md CHANGED
@@ -295,8 +295,9 @@ See [Security Model](docs/security.md) for the full boundary and threat model.
295
295
  | --- | --- | --- |
296
296
  | Linux | Supported | Requires Node, npm, Git, and Bash. |
297
297
  | macOS | Supported | Requires Node, npm, Git, and Bash. |
298
- | Windows with Git Bash, WSL, MSYS2, or Cygwin Bash | Supported | Git Bash is the simplest native Windows setup. |
299
- | Windows PowerShell or `cmd.exe` only | Not supported yet | Install Git Bash or use WSL. |
298
+ | Windows with PowerShell 7 (`pwsh`) | Supported | Agent commands and Hooks use one native `pwsh` runtime without loading the user profile. |
299
+ | Windows with Git Bash, WSL, MSYS2, or Cygwin Bash | Supported | Bash remains available as a compatibility path. |
300
+ | Windows PowerShell 5.1 (`powershell.exe`) or `cmd.exe` only | Not supported yet | These runtimes are separate follow-up stages. |
300
301
 
301
302
  You can check the local runtime with:
302
303
 
@@ -48,6 +48,6 @@ PTY 依赖 optional `node-pty`。缺少该依赖时 ForgeRelay 会明确报错
48
48
 
49
49
  ## Platform notes
50
50
 
51
- ForgeRelay shell runtime 需要 Bash。Windows 上原生 PowerShell `cmd.exe` 当前不是该 runtime 的执行 shell;使用 Git Bash、WSL、MSYS2 或 Cygwin Bash,并用 `forgerelay doctor` 检查环境。
51
+ ForgeRelay Windows 上支持 PowerShell 7 (`pwsh`) 作为原生 Command Shell Runtime;非 TTY Agent 命令与 Hooks 使用同一个 `pwsh`,禁止加载用户 profile,PTY 也复用同一 executable。公共工具名仍保留为 `bash` 以兼容 Host contract,实际命令语言以 ForgeRelay 报告的 Command Shell Runtime 为准。Windows PowerShell 5.1 (`powershell.exe`) 与 `cmd.exe` 仍属于后续 runtime 阶段;Git Bash、WSL、MSYS2 或 Cygwin Bash 仍可作为 Bash 兼容路径。用 `forgerelay doctor` 检查当前 shell identity、version 与 executable。
52
52
 
53
53
  Shell 可以作为用户开发任务的一部分修改普通项目文件,但始终受 ForgeRelay core mutation/safety contract 约束。涉及 privileged OS files、credentials、configuration 或外部硬件持久写入时,不要用本指南替代 core authorization/safety 规则。
package/dist/cli/init.js CHANGED
@@ -4,6 +4,7 @@ import { publicEndpointUrl } from "../mcp/oauth/public-url.js";
4
4
  import { expandHomePath } from "../mcp/filesystem/roots.js";
5
5
  import { detectLauncherCommandShell, } from "../runtime/shell/command-shell-runtime.js";
6
6
  import { seedShellInstructionFiles, shellInstructionFamiliesToSeed, } from "../runtime/instructions/shell-instructions.js";
7
+ import { seedPowerShellSkill, } from "../runtime/instructions/powershell-skill.js";
7
8
  import { installManagedLanguageServers, installedManagedLanguageServers, managedLanguageServerOptions, } from "../lsp/runtime/managed-language-servers.js";
8
9
  import { generateInstanceId, generateOwnerToken, loadForgeRelayFiles, resolveSubagentsFlag, writeForgeRelayAuth, writeForgeRelayConfig, } from "../runtime/config/user-config.js";
9
10
  import { classifyClientFacingBaseUrl, compactPublicBaseUrlConfig, hasInsecureLanBaseUrl, isLoopbackBindAddress, normalizePublicBaseUrlsInput, setupBindAddress, SetupCancelledError, textPrompt, validateHttpsProxyBaseUrls, validateLanClientFacingBaseUrls, validatePort, } from "./setup-support.js";
@@ -159,6 +160,7 @@ export async function runInit({ force, version }) {
159
160
  const shellInstructionFamilies = shellInstructionFamiliesToSeed(process.platform, commandShell.family);
160
161
  let shellInstructions = files.config.shellInstructions;
161
162
  let shellInstructionSeedResults = [];
163
+ let powerShellSkillSeedResult;
162
164
  if (shellInstructionFamilies.length > 0) {
163
165
  const shellInstructionsAnswer = await prompts.confirm({
164
166
  message: "Load ForgeRelay-provided extra Instructions for this command shell?",
@@ -177,6 +179,15 @@ export async function runInit({ force, version }) {
177
179
  prompts.note(failedShellInstructions.map((result) => `${result.family}: ${result.error ?? "download failed"}\n${result.sourceUrl}`).join("\n\n"), "Shell Instructions unavailable");
178
180
  }
179
181
  }
182
+ if (commandShell.family === "pwsh") {
183
+ powerShellSkillSeedResult = await seedPowerShellSkill({
184
+ configDir: files.dir,
185
+ version,
186
+ });
187
+ if (powerShellSkillSeedResult.status === "failed") {
188
+ prompts.note(`${powerShellSkillSeedResult.error ?? "download failed"}\n${powerShellSkillSeedResult.sourceUrl}`, "PowerShell Skill unavailable");
189
+ }
190
+ }
180
191
  const installedManaged = installedManagedLanguageServers(files.dir);
181
192
  const selectedManaged = await prompts.multiselect({
182
193
  message: "Which Language Servers should ForgeRelay manage with npm?",
@@ -244,6 +255,9 @@ export async function runInit({ force, version }) {
244
255
  ...shellInstructionSeedResults.map((result) => `Shell Instructions ${result.family}: ${result.status} (${result.path})`),
245
256
  ]
246
257
  : []),
258
+ ...(powerShellSkillSeedResult
259
+ ? [`PowerShell Skill: ${powerShellSkillSeedResult.status} (${powerShellSkillSeedResult.path})`]
260
+ : []),
247
261
  ...clientFacingBaseUrls.map((baseUrl, index) => `${index === 0 ? "Client-facing MCP URL" : "Client-facing MCP alias"}: ${publicEndpointUrl(baseUrl, "mcp").toString()}`),
248
262
  ];
249
263
  if (networkMode === "ssh") {
@@ -14,7 +14,7 @@ const defaultProcessTreeRuntime = {
14
14
  export function resolveShellCommand(command, platform = process.platform, environment = process.env) {
15
15
  return resolveShellCommandForRuntime(command, resolveCompatibilityCommandShellRuntime(platform, environment));
16
16
  }
17
- export function resolveShellCommandForRuntime(command, runtime) {
17
+ export function resolveShellCommandForRuntime(command, runtime, options = {}) {
18
18
  // Agent and Hook commands must not source the user's interactive/login shell
19
19
  // configuration. ForgeRelay already inherits PATH and other environment from
20
20
  // the process that launched the server; re-running zsh/bash as a login shell
@@ -35,12 +35,40 @@ export function resolveShellCommandForRuntime(command, runtime) {
35
35
  return { executable: runtime.executable, args: ["-f", "-c", command] };
36
36
  case "sh":
37
37
  return { executable: runtime.executable, args: ["-c", command] };
38
- case "fish":
39
38
  case "pwsh":
39
+ return {
40
+ executable: runtime.executable,
41
+ args: [
42
+ "-NoLogo",
43
+ "-NoProfile",
44
+ ...(options.interactive ? [] : ["-NonInteractive"]),
45
+ "-Command",
46
+ command,
47
+ ],
48
+ };
40
49
  case "powershell":
50
+ return {
51
+ executable: runtime.executable,
52
+ args: [
53
+ "-NoLogo",
54
+ "-NoProfile",
55
+ ...(options.interactive ? [] : ["-NonInteractive"]),
56
+ "-Command",
57
+ windowsPowerShellUtf8Command(command),
58
+ ],
59
+ };
60
+ case "fish":
41
61
  throw new Error(`Command shell runtime ${runtime.family} is identified but native execution support is not enabled in this release stage. ForgeRelay will not silently execute the command through another shell.`);
42
62
  }
43
63
  }
64
+ function windowsPowerShellUtf8Command(command) {
65
+ return [
66
+ "[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)",
67
+ "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
68
+ "$OutputEncoding = [Console]::OutputEncoding",
69
+ command,
70
+ ].join("; ");
71
+ }
44
72
  export function terminateProcessTree(child, signal, detached, runtime = defaultProcessTreeRuntime) {
45
73
  if (runtime.platform === "win32" && child.pid) {
46
74
  if (runtime.killWindowsTree(child.pid))
@@ -58,3 +86,40 @@ export function terminateProcessTree(child, signal, detached, runtime = defaultP
58
86
  }
59
87
  child.kill(signal);
60
88
  }
89
+ export function releasePtyProcessResources(pty, platform = process.platform) {
90
+ if (platform !== "win32")
91
+ return;
92
+ // node-pty 1.1.0 leaves ConPTY resources referenced after exit: the input
93
+ // PipeWrap is not destroyed (microsoft/node-pty#947), and the system-ConPTY
94
+ // natural-exit path does not dispose its conout worker. Keep both workarounds
95
+ // at the PTY boundary so the shared ProcessManager stays platform-neutral.
96
+ const agent = pty._agent;
97
+ try {
98
+ agent?._conoutSocketWorker?.dispose?.();
99
+ }
100
+ catch {
101
+ // The worker may already be disposing after an explicit PTY kill.
102
+ }
103
+ const inputSocket = agent?.inSocket;
104
+ if (!inputSocket || inputSocket.destroyed || typeof inputSocket.destroy !== "function")
105
+ return;
106
+ try {
107
+ inputSocket.destroy();
108
+ }
109
+ catch {
110
+ // Completion must remain observable even if node-pty already released it.
111
+ }
112
+ }
113
+ export function terminatePtyProcessTree(pty, signal, runtime = defaultProcessTreeRuntime) {
114
+ if (runtime.platform === "win32") {
115
+ runtime.killWindowsTree(pty.pid);
116
+ try {
117
+ pty.kill();
118
+ }
119
+ catch {
120
+ // The PTY may already have completed after tree termination.
121
+ }
122
+ return;
123
+ }
124
+ pty.kill(signal);
125
+ }
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { resolveShellCommandForRuntime, terminateProcessTree } from "./process-platform.js";
2
+ import { releasePtyProcessResources, resolveShellCommandForRuntime, terminateProcessTree, terminatePtyProcessTree } from "./process-platform.js";
3
3
  import { resolveCompatibilityCommandShellRuntime, snapshotCommandShellRuntime } from "../../runtime/shell/command-shell-runtime.js";
4
4
  const DEFAULT_EXEC_YIELD_MS = 10_000;
5
5
  const DEFAULT_INTERACTIVE_YIELD_MS = 250;
@@ -240,7 +240,7 @@ export class ProcessManager {
240
240
  const processEntry = this.createProcess(input);
241
241
  this.processes.set(processEntry.id, processEntry);
242
242
  try {
243
- if (input.tty && process.platform !== "win32")
243
+ if (input.tty)
244
244
  await this.startPty(processEntry, input);
245
245
  else
246
246
  this.startPipe(processEntry, input);
@@ -455,7 +455,6 @@ export class ProcessManager {
455
455
  processEntry.process = {
456
456
  write: (data) => child.stdin.write(data),
457
457
  kill: (signal = "SIGTERM") => terminateProcessTree(child, signal, detached),
458
- resize: input.tty ? () => undefined : undefined,
459
458
  };
460
459
  child.stdout.on("data", (data) => this.append(processEntry, "stdout", data));
461
460
  child.stderr.on("data", (data) => this.append(processEntry, "stderr", data));
@@ -470,31 +469,26 @@ export class ProcessManager {
470
469
  catch {
471
470
  throw new Error("PTY support requires the optional node-pty dependency.");
472
471
  }
473
- const shell = resolveShellCommandForRuntime(input.command, this.commandShellRuntime);
474
- let pty;
475
- try {
476
- pty = nodePty.spawn(shell.executable, shell.args, {
477
- cwd: input.cwd,
478
- env: processEnvironment({
479
- workspaceId: input.workspaceId,
480
- workspaceRoot: input.workspaceRoot,
481
- codexCi: input.codexCi,
482
- }),
483
- name: "xterm-256color",
484
- cols: processEntry.columns,
485
- rows: processEntry.rows,
486
- });
487
- }
488
- catch (error) {
489
- throw error;
490
- }
472
+ const shell = resolveShellCommandForRuntime(input.command, this.commandShellRuntime, { interactive: true });
473
+ const pty = nodePty.spawn(shell.executable, shell.args, {
474
+ cwd: input.cwd,
475
+ env: processEnvironment({
476
+ workspaceId: input.workspaceId,
477
+ workspaceRoot: input.workspaceRoot,
478
+ codexCi: input.codexCi,
479
+ }),
480
+ name: "xterm-256color",
481
+ cols: processEntry.columns,
482
+ rows: processEntry.rows,
483
+ });
491
484
  processEntry.process = {
492
485
  write: (data) => pty.write(data),
493
- kill: (signal) => pty.kill(signal),
486
+ kill: (signal = "SIGTERM") => terminatePtyProcessTree(pty, signal),
494
487
  resize: (columns, rows) => pty.resize(columns, rows),
495
488
  };
496
489
  pty.onData((data) => this.append(processEntry, "pty", data));
497
490
  pty.onExit(({ exitCode, signal }) => {
491
+ releasePtyProcessResources(pty);
498
492
  this.finish(processEntry, exitCode, signal === 0 ? undefined : String(signal));
499
493
  });
500
494
  }
@@ -0,0 +1,65 @@
1
+ import { constants as fsConstants } from "node:fs";
2
+ import { access, mkdir, writeFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ const TEMPLATE_REPOSITORY_RAW = "https://raw.githubusercontent.com/Akira-TL/forgerelay";
5
+ const POWERSHELL_SKILL_PATH = "templates/skills/powershell/SKILL.md";
6
+ export function powerShellSkillPath(configDir) {
7
+ return join(resolve(configDir), "skills", "powershell", "SKILL.md");
8
+ }
9
+ export function powerShellSkillTemplateUrl(version) {
10
+ const normalizedVersion = version.trim().replace(/^v/, "");
11
+ if (!normalizedVersion)
12
+ throw new Error("ForgeRelay version is required to fetch the PowerShell Skill.");
13
+ return `${TEMPLATE_REPOSITORY_RAW}/v${encodeURIComponent(normalizedVersion)}/${POWERSHELL_SKILL_PATH}`;
14
+ }
15
+ export async function seedPowerShellSkill(options) {
16
+ const path = powerShellSkillPath(options.configDir);
17
+ const sourceUrl = powerShellSkillTemplateUrl(options.version);
18
+ if (!options.reset && await fileExists(path)) {
19
+ return { path, status: "preserved", sourceUrl };
20
+ }
21
+ await mkdir(join(resolve(options.configDir), "skills", "powershell"), {
22
+ recursive: true,
23
+ mode: 0o700,
24
+ });
25
+ const fetchText = options.fetchText ?? fetchSkillText;
26
+ try {
27
+ const content = await fetchText(sourceUrl);
28
+ if (!content.trim())
29
+ throw new Error("template response was empty");
30
+ await writeFile(path, content.endsWith("\n") ? content : `${content}\n`, {
31
+ encoding: "utf8",
32
+ mode: 0o600,
33
+ flag: options.reset ? "w" : "wx",
34
+ });
35
+ return { path, status: "created", sourceUrl };
36
+ }
37
+ catch (error) {
38
+ if (!options.reset && await fileExists(path)) {
39
+ return { path, status: "preserved", sourceUrl };
40
+ }
41
+ return {
42
+ path,
43
+ status: "failed",
44
+ sourceUrl,
45
+ error: error instanceof Error ? error.message : String(error),
46
+ };
47
+ }
48
+ }
49
+ async function fetchSkillText(url) {
50
+ const response = await fetch(url, {
51
+ headers: { "user-agent": "forgerelay-powershell-skill" },
52
+ });
53
+ if (!response.ok)
54
+ throw new Error(`HTTP ${response.status} while fetching ${url}`);
55
+ return response.text();
56
+ }
57
+ async function fileExists(path) {
58
+ try {
59
+ await access(path, fsConstants.F_OK);
60
+ return true;
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
@@ -21,23 +21,31 @@ export function resolveCommandShellRuntime(input) {
21
21
  * strict precedence resolver above: new configuration should call
22
22
  * resolveCommandShellRuntime instead of inventing another fallback.
23
23
  */
24
- export function resolveConfiguredCommandShellRuntime(preference, platform = process.platform, environment = process.env) {
24
+ export function resolveConfiguredCommandShellRuntime(preference, platform = process.platform, environment = process.env, metadata = {}) {
25
25
  const legacyExplicit = environment.FORGERELAY_COMMAND_SHELL?.trim();
26
+ let runtime;
26
27
  if (legacyExplicit) {
27
28
  const explicit = normalizeCommandShellSelection({ executable: legacyExplicit }, platform, environment, true);
28
- return resolveCommandShellRuntime({ explicit });
29
+ runtime = resolveCommandShellRuntime({ explicit });
29
30
  }
30
- if (!preference)
31
- return resolveCompatibilityCommandShellRuntime(platform, environment);
32
- if (preference.mode === "pinned") {
31
+ else if (!preference) {
32
+ runtime = resolveCompatibilityCommandShellRuntime(platform, environment);
33
+ }
34
+ else if (preference.mode === "pinned") {
33
35
  const explicit = normalizeCommandShellSelection(preference, platform, environment, true);
34
- return resolveCommandShellRuntime({ explicit });
36
+ runtime = resolveCommandShellRuntime({ explicit });
37
+ }
38
+ else {
39
+ const launcher = detectLauncherCommandShell({ platform, environment });
40
+ if (launcher) {
41
+ runtime = resolveCommandShellRuntime({ launcher });
42
+ }
43
+ else {
44
+ const recordedFallback = normalizeCommandShellSelection(preference, platform, environment, true);
45
+ runtime = resolveCommandShellRuntime({ recordedFallback });
46
+ }
35
47
  }
36
- const launcher = detectLauncherCommandShell({ platform, environment });
37
- if (launcher)
38
- return resolveCommandShellRuntime({ launcher });
39
- const recordedFallback = normalizeCommandShellSelection(preference, platform, environment, true);
40
- return resolveCommandShellRuntime({ recordedFallback });
48
+ return enrichConfiguredCommandShellRuntime(runtime, metadata.probePowerShell7Version ?? probePowerShell7Version, metadata.probeWindowsPowerShellVersion ?? probeWindowsPowerShellVersion);
41
49
  }
42
50
  export function resolveCompatibilityCommandShellRuntime(platform = process.platform, environment = process.env) {
43
51
  if (platform === "win32") {
@@ -121,6 +129,69 @@ export function formatCommandShellRuntime(runtime) {
121
129
  export function snapshotCommandShellRuntime(runtime) {
122
130
  return { ...runtime, capabilities: [...runtime.capabilities] };
123
131
  }
132
+ function enrichConfiguredCommandShellRuntime(runtime, powerShell7VersionProbe, windowsPowerShellVersionProbe) {
133
+ if (runtime.family === "pwsh") {
134
+ const version = powerShell7VersionProbe(runtime.executable).trim();
135
+ const major = Number.parseInt(version.split(".", 1)[0] ?? "", 10);
136
+ if (!version || !Number.isInteger(major)) {
137
+ throw new Error(`Unable to determine PowerShell 7 version from ${runtime.executable}.`);
138
+ }
139
+ if (major < 7) {
140
+ throw new Error(`Configured pwsh runtime must be PowerShell 7 or newer; detected ${version} at ${runtime.executable}.`);
141
+ }
142
+ return {
143
+ ...runtime,
144
+ version,
145
+ capabilities: Array.from(new Set([
146
+ ...runtime.capabilities,
147
+ "profile-isolation",
148
+ "pipeline-chain-operators",
149
+ ])),
150
+ };
151
+ }
152
+ if (runtime.family === "powershell") {
153
+ const version = windowsPowerShellVersionProbe(runtime.executable).trim();
154
+ const [majorText, minorText] = version.split(".", 3);
155
+ const major = Number.parseInt(majorText ?? "", 10);
156
+ const minor = Number.parseInt(minorText ?? "", 10);
157
+ if (!version || !Number.isInteger(major) || !Number.isInteger(minor)) {
158
+ throw new Error(`Unable to determine Windows PowerShell version from ${runtime.executable}.`);
159
+ }
160
+ if (major !== 5 || minor !== 1) {
161
+ throw new Error(`Configured powershell runtime must be Windows PowerShell 5.1; detected ${version} at ${runtime.executable}. ForgeRelay will not substitute pwsh for the selected runtime.`);
162
+ }
163
+ return {
164
+ ...runtime,
165
+ version,
166
+ capabilities: Array.from(new Set([
167
+ ...runtime.capabilities,
168
+ "profile-isolation",
169
+ "powershell-5.1",
170
+ ])),
171
+ };
172
+ }
173
+ return runtime;
174
+ }
175
+ function probePowerShell7Version(executable) {
176
+ return probePowerShellVersion(executable, "PowerShell 7");
177
+ }
178
+ function probeWindowsPowerShellVersion(executable) {
179
+ return probePowerShellVersion(executable, "Windows PowerShell");
180
+ }
181
+ function probePowerShellVersion(executable, label) {
182
+ const result = spawnSync(executable, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "$PSVersionTable.PSVersion.ToString()"], { encoding: "utf8", windowsHide: true, timeout: 5_000 });
183
+ if (result.error) {
184
+ throw new Error(`Unable to query ${label} version from ${executable}: ${result.error.message}`);
185
+ }
186
+ if (result.status !== 0) {
187
+ const detail = result.stderr?.trim();
188
+ throw new Error(`Unable to query ${label} version from ${executable}: exited with code ${result.status ?? "unknown"}${detail ? `: ${detail}` : ""}.`);
189
+ }
190
+ const version = result.stdout?.trim();
191
+ if (!version)
192
+ throw new Error(`Unable to query ${label} version from ${executable}: no version was reported.`);
193
+ return version;
194
+ }
124
195
  export function commandShellAgentInstruction(runtime) {
125
196
  if (runtime.family === "bash")
126
197
  return "";
@@ -130,6 +201,9 @@ export function commandShellAgentInstruction(runtime) {
130
201
  `Executable: ${runtime.executable}.`,
131
202
  `Selection source: ${runtime.source}.`,
132
203
  "Write shell commands for this runtime rather than assuming Bash syntax.",
204
+ ...(runtime.family === "powershell"
205
+ ? ["Windows PowerShell 5.1 does not support PowerShell 7 pipeline-chain operators && or ||."]
206
+ : []),
133
207
  ].join(" ");
134
208
  }
135
209
  function runtimeFromSelection(selection, source) {
package/dist/server.js CHANGED
@@ -124,7 +124,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
124
124
  };
125
125
  };
126
126
  const toolDescriptions = buildToolDescriptions(config);
127
- const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result, (snapshot) => recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId)));
127
+ const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result, (snapshot) => recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId)), config.commandShellRuntime);
128
128
  const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
129
129
  const artifactDownloadAvailable = config.artifactsEnabled && isArtifactDownloadSupportedPlatform();
130
130
  const reviewChangesAvailable = config.widgets === "changes";
@@ -13,7 +13,7 @@ export async function executeSubagentRun(config, input, providerRunner = runSuba
13
13
  if (record.activeRun?.id !== input.runId) {
14
14
  throw new Error(`Subagent Run ${input.runId} is not active for Session ${record.id}.`);
15
15
  }
16
- const hooks = new HookRunner(config.hooks, config.logging);
16
+ const hooks = new HookRunner(config.hooks, config.logging, process.env, undefined, config.commandShellRuntime);
17
17
  const hookInvocation = {
18
18
  workspaceId: record.workspaceId,
19
19
  workspaceRoot: record.workspaceRoot,
package/docs/gotchas.md CHANGED
@@ -162,10 +162,12 @@ ForgeRelay does not intentionally leave the source checkout in a merge-conflict
162
162
 
163
163
  ## Windows shell commands fail
164
164
 
165
- ForgeRelay shell execution requires Bash. Native PowerShell and `cmd.exe` command
166
- execution are not currently supported by the shell runtime.
165
+ ForgeRelay supports PowerShell 7 (`pwsh`) as a native Windows Command Shell Runtime.
166
+ Agent commands and Hooks share that runtime and do not load the user PowerShell profile.
167
+ Windows PowerShell 5.1 (`powershell.exe`) and `cmd.exe` remain separate follow-up runtimes.
168
+ Git Bash, WSL, MSYS2, and Cygwin Bash also remain supported compatibility paths.
167
169
 
168
- Use Git Bash, WSL, MSYS2, or Cygwin Bash, then run:
170
+ Check the selected runtime with:
169
171
 
170
172
  ```bash
171
173
  forgerelay doctor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Local development control plane for MCP coding agents.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Akira-TL/forgerelay#readme",
@@ -42,6 +42,8 @@
42
42
  "debug:accept:relay": "node scripts/debug/relay-accept.mjs",
43
43
  "traffic:audit": "node --import tsx scripts/debug/traffic/traffic-audit.mjs",
44
44
  "lsp:interop": "node scripts/lsp/interop.mjs",
45
+ "pwsh:accept": "node scripts/ci/pwsh-acceptance.mjs",
46
+ "powershell51:accept": "node scripts/ci/powershell51-acceptance.mjs",
45
47
  "wiki:check": "node scripts/wiki/sync.mjs check",
46
48
  "wiki:publish": "node scripts/wiki/sync.mjs publish",
47
49
  "architecture:check": "node scripts/ci/architecture.mjs",
@@ -52,7 +54,7 @@
52
54
  "release:push-ready": "node scripts/release/push-ready.mjs",
53
55
  "postinstall": "node scripts/fix-node-pty-permissions.mjs",
54
56
  "start": "node dist/cli.js serve",
55
- "test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/workspaces/relay/tests/recovery.test.ts && tsx src/workspaces/relay/tests/checkpoint.test.ts && tsx src/runtime/config/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/runtime/state/runtime-lease.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/git/worktree-recovery.test.ts && tsx src/mcp/server/workspace/workspace-inventory.test.ts && tsx src/mcp/server/workspace/workspace-recovery.test.ts && tsx src/mcp/server/workspace/workspace-checkpoint.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/maintenance.test.ts && tsx src/cli/maintenance-prune.test.ts && tsx src/cli/cli.test.ts",
57
+ "test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/push-ready.test.mjs scripts/release/release-version.test.mjs && tsx src/mcp/oauth/router.test.ts && tsx src/workspaces/relay/auth/remote-auth-cli.test.ts && tsx src/workspaces/relay/auth/remote-ssh-auth-cli.test.ts && tsx src/workspaces/relay/tests/lifecycle.test.ts && tsx src/workspaces/relay/tests/routing.test.ts && tsx src/workspaces/relay/tests/ssh.test.ts && tsx src/workspaces/relay/tests/process.test.ts && tsx src/workspaces/relay/tests/recovery.test.ts && tsx src/workspaces/relay/tests/checkpoint.test.ts && tsx src/runtime/config/config.test.ts && tsx src/runtime/instructions/powershell-skill.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/runtime/managed-language-servers.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/runtime/logging/logger.test.ts && tsx src/runtime/logging/proxy-trust.test.ts && tsx src/runtime/state/lock/file-lock.test.ts && tsx src/runtime/state/runtime-lease.test.ts && tsx src/mcp/panel/mcp-app-template.test.ts && tsx src/mcp/hooks/hooks.test.ts && tsx src/mcp/server/core/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/mcp/request-meta.test.ts && tsx src/mcp/artifacts/incoming-artifacts.test.ts && tsx src/mcp/artifacts/artifact-download.test.ts && tsx src/ui/core/card-types.test.ts && tsx src/ui/activity/model.test.ts && tsx src/ui/activity/detail-card.test.ts && tsx src/ui/review/patch-display.test.ts && tsx src/ui/core/tool-display.test.ts && tsx src/mcp/filesystem/apply-patch.test.ts && tsx src/mcp/process/process-platform.test.ts && tsx src/mcp/process/process-sessions.test.ts && tsx src/mcp/server/transport/mcp-sessions.test.ts && tsx src/mcp/server/transport/server-shutdown.test.ts && tsx src/mcp/server/operations/mutation-diagnostics.test.ts && tsx src/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/adapters/pi.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/mcp/filesystem/roots.test.ts && tsx src/mcp/filesystem/file-mutations.test.ts && tsx src/mcp/operations/edit-preflight.test.ts && tsx src/workspaces/resources/skills.test.ts && tsx src/runtime/state/db/migrations.test.ts && tsx src/workspaces/state/workspace-store.test.ts && tsx src/workspaces/tasks/workspace-tasks.test.ts && tsx src/workspaces/tasks/workspace-task-reminders.test.ts && tsx src/activity/history/audit-store.test.ts && tsx src/activity/history/bash-output-store.test.ts && tsx src/activity/runtime/lifecycle.test.ts && tsx src/activity/history/query-service.test.ts && tsx src/mcp/operations/core-operation-executor.test.ts && tsx src/mcp/operations/bulk-mutation.test.ts && tsx src/mcp/operations/batch/scheduler.test.ts && tsx src/mcp/operations/batch/executor-policy.test.ts && tsx src/workspaces.test.ts && tsx src/workspaces/conversation-checkout.test.ts && tsx src/workspaces/conversation-worktree.test.ts && tsx src/workspaces/git/worktree-recovery.test.ts && tsx src/mcp/server/workspace/workspace-inventory.test.ts && tsx src/mcp/server/workspace/workspace-recovery.test.ts && tsx src/mcp/server/workspace/workspace-checkpoint.test.ts && tsx src/workspaces/review/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && npm run build:app && tsx src/mcp/process/server.test.ts && tsx src/mcp/panel/server.test.ts && tsx src/mcp/server/server.test.ts && tsx src/mcp/oauth/oauth-store.test.ts && tsx src/cli/maintenance.test.ts && tsx src/cli/maintenance-prune.test.ts && tsx src/cli/cli.test.ts",
56
58
  "typecheck": "tsc -p tsconfig.json --noEmit",
57
59
  "release:check": "node scripts/release-version.mjs check",
58
60
  "release:tag-check": "node scripts/release-version.mjs tag",