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,341 @@
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.AgentRuntimeControls = exports.RuntimeControlError = void 0;
7
+ exports.validateRuntimeResumeInput = validateRuntimeResumeInput;
8
+ exports.readAgentRuntimeStatus = readAgentRuntimeStatus;
9
+ const agent_runtime_settlement_1 = require("./agent-runtime-settlement");
10
+ const agent_runtime_metrics_1 = require("./agent-runtime-metrics");
11
+ const node_child_process_1 = require("node:child_process");
12
+ const node_crypto_1 = require("node:crypto");
13
+ const node_fs_1 = __importDefault(require("node:fs"));
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ const node_util_1 = require("node:util");
16
+ const agent_runtime_loader_1 = require("./agent-runtime-loader");
17
+ const agent_runtime_bridge_1 = require("./agent-runtime-bridge");
18
+ const core_node_runtime_1 = require("./core-node-runtime");
19
+ const win_spawn_1 = require("./util/win-spawn");
20
+ const workspace_resolution_1 = require("./workspace-resolution");
21
+ const loop_runs_store_1 = require("./loop-runs-store");
22
+ const multi_repo_execution_store_1 = require("./multi-repo-execution-store");
23
+ const db_1 = require("./db");
24
+ const loop_run_manager_1 = require("./loop-run-manager");
25
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
26
+ const STEP_IDS = ['architect', 'developer', 'verify', 'reviewer', 'archive'];
27
+ const ANSWER_LIMIT = 20_000;
28
+ class RuntimeControlError extends Error {
29
+ statusCode;
30
+ code;
31
+ constructor(statusCode, code, message) {
32
+ super(message);
33
+ this.statusCode = statusCode;
34
+ this.code = code;
35
+ }
36
+ }
37
+ exports.RuntimeControlError = RuntimeControlError;
38
+ function validateRuntimeResumeInput(input) {
39
+ if (!input || typeof input !== 'object' || Array.isArray(input))
40
+ throw new RuntimeControlError(400, 'invalid_resume_request', 'Resume request must be an object');
41
+ const body = input;
42
+ for (const [key, value] of Object.entries(body)) {
43
+ if (key === 'answer') {
44
+ if (typeof value !== 'string' || !value.trim() || value.length > ANSWER_LIMIT)
45
+ throw new RuntimeControlError(400, 'invalid_resume_request', `An answer must be a nonempty string of at most ${ANSWER_LIMIT} characters`);
46
+ continue;
47
+ }
48
+ if (!['approve', 'recover', 'invalidate'].includes(key) || !Array.isArray(value) || value.length > STEP_IDS.length || !value.every((id) => typeof id === 'string' && STEP_IDS.includes(id)) || new Set(value).size !== value.length)
49
+ throw new RuntimeControlError(400, 'invalid_resume_request', 'Only approve, recover and invalidate arrays of known phase IDs are allowed');
50
+ }
51
+ return body;
52
+ }
53
+ /** A question stays pending until Core records its answer. */
54
+ function openQuestion(state) {
55
+ return state.pendingQuestion && state.pendingQuestion.answeredAt === undefined ? state.pendingQuestion : undefined;
56
+ }
57
+ /** Status is a read-only Core CLI operation. It never invokes a provider. */
58
+ async function readAgentRuntimeStatus(contextPath, cwd, env) {
59
+ const cli = (0, agent_runtime_loader_1.findCoreAgentRuntimeCli)();
60
+ if (!cli)
61
+ throw new RuntimeControlError(503, 'runtime_unavailable', 'Update Core to inspect or resume agent runtime executions');
62
+ 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
+ cwd, env: (0, win_spawn_1.windowsSpawnEnv)(env), windowsHide: true, timeout: 15000, maxBuffer: 4 * 1024 * 1024, encoding: 'utf8',
64
+ });
65
+ const result = JSON.parse(stdout);
66
+ if (result.type !== 'runtime-status' || result.state === undefined)
67
+ throw new Error('Core returned an invalid runtime status');
68
+ return result.state;
69
+ }
70
+ /** One controller per ProjectContext; Core's durable lease remains the final
71
+ * cross-process guard. Resume never constructs a new context or worktree. */
72
+ class AgentRuntimeControls {
73
+ ctx;
74
+ dependencies;
75
+ isActive(runId) { return this.active.has(runId); }
76
+ activeRunIds() { return [...this.active.keys()]; }
77
+ active = new Map();
78
+ errors = new Map();
79
+ statusCache = new Map();
80
+ disposed = false;
81
+ constructor(ctx, dependencies = { status: readAgentRuntimeStatus, execute: agent_runtime_bridge_1.runAgentRuntimeInvocation, kill: win_spawn_1.treeKillSafe, settle: agent_runtime_settlement_1.settleRuntimeContinuation }) {
82
+ this.ctx = ctx;
83
+ this.dependencies = dependencies;
84
+ }
85
+ context(runId) {
86
+ if (!SAFE_ID.test(runId))
87
+ throw new RuntimeControlError(400, 'invalid_run_id', 'Invalid runtime run ID');
88
+ const execution = (0, workspace_resolution_1.resolveProjectExecution)(this.ctx.project);
89
+ const backlogRoot = node_fs_1.default.realpathSync(node_path_1.default.dirname(execution.specrailsDir));
90
+ const directory = node_path_1.default.join(execution.specrailsDir, 'pipeline', runId);
91
+ const file = node_path_1.default.join(directory, 'desktop-context.json');
92
+ if (!node_fs_1.default.existsSync(file))
93
+ throw new RuntimeControlError(404, 'runtime_run_not_found', 'Runtime execution context is unavailable');
94
+ if (node_fs_1.default.realpathSync(file) !== node_path_1.default.join(node_fs_1.default.realpathSync(execution.specrailsDir), 'pipeline', runId, 'desktop-context.json'))
95
+ throw new RuntimeControlError(409, 'runtime_scope_changed', 'Runtime context path no longer matches its project');
96
+ const frozen = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
97
+ try {
98
+ 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))
99
+ throw new Error();
100
+ for (const root of [frozen.artifactRoot, ...frozen.repositories.map((repository) => repository.path)]) {
101
+ if (!node_path_1.default.isAbsolute(root) || node_fs_1.default.realpathSync(root) !== root || !node_fs_1.default.statSync(root).isDirectory())
102
+ throw new Error();
103
+ }
104
+ }
105
+ catch {
106
+ throw new RuntimeControlError(409, 'runtime_scope_unavailable', 'The original runtime worktree or project scope is unavailable. Start a new implementation.');
107
+ }
108
+ const stored = this.ctx.db.prepare('SELECT execution_manifest FROM loop_runs WHERE id = ?').get(runId);
109
+ const manifest = (0, multi_repo_execution_store_1.readExecutionManifest)(stored?.execution_manifest);
110
+ if (manifest && (manifest.projectId !== this.ctx.project.id || manifest.repositories.length !== frozen.repositories.length || !manifest.repositories.every((repository) => frozen.repositories.some((item) => item.id === repository.repositoryId && item.path === repository.worktreePath))))
111
+ throw new RuntimeControlError(409, 'runtime_scope_changed', 'The saved worktree manifest differs from the frozen runtime scope');
112
+ const hostFile = node_path_1.default.join(directory, 'desktop-runtime-host.json');
113
+ if (!node_fs_1.default.existsSync(hostFile))
114
+ throw new RuntimeControlError(409, 'runtime_host_unavailable', 'The original runtime host settings are missing. Start a new implementation.');
115
+ const host = JSON.parse(node_fs_1.default.readFileSync(hostFile, 'utf8'));
116
+ if (host.schemaVersion !== 1 || typeof host.cwd !== 'string' || ![frozen.backlogRoot, ...frozen.repositories.map((repository) => repository.path)].includes(host.cwd) || !host.env || typeof host.env !== 'object' || Array.isArray(host.env) || Object.entries(host.env).some(([key, value]) => !agent_runtime_bridge_1.RUNTIME_HOST_ENV_KEYS.includes(key) || typeof value !== 'string'))
117
+ throw new RuntimeControlError(409, 'runtime_host_invalid', 'The saved runtime host settings are invalid');
118
+ const env = { ...(0, workspace_resolution_1.resolveLoopBaseEnv)(this.ctx.project) };
119
+ for (const key of agent_runtime_bridge_1.RUNTIME_HOST_ENV_KEYS)
120
+ delete env[key];
121
+ Object.assign(env, host.env, { SPECRAILS_EXECUTION_CONTEXT: file });
122
+ return { file, frozen, cwd: host.cwd, env };
123
+ }
124
+ async list() {
125
+ const directory = node_path_1.default.join((0, workspace_resolution_1.resolveProjectExecution)(this.ctx.project).specrailsDir, 'pipeline');
126
+ if (!node_fs_1.default.existsSync(directory))
127
+ return [];
128
+ const entries = node_fs_1.default.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory() && SAFE_ID.test(entry.name) && node_fs_1.default.existsSync(node_path_1.default.join(directory, entry.name, 'agent-runtime-request.json')))
129
+ .sort((a, b) => node_fs_1.default.statSync(node_path_1.default.join(directory, b.name)).mtimeMs - node_fs_1.default.statSync(node_path_1.default.join(directory, a.name)).mtimeMs).slice(0, 20);
130
+ const runs = [];
131
+ // Bounded sequential reads avoid spawning one Node process per historical run at once.
132
+ for (const entry of entries)
133
+ runs.push(await this.summary(entry.name));
134
+ return runs;
135
+ }
136
+ async summary(runId) {
137
+ try {
138
+ const { file, cwd, env } = this.context(runId);
139
+ // Core owns checkpoint parsing. Its atomic file revision is only a cache
140
+ // key, keeping idle settings panes from spawning CLI processes repeatedly.
141
+ const checkpoint = node_path_1.default.join(node_path_1.default.dirname(file), 'agent-workflow', runId, 'checkpoint.json');
142
+ const stat = node_fs_1.default.existsSync(checkpoint) ? node_fs_1.default.statSync(checkpoint) : null;
143
+ const fingerprint = stat ? `${stat.mtimeMs}:${stat.size}` : null;
144
+ const cached = this.statusCache.get(runId);
145
+ const state = fingerprint && cached?.fingerprint === fingerprint ? cached.state : await this.dependencies.status(file, cwd, env);
146
+ if (!state || state.runId !== runId)
147
+ throw new Error('No saved workflow state is available');
148
+ if (fingerprint)
149
+ this.statusCache.set(runId, { fingerprint, state });
150
+ const parent = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
151
+ const active = this.active.has(runId) || parent?.status === 'running' || parent?.status === 'paused';
152
+ const recoverableSteps = Object.entries(state.steps).filter(([, step]) => ['running', 'interrupted'].includes(step.status)).map(([id]) => id);
153
+ return { runId, status: state.status === 'running' && !active ? 'interrupted' : state.status, nextStep: state.nextStep,
154
+ updatedAt: state.updatedAt, error: this.errors.get(runId) ?? state.error, pendingApproval: state.pendingApproval,
155
+ traceId: state.traceId, pendingQuestion: openQuestion(state),
156
+ metrics: (0, agent_runtime_metrics_1.readRuntimeEfficiency)(state.metrics),
157
+ canSettle: !active && state.status === 'succeeded' && this.ctx.db.prepare('SELECT status FROM jobs WHERE id = ?').get(runId)?.status !== 'completed',
158
+ recoverableSteps, active, canCancel: this.active.has(runId), canResume: !active && parent?.status === 'completed' && state.status !== 'succeeded' };
159
+ }
160
+ catch (error) {
161
+ 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' };
162
+ }
163
+ }
164
+ async resume(runId, input) {
165
+ if (this.disposed)
166
+ throw new RuntimeControlError(503, 'runtime_shutting_down', 'Project runtime is shutting down');
167
+ const parent = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
168
+ if (!parent || parent.status !== 'completed' || this.active.has(runId))
169
+ throw new RuntimeControlError(409, 'runtime_run_active', 'Wait for the original Desktop execution to settle before resuming');
170
+ if (parent.rail_index != null && [...(this.ctx.railLoopRuns?.values() ?? []), ...(this.ctx.railJobs?.values() ?? [])].some(meta => meta.railIndex === parent.rail_index))
171
+ throw new RuntimeControlError(409, 'runtime_rail_active', 'Wait for the implementation card to finish its active job before resuming');
172
+ const context = this.context(runId);
173
+ // Reserve before awaiting status so concurrent resume requests cannot race.
174
+ const active = { cancelled: false };
175
+ this.active.set(runId, active);
176
+ try {
177
+ // Accounting from a prior continuation must settle even when Core now
178
+ // reports success and correctly refuses any further execution.
179
+ (0, loop_run_manager_1.recoverOrphanLoopStepAccounting)(this.ctx.db, new Date().toISOString(), runId);
180
+ const state = await this.dependencies.status(context.file, context.cwd, context.env);
181
+ if (!state || state.runId !== runId || state.status === 'succeeded')
182
+ throw new RuntimeControlError(409, 'runtime_not_resumable', 'This execution has no resumable workflow');
183
+ if (parent.rail_index != null && [...(this.ctx.railLoopRuns?.values() ?? []), ...(this.ctx.railJobs?.values() ?? [])].some(meta => meta.railIndex === parent.rail_index))
184
+ throw new RuntimeControlError(409, 'runtime_rail_active', 'The implementation card became active while checking the saved execution');
185
+ // A pending question resumes the architect only with the operator's answer.
186
+ if (openQuestion(state) && !input.answer)
187
+ throw new RuntimeControlError(400, 'answer_required', 'This execution is waiting for an answer to the architect\'s question');
188
+ this.errors.delete(runId);
189
+ this.statusCache.delete(runId);
190
+ const startedAt = new Date().toISOString();
191
+ const invocationId = (0, node_crypto_1.randomUUID)();
192
+ const stepKey = `runtime-resume:${invocationId}`;
193
+ // Both live settlement and restart use the existing loop ledger.
194
+ const previous = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
195
+ let sequence = this.ctx.db.prepare('SELECT COALESCE(MAX(seq), -1) AS seq FROM events WHERE job_id = ?').get(runId).seq;
196
+ const startedAtMs = Date.parse(startedAt);
197
+ let ticketIds = [];
198
+ try {
199
+ const ids = JSON.parse(previous.ticket_ids_json ?? '[]');
200
+ if (Array.isArray(ids))
201
+ ticketIds = ids.filter((id) => typeof id === 'number' && Number.isSafeInteger(id));
202
+ }
203
+ catch { /* Older loop records can lack a ticket scope. */ }
204
+ (0, loop_runs_store_1.stageLoopStepRecovery)(this.ctx.db, {
205
+ version: 1, runId, stepKey, invocationId, projectId: this.ctx.project.id,
206
+ provider: 'agent-runtime', model: 'per-role', surfaceRefId: stepKey,
207
+ ticketIds, startedAt, baseline: (0, loop_runs_store_1.readLoopJobUsage)(this.ctx.db, runId),
208
+ completedEventSeq: sequence, providerCostBaseline: 0, providerTurnsBaseline: 0,
209
+ loopDurationBaseline: previous.total_duration_ms, completedDurationMs: 0,
210
+ iterationCount: previous.iteration_count, activeTurnStartedAtMs: startedAtMs, lastActivityAtMs: startedAtMs,
211
+ });
212
+ const logLine = (line, source = 'stdout') => {
213
+ if (this.disposed)
214
+ return;
215
+ try {
216
+ (0, db_1.appendEvent)(this.ctx.db, runId, ++sequence, { event_type: 'log', source, payload: JSON.stringify({ line: line.replace(/\n$/, '') }) });
217
+ }
218
+ catch { /* events table best-effort */ }
219
+ try {
220
+ this.ctx.broadcast?.({ type: 'log', source, line, timestamp: new Date().toISOString(), processId: runId });
221
+ }
222
+ catch { /* persisted log remains authoritative */ }
223
+ };
224
+ if (parent.rail_index != null)
225
+ this.ctx.railLoopRuns?.set(runId, { railIndex: parent.rail_index, ticketIds, requiresTerminalIntent: true });
226
+ const publishActivity = (running) => {
227
+ try {
228
+ this.ctx.broadcast?.({ type: 'runtime.continuation', projectId: this.ctx.project.id, jobId: runId, railIndex: parent.rail_index, active: running });
229
+ }
230
+ catch { /* durable log remains available */ }
231
+ };
232
+ publishActivity(true);
233
+ 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'] : [])];
234
+ logLine(`[runtime] continuation started from phase ${state.nextStep ?? 'end'}${requested.length ? ' (' + requested.join('; ') + ')' : ''}; the original worktree and frozen scope are reused`);
235
+ void this.dependencies.execute({
236
+ contextPath: context.file, cwd: context.cwd, env: context.env, resume: true, ...input,
237
+ // Readable progress lands in the same job log as the original rail run.
238
+ onLine: logLine,
239
+ onRawLine: (line) => {
240
+ if (this.disposed)
241
+ return;
242
+ let eventType = 'agent-runtime';
243
+ try {
244
+ const parsed = JSON.parse(line);
245
+ if (typeof parsed.type === 'string')
246
+ eventType = parsed.type;
247
+ }
248
+ catch { /* Keep raw diagnostics available to recovery. */ }
249
+ this.ctx.db.transaction(() => {
250
+ (0, db_1.appendEvent)(this.ctx.db, runId, ++sequence, { event_type: eventType, source: 'stdout', payload: line });
251
+ (0, loop_runs_store_1.updateLoopStepActivityCheckpoint)(this.ctx.db, runId, stepKey, undefined, Date.now());
252
+ })();
253
+ try {
254
+ this.ctx.broadcast?.({ type: 'event', jobId: runId, event_type: eventType, source: 'stdout', payload: line, seq: sequence, timestamp: new Date().toISOString() });
255
+ }
256
+ catch { /* persisted event remains authoritative */ }
257
+ },
258
+ onSpawn: (child) => {
259
+ active.child = child;
260
+ if (this.disposed && child.pid)
261
+ this.dependencies.kill(child.pid, 'SIGKILL');
262
+ else if (active.cancelled)
263
+ this.cancel(runId);
264
+ },
265
+ }).then(async (result) => {
266
+ if (this.disposed)
267
+ return;
268
+ logLine(result.failed ? `[runtime] continuation ended: ${result.errorText ?? 'failed'}` : '[runtime] continuation completed', result.failed ? 'stderr' : 'stdout');
269
+ (0, loop_runs_store_1.setLoopStepSettledResult)(this.ctx.db, runId, stepKey, { ...result, provider: 'agent-runtime', model: 'per-role', failed: active.cancelled || result.failed });
270
+ (0, loop_run_manager_1.recoverOrphanLoopStepAccounting)(this.ctx.db, new Date().toISOString(), runId);
271
+ if (result.errorText)
272
+ this.errors.set(runId, result.errorText);
273
+ if (!result.failed && !active.cancelled)
274
+ 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 });
275
+ }).catch(() => {
276
+ if (this.disposed)
277
+ return;
278
+ const message = 'Runtime continuation failed. Inspect the saved phase state before retrying.';
279
+ this.errors.set(runId, message);
280
+ logLine(`[runtime] ${message}`, 'stderr');
281
+ })
282
+ .finally(() => {
283
+ clearTimeout(active.forceKillTimer);
284
+ this.active.delete(runId);
285
+ this.ctx.railLoopRuns?.delete(runId);
286
+ this.statusCache.delete(runId);
287
+ publishActivity(false);
288
+ });
289
+ }
290
+ catch (error) {
291
+ this.active.delete(runId);
292
+ this.ctx.railLoopRuns?.delete(runId);
293
+ try {
294
+ this.ctx.broadcast?.({ type: 'runtime.continuation', projectId: this.ctx.project.id, jobId: runId, railIndex: parent.rail_index, active: false });
295
+ }
296
+ catch { /* admission failed */ }
297
+ throw error;
298
+ }
299
+ }
300
+ async settle(runId) {
301
+ if (this.active.has(runId) || (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId)?.status !== 'completed')
302
+ throw new RuntimeControlError(409, 'runtime_run_active', 'Wait for the execution to settle');
303
+ const context = this.context(runId);
304
+ this.active.set(runId, { cancelled: false });
305
+ try {
306
+ 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 });
307
+ this.errors.delete(runId);
308
+ }
309
+ catch (error) {
310
+ throw new RuntimeControlError(409, 'runtime_settlement_blocked', error instanceof Error ? error.message : 'Could not prepare the recovered delivery');
311
+ }
312
+ finally {
313
+ this.active.delete(runId);
314
+ this.statusCache.delete(runId);
315
+ 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 });
316
+ }
317
+ }
318
+ cancel(runId) {
319
+ const active = this.active.get(runId);
320
+ if (!active)
321
+ throw new RuntimeControlError(409, 'runtime_not_active', 'Only a continuation started here can be cancelled here; stop active rail runs from their job controls');
322
+ active.cancelled = true;
323
+ if (active.child?.pid) {
324
+ this.dependencies.kill(active.child.pid, this.disposed ? 'SIGKILL' : 'SIGTERM');
325
+ if (!this.disposed && !active.forceKillTimer) {
326
+ const pid = active.child.pid;
327
+ active.forceKillTimer = setTimeout(() => {
328
+ if (this.active.get(runId) === active)
329
+ this.dependencies.kill(pid, 'SIGKILL');
330
+ }, 3000);
331
+ active.forceKillTimer.unref?.();
332
+ }
333
+ }
334
+ }
335
+ shutdown() {
336
+ this.disposed = true;
337
+ for (const runId of this.active.keys())
338
+ this.cancel(runId);
339
+ }
340
+ }
341
+ exports.AgentRuntimeControls = AgentRuntimeControls;
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findCoreAgentRuntimeEntry = findCoreAgentRuntimeEntry;
4
+ exports.resetCoreAgentRuntimeApiCache = resetCoreAgentRuntimeApiCache;
5
+ exports.loadCoreAgentRuntime = loadCoreAgentRuntime;
6
+ exports.findCoreAgentRuntimeCli = findCoreAgentRuntimeCli;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_child_process_1 = require("node:child_process");
9
+ const node_module_1 = require("node:module");
10
+ const node_path_1 = require("node:path");
11
+ const core_runtime_1 = require("./core-runtime");
12
+ const core_node_runtime_1 = require("./core-node-runtime");
13
+ const win_spawn_1 = require("./util/win-spawn");
14
+ /** An explicit or bundled installation is authoritative: never silently replace
15
+ * a broken production bundle with a developer's checkout. */
16
+ function findCoreAgentRuntimeEntry() {
17
+ const explicit = process.env.SPECRAILS_CORE_RUNTIME_PATH;
18
+ if (explicit) {
19
+ const entry = (0, node_path_1.resolve)(explicit);
20
+ return (0, node_fs_1.existsSync)(entry) ? entry : null;
21
+ }
22
+ const selected = (0, core_runtime_1.getCoreRuntimeStatus)();
23
+ if (selected.error)
24
+ return null;
25
+ if (selected.runtime) {
26
+ const entry = (0, node_path_1.join)(selected.runtime.root, 'dist', 'agent-runtime', 'index.js');
27
+ if ((0, node_fs_1.existsSync)(entry))
28
+ return entry;
29
+ // Match Desktop's activated Core package, including managed upgrades. A
30
+ // missing API must never load an older bundle behind an active framework.
31
+ if (process.env.NODE_ENV === 'production' || ['managed', 'override', 'bundled'].includes(selected.runtime.source))
32
+ return null;
33
+ }
34
+ try {
35
+ return (0, node_module_1.createRequire)((0, node_path_1.join)(process.cwd(), 'package.json')).resolve('specrails-core/agent-runtime');
36
+ }
37
+ catch { /* A source checkout can use the sibling Core build. */ }
38
+ if (process.env.NODE_ENV !== 'production') {
39
+ const entry = (0, node_path_1.resolve)(process.cwd(), '..', 'specrails-core', 'dist', 'agent-runtime', 'index.js');
40
+ if ((0, node_fs_1.existsSync)(entry))
41
+ return entry;
42
+ }
43
+ return null;
44
+ }
45
+ /** The api probe is stable for a given CLI file; repeated settings saves need not respawn it.
46
+ * Validation stays uncached because its answer depends on the submitted configuration. */
47
+ const apiProbeCache = new Map();
48
+ function resetCoreAgentRuntimeApiCache() { apiProbeCache.clear(); }
49
+ async function loadCoreAgentRuntime() {
50
+ const cli = findCoreAgentRuntimeCli();
51
+ if (!cli)
52
+ throw new Error('Programmatic agent runtime is unavailable. Build or bundle a compatible specrails-core release.');
53
+ // Core is ESM. Desktop ships both CJS and a pkg sidecar; importing external
54
+ // ESM from either depends on host-loader details. The bundled ordinary Node
55
+ // process is the same portable boundary used for execution and inspection.
56
+ const invoke = (args, input) => (0, node_child_process_1.execFileSync)((0, core_node_runtime_1.resolveCoreNodeRuntime)(), [cli, ...args], {
57
+ cwd: (0, node_path_1.dirname)(cli), env: (0, win_spawn_1.windowsSpawnEnv)(process.env), input, encoding: 'utf8',
58
+ timeout: 15_000, maxBuffer: 2 * 1024 * 1024, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
59
+ });
60
+ const stat = (0, node_fs_1.statSync)(cli);
61
+ const cached = apiProbeCache.get(cli);
62
+ if (!cached || cached.mtimeMs !== stat.mtimeMs || cached.size !== stat.size) {
63
+ const api = JSON.parse(invoke(['api']));
64
+ if (api.type !== 'runtime-api' || api.apiVersion !== 1) {
65
+ throw new Error('Incompatible Core agent runtime API; expected version 1. Update the paired Core bundle.');
66
+ }
67
+ apiProbeCache.set(cli, { mtimeMs: stat.mtimeMs, size: stat.size });
68
+ }
69
+ return {
70
+ RUNTIME_API_VERSION: 1,
71
+ rolePromptDefaults() {
72
+ const result = JSON.parse(invoke(['prompts']));
73
+ if (result.type !== 'runtime-role-prompts' || !['architect', 'developer', 'reviewer'].every(role => typeof result.defaults?.[role] === 'string' && result.defaults[role].trim()))
74
+ throw new Error('Core role prompt catalog is unavailable. Update the paired Core bundle.');
75
+ return result.defaults;
76
+ },
77
+ validateRuntimeConfig(input) {
78
+ const result = JSON.parse(invoke(['validate', '--stdin'], JSON.stringify(input)));
79
+ if (result.type !== 'runtime-config-valid')
80
+ throw new Error('Core did not validate the runtime configuration');
81
+ return input;
82
+ },
83
+ };
84
+ }
85
+ function findCoreAgentRuntimeCli() {
86
+ const entry = findCoreAgentRuntimeEntry();
87
+ if (!entry)
88
+ return null;
89
+ const cli = (0, node_path_1.join)((0, node_path_1.dirname)(entry), 'cli.js');
90
+ return (0, node_fs_1.existsSync)(cli) ? cli : null;
91
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readRuntimeEfficiency = readRuntimeEfficiency;
4
+ const COUNTS = ['attempts', 'measuredAttempts', 'providerCalls', 'toolCalls', 'inputTokens', 'outputTokens', 'uncachedInputTokens', 'cacheReadInputTokens', 'cacheWriteInputTokens'];
5
+ const KEYS = [...COUNTS, 'durationMs', 'agentDurationMs', 'costUsd'];
6
+ const PHASES = ['architect', 'developer', 'verify', 'reviewer', 'archive'];
7
+ function object(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null; }
8
+ function counters(value) {
9
+ const input = object(value);
10
+ if (!input)
11
+ return null;
12
+ const output = {};
13
+ for (const key of KEYS) {
14
+ const number = input[key];
15
+ if (number === null && key !== 'attempts' && key !== 'measuredAttempts') {
16
+ output[key] = null;
17
+ continue;
18
+ }
19
+ if (typeof number !== 'number' || !Number.isFinite(number) || number < 0 || (COUNTS.includes(key) && !Number.isSafeInteger(number)))
20
+ return null;
21
+ output[key] = number;
22
+ }
23
+ if (output.measuredAttempts > output.attempts)
24
+ return null;
25
+ return output;
26
+ }
27
+ /** Older Core versions omit metrics; malformed/unknown versions do not break run controls. */
28
+ function readRuntimeEfficiency(value) {
29
+ const input = object(value);
30
+ if (input?.schemaVersion !== 1 || !Array.isArray(input.phases) || input.phases.length > PHASES.length)
31
+ return undefined;
32
+ const total = counters(input.total);
33
+ if (!total)
34
+ return undefined;
35
+ const phases = [];
36
+ for (const raw of input.phases) {
37
+ const phase = object(raw), count = counters(phase);
38
+ if (!phase || !count || typeof phase.stepId !== 'string' || !PHASES.includes(phase.stepId) || phases.some(item => item.stepId === phase.stepId))
39
+ return undefined;
40
+ for (const key of ['providers', 'models'])
41
+ if (!Array.isArray(phase[key]) || phase[key].length > 50 || !phase[key].every((item) => typeof item === 'string' && item.length > 0 && item.length <= 256))
42
+ return undefined;
43
+ phases.push({ ...count, stepId: phase.stepId, providers: [...phase.providers], models: [...phase.models] });
44
+ }
45
+ return { schemaVersion: 1, total, phases };
46
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hasAgentRuntimeRequest = hasAgentRuntimeRequest;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const workspace_resolution_1 = require("./workspace-resolution");
7
+ function hasAgentRuntimeRequest(project, runId) {
8
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(runId))
9
+ return false;
10
+ const file = (0, node_path_1.join)((0, workspace_resolution_1.resolveProjectExecution)(project).specrailsDir, 'pipeline', runId, 'agent-runtime-request.json');
11
+ return (0, node_fs_1.existsSync)(file);
12
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toolRepositories = toolRepositories;
4
+ const node_path_1 = require("node:path");
5
+ /** Attribute only explicit paths or repository roots present in a tool action.
6
+ * Never carry a previous action's repository into an unrelated tool call. */
7
+ function toolRepositories(event, repositories, defaultCwd) {
8
+ const found = new Set();
9
+ const cwd = typeof event.cwd === 'string' && (0, node_path_1.isAbsolute)(event.cwd) ? event.cwd : defaultCwd;
10
+ const paths = Array.isArray(event.targetPaths) ? event.targetPaths.filter((p) => typeof p === 'string') : [];
11
+ if (!paths.length && typeof event.detail === 'string' && (0, node_path_1.isAbsolute)(event.detail) && !event.detail.endsWith('…'))
12
+ paths.push(event.detail);
13
+ for (const target of paths) {
14
+ const absolute = (0, node_path_1.resolve)(cwd, target);
15
+ const matches = repositories.filter(repo => {
16
+ if (!repo.path)
17
+ return false;
18
+ const suffix = (0, node_path_1.relative)(repo.path, absolute);
19
+ return suffix === '' || (!(0, node_path_1.isAbsolute)(suffix) && suffix !== '..' && !suffix.startsWith('..' + node_path_1.sep));
20
+ }).sort((a, b) => b.path.length - a.path.length);
21
+ if (matches[0])
22
+ found.add(matches[0].id);
23
+ }
24
+ // Shell activity may contain `cd /repo && ...` or address several repositories.
25
+ if (!paths.length && typeof event.detail === 'string') {
26
+ for (const repo of repositories) {
27
+ if (!repo.path)
28
+ continue;
29
+ const escaped = repo.path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
30
+ if (new RegExp('(?:^|[\\s\\x22\\x27=])' + escaped + '(?=$|[\\s/\\\\\\x22\\x27;&])').test(event.detail))
31
+ found.add(repo.id);
32
+ }
33
+ }
34
+ if (!found.size && !paths.length && typeof event.cwd === 'string' && (0, node_path_1.isAbsolute)(event.cwd))
35
+ return toolRepositories({ targetPaths: [event.cwd] }, repositories, defaultCwd);
36
+ return repositories.filter(repo => found.has(repo.id));
37
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAgentRuntimeSettingsRoutes = registerAgentRuntimeSettingsRoutes;
4
+ const node_fs_1 = require("node:fs");
5
+ const agent_runtime_loader_1 = require("./agent-runtime-loader");
6
+ const agent_runtime_settings_1 = require("./agent-runtime-settings");
7
+ const agent_runtime_verification_suggestions_1 = require("./agent-runtime-verification-suggestions");
8
+ const project_repositories_1 = require("./project-repositories");
9
+ function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
10
+ router.get('/:projectId/agent-runtime/config', (_req, res) => {
11
+ try {
12
+ const project = ctx(_req).project;
13
+ const configured = (0, node_fs_1.existsSync)((0, agent_runtime_settings_1.agentRuntimeConfigPath)(project));
14
+ const config = (0, agent_runtime_settings_1.loadAgentRuntimeConfig)(project);
15
+ res.json({ configured, config, runtimeAvailable: (0, agent_runtime_loader_1.findCoreAgentRuntimeEntry)() !== null });
16
+ }
17
+ catch (err) {
18
+ const validation = err instanceof agent_runtime_settings_1.AgentRuntimeConfigError;
19
+ res.status(validation ? 422 : 500).json({ error: validation ? 'invalid_runtime_config' : 'runtime_config_read_failed', message: validation ? err.message : 'Could not read runtime configuration' });
20
+ }
21
+ });
22
+ /** Offline detection of each repository's own checks; never invokes a model.
23
+ * Verification stays optional: the architect proposes commands for uncovered
24
+ * repositories at run time, and Core admits repositories without any check. */
25
+ router.get('/:projectId/agent-runtime/verification-suggestions', (req, res) => {
26
+ try {
27
+ const project = ctx(req).project;
28
+ const repositories = (0, project_repositories_1.getProjectRepositories)(project).map((repository) => ({ id: repository.id, name: repository.name, path: repository.path }));
29
+ res.json({ repositories: repositories.map(({ id, name }) => ({ id, name })), suggestions: (0, agent_runtime_verification_suggestions_1.suggestVerificationCommands)(repositories) });
30
+ }
31
+ catch {
32
+ res.status(500).json({ error: 'verification_suggestions_failed', message: 'Could not inspect the project repositories' });
33
+ }
34
+ });
35
+ router.put('/:projectId/agent-runtime/config', async (req, res) => {
36
+ try {
37
+ (0, agent_runtime_settings_1.validateAgentRuntimeConfig)(req.body);
38
+ const config = (0, agent_runtime_settings_1.validateAgentRuntimeConfig)({ ...req.body, enabled: true, providers: (0, agent_runtime_settings_1.loadRuntimeProviders)() });
39
+ const runtimeAvailable = (0, agent_runtime_loader_1.findCoreAgentRuntimeEntry)() !== null;
40
+ if (config.enabled && !runtimeAvailable) {
41
+ res.status(503).json({ error: 'runtime_unavailable', message: 'Update Core to an installation with the agent runtime before enabling it' });
42
+ return;
43
+ }
44
+ if (config.enabled && runtimeAvailable) {
45
+ try {
46
+ (await (0, agent_runtime_loader_1.loadCoreAgentRuntime)()).validateRuntimeConfig(config);
47
+ }
48
+ catch {
49
+ res.status(503).json({ error: 'runtime_incompatible', message: 'The installed Core runtime could not validate this configuration; update Core and retry' });
50
+ return;
51
+ }
52
+ }
53
+ const saved = (0, agent_runtime_settings_1.saveAgentRuntimeConfig)(ctx(req).project, config);
54
+ res.json({ configured: true, config: saved, runtimeAvailable });
55
+ }
56
+ catch (err) {
57
+ const validation = err instanceof agent_runtime_settings_1.AgentRuntimeConfigError;
58
+ res.status(validation ? 400 : 500).json({ error: validation ? 'invalid_runtime_config' : 'runtime_config_write_failed', message: validation ? err.message : 'Could not save runtime configuration' });
59
+ }
60
+ });
61
+ }