specrails-desktop 2.45.0 → 2.47.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 (142) hide show
  1. package/client/dist/assets/{ActivityFeedPage-D3SQklu5.js → ActivityFeedPage-DSCpSOGV.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-DNGxE33d.js → AgentBrowserCapture-mc1c5pgS.js} +1 -1
  3. package/client/dist/assets/AgentModeAnalyticsPane--UkamBTm.js +2 -0
  4. package/client/dist/assets/AgentModeCodePane-DJCswsvU.js +2 -0
  5. package/client/dist/assets/AgentModeJobsPane-Dw3DNFkh.js +1 -0
  6. package/client/dist/assets/AgentsPage-ChElPUL9.js +87 -0
  7. package/client/dist/assets/{AnalyticsPage-BqV6mM2O.js → AnalyticsPage-BGtkFHBp.js} +1 -1
  8. package/client/dist/assets/CodePage-BBnw3VGn.js +3 -0
  9. package/client/dist/assets/{DesktopAnalyticsPage-D6WNax3K.js → DesktopAnalyticsPage-B85tNScc.js} +1 -1
  10. package/client/dist/assets/{DocsDialog-68jKUH9f.js → DocsDialog-D4cXrOMl.js} +1 -1
  11. package/client/dist/assets/{DocsPage-BoGJgRpm.js → DocsPage-BCAbC6qy.js} +1 -1
  12. package/client/dist/assets/{ExportDropdown-B2C_XLGp.js → ExportDropdown-BwuvqS2Y.js} +1 -1
  13. package/client/dist/assets/InteractiveJobComposer-Coq-Zhk0.js +19 -0
  14. package/client/dist/assets/JobDetailModal--Mjam0Nc.js +1 -0
  15. package/client/dist/assets/JobDetailPage-B6ShWSBh.js +1 -0
  16. package/client/dist/assets/JobsPage-DHU_ep_B.js +1 -0
  17. package/client/dist/assets/{LoopBuilderPage-D21GxFmL.js → LoopBuilderPage-Byvq8Oqx.js} +2 -2
  18. package/client/dist/assets/{LoopPreviewModal-Bo3fYFxN.js → LoopPreviewModal-BMC9Mgj1.js} +1 -1
  19. package/client/dist/assets/{LoopsPage-BcrBw1cg.js → LoopsPage-CFGl-Nw8.js} +1 -1
  20. package/client/dist/assets/{MinimizedChatsContext-Dsv2eHf_.js → MinimizedChatsContext-DHjHBYqa.js} +1 -1
  21. package/client/dist/assets/{PluginsPage-BbyTmSLx.js → PluginsPage-BHygRnW6.js} +2 -2
  22. package/client/dist/assets/{ProjectSettingsDialog-B3lEXLv7.js → ProjectSettingsDialog-nspqxH-M.js} +1 -1
  23. package/client/dist/assets/{RepositoryDeliveries-ok_Wo65C.js → RepositoryDeliveries-Dq9NJOWi.js} +1 -1
  24. package/client/dist/assets/{RepositoryScopeSelector-DU6QwNaS.js → RepositoryScopeSelector-C3jKG2z0.js} +1 -1
  25. package/client/dist/assets/{ReviewPacketPage-BBCeHtgd.js → ReviewPacketPage-C9mqhi65.js} +1 -1
  26. package/client/dist/assets/{TemplatePreviewModal-CfNmxS6O.js → TemplatePreviewModal-BmwoAjOr.js} +1 -1
  27. package/client/dist/assets/{TicketDetailModalContext-DW3eQ0I7.js → TicketDetailModalContext-lYmoONrd.js} +1 -1
  28. package/client/dist/assets/{Trans-BHPMWkOw.js → Trans-C0KyPo2O.js} +1 -1
  29. package/client/dist/assets/agentRuntime-62GWdc26.js +1 -0
  30. package/client/dist/assets/agentRuntime-BN1mKAuY.js +1 -0
  31. package/client/dist/assets/agentRuntime-BNBCzQLM.js +1 -0
  32. package/client/dist/assets/agentRuntime-BQE_K1GG.js +1 -0
  33. package/client/dist/assets/agentRuntime-CCU3HCtS.js +1 -0
  34. package/client/dist/assets/agentRuntime-DT72Bpdv.js +1 -0
  35. package/client/dist/assets/agentRuntime-ZfMPeeiP.js +1 -0
  36. package/client/dist/assets/agentRuntime-g3W8Ry4d.js +1 -0
  37. package/client/dist/assets/{agents-IUOgqT5t.js → agents-BIT1UdTy.js} +1 -1
  38. package/client/dist/assets/{agents-DaYcxAr1.js → agents-C7fKfvm8.js} +1 -1
  39. package/client/dist/assets/{agents-ms7TwG16.js → agents-CST-lpYX.js} +1 -1
  40. package/client/dist/assets/{agents-Bvyi3y6R.js → agents-Cm2JYrAa.js} +1 -1
  41. package/client/dist/assets/{agents-BHp0_FQt.js → agents-DsgGMGwU.js} +1 -1
  42. package/client/dist/assets/{agents-45GbINfh.js → agents-DyI4rbHa.js} +1 -1
  43. package/client/dist/assets/{agents-rzPnqoBR.js → agents-H6tVQUNP.js} +1 -1
  44. package/client/dist/assets/{agents-DMKFStHL.js → agents-_5mJMkAE.js} +1 -1
  45. package/client/dist/assets/format-duration-NJVPOPBG.js +1 -0
  46. package/client/dist/assets/{formatDistanceToNow-Bq8iptrG.js → formatDistanceToNow-D44phX1J.js} +1 -1
  47. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BVFky3l_.js → getTimezoneOffsetInMilliseconds-EAh9LFSx.js} +1 -1
  48. package/client/dist/assets/index-Cfh9aaep.css +2 -0
  49. package/client/dist/assets/index-DgMBighL.js +76 -0
  50. package/client/dist/assets/{jira-api-zrRdZFBB.js → jira-api-BQR6G0YY.js} +1 -1
  51. package/client/dist/assets/narration-B1uGfofH.js +1 -0
  52. package/client/dist/assets/narration-BKcL4Srl.js +1 -0
  53. package/client/dist/assets/narration-BtjKyHVM.js +1 -0
  54. package/client/dist/assets/narration-CqNvgRi9.js +1 -0
  55. package/client/dist/assets/narration-DiPyj06T.js +1 -0
  56. package/client/dist/assets/narration-DrBAf8uv.js +1 -0
  57. package/client/dist/assets/narration-OyujFouL.js +1 -0
  58. package/client/dist/assets/narration-UWi_xIZ-.js +1 -0
  59. package/client/dist/assets/{project-repositories-CFRaB9UB.js → project-repositories-GRYdjlKB.js} +1 -1
  60. package/client/dist/assets/{provider-capabilities-B-wOHfLP.js → provider-capabilities-CXipwrXJ.js} +1 -1
  61. package/client/dist/assets/{settings-OOyDrZQo.js → settings-B3rWEwOx.js} +1 -1
  62. package/client/dist/assets/{settings-CNiERE-R.js → settings-BcVoiMyw.js} +1 -1
  63. package/client/dist/assets/settings-BlYvaQ7w.js +1 -0
  64. package/client/dist/assets/{settings-DJ2jl3hJ.js → settings-CD5uEBiL.js} +1 -1
  65. package/client/dist/assets/{settings-a7NIODme.js → settings-CyhS0DLJ.js} +1 -1
  66. package/client/dist/assets/{settings-DVRVybR_.js → settings-DCMFUGDs.js} +1 -1
  67. package/client/dist/assets/{settings-CrtCs5-B.js → settings-DEgcA0jd.js} +1 -1
  68. package/client/dist/assets/{settings-ChOCVjX_.js → settings-DmbYKyMJ.js} +1 -1
  69. package/client/dist/assets/{spending-lTZ-bm_y.js → spending-CN9PHM65.js} +1 -1
  70. package/client/dist/assets/{useDesktop-Duv5ddAa.js → useDesktop-iius_s5d.js} +1 -1
  71. package/client/dist/assets/{useSharedWebSocket-5mj2SMKn.js → useSharedWebSocket-D7hkZ3Ka.js} +2 -2
  72. package/client/dist/index.html +20 -19
  73. package/docs/internals/programmatic-agent-runtime.md +19 -8
  74. package/docs/plans/implementation-efficiency-artifact.json +18 -0
  75. package/docs/plans/implementation-efficiency-evaluation.json +1817 -0
  76. package/docs/plans/implementation-efficiency-evaluation.md +41 -0
  77. package/docs/plans/implementation-efficiency-progress.md +48 -0
  78. package/docs/plans/implementation-efficiency-release.md +41 -0
  79. package/docs/plans/implementation-efficiency.md +124 -0
  80. package/package.json +1 -1
  81. package/server/dist/agent-mcp-config.js +5 -0
  82. package/server/dist/agent-operator-prompt.js +2 -2
  83. package/server/dist/agent-runtime-bridge.js +69 -17
  84. package/server/dist/agent-runtime-controls-router.js +50 -0
  85. package/server/dist/agent-runtime-controls.js +123 -12
  86. package/server/dist/agent-runtime-effective-config.js +32 -0
  87. package/server/dist/agent-runtime-events.js +46 -0
  88. package/server/dist/agent-runtime-history.js +47 -0
  89. package/server/dist/agent-runtime-loader.js +90 -5
  90. package/server/dist/agent-runtime-metrics.js +122 -0
  91. package/server/dist/agent-runtime-package.js +205 -0
  92. package/server/dist/agent-runtime-repositories.js +37 -0
  93. package/server/dist/agent-runtime-settings-router.js +29 -6
  94. package/server/dist/agent-runtime-settings.js +151 -21
  95. package/server/dist/agent-runtime-settlement.js +133 -0
  96. package/server/dist/contract-refine-runner.js +7 -7
  97. package/server/dist/core-completion.js +8 -5
  98. package/server/dist/desktop-router.js +26 -0
  99. package/server/dist/job-listing.js +7 -3
  100. package/server/dist/loop-command-catalog.js +2 -2
  101. package/server/dist/loop-executors.js +6 -4
  102. package/server/dist/loop-run-manager.js +12 -4
  103. package/server/dist/mcp/tools/rails.js +1 -0
  104. package/server/dist/mcp/tools/specs.js +7 -4
  105. package/server/dist/multi-repo-execution.js +1 -0
  106. package/server/dist/project-router-jobs.js +8 -2
  107. package/server/dist/project-router-tickets.js +4 -4
  108. package/server/dist/providers/runtime.js +6 -0
  109. package/server/dist/rail-isolated-launch.js +2 -1
  110. package/server/dist/rails-router.js +29 -1
  111. package/server/dist/runtime-role-prompts-router.js +25 -0
  112. package/server/dist/schemas/agent-runtime.schema.json +25 -3
  113. package/server/dist/schemas/fixtures/runtime-efficiency-summary.v1.json +790 -0
  114. package/server/dist/worktree-overlay.js +32 -1
  115. package/client/dist/assets/AgentModeAnalyticsPane-BUvZNoj0.js +0 -2
  116. package/client/dist/assets/AgentModeCodePane-YW717UvJ.js +0 -2
  117. package/client/dist/assets/AgentModeJobsPane-Cxg_eNmd.js +0 -1
  118. package/client/dist/assets/AgentsPage-B_DCByok.js +0 -87
  119. package/client/dist/assets/CodePage-PyZKCj1B.js +0 -3
  120. package/client/dist/assets/InteractiveJobComposer-l8q-r5Yy.js +0 -19
  121. package/client/dist/assets/JobDetailModal-C-X2GI9j.js +0 -1
  122. package/client/dist/assets/JobDetailPage-C6Jdm7S9.js +0 -1
  123. package/client/dist/assets/JobsPage-DycM6Vgp.js +0 -1
  124. package/client/dist/assets/agentRuntime-14faDjWM.js +0 -1
  125. package/client/dist/assets/agentRuntime-BIokojkJ.js +0 -1
  126. package/client/dist/assets/agentRuntime-Bu4xEcRw.js +0 -1
  127. package/client/dist/assets/agentRuntime-Cz0ASixB.js +0 -1
  128. package/client/dist/assets/agentRuntime-DESdGXLV.js +0 -1
  129. package/client/dist/assets/agentRuntime-DhX5Xy5S.js +0 -1
  130. package/client/dist/assets/agentRuntime-Dpbh_gLp.js +0 -1
  131. package/client/dist/assets/agentRuntime-jS2lplFm.js +0 -1
  132. package/client/dist/assets/index-CBLYwPw6.css +0 -2
  133. package/client/dist/assets/index-DOKftX7y.js +0 -76
  134. package/client/dist/assets/narration-BA8Yw5ZL.js +0 -1
  135. package/client/dist/assets/narration-BuMFUS7Q.js +0 -1
  136. package/client/dist/assets/narration-Bv_iQQG5.js +0 -1
  137. package/client/dist/assets/narration-C6Ost9nL.js +0 -1
  138. package/client/dist/assets/narration-CIbjo2pA.js +0 -1
  139. package/client/dist/assets/narration-CNJ2-2-a.js +0 -1
  140. package/client/dist/assets/narration-OzsPJCXM.js +0 -1
  141. package/client/dist/assets/narration-zCdgiuSU.js +0 -1
  142. package/client/dist/assets/settings-DzXZCb1h.js +0 -1
@@ -5,6 +5,8 @@ exports.truncate = truncate;
5
5
  exports.resolveRunVars = resolveRunVars;
6
6
  exports.extractChangeId = extractChangeId;
7
7
  exports.recoverOrphanLoopStepAccounting = recoverOrphanLoopStepAccounting;
8
+ const agent_runtime_events_1 = require("./agent-runtime-events");
9
+ const agent_runtime_settings_1 = require("./agent-runtime-settings");
8
10
  const multi_repo_execution_store_1 = require("./multi-repo-execution-store");
9
11
  const win_spawn_1 = require("./util/win-spawn");
10
12
  const db_1 = require("./db");
@@ -561,7 +563,7 @@ class LoopRunManager {
561
563
  async run(req) {
562
564
  // Later fresh steps must verify the admitted spec, even if its caller or
563
565
  // backlog changes while the implementation is running.
564
- req = { ...req, graph: structuredClone(req.graph) };
566
+ req = { ...req, runtimeProviderOverride: (0, agent_runtime_settings_1.validateRuntimeProviderOverride)(req.runtimeProviderOverride), graph: structuredClone(req.graph) };
565
567
  if (req.spec)
566
568
  req = { ...req, spec: structuredClone(req.spec) };
567
569
  if (req.executionManifest)
@@ -752,6 +754,12 @@ class LoopRunManager {
752
754
  logLine(line);
753
755
  return;
754
756
  }
757
+ if (eventType === 'runtime-efficiency-event') {
758
+ const validated = (0, agent_runtime_events_1.runtimeEfficiencyEventLine)(line, runId);
759
+ if (!validated || (0, agent_runtime_events_1.isRecordedRuntimeEfficiencyEvent)(this.db, runId, validated))
760
+ return;
761
+ line = validated;
762
+ }
755
763
  const s = takeSeq();
756
764
  try {
757
765
  (0, db_1.appendEvent)(this.db, runId, s, { event_type: eventType, source: 'stdout', payload: line });
@@ -1099,7 +1107,7 @@ class LoopRunManager {
1099
1107
  // were noise once the step explorer landed; the log now opens on real
1100
1108
  // output). `template` is omitted when rendering changed nothing (a
1101
1109
  // plain free-text prompt would just duplicate `command`).
1102
- const stepTitle = `🤖 ${nodeLabel || 'AI Step'} (${nodeProvider}/${nodeModel}${nodeEffort ? `, effort: ${nodeEffort}` : ''})`;
1110
+ const stepTitle = requiresCoreCompletion ? '🤖 Implementation (agent runtime)' : `🤖 ${nodeLabel || 'AI Step'} (${nodeProvider}/${nodeModel}${nodeEffort ? `, effort: ${nodeEffort}` : ''})`;
1103
1111
  const stepDetail = {
1104
1112
  ...(rawTemplate && rawTemplate !== base ? { template: capText(rawTemplate, STEP_TEMPLATE_CAP) } : {}),
1105
1113
  ...(base ? { command: capText(base, STEP_COMMAND_CAP) } : {}),
@@ -1145,7 +1153,7 @@ class LoopRunManager {
1145
1153
  nextEventSeq: takeSeq,
1146
1154
  recoveryStepKey: aiRecoveryKey,
1147
1155
  })
1148
- : this.executors.runAiStep({ coreRun, prompt, sessionId, provider: nodeProvider, model: nodeModel, effort: nodeEffort, profileName: req.profileName, cwd: req.cwd, repoDir: req.repoDir, executionManifest: req.executionManifest, onLine: logLine, onRawLine, onSpawn: (c) => this._activeChild.set(runId, c), aiStepTimeoutMs: effectiveTimeoutMs, idleTimeoutMs });
1156
+ : this.executors.runAiStep({ runtimeProviderOverride: req.runtimeProviderOverride, coreRun, prompt, sessionId, provider: nodeProvider, model: nodeModel, effort: nodeEffort, profileName: req.profileName, cwd: req.cwd, repoDir: req.repoDir, executionManifest: req.executionManifest, onLine: logLine, onRawLine, onSpawn: (c) => this._activeChild.set(runId, c), aiStepTimeoutMs: effectiveTimeoutMs, idleTimeoutMs });
1149
1157
  };
1150
1158
  let res = await runAttempt(aiSessionId);
1151
1159
  if (this._disposed)
@@ -1691,7 +1699,7 @@ class LoopRunManager {
1691
1699
  // `≥` when any cost-bearing step ended unpriced (timeout/crash) — the figure
1692
1700
  // is a lower bound, not exact. Providers without usage telemetry get an
1693
1701
  // explicit unavailable marker, never a fabricated "$0.0000".
1694
- logLine(`\n■ Loop execution finished: ${outcome} — ${stepNum} step${stepNum === 1 ? '' : 's'}, ${iteration} decider evaluation${iteration === 1 ? '' : 's'}, ${finalJobUsage.numTurns ?? 'unknown'} agent turns, ` +
1702
+ logLine(`\n■ Loop execution finished: ${outcome} — ` +
1695
1703
  (costAvailable
1696
1704
  ? `${costUncertain ? '≥ ' : ''}$${totalCost.toFixed(4)}`
1697
1705
  : 'usage/cost unavailable'));
@@ -47,6 +47,7 @@ function railsTools() {
47
47
  'stop (destructive — kills all active jobs and loop runs for the rail). ' +
48
48
  'For on_review tickets with an already-open GitHub PR (including a published pr_ready delivery), launch automatically tries to continue that PR head branch; Jira-linked in_progress tickets can do the same when the PR match is explicit; fresh tickets still start from the project integration branch. ' +
49
49
  'When launched from the in-app agent chat without an explicit aiEngine, the engine defaults to your conversation\'s provider. On that same provider, an omitted model inherits the conversation model; an explicit model wins (pass aiEngine to override; launch_all always uses each rail\'s stored engine). ' +
50
+ 'For Implement and Batch Implement, the selected launch provider applies to ALL runtime roles (architect, developer, reviewer); an explicit launch model/effort also applies to each role. Resuming an existing execution keeps its original frozen role configuration; start a new launch to change providers. ' +
50
51
  'User-facing naming: call the free-form autonomous mode "Freestyle"; use "freestyle" as the canonical API enum value for that same capability. ' +
51
52
  'For small OpenSpec-governed work, recommend "SDD Quick (OpenSpec)" and launch with mode "loop" plus loopId "factory:sdd-quick-openspec"; keep Freestyle for ticket-local implementation-only work. ' +
52
53
  'NAMING: railIndex is the 0-BASED internal identity; the dashboard shows rails 1-based ("Rail N" = railIndex N-1). When talking to the user, ALWAYS say "Rail <railIndex + 1>" (or the rail\'s custom name) — results include railLabel with the correct user-facing label.',
@@ -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,
@@ -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) => {
@@ -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
  }
@@ -5,6 +5,7 @@ exports.isSafeCustomModelAlias = isSafeCustomModelAlias;
5
5
  exports.isModelAvailableForAdapter = isModelAvailableForAdapter;
6
6
  exports.supportsToolPolicy = supportsToolPolicy;
7
7
  exports.pureOutputToolPolicy = pureOutputToolPolicy;
8
+ exports.supportsContractRefine = supportsContractRefine;
8
9
  exports.requireToolPolicy = requireToolPolicy;
9
10
  exports.reasoningEffortsForModel = reasoningEffortsForModel;
10
11
  exports.defaultReasoningEffortForModel = defaultReasoningEffortForModel;
@@ -61,6 +62,11 @@ function pureOutputToolPolicy(adapter) {
61
62
  return 'read-only';
62
63
  return null;
63
64
  }
65
+ /** Contract extraction also supports Codex's enforced read-only transform. */
66
+ function supportsContractRefine(adapter) {
67
+ return adapter.capabilities.structuredActions === true
68
+ || (adapter.id === 'codex' && pureOutputToolPolicy(adapter) !== null);
69
+ }
64
70
  /** Throw a stable, provider-identifying error for an unsupported boundary. */
65
71
  function requireToolPolicy(adapter, policy) {
66
72
  if (!supportsToolPolicy(adapter, policy)) {
@@ -1019,7 +1019,7 @@ async function launchIsolatedRail(input, io = {}) {
1019
1019
  // A paused/interrupted programmatic workflow owns unfinished changes and
1020
1020
  // verification evidence in this exact mount. Keep them available for
1021
1021
  // explicit Core recovery, including a currently clean worktree.
1022
- if (implementationOutcome === 'failed' && (0, agent_runtime_paths_1.hasAgentRuntimeRequest)(ctx.project, a.runId)) {
1022
+ if (implementationOutcome === 'failed' && (0, agent_runtime_paths_1.hasAgentRuntimeRequest)(input.runtimeStateProject ?? ctx.project, a.runId)) {
1023
1023
  markWorktree(a, 'needs-review');
1024
1024
  return {
1025
1025
  run: a, implementationOutcome, deliveryOutcome: 'blocked',
@@ -1187,6 +1187,7 @@ async function launchIsolatedRail(input, io = {}) {
1187
1187
  });
1188
1188
  const spec = ctx.getTicketSpec(a.ticketId);
1189
1189
  const enginePromise = ctx.loopRunManager.run({
1190
+ runtimeProviderOverride: input.runtimeProviderOverride,
1190
1191
  runId: a.runId, loopId, loopName, graph: loopGraph, projectId: ctx.project.id,
1191
1192
  cwd: a.handle.worktreePath, repoDir: a.handle.worktreePath,
1192
1193
  repositoryId: singleRepositoryId,
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.prDeliveryRevisionAllowed = prDeliveryRevisionAllowed;
4
4
  exports.createRailsRouter = createRailsRouter;
5
+ const agent_runtime_settings_1 = require("./agent-runtime-settings");
5
6
  const express_1 = require("express");
6
7
  const rails_store_1 = require("./rails-store");
7
8
  const queue_manager_1 = require("./queue-manager");
@@ -432,6 +433,14 @@ function createRailsRouter() {
432
433
  res.status(400).json({ error: 'Invalid rail index' });
433
434
  return;
434
435
  }
436
+ let runtimeProviderOverride;
437
+ try {
438
+ runtimeProviderOverride = (0, agent_runtime_settings_1.validateRuntimeProviderOverride)(req.body?.runtimeProviderOverride);
439
+ }
440
+ catch {
441
+ res.status(400).json({ error: 'invalid_runtime_provider_override' });
442
+ return;
443
+ }
435
444
  let { mode = 'implement' } = req.body ?? {};
436
445
  const { repositoryIds: rawRepositoryIds, baseDeliveryIds, profileName, aiEngine, model, loopId: rawLoopId, reasoning_effort, originConversationId, originSurface, targetPrNumber, revisionOfDeliveryId, revisionNote, baseBranch: rawBaseBranch } = req.body ?? {};
437
446
  // Revision launch (nontech-review-experience Wave 3): the user asked for a
@@ -635,6 +644,21 @@ function createRailsRouter() {
635
644
  // Only pass a provider override when one was actually requested (keeps
636
645
  // single-provider rails on the legacy code path).
637
646
  const railProvider = requestedEngine ? engineCheck.provider : undefined;
647
+ // Mission/MCP and Launch-all submit aiEngine or use the saved rail engine.
648
+ // Resolve this at the shared boundary, not only in the Dashboard payload.
649
+ if (!runtimeProviderOverride && requestedEngine) {
650
+ try {
651
+ runtimeProviderOverride = (0, agent_runtime_settings_1.validateRuntimeProviderOverride)({ provider: engineCheck.provider, ...(model ? { model } : {}), ...(reasoning_effort ? { effort: reasoning_effort } : {}) });
652
+ }
653
+ catch {
654
+ res.status(400).json({ error: 'invalid_runtime_provider_override' });
655
+ return;
656
+ }
657
+ }
658
+ if (runtimeProviderOverride && runtimeProviderOverride.provider !== engineCheck.provider) {
659
+ res.status(400).json({ error: 'runtime_provider_mismatch' });
660
+ return;
661
+ }
638
662
  // Freestyle bypasses the OpenSpec pipeline and hands the raw spec to a
639
663
  // provider that explicitly advertises autonomous freestyle support.
640
664
  if (mode === 'freestyle' && (0, providers_1.getAdapter)(engineCheck.provider).capabilities.freestyle !== true) {
@@ -907,6 +931,7 @@ function createRailsRouter() {
907
931
  }
908
932
  try {
909
933
  const ids = await (0, rail_isolated_launch_1.launchIsolatedRail)({
934
+ runtimeProviderOverride,
910
935
  ctx: c, railIndex, ticketIds: [...rail.ticketIds], repositoryIds, ...repositoryBases, loopId, loopName, loopGraph,
911
936
  provider: loopProvider, model: loopModel, effort, scope,
912
937
  profileName: resolvedProfile,
@@ -1015,6 +1040,7 @@ function createRailsRouter() {
1015
1040
  };
1016
1041
  const runPromise = c.loopRunManager
1017
1042
  .run({
1043
+ runtimeProviderOverride,
1018
1044
  runId,
1019
1045
  loopId,
1020
1046
  loopName,
@@ -1211,7 +1237,8 @@ function createRailsRouter() {
1211
1237
  // (so we deliberately do NOT delete railLoopRuns here).
1212
1238
  for (const runId of targetLoopRunIds) {
1213
1239
  try {
1214
- c.loopRunManager.cancel(runId);
1240
+ if (!(0, agent_runtime_controls_router_1.cancelRuntimeContinuation)(c, runId))
1241
+ c.loopRunManager.cancel(runId);
1215
1242
  canceledCount++;
1216
1243
  }
1217
1244
  catch (err) {
@@ -1534,3 +1561,4 @@ function createRailsRouter() {
1534
1561
  });
1535
1562
  return router;
1536
1563
  }
1564
+ const agent_runtime_controls_router_1 = require("./agent-runtime-controls-router");
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerRuntimeRolePromptRoutes = registerRuntimeRolePromptRoutes;
4
+ const agent_runtime_settings_1 = require("./agent-runtime-settings");
5
+ const agent_runtime_loader_1 = require("./agent-runtime-loader");
6
+ function registerRuntimeRolePromptRoutes(router) {
7
+ router.get('/runtime-role-prompts', async (_req, res) => {
8
+ try {
9
+ const defaults = (await (0, agent_runtime_loader_1.loadCoreAgentRuntime)()).rolePromptDefaults();
10
+ res.json({ defaults, overrides: (0, agent_runtime_settings_1.loadRuntimeRolePrompts)() });
11
+ }
12
+ catch (error) {
13
+ res.status(422).json({ message: error.message });
14
+ }
15
+ });
16
+ router.put('/runtime-role-prompts', async (req, res) => {
17
+ try {
18
+ const defaults = (await (0, agent_runtime_loader_1.loadCoreAgentRuntime)()).rolePromptDefaults();
19
+ res.json({ defaults, overrides: (0, agent_runtime_settings_1.saveRuntimeRolePrompts)(req.body.overrides) });
20
+ }
21
+ catch (error) {
22
+ res.status(error instanceof agent_runtime_settings_1.AgentRuntimeConfigError ? 400 : 500).json({ message: error.message });
23
+ }
24
+ });
25
+ }
@@ -4,10 +4,20 @@
4
4
  "title": "Specrails programmatic agent runtime",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
- "required": ["schemaVersion", "enabled", "providers", "agents", "verification"],
7
+ "required": ["schemaVersion", "providers", "agents", "verification"],
8
8
  "properties": {
9
9
  "schemaVersion": { "const": 1 },
10
- "enabled": { "type": "boolean" },
10
+ "efficiency": { "type": "object", "additionalProperties": false, "required": ["schemaVersion"], "properties": {
11
+ "schemaVersion": { "const": 1 }, "contextMode": { "enum": ["full", "incremental"] }, "reviewMode": { "enum": ["full", "incremental"] },
12
+ "planning": { "enum": ["full", "proportional"] }, "acceptDeveloperChecks": { "type": "boolean" },
13
+ "verification": { "type": "object", "additionalProperties": false, "properties": { "maxConcurrency": { "type": "integer", "minimum": 1, "maximum": 4 } } }
14
+ } },
15
+ "rolePrompts": { "type": "object", "additionalProperties": false, "properties": {
16
+ "architect": { "type": "string", "minLength": 1, "maxLength": 20000, "pattern": "\\S" },
17
+ "developer": { "type": "string", "minLength": 1, "maxLength": 20000, "pattern": "\\S" },
18
+ "reviewer": { "type": "string", "minLength": 1, "maxLength": 20000, "pattern": "\\S" }
19
+ } },
20
+ "enabled": { "type": "boolean", "deprecated": true, "description": "Accepted for saved configuration compatibility; implementation always uses the agent runtime." },
11
21
  "providers": { "type": "array", "items": { "oneOf": [
12
22
  { "type": "object", "additionalProperties": false, "required": ["id", "kind", "cli"], "properties": {
13
23
  "id": { "$ref": "#/definitions/id" }, "kind": { "const": "cli" }, "cli": { "enum": ["claude", "codex", "gemini", "kimi"] }
@@ -26,6 +36,7 @@
26
36
  "maxCostUsd": { "type": "number", "exclusiveMinimum": 0 }, "timeoutMs": { "$ref": "#/definitions/positiveInteger" }
27
37
  } },
28
38
  "verification": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["repositoryId", "command", "args"], "properties": {
39
+ "key": { "$ref": "#/definitions/id" }, "label": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "\\S" }, "policy": { "$ref": "#/definitions/checkPolicy" },
29
40
  "repositoryId": { "$ref": "#/definitions/id" }, "command": { "type": "string", "minLength": 1 },
30
41
  "args": { "type": "array", "items": { "type": "string" } }, "cwd": { "type": "string", "minLength": 1 },
31
42
  "env": { "type": "object", "propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" }, "additionalProperties": { "type": "string" } },
@@ -44,10 +55,21 @@
44
55
  } }
45
56
  },
46
57
  "definitions": {
58
+ "effort": { "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,31}$" },
59
+ "inputList": { "type": "array", "maxItems": 256, "items": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "\\S" } },
60
+ "checkPolicy": { "type": "object", "additionalProperties": false, "properties": {
61
+ "reuse": { "enum": ["never", "snapshot-local"] }, "inputs": { "$ref": "#/definitions/inputList" },
62
+ "toolchainInputs": { "$ref": "#/definitions/inputList" }, "resources": { "$ref": "#/definitions/inputList" },
63
+ "deterministic": { "type": "boolean" }, "readOnly": { "type": "boolean" }, "independentGroup": { "$ref": "#/definitions/id" }
64
+ } },
47
65
  "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$" },
48
66
  "positiveInteger": { "type": "integer", "minimum": 1 },
49
67
  "score": { "type": "number", "minimum": 0, "maximum": 100 },
50
- "agent": { "type": "object", "additionalProperties": false, "required": ["provider"], "properties": {
68
+ "agent": { "type": "object", "additionalProperties": false, "required": ["provider"], "dependencies": { "escalation": ["model"] }, "properties": {
69
+ "effort": { "$ref": "#/definitions/effort" },
70
+ "escalation": { "type": "object", "additionalProperties": false, "required": ["model"], "properties": {
71
+ "model": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[^-\\r\\n\\u0000][^\\r\\n\\u0000]*$" }, "effort": { "$ref": "#/definitions/effort" }
72
+ } },
51
73
  "provider": { "$ref": "#/definitions/id" }, "model": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[^-\\r\\n\\u0000][^\\r\\n\\u0000]*$" },
52
74
  "maxTurns": { "$ref": "#/definitions/positiveInteger" }
53
75
  } }