specrails-desktop 2.45.0 → 2.46.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-D3SQklu5.js → ActivityFeedPage-CsHQPpvl.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-DNGxE33d.js → AgentBrowserCapture-BVcCK7x0.js} +1 -1
- package/client/dist/assets/AgentModeAnalyticsPane-C5F3m29G.js +2 -0
- package/client/dist/assets/AgentModeCodePane-C5QuKsu0.js +2 -0
- package/client/dist/assets/AgentModeJobsPane-rnD-EPMd.js +1 -0
- package/client/dist/assets/AgentsPage-DiyJWZcU.js +87 -0
- package/client/dist/assets/{AnalyticsPage-BqV6mM2O.js → AnalyticsPage-Dkmb0dnd.js} +1 -1
- package/client/dist/assets/CodePage-C0dH99-t.js +3 -0
- package/client/dist/assets/{DesktopAnalyticsPage-D6WNax3K.js → DesktopAnalyticsPage-DuTjr2n8.js} +1 -1
- package/client/dist/assets/{DocsDialog-68jKUH9f.js → DocsDialog-BVZJZHVR.js} +1 -1
- package/client/dist/assets/{DocsPage-BoGJgRpm.js → DocsPage-D6KHTv8V.js} +1 -1
- package/client/dist/assets/{ExportDropdown-B2C_XLGp.js → ExportDropdown-pWZGYCv3.js} +1 -1
- package/client/dist/assets/InteractiveJobComposer-Ck3PQxjN.js +19 -0
- package/client/dist/assets/JobDetailModal-DkoMH3ie.js +1 -0
- package/client/dist/assets/JobDetailPage-CBVivcl2.js +1 -0
- package/client/dist/assets/JobsPage-CzxEeca9.js +1 -0
- package/client/dist/assets/{LoopBuilderPage-D21GxFmL.js → LoopBuilderPage-CtmOE4-b.js} +2 -2
- package/client/dist/assets/{LoopPreviewModal-Bo3fYFxN.js → LoopPreviewModal-BYeLjQbr.js} +1 -1
- package/client/dist/assets/{LoopsPage-BcrBw1cg.js → LoopsPage-6iW7g25k.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext-Dsv2eHf_.js → MinimizedChatsContext-B6wVevPq.js} +1 -1
- package/client/dist/assets/{PluginsPage-BbyTmSLx.js → PluginsPage-Bf1z3WPF.js} +2 -2
- package/client/dist/assets/{ProjectSettingsDialog-B3lEXLv7.js → ProjectSettingsDialog-DsCRIc4v.js} +1 -1
- package/client/dist/assets/{RepositoryDeliveries-ok_Wo65C.js → RepositoryDeliveries-BQm_3TO8.js} +1 -1
- package/client/dist/assets/{RepositoryScopeSelector-DU6QwNaS.js → RepositoryScopeSelector-DnAFtjTz.js} +1 -1
- package/client/dist/assets/{ReviewPacketPage-BBCeHtgd.js → ReviewPacketPage-CaXAwtR7.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-CfNmxS6O.js → TemplatePreviewModal-9aRFpJ_J.js} +1 -1
- package/client/dist/assets/{TicketDetailModalContext-DW3eQ0I7.js → TicketDetailModalContext-B1X5glzV.js} +1 -1
- package/client/dist/assets/{Trans-BHPMWkOw.js → Trans-BwLgX6n7.js} +1 -1
- package/client/dist/assets/agentRuntime-BUagD2I_.js +1 -0
- package/client/dist/assets/agentRuntime-BkL70WtX.js +1 -0
- package/client/dist/assets/agentRuntime-CWXVqgqE.js +1 -0
- package/client/dist/assets/agentRuntime-CvNBdUAe.js +1 -0
- package/client/dist/assets/agentRuntime-D4RmeuNY.js +1 -0
- package/client/dist/assets/agentRuntime-DTidBWKg.js +1 -0
- package/client/dist/assets/agentRuntime-DhKYfyaU.js +1 -0
- package/client/dist/assets/agentRuntime-LL8_GLx9.js +1 -0
- package/client/dist/assets/format-duration-NJVPOPBG.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-Bq8iptrG.js → formatDistanceToNow-CeZv93iR.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BVFky3l_.js → getTimezoneOffsetInMilliseconds-DxAkjvJb.js} +1 -1
- package/client/dist/assets/index-Cfh9aaep.css +2 -0
- package/client/dist/assets/index-DYoyDWim.js +76 -0
- package/client/dist/assets/{jira-api-zrRdZFBB.js → jira-api-ChjVGJx_.js} +1 -1
- package/client/dist/assets/narration-4OSg0b5-.js +1 -0
- package/client/dist/assets/narration-BPMOQPFe.js +1 -0
- package/client/dist/assets/narration-BSldQfjd.js +1 -0
- package/client/dist/assets/narration-CvQdWdR5.js +1 -0
- package/client/dist/assets/narration-D-e9ssxA.js +1 -0
- package/client/dist/assets/narration-DKLjXIFI.js +1 -0
- package/client/dist/assets/narration-DnrKC3mF.js +1 -0
- package/client/dist/assets/narration-Dypjxeel.js +1 -0
- package/client/dist/assets/{project-repositories-CFRaB9UB.js → project-repositories-BvBj9XH9.js} +1 -1
- package/client/dist/assets/{provider-capabilities-B-wOHfLP.js → provider-capabilities-CXipwrXJ.js} +1 -1
- package/client/dist/assets/{settings-OOyDrZQo.js → settings-B3rWEwOx.js} +1 -1
- package/client/dist/assets/{settings-CNiERE-R.js → settings-BcVoiMyw.js} +1 -1
- package/client/dist/assets/settings-BlYvaQ7w.js +1 -0
- package/client/dist/assets/{settings-DJ2jl3hJ.js → settings-CD5uEBiL.js} +1 -1
- package/client/dist/assets/{settings-a7NIODme.js → settings-CyhS0DLJ.js} +1 -1
- package/client/dist/assets/{settings-DVRVybR_.js → settings-DCMFUGDs.js} +1 -1
- package/client/dist/assets/{settings-CrtCs5-B.js → settings-DEgcA0jd.js} +1 -1
- package/client/dist/assets/{settings-ChOCVjX_.js → settings-DmbYKyMJ.js} +1 -1
- package/client/dist/assets/{spending-lTZ-bm_y.js → spending-DNM6NAoA.js} +1 -1
- package/client/dist/assets/{useDesktop-Duv5ddAa.js → useDesktop-mxHVQOdx.js} +1 -1
- package/client/dist/assets/{useSharedWebSocket-5mj2SMKn.js → useSharedWebSocket-DnxHj8jP.js} +2 -2
- package/client/dist/index.html +19 -18
- package/docs/internals/programmatic-agent-runtime.md +19 -8
- package/package.json +1 -1
- package/server/dist/agent-mcp-config.js +5 -0
- package/server/dist/agent-operator-prompt.js +2 -2
- package/server/dist/agent-runtime-bridge.js +62 -14
- package/server/dist/agent-runtime-controls-router.js +42 -0
- package/server/dist/agent-runtime-controls.js +68 -4
- package/server/dist/agent-runtime-loader.js +6 -0
- package/server/dist/agent-runtime-metrics.js +46 -0
- package/server/dist/agent-runtime-repositories.js +37 -0
- package/server/dist/agent-runtime-settings-router.js +5 -3
- package/server/dist/agent-runtime-settings.js +125 -21
- package/server/dist/agent-runtime-settlement.js +133 -0
- package/server/dist/contract-refine-runner.js +7 -7
- package/server/dist/core-completion.js +8 -5
- package/server/dist/desktop-router.js +26 -0
- package/server/dist/job-listing.js +7 -3
- package/server/dist/loop-command-catalog.js +2 -2
- package/server/dist/loop-executors.js +5 -3
- package/server/dist/loop-run-manager.js +2 -2
- package/server/dist/mcp/tools/specs.js +7 -4
- package/server/dist/multi-repo-execution.js +1 -0
- package/server/dist/project-router-jobs.js +8 -2
- package/server/dist/project-router-tickets.js +4 -4
- package/server/dist/providers/runtime.js +6 -0
- package/server/dist/rail-isolated-launch.js +1 -1
- package/server/dist/rails-router.js +3 -1
- package/server/dist/runtime-role-prompts-router.js +25 -0
- package/server/dist/schemas/agent-runtime.schema.json +7 -2
- package/server/dist/worktree-overlay.js +32 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BUvZNoj0.js +0 -2
- package/client/dist/assets/AgentModeCodePane-YW717UvJ.js +0 -2
- package/client/dist/assets/AgentModeJobsPane-Cxg_eNmd.js +0 -1
- package/client/dist/assets/AgentsPage-B_DCByok.js +0 -87
- package/client/dist/assets/CodePage-PyZKCj1B.js +0 -3
- package/client/dist/assets/InteractiveJobComposer-l8q-r5Yy.js +0 -19
- package/client/dist/assets/JobDetailModal-C-X2GI9j.js +0 -1
- package/client/dist/assets/JobDetailPage-C6Jdm7S9.js +0 -1
- package/client/dist/assets/JobsPage-DycM6Vgp.js +0 -1
- package/client/dist/assets/agentRuntime-14faDjWM.js +0 -1
- package/client/dist/assets/agentRuntime-BIokojkJ.js +0 -1
- package/client/dist/assets/agentRuntime-Bu4xEcRw.js +0 -1
- package/client/dist/assets/agentRuntime-Cz0ASixB.js +0 -1
- package/client/dist/assets/agentRuntime-DESdGXLV.js +0 -1
- package/client/dist/assets/agentRuntime-DhX5Xy5S.js +0 -1
- package/client/dist/assets/agentRuntime-Dpbh_gLp.js +0 -1
- package/client/dist/assets/agentRuntime-jS2lplFm.js +0 -1
- package/client/dist/assets/index-CBLYwPw6.css +0 -2
- package/client/dist/assets/index-DOKftX7y.js +0 -76
- package/client/dist/assets/narration-BA8Yw5ZL.js +0 -1
- package/client/dist/assets/narration-BuMFUS7Q.js +0 -1
- package/client/dist/assets/narration-Bv_iQQG5.js +0 -1
- package/client/dist/assets/narration-C6Ost9nL.js +0 -1
- package/client/dist/assets/narration-CIbjo2pA.js +0 -1
- package/client/dist/assets/narration-CNJ2-2-a.js +0 -1
- package/client/dist/assets/narration-OzsPJCXM.js +0 -1
- package/client/dist/assets/narration-zCdgiuSU.js +0 -1
- package/client/dist/assets/settings-DzXZCb1h.js +0 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toolRepositories = toolRepositories;
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
/** Attribute only explicit paths or repository roots present in a tool action.
|
|
6
|
+
* Never carry a previous action's repository into an unrelated tool call. */
|
|
7
|
+
function toolRepositories(event, repositories, defaultCwd) {
|
|
8
|
+
const found = new Set();
|
|
9
|
+
const cwd = typeof event.cwd === 'string' && (0, node_path_1.isAbsolute)(event.cwd) ? event.cwd : defaultCwd;
|
|
10
|
+
const paths = Array.isArray(event.targetPaths) ? event.targetPaths.filter((p) => typeof p === 'string') : [];
|
|
11
|
+
if (!paths.length && typeof event.detail === 'string' && (0, node_path_1.isAbsolute)(event.detail) && !event.detail.endsWith('…'))
|
|
12
|
+
paths.push(event.detail);
|
|
13
|
+
for (const target of paths) {
|
|
14
|
+
const absolute = (0, node_path_1.resolve)(cwd, target);
|
|
15
|
+
const matches = repositories.filter(repo => {
|
|
16
|
+
if (!repo.path)
|
|
17
|
+
return false;
|
|
18
|
+
const suffix = (0, node_path_1.relative)(repo.path, absolute);
|
|
19
|
+
return suffix === '' || (!(0, node_path_1.isAbsolute)(suffix) && suffix !== '..' && !suffix.startsWith('..' + node_path_1.sep));
|
|
20
|
+
}).sort((a, b) => b.path.length - a.path.length);
|
|
21
|
+
if (matches[0])
|
|
22
|
+
found.add(matches[0].id);
|
|
23
|
+
}
|
|
24
|
+
// Shell activity may contain `cd /repo && ...` or address several repositories.
|
|
25
|
+
if (!paths.length && typeof event.detail === 'string') {
|
|
26
|
+
for (const repo of repositories) {
|
|
27
|
+
if (!repo.path)
|
|
28
|
+
continue;
|
|
29
|
+
const escaped = repo.path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
30
|
+
if (new RegExp('(?:^|[\\s\\x22\\x27=])' + escaped + '(?=$|[\\s/\\\\\\x22\\x27;&])').test(event.detail))
|
|
31
|
+
found.add(repo.id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!found.size && !paths.length && typeof event.cwd === 'string' && (0, node_path_1.isAbsolute)(event.cwd))
|
|
35
|
+
return toolRepositories({ targetPaths: [event.cwd] }, repositories, defaultCwd);
|
|
36
|
+
return repositories.filter(repo => found.has(repo.id));
|
|
37
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerAgentRuntimeSettingsRoutes = registerAgentRuntimeSettingsRoutes;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
4
5
|
const agent_runtime_loader_1 = require("./agent-runtime-loader");
|
|
5
6
|
const agent_runtime_settings_1 = require("./agent-runtime-settings");
|
|
6
7
|
const agent_runtime_verification_suggestions_1 = require("./agent-runtime-verification-suggestions");
|
|
@@ -9,8 +10,9 @@ function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
|
|
|
9
10
|
router.get('/:projectId/agent-runtime/config', (_req, res) => {
|
|
10
11
|
try {
|
|
11
12
|
const project = ctx(_req).project;
|
|
13
|
+
const configured = (0, node_fs_1.existsSync)((0, agent_runtime_settings_1.agentRuntimeConfigPath)(project));
|
|
12
14
|
const config = (0, agent_runtime_settings_1.loadAgentRuntimeConfig)(project);
|
|
13
|
-
res.json({ configured
|
|
15
|
+
res.json({ configured, config, runtimeAvailable: (0, agent_runtime_loader_1.findCoreAgentRuntimeEntry)() !== null });
|
|
14
16
|
}
|
|
15
17
|
catch (err) {
|
|
16
18
|
const validation = err instanceof agent_runtime_settings_1.AgentRuntimeConfigError;
|
|
@@ -32,13 +34,13 @@ function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
|
|
|
32
34
|
});
|
|
33
35
|
router.put('/:projectId/agent-runtime/config', async (req, res) => {
|
|
34
36
|
try {
|
|
35
|
-
|
|
37
|
+
(0, agent_runtime_settings_1.validateAgentRuntimeConfig)(req.body);
|
|
38
|
+
const config = (0, agent_runtime_settings_1.validateAgentRuntimeConfig)({ ...req.body, enabled: true, providers: (0, agent_runtime_settings_1.loadRuntimeProviders)() });
|
|
36
39
|
const runtimeAvailable = (0, agent_runtime_loader_1.findCoreAgentRuntimeEntry)() !== null;
|
|
37
40
|
if (config.enabled && !runtimeAvailable) {
|
|
38
41
|
res.status(503).json({ error: 'runtime_unavailable', message: 'Update Core to an installation with the agent runtime before enabling it' });
|
|
39
42
|
return;
|
|
40
43
|
}
|
|
41
|
-
// Disabling must remain possible even when an installed runtime is broken.
|
|
42
44
|
if (config.enabled && runtimeAvailable) {
|
|
43
45
|
try {
|
|
44
46
|
(await (0, agent_runtime_loader_1.loadCoreAgentRuntime)()).validateRuntimeConfig(config);
|
|
@@ -7,10 +7,19 @@ exports.AgentRuntimeConfigError = exports.REVIEW_THRESHOLD_FLOORS = void 0;
|
|
|
7
7
|
exports.validateAgentRuntimeConfig = validateAgentRuntimeConfig;
|
|
8
8
|
exports.defaultAgentRuntimeConfig = defaultAgentRuntimeConfig;
|
|
9
9
|
exports.agentRuntimeConfigPath = agentRuntimeConfigPath;
|
|
10
|
+
exports.runtimeProvidersPath = runtimeProvidersPath;
|
|
11
|
+
exports.loadRuntimeProviders = loadRuntimeProviders;
|
|
12
|
+
exports.validateRuntimeProviders = validateRuntimeProviders;
|
|
13
|
+
exports.saveRuntimeProviders = saveRuntimeProviders;
|
|
14
|
+
exports.loadRuntimeConfigFile = loadRuntimeConfigFile;
|
|
10
15
|
exports.loadAgentRuntimeConfig = loadAgentRuntimeConfig;
|
|
11
16
|
exports.saveAgentRuntimeConfig = saveAgentRuntimeConfig;
|
|
17
|
+
exports.validateRuntimeRolePrompts = validateRuntimeRolePrompts;
|
|
18
|
+
exports.loadRuntimeRolePrompts = loadRuntimeRolePrompts;
|
|
19
|
+
exports.saveRuntimeRolePrompts = saveRuntimeRolePrompts;
|
|
12
20
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
21
|
const node_path_1 = __importDefault(require("node:path"));
|
|
22
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
14
23
|
const node_crypto_1 = require("node:crypto");
|
|
15
24
|
const ajv_1 = __importDefault(require("ajv"));
|
|
16
25
|
const agent_runtime_schema_json_1 = __importDefault(require("./schemas/agent-runtime.schema.json"));
|
|
@@ -35,6 +44,8 @@ function validateAgentRuntimeConfig(input) {
|
|
|
35
44
|
throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${error?.instancePath || '/'}: ${error?.message ?? 'invalid value'}`);
|
|
36
45
|
}
|
|
37
46
|
const config = input;
|
|
47
|
+
if (config.rolePrompts !== undefined)
|
|
48
|
+
validateRuntimeRolePrompts(config.rolePrompts);
|
|
38
49
|
if (new Set(config.providers.map(({ id }) => id)).size !== config.providers.length)
|
|
39
50
|
throw new AgentRuntimeConfigError('Provider IDs must be unique');
|
|
40
51
|
for (const provider of config.providers) {
|
|
@@ -89,7 +100,7 @@ function validateAgentRuntimeConfig(input) {
|
|
|
89
100
|
function defaultAgentRuntimeConfig(project) {
|
|
90
101
|
const provider = CLI_PROVIDERS.includes(project.provider) ? project.provider : 'claude';
|
|
91
102
|
return {
|
|
92
|
-
schemaVersion: 1, enabled:
|
|
103
|
+
schemaVersion: 1, enabled: true,
|
|
93
104
|
providers: CLI_PROVIDERS.map((cli) => ({ id: cli, kind: 'cli', cli })),
|
|
94
105
|
agents: { architect: { provider }, developer: { provider }, reviewer: { provider } },
|
|
95
106
|
verification: [],
|
|
@@ -98,38 +109,131 @@ function defaultAgentRuntimeConfig(project) {
|
|
|
98
109
|
function agentRuntimeConfigPath(project) {
|
|
99
110
|
return node_path_1.default.join((0, workspace_resolution_1.resolveProjectExecution)(project).specrailsDir, 'agent-runtime.json');
|
|
100
111
|
}
|
|
101
|
-
/**
|
|
102
|
-
function
|
|
103
|
-
|
|
112
|
+
/** Global connections are resolved for new runs; admitted runs keep their snapshot. */
|
|
113
|
+
function runtimeProvidersPath() { return node_path_1.default.join(node_os_1.default.homedir(), '.specrails', 'runtime-providers.json'); }
|
|
114
|
+
function atomicJson(file, value) {
|
|
115
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
|
|
116
|
+
const temporary = `${file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
117
|
+
try {
|
|
118
|
+
node_fs_1.default.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
119
|
+
node_fs_1.default.renameSync(temporary, file);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
node_fs_1.default.rmSync(temporary, { force: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function loadRuntimeProviders() {
|
|
104
126
|
try {
|
|
105
|
-
|
|
127
|
+
return validateRuntimeProviders(JSON.parse(node_fs_1.default.readFileSync(runtimeProvidersPath(), 'utf8')));
|
|
106
128
|
}
|
|
107
|
-
catch (
|
|
108
|
-
if (
|
|
109
|
-
return
|
|
110
|
-
throw
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error.code === 'ENOENT')
|
|
131
|
+
return defaultAgentRuntimeConfig({ path: '' }).providers;
|
|
132
|
+
throw error;
|
|
111
133
|
}
|
|
134
|
+
}
|
|
135
|
+
function validateRuntimeProviders(input) {
|
|
136
|
+
if (!Array.isArray(input) || !input.length)
|
|
137
|
+
throw new AgentRuntimeConfigError('At least one provider connection is required');
|
|
138
|
+
const first = input[0];
|
|
139
|
+
const role = { provider: first?.id, ...(first?.kind === 'openai-compatible' ? { model: 'validation' } : {}) };
|
|
140
|
+
return validateAgentRuntimeConfig({ schemaVersion: 1, enabled: true, providers: input, agents: { architect: role, developer: role, reviewer: role }, verification: [] }).providers;
|
|
141
|
+
}
|
|
142
|
+
function saveRuntimeProviders(input) {
|
|
143
|
+
const providers = validateRuntimeProviders(input);
|
|
144
|
+
atomicJson(runtimeProvidersPath(), providers);
|
|
145
|
+
return providers;
|
|
146
|
+
}
|
|
147
|
+
/** Migrate embedded connections once. Conflicting IDs receive a stable ID so
|
|
148
|
+
* two projects with different endpoints never silently share a connection. */
|
|
149
|
+
function loadRuntimeConfigFile(file, fallbackProvider) {
|
|
112
150
|
let parsed;
|
|
151
|
+
let text;
|
|
113
152
|
try {
|
|
114
|
-
|
|
153
|
+
text = node_fs_1.default.readFileSync(file, 'utf8');
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error.code !== 'ENOENT')
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
if (text !== undefined) {
|
|
160
|
+
try {
|
|
161
|
+
parsed = JSON.parse(text);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
throw new AgentRuntimeConfigError('Saved runtime configuration is not valid JSON');
|
|
165
|
+
}
|
|
166
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
167
|
+
throw new AgentRuntimeConfigError('Saved runtime configuration must be an object');
|
|
115
168
|
}
|
|
116
|
-
|
|
117
|
-
|
|
169
|
+
const providers = loadRuntimeProviders();
|
|
170
|
+
if (!parsed) {
|
|
171
|
+
const config = defaultAgentRuntimeConfig({ path: '', provider: fallbackProvider });
|
|
172
|
+
const selected = providers.find(item => item.id === config.agents.architect.provider) ?? providers.find(item => item.kind === 'cli');
|
|
173
|
+
if (!selected || selected.kind !== 'cli')
|
|
174
|
+
throw new AgentRuntimeConfigError('Configure project role models before using API-only connections');
|
|
175
|
+
for (const role of ROLES)
|
|
176
|
+
config.agents[role].provider = selected.id;
|
|
177
|
+
return validateAgentRuntimeConfig({ ...config, providers });
|
|
178
|
+
}
|
|
179
|
+
if (Array.isArray(parsed.providers)) {
|
|
180
|
+
const legacy = validateAgentRuntimeConfig(parsed);
|
|
181
|
+
for (const original of legacy.providers) {
|
|
182
|
+
let connection = providers.find(item => item.id === original.id);
|
|
183
|
+
if (connection && JSON.stringify(connection) !== JSON.stringify(original)) {
|
|
184
|
+
const id = `${original.id.slice(0, 117)}-${(0, node_crypto_1.createHash)('sha256').update(JSON.stringify(original)).digest('hex').slice(0, 10)}`;
|
|
185
|
+
for (const role of ROLES)
|
|
186
|
+
if (legacy.agents[role].provider === original.id)
|
|
187
|
+
legacy.agents[role].provider = id;
|
|
188
|
+
connection = providers.find(item => item.id === id);
|
|
189
|
+
if (!connection)
|
|
190
|
+
providers.push({ ...original, id });
|
|
191
|
+
}
|
|
192
|
+
else if (!connection)
|
|
193
|
+
providers.push(original);
|
|
194
|
+
}
|
|
195
|
+
saveRuntimeProviders(providers);
|
|
196
|
+
const { providers: _embedded, enabled: _enabled, ...settings } = legacy;
|
|
197
|
+
atomicJson(file, settings);
|
|
198
|
+
parsed = settings;
|
|
118
199
|
}
|
|
119
|
-
return validateAgentRuntimeConfig(parsed);
|
|
200
|
+
return validateAgentRuntimeConfig({ ...parsed, enabled: true, providers });
|
|
201
|
+
}
|
|
202
|
+
/** Missing settings use the programmatic engine. Malformed files fail closed. */
|
|
203
|
+
function loadAgentRuntimeConfig(project) {
|
|
204
|
+
return loadRuntimeConfigFile(agentRuntimeConfigPath(project), project.provider);
|
|
120
205
|
}
|
|
121
206
|
/** Atomic replacement works on macOS and Windows, with no partial file on errors. */
|
|
122
207
|
function saveAgentRuntimeConfig(project, input) {
|
|
123
|
-
const config = validateAgentRuntimeConfig(input);
|
|
208
|
+
const config = validateAgentRuntimeConfig({ ...input, enabled: true, providers: loadRuntimeProviders() });
|
|
124
209
|
const file = agentRuntimeConfigPath(project);
|
|
125
|
-
|
|
126
|
-
|
|
210
|
+
const { providers: _providers, enabled: _enabled, ...settings } = config;
|
|
211
|
+
atomicJson(file, settings);
|
|
212
|
+
return config;
|
|
213
|
+
}
|
|
214
|
+
function validateRuntimeRolePrompts(input) {
|
|
215
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
216
|
+
throw new AgentRuntimeConfigError('Role prompts must be an object');
|
|
217
|
+
const prompts = {};
|
|
218
|
+
for (const [role, text] of Object.entries(input)) {
|
|
219
|
+
if (!ROLES.includes(role) || typeof text !== 'string' || !text.trim() || text.length > 20000 || text.includes('\0'))
|
|
220
|
+
throw new AgentRuntimeConfigError('Role prompts must use architect, developer or reviewer with 1–20000 nonblank characters and no null bytes');
|
|
221
|
+
prompts[role] = text;
|
|
222
|
+
}
|
|
223
|
+
return prompts;
|
|
224
|
+
}
|
|
225
|
+
function loadRuntimeRolePrompts() {
|
|
127
226
|
try {
|
|
128
|
-
node_fs_1.default.
|
|
129
|
-
node_fs_1.default.renameSync(temporary, file);
|
|
227
|
+
return validateRuntimeRolePrompts(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(node_os_1.default.homedir(), '.specrails', 'runtime-role-prompts.json'), 'utf8')));
|
|
130
228
|
}
|
|
131
|
-
|
|
132
|
-
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (error.code === 'ENOENT')
|
|
231
|
+
return {};
|
|
232
|
+
throw error;
|
|
133
233
|
}
|
|
134
|
-
|
|
234
|
+
}
|
|
235
|
+
function saveRuntimeRolePrompts(input) {
|
|
236
|
+
const prompts = validateRuntimeRolePrompts(input);
|
|
237
|
+
atomicJson(node_path_1.default.join(node_os_1.default.homedir(), '.specrails', 'runtime-role-prompts.json'), prompts);
|
|
238
|
+
return prompts;
|
|
135
239
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.settleRuntimeContinuation = settleRuntimeContinuation;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const db_1 = require("./db");
|
|
10
|
+
const core_execution_1 = require("./core-execution");
|
|
11
|
+
const core_completion_1 = require("./core-completion");
|
|
12
|
+
const worktree_manager_1 = require("./worktree-manager");
|
|
13
|
+
const rail_pr_store_1 = require("./rail-pr-store");
|
|
14
|
+
const multi_repo_execution_1 = require("./multi-repo-execution");
|
|
15
|
+
/** Complete the host's local settlement after Core recovery. Never pushes or merges. */
|
|
16
|
+
async function settleRuntimeContinuation(input, dependencies = { verify: core_execution_1.checkCoreCompletion, git: worktree_manager_1.defaultGitRunner, commit: worktree_manager_1.commitWorktreeAndVerify }) {
|
|
17
|
+
const { db, runId } = input;
|
|
18
|
+
const verified = dependencies.verify(input.contextPath, input.cwd, input.env, runId);
|
|
19
|
+
if (!verified.valid)
|
|
20
|
+
throw new Error(verified.reason ?? 'Core completion is not verified');
|
|
21
|
+
const frozen = JSON.parse(node_fs_1.default.readFileSync(input.contextPath, 'utf8'));
|
|
22
|
+
if (frozen.runId !== runId)
|
|
23
|
+
throw new Error('Runtime identity changed');
|
|
24
|
+
const all = db.prepare('SELECT * FROM rail_pr_deliveries').all().filter(row => (0, rail_pr_store_1.toPrDeliverySnapshot)(row).runIds.includes(runId));
|
|
25
|
+
const parents = new Set(all.map(row => row.parent_delivery_id).filter((id) => !!id));
|
|
26
|
+
const rows = all.filter(row => !parents.has(row.id) && row.decision === 'implementation_failed');
|
|
27
|
+
if (all.some(row => parents.has(row.id) && ['discarded', 'superseded', 'merged', 'completed'].includes(row.decision)))
|
|
28
|
+
throw new Error('Delivery is no longer available for recovery');
|
|
29
|
+
const token = (0, node_crypto_1.randomUUID)(), claimed = [];
|
|
30
|
+
const updates = [];
|
|
31
|
+
const gitText = async (args, cwd) => {
|
|
32
|
+
const result = await dependencies.git.run(args, cwd);
|
|
33
|
+
if (result.code !== 0)
|
|
34
|
+
throw new Error(`Cannot inspect recovered worktree: ${result.stderr}`);
|
|
35
|
+
return result.stdout.trim();
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
// Claim the entire group before Git effects; a concurrent discard must win or lose atomically.
|
|
39
|
+
db.transaction(() => {
|
|
40
|
+
for (const row of all.filter(row => parents.has(row.id) || rows.includes(row))) {
|
|
41
|
+
if (!(0, rail_pr_store_1.claimPrDeliveryOperation)(db, row.id, row.decision, 'recover-and-retry', token))
|
|
42
|
+
throw new Error('Delivery is busy; retry settlement after its current operation');
|
|
43
|
+
claimed.push(row);
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
if (row.pr_url)
|
|
48
|
+
throw new Error('An existing PR requires explicit delivery recovery');
|
|
49
|
+
if (!(0, rail_pr_store_1.toPrDeliverySnapshot)(row).branches.some(unit => unit.runId === runId))
|
|
50
|
+
throw new Error('Delivery has no branch recorded for this runtime');
|
|
51
|
+
for (const unit of (0, rail_pr_store_1.toPrDeliverySnapshot)(row).branches.filter(unit => unit.runId === runId)) {
|
|
52
|
+
if (!unit.worktreePath || !frozen.repositories.some(repo => repo.path === unit.worktreePath) || node_fs_1.default.realpathSync(unit.worktreePath) !== unit.worktreePath)
|
|
53
|
+
throw new Error('Recovered worktree no longer matches the frozen runtime scope');
|
|
54
|
+
if (await gitText(['branch', '--show-current'], unit.worktreePath) !== unit.branch)
|
|
55
|
+
throw new Error('Recovered worktree branch changed');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const row of rows) {
|
|
59
|
+
const branches = (0, rail_pr_store_1.toPrDeliverySnapshot)(row).branches;
|
|
60
|
+
for (const unit of branches.filter(unit => unit.runId === runId)) {
|
|
61
|
+
const result = await dependencies.commit(dependencies.git, unit.worktreePath, `specrails: ticket-${unit.ticketId} recovered (run ${runId})`, unit.overlayExcludes ?? []);
|
|
62
|
+
if (!result.clean || result.error)
|
|
63
|
+
throw new Error(result.error ?? 'Recovered worktree still contains uncommitted changes');
|
|
64
|
+
const sha = await gitText(['rev-parse', 'HEAD'], unit.worktreePath);
|
|
65
|
+
if (!/^[a-f0-9]{40,64}$/i.test(sha))
|
|
66
|
+
throw new Error('Invalid recovered commit');
|
|
67
|
+
Object.assign(unit, { succeeded: true, implementationOutcome: 'succeeded', deliveryOutcome: sha !== unit.initialSha ? 'ready' : 'no_changes', finalSha: sha, changed: sha !== unit.initialSha, failureCode: null });
|
|
68
|
+
}
|
|
69
|
+
updates.push({ row, branches });
|
|
70
|
+
}
|
|
71
|
+
const finalCheck = dependencies.verify(input.contextPath, input.cwd, input.env, runId);
|
|
72
|
+
if (!finalCheck.valid)
|
|
73
|
+
throw new Error(finalCheck.reason ?? 'Candidate changed during settlement');
|
|
74
|
+
const core = await (0, core_completion_1.readCoreCompletion)(input);
|
|
75
|
+
db.transaction(() => {
|
|
76
|
+
for (const { row, branches } of updates) {
|
|
77
|
+
const ready = branches.every(unit => unit.succeeded);
|
|
78
|
+
const noChanges = ready && branches.every(unit => unit.changed === false);
|
|
79
|
+
if (!(0, rail_pr_store_1.transitionClaimedDecision)(db, row.id, row.decision, noChanges ? 'no_changes' : 'on_review', token, {
|
|
80
|
+
branches, implementationOutcome: ready ? 'succeeded' : 'partially_succeeded', deliveryOutcome: noChanges ? 'no_changes' : 'ready',
|
|
81
|
+
statusCode: noChanges ? 'no_changes' : ready ? 'ready_for_review' : 'partial_success', statusDetail: null,
|
|
82
|
+
...(branches.length === 1 ? { deliverySha: branches[0].finalSha } : {}),
|
|
83
|
+
}))
|
|
84
|
+
throw new Error('Delivery changed during runtime settlement');
|
|
85
|
+
for (const unit of branches.filter(unit => unit.runId === runId))
|
|
86
|
+
db.prepare("UPDATE rail_worktrees SET merge_state = 'built', updated_at = datetime('now') WHERE run_id = ? AND worktree_path = ?").run(runId, unit.worktreePath);
|
|
87
|
+
}
|
|
88
|
+
for (const parentId of parents)
|
|
89
|
+
(0, multi_repo_execution_1.refreshRepositoryDeliveryGroup)(db, parentId);
|
|
90
|
+
db.prepare("UPDATE jobs SET status = 'completed', exit_code = 0 WHERE id = ?").run(runId);
|
|
91
|
+
db.prepare("UPDATE loop_runs SET status = 'completed', final_outcome = 'success' WHERE id = ?").run(runId);
|
|
92
|
+
let seq = db.prepare('SELECT COALESCE(MAX(seq), -1) + 1 AS seq FROM events WHERE job_id = ?').get(runId).seq;
|
|
93
|
+
const lastStep = db.prepare("SELECT payload FROM events WHERE job_id = ? AND event_type = 'loop_step' ORDER BY seq DESC LIMIT 1").get(runId);
|
|
94
|
+
const lastEnd = db.prepare("SELECT payload FROM events WHERE job_id = ? AND event_type = 'loop_step_end' ORDER BY seq DESC LIMIT 1").get(runId);
|
|
95
|
+
if (lastStep && lastEnd) {
|
|
96
|
+
const step = JSON.parse(lastStep.payload), end = JSON.parse(lastEnd.payload);
|
|
97
|
+
// Supersede only this implementation step, never an unrelated shell/decider failure.
|
|
98
|
+
if (step.kind === 'ai-step' && step.template === '{{cmd:implement}}' && end.index === step.index && end.nodeId === step.nodeId && end.status === 'failed') {
|
|
99
|
+
const job = db.prepare('SELECT duration_ms FROM jobs WHERE id = ?').get(runId);
|
|
100
|
+
(0, db_1.appendEvent)(db, runId, seq++, { event_type: 'loop_step_end', source: 'stdout', payload: JSON.stringify({ index: step.index, nodeId: step.nodeId, status: 'ok', exitCode: 0, durationMs: job.duration_ms ?? end.durationMs, recovered: true }) });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const oldCompletion = db.prepare("SELECT payload FROM events WHERE job_id = ? AND event_type = 'loop_completion' ORDER BY seq DESC LIMIT 1").get(runId);
|
|
104
|
+
let previous = {};
|
|
105
|
+
try {
|
|
106
|
+
previous = oldCompletion ? JSON.parse(oldCompletion.payload) : {};
|
|
107
|
+
}
|
|
108
|
+
catch { /* old event format */ }
|
|
109
|
+
const usage = db.prepare('SELECT total_cost_usd,num_turns FROM jobs WHERE id = ?').get(runId);
|
|
110
|
+
(0, db_1.appendEvent)(db, runId, seq, { event_type: 'loop_completion', source: 'stdout', payload: JSON.stringify({ ...previous, version: 1, execution: 'success', core, turns: usage.num_turns, costUsd: usage.total_cost_usd, recovered: true }) });
|
|
111
|
+
(0, db_1.appendEvent)(db, runId, seq + 1, { event_type: 'log', source: 'stdout', payload: JSON.stringify({ line: '[runtime] Recovery verified and local delivery prepared for review. The original failed attempt remains in history. No PR was published.' }) });
|
|
112
|
+
})();
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
for (const row of claimed)
|
|
116
|
+
(0, rail_pr_store_1.releasePrDeliveryOperation)(db, row.id, token);
|
|
117
|
+
}
|
|
118
|
+
for (const id of new Set([...updates.map(update => update.row.id), ...parents])) {
|
|
119
|
+
const row = (0, rail_pr_store_1.getPrDelivery)(db, id);
|
|
120
|
+
try {
|
|
121
|
+
if (row)
|
|
122
|
+
input.broadcast?.((0, rail_pr_store_1.toRailPrStateMessage)(input.projectId, (0, rail_pr_store_1.toPrDeliverySnapshot)(row)));
|
|
123
|
+
}
|
|
124
|
+
catch { /* durable state remains authoritative */ }
|
|
125
|
+
}
|
|
126
|
+
const events = db.prepare('SELECT seq,event_type,source,payload,timestamp FROM events WHERE job_id = ? ORDER BY seq DESC LIMIT 3').all(runId);
|
|
127
|
+
for (const event of events.reverse()) {
|
|
128
|
+
try {
|
|
129
|
+
input.broadcast?.({ type: 'event', projectId: input.projectId, jobId: runId, ...event });
|
|
130
|
+
}
|
|
131
|
+
catch { /* replay remains available */ }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Contract Refine Runner
|
|
4
4
|
*
|
|
5
|
-
* Standalone runner that spawns a single
|
|
5
|
+
* Standalone runner that spawns a single provider turn to produce the
|
|
6
6
|
* Contract Layer for a just-committed Explore Spec ticket. Lives outside the
|
|
7
7
|
* ChatManager lifecycle for now (design.md D3 — "thin sibling helper" option):
|
|
8
8
|
* the refine is fire-and-forget with a 60 s budget per invocation and no
|
|
@@ -82,12 +82,12 @@ function buildRefineArgs(adapter, model, systemPrompt, sessionId) {
|
|
|
82
82
|
model,
|
|
83
83
|
systemPrompt: foldForResume ? undefined : systemPrompt,
|
|
84
84
|
sessionId,
|
|
85
|
-
toolPolicy: 'none',
|
|
85
|
+
toolPolicy: (0, runtime_1.pureOutputToolPolicy)(adapter) ?? (0, runtime_1.requireToolPolicy)(adapter, 'none'),
|
|
86
86
|
maxTurns: 1,
|
|
87
87
|
};
|
|
88
88
|
return { args: adapter.buildArgs(action, options), options };
|
|
89
89
|
}
|
|
90
|
-
/** Build the pure-output, no-resume invocation used by Quick Refine and by the
|
|
90
|
+
/** Build the restricted pure-output, no-resume invocation used by Quick Refine and by the
|
|
91
91
|
* one-time compatibility recovery for cwd-scoped Explore sessions. */
|
|
92
92
|
function buildFreshRefineArgs(adapter, model, title, description) {
|
|
93
93
|
const systemPrompt = [
|
|
@@ -105,7 +105,7 @@ function buildFreshRefineArgs(adapter, model, title, description) {
|
|
|
105
105
|
prompt: explore_contract_refine_1.CONTRACT_MARKER_USER_MESSAGE,
|
|
106
106
|
model,
|
|
107
107
|
systemPrompt,
|
|
108
|
-
toolPolicy: 'none',
|
|
108
|
+
toolPolicy: (0, runtime_1.pureOutputToolPolicy)(adapter) ?? (0, runtime_1.requireToolPolicy)(adapter, 'none'),
|
|
109
109
|
maxTurns: 1,
|
|
110
110
|
};
|
|
111
111
|
return { args: adapter.buildArgs('spec-gen', options), options };
|
|
@@ -362,8 +362,8 @@ async function runContractRefine(deps, conversationId, ticketId) {
|
|
|
362
362
|
return { ok: false, reason: 'not-explore', ticketId, conversationId };
|
|
363
363
|
}
|
|
364
364
|
const adapter = (0, providers_1.getAdapter)(conversation.provider ?? deps.providerId ?? 'claude');
|
|
365
|
-
if (
|
|
366
|
-
console.log(`[contract-refine-runner] skip: provider '${adapter.id}' does not support
|
|
365
|
+
if (!(0, runtime_1.supportsContractRefine)(adapter)) {
|
|
366
|
+
console.log(`[contract-refine-runner] skip: provider '${adapter.id}' does not support contract refinement`);
|
|
367
367
|
return { ok: false, reason: 'provider-unsupported', ticketId, conversationId };
|
|
368
368
|
}
|
|
369
369
|
// Per-conversation gating: contractRefine on the conversation's stored
|
|
@@ -602,7 +602,7 @@ async function runContractRefineForQuick(deps, ticketId, generatedTitle, generat
|
|
|
602
602
|
}
|
|
603
603
|
const admission = (0, process_admission_1.captureProcessAdmission)(deps.projectId);
|
|
604
604
|
const adapter = (0, providers_1.getAdapter)(deps.providerId ?? 'claude');
|
|
605
|
-
if (
|
|
605
|
+
if (!(0, runtime_1.supportsContractRefine)(adapter)) {
|
|
606
606
|
return { ok: false, reason: 'provider-unsupported', ticketId, conversationId: '' };
|
|
607
607
|
}
|
|
608
608
|
const resolvedModel = model ?? adapter.defaultModel();
|
|
@@ -6,7 +6,8 @@ const node_child_process_1 = require("node:child_process");
|
|
|
6
6
|
const node_fs_1 = require("node:fs");
|
|
7
7
|
const node_path_1 = require("node:path");
|
|
8
8
|
const node_util_1 = require("node:util");
|
|
9
|
-
const
|
|
9
|
+
const core_node_runtime_1 = require("./core-node-runtime");
|
|
10
|
+
const agent_runtime_loader_1 = require("./agent-runtime-loader");
|
|
10
11
|
const obj = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
11
12
|
const strings = (value) => Array.isArray(value) && value.every(item => typeof item === 'string');
|
|
12
13
|
const count = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
|
|
@@ -43,14 +44,16 @@ function parseCoreCompletion(value, runId) {
|
|
|
43
44
|
}), };
|
|
44
45
|
}
|
|
45
46
|
async function readCoreCompletion(input) {
|
|
46
|
-
const
|
|
47
|
-
|
|
47
|
+
const programmatic = (0, node_fs_1.existsSync)((0, node_path_1.join)((0, node_path_1.dirname)(input.contextPath), 'agent-runtime-request.json'));
|
|
48
|
+
const runtime = programmatic ? (0, agent_runtime_loader_1.findCoreAgentRuntimeCli)() : (0, node_path_1.join)(input.cwd, '.specrails', 'runtime', 'pipeline.mjs');
|
|
49
|
+
if (!runtime || !(0, node_fs_1.existsSync)(runtime))
|
|
48
50
|
return null;
|
|
49
51
|
try {
|
|
50
|
-
const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)((0,
|
|
52
|
+
const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)((0, core_node_runtime_1.resolveCoreNodeRuntime)(), [runtime, 'status', '--context', input.contextPath], {
|
|
51
53
|
cwd: input.cwd, env: input.env, encoding: 'utf8', timeout: 15_000, maxBuffer: 4 * 1024 * 1024, windowsHide: true,
|
|
52
54
|
});
|
|
53
|
-
|
|
55
|
+
const payload = JSON.parse(stdout);
|
|
56
|
+
return parseCoreCompletion(programmatic ? payload.pipeline : payload, input.runId);
|
|
54
57
|
}
|
|
55
58
|
catch {
|
|
56
59
|
return null;
|
|
@@ -4,7 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createDesktopRouter = createDesktopRouter;
|
|
7
|
+
const runtime_role_prompts_router_1 = require("./runtime-role-prompts-router");
|
|
7
8
|
const express_1 = require("express");
|
|
9
|
+
const agent_runtime_settings_1 = require("./agent-runtime-settings");
|
|
8
10
|
const crypto_1 = require("crypto");
|
|
9
11
|
const path_1 = __importDefault(require("path"));
|
|
10
12
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -1227,6 +1229,30 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
1227
1229
|
const monthlyBudgetUsd = Number.isFinite(parsed) && parsed >= 0 ? parsed : 5.0;
|
|
1228
1230
|
res.json({ language, monthlyBudgetUsd });
|
|
1229
1231
|
});
|
|
1232
|
+
(0, runtime_role_prompts_router_1.registerRuntimeRolePromptRoutes)(router);
|
|
1233
|
+
router.get('/runtime-providers', (_req, res) => {
|
|
1234
|
+
try {
|
|
1235
|
+
for (const project of (0, desktop_db_1.listProjects)(registry.desktopDb))
|
|
1236
|
+
(0, agent_runtime_settings_1.loadAgentRuntimeConfig)(project);
|
|
1237
|
+
res.json({ providers: (0, agent_runtime_settings_1.loadRuntimeProviders)() });
|
|
1238
|
+
}
|
|
1239
|
+
catch (error) {
|
|
1240
|
+
res.status(422).json({ message: error.message });
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
router.put('/runtime-providers', (req, res) => {
|
|
1244
|
+
try {
|
|
1245
|
+
const projects = (0, desktop_db_1.listProjects)(registry.desktopDb).map(project => (0, agent_runtime_settings_1.loadAgentRuntimeConfig)(project));
|
|
1246
|
+
const providers = (0, agent_runtime_settings_1.validateRuntimeProviders)(req.body.providers);
|
|
1247
|
+
for (const config of projects)
|
|
1248
|
+
if (config)
|
|
1249
|
+
(0, agent_runtime_settings_1.validateAgentRuntimeConfig)({ ...config, providers });
|
|
1250
|
+
res.json({ providers: (0, agent_runtime_settings_1.saveRuntimeProviders)(providers) });
|
|
1251
|
+
}
|
|
1252
|
+
catch (error) {
|
|
1253
|
+
res.status(error instanceof agent_runtime_settings_1.AgentRuntimeConfigError ? 400 : 500).json({ message: error.message });
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1230
1256
|
// ─── Specrails Agents defaults (global per-provider agent model/effort) ───
|
|
1231
1257
|
// App-level layer read AT SPAWN TIME by QueueManager / rails-router / loop
|
|
1232
1258
|
// executors — a change applies to the next run with zero restart.
|
|
@@ -37,7 +37,9 @@ function historyConditions(opts) {
|
|
|
37
37
|
const conditions = ["jobs.status <> 'queued'"];
|
|
38
38
|
const params = [];
|
|
39
39
|
if (opts.status) {
|
|
40
|
-
|
|
40
|
+
const active = opts.activeRuntimeIds ?? [];
|
|
41
|
+
conditions.push(active.length ? `CASE WHEN jobs.id IN (${active.map(() => '?').join(',')}) THEN 'running' ELSE jobs.status END = ?` : 'jobs.status = ?');
|
|
42
|
+
params.push(...active);
|
|
41
43
|
params.push(opts.status);
|
|
42
44
|
}
|
|
43
45
|
if (opts.from) {
|
|
@@ -118,14 +120,16 @@ function listUnifiedJobs(db, opts) {
|
|
|
118
120
|
const remaining = limit - queuedRows.length;
|
|
119
121
|
const historyOffset = Math.max(0, offset - queuedTotal);
|
|
120
122
|
if (remaining > 0 && historyOffset < historyTotal) {
|
|
123
|
+
const active = opts.activeRuntimeIds ?? [];
|
|
124
|
+
const activeOrder = active.length ? `CASE WHEN jobs.id IN (${active.map(() => '?').join(',')}) THEN 0 ELSE 1 END, ` : '';
|
|
121
125
|
historyRows = db.prepare(`
|
|
122
126
|
SELECT jobs.*, jp.profile_name AS profile_name, NULL AS enqueued_at
|
|
123
127
|
FROM jobs
|
|
124
128
|
LEFT JOIN job_profiles jp ON jp.job_id = jobs.id
|
|
125
129
|
${history.sql}
|
|
126
|
-
ORDER BY jobs.started_at DESC, jobs.id ASC
|
|
130
|
+
ORDER BY ${activeOrder}jobs.started_at DESC, jobs.id ASC
|
|
127
131
|
LIMIT ? OFFSET ?
|
|
128
|
-
`).all(...history.params, remaining, historyOffset);
|
|
132
|
+
`).all(...history.params, ...active, remaining, historyOffset);
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
return { jobs: [...queuedRows, ...historyRows], total: queuedTotal + historyTotal };
|
|
@@ -68,14 +68,14 @@ exports.LOOP_COMMANDS = [
|
|
|
68
68
|
{
|
|
69
69
|
name: 'implement',
|
|
70
70
|
label: 'implement',
|
|
71
|
-
description: "Run
|
|
71
|
+
description: "Run Core’s programmatic agent workflow (architect → developer → verification → reviewer → archive) over the rail's tickets.",
|
|
72
72
|
coreCommand: 'implement',
|
|
73
73
|
ticketScope: 'all',
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
name: 'batch',
|
|
77
77
|
label: 'batch',
|
|
78
|
-
description: "Run
|
|
78
|
+
description: "Run all selected tickets as one frozen candidate through Core’s programmatic agent workflow.",
|
|
79
79
|
coreCommand: 'batch-implement',
|
|
80
80
|
ticketScope: 'all',
|
|
81
81
|
},
|
|
@@ -234,17 +234,19 @@ function createLoopExecutors(opts = {}) {
|
|
|
234
234
|
// agent self-heal on Windows.
|
|
235
235
|
const baseEnv = resolveEnv();
|
|
236
236
|
const existingContext = coreRun ? runtimeContextPath(cwd, coreRun.runId, baseEnv) : undefined;
|
|
237
|
-
const programmatic = coreRun && existingContext && (
|
|
237
|
+
const programmatic = coreRun && existingContext && (coreRun.implementation || (coreRun.verificationStep && existsRuntimeRequest(existingContext)));
|
|
238
238
|
const baseStepEnv = programmatic ? programmaticStepEnv(baseEnv, repoDir, executionManifest) : withProfileEnv(aiStepEnv(baseEnv, repoDir, executionManifest), provider, profileName);
|
|
239
239
|
const core = coreRun ? (0, core_execution_1.prepareCoreExecution)({ run: coreRun, cwd, repoDir, manifest: executionManifest, env: baseStepEnv }) : undefined;
|
|
240
240
|
const stepEnv = core?.env ?? baseStepEnv;
|
|
241
|
-
if (coreRun?.implementation && core
|
|
241
|
+
if (coreRun?.implementation && core) {
|
|
242
|
+
completionContexts.set(coreRun.runId, { cwd, contextPath: core.contextPath, env: stepEnv, runId: coreRun.runId });
|
|
242
243
|
const admitted = existsRuntimeRequest(core.contextPath);
|
|
243
244
|
// Repeated implementation nodes may validate completed work, but may
|
|
244
245
|
// never silently replay an interrupted mutating phase.
|
|
245
246
|
return (0, agent_runtime_bridge_1.runAgentRuntimeInvocation)({
|
|
246
247
|
contextPath: core.contextPath, cwd, env: { ...programmaticStepEnv(resolveEnv(), repoDir, executionManifest), SPECRAILS_EXECUTION_CONTEXT: core.contextPath }, configPath: runtimeConfigPath(cwd),
|
|
247
248
|
change: (0, agent_runtime_bridge_1.runtimeChangeName)(coreRun.runId), resume: admitted,
|
|
249
|
+
defaultProvider: provider, selectedModel: model,
|
|
248
250
|
onLine, onRawLine, onSpawn, timeoutMs: aiStepTimeoutMs,
|
|
249
251
|
});
|
|
250
252
|
}
|
|
@@ -464,7 +466,7 @@ function createLoopExecutors(opts = {}) {
|
|
|
464
466
|
const env = resolveEnv();
|
|
465
467
|
const root = env.SPECRAILS_TICKETS_PATH ? (0, node_path_1.dirname)((0, node_path_1.dirname)(env.SPECRAILS_TICKETS_PATH)) : cwd;
|
|
466
468
|
const contextPath = (0, node_path_1.join)(root, '.specrails', 'pipeline', coreRun.runId, 'desktop-context.json');
|
|
467
|
-
if (
|
|
469
|
+
if (coreRun.implementation || existsRuntimeRequest(contextPath))
|
|
468
470
|
return null;
|
|
469
471
|
}
|
|
470
472
|
if (!(0, feature_flags_1.isInteractiveJobsEnabled)())
|
|
@@ -1099,7 +1099,7 @@ class LoopRunManager {
|
|
|
1099
1099
|
// were noise once the step explorer landed; the log now opens on real
|
|
1100
1100
|
// output). `template` is omitted when rendering changed nothing (a
|
|
1101
1101
|
// plain free-text prompt would just duplicate `command`).
|
|
1102
|
-
const stepTitle = `🤖 ${nodeLabel || 'AI Step'} (${nodeProvider}/${nodeModel}${nodeEffort ? `, effort: ${nodeEffort}` : ''})`;
|
|
1102
|
+
const stepTitle = requiresCoreCompletion ? '🤖 Implementation (agent runtime)' : `🤖 ${nodeLabel || 'AI Step'} (${nodeProvider}/${nodeModel}${nodeEffort ? `, effort: ${nodeEffort}` : ''})`;
|
|
1103
1103
|
const stepDetail = {
|
|
1104
1104
|
...(rawTemplate && rawTemplate !== base ? { template: capText(rawTemplate, STEP_TEMPLATE_CAP) } : {}),
|
|
1105
1105
|
...(base ? { command: capText(base, STEP_COMMAND_CAP) } : {}),
|
|
@@ -1691,7 +1691,7 @@ class LoopRunManager {
|
|
|
1691
1691
|
// `≥` when any cost-bearing step ended unpriced (timeout/crash) — the figure
|
|
1692
1692
|
// is a lower bound, not exact. Providers without usage telemetry get an
|
|
1693
1693
|
// explicit unavailable marker, never a fabricated "$0.0000".
|
|
1694
|
-
logLine(`\n■ Loop execution finished: ${outcome} —
|
|
1694
|
+
logLine(`\n■ Loop execution finished: ${outcome} — ` +
|
|
1695
1695
|
(costAvailable
|
|
1696
1696
|
? `${costUncertain ? '≥ ' : ''}$${totalCost.toFixed(4)}`
|
|
1697
1697
|
: 'usage/cost unavailable'));
|