specrails-desktop 2.27.0 → 2.28.0
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/client/dist/assets/{ActivityFeedPage-BlkNVXCv.js → ActivityFeedPage-DcMmDAP3.js} +1 -1
- package/client/dist/assets/AgentBrowserCapture-CZ3oc1UL.js +1 -0
- package/client/dist/assets/AgentIntegrationsModal-DkqooCFn.js +1 -0
- package/client/dist/assets/{AgentModeCodePane-BUR_QxqR.js → AgentModeCodePane-CM77ljte.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-CnRXelns.js → AgentModeJobsPane-DbZzogbi.js} +1 -1
- package/client/dist/assets/AgentsPage-BZIml7eL.js +86 -0
- package/client/dist/assets/{AnalyticsPage-C_eT3I3i.js → AnalyticsPage-B4qflNxi.js} +1 -1
- package/client/dist/assets/{BarChart-DuzYxiJ6.js → BarChart-Ckstq9fl.js} +1 -1
- package/client/dist/assets/CodePage-DFZXLbI1.js +2 -0
- package/client/dist/assets/{DesktopAnalyticsPage-BSeBkF2n.js → DesktopAnalyticsPage-DbgMa8OO.js} +1 -1
- package/client/dist/assets/{DocsDialog-CGzqDyN3.js → DocsDialog-BQc9Kwev.js} +2 -2
- package/client/dist/assets/DocsPage-OsV44ECy.js +11 -0
- package/client/dist/assets/{ExportDropdown-IehskH-7.js → ExportDropdown-a43XHO3Y.js} +1 -1
- package/client/dist/assets/IntegrationsPage-C3E8hZhB.js +1 -0
- package/client/dist/assets/InteractiveJobComposer-zj8SrOAl.js +19 -0
- package/client/dist/assets/JobDetailModal-Cm6ZeVoh.js +1 -0
- package/client/dist/assets/JobDetailPage-DN67LsdX.js +1 -0
- package/client/dist/assets/JobsPage-DF9wKGbw.js +1 -0
- package/client/dist/assets/LoopBuilderPage-RJ6GGjli.js +7 -0
- package/client/dist/assets/{LoopPreviewModal-BNUQ9iL8.js → LoopPreviewModal-B2wmifF6.js} +1 -1
- package/client/dist/assets/LoopsPage-DdlvH-dJ.js +1 -0
- package/client/dist/assets/PluginsPage-9DI0a6jI.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-dgy5hqPd.js +1 -0
- package/client/dist/assets/{TemplatePreviewModal-C6mndL8D.js → TemplatePreviewModal-CERf1IYB.js} +1 -1
- package/client/dist/assets/agent-BRJCESkc2.js +1 -0
- package/client/dist/assets/agent-BuYz6fJd.js +1 -0
- package/client/dist/assets/agent-CxnRzL6Z.js +1 -0
- package/client/dist/assets/agent-D4JAuEGN2.js +1 -0
- package/client/dist/assets/agent-DNQlI8EN.js +1 -0
- package/client/dist/assets/agent-DyzUH89R.js +1 -0
- package/client/dist/assets/agent-MaTxBlg5.js +1 -0
- package/client/dist/assets/agent-ostmMYNz2.js +1 -0
- package/client/dist/assets/{brain-Dia32foa.js → brain-BPbhM1Jx.js} +1 -1
- package/client/dist/assets/{dist-js-D_4k5prK.js → dist-js-Byjcju1W.js} +1 -1
- package/client/dist/assets/{dist-js-DN7Pxudf.js → dist-js-Dh-9lbLD.js} +1 -1
- package/client/dist/assets/index-89SuhD4Y.js +158 -0
- package/client/dist/assets/index-DOGzWJhO.css +2 -0
- package/client/dist/assets/integrations-By80EBkf.js +1 -0
- package/client/dist/assets/integrations-CAjq5tdW2.js +1 -0
- package/client/dist/assets/integrations-CG2dp3Kd.js +1 -0
- package/client/dist/assets/integrations-CP_aiV_8.js +1 -0
- package/client/dist/assets/integrations-CUNe3t9l2.js +1 -0
- package/client/dist/assets/integrations-CWjh5VRk.js +1 -0
- package/client/dist/assets/integrations-JOHSCvB3.js +1 -0
- package/client/dist/assets/integrations-RJfzXkPx2.js +1 -0
- package/client/dist/assets/{lib-BxSZPd0W.js → lib-yKyTNJ1c.js} +1 -1
- package/client/dist/assets/nav-B2_ToNqS.js +1 -0
- package/client/dist/assets/nav-BAMkVEId.js +1 -0
- package/client/dist/assets/nav-BTg8s-cD.js +1 -0
- package/client/dist/assets/nav-BY_4EWRv.js +1 -0
- package/client/dist/assets/nav-CVXtv0wX2.js +1 -0
- package/client/dist/assets/nav-D0Erv9ZJ2.js +1 -0
- package/client/dist/assets/nav-DUuw9VIU2.js +1 -0
- package/client/dist/assets/nav-S4HM94vW.js +1 -0
- package/client/dist/assets/{upload-4u055Eea.js → upload-Qh1NE3xi.js} +1 -1
- package/client/dist/assets/{value-C3Qe3VXm.js → value-BMPeyInx.js} +1 -1
- package/client/dist/index.html +29 -29
- package/package.json +1 -1
- package/server/dist/agent-chat-router.js +2 -0
- package/server/dist/agent-operator-prompt.js +10 -3
- package/server/dist/global-plugins-router.js +62 -0
- package/server/dist/headroom-manager.js +971 -0
- package/server/dist/headroom-routing.js +53 -0
- package/server/dist/index.js +7 -0
- package/server/dist/mcp/guide.js +3 -0
- package/server/dist/mcp/tools/jobs.js +52 -9
- package/server/dist/path-resolver.js +10 -3
- package/server/dist/project-router-jobs.js +56 -6
- package/server/dist/setup-manager.js +32 -29
- package/server/dist/setup-prerequisites.js +3 -1
- package/server/dist/transient-children.js +152 -13
- package/server/dist/util/cli-prompt.js +3 -0
- package/client/dist/assets/AgentBrowserCapture-76Ri2QR1.js +0 -1
- package/client/dist/assets/AgentIntegrationsModal-D9hVgaBR.js +0 -1
- package/client/dist/assets/AgentsPage-DEt9vpzh.js +0 -86
- package/client/dist/assets/CodePage-72-FLBNw.js +0 -2
- package/client/dist/assets/DocsPage-BB3VGFM9.js +0 -11
- package/client/dist/assets/IntegrationsPage-X8bLyuDk.js +0 -3
- package/client/dist/assets/InteractiveJobComposer-D1HUALME.js +0 -19
- package/client/dist/assets/JobDetailModal-CMg6aKSO.js +0 -1
- package/client/dist/assets/JobDetailPage-Bg01rzeu.js +0 -1
- package/client/dist/assets/JobsPage-Cwkm9yYc.js +0 -1
- package/client/dist/assets/LoopBuilderPage-4uPNN8Xn.js +0 -7
- package/client/dist/assets/LoopsPage-DLXESZxq.js +0 -1
- package/client/dist/assets/ProjectSettingsDialog-DP6hAug-.js +0 -1
- package/client/dist/assets/agent-B0Rg_eLw2.js +0 -1
- package/client/dist/assets/agent-B8f79myL.js +0 -1
- package/client/dist/assets/agent-BCH7a3z-.js +0 -1
- package/client/dist/assets/agent-BELAd4RF.js +0 -1
- package/client/dist/assets/agent-BrCnHlvX2.js +0 -1
- package/client/dist/assets/agent-CrcIj6WE2.js +0 -1
- package/client/dist/assets/agent-CtwLUmjl.js +0 -1
- package/client/dist/assets/agent-xG4qLg4R.js +0 -1
- package/client/dist/assets/index-BbVOi-dY.js +0 -158
- package/client/dist/assets/index-o3Q2Lii5.css +0 -2
- package/client/dist/assets/integrations-BQNkrfr7.js +0 -1
- package/client/dist/assets/integrations-BdN2f6bP.js +0 -1
- package/client/dist/assets/integrations-BrIUJmVf.js +0 -1
- package/client/dist/assets/integrations-CuWnMR5L.js +0 -1
- package/client/dist/assets/integrations-Cyp7cSYv2.js +0 -1
- package/client/dist/assets/integrations-DDR3OHdq.js +0 -1
- package/client/dist/assets/integrations-DVxi8b0j2.js +0 -1
- package/client/dist/assets/integrations-RuNEX3La2.js +0 -1
- package/client/dist/assets/nav-BFCUtHUQ2.js +0 -1
- package/client/dist/assets/nav-BouzorQ5.js +0 -1
- package/client/dist/assets/nav-Bpr-K0tD.js +0 -1
- package/client/dist/assets/nav-CpFrRDXd2.js +0 -1
- package/client/dist/assets/nav-CzKzuwH8.js +0 -1
- package/client/dist/assets/nav-Dkv4Vlr_2.js +0 -1
- package/client/dist/assets/nav-GG6azCbh.js +0 -1
- package/client/dist/assets/nav-prHkMqcr.js +0 -1
- /package/client/dist/assets/{activity-D8vQJrpW.js → activity-Dbl71S-E.js} +0 -0
- /package/client/dist/assets/{addspec-DxW6oPsk.js → addspec-DrXqP8ip.js} +0 -0
- /package/client/dist/assets/{agents-Cev2oFfX.js → agents-CGDn_3kB.js} +0 -0
- /package/client/dist/assets/{agentstudio-CZm2rOzt.js → agentstudio-Dxe_94PK.js} +0 -0
- /package/client/dist/assets/{aiedit-DYtnZU9H.js → aiedit-DrfHQnlz.js} +0 -0
- /package/client/dist/assets/{analytics-Ce-sPORu.js → analytics-CxDYcBl9.js} +0 -0
- /package/client/dist/assets/{attachments-Ci6_42Kh.js → attachments-DvxQbBTC.js} +0 -0
- /package/client/dist/assets/{auth-Cvsb7-Ek.js → auth-DpZq5p1e.js} +0 -0
- /package/client/dist/assets/{browser-CdS6O1Qb.js → browser-DM8G151q.js} +0 -0
- /package/client/dist/assets/{chat-VMflhzxn.js → chat-0dg5hOo_.js} +0 -0
- /package/client/dist/assets/{code-thSEptMx.js → code-B62fClk-.js} +0 -0
- /package/client/dist/assets/{commands-CckaBWdV.js → commands-DCJ-R9E4.js} +0 -0
- /package/client/dist/assets/{common-CYaT2yHp.js → common-DXrmj6WD.js} +0 -0
- /package/client/dist/assets/{dashboard-BawQCyrV.js → dashboard-DBuLfWo9.js} +0 -0
- /package/client/dist/assets/{explore-BKVV5JSu.js → explore-_XyU5e7R.js} +0 -0
- /package/client/dist/assets/{jira-B3613-gj.js → jira-DeIjwCK3.js} +0 -0
- /package/client/dist/assets/{jobs-D5sZzgLs.js → jobs-DrYmsKy5.js} +0 -0
- /package/client/dist/assets/{loops-Brnx4Fji.js → loops-CO6q75W8.js} +0 -0
- /package/client/dist/assets/{mcp-CIH0pH1K.js → mcp-HwBbQDk0.js} +0 -0
- /package/client/dist/assets/{settings-BFv6UbKV.js → settings-BYCMtxOz.js} +0 -0
- /package/client/dist/assets/{setup-RiUHzq7E.js → setup-D2CmgAro.js} +0 -0
- /package/client/dist/assets/{specs-CmEZY57v.js → specs-PGIOzMtc.js} +0 -0
- /package/client/dist/assets/{terminal-DFPNRLKp.js → terminal-CFt56A5f.js} +0 -0
- /package/client/dist/assets/{tickets-CkfbISuK.js → tickets-BHv3ewvq.js} +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setHeadroomRoutingState = setHeadroomRoutingState;
|
|
4
|
+
exports.getHeadroomRoutingState = getHeadroomRoutingState;
|
|
5
|
+
exports.providerForBinary = providerForBinary;
|
|
6
|
+
exports.applyHeadroomEnvForBinary = applyHeadroomEnvForBinary;
|
|
7
|
+
exports.withHeadroomSpawnEnv = withHeadroomSpawnEnv;
|
|
8
|
+
const DEFAULT_PORT = 8787;
|
|
9
|
+
let routingState = {
|
|
10
|
+
port: DEFAULT_PORT,
|
|
11
|
+
activeProviders: {},
|
|
12
|
+
};
|
|
13
|
+
function setHeadroomRoutingState(next) {
|
|
14
|
+
routingState = {
|
|
15
|
+
port: Number.isInteger(next.port) ? next.port : DEFAULT_PORT,
|
|
16
|
+
activeProviders: { ...next.activeProviders },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function getHeadroomRoutingState() {
|
|
20
|
+
return {
|
|
21
|
+
port: routingState.port,
|
|
22
|
+
activeProviders: { ...routingState.activeProviders },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function providerForBinary(binary) {
|
|
26
|
+
const name = binary.toLowerCase().replace(/\.cmd$|\.exe$/, '');
|
|
27
|
+
if (name === 'codex')
|
|
28
|
+
return 'codex';
|
|
29
|
+
if (name === 'claude')
|
|
30
|
+
return 'claude';
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function applyHeadroomEnvForBinary(binary, env) {
|
|
34
|
+
const provider = providerForBinary(binary);
|
|
35
|
+
if (!provider || !routingState.activeProviders[provider])
|
|
36
|
+
return env ?? process.env;
|
|
37
|
+
const base = `http://127.0.0.1:${routingState.port}`;
|
|
38
|
+
const merged = { ...(env ?? process.env) };
|
|
39
|
+
if (provider === 'codex') {
|
|
40
|
+
merged.OPENAI_BASE_URL = `${base}/v1`;
|
|
41
|
+
}
|
|
42
|
+
else if (provider === 'claude') {
|
|
43
|
+
merged.ANTHROPIC_BASE_URL = base;
|
|
44
|
+
}
|
|
45
|
+
merged.HEADROOM_PORT = String(routingState.port);
|
|
46
|
+
return merged;
|
|
47
|
+
}
|
|
48
|
+
function withHeadroomSpawnEnv(binary, options = {}) {
|
|
49
|
+
return {
|
|
50
|
+
...options,
|
|
51
|
+
env: applyHeadroomEnvForBinary(binary, options.env),
|
|
52
|
+
};
|
|
53
|
+
}
|
package/server/dist/index.js
CHANGED
|
@@ -29,6 +29,8 @@ const agent_chat_router_1 = require("./agent-chat-router");
|
|
|
29
29
|
const agent_chat_registry_1 = require("./agent-chat-registry");
|
|
30
30
|
const browser_playwright_1 = require("./browser-playwright");
|
|
31
31
|
const telemetry_receiver_1 = require("./telemetry-receiver");
|
|
32
|
+
const headroom_manager_1 = require("./headroom-manager");
|
|
33
|
+
const global_plugins_router_1 = require("./global-plugins-router");
|
|
32
34
|
const telemetry_compactor_1 = require("./telemetry-compactor");
|
|
33
35
|
const framework_manager_1 = require("./framework-manager");
|
|
34
36
|
const artifact_registry_1 = require("./artifact-registry");
|
|
@@ -537,6 +539,11 @@ function applyPtyWsRateLimiting(ws) {
|
|
|
537
539
|
mcpManager.start().catch((err) => console.error('[mcp] boot start failed:', err));
|
|
538
540
|
}
|
|
539
541
|
// ─── App-global agent chat (drives the app via its own MCP) ────────────────
|
|
542
|
+
const headroomManager = new headroom_manager_1.HeadroomManager(registry.desktopDb, broadcast, () => registry.installedProvidersUnion().filter((provider) => provider === 'codex' || provider === 'claude'));
|
|
543
|
+
app.use('/api/global-plugins', (0, global_plugins_router_1.createGlobalPluginsRouter)(headroomManager));
|
|
544
|
+
headroomManager.startActiveProxyOnBoot().catch((err) => {
|
|
545
|
+
console.error('[headroom] boot start failed:', err);
|
|
546
|
+
});
|
|
540
547
|
const agentChatManager = new agent_chat_manager_1.AgentChatManager(broadcast, registry.desktopDb, port, registry);
|
|
541
548
|
_agentChatManager = agentChatManager;
|
|
542
549
|
// Publish the instance to the process-wide registry so the rails layer can
|
package/server/dist/mcp/guide.js
CHANGED
|
@@ -149,6 +149,9 @@ heading inside the description); \`labels\`; \`priority\`. Spec content is Engli
|
|
|
149
149
|
- \`specrails_jobs(spawn, command)\` bypasses rails and enqueues an arbitrary
|
|
150
150
|
slash-command job (e.g. \`/specrails:implement #5 --yes\`); \`queue\`, \`pause\`,
|
|
151
151
|
\`resume\`, \`reorder\`, \`priority\` manage the queue.
|
|
152
|
+
- Long-running shell commands launched with \`specrails_jobs(background_start)\`
|
|
153
|
+
create chat chips; use \`specrails_jobs(background_logs, pid)\` to read bounded
|
|
154
|
+
stdout/stderr tail when a chip exits or fails.
|
|
152
155
|
|
|
153
156
|
## Async results (IMPORTANT)
|
|
154
157
|
|
|
@@ -8,6 +8,7 @@ const zod_1 = require("zod");
|
|
|
8
8
|
const types_1 = require("./types");
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const transient_children_1 = require("../../transient-children");
|
|
11
|
+
const loop_runs_store_1 = require("../../loop-runs-store");
|
|
11
12
|
function resolveBackgroundCwd(projectRoot, cwd) {
|
|
12
13
|
const resolved = path_1.default.resolve(projectRoot, cwd && cwd.trim() ? cwd : '.');
|
|
13
14
|
const root = path_1.default.resolve(projectRoot);
|
|
@@ -24,11 +25,6 @@ function backgroundHooks(broadcast) {
|
|
|
24
25
|
projectId: process.projectId,
|
|
25
26
|
timestamp: new Date().toISOString(),
|
|
26
27
|
}),
|
|
27
|
-
onOutput: (event) => broadcast({
|
|
28
|
-
type: 'background_process.output',
|
|
29
|
-
...event,
|
|
30
|
-
timestamp: new Date().toISOString(),
|
|
31
|
-
}),
|
|
32
28
|
onExited: (process) => broadcast({
|
|
33
29
|
type: 'background_process.exited',
|
|
34
30
|
process,
|
|
@@ -37,6 +33,20 @@ function backgroundHooks(broadcast) {
|
|
|
37
33
|
}),
|
|
38
34
|
};
|
|
39
35
|
}
|
|
36
|
+
function backgroundStartBusyReason(projectCtx) {
|
|
37
|
+
const activeJobId = typeof projectCtx.queueManager?.getActiveJobId === 'function' ? projectCtx.queueManager.getActiveJobId() : null;
|
|
38
|
+
if (activeJobId)
|
|
39
|
+
return `job ${activeJobId} is still running`;
|
|
40
|
+
try {
|
|
41
|
+
const runningLoops = (0, loop_runs_store_1.listRunningLoopRuns)(projectCtx.db, projectCtx.project.id);
|
|
42
|
+
if (runningLoops.length > 0)
|
|
43
|
+
return `loop run ${runningLoops[0].id} is still running`;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Some unit harnesses stub the DB narrowly; absence of loop metadata is not a blocker.
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
40
50
|
/**
|
|
41
51
|
* Jobs domain facade. Maps every in-scope jobs/queue action to its REST
|
|
42
52
|
* endpoint on the loopback API (server/project-router-jobs.ts, plus the
|
|
@@ -54,7 +64,7 @@ function jobsTools() {
|
|
|
54
64
|
title: 'Jobs & Queue',
|
|
55
65
|
description: 'Manage a project\'s AI-pipeline job queue and individual jobs. ' +
|
|
56
66
|
'Actions: list, get, queue, spawn (ai-spawn — enqueues an arbitrary slash-command job, returns {jobId,position}, async; from the in-app agent chat the engine defaults to your conversation\'s provider — pass aiEngine to override), ' +
|
|
57
|
-
'background_start (operate-level shell command tied to the current agent chat; requires explicit user confirmation before use), background_kill, ' +
|
|
67
|
+
'background_start (operate-level shell command tied to the current agent chat; requires confirmed:true after explicit user confirmation before use), background_logs, background_kill, ' +
|
|
58
68
|
'cancel (destructive — cancels a running/queued job or deletes a terminal one), ' +
|
|
59
69
|
'purge (destructive — bulk-delete persisted job rows in a date range), ' +
|
|
60
70
|
'pause / resume (queue), reorder (queued-job order), priority (change a queued job\'s priority), ' +
|
|
@@ -100,6 +110,7 @@ function jobsTools() {
|
|
|
100
110
|
'interactive_turn',
|
|
101
111
|
'finalize',
|
|
102
112
|
'background_start',
|
|
113
|
+
'background_logs',
|
|
103
114
|
'background_kill',
|
|
104
115
|
])
|
|
105
116
|
.describe('Operation to perform'),
|
|
@@ -110,7 +121,7 @@ function jobsTools() {
|
|
|
110
121
|
.optional()
|
|
111
122
|
.describe('Job id (for get / cancel / priority / diagnostic / interactive_turn / finalize)'),
|
|
112
123
|
// ── list / export pagination + filters ──
|
|
113
|
-
limit: zod_1.z.number().optional().describe('Page size for list (1-200, default 50) / activity (1-100, default 50)'),
|
|
124
|
+
limit: zod_1.z.number().optional().describe('Page size for list (1-200, default 50) / activity (1-100, default 50); background_logs returns the last N buffered log lines'),
|
|
114
125
|
offset: zod_1.z.number().optional().describe('Page offset for list'),
|
|
115
126
|
status: zod_1.z.string().optional().describe('Filter by job status (list)'),
|
|
116
127
|
from: zod_1.z.string().optional().describe('ISO date lower bound (list / export / purge)'),
|
|
@@ -136,9 +147,11 @@ function jobsTools() {
|
|
|
136
147
|
.describe('reorder: exact set of currently-queued job ids in the desired order; compare: exactly 2 job ids'),
|
|
137
148
|
// ── interactive_turn ──
|
|
138
149
|
text: zod_1.z.string().optional().describe('Prompt text for interactive_turn'),
|
|
139
|
-
chatId: zod_1.z.string().optional().describe('Agent chat conversation id for background_start/background_kill ownership; defaults to the in-app agent conversation that called the tool'),
|
|
150
|
+
chatId: zod_1.z.string().optional().describe('Agent chat conversation id for background_start/background_logs/background_kill ownership; defaults to the in-app agent conversation that called the tool'),
|
|
140
151
|
cwd: zod_1.z.string().optional().describe('Optional cwd for background_start; resolved inside the selected project root'),
|
|
141
|
-
pid: zod_1.z.number().optional().describe('Background process pid for background_kill'),
|
|
152
|
+
pid: zod_1.z.number().optional().describe('Background process pid for background_logs/background_kill'),
|
|
153
|
+
confirmed: zod_1.z.boolean().optional().describe('background_start only: must be true after explicit user confirmation for this exact command'),
|
|
154
|
+
allowWhileBusy: zod_1.z.boolean().optional().describe('background_start only: override active job/loop guard after explicit user confirmation'),
|
|
142
155
|
// ── export ──
|
|
143
156
|
format: zod_1.z.enum(['json', 'csv']).optional().describe('Export format (default json)'),
|
|
144
157
|
// ── default_spec_model ──
|
|
@@ -314,10 +327,16 @@ function jobsTools() {
|
|
|
314
327
|
const command = args.command;
|
|
315
328
|
if (!command || !command.trim())
|
|
316
329
|
throw new Error('background_start requires a "command".');
|
|
330
|
+
if (args.confirmed !== true)
|
|
331
|
+
throw new Error('background_start requires confirmed:true after explicit user confirmation for this exact command.');
|
|
317
332
|
const chatId = args.chatId ?? ctx.originConversationId ?? undefined;
|
|
318
333
|
if (!chatId || !chatId.trim())
|
|
319
334
|
throw new Error('background_start requires a "chatId".');
|
|
320
335
|
const projectCtx = (0, types_1.requireProject)(ctx, args.projectId);
|
|
336
|
+
const busyReason = backgroundStartBusyReason(projectCtx);
|
|
337
|
+
if (busyReason && args.allowWhileBusy !== true) {
|
|
338
|
+
throw new Error(`background_start refused because ${busyReason}. Wait for it to finish, or pass allowWhileBusy:true only after explicit user confirmation.`);
|
|
339
|
+
}
|
|
321
340
|
const cwd = resolveBackgroundCwd(projectCtx.project.path, args.cwd);
|
|
322
341
|
const process = (0, transient_children_1.startBackgroundProcess)(command.trim(), cwd, chatId.trim(), projectCtx.project.id, backgroundHooks(ctx.broadcast));
|
|
323
342
|
return { ok: true, process };
|
|
@@ -335,6 +354,30 @@ function jobsTools() {
|
|
|
335
354
|
throw new Error('background_kill requires a registered pid in the same project/chat.');
|
|
336
355
|
return { ok: true, pid, status: 'killing' };
|
|
337
356
|
}
|
|
357
|
+
case 'background_logs': {
|
|
358
|
+
const pid = args.pid;
|
|
359
|
+
if (typeof pid !== 'number' || !Number.isFinite(pid))
|
|
360
|
+
throw new Error('background_logs requires a numeric "pid".');
|
|
361
|
+
const chatId = args.chatId ?? ctx.originConversationId ?? undefined;
|
|
362
|
+
if (!chatId || !chatId.trim())
|
|
363
|
+
throw new Error('background_logs requires a "chatId".');
|
|
364
|
+
const projectCtx = (0, types_1.requireProject)(ctx, args.projectId);
|
|
365
|
+
const limit = typeof args.limit === 'number' && Number.isFinite(args.limit) ? args.limit : undefined;
|
|
366
|
+
const logs = (0, transient_children_1.getBackgroundProcessLogs)(pid, {
|
|
367
|
+
projectId: projectCtx.project.id,
|
|
368
|
+
chatId: chatId.trim(),
|
|
369
|
+
...(limit !== undefined ? { limit } : {}),
|
|
370
|
+
});
|
|
371
|
+
if (!logs)
|
|
372
|
+
throw new Error('background_logs requires a registered pid in the same project/chat; logs may have expired.');
|
|
373
|
+
return {
|
|
374
|
+
ok: true,
|
|
375
|
+
...logs,
|
|
376
|
+
hint: logs.lines.length > 0
|
|
377
|
+
? 'Inspect stdout/stderr to explain why the background process exited or failed.'
|
|
378
|
+
: 'No stdout/stderr lines have been captured yet; the process may not have emitted newline-terminated output.',
|
|
379
|
+
};
|
|
380
|
+
}
|
|
338
381
|
default:
|
|
339
382
|
throw new Error(`Unknown action "${action}".`);
|
|
340
383
|
}
|
|
@@ -105,7 +105,14 @@ function resolveBundledBinDirs(runtimesPath) {
|
|
|
105
105
|
else if (fileExists(path_1.default.join(runtimesPath, 'git', 'bin', 'git'))) {
|
|
106
106
|
gitBinDir = path_1.default.join(runtimesPath, 'git', 'bin');
|
|
107
107
|
}
|
|
108
|
-
|
|
108
|
+
const uvBinDir = isWin
|
|
109
|
+
? (fileExists(path_1.default.join(runtimesPath, 'uv', 'uv.exe')) ? path_1.default.join(runtimesPath, 'uv')
|
|
110
|
+
: fileExists(path_1.default.join(runtimesPath, 'uv', 'bin', 'uv.exe')) ? path_1.default.join(runtimesPath, 'uv', 'bin')
|
|
111
|
+
: null)
|
|
112
|
+
: (fileExists(path_1.default.join(runtimesPath, 'uv', 'bin', 'uv')) ? path_1.default.join(runtimesPath, 'uv', 'bin')
|
|
113
|
+
: fileExists(path_1.default.join(runtimesPath, 'uv', 'uv')) ? path_1.default.join(runtimesPath, 'uv')
|
|
114
|
+
: null);
|
|
115
|
+
return { nodeBinDir, gitBinDir, uvBinDir };
|
|
109
116
|
}
|
|
110
117
|
/**
|
|
111
118
|
* Bin dir of the bundled GitHub CLI (`runtimes/gh/bin/gh[.exe]`), or null.
|
|
@@ -266,7 +273,7 @@ function resolveStartupPathBase() {
|
|
|
266
273
|
if (process.env.SPECRAILS_IS_DESKTOP === '1') {
|
|
267
274
|
const runtimesPath = process.env.SPECRAILS_BUNDLED_RUNTIMES_PATH;
|
|
268
275
|
if (runtimesPath) {
|
|
269
|
-
const { nodeBinDir, gitBinDir } = resolveBundledBinDirs(runtimesPath);
|
|
276
|
+
const { nodeBinDir, gitBinDir, uvBinDir } = resolveBundledBinDirs(runtimesPath);
|
|
270
277
|
// Activate the bundle only when BOTH node and git are present. A partial
|
|
271
278
|
// bundle (one tool present, the other missing — a botched extraction) is
|
|
272
279
|
// treated as NOT active so the full system fallback (fast-path + login-shell)
|
|
@@ -275,7 +282,7 @@ function resolveStartupPathBase() {
|
|
|
275
282
|
if (nodeBinDir && gitBinDir) {
|
|
276
283
|
const inherited = splitPath(process.env.PATH);
|
|
277
284
|
const inheritedSet = new Set(inherited);
|
|
278
|
-
const bundledDirs = [nodeBinDir, gitBinDir].filter((d) => !inheritedSet.has(d));
|
|
285
|
+
const bundledDirs = [nodeBinDir, gitBinDir, uvBinDir].filter((d) => !!d && !inheritedSet.has(d));
|
|
279
286
|
bundledDirs.forEach((d) => inheritedSet.add(d));
|
|
280
287
|
// Provider CLIs (claude/codex/gemini) are NEVER bundled — they always
|
|
281
288
|
// come from the system. On Windows their `.cmd` shims live in
|
|
@@ -18,6 +18,20 @@ const metrics_1 = require("./metrics");
|
|
|
18
18
|
const ticket_store_1 = require("./ticket-store");
|
|
19
19
|
const transient_children_1 = require("./transient-children");
|
|
20
20
|
const project_router_helpers_1 = require("./project-router-helpers");
|
|
21
|
+
function backgroundStartBusyReason(c) {
|
|
22
|
+
const activeJobId = typeof c.queueManager?.getActiveJobId === 'function' ? c.queueManager.getActiveJobId() : null;
|
|
23
|
+
if (activeJobId)
|
|
24
|
+
return `job ${activeJobId} is still running`;
|
|
25
|
+
try {
|
|
26
|
+
const runningLoops = (0, loop_runs_store_1.listRunningLoopRuns)(c.db, c.project.id);
|
|
27
|
+
if (runningLoops.length > 0)
|
|
28
|
+
return `loop run ${runningLoops[0].id} is still running`;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// If the loop table is unavailable in a test harness, do not block startup.
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
21
35
|
function registerJobsRoutes(deps) {
|
|
22
36
|
const { router, registry, ctx, ticketPath } = deps;
|
|
23
37
|
// ─── Queue / Spawn routes ────────────────────────────────────────────────────
|
|
@@ -101,8 +115,36 @@ function registerJobsRoutes(deps) {
|
|
|
101
115
|
}),
|
|
102
116
|
});
|
|
103
117
|
});
|
|
118
|
+
router.get('/:projectId/background-processes/:pid/logs', (req, res) => {
|
|
119
|
+
const c = ctx(req);
|
|
120
|
+
const pid = Number(req.params.pid);
|
|
121
|
+
const chatId = typeof req.query.chatId === 'string' ? req.query.chatId : '';
|
|
122
|
+
const limit = typeof req.query.limit === 'string' ? Number(req.query.limit) : undefined;
|
|
123
|
+
if (!Number.isFinite(pid)) {
|
|
124
|
+
res.status(400).json({ error: 'pid must be numeric' });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!chatId.trim()) {
|
|
128
|
+
res.status(400).json({ error: 'chatId is required' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (limit !== undefined && (!Number.isFinite(limit) || limit <= 0)) {
|
|
132
|
+
res.status(400).json({ error: 'limit must be a positive number' });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const logs = (0, transient_children_1.getBackgroundProcessLogs)(pid, {
|
|
136
|
+
projectId: c.project.id,
|
|
137
|
+
chatId: chatId.trim(),
|
|
138
|
+
...(limit !== undefined ? { limit } : {}),
|
|
139
|
+
});
|
|
140
|
+
if (!logs) {
|
|
141
|
+
res.status(404).json({ error: 'background process logs not found for project/chat' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
res.json(logs);
|
|
145
|
+
});
|
|
104
146
|
router.post('/:projectId/background-processes', (req, res) => {
|
|
105
|
-
const { command, cwd, chatId } = req.body ?? {};
|
|
147
|
+
const { command, cwd, chatId, confirmed, allowWhileBusy } = req.body ?? {};
|
|
106
148
|
const c = ctx(req);
|
|
107
149
|
if (!command || typeof command !== 'string' || !command.trim()) {
|
|
108
150
|
res.status(400).json({ error: 'command is required' });
|
|
@@ -112,6 +154,19 @@ function registerJobsRoutes(deps) {
|
|
|
112
154
|
res.status(400).json({ error: 'chatId is required' });
|
|
113
155
|
return;
|
|
114
156
|
}
|
|
157
|
+
if (confirmed !== true) {
|
|
158
|
+
res.status(400).json({ error: 'confirmed must be true after explicit user confirmation' });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const busyReason = backgroundStartBusyReason(c);
|
|
162
|
+
if (busyReason && allowWhileBusy !== true) {
|
|
163
|
+
res.status(409).json({
|
|
164
|
+
error: 'project is busy',
|
|
165
|
+
reason: busyReason,
|
|
166
|
+
hint: 'Wait for the running job/loop to finish, or pass allowWhileBusy:true only after explicit user confirmation.',
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
115
170
|
const resolvedCwd = path_1.default.resolve(c.project.path, typeof cwd === 'string' && cwd.trim() ? cwd : '.');
|
|
116
171
|
const root = path_1.default.resolve(c.project.path);
|
|
117
172
|
if (resolvedCwd !== root && !resolvedCwd.startsWith(root + path_1.default.sep)) {
|
|
@@ -126,11 +181,6 @@ function registerJobsRoutes(deps) {
|
|
|
126
181
|
projectId: process.projectId,
|
|
127
182
|
timestamp: new Date().toISOString(),
|
|
128
183
|
}),
|
|
129
|
-
onOutput: (event) => c.broadcast({
|
|
130
|
-
type: 'background_process.output',
|
|
131
|
-
...event,
|
|
132
|
-
timestamp: new Date().toISOString(),
|
|
133
|
-
}),
|
|
134
184
|
onExited: (process) => c.broadcast({
|
|
135
185
|
type: 'background_process.exited',
|
|
136
186
|
process,
|
|
@@ -275,12 +275,12 @@ function deployTemplates(projectPath, selectedAgents) {
|
|
|
275
275
|
// Full install: 7-phase enrichment flow (claude /specrails:enrich)
|
|
276
276
|
exports.CHECKPOINTS = [
|
|
277
277
|
{ key: 'base_install', name: 'Base installation' },
|
|
278
|
-
{ key: '
|
|
279
|
-
{ key: '
|
|
280
|
-
{ key: '
|
|
278
|
+
{ key: 'agent_selection', name: 'Agent selection' },
|
|
279
|
+
{ key: 'codebase_analysis', name: 'Codebase analysis' },
|
|
280
|
+
{ key: 'vpc_discovery', name: 'VPC discovery' },
|
|
281
281
|
{ key: 'agent_generation', name: 'Agent generation' },
|
|
282
|
-
{ key: '
|
|
283
|
-
{ key: '
|
|
282
|
+
{ key: 'persona_synthesis', name: 'Persona synthesis' },
|
|
283
|
+
{ key: 'command_generation', name: 'Command generation' },
|
|
284
284
|
];
|
|
285
285
|
// Quick install: 3-phase non-interactive flow (npx init --from-config)
|
|
286
286
|
exports.QUICK_CHECKPOINTS = [
|
|
@@ -303,19 +303,20 @@ function checkFilesystem(projectPath) {
|
|
|
303
303
|
const hasCommands = (((0, fs_1.existsSync)((0, path_1.join)(projectPath, dir, 'commands', 'sr')) && hasFiles((0, path_1.join)(projectPath, dir, 'commands', 'sr'), /\.md$/)) ||
|
|
304
304
|
((0, fs_1.existsSync)((0, path_1.join)(projectPath, dir, 'commands', 'specrails')) && hasFiles((0, path_1.join)(projectPath, dir, 'commands', 'specrails'), /\.md$/)));
|
|
305
305
|
const hasCLAUDE = (0, fs_1.existsSync)((0, path_1.join)(projectPath, 'CLAUDE.md'));
|
|
306
|
+
const hasInstallConfig = (0, fs_1.existsSync)((0, path_1.join)(projectPath, '.specrails', 'install-config.yaml')) ||
|
|
307
|
+
(0, fs_1.existsSync)((0, path_1.join)(projectPath, dir, 'install-config.yaml'));
|
|
308
|
+
const hasAgentConfig = (0, fs_1.existsSync)((0, path_1.join)(projectPath, dir, 'agents.yaml'));
|
|
309
|
+
const hasBacklogConfig = (0, fs_1.existsSync)((0, path_1.join)(projectPath, dir, 'backlog-config.json'));
|
|
306
310
|
return {
|
|
307
311
|
base_install: hasBaseInstall,
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
312
|
+
// Back-compat filesystem signals from older cores are translated to the
|
|
313
|
+
// current integration-contract checkpoint keys.
|
|
314
|
+
agent_selection: hasInstallConfig || hasAgentConfig || hasBacklogConfig || hasAgents || hasCommands,
|
|
315
|
+
codebase_analysis: hasBaseInstall && (hasCLAUDE || hasSetupTemplates),
|
|
316
|
+
vpc_discovery: hasPersonas,
|
|
317
|
+
persona_synthesis: hasPersonas,
|
|
314
318
|
agent_generation: hasAgents,
|
|
315
|
-
|
|
316
|
-
// Final verification: agents + commands must exist (manifest from install.sh is unreliable —
|
|
317
|
-
// it's created during scaffolding before /setup generates the actual artifacts)
|
|
318
|
-
final_verification: hasAgents && hasCommands,
|
|
319
|
+
command_generation: hasCommands || (hasAgents && hasRules),
|
|
319
320
|
};
|
|
320
321
|
}
|
|
321
322
|
function hasFiles(dir, pattern) {
|
|
@@ -357,21 +358,23 @@ function readEnrichMdContent(projectPath) {
|
|
|
357
358
|
// ─── Stream-based checkpoint detection ───────────────────────────────────────
|
|
358
359
|
function detectCheckpointFromText(text) {
|
|
359
360
|
const hits = [];
|
|
360
|
-
// Match phase headers from Claude's /specrails:enrich output (and legacy /setup)
|
|
361
|
+
// Match phase headers from Claude's /specrails:enrich output (and legacy /setup).
|
|
362
|
+
// Older specrails-core prompts used different checkpoint names; keep accepting
|
|
363
|
+
// those phrases but emit only the current integration-contract keys.
|
|
361
364
|
if (/phase\s*1|codebase\s*analysis|repository\s*analysis/i.test(text)) {
|
|
362
|
-
hits.push({ key: '
|
|
365
|
+
hits.push({ key: 'codebase_analysis', detail: 'Analyzing codebase...' });
|
|
363
366
|
}
|
|
364
|
-
if (/phase\s*2|user\s*personas|product\s*discovery/i.test(text)) {
|
|
365
|
-
hits.push({ key: '
|
|
367
|
+
if (/phase\s*2|user\s*personas|product\s*discovery|vpc\s*discovery/i.test(text)) {
|
|
368
|
+
hits.push({ key: 'vpc_discovery', detail: 'Discovering personas...' });
|
|
366
369
|
}
|
|
367
370
|
if (/phase\s*3|configuration|agent\s*selection|backlog\s*provider/i.test(text)) {
|
|
368
|
-
hits.push({ key: '
|
|
371
|
+
hits.push({ key: 'agent_selection', detail: 'Configuring agents...' });
|
|
369
372
|
}
|
|
370
373
|
if (/generating\s*all\s*files|writing.*agent|sr-architect|sr-developer|sr-reviewer/i.test(text)) {
|
|
371
374
|
hits.push({ key: 'agent_generation', detail: 'Generating agents...' });
|
|
372
375
|
}
|
|
373
376
|
if (/command\s*selection|installing.*commands|\.claude\/commands\/(sr|specrails)/i.test(text)) {
|
|
374
|
-
hits.push({ key: '
|
|
377
|
+
hits.push({ key: 'command_generation', detail: 'Configuring commands...' });
|
|
375
378
|
}
|
|
376
379
|
// TUI output patterns from specrails-core init --from-config.
|
|
377
380
|
// Covers both the retired bash installer (✓ config loaded, reading
|
|
@@ -385,7 +388,7 @@ function detectCheckpointFromText(text) {
|
|
|
385
388
|
hits.push({ key: 'agent_generation', detail: 'Installing specrails artefacts...' });
|
|
386
389
|
}
|
|
387
390
|
if (/writing\s*manifest|wrote\s+.*specrails-manifest/i.test(text)) {
|
|
388
|
-
hits.push({ key: '
|
|
391
|
+
hits.push({ key: 'command_generation' });
|
|
389
392
|
}
|
|
390
393
|
if (/✓\s*installed|installation\s*complete|init\s*complete|update\s*complete/i.test(text)) {
|
|
391
394
|
hits.push({ key: 'quick_complete' });
|
|
@@ -394,7 +397,7 @@ function detectCheckpointFromText(text) {
|
|
|
394
397
|
if (text.includes('.specrails-version') || text.includes('specrails/specrails-version'))
|
|
395
398
|
hits.push({ key: 'base_install' });
|
|
396
399
|
if (text.includes('/agents/personas/') && text.includes('.md')) {
|
|
397
|
-
hits.push({ key: '
|
|
400
|
+
hits.push({ key: 'persona_synthesis', detail: 'Writing personas...' });
|
|
398
401
|
}
|
|
399
402
|
// Claude path: .claude/agents/sr-<name>.md
|
|
400
403
|
if (/\/agents\/sr-[^/]+\.md/.test(text)) {
|
|
@@ -407,24 +410,24 @@ function detectCheckpointFromText(text) {
|
|
|
407
410
|
hits.push({ key: 'agent_generation', detail: 'Writing agent skills...' });
|
|
408
411
|
}
|
|
409
412
|
if ((text.includes('/commands/sr/') || text.includes('/commands/specrails/')) && text.includes('.md')) {
|
|
410
|
-
hits.push({ key: '
|
|
413
|
+
hits.push({ key: 'command_generation', detail: 'Writing commands...' });
|
|
411
414
|
}
|
|
412
415
|
// Codex enrich/doctor skills (the non-rail commands) also indicate
|
|
413
|
-
//
|
|
416
|
+
// command_generation progress.
|
|
414
417
|
if (/\.codex\/skills\/(enrich|doctor)\/SKILL\.md/.test(text)) {
|
|
415
|
-
hits.push({ key: '
|
|
418
|
+
hits.push({ key: 'command_generation', detail: 'Writing codex command skills...' });
|
|
416
419
|
}
|
|
417
420
|
if (text.includes('/rules/') && text.includes('.md')) {
|
|
418
|
-
hits.push({ key: '
|
|
421
|
+
hits.push({ key: 'command_generation', detail: 'Writing conventions...' });
|
|
419
422
|
}
|
|
420
423
|
// Codex sandbox / approval policy lives inside .codex/config.toml
|
|
421
424
|
// (top-level `sandbox_mode` + `approval_policy` keys, per codex
|
|
422
425
|
// 0.128.0+). There is no separate Starlark rules file.
|
|
423
426
|
if (/\.codex\/config\.toml/.test(text)) {
|
|
424
|
-
hits.push({ key: '
|
|
427
|
+
hits.push({ key: 'agent_selection', detail: 'Writing codex sandbox config...' });
|
|
425
428
|
}
|
|
426
429
|
if (text.includes('.specrails-manifest.json') || text.includes('specrails/specrails-manifest.json')) {
|
|
427
|
-
hits.push({ key: '
|
|
430
|
+
hits.push({ key: 'command_generation' });
|
|
428
431
|
}
|
|
429
432
|
return hits;
|
|
430
433
|
}
|
|
@@ -122,7 +122,7 @@ function brokenSymlinkHint(label, command, resolvedPath) {
|
|
|
122
122
|
const where = resolvedPath ? ` at ${resolvedPath}` : '';
|
|
123
123
|
return `${label} found${where} but failed to execute — possibly a broken symlink or a stale install. Reinstall ${command} or remove the stale link${resolvedPath ? ` at ${resolvedPath}` : ''}.`;
|
|
124
124
|
}
|
|
125
|
-
const BUNDLED_TOOL_KEYS = new Set(['node', 'npm', 'npx', 'git']);
|
|
125
|
+
const BUNDLED_TOOL_KEYS = new Set(['node', 'npm', 'npx', 'git', 'uv']);
|
|
126
126
|
function isBundledTool(key) {
|
|
127
127
|
return BUNDLED_TOOL_KEYS.has(key);
|
|
128
128
|
}
|
|
@@ -138,6 +138,7 @@ function getBundledToolCandidates(runtimesBase, tool) {
|
|
|
138
138
|
npx: [path_1.default.join(runtimesBase, 'node', 'npx.cmd')],
|
|
139
139
|
git: [path_1.default.join(runtimesBase, 'git', 'cmd', 'git.exe'), path_1.default.join(runtimesBase, 'git', 'bin', 'git.exe')],
|
|
140
140
|
gh: [path_1.default.join(runtimesBase, 'gh', 'bin', 'gh.exe')],
|
|
141
|
+
uv: [path_1.default.join(runtimesBase, 'uv', 'uv.exe'), path_1.default.join(runtimesBase, 'uv', 'bin', 'uv.exe')],
|
|
141
142
|
};
|
|
142
143
|
return map[tool];
|
|
143
144
|
}
|
|
@@ -147,6 +148,7 @@ function getBundledToolCandidates(runtimesBase, tool) {
|
|
|
147
148
|
npx: [path_1.default.join(runtimesBase, 'node', 'bin', 'npx')],
|
|
148
149
|
git: [path_1.default.join(runtimesBase, 'git', 'bin', 'git')],
|
|
149
150
|
gh: [path_1.default.join(runtimesBase, 'gh', 'bin', 'gh')],
|
|
151
|
+
uv: [path_1.default.join(runtimesBase, 'uv', 'bin', 'uv'), path_1.default.join(runtimesBase, 'uv', 'uv')],
|
|
150
152
|
};
|
|
151
153
|
return map[tool];
|
|
152
154
|
}
|