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,239 @@
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.AgentRuntimeConfigError = exports.REVIEW_THRESHOLD_FLOORS = void 0;
7
+ exports.validateAgentRuntimeConfig = validateAgentRuntimeConfig;
8
+ exports.defaultAgentRuntimeConfig = defaultAgentRuntimeConfig;
9
+ exports.agentRuntimeConfigPath = agentRuntimeConfigPath;
10
+ exports.runtimeProvidersPath = runtimeProvidersPath;
11
+ exports.loadRuntimeProviders = loadRuntimeProviders;
12
+ exports.validateRuntimeProviders = validateRuntimeProviders;
13
+ exports.saveRuntimeProviders = saveRuntimeProviders;
14
+ exports.loadRuntimeConfigFile = loadRuntimeConfigFile;
15
+ exports.loadAgentRuntimeConfig = loadAgentRuntimeConfig;
16
+ exports.saveAgentRuntimeConfig = saveAgentRuntimeConfig;
17
+ exports.validateRuntimeRolePrompts = validateRuntimeRolePrompts;
18
+ exports.loadRuntimeRolePrompts = loadRuntimeRolePrompts;
19
+ exports.saveRuntimeRolePrompts = saveRuntimeRolePrompts;
20
+ const node_fs_1 = __importDefault(require("node:fs"));
21
+ const node_path_1 = __importDefault(require("node:path"));
22
+ const node_os_1 = __importDefault(require("node:os"));
23
+ const node_crypto_1 = require("node:crypto");
24
+ const ajv_1 = __importDefault(require("ajv"));
25
+ const agent_runtime_schema_json_1 = __importDefault(require("./schemas/agent-runtime.schema.json"));
26
+ const workspace_resolution_1 = require("./workspace-resolution");
27
+ /** Core's own review gate. Configured thresholds may only tighten it (mirrors Core's config.ts). */
28
+ exports.REVIEW_THRESHOLD_FLOORS = {
29
+ minScore: 70,
30
+ aspects: { type_correctness: 60, pattern_adherence: 60, test_coverage: 60, security: 75, architectural_alignment: 60 },
31
+ };
32
+ const CLI_PROVIDERS = ['claude', 'codex', 'gemini', 'kimi'];
33
+ const ROLES = ['architect', 'developer', 'reviewer'];
34
+ const schemaValidator = new ajv_1.default({ allErrors: true }).compile(agent_runtime_schema_json_1.default);
35
+ class AgentRuntimeConfigError extends Error {
36
+ constructor(message) { super(message); this.name = 'AgentRuntimeConfigError'; }
37
+ }
38
+ exports.AgentRuntimeConfigError = AgentRuntimeConfigError;
39
+ /** Vendored from Core's schema; parity tests keep offline configuration aligned.
40
+ * Semantic checks mirror Core because JSON Schema cannot check provider refs. */
41
+ function validateAgentRuntimeConfig(input) {
42
+ if (!schemaValidator(input)) {
43
+ const error = schemaValidator.errors?.[0];
44
+ throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${error?.instancePath || '/'}: ${error?.message ?? 'invalid value'}`);
45
+ }
46
+ const config = input;
47
+ if (config.rolePrompts !== undefined)
48
+ validateRuntimeRolePrompts(config.rolePrompts);
49
+ if (new Set(config.providers.map(({ id }) => id)).size !== config.providers.length)
50
+ throw new AgentRuntimeConfigError('Provider IDs must be unique');
51
+ for (const provider of config.providers) {
52
+ if (provider.kind !== 'openai-compatible')
53
+ continue;
54
+ let url;
55
+ try {
56
+ url = new URL(provider.baseUrl);
57
+ }
58
+ catch {
59
+ throw new AgentRuntimeConfigError('Provider base URL must be an absolute HTTP(S) URL');
60
+ }
61
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || provider.baseUrl.includes('\0'))
62
+ throw new AgentRuntimeConfigError('Provider URLs must use HTTP(S) without credentials, query parameters or fragments');
63
+ }
64
+ for (const role of ROLES) {
65
+ const agent = config.agents[role];
66
+ const provider = config.providers.find(({ id }) => id === agent.provider);
67
+ if (!provider)
68
+ throw new AgentRuntimeConfigError(`Role ${role} references a provider that is not configured`);
69
+ if (provider.kind === 'openai-compatible' && !agent.model)
70
+ throw new AgentRuntimeConfigError(`Role ${role} requires a model for its API provider`);
71
+ if (agent.model !== undefined && !agent.model.trim())
72
+ throw new AgentRuntimeConfigError(`Role ${role} requires a nonempty model identifier`);
73
+ if (agent.maxTurns !== undefined && !Number.isSafeInteger(agent.maxTurns))
74
+ throw new AgentRuntimeConfigError(`Role ${role} maxTurns must be a safe integer`);
75
+ }
76
+ for (const [key, value] of Object.entries(config.limits ?? {})) {
77
+ if (!Number.isFinite(value) || (key !== 'maxCostUsd' && !Number.isSafeInteger(value)))
78
+ throw new AgentRuntimeConfigError('Workflow limits must be finite, safe numbers');
79
+ }
80
+ for (const command of config.verification) {
81
+ if (![command.command, ...command.args, ...(command.cwd === undefined ? [] : [command.cwd])].every((value) => !value.includes('\0')) || !command.command.trim() || (command.cwd !== undefined && !command.cwd.trim()))
82
+ throw new AgentRuntimeConfigError('Verification command contains an empty or invalid value');
83
+ if (command.timeoutMs !== undefined && !Number.isSafeInteger(command.timeoutMs))
84
+ throw new AgentRuntimeConfigError('Verification timeout must be a safe integer');
85
+ for (const [key, value] of Object.entries(command.env ?? {})) {
86
+ if (/(?:token|secret|password|api_?key|credential)/i.test(key))
87
+ throw new AgentRuntimeConfigError('Verification credentials must be inherited from environment variables, never saved');
88
+ if (value.includes('\0'))
89
+ throw new AgentRuntimeConfigError('Verification environment contains an invalid value');
90
+ }
91
+ }
92
+ if (config.review?.minScore !== undefined && config.review.minScore < exports.REVIEW_THRESHOLD_FLOORS.minScore)
93
+ throw new AgentRuntimeConfigError(`Review threshold review.minScore must be at least ${exports.REVIEW_THRESHOLD_FLOORS.minScore} (Core's own review gate)`);
94
+ for (const [aspect, value] of Object.entries(config.review?.aspects ?? {})) {
95
+ if (value < exports.REVIEW_THRESHOLD_FLOORS.aspects[aspect])
96
+ throw new AgentRuntimeConfigError(`Review threshold review.aspects.${aspect} must be at least ${exports.REVIEW_THRESHOLD_FLOORS.aspects[aspect]} (Core's own review gate)`);
97
+ }
98
+ return structuredClone(config);
99
+ }
100
+ function defaultAgentRuntimeConfig(project) {
101
+ const provider = CLI_PROVIDERS.includes(project.provider) ? project.provider : 'claude';
102
+ return {
103
+ schemaVersion: 1, enabled: true,
104
+ providers: CLI_PROVIDERS.map((cli) => ({ id: cli, kind: 'cli', cli })),
105
+ agents: { architect: { provider }, developer: { provider }, reviewer: { provider } },
106
+ verification: [],
107
+ };
108
+ }
109
+ function agentRuntimeConfigPath(project) {
110
+ return node_path_1.default.join((0, workspace_resolution_1.resolveProjectExecution)(project).specrailsDir, 'agent-runtime.json');
111
+ }
112
+ /** Global connections are resolved for new runs; admitted runs keep their snapshot. */
113
+ function runtimeProvidersPath() { return node_path_1.default.join(node_os_1.default.homedir(), '.specrails', 'runtime-providers.json'); }
114
+ function atomicJson(file, value) {
115
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
116
+ const temporary = `${file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
117
+ try {
118
+ node_fs_1.default.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
119
+ node_fs_1.default.renameSync(temporary, file);
120
+ }
121
+ finally {
122
+ node_fs_1.default.rmSync(temporary, { force: true });
123
+ }
124
+ }
125
+ function loadRuntimeProviders() {
126
+ try {
127
+ return validateRuntimeProviders(JSON.parse(node_fs_1.default.readFileSync(runtimeProvidersPath(), 'utf8')));
128
+ }
129
+ catch (error) {
130
+ if (error.code === 'ENOENT')
131
+ return defaultAgentRuntimeConfig({ path: '' }).providers;
132
+ throw error;
133
+ }
134
+ }
135
+ function validateRuntimeProviders(input) {
136
+ if (!Array.isArray(input) || !input.length)
137
+ throw new AgentRuntimeConfigError('At least one provider connection is required');
138
+ const first = input[0];
139
+ const role = { provider: first?.id, ...(first?.kind === 'openai-compatible' ? { model: 'validation' } : {}) };
140
+ return validateAgentRuntimeConfig({ schemaVersion: 1, enabled: true, providers: input, agents: { architect: role, developer: role, reviewer: role }, verification: [] }).providers;
141
+ }
142
+ function saveRuntimeProviders(input) {
143
+ const providers = validateRuntimeProviders(input);
144
+ atomicJson(runtimeProvidersPath(), providers);
145
+ return providers;
146
+ }
147
+ /** Migrate embedded connections once. Conflicting IDs receive a stable ID so
148
+ * two projects with different endpoints never silently share a connection. */
149
+ function loadRuntimeConfigFile(file, fallbackProvider) {
150
+ let parsed;
151
+ let text;
152
+ try {
153
+ text = node_fs_1.default.readFileSync(file, 'utf8');
154
+ }
155
+ catch (error) {
156
+ if (error.code !== 'ENOENT')
157
+ throw error;
158
+ }
159
+ if (text !== undefined) {
160
+ try {
161
+ parsed = JSON.parse(text);
162
+ }
163
+ catch {
164
+ throw new AgentRuntimeConfigError('Saved runtime configuration is not valid JSON');
165
+ }
166
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
167
+ throw new AgentRuntimeConfigError('Saved runtime configuration must be an object');
168
+ }
169
+ const providers = loadRuntimeProviders();
170
+ if (!parsed) {
171
+ const config = defaultAgentRuntimeConfig({ path: '', provider: fallbackProvider });
172
+ const selected = providers.find(item => item.id === config.agents.architect.provider) ?? providers.find(item => item.kind === 'cli');
173
+ if (!selected || selected.kind !== 'cli')
174
+ throw new AgentRuntimeConfigError('Configure project role models before using API-only connections');
175
+ for (const role of ROLES)
176
+ config.agents[role].provider = selected.id;
177
+ return validateAgentRuntimeConfig({ ...config, providers });
178
+ }
179
+ if (Array.isArray(parsed.providers)) {
180
+ const legacy = validateAgentRuntimeConfig(parsed);
181
+ for (const original of legacy.providers) {
182
+ let connection = providers.find(item => item.id === original.id);
183
+ if (connection && JSON.stringify(connection) !== JSON.stringify(original)) {
184
+ const id = `${original.id.slice(0, 117)}-${(0, node_crypto_1.createHash)('sha256').update(JSON.stringify(original)).digest('hex').slice(0, 10)}`;
185
+ for (const role of ROLES)
186
+ if (legacy.agents[role].provider === original.id)
187
+ legacy.agents[role].provider = id;
188
+ connection = providers.find(item => item.id === id);
189
+ if (!connection)
190
+ providers.push({ ...original, id });
191
+ }
192
+ else if (!connection)
193
+ providers.push(original);
194
+ }
195
+ saveRuntimeProviders(providers);
196
+ const { providers: _embedded, enabled: _enabled, ...settings } = legacy;
197
+ atomicJson(file, settings);
198
+ parsed = settings;
199
+ }
200
+ return validateAgentRuntimeConfig({ ...parsed, enabled: true, providers });
201
+ }
202
+ /** Missing settings use the programmatic engine. Malformed files fail closed. */
203
+ function loadAgentRuntimeConfig(project) {
204
+ return loadRuntimeConfigFile(agentRuntimeConfigPath(project), project.provider);
205
+ }
206
+ /** Atomic replacement works on macOS and Windows, with no partial file on errors. */
207
+ function saveAgentRuntimeConfig(project, input) {
208
+ const config = validateAgentRuntimeConfig({ ...input, enabled: true, providers: loadRuntimeProviders() });
209
+ const file = agentRuntimeConfigPath(project);
210
+ const { providers: _providers, enabled: _enabled, ...settings } = config;
211
+ atomicJson(file, settings);
212
+ return config;
213
+ }
214
+ function validateRuntimeRolePrompts(input) {
215
+ if (!input || typeof input !== 'object' || Array.isArray(input))
216
+ throw new AgentRuntimeConfigError('Role prompts must be an object');
217
+ const prompts = {};
218
+ for (const [role, text] of Object.entries(input)) {
219
+ if (!ROLES.includes(role) || typeof text !== 'string' || !text.trim() || text.length > 20000 || text.includes('\0'))
220
+ throw new AgentRuntimeConfigError('Role prompts must use architect, developer or reviewer with 1–20000 nonblank characters and no null bytes');
221
+ prompts[role] = text;
222
+ }
223
+ return prompts;
224
+ }
225
+ function loadRuntimeRolePrompts() {
226
+ try {
227
+ return validateRuntimeRolePrompts(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(node_os_1.default.homedir(), '.specrails', 'runtime-role-prompts.json'), 'utf8')));
228
+ }
229
+ catch (error) {
230
+ if (error.code === 'ENOENT')
231
+ return {};
232
+ throw error;
233
+ }
234
+ }
235
+ function saveRuntimeRolePrompts(input) {
236
+ const prompts = validateRuntimeRolePrompts(input);
237
+ atomicJson(node_path_1.default.join(node_os_1.default.homedir(), '.specrails', 'runtime-role-prompts.json'), prompts);
238
+ return prompts;
239
+ }
@@ -0,0 +1,133 @@
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.settleRuntimeContinuation = settleRuntimeContinuation;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_crypto_1 = require("node:crypto");
9
+ const db_1 = require("./db");
10
+ const core_execution_1 = require("./core-execution");
11
+ const core_completion_1 = require("./core-completion");
12
+ const worktree_manager_1 = require("./worktree-manager");
13
+ const rail_pr_store_1 = require("./rail-pr-store");
14
+ const multi_repo_execution_1 = require("./multi-repo-execution");
15
+ /** Complete the host's local settlement after Core recovery. Never pushes or merges. */
16
+ async function settleRuntimeContinuation(input, dependencies = { verify: core_execution_1.checkCoreCompletion, git: worktree_manager_1.defaultGitRunner, commit: worktree_manager_1.commitWorktreeAndVerify }) {
17
+ const { db, runId } = input;
18
+ const verified = dependencies.verify(input.contextPath, input.cwd, input.env, runId);
19
+ if (!verified.valid)
20
+ throw new Error(verified.reason ?? 'Core completion is not verified');
21
+ const frozen = JSON.parse(node_fs_1.default.readFileSync(input.contextPath, 'utf8'));
22
+ if (frozen.runId !== runId)
23
+ throw new Error('Runtime identity changed');
24
+ const all = db.prepare('SELECT * FROM rail_pr_deliveries').all().filter(row => (0, rail_pr_store_1.toPrDeliverySnapshot)(row).runIds.includes(runId));
25
+ const parents = new Set(all.map(row => row.parent_delivery_id).filter((id) => !!id));
26
+ const rows = all.filter(row => !parents.has(row.id) && row.decision === 'implementation_failed');
27
+ if (all.some(row => parents.has(row.id) && ['discarded', 'superseded', 'merged', 'completed'].includes(row.decision)))
28
+ throw new Error('Delivery is no longer available for recovery');
29
+ const token = (0, node_crypto_1.randomUUID)(), claimed = [];
30
+ const updates = [];
31
+ const gitText = async (args, cwd) => {
32
+ const result = await dependencies.git.run(args, cwd);
33
+ if (result.code !== 0)
34
+ throw new Error(`Cannot inspect recovered worktree: ${result.stderr}`);
35
+ return result.stdout.trim();
36
+ };
37
+ try {
38
+ // Claim the entire group before Git effects; a concurrent discard must win or lose atomically.
39
+ db.transaction(() => {
40
+ for (const row of all.filter(row => parents.has(row.id) || rows.includes(row))) {
41
+ if (!(0, rail_pr_store_1.claimPrDeliveryOperation)(db, row.id, row.decision, 'recover-and-retry', token))
42
+ throw new Error('Delivery is busy; retry settlement after its current operation');
43
+ claimed.push(row);
44
+ }
45
+ })();
46
+ for (const row of rows) {
47
+ if (row.pr_url)
48
+ throw new Error('An existing PR requires explicit delivery recovery');
49
+ if (!(0, rail_pr_store_1.toPrDeliverySnapshot)(row).branches.some(unit => unit.runId === runId))
50
+ throw new Error('Delivery has no branch recorded for this runtime');
51
+ for (const unit of (0, rail_pr_store_1.toPrDeliverySnapshot)(row).branches.filter(unit => unit.runId === runId)) {
52
+ if (!unit.worktreePath || !frozen.repositories.some(repo => repo.path === unit.worktreePath) || node_fs_1.default.realpathSync(unit.worktreePath) !== unit.worktreePath)
53
+ throw new Error('Recovered worktree no longer matches the frozen runtime scope');
54
+ if (await gitText(['branch', '--show-current'], unit.worktreePath) !== unit.branch)
55
+ throw new Error('Recovered worktree branch changed');
56
+ }
57
+ }
58
+ for (const row of rows) {
59
+ const branches = (0, rail_pr_store_1.toPrDeliverySnapshot)(row).branches;
60
+ for (const unit of branches.filter(unit => unit.runId === runId)) {
61
+ const result = await dependencies.commit(dependencies.git, unit.worktreePath, `specrails: ticket-${unit.ticketId} recovered (run ${runId})`, unit.overlayExcludes ?? []);
62
+ if (!result.clean || result.error)
63
+ throw new Error(result.error ?? 'Recovered worktree still contains uncommitted changes');
64
+ const sha = await gitText(['rev-parse', 'HEAD'], unit.worktreePath);
65
+ if (!/^[a-f0-9]{40,64}$/i.test(sha))
66
+ throw new Error('Invalid recovered commit');
67
+ Object.assign(unit, { succeeded: true, implementationOutcome: 'succeeded', deliveryOutcome: sha !== unit.initialSha ? 'ready' : 'no_changes', finalSha: sha, changed: sha !== unit.initialSha, failureCode: null });
68
+ }
69
+ updates.push({ row, branches });
70
+ }
71
+ const finalCheck = dependencies.verify(input.contextPath, input.cwd, input.env, runId);
72
+ if (!finalCheck.valid)
73
+ throw new Error(finalCheck.reason ?? 'Candidate changed during settlement');
74
+ const core = await (0, core_completion_1.readCoreCompletion)(input);
75
+ db.transaction(() => {
76
+ for (const { row, branches } of updates) {
77
+ const ready = branches.every(unit => unit.succeeded);
78
+ const noChanges = ready && branches.every(unit => unit.changed === false);
79
+ if (!(0, rail_pr_store_1.transitionClaimedDecision)(db, row.id, row.decision, noChanges ? 'no_changes' : 'on_review', token, {
80
+ branches, implementationOutcome: ready ? 'succeeded' : 'partially_succeeded', deliveryOutcome: noChanges ? 'no_changes' : 'ready',
81
+ statusCode: noChanges ? 'no_changes' : ready ? 'ready_for_review' : 'partial_success', statusDetail: null,
82
+ ...(branches.length === 1 ? { deliverySha: branches[0].finalSha } : {}),
83
+ }))
84
+ throw new Error('Delivery changed during runtime settlement');
85
+ for (const unit of branches.filter(unit => unit.runId === runId))
86
+ db.prepare("UPDATE rail_worktrees SET merge_state = 'built', updated_at = datetime('now') WHERE run_id = ? AND worktree_path = ?").run(runId, unit.worktreePath);
87
+ }
88
+ for (const parentId of parents)
89
+ (0, multi_repo_execution_1.refreshRepositoryDeliveryGroup)(db, parentId);
90
+ db.prepare("UPDATE jobs SET status = 'completed', exit_code = 0 WHERE id = ?").run(runId);
91
+ db.prepare("UPDATE loop_runs SET status = 'completed', final_outcome = 'success' WHERE id = ?").run(runId);
92
+ let seq = db.prepare('SELECT COALESCE(MAX(seq), -1) + 1 AS seq FROM events WHERE job_id = ?').get(runId).seq;
93
+ const lastStep = db.prepare("SELECT payload FROM events WHERE job_id = ? AND event_type = 'loop_step' ORDER BY seq DESC LIMIT 1").get(runId);
94
+ const lastEnd = db.prepare("SELECT payload FROM events WHERE job_id = ? AND event_type = 'loop_step_end' ORDER BY seq DESC LIMIT 1").get(runId);
95
+ if (lastStep && lastEnd) {
96
+ const step = JSON.parse(lastStep.payload), end = JSON.parse(lastEnd.payload);
97
+ // Supersede only this implementation step, never an unrelated shell/decider failure.
98
+ if (step.kind === 'ai-step' && step.template === '{{cmd:implement}}' && end.index === step.index && end.nodeId === step.nodeId && end.status === 'failed') {
99
+ const job = db.prepare('SELECT duration_ms FROM jobs WHERE id = ?').get(runId);
100
+ (0, db_1.appendEvent)(db, runId, seq++, { event_type: 'loop_step_end', source: 'stdout', payload: JSON.stringify({ index: step.index, nodeId: step.nodeId, status: 'ok', exitCode: 0, durationMs: job.duration_ms ?? end.durationMs, recovered: true }) });
101
+ }
102
+ }
103
+ const oldCompletion = db.prepare("SELECT payload FROM events WHERE job_id = ? AND event_type = 'loop_completion' ORDER BY seq DESC LIMIT 1").get(runId);
104
+ let previous = {};
105
+ try {
106
+ previous = oldCompletion ? JSON.parse(oldCompletion.payload) : {};
107
+ }
108
+ catch { /* old event format */ }
109
+ const usage = db.prepare('SELECT total_cost_usd,num_turns FROM jobs WHERE id = ?').get(runId);
110
+ (0, db_1.appendEvent)(db, runId, seq, { event_type: 'loop_completion', source: 'stdout', payload: JSON.stringify({ ...previous, version: 1, execution: 'success', core, turns: usage.num_turns, costUsd: usage.total_cost_usd, recovered: true }) });
111
+ (0, db_1.appendEvent)(db, runId, seq + 1, { event_type: 'log', source: 'stdout', payload: JSON.stringify({ line: '[runtime] Recovery verified and local delivery prepared for review. The original failed attempt remains in history. No PR was published.' }) });
112
+ })();
113
+ }
114
+ finally {
115
+ for (const row of claimed)
116
+ (0, rail_pr_store_1.releasePrDeliveryOperation)(db, row.id, token);
117
+ }
118
+ for (const id of new Set([...updates.map(update => update.row.id), ...parents])) {
119
+ const row = (0, rail_pr_store_1.getPrDelivery)(db, id);
120
+ try {
121
+ if (row)
122
+ input.broadcast?.((0, rail_pr_store_1.toRailPrStateMessage)(input.projectId, (0, rail_pr_store_1.toPrDeliverySnapshot)(row)));
123
+ }
124
+ catch { /* durable state remains authoritative */ }
125
+ }
126
+ const events = db.prepare('SELECT seq,event_type,source,payload,timestamp FROM events WHERE job_id = ? ORDER BY seq DESC LIMIT 3').all(runId);
127
+ for (const event of events.reverse()) {
128
+ try {
129
+ input.broadcast?.({ type: 'event', projectId: input.projectId, jobId: runId, ...event });
130
+ }
131
+ catch { /* replay remains available */ }
132
+ }
133
+ }
@@ -0,0 +1,119 @@
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.suggestVerificationCommands = suggestVerificationCommands;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const NPM_TEST_PLACEHOLDER = /no test specified/i;
10
+ /** Script names worth running as verification, in priority order. Only scripts
11
+ * that exist in the repository are proposed; nothing is invented. */
12
+ const SCRIPT_CANDIDATES = [
13
+ { names: ['test', 'test:unit', 'test:ci'], label: 'test script' },
14
+ { names: ['typecheck', 'type-check', 'types', 'check-types'], label: 'type check script' },
15
+ { names: ['lint'], label: 'lint script' },
16
+ ];
17
+ const BUILD_SCRIPTS = ['build', 'compile'];
18
+ function exists(root, ...parts) {
19
+ try {
20
+ return node_fs_1.default.existsSync(node_path_1.default.join(root, ...parts));
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ function readJson(file) {
27
+ try {
28
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
29
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined;
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ function packageManager(root) {
36
+ if (exists(root, 'pnpm-lock.yaml'))
37
+ return 'pnpm';
38
+ if (exists(root, 'yarn.lock'))
39
+ return 'yarn';
40
+ if (exists(root, 'bun.lockb') || exists(root, 'bun.lock'))
41
+ return 'bun';
42
+ return 'npm';
43
+ }
44
+ function runScript(manager, script) {
45
+ // `npm test` and `npm run <x>` are the portable forms; on Windows the npm
46
+ // shim is resolved by Core's verification runner, never through a shell.
47
+ if (script === 'test')
48
+ return { command: manager, args: ['test'] };
49
+ return { command: manager, args: ['run', script] };
50
+ }
51
+ function nodeSuggestions(repository) {
52
+ const pkg = readJson(node_path_1.default.join(repository.path, 'package.json'));
53
+ if (!pkg)
54
+ return [];
55
+ const scripts = pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts) ? pkg.scripts : {};
56
+ const manager = packageManager(repository.path);
57
+ const out = [];
58
+ for (const candidate of SCRIPT_CANDIDATES) {
59
+ const name = candidate.names.find((script) => typeof scripts[script] === 'string' && String(scripts[script]).trim() && !NPM_TEST_PLACEHOLDER.test(String(scripts[script])));
60
+ if (name)
61
+ out.push({ repositoryId: repository.id, ...runScript(manager, name), reason: `package.json ${candidate.label} "${name}"` });
62
+ }
63
+ if (!out.length) {
64
+ const build = BUILD_SCRIPTS.find((script) => typeof scripts[script] === 'string' && String(scripts[script]).trim());
65
+ if (build)
66
+ out.push({ repositoryId: repository.id, ...runScript(manager, build), reason: `package.json build script "${build}" (no test or type check script found)` });
67
+ }
68
+ return out;
69
+ }
70
+ function otherSuggestions(repository) {
71
+ const root = repository.path;
72
+ const windows = process.platform === 'win32';
73
+ const out = [];
74
+ const add = (command, args, reason) => out.push({ repositoryId: repository.id, command, args, reason });
75
+ if (exists(root, 'Cargo.toml'))
76
+ add('cargo', ['test'], 'Cargo.toml');
77
+ if (exists(root, 'go.mod'))
78
+ add('go', ['test', './...'], 'go.mod');
79
+ if (exists(root, 'pyproject.toml') || exists(root, 'pytest.ini') || exists(root, 'setup.cfg') || exists(root, 'tox.ini')) {
80
+ if (exists(root, 'tests') || exists(root, 'test') || exists(root, 'pytest.ini'))
81
+ add(windows ? 'python' : 'python3', ['-m', 'pytest'], 'Python project with a tests directory');
82
+ }
83
+ if (exists(root, 'mix.exs'))
84
+ add('mix', ['test'], 'mix.exs');
85
+ if (exists(root, 'Package.swift'))
86
+ add('swift', ['test'], 'Package.swift');
87
+ if (exists(root, 'Gemfile') && exists(root, 'spec'))
88
+ add('bundle', ['exec', 'rspec'], 'Gemfile with a spec directory');
89
+ if (exists(root, 'pom.xml'))
90
+ add(exists(root, windows ? 'mvnw.cmd' : 'mvnw') ? (windows ? 'mvnw.cmd' : './mvnw') : 'mvn', ['-q', 'test'], 'pom.xml');
91
+ if (exists(root, 'build.gradle') || exists(root, 'build.gradle.kts'))
92
+ add(exists(root, windows ? 'gradlew.bat' : 'gradlew') ? (windows ? 'gradlew.bat' : './gradlew') : 'gradle', ['test'], 'Gradle build');
93
+ try {
94
+ if (node_fs_1.default.readdirSync(root).some((name) => /\.(sln|csproj|fsproj)$/i.test(name)))
95
+ add('dotnet', ['test'], '.NET solution or project');
96
+ }
97
+ catch { /* unreadable root: nothing to suggest */ }
98
+ if (!out.length && !windows && exists(root, 'Makefile')) {
99
+ try {
100
+ if (/^test\s*:/m.test(node_fs_1.default.readFileSync(node_path_1.default.join(root, 'Makefile'), 'utf8')))
101
+ add('make', ['test'], 'Makefile test target');
102
+ }
103
+ catch { /* ignore */ }
104
+ }
105
+ return out;
106
+ }
107
+ /** Deterministic, offline detection of a repository's own checks. Repositories
108
+ * with nothing recognisable produce no entry: the architect proposes commands
109
+ * at run time, and Core admits repositories that have no automated check. */
110
+ function suggestVerificationCommands(repositories) {
111
+ const out = [];
112
+ for (const repository of repositories) {
113
+ if (!repository.path || !node_fs_1.default.existsSync(repository.path))
114
+ continue;
115
+ const node = nodeSuggestions(repository);
116
+ out.push(...(node.length ? node : otherSuggestions(repository)));
117
+ }
118
+ return out;
119
+ }
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Contract Refine Runner
4
4
  *
5
- * Standalone runner that spawns a single Claude turn to produce the
5
+ * Standalone runner that spawns a single provider turn to produce the
6
6
  * Contract Layer for a just-committed Explore Spec ticket. Lives outside the
7
7
  * ChatManager lifecycle for now (design.md D3 — "thin sibling helper" option):
8
8
  * the refine is fire-and-forget with a 60 s budget per invocation and no
@@ -82,12 +82,12 @@ function buildRefineArgs(adapter, model, systemPrompt, sessionId) {
82
82
  model,
83
83
  systemPrompt: foldForResume ? undefined : systemPrompt,
84
84
  sessionId,
85
- toolPolicy: 'none',
85
+ toolPolicy: (0, runtime_1.pureOutputToolPolicy)(adapter) ?? (0, runtime_1.requireToolPolicy)(adapter, 'none'),
86
86
  maxTurns: 1,
87
87
  };
88
88
  return { args: adapter.buildArgs(action, options), options };
89
89
  }
90
- /** Build the pure-output, no-resume invocation used by Quick Refine and by the
90
+ /** Build the restricted pure-output, no-resume invocation used by Quick Refine and by the
91
91
  * one-time compatibility recovery for cwd-scoped Explore sessions. */
92
92
  function buildFreshRefineArgs(adapter, model, title, description) {
93
93
  const systemPrompt = [
@@ -105,7 +105,7 @@ function buildFreshRefineArgs(adapter, model, title, description) {
105
105
  prompt: explore_contract_refine_1.CONTRACT_MARKER_USER_MESSAGE,
106
106
  model,
107
107
  systemPrompt,
108
- toolPolicy: 'none',
108
+ toolPolicy: (0, runtime_1.pureOutputToolPolicy)(adapter) ?? (0, runtime_1.requireToolPolicy)(adapter, 'none'),
109
109
  maxTurns: 1,
110
110
  };
111
111
  return { args: adapter.buildArgs('spec-gen', options), options };
@@ -362,8 +362,8 @@ async function runContractRefine(deps, conversationId, ticketId) {
362
362
  return { ok: false, reason: 'not-explore', ticketId, conversationId };
363
363
  }
364
364
  const adapter = (0, providers_1.getAdapter)(conversation.provider ?? deps.providerId ?? 'claude');
365
- if (adapter.capabilities.structuredActions !== true) {
366
- console.log(`[contract-refine-runner] skip: provider '${adapter.id}' does not support structured actions`);
365
+ if (!(0, runtime_1.supportsContractRefine)(adapter)) {
366
+ console.log(`[contract-refine-runner] skip: provider '${adapter.id}' does not support contract refinement`);
367
367
  return { ok: false, reason: 'provider-unsupported', ticketId, conversationId };
368
368
  }
369
369
  // Per-conversation gating: contractRefine on the conversation's stored
@@ -602,7 +602,7 @@ async function runContractRefineForQuick(deps, ticketId, generatedTitle, generat
602
602
  }
603
603
  const admission = (0, process_admission_1.captureProcessAdmission)(deps.projectId);
604
604
  const adapter = (0, providers_1.getAdapter)(deps.providerId ?? 'claude');
605
- if (adapter.capabilities.structuredActions !== true) {
605
+ if (!(0, runtime_1.supportsContractRefine)(adapter)) {
606
606
  return { ok: false, reason: 'provider-unsupported', ticketId, conversationId: '' };
607
607
  }
608
608
  const resolvedModel = model ?? adapter.defaultModel();
@@ -6,7 +6,8 @@ const node_child_process_1 = require("node:child_process");
6
6
  const node_fs_1 = require("node:fs");
7
7
  const node_path_1 = require("node:path");
8
8
  const node_util_1 = require("node:util");
9
- const path_resolver_1 = require("./path-resolver");
9
+ const core_node_runtime_1 = require("./core-node-runtime");
10
+ const agent_runtime_loader_1 = require("./agent-runtime-loader");
10
11
  const obj = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
11
12
  const strings = (value) => Array.isArray(value) && value.every(item => typeof item === 'string');
12
13
  const count = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
@@ -43,14 +44,16 @@ function parseCoreCompletion(value, runId) {
43
44
  }), };
44
45
  }
45
46
  async function readCoreCompletion(input) {
46
- const runtime = (0, node_path_1.join)(input.cwd, '.specrails', 'runtime', 'pipeline.mjs');
47
- if (!(0, node_fs_1.existsSync)(runtime))
47
+ const programmatic = (0, node_fs_1.existsSync)((0, node_path_1.join)((0, node_path_1.dirname)(input.contextPath), 'agent-runtime-request.json'));
48
+ const runtime = programmatic ? (0, agent_runtime_loader_1.findCoreAgentRuntimeCli)() : (0, node_path_1.join)(input.cwd, '.specrails', 'runtime', 'pipeline.mjs');
49
+ if (!runtime || !(0, node_fs_1.existsSync)(runtime))
48
50
  return null;
49
51
  try {
50
- const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)((0, path_resolver_1.resolveBundledNodeExe)() ?? process.execPath, [runtime, 'status', '--context', input.contextPath], {
52
+ const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)((0, core_node_runtime_1.resolveCoreNodeRuntime)(), [runtime, 'status', '--context', input.contextPath], {
51
53
  cwd: input.cwd, env: input.env, encoding: 'utf8', timeout: 15_000, maxBuffer: 4 * 1024 * 1024, windowsHide: true,
52
54
  });
53
- return parseCoreCompletion(JSON.parse(stdout), input.runId);
55
+ const payload = JSON.parse(stdout);
56
+ return parseCoreCompletion(programmatic ? payload.pipeline : payload, input.runId);
54
57
  }
55
58
  catch {
56
59
  return null;
@@ -6,8 +6,9 @@ exports.checkCoreCompletion = checkCoreCompletion;
6
6
  const node_child_process_1 = require("node:child_process");
7
7
  const node_fs_1 = require("node:fs");
8
8
  const node_path_1 = require("node:path");
9
- const path_resolver_1 = require("./path-resolver");
9
+ const core_node_runtime_1 = require("./core-node-runtime");
10
10
  const core_update_state_1 = require("./core-update-state");
11
+ const agent_runtime_loader_1 = require("./agent-runtime-loader");
11
12
  const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
12
13
  // Resolve existing ancestors too: the tickets file may not exist yet, and macOS
13
14
  // exposes temporary directories through both /var and /private/var.
@@ -35,7 +36,7 @@ function prepareCoreExecution(input) {
35
36
  (0, core_update_state_1.assertWorkspaceCoreReady)(cwd);
36
37
  const repositories = manifest
37
38
  ? manifest.repositories.map(repo => ({ id: repo.repositoryId, name: repo.name, path: (0, node_fs_1.realpathSync)(repo.worktreePath), baseSha: repo.baseSha }))
38
- : [{ id: run.repositoryId ?? (run.spec?.repositoryIds?.length === 1 ? run.spec.repositoryIds[0] : 'primary'), name: (0, node_path_1.basename)(input.repoDir ?? cwd), path: (0, node_fs_1.realpathSync)(input.repoDir ?? cwd) }];
39
+ : [{ id: run.repositoryId ?? (run.spec?.repositoryIds?.length === 1 ? run.spec.repositoryIds[0] : run.projectId ? `primary-${run.projectId}` : 'primary'), name: (0, node_path_1.basename)(input.repoDir ?? cwd), path: (0, node_fs_1.realpathSync)(input.repoDir ?? cwd) }];
39
40
  if (!repositories.length || repositories.some(repo => !SAFE_ID.test(repo.id)))
40
41
  throw new Error('Invalid Core repository scope');
41
42
  const artifactRepositoryId = manifest?.artifactRepositoryId ?? repositories[0].id;
@@ -91,7 +92,7 @@ function coreVerificationContext(contextPath, cwd, env, runId) {
91
92
  if (!(0, node_fs_1.existsSync)(helper))
92
93
  return '';
93
94
  try {
94
- const stdout = (0, node_child_process_1.execFileSync)((0, path_resolver_1.resolveBundledNodeExe)() ?? process.execPath, [helper, 'status', '--context', contextPath], {
95
+ const stdout = (0, node_child_process_1.execFileSync)((0, core_node_runtime_1.resolveCoreNodeRuntime)(), [helper, 'status', '--context', contextPath], {
95
96
  cwd, env, encoding: 'utf8', timeout: 15_000, maxBuffer: 4 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'],
96
97
  });
97
98
  const status = JSON.parse(stdout);
@@ -115,14 +116,18 @@ function coreVerificationContext(contextPath, cwd, env, runId) {
115
116
  * revalidate the journal on disk before Desktop accepts the implementation step,
116
117
  * including when an agent claims PASS after its final receipt went stale. */
117
118
  function checkCoreCompletion(contextPath, cwd, env, runId) {
118
- const helper = (0, node_path_1.join)(cwd, '.specrails', 'runtime', 'pipeline.mjs');
119
- if (!(0, node_fs_1.existsSync)(helper))
119
+ const programmatic = (0, node_fs_1.existsSync)((0, node_path_1.join)((0, node_path_1.dirname)(contextPath), 'agent-runtime-request.json'));
120
+ const helper = programmatic ? (0, agent_runtime_loader_1.findCoreAgentRuntimeCli)() : (0, node_path_1.join)(cwd, '.specrails', 'runtime', 'pipeline.mjs');
121
+ if (!helper || !(0, node_fs_1.existsSync)(helper))
120
122
  return { valid: false, reason: 'Core completion cannot be validated: pipeline runtime is missing' };
121
123
  try {
122
- const stdout = (0, node_child_process_1.execFileSync)((0, path_resolver_1.resolveBundledNodeExe)() ?? process.execPath, [helper, 'status', '--context', contextPath], {
124
+ const stdout = (0, node_child_process_1.execFileSync)((0, core_node_runtime_1.resolveCoreNodeRuntime)(), [helper, 'status', '--context', contextPath, ...(programmatic ? ['--compact'] : [])], {
123
125
  cwd, env, encoding: 'utf8', timeout: 15_000, maxBuffer: 4 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'],
124
126
  });
125
- const status = JSON.parse(stdout);
127
+ const payload = JSON.parse(stdout);
128
+ if (programmatic && payload.state?.status !== 'succeeded')
129
+ return { valid: false, reason: 'Programmatic workflow has not completed successfully' };
130
+ const status = (programmatic ? payload.pipeline : payload);
126
131
  if (status.schemaVersion !== 1 || status.runId !== runId)
127
132
  return { valid: false, reason: 'Core completion returned an invalid or mismatched run identity' };
128
133
  const reasons = [];
@@ -146,7 +151,11 @@ function checkCoreCompletion(contextPath, cwd, env, runId) {
146
151
  if (status.resumePhase && ['architect', 'developer', 'reviewer', 'archive'].includes(status.resumePhase))
147
152
  reasons.push(`resume from ${status.resumePhase}`);
148
153
  const receipt = status.verification?.receipt;
149
- if (status.verification?.valid !== true || receipt?.kind !== 'full' || !receipt.commands?.length || receipt.commands.some(command => command.exitCode !== 0)) {
154
+ // A full receipt may explicitly record repositories without any automated
155
+ // check (the runtime admits them); an empty command list without that record
156
+ // is still no evidence.
157
+ const admittedWithoutCommands = Array.isArray(receipt?.unverifiedRepositories) && receipt.unverifiedRepositories.length > 0;
158
+ if (status.verification?.valid !== true || receipt?.kind !== 'full' || !Array.isArray(receipt.commands) || (!receipt.commands.length && !admittedWithoutCommands) || receipt.commands.some(command => command.exitCode !== 0)) {
150
159
  reasons.push(...(status.verification?.reasons?.length ? status.verification.reasons : ['No valid full verification receipt']));
151
160
  }
152
161
  return reasons.length ? { valid: false, reason: `Core completion blocked: ${[...new Set(reasons)].join('; ')}` } : { valid: true };