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
@@ -6,12 +6,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AgentRuntimeControls = exports.RuntimeControlError = void 0;
7
7
  exports.validateRuntimeResumeInput = validateRuntimeResumeInput;
8
8
  exports.readAgentRuntimeStatus = readAgentRuntimeStatus;
9
+ const agent_runtime_events_1 = require("./agent-runtime-events");
10
+ const agent_runtime_history_1 = require("./agent-runtime-history");
11
+ const agent_runtime_settlement_1 = require("./agent-runtime-settlement");
12
+ const agent_runtime_metrics_1 = require("./agent-runtime-metrics");
9
13
  const node_child_process_1 = require("node:child_process");
10
14
  const node_crypto_1 = require("node:crypto");
11
15
  const node_fs_1 = __importDefault(require("node:fs"));
12
16
  const node_path_1 = __importDefault(require("node:path"));
13
17
  const node_util_1 = require("node:util");
14
- const agent_runtime_loader_1 = require("./agent-runtime-loader");
18
+ const agent_runtime_package_1 = require("./agent-runtime-package");
15
19
  const agent_runtime_bridge_1 = require("./agent-runtime-bridge");
16
20
  const core_node_runtime_1 = require("./core-node-runtime");
17
21
  const win_spawn_1 = require("./util/win-spawn");
@@ -54,7 +58,13 @@ function openQuestion(state) {
54
58
  }
55
59
  /** Status is a read-only Core CLI operation. It never invokes a provider. */
56
60
  async function readAgentRuntimeStatus(contextPath, cwd, env) {
57
- const cli = (0, agent_runtime_loader_1.findCoreAgentRuntimeCli)();
61
+ let cli;
62
+ try {
63
+ cli = (0, agent_runtime_package_1.resolveRetainedAgentRuntime)(contextPath);
64
+ }
65
+ catch (error) {
66
+ throw new RuntimeControlError(409, 'original_runtime_unavailable', error instanceof Error ? error.message : 'The original runtime package is unavailable');
67
+ }
58
68
  if (!cli)
59
69
  throw new RuntimeControlError(503, 'runtime_unavailable', 'Update Core to inspect or resume agent runtime executions');
60
70
  const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)((0, core_node_runtime_1.resolveCoreNodeRuntime)(), [cli, 'status', '--context', contextPath, '--compact'], {
@@ -63,22 +73,29 @@ async function readAgentRuntimeStatus(contextPath, cwd, env) {
63
73
  const result = JSON.parse(stdout);
64
74
  if (result.type !== 'runtime-status' || result.state === undefined)
65
75
  throw new Error('Core returned an invalid runtime status');
66
- return result.state;
76
+ let selection;
77
+ try {
78
+ selection = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(node_path_1.default.dirname(contextPath), 'desktop-runtime-selection.json'), 'utf8'));
79
+ }
80
+ catch { /* Original hosts may not record selection origins. */ }
81
+ return result.state ? { ...result.state, efficiencySummary: (0, agent_runtime_metrics_1.applyRuntimeSelectionOrigins)((0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(result.efficiencySummary), selection, result.state.runId) } : null;
67
82
  }
68
83
  /** One controller per ProjectContext; Core's durable lease remains the final
69
84
  * cross-process guard. Resume never constructs a new context or worktree. */
70
85
  class AgentRuntimeControls {
71
86
  ctx;
72
87
  dependencies;
88
+ isActive(runId) { return this.active.has(runId); }
89
+ activeRunIds() { return [...this.active.keys()]; }
73
90
  active = new Map();
74
91
  errors = new Map();
75
92
  statusCache = new Map();
76
93
  disposed = false;
77
- constructor(ctx, dependencies = { status: readAgentRuntimeStatus, execute: agent_runtime_bridge_1.runAgentRuntimeInvocation, kill: win_spawn_1.treeKillSafe }) {
94
+ constructor(ctx, dependencies = { status: readAgentRuntimeStatus, execute: agent_runtime_bridge_1.runAgentRuntimeInvocation, kill: win_spawn_1.treeKillSafe, settle: agent_runtime_settlement_1.settleRuntimeContinuation }) {
78
95
  this.ctx = ctx;
79
96
  this.dependencies = dependencies;
80
97
  }
81
- context(runId) {
98
+ context(runId, historical = false) {
82
99
  if (!SAFE_ID.test(runId))
83
100
  throw new RuntimeControlError(400, 'invalid_run_id', 'Invalid runtime run ID');
84
101
  const execution = (0, workspace_resolution_1.resolveProjectExecution)(this.ctx.project);
@@ -94,7 +111,7 @@ class AgentRuntimeControls {
94
111
  if (frozen.runId !== runId || node_fs_1.default.realpathSync(frozen.backlogRoot) !== backlogRoot || !Array.isArray(frozen.repositories) || !frozen.repositories.length || !frozen.repositories.some((repository) => repository.path === frozen.artifactRoot))
95
112
  throw new Error();
96
113
  for (const root of [frozen.artifactRoot, ...frozen.repositories.map((repository) => repository.path)]) {
97
- if (!node_path_1.default.isAbsolute(root) || node_fs_1.default.realpathSync(root) !== root || !node_fs_1.default.statSync(root).isDirectory())
114
+ if (!node_path_1.default.isAbsolute(root) || (!historical && (node_fs_1.default.realpathSync(root) !== root || !node_fs_1.default.statSync(root).isDirectory())))
98
115
  throw new Error();
99
116
  }
100
117
  }
@@ -117,6 +134,21 @@ class AgentRuntimeControls {
117
134
  Object.assign(env, host.env, { SPECRAILS_EXECUTION_CONTEXT: file });
118
135
  return { file, frozen, cwd: host.cwd, env };
119
136
  }
137
+ async evidence(runId, query) {
138
+ const { file, frozen, env } = this.context(runId, true);
139
+ const flags = [];
140
+ for (const [key, value] of Object.entries(query)) {
141
+ if (!['id', 'section', 'sourceId', 'cursor', 'limit'].includes(key) || typeof value !== 'string' || value.length > 1024)
142
+ throw new RuntimeControlError(400, 'invalid_evidence_query', 'Invalid evidence query');
143
+ flags.push('--' + (key === 'sourceId' ? 'source-id' : key), value);
144
+ }
145
+ const cli = (0, agent_runtime_package_1.resolveRetainedAgentRuntime)(file);
146
+ const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)((0, core_node_runtime_1.resolveCoreNodeRuntime)(), [cli, 'evidence', '--context', file, ...flags], { cwd: frozen.backlogRoot, env: (0, win_spawn_1.windowsSpawnEnv)(env), windowsHide: true, timeout: 15000, maxBuffer: 4 * 1024 * 1024, encoding: 'utf8' });
147
+ const result = JSON.parse(stdout);
148
+ if (result.schemaVersion !== 1 || typeof result.available !== 'boolean' || typeof result.truncated !== 'boolean')
149
+ throw new RuntimeControlError(503, 'evidence_unavailable', 'Core evidence is unavailable or incompatible');
150
+ return result;
151
+ }
120
152
  async list() {
121
153
  const directory = node_path_1.default.join((0, workspace_resolution_1.resolveProjectExecution)(this.ctx.project).specrailsDir, 'pipeline');
122
154
  if (!node_fs_1.default.existsSync(directory))
@@ -136,7 +168,7 @@ class AgentRuntimeControls {
136
168
  // key, keeping idle settings panes from spawning CLI processes repeatedly.
137
169
  const checkpoint = node_path_1.default.join(node_path_1.default.dirname(file), 'agent-workflow', runId, 'checkpoint.json');
138
170
  const stat = node_fs_1.default.existsSync(checkpoint) ? node_fs_1.default.statSync(checkpoint) : null;
139
- const fingerprint = stat ? `${stat.mtimeMs}:${stat.size}` : null;
171
+ const fingerprint = stat ? (0, node_crypto_1.createHash)('sha256').update(node_fs_1.default.readFileSync(checkpoint)).digest('hex') : null;
140
172
  const cached = this.statusCache.get(runId);
141
173
  const state = fingerprint && cached?.fingerprint === fingerprint ? cached.state : await this.dependencies.status(file, cwd, env);
142
174
  if (!state || state.runId !== runId)
@@ -145,13 +177,28 @@ class AgentRuntimeControls {
145
177
  this.statusCache.set(runId, { fingerprint, state });
146
178
  const parent = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
147
179
  const active = this.active.has(runId) || parent?.status === 'running' || parent?.status === 'paused';
180
+ let projection = null;
181
+ try {
182
+ projection = (0, agent_runtime_history_1.readRuntimeHistory)(file);
183
+ }
184
+ catch { /* Invalid advisory history cannot replace live state. */ }
185
+ const superseding = !active && projection && ['failed', 'cancelled', 'running'].includes(projection.status) && Date.parse(projection.updatedAt) > Date.parse(state.updatedAt ?? '1970-01-01') ? projection : null;
148
186
  const recoverableSteps = Object.entries(state.steps).filter(([, step]) => ['running', 'interrupted'].includes(step.status)).map(([id]) => id);
149
- return { runId, status: state.status === 'running' && !active ? 'interrupted' : state.status, nextStep: state.nextStep,
150
- updatedAt: state.updatedAt, error: this.errors.get(runId) ?? state.error, pendingApproval: state.pendingApproval,
187
+ return { runId, status: superseding ? (superseding.status === 'running' ? 'interrupted' : superseding.status) : state.status === 'running' && !active ? 'interrupted' : state.status, nextStep: state.nextStep,
188
+ updatedAt: superseding?.updatedAt ?? state.updatedAt, error: this.errors.get(runId) ?? superseding?.error ?? state.error, pendingApproval: state.pendingApproval,
151
189
  traceId: state.traceId, pendingQuestion: openQuestion(state),
190
+ metrics: (0, agent_runtime_metrics_1.readRuntimeEfficiency)(superseding ? superseding.metrics : state.metrics), efficiencySummary: (0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(superseding ? superseding.efficiencySummary : state.efficiencySummary),
191
+ canSettle: !superseding && !active && state.status === 'succeeded' && this.ctx.db.prepare('SELECT status FROM jobs WHERE id = ?').get(runId)?.status !== 'completed',
152
192
  recoverableSteps, active, canCancel: this.active.has(runId), canResume: !active && parent?.status === 'completed' && state.status !== 'succeeded' };
153
193
  }
154
194
  catch (error) {
195
+ try {
196
+ const { file } = this.context(runId, true);
197
+ const historical = (0, agent_runtime_history_1.readRuntimeHistory)(file);
198
+ if (historical)
199
+ return { ...historical, runId, status: historical.status === 'running' ? 'interrupted' : historical.status, historical: true, active: this.active.has(runId), canResume: false, canSettle: false, canCancel: this.active.has(runId), recoverableSteps: [], error: error instanceof RuntimeControlError ? error.message : 'Historical result; live runtime verification is unavailable' };
200
+ }
201
+ catch { /* No trustworthy historical projection is available. */ }
155
202
  return { runId, status: 'unavailable', nextStep: null, active: this.active.has(runId), canResume: false, canCancel: this.active.has(runId), recoverableSteps: [], error: error instanceof RuntimeControlError ? error.message : 'Could not inspect the saved runtime execution' };
156
203
  }
157
204
  }
@@ -161,6 +208,8 @@ class AgentRuntimeControls {
161
208
  const parent = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
162
209
  if (!parent || parent.status !== 'completed' || this.active.has(runId))
163
210
  throw new RuntimeControlError(409, 'runtime_run_active', 'Wait for the original Desktop execution to settle before resuming');
211
+ if (parent.rail_index != null && [...(this.ctx.railLoopRuns?.values() ?? []), ...(this.ctx.railJobs?.values() ?? [])].some(meta => meta.railIndex === parent.rail_index))
212
+ throw new RuntimeControlError(409, 'runtime_rail_active', 'Wait for the implementation card to finish its active job before resuming');
164
213
  const context = this.context(runId);
165
214
  // Reserve before awaiting status so concurrent resume requests cannot race.
166
215
  const active = { cancelled: false };
@@ -172,6 +221,8 @@ class AgentRuntimeControls {
172
221
  const state = await this.dependencies.status(context.file, context.cwd, context.env);
173
222
  if (!state || state.runId !== runId || state.status === 'succeeded')
174
223
  throw new RuntimeControlError(409, 'runtime_not_resumable', 'This execution has no resumable workflow');
224
+ if (parent.rail_index != null && [...(this.ctx.railLoopRuns?.values() ?? []), ...(this.ctx.railJobs?.values() ?? [])].some(meta => meta.railIndex === parent.rail_index))
225
+ throw new RuntimeControlError(409, 'runtime_rail_active', 'The implementation card became active while checking the saved execution');
175
226
  // A pending question resumes the architect only with the operator's answer.
176
227
  if (openQuestion(state) && !input.answer)
177
228
  throw new RuntimeControlError(400, 'answer_required', 'This execution is waiting for an answer to the architect\'s question');
@@ -206,7 +257,20 @@ class AgentRuntimeControls {
206
257
  (0, db_1.appendEvent)(this.ctx.db, runId, ++sequence, { event_type: 'log', source, payload: JSON.stringify({ line: line.replace(/\n$/, '') }) });
207
258
  }
208
259
  catch { /* events table best-effort */ }
260
+ try {
261
+ this.ctx.broadcast?.({ type: 'log', source, line, timestamp: new Date().toISOString(), processId: runId });
262
+ }
263
+ catch { /* persisted log remains authoritative */ }
264
+ };
265
+ if (parent.rail_index != null)
266
+ this.ctx.railLoopRuns?.set(runId, { railIndex: parent.rail_index, ticketIds, requiresTerminalIntent: true });
267
+ const publishActivity = (running) => {
268
+ try {
269
+ this.ctx.broadcast?.({ type: 'runtime.continuation', projectId: this.ctx.project.id, jobId: runId, railIndex: parent.rail_index, active: running });
270
+ }
271
+ catch { /* durable log remains available */ }
209
272
  };
273
+ publishActivity(true);
210
274
  const requested = [...(input.approve?.length ? ['approve ' + input.approve.join(',')] : []), ...(input.recover?.length ? ['recover ' + input.recover.join(',')] : []), ...(input.invalidate?.length ? ['invalidate ' + input.invalidate.join(',')] : []), ...(input.answer ? ['answered question'] : [])];
211
275
  logLine(`[runtime] continuation started from phase ${state.nextStep ?? 'end'}${requested.length ? ' (' + requested.join('; ') + ')' : ''}; the original worktree and frozen scope are reused`);
212
276
  void this.dependencies.execute({
@@ -223,10 +287,20 @@ class AgentRuntimeControls {
223
287
  eventType = parsed.type;
224
288
  }
225
289
  catch { /* Keep raw diagnostics available to recovery. */ }
290
+ if (eventType === 'runtime-efficiency-event') {
291
+ const validated = (0, agent_runtime_events_1.runtimeEfficiencyEventLine)(line, runId);
292
+ if (!validated || (0, agent_runtime_events_1.isRecordedRuntimeEfficiencyEvent)(this.ctx.db, runId, validated))
293
+ return;
294
+ line = validated;
295
+ }
226
296
  this.ctx.db.transaction(() => {
227
297
  (0, db_1.appendEvent)(this.ctx.db, runId, ++sequence, { event_type: eventType, source: 'stdout', payload: line });
228
298
  (0, loop_runs_store_1.updateLoopStepActivityCheckpoint)(this.ctx.db, runId, stepKey, undefined, Date.now());
229
299
  })();
300
+ try {
301
+ this.ctx.broadcast?.({ type: 'event', jobId: runId, event_type: eventType, source: 'stdout', payload: line, seq: sequence, timestamp: new Date().toISOString() });
302
+ }
303
+ catch { /* persisted event remains authoritative */ }
230
304
  },
231
305
  onSpawn: (child) => {
232
306
  active.child = child;
@@ -235,7 +309,7 @@ class AgentRuntimeControls {
235
309
  else if (active.cancelled)
236
310
  this.cancel(runId);
237
311
  },
238
- }).then((result) => {
312
+ }).then(async (result) => {
239
313
  if (this.disposed)
240
314
  return;
241
315
  logLine(result.failed ? `[runtime] continuation ended: ${result.errorText ?? 'failed'}` : '[runtime] continuation completed', result.failed ? 'stderr' : 'stdout');
@@ -243,14 +317,51 @@ class AgentRuntimeControls {
243
317
  (0, loop_run_manager_1.recoverOrphanLoopStepAccounting)(this.ctx.db, new Date().toISOString(), runId);
244
318
  if (result.errorText)
245
319
  this.errors.set(runId, result.errorText);
246
- }).catch(() => { this.errors.set(runId, 'Runtime continuation failed. Inspect the saved phase state before retrying.'); })
247
- .finally(() => { clearTimeout(active.forceKillTimer); this.active.delete(runId); });
320
+ if (!result.failed && !active.cancelled)
321
+ await this.dependencies.settle?.({ db: this.ctx.db, projectId: this.ctx.project.id, runId, contextPath: context.file, cwd: context.cwd, env: context.env, broadcast: this.ctx.broadcast });
322
+ }).catch(() => {
323
+ if (this.disposed)
324
+ return;
325
+ const message = 'Runtime continuation failed. Inspect the saved phase state before retrying.';
326
+ this.errors.set(runId, message);
327
+ logLine(`[runtime] ${message}`, 'stderr');
328
+ })
329
+ .finally(() => {
330
+ clearTimeout(active.forceKillTimer);
331
+ this.active.delete(runId);
332
+ this.ctx.railLoopRuns?.delete(runId);
333
+ this.statusCache.delete(runId);
334
+ publishActivity(false);
335
+ });
248
336
  }
249
337
  catch (error) {
250
338
  this.active.delete(runId);
339
+ this.ctx.railLoopRuns?.delete(runId);
340
+ try {
341
+ this.ctx.broadcast?.({ type: 'runtime.continuation', projectId: this.ctx.project.id, jobId: runId, railIndex: parent.rail_index, active: false });
342
+ }
343
+ catch { /* admission failed */ }
251
344
  throw error;
252
345
  }
253
346
  }
347
+ async settle(runId) {
348
+ if (this.active.has(runId) || (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId)?.status !== 'completed')
349
+ throw new RuntimeControlError(409, 'runtime_run_active', 'Wait for the execution to settle');
350
+ const context = this.context(runId);
351
+ this.active.set(runId, { cancelled: false });
352
+ try {
353
+ await (this.dependencies.settle ?? agent_runtime_settlement_1.settleRuntimeContinuation)({ db: this.ctx.db, projectId: this.ctx.project.id, runId, contextPath: context.file, cwd: context.cwd, env: context.env, broadcast: this.ctx.broadcast });
354
+ this.errors.delete(runId);
355
+ }
356
+ catch (error) {
357
+ throw new RuntimeControlError(409, 'runtime_settlement_blocked', error instanceof Error ? error.message : 'Could not prepare the recovered delivery');
358
+ }
359
+ finally {
360
+ this.active.delete(runId);
361
+ this.statusCache.delete(runId);
362
+ this.ctx.broadcast?.({ type: 'runtime.continuation', projectId: this.ctx.project.id, jobId: runId, railIndex: (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId)?.rail_index ?? null, active: false });
363
+ }
364
+ }
254
365
  cancel(runId) {
255
366
  const active = this.active.get(runId);
256
367
  if (!active)
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveEffectiveRuntimeConfig = resolveEffectiveRuntimeConfig;
4
+ const providers_1 = require("./providers");
5
+ /** A pure admission resolver; only deliberate launch intent overrides a project role. */
6
+ function resolveEffectiveRuntimeConfig(input, options) {
7
+ const config = structuredClone(input);
8
+ const override = options.providerOverride;
9
+ if (override) {
10
+ const provider = config.providers.find(entry => entry.id === override.provider);
11
+ if (!provider)
12
+ throw new Error(`Selected runtime provider is not configured: ${override.provider}`);
13
+ for (const role of ['architect', 'developer', 'reviewer']) {
14
+ const current = config.agents[role];
15
+ const changedProvider = current.provider !== provider.id;
16
+ const changedModel = override.model !== undefined && override.model !== current.model;
17
+ config.agents[role] = {
18
+ ...current, provider: provider.id,
19
+ model: override.model ?? (changedProvider ? undefined : current.model),
20
+ effort: override.effort ?? (changedProvider || changedModel ? undefined : current.effort),
21
+ escalation: changedProvider || changedModel ? undefined : current.escalation,
22
+ };
23
+ }
24
+ }
25
+ for (const agent of Object.values(config.agents)) {
26
+ const provider = config.providers.find(entry => entry.id === agent.provider);
27
+ if (!agent.model && provider?.kind === 'cli')
28
+ agent.model = (0, providers_1.getAdapter)(provider.cli).defaultModel();
29
+ }
30
+ config.verification = config.verification.filter(check => options.repositoryIds.includes(check.repositoryId));
31
+ return { config, origins: { architect: override ? 'explicit-launch-override' : options.source, developer: override ? 'explicit-launch-override' : options.source, reviewer: override ? 'explicit-launch-override' : options.source } };
32
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runtimeEfficiencyEventLine = runtimeEfficiencyEventLine;
4
+ exports.isRecordedRuntimeEfficiencyEvent = isRecordedRuntimeEfficiencyEvent;
5
+ /** Efficiency events are advisory measurements, never workflow or delivery authority. */
6
+ function runtimeEfficiencyEventLine(line, runId) {
7
+ if (line.length > 16384)
8
+ return null;
9
+ let event;
10
+ try {
11
+ event = JSON.parse(line);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ if (event?.type !== 'runtime-efficiency-event' || event.schemaVersion !== 1 || event.runId !== runId || typeof event.eventId !== 'string' || !event.eventId.startsWith(runId + ':') || event.eventId.length > 256 || typeof event.attemptId !== 'string' || event.attemptId.length > 256 || !['role-context', 'role-route', 'check-started', 'check-finished', 'check-reused', 'check-invalidated'].includes(String(event.kind)))
17
+ return null;
18
+ if (!event.payload || typeof event.payload !== 'object' || Array.isArray(event.payload))
19
+ return null;
20
+ const raw = event.payload, payload = {};
21
+ for (const key of ['invocationId', 'provider', 'model', 'requestedEffort', 'kind', 'tier', 'routeReason', 'contextMode', 'status', 'executionId', 'repositoryId', 'checkId', 'label', 'reason']) {
22
+ const value = raw[key];
23
+ if (value === undefined)
24
+ continue;
25
+ if (value !== null && (typeof value !== 'string' || value.length > 256))
26
+ return null;
27
+ payload[key] = value;
28
+ }
29
+ for (const key of ['ordinal', 'promptBytes', 'contextBytes', 'handoffBytes', 'durationMs', 'toolCalls']) {
30
+ if (raw[key] === undefined)
31
+ continue;
32
+ if (typeof raw[key] !== 'number' || !Number.isFinite(raw[key]) || raw[key] < 0)
33
+ return null;
34
+ payload[key] = raw[key];
35
+ }
36
+ if (raw.exitCode !== undefined) {
37
+ if (typeof raw.exitCode !== 'number' || !Number.isSafeInteger(raw.exitCode))
38
+ return null;
39
+ payload.exitCode = raw.exitCode;
40
+ }
41
+ return JSON.stringify({ type: event.type, schemaVersion: 1, eventId: event.eventId, runId, attemptId: event.attemptId, kind: event.kind, payload });
42
+ }
43
+ function isRecordedRuntimeEfficiencyEvent(db, runId, line) {
44
+ const event = JSON.parse(line);
45
+ return Boolean(db.prepare("SELECT 1 FROM events WHERE job_id = ? AND event_type = 'runtime-efficiency-event' AND CASE WHEN json_valid(payload) THEN json_extract(payload, '$.eventId') END = ? LIMIT 1").get(runId, event.eventId));
46
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.writeRuntimeHistory = writeRuntimeHistory;
7
+ exports.readRuntimeHistory = readRuntimeHistory;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_crypto_1 = require("node:crypto");
11
+ const agent_runtime_metrics_1 = require("./agent-runtime-metrics");
12
+ const hash = (value) => (0, node_crypto_1.createHash)('sha256').update(value).digest('hex');
13
+ /** A replaceable projection, not an authority to resume, accept or deliver code. */
14
+ function writeRuntimeHistory(contextPath, input) {
15
+ const contextText = node_fs_1.default.readFileSync(contextPath, 'utf8'), context = JSON.parse(contextText);
16
+ let selection;
17
+ try {
18
+ selection = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(node_path_1.default.dirname(contextPath), 'desktop-runtime-selection.json'), 'utf8'));
19
+ }
20
+ catch { /* Old hosts have no explicit provenance. */ }
21
+ const result = { runId: context.runId, status: typeof input.status === 'string' ? input.status : 'failed', nextStep: typeof input.nextStep === 'string' ? input.nextStep : null,
22
+ updatedAt: new Date().toISOString(), ...(typeof input.error === 'string' ? { error: input.error.slice(0, 4000) } : {}),
23
+ metrics: (0, agent_runtime_metrics_1.readRuntimeEfficiency)(input.metrics), efficiencySummary: (0, agent_runtime_metrics_1.applyRuntimeSelectionOrigins)((0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(input.efficiencySummary), selection, context.runId), steps: {} };
24
+ const payload = JSON.stringify({ schemaVersion: 1, contextHash: hash(contextText), result });
25
+ const file = node_path_1.default.join(node_path_1.default.dirname(contextPath), 'desktop-runtime-history.json'), temporary = file + '.' + (0, node_crypto_1.randomUUID)() + '.tmp';
26
+ try {
27
+ node_fs_1.default.writeFileSync(temporary, JSON.stringify({ payload, integrity: hash(payload) }), { mode: 0o600, flag: 'wx' });
28
+ node_fs_1.default.renameSync(temporary, file);
29
+ }
30
+ finally {
31
+ node_fs_1.default.rmSync(temporary, { force: true });
32
+ }
33
+ }
34
+ function readRuntimeHistory(contextPath) {
35
+ const file = node_path_1.default.join(node_path_1.default.dirname(contextPath), 'desktop-runtime-history.json');
36
+ if (!node_fs_1.default.existsSync(file))
37
+ return null;
38
+ if (node_fs_1.default.lstatSync(file).isSymbolicLink() || node_fs_1.default.statSync(file).size > 128 * 1024)
39
+ throw new Error('Historical runtime projection is invalid');
40
+ const envelope = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
41
+ if (typeof envelope.payload !== 'string' || hash(envelope.payload) !== envelope.integrity)
42
+ throw new Error('Historical runtime projection integrity failed');
43
+ const value = JSON.parse(envelope.payload), contextText = node_fs_1.default.readFileSync(contextPath, 'utf8');
44
+ if (value.schemaVersion !== 1 || value.contextHash !== hash(contextText) || value.result?.runId !== JSON.parse(contextText).runId || !['running', 'succeeded', 'paused', 'failed', 'blocked', 'cancelled'].includes(value.result.status))
45
+ throw new Error('Historical runtime projection scope changed');
46
+ return { ...value.result, metrics: (0, agent_runtime_metrics_1.readRuntimeEfficiency)(value.result.metrics), efficiencySummary: (0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(value.result.efficiencySummary), steps: {} };
47
+ }
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.findCoreAgentRuntimeEntry = findCoreAgentRuntimeEntry;
4
4
  exports.resetCoreAgentRuntimeApiCache = resetCoreAgentRuntimeApiCache;
5
5
  exports.loadCoreAgentRuntime = loadCoreAgentRuntime;
6
+ exports.requireRuntimeCapabilities = requireRuntimeCapabilities;
7
+ exports.validateRoleCapabilities = validateRoleCapabilities;
8
+ exports.validateRequestedRoleEfforts = validateRequestedRoleEfforts;
6
9
  exports.findCoreAgentRuntimeCli = findCoreAgentRuntimeCli;
7
10
  const node_fs_1 = require("node:fs");
8
11
  const node_child_process_1 = require("node:child_process");
@@ -11,6 +14,7 @@ const node_path_1 = require("node:path");
11
14
  const core_runtime_1 = require("./core-runtime");
12
15
  const core_node_runtime_1 = require("./core-node-runtime");
13
16
  const win_spawn_1 = require("./util/win-spawn");
17
+ const agent_runtime_package_1 = require("./agent-runtime-package");
14
18
  /** An explicit or bundled installation is authoritative: never silently replace
15
19
  * a broken production bundle with a developer's checkout. */
16
20
  function findCoreAgentRuntimeEntry() {
@@ -42,7 +46,7 @@ function findCoreAgentRuntimeEntry() {
42
46
  }
43
47
  return null;
44
48
  }
45
- /** The api probe is stable for a given CLI file; repeated settings saves need not respawn it.
49
+ /** The api probe is stable for executable package content; settings saves need not respawn it.
46
50
  * Validation stays uncached because its answer depends on the submitted configuration. */
47
51
  const apiProbeCache = new Map();
48
52
  function resetCoreAgentRuntimeApiCache() { apiProbeCache.clear(); }
@@ -57,18 +61,42 @@ async function loadCoreAgentRuntime() {
57
61
  cwd: (0, node_path_1.dirname)(cli), env: (0, win_spawn_1.windowsSpawnEnv)(process.env), input, encoding: 'utf8',
58
62
  timeout: 15_000, maxBuffer: 2 * 1024 * 1024, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
59
63
  });
60
- const stat = (0, node_fs_1.statSync)(cli);
64
+ const fingerprint = (0, agent_runtime_package_1.runtimeEntryFingerprint)(cli);
61
65
  const cached = apiProbeCache.get(cli);
62
- if (!cached || cached.mtimeMs !== stat.mtimeMs || cached.size !== stat.size) {
63
- const api = JSON.parse(invoke(['api']));
66
+ let api = cached?.fingerprint === fingerprint ? cached.api : undefined;
67
+ if (!api) {
68
+ api = JSON.parse(invoke(['api']));
64
69
  if (api.type !== 'runtime-api' || api.apiVersion !== 1) {
65
70
  throw new Error('Incompatible Core agent runtime API; expected version 1. Update the paired Core bundle.');
66
71
  }
67
- apiProbeCache.set(cli, { mtimeMs: stat.mtimeMs, size: stat.size });
72
+ if (api.capabilities !== undefined && (!api.capabilities || typeof api.capabilities !== 'object' || Array.isArray(api.capabilities) || Object.values(api.capabilities).some(value => !Number.isSafeInteger(value) || value < 1)))
73
+ throw new Error('Core returned malformed runtime capabilities');
74
+ if (api.workflowVersions !== undefined && (!Array.isArray(api.workflowVersions) || !api.workflowVersions.every(value => typeof value === 'string' && /^\d+$/.test(value))))
75
+ throw new Error('Core returned malformed workflow versions');
76
+ if (api.runtimeIdentity !== undefined) {
77
+ const identity = api.runtimeIdentity;
78
+ if (!identity || identity.apiVersion !== 1 || !['packageVersion', 'workflowVersion', 'instructionsVersion', 'packageIntegrity'].every(key => typeof identity[key] === 'string') || !/^sha256:[a-f0-9]{64}$/.test(identity.packageIntegrity))
79
+ throw new Error('Core returned malformed runtime identity');
80
+ }
81
+ apiProbeCache.set(cli, { fingerprint, api });
68
82
  }
69
83
  return {
70
84
  RUNTIME_API_VERSION: 1,
85
+ api,
86
+ capabilities(input) {
87
+ if (api.capabilities?.efficientRoleExecution !== 1)
88
+ throw new Error('Installed Core does not support role capability introspection. Update the paired Core package.');
89
+ const result = JSON.parse(invoke(['capabilities', '--stdin'], JSON.stringify(input)));
90
+ return { ...validateRoleCapabilities(result, input), runtimeIdentity: api.runtimeIdentity };
91
+ },
92
+ rolePromptDefaults() {
93
+ const result = JSON.parse(invoke(['prompts']));
94
+ if (result.type !== 'runtime-role-prompts' || !['architect', 'developer', 'reviewer'].every(role => typeof result.defaults?.[role] === 'string' && result.defaults[role].trim()))
95
+ throw new Error('Core role prompt catalog is unavailable. Update the paired Core bundle.');
96
+ return result.defaults;
97
+ },
71
98
  validateRuntimeConfig(input) {
99
+ requireRuntimeCapabilities(api, input);
72
100
  const result = JSON.parse(invoke(['validate', '--stdin'], JSON.stringify(input)));
73
101
  if (result.type !== 'runtime-config-valid')
74
102
  throw new Error('Core did not validate the runtime configuration');
@@ -76,6 +104,63 @@ async function loadCoreAgentRuntime() {
76
104
  },
77
105
  };
78
106
  }
107
+ function requireRuntimeCapabilities(api, input) {
108
+ if (!input || typeof input !== 'object' || Array.isArray(input))
109
+ return;
110
+ const config = input;
111
+ if ((config.efficiency !== undefined || Object.values(config.agents ?? {}).some(role => role?.effort !== undefined || role?.escalation !== undefined)) && api.capabilities?.efficientRoleExecution !== 1)
112
+ throw new Error('Requested role efficiency policy is unsupported by this Core runtime. Update the paired Core package.');
113
+ if (Array.isArray(config.verification) && config.verification.some(check => check?.policy !== undefined || check?.key !== undefined || check?.label !== undefined) && api.capabilities?.reproducibleVerification !== 1)
114
+ throw new Error('Requested verification policy is unsupported by this Core runtime. Update the paired Core package.');
115
+ }
116
+ /** Validate transport evidence against the exact submitted role selections. */
117
+ function validateRoleCapabilities(value, input) {
118
+ const invalid = () => { throw new Error('Core returned malformed role capabilities'); };
119
+ const object = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : invalid();
120
+ const result = object(value), config = object(input);
121
+ const choices = Object.entries(object(config.agents));
122
+ if (result.type !== 'runtime-capabilities' || result.schemaVersion !== 1 || !Array.isArray(result.roles) || result.roles.length > 6)
123
+ return invalid();
124
+ const expected = choices.flatMap(([role, value]) => {
125
+ const selected = object(value);
126
+ return [['base', selected], ...(selected.escalation ? [['escalation', object(selected.escalation)]] : [])].map(([tier, choice]) => {
127
+ const model = choice;
128
+ return { role, tier: String(tier), provider: selected.provider, model: model.model ?? null, requestedEffort: model.effort ?? null };
129
+ });
130
+ });
131
+ if (expected.length !== result.roles.length || expected.length < 3)
132
+ return invalid();
133
+ const seen = new Set();
134
+ const roles = result.roles.map(value => {
135
+ const row = object(value);
136
+ const selected = expected.find(item => Object.entries(item).every(([key, value]) => row[key] === value));
137
+ if (!selected)
138
+ return invalid();
139
+ const key = String(row.role) + ':' + String(row.tier);
140
+ if (seen.has(key))
141
+ return invalid();
142
+ seen.add(key);
143
+ if (typeof row.transport !== 'string' || !row.transport || row.transport.length > 128 || !['supported', 'unsupported', 'unknown'].includes(String(row.continuation)) || !['supported', 'unsupported', 'unknown'].includes(String(row.effortSupport)) || typeof row.observedModel !== 'boolean' || typeof row.observedEffort !== 'boolean')
144
+ return invalid();
145
+ if (row.supportedEfforts !== null && (!Array.isArray(row.supportedEfforts) || row.supportedEfforts.length > 32 || !row.supportedEfforts.every((level) => typeof level === 'string' && /^[a-z][a-z0-9_-]{0,31}$/.test(level))))
146
+ return invalid();
147
+ const levels = row.supportedEfforts;
148
+ if (row.effortSupport === 'supported' && !levels?.length || row.effortSupport === 'unknown' && levels !== null || row.effortSupport === 'unsupported' && levels?.length !== 0)
149
+ return invalid();
150
+ return { ...selected, transport: row.transport, continuation: row.continuation, effortSupport: row.effortSupport, supportedEfforts: levels, observedModel: row.observedModel, observedEffort: row.observedEffort };
151
+ });
152
+ return { type: 'runtime-capabilities', schemaVersion: 1, roles };
153
+ }
154
+ function validateRequestedRoleEfforts(runtime, config) {
155
+ if (!Object.values(config.agents).some(role => role.effort !== undefined || role.escalation?.effort !== undefined))
156
+ return;
157
+ if (!runtime.capabilities)
158
+ throw new Error('Installed Core cannot validate requested role effort');
159
+ const result = validateRoleCapabilities(runtime.capabilities(config), config);
160
+ for (const row of result.roles)
161
+ if (row.requestedEffort !== null && (row.effortSupport !== 'supported' || !row.supportedEfforts?.includes(String(row.requestedEffort))))
162
+ throw new Error(`${row.role} (${row.tier}): effort '${row.requestedEffort}' is not confirmed for ${row.transport}. Select provider default or a supported effort.`);
163
+ }
79
164
  function findCoreAgentRuntimeCli() {
80
165
  const entry = findCoreAgentRuntimeEntry();
81
166
  if (!entry)