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.
Files changed (131) hide show
  1. package/README.md +2 -0
  2. package/client/dist/assets/{ActivityFeedPage-CL1BUXoj.js → ActivityFeedPage-CsHQPpvl.js} +1 -1
  3. package/client/dist/assets/AgentBrowserCapture-BVcCK7x0.js +1 -0
  4. package/client/dist/assets/AgentModeAnalyticsPane-C5F3m29G.js +2 -0
  5. package/client/dist/assets/AgentModeCodePane-C5QuKsu0.js +2 -0
  6. package/client/dist/assets/AgentModeJobsPane-rnD-EPMd.js +1 -0
  7. package/client/dist/assets/{AgentsPage-DbLezyI6.js → AgentsPage-DiyJWZcU.js} +3 -3
  8. package/client/dist/assets/{AnalyticsPage-DTsg-N5z.js → AnalyticsPage-Dkmb0dnd.js} +1 -1
  9. package/client/dist/assets/CodePage-C0dH99-t.js +3 -0
  10. package/client/dist/assets/{DesktopAnalyticsPage-Y0Z1ryQr.js → DesktopAnalyticsPage-DuTjr2n8.js} +1 -1
  11. package/client/dist/assets/{DocsDialog-KeCOxvGH.js → DocsDialog-BVZJZHVR.js} +1 -1
  12. package/client/dist/assets/{DocsPage-Bt1PgYPO.js → DocsPage-D6KHTv8V.js} +1 -1
  13. package/client/dist/assets/{ExportDropdown-DczgU4at.js → ExportDropdown-pWZGYCv3.js} +1 -1
  14. package/client/dist/assets/InteractiveJobComposer-Ck3PQxjN.js +19 -0
  15. package/client/dist/assets/JobDetailModal-DkoMH3ie.js +1 -0
  16. package/client/dist/assets/JobDetailPage-CBVivcl2.js +1 -0
  17. package/client/dist/assets/JobsPage-CzxEeca9.js +1 -0
  18. package/client/dist/assets/{LoopBuilderPage-CqC4hbXb.js → LoopBuilderPage-CtmOE4-b.js} +2 -2
  19. package/client/dist/assets/{LoopPreviewModal-T0zuFv6y.js → LoopPreviewModal-BYeLjQbr.js} +1 -1
  20. package/client/dist/assets/{LoopsPage-DrQGcZv2.js → LoopsPage-6iW7g25k.js} +1 -1
  21. package/client/dist/assets/{MinimizedChatsContext-C843Y13h.js → MinimizedChatsContext-B6wVevPq.js} +1 -1
  22. package/client/dist/assets/PluginsPage-Bf1z3WPF.js +2 -0
  23. package/client/dist/assets/ProjectSettingsDialog-DsCRIc4v.js +1 -0
  24. package/client/dist/assets/{RepositoryDeliveries-bgIHE8ep.js → RepositoryDeliveries-BQm_3TO8.js} +1 -1
  25. package/client/dist/assets/{RepositoryScopeSelector-CZ_U_R3U.js → RepositoryScopeSelector-DnAFtjTz.js} +1 -1
  26. package/client/dist/assets/{ReviewPacketPage-DbKO6pT8.js → ReviewPacketPage-CaXAwtR7.js} +1 -1
  27. package/client/dist/assets/TemplatePreviewModal-9aRFpJ_J.js +1 -0
  28. package/client/dist/assets/{TicketDetailModalContext-BbRA6cb7.js → TicketDetailModalContext-B1X5glzV.js} +1 -1
  29. package/client/dist/assets/{Trans-mjFZpHw1.js → Trans-BwLgX6n7.js} +1 -1
  30. package/client/dist/assets/agentRuntime-BUagD2I_.js +1 -0
  31. package/client/dist/assets/agentRuntime-BkL70WtX.js +1 -0
  32. package/client/dist/assets/agentRuntime-CWXVqgqE.js +1 -0
  33. package/client/dist/assets/agentRuntime-CvNBdUAe.js +1 -0
  34. package/client/dist/assets/agentRuntime-D4RmeuNY.js +1 -0
  35. package/client/dist/assets/agentRuntime-DTidBWKg.js +1 -0
  36. package/client/dist/assets/agentRuntime-DhKYfyaU.js +1 -0
  37. package/client/dist/assets/agentRuntime-LL8_GLx9.js +1 -0
  38. package/client/dist/assets/format-duration-NJVPOPBG.js +1 -0
  39. package/client/dist/assets/{formatDistanceToNow-BIL-HxGy.js → formatDistanceToNow-CeZv93iR.js} +1 -1
  40. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-Cs9z2sdO.js → getTimezoneOffsetInMilliseconds-DxAkjvJb.js} +1 -1
  41. package/client/dist/assets/index-Cfh9aaep.css +2 -0
  42. package/client/dist/assets/index-DYoyDWim.js +76 -0
  43. package/client/dist/assets/{jira-api-B2BBZQTx.js → jira-api-ChjVGJx_.js} +1 -1
  44. package/client/dist/assets/narration-4OSg0b5-.js +1 -0
  45. package/client/dist/assets/narration-BPMOQPFe.js +1 -0
  46. package/client/dist/assets/narration-BSldQfjd.js +1 -0
  47. package/client/dist/assets/narration-CvQdWdR5.js +1 -0
  48. package/client/dist/assets/narration-D-e9ssxA.js +1 -0
  49. package/client/dist/assets/narration-DKLjXIFI.js +1 -0
  50. package/client/dist/assets/narration-DnrKC3mF.js +1 -0
  51. package/client/dist/assets/narration-Dypjxeel.js +1 -0
  52. package/client/dist/assets/{project-repositories-xSqimlaj.js → project-repositories-BvBj9XH9.js} +1 -1
  53. package/client/dist/assets/{provider-capabilities-B-wOHfLP.js → provider-capabilities-CXipwrXJ.js} +1 -1
  54. package/client/dist/assets/{settings-2Yx_9yJM.js → settings-B3rWEwOx.js} +1 -1
  55. package/client/dist/assets/{settings-BLOsrXEw.js → settings-BcVoiMyw.js} +1 -1
  56. package/client/dist/assets/settings-BlYvaQ7w.js +1 -0
  57. package/client/dist/assets/{settings-DPjNiUsS.js → settings-CD5uEBiL.js} +1 -1
  58. package/client/dist/assets/{settings-CpKwTew1.js → settings-CyhS0DLJ.js} +1 -1
  59. package/client/dist/assets/{settings-FIqwhTup.js → settings-DCMFUGDs.js} +1 -1
  60. package/client/dist/assets/{settings-C6s9PtRD.js → settings-DEgcA0jd.js} +1 -1
  61. package/client/dist/assets/{settings-NSWQjY5B.js → settings-DmbYKyMJ.js} +1 -1
  62. package/client/dist/assets/{spending-DXOcv1tn.js → spending-DNM6NAoA.js} +1 -1
  63. package/client/dist/assets/{useDesktop-gVGoWWiY.js → useDesktop-mxHVQOdx.js} +1 -1
  64. package/client/dist/assets/useSharedWebSocket-DnxHj8jP.js +2 -0
  65. package/client/dist/index.html +19 -17
  66. package/docs/README.md +1 -0
  67. package/docs/internals/README.md +2 -0
  68. package/docs/internals/agent-runtime-framework-evaluation.md +208 -0
  69. package/docs/internals/programmatic-agent-runtime-validation.md +72 -0
  70. package/docs/internals/programmatic-agent-runtime.md +140 -0
  71. package/package.json +1 -1
  72. package/server/dist/agent-mcp-config.js +5 -0
  73. package/server/dist/agent-operator-prompt.js +2 -2
  74. package/server/dist/agent-runtime-accounting.js +73 -0
  75. package/server/dist/agent-runtime-bridge.js +231 -0
  76. package/server/dist/agent-runtime-controls-router.js +86 -0
  77. package/server/dist/agent-runtime-controls.js +341 -0
  78. package/server/dist/agent-runtime-loader.js +91 -0
  79. package/server/dist/agent-runtime-metrics.js +46 -0
  80. package/server/dist/agent-runtime-paths.js +12 -0
  81. package/server/dist/agent-runtime-repositories.js +37 -0
  82. package/server/dist/agent-runtime-settings-router.js +61 -0
  83. package/server/dist/agent-runtime-settings.js +239 -0
  84. package/server/dist/agent-runtime-settlement.js +133 -0
  85. package/server/dist/agent-runtime-verification-suggestions.js +119 -0
  86. package/server/dist/contract-refine-runner.js +7 -7
  87. package/server/dist/core-completion.js +8 -5
  88. package/server/dist/core-execution.js +17 -8
  89. package/server/dist/core-node-runtime.js +12 -0
  90. package/server/dist/desktop-router.js +26 -0
  91. package/server/dist/job-listing.js +7 -3
  92. package/server/dist/loop-command-catalog.js +2 -2
  93. package/server/dist/loop-executors.js +44 -2
  94. package/server/dist/loop-factory.js +1 -1
  95. package/server/dist/loop-run-manager.js +62 -28
  96. package/server/dist/mcp/tools/specs.js +7 -4
  97. package/server/dist/multi-repo-execution.js +1 -0
  98. package/server/dist/project-registry.js +9 -0
  99. package/server/dist/project-router-jobs.js +8 -2
  100. package/server/dist/project-router-settings.js +4 -0
  101. package/server/dist/project-router-tickets.js +4 -4
  102. package/server/dist/providers/runtime.js +6 -0
  103. package/server/dist/rail-isolated-launch.js +18 -0
  104. package/server/dist/rails-router.js +4 -1
  105. package/server/dist/runtime-role-prompts-router.js +25 -0
  106. package/server/dist/schemas/agent-runtime.schema.json +60 -0
  107. package/server/dist/worktree-overlay.js +32 -1
  108. package/client/dist/assets/AgentBrowserCapture-BQem50z4.js +0 -1
  109. package/client/dist/assets/AgentModeAnalyticsPane-BC_xbT_l.js +0 -2
  110. package/client/dist/assets/AgentModeCodePane-DK-q3K6Y.js +0 -2
  111. package/client/dist/assets/AgentModeJobsPane-C7ELTPC1.js +0 -1
  112. package/client/dist/assets/CodePage-DwgbeqQ_.js +0 -3
  113. package/client/dist/assets/InteractiveJobComposer-BcR6xlHU.js +0 -19
  114. package/client/dist/assets/JobDetailModal-DKA_DUHe.js +0 -1
  115. package/client/dist/assets/JobDetailPage-vaOcE-cI.js +0 -1
  116. package/client/dist/assets/JobsPage-Bwoe-Fb0.js +0 -1
  117. package/client/dist/assets/PluginsPage-CTUHX_Lu.js +0 -2
  118. package/client/dist/assets/ProjectSettingsDialog-DeL8md1s.js +0 -1
  119. package/client/dist/assets/TemplatePreviewModal-_CgCCV7L.js +0 -1
  120. package/client/dist/assets/index-Bfj8HvX1.css +0 -2
  121. package/client/dist/assets/index-UnaJTMIy.js +0 -76
  122. package/client/dist/assets/narration-B-ynWTpn.js +0 -1
  123. package/client/dist/assets/narration-Bj_laZwg.js +0 -1
  124. package/client/dist/assets/narration-BxHpmMv_.js +0 -1
  125. package/client/dist/assets/narration-C1Px0-0j.js +0 -1
  126. package/client/dist/assets/narration-CAXhGbzM.js +0 -1
  127. package/client/dist/assets/narration-DQ1l_Kq6.js +0 -1
  128. package/client/dist/assets/narration-XISXwqZd.js +0 -1
  129. package/client/dist/assets/narration-kwzfglQo.js +0 -1
  130. package/client/dist/assets/settings-CCNZN5Q-.js +0 -1
  131. 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
- conditions.push('jobs.status = ?');
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 the specrails implement pipeline (architect → developer → reviewer) over the rail's tickets, via the native /specrails:implement command.",
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 the batch-implement pipeline over ALL the rail's tickets in one pass (parallel internally).",
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 baseStepEnv = withProfileEnv(aiStepEnv(resolveEnv(), repoDir, executionManifest), provider, profileName);
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 baseStepEnv = withProfileEnv(aiStepEnv(resolveEnv(), repoDir, executionManifest), provider, profileName);
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 usageTelemetryAvailable = (0, providers_1.getAdapter)(payload.provider).capabilities.reportsUsage !== false;
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
- const usageTelemetryAvailable = adapter.capabilities.reportsUsage !== false;
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
- // A cost-bearing step that produced work (tokens) or hard-failed but reports
834
- // no priced cost → its real spend is missing from the total. The common case
835
- // is a claude step killed by timeout before its terminal `result` event:
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 — the process ended before billing (timeout/crash), so it counts as $0. The loop total is a lower bound.${maxCostUsd !== undefined ? ' The cost cap may under-count.' : ''}`, 'stderr');
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
- (usageTelemetryAvailable ? `cost=$${totalCost.toFixed(4)}` : 'usage=unavailable'));
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} — ${stepNum} step${stepNum === 1 ? '' : 's'}, ${iteration} decider evaluation${iteration === 1 ? '' : 's'}, ${finalJobUsage.numTurns ?? 'unknown'} agent turns, ` +
1661
- (usageTelemetryAvailable
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: usageTelemetryAvailable ? totalCost : null,
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: usageTelemetryAvailable ? totalCost : null,
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: usageTelemetryAvailable ? totalCost : null,
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-only; ' +
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 an installed provider; defaults to primary)'),
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-only). DEFAULTS TO TRUE — pass false to opt out (e.g. the user asked for no contract layer)'),
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).capabilities.structuredActions !== true
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).capabilities.structuredActions === true) {
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).capabilities.structuredActions === true) {
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).capabilities.structuredActions !== true) {
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
  }