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.
- package/README.md +2 -0
- package/client/dist/assets/{ActivityFeedPage-CL1BUXoj.js → ActivityFeedPage-CsHQPpvl.js} +1 -1
- package/client/dist/assets/AgentBrowserCapture-BVcCK7x0.js +1 -0
- package/client/dist/assets/AgentModeAnalyticsPane-C5F3m29G.js +2 -0
- package/client/dist/assets/AgentModeCodePane-C5QuKsu0.js +2 -0
- package/client/dist/assets/AgentModeJobsPane-rnD-EPMd.js +1 -0
- package/client/dist/assets/{AgentsPage-DbLezyI6.js → AgentsPage-DiyJWZcU.js} +3 -3
- package/client/dist/assets/{AnalyticsPage-DTsg-N5z.js → AnalyticsPage-Dkmb0dnd.js} +1 -1
- package/client/dist/assets/CodePage-C0dH99-t.js +3 -0
- package/client/dist/assets/{DesktopAnalyticsPage-Y0Z1ryQr.js → DesktopAnalyticsPage-DuTjr2n8.js} +1 -1
- package/client/dist/assets/{DocsDialog-KeCOxvGH.js → DocsDialog-BVZJZHVR.js} +1 -1
- package/client/dist/assets/{DocsPage-Bt1PgYPO.js → DocsPage-D6KHTv8V.js} +1 -1
- package/client/dist/assets/{ExportDropdown-DczgU4at.js → ExportDropdown-pWZGYCv3.js} +1 -1
- package/client/dist/assets/InteractiveJobComposer-Ck3PQxjN.js +19 -0
- package/client/dist/assets/JobDetailModal-DkoMH3ie.js +1 -0
- package/client/dist/assets/JobDetailPage-CBVivcl2.js +1 -0
- package/client/dist/assets/JobsPage-CzxEeca9.js +1 -0
- package/client/dist/assets/{LoopBuilderPage-CqC4hbXb.js → LoopBuilderPage-CtmOE4-b.js} +2 -2
- package/client/dist/assets/{LoopPreviewModal-T0zuFv6y.js → LoopPreviewModal-BYeLjQbr.js} +1 -1
- package/client/dist/assets/{LoopsPage-DrQGcZv2.js → LoopsPage-6iW7g25k.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext-C843Y13h.js → MinimizedChatsContext-B6wVevPq.js} +1 -1
- package/client/dist/assets/PluginsPage-Bf1z3WPF.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-DsCRIc4v.js +1 -0
- package/client/dist/assets/{RepositoryDeliveries-bgIHE8ep.js → RepositoryDeliveries-BQm_3TO8.js} +1 -1
- package/client/dist/assets/{RepositoryScopeSelector-CZ_U_R3U.js → RepositoryScopeSelector-DnAFtjTz.js} +1 -1
- package/client/dist/assets/{ReviewPacketPage-DbKO6pT8.js → ReviewPacketPage-CaXAwtR7.js} +1 -1
- package/client/dist/assets/TemplatePreviewModal-9aRFpJ_J.js +1 -0
- package/client/dist/assets/{TicketDetailModalContext-BbRA6cb7.js → TicketDetailModalContext-B1X5glzV.js} +1 -1
- package/client/dist/assets/{Trans-mjFZpHw1.js → Trans-BwLgX6n7.js} +1 -1
- package/client/dist/assets/agentRuntime-BUagD2I_.js +1 -0
- package/client/dist/assets/agentRuntime-BkL70WtX.js +1 -0
- package/client/dist/assets/agentRuntime-CWXVqgqE.js +1 -0
- package/client/dist/assets/agentRuntime-CvNBdUAe.js +1 -0
- package/client/dist/assets/agentRuntime-D4RmeuNY.js +1 -0
- package/client/dist/assets/agentRuntime-DTidBWKg.js +1 -0
- package/client/dist/assets/agentRuntime-DhKYfyaU.js +1 -0
- package/client/dist/assets/agentRuntime-LL8_GLx9.js +1 -0
- package/client/dist/assets/format-duration-NJVPOPBG.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-BIL-HxGy.js → formatDistanceToNow-CeZv93iR.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-Cs9z2sdO.js → getTimezoneOffsetInMilliseconds-DxAkjvJb.js} +1 -1
- package/client/dist/assets/index-Cfh9aaep.css +2 -0
- package/client/dist/assets/index-DYoyDWim.js +76 -0
- package/client/dist/assets/{jira-api-B2BBZQTx.js → jira-api-ChjVGJx_.js} +1 -1
- package/client/dist/assets/narration-4OSg0b5-.js +1 -0
- package/client/dist/assets/narration-BPMOQPFe.js +1 -0
- package/client/dist/assets/narration-BSldQfjd.js +1 -0
- package/client/dist/assets/narration-CvQdWdR5.js +1 -0
- package/client/dist/assets/narration-D-e9ssxA.js +1 -0
- package/client/dist/assets/narration-DKLjXIFI.js +1 -0
- package/client/dist/assets/narration-DnrKC3mF.js +1 -0
- package/client/dist/assets/narration-Dypjxeel.js +1 -0
- package/client/dist/assets/{project-repositories-xSqimlaj.js → project-repositories-BvBj9XH9.js} +1 -1
- package/client/dist/assets/{provider-capabilities-B-wOHfLP.js → provider-capabilities-CXipwrXJ.js} +1 -1
- package/client/dist/assets/{settings-2Yx_9yJM.js → settings-B3rWEwOx.js} +1 -1
- package/client/dist/assets/{settings-BLOsrXEw.js → settings-BcVoiMyw.js} +1 -1
- package/client/dist/assets/settings-BlYvaQ7w.js +1 -0
- package/client/dist/assets/{settings-DPjNiUsS.js → settings-CD5uEBiL.js} +1 -1
- package/client/dist/assets/{settings-CpKwTew1.js → settings-CyhS0DLJ.js} +1 -1
- package/client/dist/assets/{settings-FIqwhTup.js → settings-DCMFUGDs.js} +1 -1
- package/client/dist/assets/{settings-C6s9PtRD.js → settings-DEgcA0jd.js} +1 -1
- package/client/dist/assets/{settings-NSWQjY5B.js → settings-DmbYKyMJ.js} +1 -1
- package/client/dist/assets/{spending-DXOcv1tn.js → spending-DNM6NAoA.js} +1 -1
- package/client/dist/assets/{useDesktop-gVGoWWiY.js → useDesktop-mxHVQOdx.js} +1 -1
- package/client/dist/assets/useSharedWebSocket-DnxHj8jP.js +2 -0
- package/client/dist/index.html +19 -17
- package/docs/README.md +1 -0
- package/docs/internals/README.md +2 -0
- package/docs/internals/agent-runtime-framework-evaluation.md +208 -0
- package/docs/internals/programmatic-agent-runtime-validation.md +72 -0
- package/docs/internals/programmatic-agent-runtime.md +140 -0
- package/package.json +1 -1
- package/server/dist/agent-mcp-config.js +5 -0
- package/server/dist/agent-operator-prompt.js +2 -2
- package/server/dist/agent-runtime-accounting.js +73 -0
- package/server/dist/agent-runtime-bridge.js +231 -0
- package/server/dist/agent-runtime-controls-router.js +86 -0
- package/server/dist/agent-runtime-controls.js +341 -0
- package/server/dist/agent-runtime-loader.js +91 -0
- package/server/dist/agent-runtime-metrics.js +46 -0
- package/server/dist/agent-runtime-paths.js +12 -0
- package/server/dist/agent-runtime-repositories.js +37 -0
- package/server/dist/agent-runtime-settings-router.js +61 -0
- package/server/dist/agent-runtime-settings.js +239 -0
- package/server/dist/agent-runtime-settlement.js +133 -0
- package/server/dist/agent-runtime-verification-suggestions.js +119 -0
- package/server/dist/contract-refine-runner.js +7 -7
- package/server/dist/core-completion.js +8 -5
- package/server/dist/core-execution.js +17 -8
- package/server/dist/core-node-runtime.js +12 -0
- package/server/dist/desktop-router.js +26 -0
- package/server/dist/job-listing.js +7 -3
- package/server/dist/loop-command-catalog.js +2 -2
- package/server/dist/loop-executors.js +44 -2
- package/server/dist/loop-factory.js +1 -1
- package/server/dist/loop-run-manager.js +62 -28
- package/server/dist/mcp/tools/specs.js +7 -4
- package/server/dist/multi-repo-execution.js +1 -0
- package/server/dist/project-registry.js +9 -0
- package/server/dist/project-router-jobs.js +8 -2
- package/server/dist/project-router-settings.js +4 -0
- package/server/dist/project-router-tickets.js +4 -4
- package/server/dist/providers/runtime.js +6 -0
- package/server/dist/rail-isolated-launch.js +18 -0
- package/server/dist/rails-router.js +4 -1
- package/server/dist/runtime-role-prompts-router.js +25 -0
- package/server/dist/schemas/agent-runtime.schema.json +60 -0
- package/server/dist/worktree-overlay.js +32 -1
- package/client/dist/assets/AgentBrowserCapture-BQem50z4.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BC_xbT_l.js +0 -2
- package/client/dist/assets/AgentModeCodePane-DK-q3K6Y.js +0 -2
- package/client/dist/assets/AgentModeJobsPane-C7ELTPC1.js +0 -1
- package/client/dist/assets/CodePage-DwgbeqQ_.js +0 -3
- package/client/dist/assets/InteractiveJobComposer-BcR6xlHU.js +0 -19
- package/client/dist/assets/JobDetailModal-DKA_DUHe.js +0 -1
- package/client/dist/assets/JobDetailPage-vaOcE-cI.js +0 -1
- package/client/dist/assets/JobsPage-Bwoe-Fb0.js +0 -1
- package/client/dist/assets/PluginsPage-CTUHX_Lu.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-DeL8md1s.js +0 -1
- package/client/dist/assets/TemplatePreviewModal-_CgCCV7L.js +0 -1
- package/client/dist/assets/index-Bfj8HvX1.css +0 -2
- package/client/dist/assets/index-UnaJTMIy.js +0 -76
- package/client/dist/assets/narration-B-ynWTpn.js +0 -1
- package/client/dist/assets/narration-Bj_laZwg.js +0 -1
- package/client/dist/assets/narration-BxHpmMv_.js +0 -1
- package/client/dist/assets/narration-C1Px0-0j.js +0 -1
- package/client/dist/assets/narration-CAXhGbzM.js +0 -1
- package/client/dist/assets/narration-DQ1l_Kq6.js +0 -1
- package/client/dist/assets/narration-XISXwqZd.js +0 -1
- package/client/dist/assets/narration-kwzfglQo.js +0 -1
- package/client/dist/assets/settings-CCNZN5Q-.js +0 -1
- 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
|
|
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 (
|
|
366
|
-
console.log(`[contract-refine-runner] skip: provider '${adapter.id}' does not support
|
|
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 (
|
|
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
|
|
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
|
|
47
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
119
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 };
|