specrails-desktop 2.44.2 → 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/README.md +2 -0
- package/client/dist/assets/{ActivityFeedPage-CL1BUXoj.js → ActivityFeedPage-CsHQPpvl.js} +1 -1
- package/client/dist/assets/AgentBrowserCapture-BVcCK7x0.js +1 -0
- 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-DbLezyI6.js → AgentsPage-DiyJWZcU.js} +3 -3
- package/client/dist/assets/{AnalyticsPage-DTsg-N5z.js → AnalyticsPage-Dkmb0dnd.js} +1 -1
- package/client/dist/assets/CodePage-C0dH99-t.js +3 -0
- package/client/dist/assets/{DesktopAnalyticsPage-Y0Z1ryQr.js → DesktopAnalyticsPage-DuTjr2n8.js} +1 -1
- package/client/dist/assets/{DocsDialog-KeCOxvGH.js → DocsDialog-BVZJZHVR.js} +1 -1
- package/client/dist/assets/{DocsPage-Bt1PgYPO.js → DocsPage-D6KHTv8V.js} +1 -1
- package/client/dist/assets/{ExportDropdown-DczgU4at.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-CqC4hbXb.js → LoopBuilderPage-CtmOE4-b.js} +2 -2
- package/client/dist/assets/{LoopPreviewModal-T0zuFv6y.js → LoopPreviewModal-BYeLjQbr.js} +1 -1
- package/client/dist/assets/{LoopsPage-DrQGcZv2.js → LoopsPage-6iW7g25k.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext-C843Y13h.js → MinimizedChatsContext-B6wVevPq.js} +1 -1
- package/client/dist/assets/PluginsPage-Bf1z3WPF.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-DsCRIc4v.js +1 -0
- package/client/dist/assets/{RepositoryDeliveries-bgIHE8ep.js → RepositoryDeliveries-BQm_3TO8.js} +1 -1
- package/client/dist/assets/{RepositoryScopeSelector-CZ_U_R3U.js → RepositoryScopeSelector-DnAFtjTz.js} +1 -1
- package/client/dist/assets/{ReviewPacketPage-DbKO6pT8.js → ReviewPacketPage-CaXAwtR7.js} +1 -1
- package/client/dist/assets/TemplatePreviewModal-9aRFpJ_J.js +1 -0
- package/client/dist/assets/{TicketDetailModalContext-BbRA6cb7.js → TicketDetailModalContext-B1X5glzV.js} +1 -1
- package/client/dist/assets/{Trans-mjFZpHw1.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-BIL-HxGy.js → formatDistanceToNow-CeZv93iR.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-Cs9z2sdO.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-B2BBZQTx.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-xSqimlaj.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-2Yx_9yJM.js → settings-B3rWEwOx.js} +1 -1
- package/client/dist/assets/{settings-BLOsrXEw.js → settings-BcVoiMyw.js} +1 -1
- package/client/dist/assets/settings-BlYvaQ7w.js +1 -0
- package/client/dist/assets/{settings-DPjNiUsS.js → settings-CD5uEBiL.js} +1 -1
- package/client/dist/assets/{settings-CpKwTew1.js → settings-CyhS0DLJ.js} +1 -1
- package/client/dist/assets/{settings-FIqwhTup.js → settings-DCMFUGDs.js} +1 -1
- package/client/dist/assets/{settings-C6s9PtRD.js → settings-DEgcA0jd.js} +1 -1
- package/client/dist/assets/{settings-NSWQjY5B.js → settings-DmbYKyMJ.js} +1 -1
- package/client/dist/assets/{spending-DXOcv1tn.js → spending-DNM6NAoA.js} +1 -1
- package/client/dist/assets/{useDesktop-gVGoWWiY.js → useDesktop-mxHVQOdx.js} +1 -1
- package/client/dist/assets/useSharedWebSocket-DnxHj8jP.js +2 -0
- package/client/dist/index.html +19 -17
- package/docs/README.md +1 -0
- package/docs/internals/README.md +2 -0
- package/docs/internals/agent-runtime-framework-evaluation.md +208 -0
- package/docs/internals/programmatic-agent-runtime-validation.md +72 -0
- package/docs/internals/programmatic-agent-runtime.md +140 -0
- 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-accounting.js +73 -0
- package/server/dist/agent-runtime-bridge.js +231 -0
- package/server/dist/agent-runtime-controls-router.js +86 -0
- package/server/dist/agent-runtime-controls.js +341 -0
- package/server/dist/agent-runtime-loader.js +91 -0
- package/server/dist/agent-runtime-metrics.js +46 -0
- package/server/dist/agent-runtime-paths.js +12 -0
- package/server/dist/agent-runtime-repositories.js +37 -0
- package/server/dist/agent-runtime-settings-router.js +61 -0
- package/server/dist/agent-runtime-settings.js +239 -0
- package/server/dist/agent-runtime-settlement.js +133 -0
- package/server/dist/agent-runtime-verification-suggestions.js +119 -0
- package/server/dist/contract-refine-runner.js +7 -7
- package/server/dist/core-completion.js +8 -5
- package/server/dist/core-execution.js +17 -8
- package/server/dist/core-node-runtime.js +12 -0
- 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 +44 -2
- package/server/dist/loop-factory.js +1 -1
- package/server/dist/loop-run-manager.js +62 -28
- package/server/dist/mcp/tools/specs.js +7 -4
- package/server/dist/multi-repo-execution.js +1 -0
- package/server/dist/project-registry.js +9 -0
- package/server/dist/project-router-jobs.js +8 -2
- package/server/dist/project-router-settings.js +4 -0
- package/server/dist/project-router-tickets.js +4 -4
- package/server/dist/providers/runtime.js +6 -0
- package/server/dist/rail-isolated-launch.js +18 -0
- package/server/dist/rails-router.js +4 -1
- package/server/dist/runtime-role-prompts-router.js +25 -0
- package/server/dist/schemas/agent-runtime.schema.json +60 -0
- package/server/dist/worktree-overlay.js +32 -1
- package/client/dist/assets/AgentBrowserCapture-BQem50z4.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BC_xbT_l.js +0 -2
- package/client/dist/assets/AgentModeCodePane-DK-q3K6Y.js +0 -2
- package/client/dist/assets/AgentModeJobsPane-C7ELTPC1.js +0 -1
- package/client/dist/assets/CodePage-DwgbeqQ_.js +0 -3
- package/client/dist/assets/InteractiveJobComposer-BcR6xlHU.js +0 -19
- package/client/dist/assets/JobDetailModal-DKA_DUHe.js +0 -1
- package/client/dist/assets/JobDetailPage-vaOcE-cI.js +0 -1
- package/client/dist/assets/JobsPage-Bwoe-Fb0.js +0 -1
- package/client/dist/assets/PluginsPage-CTUHX_Lu.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-DeL8md1s.js +0 -1
- package/client/dist/assets/TemplatePreviewModal-_CgCCV7L.js +0 -1
- package/client/dist/assets/index-Bfj8HvX1.css +0 -2
- package/client/dist/assets/index-UnaJTMIy.js +0 -76
- package/client/dist/assets/narration-B-ynWTpn.js +0 -1
- package/client/dist/assets/narration-Bj_laZwg.js +0 -1
- package/client/dist/assets/narration-BxHpmMv_.js +0 -1
- package/client/dist/assets/narration-C1Px0-0j.js +0 -1
- package/client/dist/assets/narration-CAXhGbzM.js +0 -1
- package/client/dist/assets/narration-DQ1l_Kq6.js +0 -1
- package/client/dist/assets/narration-XISXwqZd.js +0 -1
- package/client/dist/assets/narration-kwzfglQo.js +0 -1
- package/client/dist/assets/settings-CCNZN5Q-.js +0 -1
- package/client/dist/assets/useSharedWebSocket-DbGL_YOt.js +0 -2
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveCoreNodeRuntime = resolveCoreNodeRuntime;
|
|
4
|
+
const path_resolver_1 = require("./path-resolver");
|
|
5
|
+
/** External Core CLIs need ordinary Node. A pkg sidecar's process.execPath is
|
|
6
|
+
* the server executable: using it would recursively launch another server. */
|
|
7
|
+
function resolveCoreNodeRuntime() {
|
|
8
|
+
const bundled = (0, path_resolver_1.resolveBundledNodeExe)();
|
|
9
|
+
if (bundled)
|
|
10
|
+
return bundled;
|
|
11
|
+
return process.pkg ? 'node' : process.execPath;
|
|
12
|
+
}
|
|
@@ -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
|
},
|
|
@@ -11,6 +11,7 @@ exports.createLoopExecutors = createLoopExecutors;
|
|
|
11
11
|
*/
|
|
12
12
|
const core_completion_1 = require("./core-completion");
|
|
13
13
|
const core_execution_1 = require("./core-execution");
|
|
14
|
+
const agent_runtime_bridge_1 = require("./agent-runtime-bridge");
|
|
14
15
|
const codex_spawn_1 = require("./plugins/codex-spawn");
|
|
15
16
|
const node_child_process_1 = require("node:child_process");
|
|
16
17
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -41,6 +42,7 @@ const SHELL_OUTPUT_CAP = 256 * 1024;
|
|
|
41
42
|
// Match the queue's idle budget: a long implementation may keep running while
|
|
42
43
|
// producing output; a silent, wedged CLI must eventually release its rail.
|
|
43
44
|
const DEFAULT_INACTIVITY_TIMEOUT_MS = 30 * 60_000;
|
|
45
|
+
const existsRuntimeRequest = (contextPath) => (0, node_fs_1.existsSync)((0, node_path_1.join)((0, node_path_1.dirname)(contextPath), 'agent-runtime-request.json'));
|
|
44
46
|
function runShellCommand(command, cwd, env, timeoutMs, onLine, onSpawn) {
|
|
45
47
|
return new Promise((resolve) => {
|
|
46
48
|
const isWin = process.platform === 'win32';
|
|
@@ -116,6 +118,15 @@ function aiStepEnv(env, repoDir, manifest) {
|
|
|
116
118
|
const withMap = (0, repo_map_1.injectRepoMapEnv)(base, repoDir);
|
|
117
119
|
return manifest || (0, rail_isolation_1.isRailPrDeliveryEnabled)() ? { ...withMap, SPECRAILS_GIT_AUTO: 'false' } : withMap;
|
|
118
120
|
}
|
|
121
|
+
function programmaticStepEnv(env, repoDir, manifest) {
|
|
122
|
+
const result = aiStepEnv(env, repoDir, manifest);
|
|
123
|
+
delete result.SPECRAILS_PROFILE_PATH;
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
function runtimeContextPath(cwd, runId, env) {
|
|
127
|
+
const root = env.SPECRAILS_TICKETS_PATH ? (0, node_path_1.dirname)((0, node_path_1.dirname)(env.SPECRAILS_TICKETS_PATH)) : cwd;
|
|
128
|
+
return (0, node_path_1.join)(root, '.specrails', 'pipeline', runId, 'desktop-context.json');
|
|
129
|
+
}
|
|
119
130
|
/**
|
|
120
131
|
* Relocated cwd is the workspace; the source repo is reached via the
|
|
121
132
|
* `./project` symlink + SPECRAILS_REPO_DIR. Each provider must be told the
|
|
@@ -163,6 +174,7 @@ function createLoopExecutors(opts = {}) {
|
|
|
163
174
|
}
|
|
164
175
|
};
|
|
165
176
|
const completionContexts = new Map();
|
|
177
|
+
const runtimeConfigPath = (cwd) => (0, node_path_1.join)(opts.pluginScope?.().stateRoot ?? cwd, '.specrails', 'agent-runtime.json');
|
|
166
178
|
return {
|
|
167
179
|
async readCoreCompletion(runId) {
|
|
168
180
|
const context = completionContexts.get(runId);
|
|
@@ -220,9 +232,28 @@ function createLoopExecutors(opts = {}) {
|
|
|
220
232
|
// for the pipeline's I/O exactly like QueueManager: SPECRAILS_REPO_DIR +
|
|
221
233
|
// claude `--add-dir <repoDir>` (see the shared helpers above). Best-effort
|
|
222
234
|
// agent self-heal on Windows.
|
|
223
|
-
const
|
|
235
|
+
const baseEnv = resolveEnv();
|
|
236
|
+
const existingContext = coreRun ? runtimeContextPath(cwd, coreRun.runId, baseEnv) : undefined;
|
|
237
|
+
const programmatic = coreRun && existingContext && (coreRun.implementation || (coreRun.verificationStep && existsRuntimeRequest(existingContext)));
|
|
238
|
+
const baseStepEnv = programmatic ? programmaticStepEnv(baseEnv, repoDir, executionManifest) : withProfileEnv(aiStepEnv(baseEnv, repoDir, executionManifest), provider, profileName);
|
|
224
239
|
const core = coreRun ? (0, core_execution_1.prepareCoreExecution)({ run: coreRun, cwd, repoDir, manifest: executionManifest, env: baseStepEnv }) : undefined;
|
|
225
240
|
const stepEnv = core?.env ?? baseStepEnv;
|
|
241
|
+
if (coreRun?.implementation && core) {
|
|
242
|
+
completionContexts.set(coreRun.runId, { cwd, contextPath: core.contextPath, env: stepEnv, runId: coreRun.runId });
|
|
243
|
+
const admitted = existsRuntimeRequest(core.contextPath);
|
|
244
|
+
// Repeated implementation nodes may validate completed work, but may
|
|
245
|
+
// never silently replay an interrupted mutating phase.
|
|
246
|
+
return (0, agent_runtime_bridge_1.runAgentRuntimeInvocation)({
|
|
247
|
+
contextPath: core.contextPath, cwd, env: { ...programmaticStepEnv(resolveEnv(), repoDir, executionManifest), SPECRAILS_EXECUTION_CONTEXT: core.contextPath }, configPath: runtimeConfigPath(cwd),
|
|
248
|
+
change: (0, agent_runtime_bridge_1.runtimeChangeName)(coreRun.runId), resume: admitted,
|
|
249
|
+
defaultProvider: provider, selectedModel: model,
|
|
250
|
+
onLine, onRawLine, onSpawn, timeoutMs: aiStepTimeoutMs,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (coreRun?.verificationStep && core && existsRuntimeRequest(core.contextPath)) {
|
|
254
|
+
const checked = (0, core_execution_1.checkCoreCompletion)(core.contextPath, cwd, { ...programmaticStepEnv(resolveEnv(), repoDir, executionManifest), SPECRAILS_EXECUTION_CONTEXT: core.contextPath }, coreRun.runId);
|
|
255
|
+
return { text: checked.valid ? 'VERIFICATION: PASS — Core verified the exact implementation and review.' : checked.reason ?? 'Core verification failed', failed: !checked.valid, errorText: checked.reason, provider: 'agent-runtime', model: 'deterministic-verification', cost: 0, tokensIn: 0, tokensOut: 0, tokens: 0, estimated: false, durationMs: 0 };
|
|
256
|
+
}
|
|
226
257
|
let extraArgs = aiStepExtraArgs(adapter, cwd, repoDir, executionManifest);
|
|
227
258
|
const pluginScope = adapter.id === 'codex' ? opts.pluginScope?.() : undefined;
|
|
228
259
|
const pluginArgs = (0, codex_spawn_1.buildCodexPluginArgs)({ providerId: adapter.id, stateRoot: pluginScope?.stateRoot ?? stepEnv.SPECRAILS_WORKSPACE_DIR ?? cwd, repositoryPath: repoDir ?? cwd, legacyProviderId: pluginScope?.legacyProviderId });
|
|
@@ -431,6 +462,13 @@ function createLoopExecutors(opts = {}) {
|
|
|
431
462
|
* (non-claude providers, or the kill-switch) — byte-identical legacy.
|
|
432
463
|
*/
|
|
433
464
|
planInteractiveAiStep({ coreRun, provider, model, effort, profileName, cwd, repoDir, executionManifest, sessionId, aiStepTimeoutMs, idleTimeoutMs }) {
|
|
465
|
+
if (coreRun?.implementation || coreRun?.verificationStep) {
|
|
466
|
+
const env = resolveEnv();
|
|
467
|
+
const root = env.SPECRAILS_TICKETS_PATH ? (0, node_path_1.dirname)((0, node_path_1.dirname)(env.SPECRAILS_TICKETS_PATH)) : cwd;
|
|
468
|
+
const contextPath = (0, node_path_1.join)(root, '.specrails', 'pipeline', coreRun.runId, 'desktop-context.json');
|
|
469
|
+
if (coreRun.implementation || existsRuntimeRequest(contextPath))
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
434
472
|
if (!(0, feature_flags_1.isInteractiveJobsEnabled)())
|
|
435
473
|
return null;
|
|
436
474
|
const adapter = (0, providers_1.getAdapter)(provider);
|
|
@@ -490,8 +528,12 @@ function createLoopExecutors(opts = {}) {
|
|
|
490
528
|
};
|
|
491
529
|
},
|
|
492
530
|
validateCoreCompletion({ coreRun, provider, model, effort, profileName, cwd, repoDir, executionManifest }) {
|
|
493
|
-
const
|
|
531
|
+
const baseEnv = resolveEnv();
|
|
532
|
+
const programmatic = existsRuntimeRequest(runtimeContextPath(cwd, coreRun.runId, baseEnv));
|
|
533
|
+
const baseStepEnv = programmatic ? programmaticStepEnv(baseEnv, repoDir, executionManifest) : withProfileEnv(aiStepEnv(baseEnv, repoDir, executionManifest), provider, profileName);
|
|
494
534
|
const core = (0, core_execution_1.prepareCoreExecution)({ run: coreRun, cwd, repoDir, manifest: executionManifest, env: baseStepEnv });
|
|
535
|
+
if (existsRuntimeRequest(core.contextPath))
|
|
536
|
+
return (0, core_execution_1.checkCoreCompletion)(core.contextPath, cwd, { ...programmaticStepEnv(resolveEnv(), repoDir, executionManifest), SPECRAILS_EXECUTION_CONTEXT: core.contextPath }, coreRun.runId);
|
|
495
537
|
const env = (0, runtime_1.buildProviderEnv)((0, providers_1.getAdapter)(provider), { prompt: '', model, reasoning_effort: effort }, core.env);
|
|
496
538
|
return (0, core_execution_1.checkCoreCompletion)(core.contextPath, cwd, env, coreRun.runId);
|
|
497
539
|
},
|
|
@@ -21,7 +21,7 @@ function coreImplementationGraph(command) {
|
|
|
21
21
|
return {
|
|
22
22
|
nodes: [
|
|
23
23
|
{ id: 'start', type: 'start', position: { x: 0, y: 0 } },
|
|
24
|
-
{ id: 'main-1', type: 'ai-step', position: { x: 0, y: 110 }, data: { prompt: `{{cmd:${command}}}` } },
|
|
24
|
+
{ id: 'main-1', type: 'ai-step', position: { x: 0, y: 110 }, data: { operation: 'core-implementation', prompt: `{{cmd:${command}}}` } },
|
|
25
25
|
{ id: 'done', type: 'end', position: { x: 0, y: 220 }, data: { outcome: 'success' } },
|
|
26
26
|
],
|
|
27
27
|
edges: [
|
|
@@ -19,6 +19,7 @@ const ai_invocations_1 = require("./ai-invocations");
|
|
|
19
19
|
const provider_limit_1 = require("./provider-limit");
|
|
20
20
|
const ids_1 = require("./ids");
|
|
21
21
|
const providers_1 = require("./providers");
|
|
22
|
+
const agent_runtime_accounting_1 = require("./agent-runtime-accounting");
|
|
22
23
|
const runtime_1 = require("./providers/runtime");
|
|
23
24
|
const result_event_1 = require("./result-event");
|
|
24
25
|
const rails_store_1 = require("./rails-store");
|
|
@@ -137,6 +138,13 @@ function extractChangeId(text) {
|
|
|
137
138
|
const m = CHANGE_ID_RE.exec(text);
|
|
138
139
|
return m ? m[1] : undefined;
|
|
139
140
|
}
|
|
141
|
+
function programmaticUsageAvailability(db, runId) {
|
|
142
|
+
const row = db.prepare(`SELECT COUNT(*) AS count,
|
|
143
|
+
MAX(total_cost_usd IS NULL) AS cost_unknown,
|
|
144
|
+
MAX(tokens_in IS NULL) AS input_unknown, MAX(tokens_out IS NULL) AS output_unknown
|
|
145
|
+
FROM ai_invocations WHERE surface = 'loop' AND loop_run_id = ? AND provider = 'agent-runtime'`).get(runId);
|
|
146
|
+
return { present: row.count > 0, costUnknown: Boolean(row.cost_unknown), tokensInUnknown: Boolean(row.input_unknown), tokensOutUnknown: Boolean(row.output_unknown) };
|
|
147
|
+
}
|
|
140
148
|
function insertLoopInvocation(db, payload, result, finishedAt) {
|
|
141
149
|
const ticketIds = Array.isArray(payload.ticketIds)
|
|
142
150
|
? [...new Set(payload.ticketIds)]
|
|
@@ -193,14 +201,15 @@ function insertLoopInvocation(db, payload, result, finishedAt) {
|
|
|
193
201
|
FROM ai_invocations
|
|
194
202
|
WHERE surface = 'loop' AND loop_run_id = ?
|
|
195
203
|
`).get(payload.runId);
|
|
196
|
-
const
|
|
204
|
+
const runtimeUsage = programmaticUsageAvailability(db, payload.runId);
|
|
205
|
+
const usageTelemetryAvailable = runtimeUsage.present || (0, providers_1.getAdapter)(payload.provider).capabilities.reportsUsage !== false;
|
|
197
206
|
db.prepare(`
|
|
198
207
|
UPDATE jobs
|
|
199
208
|
SET total_cost_usd = ?, tokens_in = ?, tokens_out = ?,
|
|
200
209
|
tokens_cache_read = ?, tokens_cache_create = ?,
|
|
201
210
|
duration_ms = ?, num_turns = ?
|
|
202
211
|
WHERE id = ? AND owner = 'loop'
|
|
203
|
-
`).run(usageTelemetryAvailable ? aggregate.cost : null, usageTelemetryAvailable ? aggregate.tokens_in : null, usageTelemetryAvailable ? aggregate.tokens_out : null, usageTelemetryAvailable ? aggregate.cache_read : null, usageTelemetryAvailable ? aggregate.cache_create : null, aggregate.duration, usageTelemetryAvailable ? aggregate.turns : null, payload.runId);
|
|
212
|
+
`).run(usageTelemetryAvailable && !runtimeUsage.costUnknown ? aggregate.cost : null, usageTelemetryAvailable && !runtimeUsage.tokensInUnknown ? aggregate.tokens_in : null, usageTelemetryAvailable && !runtimeUsage.tokensOutUnknown ? aggregate.tokens_out : null, usageTelemetryAvailable && !runtimeUsage.present ? aggregate.cache_read : null, usageTelemetryAvailable && !runtimeUsage.present ? aggregate.cache_create : null, aggregate.duration, usageTelemetryAvailable && !runtimeUsage.present ? aggregate.turns : null, payload.runId);
|
|
204
213
|
}
|
|
205
214
|
/** Recover every AI/decider step that was staged before provider spawn but did
|
|
206
215
|
* not atomically land its invocation. Completed interactive turns are the jobs
|
|
@@ -266,6 +275,23 @@ function recoverOrphanLoopStepAccounting(db, finishedAt = new Date().toISOString
|
|
|
266
275
|
WHERE job_id = ? AND seq > ? AND source = 'stdout'
|
|
267
276
|
ORDER BY seq
|
|
268
277
|
`).all(payload.runId, payload.completedEventSeq ?? -1);
|
|
278
|
+
const programmatic = (0, agent_runtime_accounting_1.parseProgrammaticUsage)(rawRows.map(row => row.payload)) ?? (payload.provider === 'agent-runtime'
|
|
279
|
+
? { provider: 'agent-runtime', model: 'per-role', estimated: true, failed: true }
|
|
280
|
+
: undefined);
|
|
281
|
+
if (programmatic) {
|
|
282
|
+
const durationMs = boundedInflightDurationMs(payload, finishedAt);
|
|
283
|
+
const didRecover = (0, loop_runs_store_1.completeLoopStepRecovery)(db, payload.runId, payload.stepKey, stable => {
|
|
284
|
+
insertLoopInvocation(db, stable, { ...programmatic, durationMs }, finishedAt);
|
|
285
|
+
const aggregate = (0, loop_runs_store_1.readLoopJobUsage)(db, payload.runId);
|
|
286
|
+
const duration = (payload.loopDurationBaseline ?? 0) + durationMs;
|
|
287
|
+
db.prepare(`UPDATE loop_runs SET total_cost_usd = ?, total_tokens = ?, total_duration_ms = ?, iteration_count = MAX(iteration_count, COALESCE(?, iteration_count)) WHERE id = ?`)
|
|
288
|
+
.run(aggregate.totalCostUsd, aggregate.tokensIn + aggregate.tokensOut, duration, stable.iterationCount ?? null, payload.runId);
|
|
289
|
+
db.prepare(`UPDATE jobs SET duration_ms = ? WHERE id = ? AND owner = 'loop'`).run(duration, payload.runId);
|
|
290
|
+
});
|
|
291
|
+
if (didRecover)
|
|
292
|
+
recovered += 1;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
269
295
|
const adapter = (0, providers_1.getAdapter)(payload.provider);
|
|
270
296
|
const usageTelemetryAvailable = adapter.capabilities.reportsUsage !== false;
|
|
271
297
|
const events = rawRows
|
|
@@ -560,7 +586,7 @@ class LoopRunManager {
|
|
|
560
586
|
if (req.graph.nodes.some((node) => node.type === 'decider')) {
|
|
561
587
|
(0, providers_1.requireToolPolicy)(adapter, 'read-only');
|
|
562
588
|
}
|
|
563
|
-
|
|
589
|
+
let usageTelemetryAvailable = adapter.capabilities.reportsUsage !== false;
|
|
564
590
|
const neverAfterDispose = () => new Promise(() => { });
|
|
565
591
|
const runId = req.runId ?? (0, ids_1.newId)();
|
|
566
592
|
const maxIterations = req.graph.config.maxIterations;
|
|
@@ -828,20 +854,20 @@ class LoopRunManager {
|
|
|
828
854
|
// The REAL turn-start instant, captured before the step/decider await. The
|
|
829
855
|
// row is bucketed/ordered by started_at, so it must be the start, not finish.
|
|
830
856
|
startedAt, stepKey) => {
|
|
857
|
+
if (r.provider === 'agent-runtime')
|
|
858
|
+
usageTelemetryAvailable = true;
|
|
831
859
|
if (usageTelemetryAvailable)
|
|
832
860
|
totalCost += r.cost ?? 0;
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
// tokens streamed, but cost (and tokens) are dropped, so the step bills $0.
|
|
837
|
-
// Flag the run total as a lower bound and say so, per occurrence.
|
|
861
|
+
// Missing pricing does not identify the failure cause or prove zero spend:
|
|
862
|
+
// providers can omit billing after a successful call or reject a request
|
|
863
|
+
// before model work starts. Retain known costs and flag the incomplete sum.
|
|
838
864
|
const costBearingButUnpriced = usageTelemetryAvailable
|
|
839
865
|
&& r.cost == null
|
|
840
866
|
&& (r.failed === true || (r.tokens ?? 0) > 0);
|
|
841
867
|
if (costBearingButUnpriced) {
|
|
842
868
|
costUncertain = true;
|
|
843
869
|
costUnknownWarned = true; // this line already carries the cap caveat below
|
|
844
|
-
logLine(`⚠️ Step cost unknown —
|
|
870
|
+
logLine(`⚠️ Step cost unknown — this step did not report a priced cost. The reported loop total includes known costs only.${maxCostUsd !== undefined ? ' The cost cap may under-count.' : ''}`, 'stderr');
|
|
845
871
|
}
|
|
846
872
|
// Honest cost-cap caveat: a cost-bearing step that reports no figure means
|
|
847
873
|
// the cap can't be enforced precisely from here on. Warn once (live).
|
|
@@ -1058,7 +1084,7 @@ class LoopRunManager {
|
|
|
1058
1084
|
// --yes`, codex `$implement #<id> --yes`) — then resolve `{{spec.*}}`
|
|
1059
1085
|
// data tokens and finally `{{const:*}}` library constants.
|
|
1060
1086
|
const rawTemplate = String(node.data?.prompt ?? '');
|
|
1061
|
-
const requiresCoreCompletion = /\{\{\s*cmd:(?:implement|batch)\s*\}\}/.test(rawTemplate)
|
|
1087
|
+
const requiresCoreCompletion = node.data?.operation === 'core-implementation' || /\{\{\s*cmd:(?:implement|batch)\s*\}\}/.test(rawTemplate)
|
|
1062
1088
|
|| /^\s*(?:\/specrails:|\/skill:specrails-|\$)(?:implement|batch-implement)(?:\s|$)/.test(rawTemplate);
|
|
1063
1089
|
const expanded = (0, loop_constants_1.resolveConstants)(resolveRunVars((0, loop_graph_1.interpolateSpec)((0, loop_command_catalog_1.expandCommands)(rawTemplate, { provider: nodeProvider, ticketIds: req.spec?.ticketIds, specId: req.spec?.id }), req.spec), runVars), constMap);
|
|
1064
1090
|
const base = [(0, multi_repo_execution_store_1.executionManifestPrompt)(req.executionManifest), withReviewContinuationContext(expanded, rawTemplate, req.spec)].filter(Boolean).join('\n\n');
|
|
@@ -1073,7 +1099,7 @@ class LoopRunManager {
|
|
|
1073
1099
|
// were noise once the step explorer landed; the log now opens on real
|
|
1074
1100
|
// output). `template` is omitted when rendering changed nothing (a
|
|
1075
1101
|
// plain free-text prompt would just duplicate `command`).
|
|
1076
|
-
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}` : ''})`;
|
|
1077
1103
|
const stepDetail = {
|
|
1078
1104
|
...(rawTemplate && rawTemplate !== base ? { template: capText(rawTemplate, STEP_TEMPLATE_CAP) } : {}),
|
|
1079
1105
|
...(base ? { command: capText(base, STEP_COMMAND_CAP) } : {}),
|
|
@@ -1096,7 +1122,7 @@ class LoopRunManager {
|
|
|
1096
1122
|
const effectiveTimeoutMs = Number.isFinite(remainingMs)
|
|
1097
1123
|
? Math.max(1, Math.min(aiStepTimeoutMs === 0 ? Infinity : (aiStepTimeoutMs ?? 15 * 60_000), remainingMs))
|
|
1098
1124
|
: aiStepTimeoutMs;
|
|
1099
|
-
const coreRun = { runId, spec: req.spec, goal: req.spec ? undefined : JSON.stringify({ loop: req.loopName, graph: req.graph }), repositoryId: req.repositoryId, verificationStep: node.data?.requireVerificationPass === true || /\{\{\s*cmd:(?:verify|revision-verify|opsx:verify)\s*\}\}/.test(rawTemplate) };
|
|
1125
|
+
const coreRun = { runId, implementation: requiresCoreCompletion, spec: req.spec, goal: req.spec ? undefined : JSON.stringify({ loop: req.loopName, graph: req.graph }), repositoryId: req.repositoryId, verificationStep: node.data?.requireVerificationPass === true || /\{\{\s*cmd:(?:verify|revision-verify|opsx:verify)\s*\}\}/.test(rawTemplate) };
|
|
1100
1126
|
const interactivePlan = this.executors.planInteractiveAiStep?.({
|
|
1101
1127
|
coreRun,
|
|
1102
1128
|
provider: nodeProvider,
|
|
@@ -1248,9 +1274,7 @@ class LoopRunManager {
|
|
|
1248
1274
|
? `blocked — ${blockedReason}`
|
|
1249
1275
|
: verificationFailed
|
|
1250
1276
|
? 'verification did not finish with VERIFICATION: PASS'
|
|
1251
|
-
: undefined);
|
|
1252
|
-
if (verificationFailed && !blockedReason)
|
|
1253
|
-
logLine(`✖ ${stepErrorText}`, 'stderr');
|
|
1277
|
+
: res.failed ? 'provider invocation failed without a reported reason' : undefined);
|
|
1254
1278
|
// Make the failure reason land visibly INSIDE the step's log
|
|
1255
1279
|
// segment (the interactive session already surfaced its own note at
|
|
1256
1280
|
// settle; the Template/Command flat-log lines are gone, so without
|
|
@@ -1258,6 +1282,14 @@ class LoopRunManager {
|
|
|
1258
1282
|
if (oneShotZeroWork) {
|
|
1259
1283
|
logLine(`✖ Zero work performed — the command never ran${res.text.trim() ? `: ${res.text.trim()}` : ''}`, 'stderr');
|
|
1260
1284
|
}
|
|
1285
|
+
else if (stepFailed && stepErrorText) {
|
|
1286
|
+
// Executors can return a structured failure without streaming any
|
|
1287
|
+
// text (configuration rejection, missing CLI, provider startup).
|
|
1288
|
+
// Persist its reason before the step closes, including early stops.
|
|
1289
|
+
logLine(`✖ ${stepErrorText}`, 'stderr');
|
|
1290
|
+
}
|
|
1291
|
+
record(`loop:${runId}`, { ...res, failed: stepFailed }, aiStepStart, aiRecoveryKey);
|
|
1292
|
+
this._activeStepRecovery.delete(runId);
|
|
1261
1293
|
// Step tail marker — after the step's last streamed output (both the
|
|
1262
1294
|
// one-shot and interactive paths have fully persisted their frames by
|
|
1263
1295
|
// here: the session resolves inside onSettle, past its final writes).
|
|
@@ -1281,8 +1313,6 @@ class LoopRunManager {
|
|
|
1281
1313
|
else if (stepFailed)
|
|
1282
1314
|
aiSessionId = undefined;
|
|
1283
1315
|
history.push(`AI Step${stepFailed ? ` FAILED (${stepErrorText ?? 'provider invocation failed'})` : ''}: ${truncate(res.text)}`);
|
|
1284
|
-
record(`loop:${runId}`, { ...res, failed: stepFailed }, aiStepStart, aiRecoveryKey);
|
|
1285
|
-
this._activeStepRecovery.delete(runId);
|
|
1286
1316
|
if (blockedReason) {
|
|
1287
1317
|
const decision = await awaitHumanDecision(blockedReason);
|
|
1288
1318
|
if (decision.action === 'stop') {
|
|
@@ -1579,6 +1609,7 @@ class LoopRunManager {
|
|
|
1579
1609
|
// The ONLY way a persisted stream ends with an open step is a mid-flight
|
|
1580
1610
|
// dispose (shutdown/project removal), where this code never runs.
|
|
1581
1611
|
emitStepEnd({ status: 'failed' });
|
|
1612
|
+
let runtimeUsage = { present: false, costUnknown: false, tokensInUnknown: false, tokensOutUnknown: false };
|
|
1582
1613
|
// A traversal exception can bypass record(). Reconcile the staged step
|
|
1583
1614
|
// BEFORE writing aggregate job totals, while its jobs baseline is still
|
|
1584
1615
|
// valid. If reconciliation itself fails, finishLoopRunAndJob detects the
|
|
@@ -1586,6 +1617,8 @@ class LoopRunManager {
|
|
|
1586
1617
|
try {
|
|
1587
1618
|
const recoveredSteps = recoverOrphanLoopStepAccounting(this.db, new Date(this.now()).toISOString(), runId);
|
|
1588
1619
|
this._activeStepRecovery.delete(runId);
|
|
1620
|
+
runtimeUsage = programmaticUsageAvailability(this.db, runId);
|
|
1621
|
+
usageTelemetryAvailable ||= runtimeUsage.present;
|
|
1589
1622
|
const aggregate = this.db.prepare(`
|
|
1590
1623
|
SELECT COALESCE(SUM(total_cost_usd), 0) AS cost,
|
|
1591
1624
|
COALESCE(SUM(tokens_in), 0) AS tokens_in,
|
|
@@ -1617,11 +1650,11 @@ class LoopRunManager {
|
|
|
1617
1650
|
}
|
|
1618
1651
|
if (usageTelemetryAvailable) {
|
|
1619
1652
|
finalJobUsage = {
|
|
1620
|
-
tokensIn: aggregate.tokens_in,
|
|
1621
|
-
tokensOut: aggregate.tokens_out,
|
|
1622
|
-
tokensCacheRead: aggregate.cache_read,
|
|
1623
|
-
tokensCacheCreate: aggregate.cache_create,
|
|
1624
|
-
numTurns: aggregate.turns,
|
|
1653
|
+
tokensIn: runtimeUsage.tokensInUnknown ? null : aggregate.tokens_in,
|
|
1654
|
+
tokensOut: runtimeUsage.tokensOutUnknown ? null : aggregate.tokens_out,
|
|
1655
|
+
tokensCacheRead: runtimeUsage.present ? null : aggregate.cache_read,
|
|
1656
|
+
tokensCacheCreate: runtimeUsage.present ? null : aggregate.cache_create,
|
|
1657
|
+
numTurns: runtimeUsage.present ? null : aggregate.turns,
|
|
1625
1658
|
};
|
|
1626
1659
|
}
|
|
1627
1660
|
}
|
|
@@ -1638,8 +1671,9 @@ class LoopRunManager {
|
|
|
1638
1671
|
const executionOutcome = outcome;
|
|
1639
1672
|
if (outcome === 'success' && coreCompletion && (coreCompletion.completion.implementation !== 'complete' || !['verified', 'with-exceptions'].includes(coreCompletion.completion.validation)))
|
|
1640
1673
|
outcome = 'blocked';
|
|
1674
|
+
const costAvailable = usageTelemetryAvailable && !runtimeUsage.costUnknown;
|
|
1641
1675
|
console.log(`[loop] settle run=${runId} outcome=${outcome} iterations=${iteration} ` +
|
|
1642
|
-
(
|
|
1676
|
+
(costAvailable ? `cost=$${totalCost.toFixed(4)}` : 'cost=unavailable'));
|
|
1643
1677
|
emitRunEvent('loop_completion', {
|
|
1644
1678
|
version: 1, execution: executionOutcome, steps: stepNum, deciderEvaluations: iteration,
|
|
1645
1679
|
turns: finalJobUsage.numTurns, costUsd: usageTelemetryAvailable ? totalCost : null,
|
|
@@ -1657,8 +1691,8 @@ class LoopRunManager {
|
|
|
1657
1691
|
// `≥` when any cost-bearing step ended unpriced (timeout/crash) — the figure
|
|
1658
1692
|
// is a lower bound, not exact. Providers without usage telemetry get an
|
|
1659
1693
|
// explicit unavailable marker, never a fabricated "$0.0000".
|
|
1660
|
-
logLine(`\n■ Loop execution finished: ${outcome} —
|
|
1661
|
-
(
|
|
1694
|
+
logLine(`\n■ Loop execution finished: ${outcome} — ` +
|
|
1695
|
+
(costAvailable
|
|
1662
1696
|
? `${costUncertain ? '≥ ' : ''}$${totalCost.toFixed(4)}`
|
|
1663
1697
|
: 'usage/cost unavailable'));
|
|
1664
1698
|
if (coreCompletion)
|
|
@@ -1678,7 +1712,7 @@ class LoopRunManager {
|
|
|
1678
1712
|
job: {
|
|
1679
1713
|
exitCode: outcome === 'success' ? 0 : 1,
|
|
1680
1714
|
status: jobStatus,
|
|
1681
|
-
totalCostUsd:
|
|
1715
|
+
totalCostUsd: costAvailable ? totalCost : null,
|
|
1682
1716
|
tokensIn: finalJobUsage.tokensIn,
|
|
1683
1717
|
tokensOut: finalJobUsage.tokensOut,
|
|
1684
1718
|
tokensCacheRead: finalJobUsage.tokensCacheRead,
|
|
@@ -1699,7 +1733,7 @@ class LoopRunManager {
|
|
|
1699
1733
|
tokens_out: finalJobUsage.tokensOut,
|
|
1700
1734
|
tokens_cache_read: finalJobUsage.tokensCacheRead,
|
|
1701
1735
|
tokens_cache_create: finalJobUsage.tokensCacheCreate,
|
|
1702
|
-
total_cost_usd:
|
|
1736
|
+
total_cost_usd: costAvailable ? totalCost : null,
|
|
1703
1737
|
num_turns: finalJobUsage.numTurns,
|
|
1704
1738
|
},
|
|
1705
1739
|
timestamp: new Date(this.now()).toISOString(),
|
|
@@ -1716,7 +1750,7 @@ class LoopRunManager {
|
|
|
1716
1750
|
runId,
|
|
1717
1751
|
outcome,
|
|
1718
1752
|
iterations: iteration,
|
|
1719
|
-
totalCostUsd:
|
|
1753
|
+
totalCostUsd: costAvailable ? totalCost : null,
|
|
1720
1754
|
...(outcome === 'stalled' && stallReason ? { stallReason } : {}),
|
|
1721
1755
|
...(providerLimitHit ? { providerLimit: providerLimitHit } : {}),
|
|
1722
1756
|
};
|
|
@@ -59,7 +59,8 @@ function specsTools() {
|
|
|
59
59
|
'— pass contractRefine: false to skip it, e.g. when the user declined it), ' +
|
|
60
60
|
'generate (same as create but takes a pre-formed idea string; both accept contextScope/attachments/createLocal), ' +
|
|
61
61
|
'ai_edit (ai-spawn, AI-edit a ticket title+description), ' +
|
|
62
|
-
'cancel_ai_edit (abort an in-flight ai-edit), contract_refine (ai-spawn, append/re-fire a Contract Layer — Claude
|
|
62
|
+
'cancel_ai_edit (abort an in-flight ai-edit), contract_refine (ai-spawn, append/re-fire a Contract Layer — Claude or Codex; ' +
|
|
63
|
+
'pass numeric id, not ticketId; optional aiEngine selects the provider for origin-less specs, while Explore specs retain their origin provider; ' +
|
|
63
64
|
'works on Explore-origin AND agent-authored/Quick specs), ' +
|
|
64
65
|
'smash (ai-spawn, decompose a spec into N child sub-specs under an epic — Claude-only), ' +
|
|
65
66
|
'smash_undo (destructive, reverse a prior SMASH), delete_epic_children (destructive, delete all children of an epic), ' +
|
|
@@ -150,7 +151,7 @@ function specsTools() {
|
|
|
150
151
|
aiEngine: zod_1.z
|
|
151
152
|
.string()
|
|
152
153
|
.optional()
|
|
153
|
-
.describe('create/generate: provider/engine override (must be
|
|
154
|
+
.describe('create/generate/contract_refine: provider/engine override (must be installed; contract_refine cannot switch an Explore-origin provider)'),
|
|
154
155
|
contextScope: zod_1.z
|
|
155
156
|
.record(zod_1.z.unknown())
|
|
156
157
|
.optional()
|
|
@@ -158,7 +159,7 @@ function specsTools() {
|
|
|
158
159
|
contractRefine: zod_1.z
|
|
159
160
|
.boolean()
|
|
160
161
|
.optional()
|
|
161
|
-
.describe('create/generate/commit_draft: enrich the spec with a Contract Layer post-persist (Claude
|
|
162
|
+
.describe('create/generate/commit_draft: enrich the spec with a Contract Layer post-persist (Claude or Codex). DEFAULTS TO TRUE — pass false to opt out (e.g. the user asked for no contract layer)'),
|
|
162
163
|
attachmentIds: zod_1.z.array(zod_1.z.string()).optional().describe('create/generate/ai_edit: attachment ids (create/generate require pendingSpecId)'),
|
|
163
164
|
// ── ai_edit ──────────────────────────────────────────────────────
|
|
164
165
|
instructions: zod_1.z.string().optional().describe('ai_edit: natural-language editing instructions (required)'),
|
|
@@ -428,7 +429,9 @@ function specsTools() {
|
|
|
428
429
|
return { ...r, hint: WATCH_HINT };
|
|
429
430
|
}
|
|
430
431
|
case 'contract_refine': {
|
|
431
|
-
const r = await (0, types_1.apiCall)(ctx, 'POST', `${base}/tickets/${requireId()}/contract-refine
|
|
432
|
+
const r = await (0, types_1.apiCall)(ctx, 'POST', `${base}/tickets/${requireId()}/contract-refine`, {
|
|
433
|
+
aiEngine: args.aiEngine,
|
|
434
|
+
});
|
|
432
435
|
return { ...r, hint: CONTRACT_REFINE_HINT };
|
|
433
436
|
}
|
|
434
437
|
case 'smash': {
|
|
@@ -353,6 +353,7 @@ async function launchMultiRepositoryRail(input, io = {}) {
|
|
|
353
353
|
};
|
|
354
354
|
await (0, rail_isolated_launch_1.launchIsolatedRail)({
|
|
355
355
|
...input, ctx: childCtx, scope: effectiveScope, loopGraph: graph,
|
|
356
|
+
runtimeStateProject: ctx.project,
|
|
356
357
|
revision: input.revision && previous ? { ...input.revision, ofDeliveryId: previous.id, decision: previous.decision } : undefined,
|
|
357
358
|
requiredPrContinuation: continuation,
|
|
358
359
|
explicitPrTarget: continuation?.prNumber ? { prNumber: continuation.prNumber } : input.explicitPrTarget,
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.ProjectRegistry = void 0;
|
|
7
7
|
exports.emitRecoveredPrDelivery = emitRecoveredPrDelivery;
|
|
8
8
|
exports.reprojectActivePrDeliveries = reprojectActivePrDeliveries;
|
|
9
|
+
const agent_runtime_controls_router_1 = require("./agent-runtime-controls-router");
|
|
9
10
|
const path_1 = __importDefault(require("path"));
|
|
10
11
|
const fs_1 = __importDefault(require("fs"));
|
|
11
12
|
const os_1 = __importDefault(require("os"));
|
|
@@ -305,6 +306,10 @@ class ProjectRegistry {
|
|
|
305
306
|
// Loop engine teardown: dispose any resident interactive step sessions
|
|
306
307
|
// (SIGTERM, no settle) + kill in-flight one-shot loop children — BEFORE
|
|
307
308
|
// db.close() so a late close handler can't write to the closed handle.
|
|
309
|
+
try {
|
|
310
|
+
(0, agent_runtime_controls_router_1.shutdownAgentRuntimeControls)(ctx);
|
|
311
|
+
}
|
|
312
|
+
catch { /* ignore */ }
|
|
308
313
|
try {
|
|
309
314
|
ctx.loopRunManager.shutdown();
|
|
310
315
|
}
|
|
@@ -540,6 +545,10 @@ class ProjectRegistry {
|
|
|
540
545
|
catch { /* ignore */ }
|
|
541
546
|
// Loop engine: dispose resident interactive step sessions + kill in-flight
|
|
542
547
|
// one-shot loop children so a quit mid-run doesn't orphan claude processes.
|
|
548
|
+
try {
|
|
549
|
+
(0, agent_runtime_controls_router_1.shutdownAgentRuntimeControls)(ctx);
|
|
550
|
+
}
|
|
551
|
+
catch { /* ignore */ }
|
|
543
552
|
try {
|
|
544
553
|
ctx.loopRunManager.shutdown();
|
|
545
554
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerJobsRoutes = registerJobsRoutes;
|
|
4
|
+
const agent_runtime_controls_router_1 = require("./agent-runtime-controls-router");
|
|
4
5
|
const ids_1 = require("./ids");
|
|
5
6
|
const db_1 = require("./db");
|
|
6
7
|
const queue_manager_1 = require("./queue-manager");
|
|
@@ -229,6 +230,10 @@ function registerJobsRoutes(deps) {
|
|
|
229
230
|
router.post('/:projectId/jobs/:id/cancel', (req, res) => {
|
|
230
231
|
const id = req.params.id;
|
|
231
232
|
const c = ctx(req);
|
|
233
|
+
if ((0, agent_runtime_controls_router_1.cancelRuntimeContinuation)(c, id)) {
|
|
234
|
+
res.json({ ok: true, status: 'canceling' });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
232
237
|
// A loop run is backed by a job row but is NOT a QueueManager job — it is
|
|
233
238
|
// driven by the LoopRunManager. Cancel it through the engine (which settles
|
|
234
239
|
// it 'stopped' → finishJob('canceled') + releases the rail's tickets).
|
|
@@ -409,12 +414,13 @@ function registerJobsRoutes(deps) {
|
|
|
409
414
|
const from = req.query.from;
|
|
410
415
|
const to = req.query.to;
|
|
411
416
|
const { db } = ctx(req);
|
|
412
|
-
const result = (0, job_listing_1.listUnifiedJobs)(db, { limit, offset, status, from, to });
|
|
417
|
+
const result = (0, job_listing_1.listUnifiedJobs)(db, { limit, offset, status, from, to, activeRuntimeIds: (0, agent_runtime_controls_router_1.activeRuntimeContinuationIds)(ctx(req)) });
|
|
413
418
|
// Annotate each job with hasTelemetry so the client can show the
|
|
414
419
|
// Export diagnostic button without an extra round trip.
|
|
415
420
|
const jobsWithTelemetry = (0, db_1.getJobsWithTelemetry)(db);
|
|
416
421
|
const annotatedJobs = result.jobs.map((j) => ({
|
|
417
422
|
...j,
|
|
423
|
+
...((0, agent_runtime_controls_router_1.isRuntimeContinuationActive)(ctx(req), j.id) ? { status: 'running', finished_at: null } : {}),
|
|
418
424
|
hasTelemetry: jobsWithTelemetry.has(j.id),
|
|
419
425
|
}));
|
|
420
426
|
res.json({ jobs: annotatedJobs, total: result.total });
|
|
@@ -593,7 +599,7 @@ function registerJobsRoutes(deps) {
|
|
|
593
599
|
interactiveAcceptingTurns = qmMode !== null || loopStepActive || loopPaused;
|
|
594
600
|
interactiveSettleMode = qmMode ?? ((loopStepActive || loopPaused || loopRun) ? 'auto' : null);
|
|
595
601
|
}
|
|
596
|
-
const annotated = { ...job, hasTelemetry: (0, db_1.hasJobTelemetry)(db, jobId), tickets, interactiveSettleMode, interactiveAcceptingTurns, loopPaused, loopPauseReason };
|
|
602
|
+
const annotated = { ...job, ...((0, agent_runtime_controls_router_1.isRuntimeContinuationActive)(c, jobId) ? { status: 'running', finished_at: null } : {}), hasTelemetry: (0, db_1.hasJobTelemetry)(db, jobId), tickets, interactiveSettleMode, interactiveAcceptingTurns, loopPaused, loopPauseReason };
|
|
597
603
|
res.json({ job: annotated, events, phaseDefinitions });
|
|
598
604
|
});
|
|
599
605
|
router.delete('/:projectId/jobs', (req, res) => {
|
|
@@ -19,7 +19,11 @@ const terminal_settings_1 = require("./terminal-settings");
|
|
|
19
19
|
const terminal_marks_store_1 = require("./terminal-marks-store");
|
|
20
20
|
const project_router_helpers_1 = require("./project-router-helpers");
|
|
21
21
|
const install_config_path_1 = require("./install-config-path");
|
|
22
|
+
const agent_runtime_settings_router_1 = require("./agent-runtime-settings-router");
|
|
23
|
+
const agent_runtime_controls_router_1 = require("./agent-runtime-controls-router");
|
|
22
24
|
function registerSettingsRoutes(deps) {
|
|
25
|
+
(0, agent_runtime_settings_router_1.registerAgentRuntimeSettingsRoutes)(deps);
|
|
26
|
+
(0, agent_runtime_controls_router_1.registerAgentRuntimeControlRoutes)(deps);
|
|
23
27
|
const { router, registry, ctx, ticketPath } = deps;
|
|
24
28
|
// ─── Project settings (pipeline telemetry) ───────────────────────────────────
|
|
25
29
|
router.get('/:projectId/settings', (req, res) => {
|
|
@@ -271,7 +271,7 @@ function registerTicketsRoutes(deps) {
|
|
|
271
271
|
const rawScope = req.body?.contextScope;
|
|
272
272
|
// Contract Layer is available only when the selected adapter advertises a
|
|
273
273
|
// safe structured-action implementation.
|
|
274
|
-
const quickContractRefine = (0, providers_1.getAdapter)(provider)
|
|
274
|
+
const quickContractRefine = !(0, runtime_1.supportsContractRefine)((0, providers_1.getAdapter)(provider))
|
|
275
275
|
? false
|
|
276
276
|
: typeof req.body?.contractRefine === 'boolean'
|
|
277
277
|
? req.body.contractRefine
|
|
@@ -1057,7 +1057,7 @@ function registerTicketsRoutes(deps) {
|
|
|
1057
1057
|
if (conversationId &&
|
|
1058
1058
|
created &&
|
|
1059
1059
|
convoProvider &&
|
|
1060
|
-
(0, providers_1.getAdapter)(convoProvider)
|
|
1060
|
+
(0, runtime_1.supportsContractRefine)((0, providers_1.getAdapter)(convoProvider))) {
|
|
1061
1061
|
const createdTicketId = created.id;
|
|
1062
1062
|
const convoId = conversationId;
|
|
1063
1063
|
console.log(`[project-router] from-draft hook: scheduling refine ticket=${createdTicketId} conv=${convoId}`);
|
|
@@ -1093,7 +1093,7 @@ function registerTicketsRoutes(deps) {
|
|
|
1093
1093
|
const requestedAgentProvider = typeof body.agentProvider === 'string' ? body.agentProvider : null;
|
|
1094
1094
|
const providerCheck = (0, provider_selection_1.validateRequestedProvider)(project, requestedAgentProvider);
|
|
1095
1095
|
const refineProvider = providerCheck.ok ? providerCheck.provider : null;
|
|
1096
|
-
if (refineProvider && (0, providers_1.getAdapter)(refineProvider)
|
|
1096
|
+
if (refineProvider && (0, runtime_1.supportsContractRefine)((0, providers_1.getAdapter)(refineProvider))) {
|
|
1097
1097
|
const refineTicketId = created.id;
|
|
1098
1098
|
const refineTitle = created.title;
|
|
1099
1099
|
const refineDescription = created.description;
|
|
@@ -1262,7 +1262,7 @@ function registerTicketsRoutes(deps) {
|
|
|
1262
1262
|
return;
|
|
1263
1263
|
}
|
|
1264
1264
|
const refineProvider = refineProviderCheck.provider;
|
|
1265
|
-
if ((0, providers_1.getAdapter)(refineProvider)
|
|
1265
|
+
if (!(0, runtime_1.supportsContractRefine)((0, providers_1.getAdapter)(refineProvider))) {
|
|
1266
1266
|
res.status(409).json({ error: `contract_refine_unsupported_for_${refineProvider}` });
|
|
1267
1267
|
return;
|
|
1268
1268
|
}
|