specrails-desktop 2.44.2 → 2.45.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 (101) hide show
  1. package/README.md +2 -0
  2. package/client/dist/assets/{ActivityFeedPage-CL1BUXoj.js → ActivityFeedPage-D3SQklu5.js} +1 -1
  3. package/client/dist/assets/AgentBrowserCapture-DNGxE33d.js +1 -0
  4. package/client/dist/assets/AgentModeAnalyticsPane-BUvZNoj0.js +2 -0
  5. package/client/dist/assets/AgentModeCodePane-YW717UvJ.js +2 -0
  6. package/client/dist/assets/{AgentModeJobsPane-C7ELTPC1.js → AgentModeJobsPane-Cxg_eNmd.js} +1 -1
  7. package/client/dist/assets/AgentsPage-B_DCByok.js +87 -0
  8. package/client/dist/assets/{AnalyticsPage-DTsg-N5z.js → AnalyticsPage-BqV6mM2O.js} +1 -1
  9. package/client/dist/assets/CodePage-PyZKCj1B.js +3 -0
  10. package/client/dist/assets/{DesktopAnalyticsPage-Y0Z1ryQr.js → DesktopAnalyticsPage-D6WNax3K.js} +1 -1
  11. package/client/dist/assets/{DocsDialog-KeCOxvGH.js → DocsDialog-68jKUH9f.js} +1 -1
  12. package/client/dist/assets/{DocsPage-Bt1PgYPO.js → DocsPage-BoGJgRpm.js} +1 -1
  13. package/client/dist/assets/{ExportDropdown-DczgU4at.js → ExportDropdown-B2C_XLGp.js} +1 -1
  14. package/client/dist/assets/InteractiveJobComposer-l8q-r5Yy.js +19 -0
  15. package/client/dist/assets/{JobDetailModal-DKA_DUHe.js → JobDetailModal-C-X2GI9j.js} +1 -1
  16. package/client/dist/assets/JobDetailPage-C6Jdm7S9.js +1 -0
  17. package/client/dist/assets/{JobsPage-Bwoe-Fb0.js → JobsPage-DycM6Vgp.js} +1 -1
  18. package/client/dist/assets/{LoopBuilderPage-CqC4hbXb.js → LoopBuilderPage-D21GxFmL.js} +2 -2
  19. package/client/dist/assets/{LoopPreviewModal-T0zuFv6y.js → LoopPreviewModal-Bo3fYFxN.js} +1 -1
  20. package/client/dist/assets/{LoopsPage-DrQGcZv2.js → LoopsPage-BcrBw1cg.js} +1 -1
  21. package/client/dist/assets/{MinimizedChatsContext-C843Y13h.js → MinimizedChatsContext-Dsv2eHf_.js} +1 -1
  22. package/client/dist/assets/PluginsPage-BbyTmSLx.js +2 -0
  23. package/client/dist/assets/ProjectSettingsDialog-B3lEXLv7.js +1 -0
  24. package/client/dist/assets/{RepositoryDeliveries-bgIHE8ep.js → RepositoryDeliveries-ok_Wo65C.js} +1 -1
  25. package/client/dist/assets/{RepositoryScopeSelector-CZ_U_R3U.js → RepositoryScopeSelector-DU6QwNaS.js} +1 -1
  26. package/client/dist/assets/{ReviewPacketPage-DbKO6pT8.js → ReviewPacketPage-BBCeHtgd.js} +1 -1
  27. package/client/dist/assets/TemplatePreviewModal-CfNmxS6O.js +1 -0
  28. package/client/dist/assets/{TicketDetailModalContext-BbRA6cb7.js → TicketDetailModalContext-DW3eQ0I7.js} +1 -1
  29. package/client/dist/assets/{Trans-mjFZpHw1.js → Trans-BHPMWkOw.js} +1 -1
  30. package/client/dist/assets/agentRuntime-14faDjWM.js +1 -0
  31. package/client/dist/assets/agentRuntime-BIokojkJ.js +1 -0
  32. package/client/dist/assets/agentRuntime-Bu4xEcRw.js +1 -0
  33. package/client/dist/assets/agentRuntime-Cz0ASixB.js +1 -0
  34. package/client/dist/assets/agentRuntime-DESdGXLV.js +1 -0
  35. package/client/dist/assets/agentRuntime-DhX5Xy5S.js +1 -0
  36. package/client/dist/assets/agentRuntime-Dpbh_gLp.js +1 -0
  37. package/client/dist/assets/agentRuntime-jS2lplFm.js +1 -0
  38. package/client/dist/assets/{formatDistanceToNow-BIL-HxGy.js → formatDistanceToNow-Bq8iptrG.js} +1 -1
  39. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-Cs9z2sdO.js → getTimezoneOffsetInMilliseconds-BVFky3l_.js} +1 -1
  40. package/client/dist/assets/index-CBLYwPw6.css +2 -0
  41. package/client/dist/assets/index-DOKftX7y.js +76 -0
  42. package/client/dist/assets/{jira-api-B2BBZQTx.js → jira-api-zrRdZFBB.js} +1 -1
  43. package/client/dist/assets/{narration-C1Px0-0j.js → narration-BA8Yw5ZL.js} +1 -1
  44. package/client/dist/assets/{narration-DQ1l_Kq6.js → narration-BuMFUS7Q.js} +1 -1
  45. package/client/dist/assets/{narration-kwzfglQo.js → narration-Bv_iQQG5.js} +1 -1
  46. package/client/dist/assets/{narration-B-ynWTpn.js → narration-C6Ost9nL.js} +1 -1
  47. package/client/dist/assets/{narration-CAXhGbzM.js → narration-CIbjo2pA.js} +1 -1
  48. package/client/dist/assets/{narration-BxHpmMv_.js → narration-CNJ2-2-a.js} +1 -1
  49. package/client/dist/assets/{narration-Bj_laZwg.js → narration-OzsPJCXM.js} +1 -1
  50. package/client/dist/assets/{narration-XISXwqZd.js → narration-zCdgiuSU.js} +1 -1
  51. package/client/dist/assets/{project-repositories-xSqimlaj.js → project-repositories-CFRaB9UB.js} +1 -1
  52. package/client/dist/assets/{settings-BLOsrXEw.js → settings-CNiERE-R.js} +1 -1
  53. package/client/dist/assets/{settings-NSWQjY5B.js → settings-ChOCVjX_.js} +1 -1
  54. package/client/dist/assets/{settings-C6s9PtRD.js → settings-CrtCs5-B.js} +1 -1
  55. package/client/dist/assets/{settings-DPjNiUsS.js → settings-DJ2jl3hJ.js} +1 -1
  56. package/client/dist/assets/{settings-FIqwhTup.js → settings-DVRVybR_.js} +1 -1
  57. package/client/dist/assets/{settings-CCNZN5Q-.js → settings-DzXZCb1h.js} +1 -1
  58. package/client/dist/assets/{settings-2Yx_9yJM.js → settings-OOyDrZQo.js} +1 -1
  59. package/client/dist/assets/{settings-CpKwTew1.js → settings-a7NIODme.js} +1 -1
  60. package/client/dist/assets/{spending-DXOcv1tn.js → spending-lTZ-bm_y.js} +1 -1
  61. package/client/dist/assets/{useDesktop-gVGoWWiY.js → useDesktop-Duv5ddAa.js} +1 -1
  62. package/client/dist/assets/useSharedWebSocket-5mj2SMKn.js +2 -0
  63. package/client/dist/index.html +17 -16
  64. package/docs/README.md +1 -0
  65. package/docs/internals/README.md +2 -0
  66. package/docs/internals/agent-runtime-framework-evaluation.md +208 -0
  67. package/docs/internals/programmatic-agent-runtime-validation.md +72 -0
  68. package/docs/internals/programmatic-agent-runtime.md +129 -0
  69. package/package.json +1 -1
  70. package/server/dist/agent-runtime-accounting.js +73 -0
  71. package/server/dist/agent-runtime-bridge.js +183 -0
  72. package/server/dist/agent-runtime-controls-router.js +44 -0
  73. package/server/dist/agent-runtime-controls.js +277 -0
  74. package/server/dist/agent-runtime-loader.js +85 -0
  75. package/server/dist/agent-runtime-paths.js +12 -0
  76. package/server/dist/agent-runtime-settings-router.js +59 -0
  77. package/server/dist/agent-runtime-settings.js +135 -0
  78. package/server/dist/agent-runtime-verification-suggestions.js +119 -0
  79. package/server/dist/core-execution.js +17 -8
  80. package/server/dist/core-node-runtime.js +12 -0
  81. package/server/dist/loop-executors.js +42 -2
  82. package/server/dist/loop-factory.js +1 -1
  83. package/server/dist/loop-run-manager.js +60 -26
  84. package/server/dist/project-registry.js +9 -0
  85. package/server/dist/project-router-settings.js +4 -0
  86. package/server/dist/rail-isolated-launch.js +18 -0
  87. package/server/dist/rails-router.js +1 -0
  88. package/server/dist/schemas/agent-runtime.schema.json +55 -0
  89. package/client/dist/assets/AgentBrowserCapture-BQem50z4.js +0 -1
  90. package/client/dist/assets/AgentModeAnalyticsPane-BC_xbT_l.js +0 -2
  91. package/client/dist/assets/AgentModeCodePane-DK-q3K6Y.js +0 -2
  92. package/client/dist/assets/AgentsPage-DbLezyI6.js +0 -87
  93. package/client/dist/assets/CodePage-DwgbeqQ_.js +0 -3
  94. package/client/dist/assets/InteractiveJobComposer-BcR6xlHU.js +0 -19
  95. package/client/dist/assets/JobDetailPage-vaOcE-cI.js +0 -1
  96. package/client/dist/assets/PluginsPage-CTUHX_Lu.js +0 -2
  97. package/client/dist/assets/ProjectSettingsDialog-DeL8md1s.js +0 -1
  98. package/client/dist/assets/TemplatePreviewModal-_CgCCV7L.js +0 -1
  99. package/client/dist/assets/index-Bfj8HvX1.css +0 -2
  100. package/client/dist/assets/index-UnaJTMIy.js +0 -76
  101. package/client/dist/assets/useSharedWebSocket-DbGL_YOt.js +0 -2
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAgentRuntimeSettingsRoutes = registerAgentRuntimeSettingsRoutes;
4
+ const agent_runtime_loader_1 = require("./agent-runtime-loader");
5
+ const agent_runtime_settings_1 = require("./agent-runtime-settings");
6
+ const agent_runtime_verification_suggestions_1 = require("./agent-runtime-verification-suggestions");
7
+ const project_repositories_1 = require("./project-repositories");
8
+ function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
9
+ router.get('/:projectId/agent-runtime/config', (_req, res) => {
10
+ try {
11
+ const project = ctx(_req).project;
12
+ const config = (0, agent_runtime_settings_1.loadAgentRuntimeConfig)(project);
13
+ res.json({ configured: config !== null, config: config ?? (0, agent_runtime_settings_1.defaultAgentRuntimeConfig)(project), runtimeAvailable: (0, agent_runtime_loader_1.findCoreAgentRuntimeEntry)() !== null });
14
+ }
15
+ catch (err) {
16
+ const validation = err instanceof agent_runtime_settings_1.AgentRuntimeConfigError;
17
+ res.status(validation ? 422 : 500).json({ error: validation ? 'invalid_runtime_config' : 'runtime_config_read_failed', message: validation ? err.message : 'Could not read runtime configuration' });
18
+ }
19
+ });
20
+ /** Offline detection of each repository's own checks; never invokes a model.
21
+ * Verification stays optional: the architect proposes commands for uncovered
22
+ * repositories at run time, and Core admits repositories without any check. */
23
+ router.get('/:projectId/agent-runtime/verification-suggestions', (req, res) => {
24
+ try {
25
+ const project = ctx(req).project;
26
+ const repositories = (0, project_repositories_1.getProjectRepositories)(project).map((repository) => ({ id: repository.id, name: repository.name, path: repository.path }));
27
+ res.json({ repositories: repositories.map(({ id, name }) => ({ id, name })), suggestions: (0, agent_runtime_verification_suggestions_1.suggestVerificationCommands)(repositories) });
28
+ }
29
+ catch {
30
+ res.status(500).json({ error: 'verification_suggestions_failed', message: 'Could not inspect the project repositories' });
31
+ }
32
+ });
33
+ router.put('/:projectId/agent-runtime/config', async (req, res) => {
34
+ try {
35
+ const config = (0, agent_runtime_settings_1.validateAgentRuntimeConfig)(req.body);
36
+ const runtimeAvailable = (0, agent_runtime_loader_1.findCoreAgentRuntimeEntry)() !== null;
37
+ if (config.enabled && !runtimeAvailable) {
38
+ res.status(503).json({ error: 'runtime_unavailable', message: 'Update Core to an installation with the agent runtime before enabling it' });
39
+ return;
40
+ }
41
+ // Disabling must remain possible even when an installed runtime is broken.
42
+ if (config.enabled && runtimeAvailable) {
43
+ try {
44
+ (await (0, agent_runtime_loader_1.loadCoreAgentRuntime)()).validateRuntimeConfig(config);
45
+ }
46
+ catch {
47
+ res.status(503).json({ error: 'runtime_incompatible', message: 'The installed Core runtime could not validate this configuration; update Core and retry' });
48
+ return;
49
+ }
50
+ }
51
+ const saved = (0, agent_runtime_settings_1.saveAgentRuntimeConfig)(ctx(req).project, config);
52
+ res.json({ configured: true, config: saved, runtimeAvailable });
53
+ }
54
+ catch (err) {
55
+ const validation = err instanceof agent_runtime_settings_1.AgentRuntimeConfigError;
56
+ res.status(validation ? 400 : 500).json({ error: validation ? 'invalid_runtime_config' : 'runtime_config_write_failed', message: validation ? err.message : 'Could not save runtime configuration' });
57
+ }
58
+ });
59
+ }
@@ -0,0 +1,135 @@
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.loadAgentRuntimeConfig = loadAgentRuntimeConfig;
11
+ exports.saveAgentRuntimeConfig = saveAgentRuntimeConfig;
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const node_path_1 = __importDefault(require("node:path"));
14
+ const node_crypto_1 = require("node:crypto");
15
+ const ajv_1 = __importDefault(require("ajv"));
16
+ const agent_runtime_schema_json_1 = __importDefault(require("./schemas/agent-runtime.schema.json"));
17
+ const workspace_resolution_1 = require("./workspace-resolution");
18
+ /** Core's own review gate. Configured thresholds may only tighten it (mirrors Core's config.ts). */
19
+ exports.REVIEW_THRESHOLD_FLOORS = {
20
+ minScore: 70,
21
+ aspects: { type_correctness: 60, pattern_adherence: 60, test_coverage: 60, security: 75, architectural_alignment: 60 },
22
+ };
23
+ const CLI_PROVIDERS = ['claude', 'codex', 'gemini', 'kimi'];
24
+ const ROLES = ['architect', 'developer', 'reviewer'];
25
+ const schemaValidator = new ajv_1.default({ allErrors: true }).compile(agent_runtime_schema_json_1.default);
26
+ class AgentRuntimeConfigError extends Error {
27
+ constructor(message) { super(message); this.name = 'AgentRuntimeConfigError'; }
28
+ }
29
+ exports.AgentRuntimeConfigError = AgentRuntimeConfigError;
30
+ /** Vendored from Core's schema; parity tests keep offline configuration aligned.
31
+ * Semantic checks mirror Core because JSON Schema cannot check provider refs. */
32
+ function validateAgentRuntimeConfig(input) {
33
+ if (!schemaValidator(input)) {
34
+ const error = schemaValidator.errors?.[0];
35
+ throw new AgentRuntimeConfigError(`Invalid runtime configuration at ${error?.instancePath || '/'}: ${error?.message ?? 'invalid value'}`);
36
+ }
37
+ const config = input;
38
+ if (new Set(config.providers.map(({ id }) => id)).size !== config.providers.length)
39
+ throw new AgentRuntimeConfigError('Provider IDs must be unique');
40
+ for (const provider of config.providers) {
41
+ if (provider.kind !== 'openai-compatible')
42
+ continue;
43
+ let url;
44
+ try {
45
+ url = new URL(provider.baseUrl);
46
+ }
47
+ catch {
48
+ throw new AgentRuntimeConfigError('Provider base URL must be an absolute HTTP(S) URL');
49
+ }
50
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || provider.baseUrl.includes('\0'))
51
+ throw new AgentRuntimeConfigError('Provider URLs must use HTTP(S) without credentials, query parameters or fragments');
52
+ }
53
+ for (const role of ROLES) {
54
+ const agent = config.agents[role];
55
+ const provider = config.providers.find(({ id }) => id === agent.provider);
56
+ if (!provider)
57
+ throw new AgentRuntimeConfigError(`Role ${role} references a provider that is not configured`);
58
+ if (provider.kind === 'openai-compatible' && !agent.model)
59
+ throw new AgentRuntimeConfigError(`Role ${role} requires a model for its API provider`);
60
+ if (agent.model !== undefined && !agent.model.trim())
61
+ throw new AgentRuntimeConfigError(`Role ${role} requires a nonempty model identifier`);
62
+ if (agent.maxTurns !== undefined && !Number.isSafeInteger(agent.maxTurns))
63
+ throw new AgentRuntimeConfigError(`Role ${role} maxTurns must be a safe integer`);
64
+ }
65
+ for (const [key, value] of Object.entries(config.limits ?? {})) {
66
+ if (!Number.isFinite(value) || (key !== 'maxCostUsd' && !Number.isSafeInteger(value)))
67
+ throw new AgentRuntimeConfigError('Workflow limits must be finite, safe numbers');
68
+ }
69
+ for (const command of config.verification) {
70
+ if (![command.command, ...command.args, ...(command.cwd === undefined ? [] : [command.cwd])].every((value) => !value.includes('\0')) || !command.command.trim() || (command.cwd !== undefined && !command.cwd.trim()))
71
+ throw new AgentRuntimeConfigError('Verification command contains an empty or invalid value');
72
+ if (command.timeoutMs !== undefined && !Number.isSafeInteger(command.timeoutMs))
73
+ throw new AgentRuntimeConfigError('Verification timeout must be a safe integer');
74
+ for (const [key, value] of Object.entries(command.env ?? {})) {
75
+ if (/(?:token|secret|password|api_?key|credential)/i.test(key))
76
+ throw new AgentRuntimeConfigError('Verification credentials must be inherited from environment variables, never saved');
77
+ if (value.includes('\0'))
78
+ throw new AgentRuntimeConfigError('Verification environment contains an invalid value');
79
+ }
80
+ }
81
+ if (config.review?.minScore !== undefined && config.review.minScore < exports.REVIEW_THRESHOLD_FLOORS.minScore)
82
+ throw new AgentRuntimeConfigError(`Review threshold review.minScore must be at least ${exports.REVIEW_THRESHOLD_FLOORS.minScore} (Core's own review gate)`);
83
+ for (const [aspect, value] of Object.entries(config.review?.aspects ?? {})) {
84
+ if (value < exports.REVIEW_THRESHOLD_FLOORS.aspects[aspect])
85
+ throw new AgentRuntimeConfigError(`Review threshold review.aspects.${aspect} must be at least ${exports.REVIEW_THRESHOLD_FLOORS.aspects[aspect]} (Core's own review gate)`);
86
+ }
87
+ return structuredClone(config);
88
+ }
89
+ function defaultAgentRuntimeConfig(project) {
90
+ const provider = CLI_PROVIDERS.includes(project.provider) ? project.provider : 'claude';
91
+ return {
92
+ schemaVersion: 1, enabled: false,
93
+ providers: CLI_PROVIDERS.map((cli) => ({ id: cli, kind: 'cli', cli })),
94
+ agents: { architect: { provider }, developer: { provider }, reviewer: { provider } },
95
+ verification: [],
96
+ };
97
+ }
98
+ function agentRuntimeConfigPath(project) {
99
+ return node_path_1.default.join((0, workspace_resolution_1.resolveProjectExecution)(project).specrailsDir, 'agent-runtime.json');
100
+ }
101
+ /** Missing configuration preserves legacy execution. Malformed files fail closed. */
102
+ function loadAgentRuntimeConfig(project) {
103
+ let text;
104
+ try {
105
+ text = node_fs_1.default.readFileSync(agentRuntimeConfigPath(project), 'utf8');
106
+ }
107
+ catch (err) {
108
+ if (err.code === 'ENOENT')
109
+ return null;
110
+ throw err;
111
+ }
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(text);
115
+ }
116
+ catch {
117
+ throw new AgentRuntimeConfigError('Saved runtime configuration is not valid JSON');
118
+ }
119
+ return validateAgentRuntimeConfig(parsed);
120
+ }
121
+ /** Atomic replacement works on macOS and Windows, with no partial file on errors. */
122
+ function saveAgentRuntimeConfig(project, input) {
123
+ const config = validateAgentRuntimeConfig(input);
124
+ const file = agentRuntimeConfigPath(project);
125
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
126
+ const temporary = `${file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
127
+ try {
128
+ node_fs_1.default.writeFileSync(temporary, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
129
+ node_fs_1.default.renameSync(temporary, file);
130
+ }
131
+ finally {
132
+ node_fs_1.default.rmSync(temporary, { force: true });
133
+ }
134
+ return config;
135
+ }
@@ -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
+ }
@@ -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 };
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveCoreNodeRuntime = resolveCoreNodeRuntime;
4
+ const path_resolver_1 = require("./path-resolver");
5
+ /** External Core CLIs need ordinary Node. A pkg sidecar's process.execPath is
6
+ * the server executable: using it would recursively launch another server. */
7
+ function resolveCoreNodeRuntime() {
8
+ const bundled = (0, path_resolver_1.resolveBundledNodeExe)();
9
+ if (bundled)
10
+ return bundled;
11
+ return process.pkg ? 'node' : process.execPath;
12
+ }
@@ -11,6 +11,7 @@ exports.createLoopExecutors = createLoopExecutors;
11
11
  */
12
12
  const core_completion_1 = require("./core-completion");
13
13
  const core_execution_1 = require("./core-execution");
14
+ const agent_runtime_bridge_1 = require("./agent-runtime-bridge");
14
15
  const codex_spawn_1 = require("./plugins/codex-spawn");
15
16
  const node_child_process_1 = require("node:child_process");
16
17
  const node_crypto_1 = require("node:crypto");
@@ -41,6 +42,7 @@ const SHELL_OUTPUT_CAP = 256 * 1024;
41
42
  // Match the queue's idle budget: a long implementation may keep running while
42
43
  // producing output; a silent, wedged CLI must eventually release its rail.
43
44
  const DEFAULT_INACTIVITY_TIMEOUT_MS = 30 * 60_000;
45
+ const existsRuntimeRequest = (contextPath) => (0, node_fs_1.existsSync)((0, node_path_1.join)((0, node_path_1.dirname)(contextPath), 'agent-runtime-request.json'));
44
46
  function runShellCommand(command, cwd, env, timeoutMs, onLine, onSpawn) {
45
47
  return new Promise((resolve) => {
46
48
  const isWin = process.platform === 'win32';
@@ -116,6 +118,15 @@ function aiStepEnv(env, repoDir, manifest) {
116
118
  const withMap = (0, repo_map_1.injectRepoMapEnv)(base, repoDir);
117
119
  return manifest || (0, rail_isolation_1.isRailPrDeliveryEnabled)() ? { ...withMap, SPECRAILS_GIT_AUTO: 'false' } : withMap;
118
120
  }
121
+ function programmaticStepEnv(env, repoDir, manifest) {
122
+ const result = aiStepEnv(env, repoDir, manifest);
123
+ delete result.SPECRAILS_PROFILE_PATH;
124
+ return result;
125
+ }
126
+ function runtimeContextPath(cwd, runId, env) {
127
+ const root = env.SPECRAILS_TICKETS_PATH ? (0, node_path_1.dirname)((0, node_path_1.dirname)(env.SPECRAILS_TICKETS_PATH)) : cwd;
128
+ return (0, node_path_1.join)(root, '.specrails', 'pipeline', runId, 'desktop-context.json');
129
+ }
119
130
  /**
120
131
  * Relocated cwd is the workspace; the source repo is reached via the
121
132
  * `./project` symlink + SPECRAILS_REPO_DIR. Each provider must be told the
@@ -163,6 +174,7 @@ function createLoopExecutors(opts = {}) {
163
174
  }
164
175
  };
165
176
  const completionContexts = new Map();
177
+ const runtimeConfigPath = (cwd) => (0, node_path_1.join)(opts.pluginScope?.().stateRoot ?? cwd, '.specrails', 'agent-runtime.json');
166
178
  return {
167
179
  async readCoreCompletion(runId) {
168
180
  const context = completionContexts.get(runId);
@@ -220,9 +232,26 @@ function createLoopExecutors(opts = {}) {
220
232
  // for the pipeline's I/O exactly like QueueManager: SPECRAILS_REPO_DIR +
221
233
  // claude `--add-dir <repoDir>` (see the shared helpers above). Best-effort
222
234
  // agent self-heal on Windows.
223
- const baseStepEnv = withProfileEnv(aiStepEnv(resolveEnv(), repoDir, executionManifest), provider, profileName);
235
+ const baseEnv = resolveEnv();
236
+ const existingContext = coreRun ? runtimeContextPath(cwd, coreRun.runId, baseEnv) : undefined;
237
+ const programmatic = coreRun && existingContext && ((coreRun.implementation && (0, agent_runtime_bridge_1.selectAgentRuntime)(runtimeConfigPath(cwd), existingContext)) || (coreRun.verificationStep && existsRuntimeRequest(existingContext)));
238
+ const baseStepEnv = programmatic ? programmaticStepEnv(baseEnv, repoDir, executionManifest) : withProfileEnv(aiStepEnv(baseEnv, repoDir, executionManifest), provider, profileName);
224
239
  const core = coreRun ? (0, core_execution_1.prepareCoreExecution)({ run: coreRun, cwd, repoDir, manifest: executionManifest, env: baseStepEnv }) : undefined;
225
240
  const stepEnv = core?.env ?? baseStepEnv;
241
+ if (coreRun?.implementation && core && (0, agent_runtime_bridge_1.selectAgentRuntime)(runtimeConfigPath(cwd), core.contextPath)) {
242
+ const admitted = existsRuntimeRequest(core.contextPath);
243
+ // Repeated implementation nodes may validate completed work, but may
244
+ // never silently replay an interrupted mutating phase.
245
+ return (0, agent_runtime_bridge_1.runAgentRuntimeInvocation)({
246
+ contextPath: core.contextPath, cwd, env: { ...programmaticStepEnv(resolveEnv(), repoDir, executionManifest), SPECRAILS_EXECUTION_CONTEXT: core.contextPath }, configPath: runtimeConfigPath(cwd),
247
+ change: (0, agent_runtime_bridge_1.runtimeChangeName)(coreRun.runId), resume: admitted,
248
+ onLine, onRawLine, onSpawn, timeoutMs: aiStepTimeoutMs,
249
+ });
250
+ }
251
+ if (coreRun?.verificationStep && core && existsRuntimeRequest(core.contextPath)) {
252
+ const checked = (0, core_execution_1.checkCoreCompletion)(core.contextPath, cwd, { ...programmaticStepEnv(resolveEnv(), repoDir, executionManifest), SPECRAILS_EXECUTION_CONTEXT: core.contextPath }, coreRun.runId);
253
+ return { text: checked.valid ? 'VERIFICATION: PASS — Core verified the exact implementation and review.' : checked.reason ?? 'Core verification failed', failed: !checked.valid, errorText: checked.reason, provider: 'agent-runtime', model: 'deterministic-verification', cost: 0, tokensIn: 0, tokensOut: 0, tokens: 0, estimated: false, durationMs: 0 };
254
+ }
226
255
  let extraArgs = aiStepExtraArgs(adapter, cwd, repoDir, executionManifest);
227
256
  const pluginScope = adapter.id === 'codex' ? opts.pluginScope?.() : undefined;
228
257
  const pluginArgs = (0, codex_spawn_1.buildCodexPluginArgs)({ providerId: adapter.id, stateRoot: pluginScope?.stateRoot ?? stepEnv.SPECRAILS_WORKSPACE_DIR ?? cwd, repositoryPath: repoDir ?? cwd, legacyProviderId: pluginScope?.legacyProviderId });
@@ -431,6 +460,13 @@ function createLoopExecutors(opts = {}) {
431
460
  * (non-claude providers, or the kill-switch) — byte-identical legacy.
432
461
  */
433
462
  planInteractiveAiStep({ coreRun, provider, model, effort, profileName, cwd, repoDir, executionManifest, sessionId, aiStepTimeoutMs, idleTimeoutMs }) {
463
+ if (coreRun?.implementation || coreRun?.verificationStep) {
464
+ const env = resolveEnv();
465
+ const root = env.SPECRAILS_TICKETS_PATH ? (0, node_path_1.dirname)((0, node_path_1.dirname)(env.SPECRAILS_TICKETS_PATH)) : cwd;
466
+ const contextPath = (0, node_path_1.join)(root, '.specrails', 'pipeline', coreRun.runId, 'desktop-context.json');
467
+ if ((coreRun.implementation && (0, agent_runtime_bridge_1.selectAgentRuntime)(runtimeConfigPath(cwd), contextPath)) || existsRuntimeRequest(contextPath))
468
+ return null;
469
+ }
434
470
  if (!(0, feature_flags_1.isInteractiveJobsEnabled)())
435
471
  return null;
436
472
  const adapter = (0, providers_1.getAdapter)(provider);
@@ -490,8 +526,12 @@ function createLoopExecutors(opts = {}) {
490
526
  };
491
527
  },
492
528
  validateCoreCompletion({ coreRun, provider, model, effort, profileName, cwd, repoDir, executionManifest }) {
493
- const baseStepEnv = withProfileEnv(aiStepEnv(resolveEnv(), repoDir, executionManifest), provider, profileName);
529
+ const baseEnv = resolveEnv();
530
+ const programmatic = existsRuntimeRequest(runtimeContextPath(cwd, coreRun.runId, baseEnv));
531
+ const baseStepEnv = programmatic ? programmaticStepEnv(baseEnv, repoDir, executionManifest) : withProfileEnv(aiStepEnv(baseEnv, repoDir, executionManifest), provider, profileName);
494
532
  const core = (0, core_execution_1.prepareCoreExecution)({ run: coreRun, cwd, repoDir, manifest: executionManifest, env: baseStepEnv });
533
+ if (existsRuntimeRequest(core.contextPath))
534
+ return (0, core_execution_1.checkCoreCompletion)(core.contextPath, cwd, { ...programmaticStepEnv(resolveEnv(), repoDir, executionManifest), SPECRAILS_EXECUTION_CONTEXT: core.contextPath }, coreRun.runId);
495
535
  const env = (0, runtime_1.buildProviderEnv)((0, providers_1.getAdapter)(provider), { prompt: '', model, reasoning_effort: effort }, core.env);
496
536
  return (0, core_execution_1.checkCoreCompletion)(core.contextPath, cwd, env, coreRun.runId);
497
537
  },
@@ -21,7 +21,7 @@ function coreImplementationGraph(command) {
21
21
  return {
22
22
  nodes: [
23
23
  { id: 'start', type: 'start', position: { x: 0, y: 0 } },
24
- { id: 'main-1', type: 'ai-step', position: { x: 0, y: 110 }, data: { prompt: `{{cmd:${command}}}` } },
24
+ { id: 'main-1', type: 'ai-step', position: { x: 0, y: 110 }, data: { operation: 'core-implementation', prompt: `{{cmd:${command}}}` } },
25
25
  { id: 'done', type: 'end', position: { x: 0, y: 220 }, data: { outcome: 'success' } },
26
26
  ],
27
27
  edges: [