specrails-desktop 2.11.8 → 2.11.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.11.8",
3
+ "version": "2.11.9",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -157,6 +157,16 @@ function windowsGlobalBinDirs() {
157
157
  dirs.push(path_1.default.join(userProfile, '.local', 'bin'));
158
158
  dirs.push(path_1.default.join(userProfile, 'scoop', 'shims'));
159
159
  }
160
+ // Windows system dirs. A GUI-launched / pkg-stripped sidecar can inherit a PATH
161
+ // missing %SystemRoot%\System32 — which holds `taskkill`/`where`, and \Wbem
162
+ // holds `wmic`. Without them, cmd.exe-mediated tools fail: `tree-kill`'s
163
+ // `taskkill` silently no-ops (a cancelled rail keeps running). Existence-gated
164
+ // by the callers; harmless when already present (the common case).
165
+ const systemRoot = process.env.SystemRoot || process.env.windir;
166
+ if (systemRoot) {
167
+ dirs.push(path_1.default.join(systemRoot, 'System32'));
168
+ dirs.push(path_1.default.join(systemRoot, 'System32', 'Wbem'));
169
+ }
160
170
  return dirs;
161
171
  }
162
172
  /**
@@ -115,6 +115,11 @@ function registerJobsRoutes(deps) {
115
115
  res.json({ ok: true, status: 'deleted' });
116
116
  }
117
117
  else {
118
+ // Surface the real error (name/message/stack) so an unexpected cancel
119
+ // failure — e.g. a Windows kill throw — is identifiable instead of being
120
+ // masked as a bare "Internal server error".
121
+ const e = err;
122
+ console.error(`[jobs] cancel ${req.params.id} failed: ${e?.name}: ${e?.message}\n${e?.stack ?? ''}`);
118
123
  res.status(500).json({ error: 'Internal server error' });
119
124
  }
120
125
  }
@@ -10,7 +10,7 @@ const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const readline_1 = require("readline");
12
12
  const ids_1 = require("./ids");
13
- const tree_kill_1 = __importDefault(require("tree-kill"));
13
+ const win_spawn_1 = require("./util/win-spawn");
14
14
  const types_1 = require("./types");
15
15
  const command_resolver_1 = require("./command-resolver");
16
16
  const cli_prompt_1 = require("./util/cli-prompt");
@@ -283,12 +283,12 @@ class QueueManager {
283
283
  if (proc && proc.pid) {
284
284
  const pid = proc.pid;
285
285
  try {
286
- (0, tree_kill_1.default)(pid, 'SIGTERM');
286
+ (0, win_spawn_1.treeKillSafe)(pid, 'SIGTERM', () => { });
287
287
  }
288
288
  catch { /* best-effort */ }
289
289
  const grace = setTimeout(() => {
290
290
  try {
291
- (0, tree_kill_1.default)(pid, 'SIGKILL', () => { });
291
+ (0, win_spawn_1.treeKillSafe)(pid, 'SIGKILL', () => { });
292
292
  }
293
293
  catch { /* best-effort */ }
294
294
  }, 5000);
@@ -993,6 +993,33 @@ class QueueManager {
993
993
  // Build supplementary context (output chaining + headless mode) that goes
994
994
  // into --append-system-prompt, keeping the user prompt clean.
995
995
  let systemAppend = '';
996
+ // Repository orientation (relocated projects only). The rail spawns from the
997
+ // WORKSPACE cwd, which holds only `.specrails/` config — NOT the source code.
998
+ // The repo is reachable via `--add-dir <repoDir>` (injected below) and the
999
+ // `./project` link. The framework templates point at `${SPECRAILS_REPO_DIR:-.}`,
1000
+ // but the agent's Read/Grep/Glob/Edit tools do NOT expand that shell-var form
1001
+ // (and PowerShell/cmd.exe don't expand POSIX `${VAR:-default}` either) — so on
1002
+ // Windows the agent reads a bogus literal path, falls back to the empty
1003
+ // workspace cwd, finds only framework files, and hallucinates a wrong/"global"
1004
+ // project. Tell it the concrete absolute repo path explicitly. Mirrors the
1005
+ // Explore-cwd orientation. Legacy (non-relocated) ⇒ cwd IS the repo ⇒ skipped
1006
+ // (byte-identical). The `\${` keeps the shell-var form literal in this string.
1007
+ if (execution.relocated && execution.repoDir) {
1008
+ systemAppend +=
1009
+ `REPOSITORY LOCATION — READ THIS FIRST:\n` +
1010
+ `This pipeline runs from a workspace directory that contains ONLY specrails ` +
1011
+ `configuration (.specrails/, agent definitions) — NOT your project's source code. ` +
1012
+ `Your project's source repository is at this ABSOLUTE path:\n` +
1013
+ ` ${execution.repoDir}\n` +
1014
+ `It is also exposed to your tools as an additional working directory (via --add-dir) ` +
1015
+ `and mounted in this cwd as ./project. Use the absolute repo path above (or ./project) ` +
1016
+ `for ALL source reads, edits, greps and globs. Your Read/Grep/Glob/Edit tools do NOT ` +
1017
+ `expand shell variables — NEVER pass a literal "\${SPECRAILS_REPO_DIR:-.}" or ` +
1018
+ `"\${SPECRAILS_REPO_DIR}" as a path; substitute the absolute path above instead. ` +
1019
+ `The spec/ticket store (.specrails/local-tickets.json) lives in THIS workspace cwd. ` +
1020
+ `Do NOT look for source files under this cwd — they exist only under the repository ` +
1021
+ `path above.\n\n`;
1022
+ }
996
1023
  // Output chaining: inject previous step's output as context for dependent jobs
997
1024
  if (job.dependsOnJobId) {
998
1025
  const parentJob = this._jobs.get(job.dependsOnJobId);
@@ -1788,10 +1815,22 @@ class QueueManager {
1788
1815
  clearTimeout(this._killTimer);
1789
1816
  this._killTimer = null;
1790
1817
  }
1791
- (0, tree_kill_1.default)(this._activeProcess.pid, 'SIGTERM');
1792
1818
  const pid = this._activeProcess.pid;
1819
+ // treeKillSafe never throws synchronously, but wrap defensively so a kill
1820
+ // failure can NEVER propagate into the cancel HTTP route as a 500. On Windows
1821
+ // it invokes an absolute taskkill (PATH-independent) so the tree is actually
1822
+ // killed; the callback surfaces any failure instead of swallowing it.
1823
+ try {
1824
+ (0, win_spawn_1.treeKillSafe)(pid, 'SIGTERM', (err) => {
1825
+ if (err)
1826
+ console.error(`[kill] SIGTERM tree-kill failed for pid ${pid}: ${err.message}`);
1827
+ });
1828
+ }
1829
+ catch (err) {
1830
+ console.error(`[kill] SIGTERM tree-kill threw for pid ${pid}: ${err.message}`);
1831
+ }
1793
1832
  this._killTimer = setTimeout(() => {
1794
- (0, tree_kill_1.default)(pid, 'SIGKILL', (err) => {
1833
+ (0, win_spawn_1.treeKillSafe)(pid, 'SIGKILL', (err) => {
1795
1834
  if (err) {
1796
1835
  // SIGKILL failed — force cleanup so queue is not permanently blocked.
1797
1836
  console.error(`[kill] SIGKILL failed for pid ${pid}: ${err.message}`);
@@ -24,6 +24,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
24
24
  return (mod && mod.__esModule) ? mod : { "default": mod };
25
25
  };
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.treeKillSafe = treeKillSafe;
27
28
  exports.spawnCli = spawnCli;
28
29
  exports.windowsSpawnEnv = windowsSpawnEnv;
29
30
  exports.stripWindowsVerbatimPrefix = stripWindowsVerbatimPrefix;
@@ -32,6 +33,39 @@ exports.__resetWindowsBinaryResolveCacheForTest = __resetWindowsBinaryResolveCac
32
33
  const child_process_1 = require("child_process");
33
34
  const path_1 = __importDefault(require("path"));
34
35
  const cross_spawn_1 = __importDefault(require("cross-spawn"));
36
+ const tree_kill_1 = __importDefault(require("tree-kill"));
37
+ /**
38
+ * Kill a process tree, robustly on Windows. `tree-kill` shells out to
39
+ * `exec('taskkill /pid … /T /F')` through cmd.exe, which resolves `taskkill`
40
+ * against the spawn env's PATH — but `taskkill.exe` lives in
41
+ * `%SystemRoot%\System32`, which a GUI-launched / pkg-stripped sidecar's PATH can
42
+ * lack, so the kill silently no-ops (the rail keeps running) and, with no
43
+ * callback, the failure is swallowed. Here we instead invoke the ABSOLUTE
44
+ * `taskkill.exe` with a SystemRoot-backfilled env (`windowsSpawnEnv`), so the
45
+ * kill never depends on PATH. POSIX delegates to `tree-kill` unchanged
46
+ * (byte-identical). Errors are always surfaced via `callback`.
47
+ */
48
+ function treeKillSafe(pid, signal, callback) {
49
+ const done = (err) => { if (callback)
50
+ callback(err); };
51
+ if (process.platform !== 'win32') {
52
+ (0, tree_kill_1.default)(pid, signal, (err) => done(err ?? undefined));
53
+ return;
54
+ }
55
+ /* c8 ignore start -- Windows-only branch; coverage runs on Linux/macOS */
56
+ try {
57
+ const env = windowsSpawnEnv();
58
+ const systemRoot = (env.SystemRoot || env.windir || 'C:\\Windows').replace(/[\\/]$/, '');
59
+ const taskkill = path_1.default.join(systemRoot, 'System32', 'taskkill.exe');
60
+ // /T = whole tree, /F = force. taskkill has no SIGTERM/SIGKILL distinction;
61
+ // `tree-kill` already always force-kills on win32, so behaviour is unchanged.
62
+ (0, child_process_1.execFile)(taskkill, ['/pid', String(pid), '/T', '/F'], { env }, (err) => done(err ?? undefined));
63
+ }
64
+ catch (err) {
65
+ done(err);
66
+ }
67
+ /* c8 ignore stop */
68
+ }
35
69
  function spawnCli(binary, args, options = {}) {
36
70
  /* c8 ignore next 8 -- Windows-only branch; coverage runs on Linux/macOS */
37
71
  if (process.platform === 'win32') {