specrails-desktop 2.23.0 → 2.23.1
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
|
@@ -326,15 +326,24 @@ summary later.
|
|
|
326
326
|
advances from "building" to the PR question on its own), and the app's rail
|
|
327
327
|
header / Job Detail page stream every event. Tell the user that, and that
|
|
328
328
|
they can ask you for a status check any time (\`specrails_jobs(get)\`).
|
|
329
|
-
- **
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
329
|
+
- **A PR-decision card only exists when the launch isolated in a git worktree.**
|
|
330
|
+
A launch response with an \`isolationUnavailable\` field means the run fell back
|
|
331
|
+
to the shared working tree — there is NO card (not in this chat, not on the
|
|
332
|
+
rail header) and NO branch. NEVER promise a card in that case; instead tell the
|
|
333
|
+
user the run writes changes DIRECTLY into their files, say why (\`no-git\` = the
|
|
334
|
+
folder is not a git repo; \`no-commits\` = git repo with no initial commit;
|
|
335
|
+
\`error\` = isolation failed, relay the detail), and — for no-git/no-commits —
|
|
336
|
+
that \`git init\` + one commit (no GitHub remote needed) unlocks the PR flow.
|
|
337
|
+
When isolation IS available (no such field), the card WILL appear on settle.
|
|
338
|
+
- **After an ISOLATED rail settles, the DECISION belongs to the user, in the
|
|
339
|
+
app's UI** (the PR card in this chat and the rail header show the same
|
|
340
|
+
buttons): Create PR → draft on GitHub; Publish; Discard. There is NO
|
|
341
|
+
"Ship/Revert" — never invent UI. When the repo has NO GitHub remote (or push
|
|
342
|
+
fails), Create PR degrades to a local-only delivery: the card then offers
|
|
343
|
+
**Integrate locally** (merge the delivered branches into the integration
|
|
344
|
+
branch in the user's checkout — requires that branch checked out + a clean
|
|
345
|
+
tree) as the way to ACCEPT the work, alongside retry and Discard. Explain
|
|
346
|
+
exactly that when the user asks "where is the PR?" on a remote-less repo.
|
|
338
347
|
- \`specrails_watch\` on a launched rail/job is RESERVED for when the user
|
|
339
348
|
explicitly asks you to wait for completion ("wait until it finishes"). Even
|
|
340
349
|
then prefer a bounded \`untilMs\` and report status back when it elapses —
|
|
@@ -133,6 +133,7 @@ function createLoopExecutors(opts = {}) {
|
|
|
133
133
|
if (repoDir) {
|
|
134
134
|
try {
|
|
135
135
|
(0, workspace_manager_1.ensureFrameworkAgents)(cwd, adapter.projectDirName);
|
|
136
|
+
(0, workspace_manager_1.ensureFrameworkCommandSubtrees)(cwd, adapter.projectDirName);
|
|
136
137
|
}
|
|
137
138
|
catch { /* best-effort */ }
|
|
138
139
|
}
|
|
@@ -243,6 +244,7 @@ function createLoopExecutors(opts = {}) {
|
|
|
243
244
|
if (repoDir) {
|
|
244
245
|
try {
|
|
245
246
|
(0, workspace_manager_1.ensureFrameworkAgents)(cwd, adapter.projectDirName);
|
|
247
|
+
(0, workspace_manager_1.ensureFrameworkCommandSubtrees)(cwd, adapter.projectDirName);
|
|
246
248
|
}
|
|
247
249
|
catch { /* best-effort */ }
|
|
248
250
|
}
|
|
@@ -208,10 +208,33 @@ function railsTools() {
|
|
|
208
208
|
body.reasoning_effort = defaults.reasoningEffort;
|
|
209
209
|
}
|
|
210
210
|
const r = await (0, types_1.apiCall)(ctx, 'POST', `${base}/${railIndex}/launch`, body);
|
|
211
|
+
const railLabel = `Rail ${railIndex + 1}`;
|
|
212
|
+
// Isolation status governs whether a PR-decision card EXISTS. When
|
|
213
|
+
// the router reports `isolationUnavailable`, the run fell back to the
|
|
214
|
+
// shared working tree: NO git worktree, NO PR delivery row, and so
|
|
215
|
+
// NO implementation/PR card will EVER appear (not in this chat, not
|
|
216
|
+
// in the rail header). The agent MUST NOT promise one — it must tell
|
|
217
|
+
// the user the run writes changes DIRECTLY to their files and why.
|
|
218
|
+
const isoReason = typeof r.isolationUnavailable === 'string' ? r.isolationUnavailable : null;
|
|
219
|
+
if (isoReason) {
|
|
220
|
+
const why = isoReason === 'no-git'
|
|
221
|
+
? 'the project folder is NOT a git repository'
|
|
222
|
+
: isoReason === 'no-commits'
|
|
223
|
+
? 'the git repository has NO commits yet (an unborn HEAD cannot be branched)'
|
|
224
|
+
: `worktree isolation failed${typeof r.isolationUnavailableDetail === 'string' ? `: ${r.isolationUnavailableDetail}` : ''}`;
|
|
225
|
+
const fix = isoReason === 'error'
|
|
226
|
+
? 'This is unexpected — report the detail to the user.'
|
|
227
|
+
: 'To get the PR flow, the user should `git init` the folder and make at least one commit (a GitHub remote is NOT required — the review flow then offers "Integrate locally" to accept without GitHub), then relaunch.';
|
|
228
|
+
return {
|
|
229
|
+
...r,
|
|
230
|
+
railLabel,
|
|
231
|
+
hint: `Launch accepted (202) on ${railLabel}, but WORKTREE ISOLATION IS UNAVAILABLE because ${why}. The run proceeds on the SHARED working tree and writes changes DIRECTLY into the user's files — there is NO PR-decision/implementation card and NO branch. Do NOT tell the user to look for a PR card; tell them the run writes to their files in place, and explain why. ${fix} When it finishes, the spec parks at on_review — the user accepts it by moving it to Done on the board (the changes are already in their files) or reverts the spec's status (which does NOT undo the file changes).`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
211
234
|
return {
|
|
212
235
|
...r,
|
|
213
|
-
railLabel
|
|
214
|
-
hint: `Launch accepted (202) on
|
|
236
|
+
railLabel,
|
|
237
|
+
hint: `Launch accepted (202) on ${railLabel} (isolated worktree, PR flow active) — tell the user it runs on "${railLabel}" (UI labels are 1-based) and that the PR-decision card will appear here and on the rail header when it settles. Use specrails_watch with the returned loopRunIds to await completion only if asked. Rails run for minutes; pass untilMs up to 600000 and re-watch on timeout.`,
|
|
215
238
|
};
|
|
216
239
|
}
|
|
217
240
|
case 'launch_all': {
|
|
@@ -1427,6 +1427,10 @@ class QueueManager {
|
|
|
1427
1427
|
if (execution.relocated) {
|
|
1428
1428
|
try {
|
|
1429
1429
|
(0, workspace_manager_1.ensureFrameworkAgents)(execution.cwd, adapter.projectDirName);
|
|
1430
|
+
// AND the dir-linked subtrees (commands/skills/rules): a broken Windows
|
|
1431
|
+
// `current` junction leaves the workspace with no `/specrails:*` commands
|
|
1432
|
+
// → the CLI reports "Unknown command: /specrails:implement".
|
|
1433
|
+
(0, workspace_manager_1.ensureFrameworkCommandSubtrees)(execution.cwd, adapter.projectDirName);
|
|
1430
1434
|
}
|
|
1431
1435
|
catch {
|
|
1432
1436
|
/* best-effort — never block a rail spawn on the repair */
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.ensureFrameworkAgents = ensureFrameworkAgents;
|
|
7
|
+
exports.ensureFrameworkCommandSubtrees = ensureFrameworkCommandSubtrees;
|
|
7
8
|
exports.workspacePathFor = workspacePathFor;
|
|
8
9
|
exports.ensureWorkspace = ensureWorkspace;
|
|
9
10
|
exports.assembleWorkspaceFramework = assembleWorkspaceFramework;
|
|
@@ -47,22 +48,25 @@ function compareFrameworkVersionDesc(a, b) {
|
|
|
47
48
|
* byte-identical). Additive (never touches user `custom-*.md`) + idempotent.
|
|
48
49
|
* Returns the number of agents copied.
|
|
49
50
|
*/
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Resolve the framework version by listing REAL version dirs — NOT via the
|
|
55
|
-
// `current` junction (which is the very thing that may be untraversable here).
|
|
56
|
-
let version;
|
|
51
|
+
/** Newest REAL framework version dir under the root — resolved by LISTING (not
|
|
52
|
+
* via the `current` junction, which is the very thing that may be untraversable
|
|
53
|
+
* on Windows). Null when the framework isn't materialized. */
|
|
54
|
+
function newestFrameworkVersion(root) {
|
|
57
55
|
try {
|
|
58
|
-
|
|
56
|
+
return fs_1.default
|
|
59
57
|
.readdirSync(root)
|
|
60
58
|
.filter((n) => /^\d+\.\d+\.\d+$/.test(n))
|
|
61
|
-
.sort(compareFrameworkVersionDesc)[0];
|
|
59
|
+
.sort(compareFrameworkVersionDesc)[0] ?? null;
|
|
62
60
|
}
|
|
63
61
|
catch {
|
|
64
|
-
return
|
|
62
|
+
return null;
|
|
65
63
|
}
|
|
64
|
+
}
|
|
65
|
+
function ensureFrameworkAgents(workspaceDir, providerDir, home) {
|
|
66
|
+
if (process.platform !== 'win32')
|
|
67
|
+
return 0;
|
|
68
|
+
const root = (0, framework_manager_1.frameworkRoot)(home);
|
|
69
|
+
const version = newestFrameworkVersion(root);
|
|
66
70
|
if (!version)
|
|
67
71
|
return 0;
|
|
68
72
|
const src = path_1.default.join(root, version, providerDir, 'agents');
|
|
@@ -93,6 +97,93 @@ function ensureFrameworkAgents(workspaceDir, providerDir, home) {
|
|
|
93
97
|
}
|
|
94
98
|
return copied;
|
|
95
99
|
}
|
|
100
|
+
/** The framework subtrees `assemble` installs as a single DIR-symlink into
|
|
101
|
+
* `current/<provider>/` (unlike agents, which are per-file). `/specrails:*`
|
|
102
|
+
* and `/opsx:*` slash commands live under `commands/`. */
|
|
103
|
+
const DIR_LINKED_SUBTREES = ['commands', 'skills', 'rules'];
|
|
104
|
+
/**
|
|
105
|
+
* Windows repair for the DIR-symlinked framework subtrees (`commands`, `skills`,
|
|
106
|
+
* `rules`) — the sibling of `ensureFrameworkAgents` for the whole-directory links.
|
|
107
|
+
*
|
|
108
|
+
* Why a SEPARATE repair: agents are linked per-file, so a broken `current`
|
|
109
|
+
* junction leaves an EMPTY-but-real `agents/` dir that `ensureFrameworkAgents`
|
|
110
|
+
* refills file-by-file. `commands`/`skills`/`rules` are instead a single
|
|
111
|
+
* dir-symlink INTO `current/<provider>/<subtree>`; when the `current` junction is
|
|
112
|
+
* untraversable by the sidecar (the documented Windows failure), that link is
|
|
113
|
+
* unreadable and the workspace has NO `/specrails:*` commands at all — the claude
|
|
114
|
+
* CLI then reports `Unknown command: /specrails:implement`. This replaces an
|
|
115
|
+
* unreadable/missing link with a REAL recursively-copied directory read straight
|
|
116
|
+
* from the versioned framework dir (never through `current`).
|
|
117
|
+
*
|
|
118
|
+
* SAFETY: only heals when the dest is missing OR unreadable OR empty — a dest
|
|
119
|
+
* that lists content is a working link/dir and is left untouched, so a working
|
|
120
|
+
* symlink's real target is NEVER deleted through. NO-OP on POSIX (per-provider
|
|
121
|
+
* symlinks resolve normally). Best-effort + idempotent. Returns the count healed.
|
|
122
|
+
*/
|
|
123
|
+
function ensureFrameworkCommandSubtrees(workspaceDir, providerDir, home) {
|
|
124
|
+
if (process.platform !== 'win32')
|
|
125
|
+
return 0;
|
|
126
|
+
const root = (0, framework_manager_1.frameworkRoot)(home);
|
|
127
|
+
const version = newestFrameworkVersion(root);
|
|
128
|
+
if (!version)
|
|
129
|
+
return 0;
|
|
130
|
+
let healed = 0;
|
|
131
|
+
for (const subtree of DIR_LINKED_SUBTREES) {
|
|
132
|
+
const src = path_1.default.join(root, version, providerDir, subtree);
|
|
133
|
+
// The versioned source must be a real, readable, non-empty dir to heal from.
|
|
134
|
+
let srcEntries;
|
|
135
|
+
try {
|
|
136
|
+
srcEntries = fs_1.default.readdirSync(src);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
continue; // this provider/version ships no such subtree
|
|
140
|
+
}
|
|
141
|
+
if (srcEntries.length === 0)
|
|
142
|
+
continue;
|
|
143
|
+
const dest = path_1.default.join(workspaceDir, providerDir, subtree);
|
|
144
|
+
// A dest that lists entries is a working link/dir → leave it (never risk
|
|
145
|
+
// deleting through a live symlink). Heal only when unreadable / empty.
|
|
146
|
+
try {
|
|
147
|
+
if (fs_1.default.readdirSync(dest).length > 0)
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
/* unreadable (broken/untraversable link, or missing) → heal below */
|
|
152
|
+
}
|
|
153
|
+
// Remove the broken link / empty dir. We only reach here when there is no
|
|
154
|
+
// traversable content, so no real framework files can be destroyed. Layered
|
|
155
|
+
// removal because a Windows dir-junction resists unlink/rmdir differently.
|
|
156
|
+
try {
|
|
157
|
+
const st = fs_1.default.lstatSync(dest);
|
|
158
|
+
if (st.isSymbolicLink()) {
|
|
159
|
+
fs_1.default.unlinkSync(dest);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
fs_1.default.rmSync(dest, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
try {
|
|
167
|
+
fs_1.default.rmdirSync(dest);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
try {
|
|
171
|
+
fs_1.default.rmSync(dest, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
catch { /* dest may be absent — fine */ }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
|
|
178
|
+
fs_1.default.cpSync(src, dest, { recursive: true });
|
|
179
|
+
healed += 1;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
/* best-effort per subtree — a copy failure must never abort the spawn */
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return healed;
|
|
186
|
+
}
|
|
96
187
|
/**
|
|
97
188
|
* WorkspaceManager — a reusable materializer for the per-project workspace dir
|
|
98
189
|
* under `~/.specrails/projects/<slug>/workspace`. This is the relocation target
|