specrails-desktop 2.11.8 → 2.11.10
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 +1 -1
- package/server/dist/contract-refine-runner.js +18 -8
- package/server/dist/path-resolver.js +10 -0
- package/server/dist/project-router-jobs.js +5 -0
- package/server/dist/queue-manager.js +58 -5
- package/server/dist/util/win-spawn.js +34 -0
- package/server/dist/workspace-manager.js +80 -0
package/package.json
CHANGED
|
@@ -94,22 +94,32 @@ function prepareContractRefineSpawn(deps, conversation) {
|
|
|
94
94
|
/* default false */
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
97
|
+
// The refine `--resume`s the Explore conversation's session, so it MUST spawn
|
|
98
|
+
// from the SAME cwd the Explore turn used — otherwise claude looks in a
|
|
99
|
+
// different per-cwd session store and fails with "No conversation found with
|
|
100
|
+
// session ID …" (the Desktop-tier bug). chat-manager `_resolveSpawnCwd` for an
|
|
101
|
+
// Explore turn uses:
|
|
102
|
+
// - mcp ON → `project.path` (NOT the relocated workspace),
|
|
103
|
+
// - mcp OFF → the app-managed explore-cwd.
|
|
104
|
+
// We mirror that EXACTLY here. The refine uses no file tools (`--disallowedTools
|
|
105
|
+
// Read,Grep,Glob,Bash`) and its prompt rides on `--system-prompt`, so it needs
|
|
106
|
+
// neither the workspace's `.mcp.json` nor its `.claude/commands`. Env still
|
|
107
|
+
// carries SPECRAILS_REPO_DIR when relocated (harmless; no tools consume it).
|
|
101
108
|
const exec = (0, workspace_resolution_1.resolveProjectExecution)({ slug: deps.projectSlug, path: deps.projectPath });
|
|
102
|
-
let cwd = exec.cwd;
|
|
103
109
|
let env = exec.relocated ? { ...process.env, ...exec.env } : undefined;
|
|
104
|
-
|
|
110
|
+
let cwd;
|
|
111
|
+
if (mcpEnabled) {
|
|
112
|
+
// Match the Explore mcp-on spawn cwd (`this._cwd` = project.path), even when
|
|
113
|
+
// relocated (where exec.cwd would be the workspace ≠ project.path).
|
|
114
|
+
cwd = deps.projectPath;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
105
117
|
try {
|
|
106
118
|
cwd = (0, explore_cwd_manager_1.ensureExploreCwd)({
|
|
107
119
|
slug: deps.projectSlug,
|
|
108
120
|
projectPath: deps.projectPath,
|
|
109
121
|
projectName: deps.projectName,
|
|
110
122
|
});
|
|
111
|
-
// explore-cwd reaches the repo via its own `./project` link; keep the
|
|
112
|
-
// relocation env so SPECRAILS_REPO_DIR is still exported when relocated.
|
|
113
123
|
}
|
|
114
124
|
catch {
|
|
115
125
|
cwd = exec.cwd;
|
|
@@ -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
|
|
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");
|
|
@@ -28,6 +28,7 @@ const interactive_job_session_1 = require("./interactive-job-session");
|
|
|
28
28
|
const attachment_manager_1 = require("./attachment-manager");
|
|
29
29
|
const ticket_store_1 = require("./ticket-store");
|
|
30
30
|
const binary_probe_1 = require("./binary-probe");
|
|
31
|
+
const workspace_manager_1 = require("./workspace-manager");
|
|
31
32
|
const workspace_resolution_1 = require("./workspace-resolution");
|
|
32
33
|
const framework_manager_1 = require("./framework-manager");
|
|
33
34
|
const openspec_shim_1 = require("./openspec-shim");
|
|
@@ -283,12 +284,12 @@ class QueueManager {
|
|
|
283
284
|
if (proc && proc.pid) {
|
|
284
285
|
const pid = proc.pid;
|
|
285
286
|
try {
|
|
286
|
-
(0,
|
|
287
|
+
(0, win_spawn_1.treeKillSafe)(pid, 'SIGTERM', () => { });
|
|
287
288
|
}
|
|
288
289
|
catch { /* best-effort */ }
|
|
289
290
|
const grace = setTimeout(() => {
|
|
290
291
|
try {
|
|
291
|
-
(0,
|
|
292
|
+
(0, win_spawn_1.treeKillSafe)(pid, 'SIGKILL', () => { });
|
|
292
293
|
}
|
|
293
294
|
catch { /* best-effort */ }
|
|
294
295
|
}, 5000);
|
|
@@ -976,6 +977,19 @@ class QueueManager {
|
|
|
976
977
|
const execution = this._resolveExecution();
|
|
977
978
|
this._jobExecution.set(jobId, execution);
|
|
978
979
|
const spawnCwd = execution.cwd;
|
|
980
|
+
// Windows repair: a relocated workspace whose `.claude/agents` was left empty
|
|
981
|
+
// by the broken `current`-junction read during assemble has no sr-* agents,
|
|
982
|
+
// so the implement pipeline can't delegate and runs inline. Self-heal here
|
|
983
|
+
// (per rail spawn) by copying the agents from the real version dir. NO-OP on
|
|
984
|
+
// POSIX (assemble's per-file symlinks already populate them).
|
|
985
|
+
if (execution.relocated) {
|
|
986
|
+
try {
|
|
987
|
+
(0, workspace_manager_1.ensureFrameworkAgents)(execution.cwd, adapter.projectDirName);
|
|
988
|
+
}
|
|
989
|
+
catch {
|
|
990
|
+
/* best-effort — never block a rail spawn on the repair */
|
|
991
|
+
}
|
|
992
|
+
}
|
|
979
993
|
job.status = 'running';
|
|
980
994
|
job.startedAt = new Date().toISOString();
|
|
981
995
|
job.queuePosition = null;
|
|
@@ -993,6 +1007,33 @@ class QueueManager {
|
|
|
993
1007
|
// Build supplementary context (output chaining + headless mode) that goes
|
|
994
1008
|
// into --append-system-prompt, keeping the user prompt clean.
|
|
995
1009
|
let systemAppend = '';
|
|
1010
|
+
// Repository orientation (relocated projects only). The rail spawns from the
|
|
1011
|
+
// WORKSPACE cwd, which holds only `.specrails/` config — NOT the source code.
|
|
1012
|
+
// The repo is reachable via `--add-dir <repoDir>` (injected below) and the
|
|
1013
|
+
// `./project` link. The framework templates point at `${SPECRAILS_REPO_DIR:-.}`,
|
|
1014
|
+
// but the agent's Read/Grep/Glob/Edit tools do NOT expand that shell-var form
|
|
1015
|
+
// (and PowerShell/cmd.exe don't expand POSIX `${VAR:-default}` either) — so on
|
|
1016
|
+
// Windows the agent reads a bogus literal path, falls back to the empty
|
|
1017
|
+
// workspace cwd, finds only framework files, and hallucinates a wrong/"global"
|
|
1018
|
+
// project. Tell it the concrete absolute repo path explicitly. Mirrors the
|
|
1019
|
+
// Explore-cwd orientation. Legacy (non-relocated) ⇒ cwd IS the repo ⇒ skipped
|
|
1020
|
+
// (byte-identical). The `\${` keeps the shell-var form literal in this string.
|
|
1021
|
+
if (execution.relocated && execution.repoDir) {
|
|
1022
|
+
systemAppend +=
|
|
1023
|
+
`REPOSITORY LOCATION — READ THIS FIRST:\n` +
|
|
1024
|
+
`This pipeline runs from a workspace directory that contains ONLY specrails ` +
|
|
1025
|
+
`configuration (.specrails/, agent definitions) — NOT your project's source code. ` +
|
|
1026
|
+
`Your project's source repository is at this ABSOLUTE path:\n` +
|
|
1027
|
+
` ${execution.repoDir}\n` +
|
|
1028
|
+
`It is also exposed to your tools as an additional working directory (via --add-dir) ` +
|
|
1029
|
+
`and mounted in this cwd as ./project. Use the absolute repo path above (or ./project) ` +
|
|
1030
|
+
`for ALL source reads, edits, greps and globs. Your Read/Grep/Glob/Edit tools do NOT ` +
|
|
1031
|
+
`expand shell variables — NEVER pass a literal "\${SPECRAILS_REPO_DIR:-.}" or ` +
|
|
1032
|
+
`"\${SPECRAILS_REPO_DIR}" as a path; substitute the absolute path above instead. ` +
|
|
1033
|
+
`The spec/ticket store (.specrails/local-tickets.json) lives in THIS workspace cwd. ` +
|
|
1034
|
+
`Do NOT look for source files under this cwd — they exist only under the repository ` +
|
|
1035
|
+
`path above.\n\n`;
|
|
1036
|
+
}
|
|
996
1037
|
// Output chaining: inject previous step's output as context for dependent jobs
|
|
997
1038
|
if (job.dependsOnJobId) {
|
|
998
1039
|
const parentJob = this._jobs.get(job.dependsOnJobId);
|
|
@@ -1788,10 +1829,22 @@ class QueueManager {
|
|
|
1788
1829
|
clearTimeout(this._killTimer);
|
|
1789
1830
|
this._killTimer = null;
|
|
1790
1831
|
}
|
|
1791
|
-
(0, tree_kill_1.default)(this._activeProcess.pid, 'SIGTERM');
|
|
1792
1832
|
const pid = this._activeProcess.pid;
|
|
1833
|
+
// treeKillSafe never throws synchronously, but wrap defensively so a kill
|
|
1834
|
+
// failure can NEVER propagate into the cancel HTTP route as a 500. On Windows
|
|
1835
|
+
// it invokes an absolute taskkill (PATH-independent) so the tree is actually
|
|
1836
|
+
// killed; the callback surfaces any failure instead of swallowing it.
|
|
1837
|
+
try {
|
|
1838
|
+
(0, win_spawn_1.treeKillSafe)(pid, 'SIGTERM', (err) => {
|
|
1839
|
+
if (err)
|
|
1840
|
+
console.error(`[kill] SIGTERM tree-kill failed for pid ${pid}: ${err.message}`);
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
catch (err) {
|
|
1844
|
+
console.error(`[kill] SIGTERM tree-kill threw for pid ${pid}: ${err.message}`);
|
|
1845
|
+
}
|
|
1793
1846
|
this._killTimer = setTimeout(() => {
|
|
1794
|
-
(0,
|
|
1847
|
+
(0, win_spawn_1.treeKillSafe)(pid, 'SIGKILL', (err) => {
|
|
1795
1848
|
if (err) {
|
|
1796
1849
|
// SIGKILL failed — force cleanup so queue is not permanently blocked.
|
|
1797
1850
|
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') {
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ensureFrameworkAgents = ensureFrameworkAgents;
|
|
6
7
|
exports.workspacePathFor = workspacePathFor;
|
|
7
8
|
exports.ensureWorkspace = ensureWorkspace;
|
|
8
9
|
exports.assembleWorkspaceFramework = assembleWorkspaceFramework;
|
|
@@ -12,6 +13,85 @@ const path_1 = __importDefault(require("path"));
|
|
|
12
13
|
const framework_manager_1 = require("./framework-manager");
|
|
13
14
|
const framework_migration_1 = require("./framework-migration");
|
|
14
15
|
const artifact_registry_1 = require("./artifact-registry");
|
|
16
|
+
/** Highest-first semver-ish sort for framework version dir names. */
|
|
17
|
+
function compareFrameworkVersionDesc(a, b) {
|
|
18
|
+
const pa = a.split('.').map((n) => parseInt(n, 10));
|
|
19
|
+
const pb = b.split('.').map((n) => parseInt(n, 10));
|
|
20
|
+
for (let i = 0; i < 3; i++) {
|
|
21
|
+
const x = pa[i] ?? 0;
|
|
22
|
+
const y = pb[i] ?? 0;
|
|
23
|
+
if (x !== y)
|
|
24
|
+
return y - x;
|
|
25
|
+
}
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Windows repair: ensure the workspace's `<providerDir>/agents` holds the
|
|
30
|
+
* framework's `sr-*` agent definitions, copied from the REAL versioned framework
|
|
31
|
+
* dir (`~/.specrails/framework/<version>/<providerDir>/agents`).
|
|
32
|
+
*
|
|
33
|
+
* Why: on Windows the `framework/current` JUNCTION can be untraversable by Node's
|
|
34
|
+
* `fs` ("UNKNOWN: scandir" / "untrusted mount point"); the bundled core's
|
|
35
|
+
* `assemble` sources the agents THROUGH `current`, so `linkAgentFiles` reads
|
|
36
|
+
* nothing and the workspace ends up with NO `sr-*` agents — the implement
|
|
37
|
+
* pipeline then has no sub-agents to delegate to (architect/developer/reviewer)
|
|
38
|
+
* and silently runs everything inline. The versioned dir is a real, traversable
|
|
39
|
+
* directory, so we read it directly. (The proper fix lives in core's `assemble`;
|
|
40
|
+
* this repairs already-broken installs without waiting for a core republish, and
|
|
41
|
+
* mirrors exactly what the fixed core does on Windows — copy from the version
|
|
42
|
+
* dir.) `assemble` never re-runs on a rail spawn, so a workspace broken at setup
|
|
43
|
+
* stays broken; this runs per rail spawn to self-heal it.
|
|
44
|
+
*
|
|
45
|
+
* NO-OP on POSIX (per-file symlinks already populate the workspace correctly;
|
|
46
|
+
* byte-identical). Additive (never touches user `custom-*.md`) + idempotent.
|
|
47
|
+
* Returns the number of agents copied.
|
|
48
|
+
*/
|
|
49
|
+
function ensureFrameworkAgents(workspaceDir, providerDir, home) {
|
|
50
|
+
if (process.platform !== 'win32')
|
|
51
|
+
return 0;
|
|
52
|
+
const root = (0, framework_manager_1.frameworkRoot)(home);
|
|
53
|
+
// Resolve the framework version by listing REAL version dirs — NOT via the
|
|
54
|
+
// `current` junction (which is the very thing that may be untraversable here).
|
|
55
|
+
let version;
|
|
56
|
+
try {
|
|
57
|
+
version = fs_1.default
|
|
58
|
+
.readdirSync(root)
|
|
59
|
+
.filter((n) => /^\d+\.\d+\.\d+$/.test(n))
|
|
60
|
+
.sort(compareFrameworkVersionDesc)[0];
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
if (!version)
|
|
66
|
+
return 0;
|
|
67
|
+
const src = path_1.default.join(root, version, providerDir, 'agents');
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = fs_1.default.readdirSync(src);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return 0; // framework agents not materialized for this provider
|
|
74
|
+
}
|
|
75
|
+
const dest = path_1.default.join(workspaceDir, providerDir, 'agents');
|
|
76
|
+
let copied = 0;
|
|
77
|
+
for (const name of entries) {
|
|
78
|
+
// Framework agents only; never clobber a user/plugin `custom-*.md`.
|
|
79
|
+
if (!name.endsWith('.md') || name.startsWith('custom-'))
|
|
80
|
+
continue;
|
|
81
|
+
const destFile = path_1.default.join(dest, name);
|
|
82
|
+
if (fs_1.default.existsSync(destFile))
|
|
83
|
+
continue;
|
|
84
|
+
try {
|
|
85
|
+
fs_1.default.mkdirSync(dest, { recursive: true });
|
|
86
|
+
fs_1.default.copyFileSync(path_1.default.join(src, name), destFile);
|
|
87
|
+
copied += 1;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* best-effort per file — one failure must never abort the rail spawn */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return copied;
|
|
94
|
+
}
|
|
15
95
|
/**
|
|
16
96
|
* WorkspaceManager — a reusable materializer for the per-project workspace dir
|
|
17
97
|
* under `~/.specrails/projects/<slug>/workspace`. This is the relocation target
|