specrails-desktop 2.45.0 → 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/client/dist/assets/{ActivityFeedPage-D3SQklu5.js → ActivityFeedPage-CsHQPpvl.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-DNGxE33d.js → AgentBrowserCapture-BVcCK7x0.js} +1 -1
- 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-DiyJWZcU.js +87 -0
- package/client/dist/assets/{AnalyticsPage-BqV6mM2O.js → AnalyticsPage-Dkmb0dnd.js} +1 -1
- package/client/dist/assets/CodePage-C0dH99-t.js +3 -0
- package/client/dist/assets/{DesktopAnalyticsPage-D6WNax3K.js → DesktopAnalyticsPage-DuTjr2n8.js} +1 -1
- package/client/dist/assets/{DocsDialog-68jKUH9f.js → DocsDialog-BVZJZHVR.js} +1 -1
- package/client/dist/assets/{DocsPage-BoGJgRpm.js → DocsPage-D6KHTv8V.js} +1 -1
- package/client/dist/assets/{ExportDropdown-B2C_XLGp.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-D21GxFmL.js → LoopBuilderPage-CtmOE4-b.js} +2 -2
- package/client/dist/assets/{LoopPreviewModal-Bo3fYFxN.js → LoopPreviewModal-BYeLjQbr.js} +1 -1
- package/client/dist/assets/{LoopsPage-BcrBw1cg.js → LoopsPage-6iW7g25k.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext-Dsv2eHf_.js → MinimizedChatsContext-B6wVevPq.js} +1 -1
- package/client/dist/assets/{PluginsPage-BbyTmSLx.js → PluginsPage-Bf1z3WPF.js} +2 -2
- package/client/dist/assets/{ProjectSettingsDialog-B3lEXLv7.js → ProjectSettingsDialog-DsCRIc4v.js} +1 -1
- package/client/dist/assets/{RepositoryDeliveries-ok_Wo65C.js → RepositoryDeliveries-BQm_3TO8.js} +1 -1
- package/client/dist/assets/{RepositoryScopeSelector-DU6QwNaS.js → RepositoryScopeSelector-DnAFtjTz.js} +1 -1
- package/client/dist/assets/{ReviewPacketPage-BBCeHtgd.js → ReviewPacketPage-CaXAwtR7.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-CfNmxS6O.js → TemplatePreviewModal-9aRFpJ_J.js} +1 -1
- package/client/dist/assets/{TicketDetailModalContext-DW3eQ0I7.js → TicketDetailModalContext-B1X5glzV.js} +1 -1
- package/client/dist/assets/{Trans-BHPMWkOw.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-Bq8iptrG.js → formatDistanceToNow-CeZv93iR.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BVFky3l_.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-zrRdZFBB.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-CFRaB9UB.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-OOyDrZQo.js → settings-B3rWEwOx.js} +1 -1
- package/client/dist/assets/{settings-CNiERE-R.js → settings-BcVoiMyw.js} +1 -1
- package/client/dist/assets/settings-BlYvaQ7w.js +1 -0
- package/client/dist/assets/{settings-DJ2jl3hJ.js → settings-CD5uEBiL.js} +1 -1
- package/client/dist/assets/{settings-a7NIODme.js → settings-CyhS0DLJ.js} +1 -1
- package/client/dist/assets/{settings-DVRVybR_.js → settings-DCMFUGDs.js} +1 -1
- package/client/dist/assets/{settings-CrtCs5-B.js → settings-DEgcA0jd.js} +1 -1
- package/client/dist/assets/{settings-ChOCVjX_.js → settings-DmbYKyMJ.js} +1 -1
- package/client/dist/assets/{spending-lTZ-bm_y.js → spending-DNM6NAoA.js} +1 -1
- package/client/dist/assets/{useDesktop-Duv5ddAa.js → useDesktop-mxHVQOdx.js} +1 -1
- package/client/dist/assets/{useSharedWebSocket-5mj2SMKn.js → useSharedWebSocket-DnxHj8jP.js} +2 -2
- package/client/dist/index.html +19 -18
- package/docs/internals/programmatic-agent-runtime.md +19 -8
- 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-bridge.js +62 -14
- package/server/dist/agent-runtime-controls-router.js +42 -0
- package/server/dist/agent-runtime-controls.js +68 -4
- package/server/dist/agent-runtime-loader.js +6 -0
- package/server/dist/agent-runtime-metrics.js +46 -0
- package/server/dist/agent-runtime-repositories.js +37 -0
- package/server/dist/agent-runtime-settings-router.js +5 -3
- package/server/dist/agent-runtime-settings.js +125 -21
- package/server/dist/agent-runtime-settlement.js +133 -0
- package/server/dist/contract-refine-runner.js +7 -7
- package/server/dist/core-completion.js +8 -5
- 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 +5 -3
- package/server/dist/loop-run-manager.js +2 -2
- package/server/dist/mcp/tools/specs.js +7 -4
- package/server/dist/multi-repo-execution.js +1 -0
- package/server/dist/project-router-jobs.js +8 -2
- package/server/dist/project-router-tickets.js +4 -4
- package/server/dist/providers/runtime.js +6 -0
- package/server/dist/rail-isolated-launch.js +1 -1
- package/server/dist/rails-router.js +3 -1
- package/server/dist/runtime-role-prompts-router.js +25 -0
- package/server/dist/schemas/agent-runtime.schema.json +7 -2
- package/server/dist/worktree-overlay.js +32 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BUvZNoj0.js +0 -2
- package/client/dist/assets/AgentModeCodePane-YW717UvJ.js +0 -2
- package/client/dist/assets/AgentModeJobsPane-Cxg_eNmd.js +0 -1
- package/client/dist/assets/AgentsPage-B_DCByok.js +0 -87
- package/client/dist/assets/CodePage-PyZKCj1B.js +0 -3
- package/client/dist/assets/InteractiveJobComposer-l8q-r5Yy.js +0 -19
- package/client/dist/assets/JobDetailModal-C-X2GI9j.js +0 -1
- package/client/dist/assets/JobDetailPage-C6Jdm7S9.js +0 -1
- package/client/dist/assets/JobsPage-DycM6Vgp.js +0 -1
- package/client/dist/assets/agentRuntime-14faDjWM.js +0 -1
- package/client/dist/assets/agentRuntime-BIokojkJ.js +0 -1
- package/client/dist/assets/agentRuntime-Bu4xEcRw.js +0 -1
- package/client/dist/assets/agentRuntime-Cz0ASixB.js +0 -1
- package/client/dist/assets/agentRuntime-DESdGXLV.js +0 -1
- package/client/dist/assets/agentRuntime-DhX5Xy5S.js +0 -1
- package/client/dist/assets/agentRuntime-Dpbh_gLp.js +0 -1
- package/client/dist/assets/agentRuntime-jS2lplFm.js +0 -1
- package/client/dist/assets/index-CBLYwPw6.css +0 -2
- package/client/dist/assets/index-DOKftX7y.js +0 -76
- package/client/dist/assets/narration-BA8Yw5ZL.js +0 -1
- package/client/dist/assets/narration-BuMFUS7Q.js +0 -1
- package/client/dist/assets/narration-Bv_iQQG5.js +0 -1
- package/client/dist/assets/narration-C6Ost9nL.js +0 -1
- package/client/dist/assets/narration-CIbjo2pA.js +0 -1
- package/client/dist/assets/narration-CNJ2-2-a.js +0 -1
- package/client/dist/assets/narration-OzsPJCXM.js +0 -1
- package/client/dist/assets/narration-zCdgiuSU.js +0 -1
- package/client/dist/assets/settings-DzXZCb1h.js +0 -1
package/client/dist/index.html
CHANGED
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
.specrails-splash__mark .pill { opacity: 1; }
|
|
205
205
|
}
|
|
206
206
|
</style>
|
|
207
|
-
<script type="module" crossorigin src="/assets/index-
|
|
207
|
+
<script type="module" crossorigin src="/assets/index-DYoyDWim.js"></script>
|
|
208
208
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CNC7AqOf.js">
|
|
209
209
|
<link rel="modulepreload" crossorigin href="/assets/jsx-runtime-zu2_FqZY.js">
|
|
210
210
|
<link rel="modulepreload" crossorigin href="/assets/react-dom-DiWzBDP9.js">
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
<link rel="modulepreload" crossorigin href="/assets/activity-DiyjsgQV.js">
|
|
214
214
|
<link rel="modulepreload" crossorigin href="/assets/addspec-D2YIQ0GU.js">
|
|
215
215
|
<link rel="modulepreload" crossorigin href="/assets/agent-Bh9K7iEt.js">
|
|
216
|
-
<link rel="modulepreload" crossorigin href="/assets/agentRuntime-
|
|
216
|
+
<link rel="modulepreload" crossorigin href="/assets/agentRuntime-D4RmeuNY.js">
|
|
217
217
|
<link rel="modulepreload" crossorigin href="/assets/agents-ms7TwG16.js">
|
|
218
218
|
<link rel="modulepreload" crossorigin href="/assets/agentstudio-CN2pokQu.js">
|
|
219
219
|
<link rel="modulepreload" crossorigin href="/assets/aiedit-iIjz2_Qk.js">
|
|
@@ -232,23 +232,23 @@
|
|
|
232
232
|
<link rel="modulepreload" crossorigin href="/assets/jobs-DMPU64ms.js">
|
|
233
233
|
<link rel="modulepreload" crossorigin href="/assets/loops-B7YpGizO.js">
|
|
234
234
|
<link rel="modulepreload" crossorigin href="/assets/mcp-CUAN6ORK.js">
|
|
235
|
-
<link rel="modulepreload" crossorigin href="/assets/narration-
|
|
235
|
+
<link rel="modulepreload" crossorigin href="/assets/narration-Dypjxeel.js">
|
|
236
236
|
<link rel="modulepreload" crossorigin href="/assets/nav-Ch-zHBG5.js">
|
|
237
237
|
<link rel="modulepreload" crossorigin href="/assets/packet-Cx-zRuWn.js">
|
|
238
|
-
<link rel="modulepreload" crossorigin href="/assets/settings-
|
|
238
|
+
<link rel="modulepreload" crossorigin href="/assets/settings-BlYvaQ7w.js">
|
|
239
239
|
<link rel="modulepreload" crossorigin href="/assets/setup-Ba11l2JA.js">
|
|
240
240
|
<link rel="modulepreload" crossorigin href="/assets/specs-DII0C8gw.js">
|
|
241
241
|
<link rel="modulepreload" crossorigin href="/assets/terminal-BAzpHtpY.js">
|
|
242
242
|
<link rel="modulepreload" crossorigin href="/assets/tickets-W6pBmbrX.js">
|
|
243
243
|
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-Bjbbp_KS.js">
|
|
244
|
-
<link rel="modulepreload" crossorigin href="/assets/useSharedWebSocket-
|
|
245
|
-
<link rel="modulepreload" crossorigin href="/assets/Trans-
|
|
246
|
-
<link rel="modulepreload" crossorigin href="/assets/useDesktop-
|
|
244
|
+
<link rel="modulepreload" crossorigin href="/assets/useSharedWebSocket-DnxHj8jP.js">
|
|
245
|
+
<link rel="modulepreload" crossorigin href="/assets/Trans-BwLgX6n7.js">
|
|
246
|
+
<link rel="modulepreload" crossorigin href="/assets/useDesktop-mxHVQOdx.js">
|
|
247
247
|
<link rel="modulepreload" crossorigin href="/assets/save-B6wGWwBR.js">
|
|
248
|
-
<link rel="modulepreload" crossorigin href="/assets/spending-
|
|
248
|
+
<link rel="modulepreload" crossorigin href="/assets/spending-DNM6NAoA.js">
|
|
249
249
|
<link rel="modulepreload" crossorigin href="/assets/sparkles-DrY8hb0X.js">
|
|
250
250
|
<link rel="modulepreload" crossorigin href="/assets/x-CBMI9ejn.js">
|
|
251
|
-
<link rel="modulepreload" crossorigin href="/assets/MinimizedChatsContext-
|
|
251
|
+
<link rel="modulepreload" crossorigin href="/assets/MinimizedChatsContext-B6wVevPq.js">
|
|
252
252
|
<link rel="modulepreload" crossorigin href="/assets/file-text-BcLCfcWZ.js">
|
|
253
253
|
<link rel="modulepreload" crossorigin href="/assets/circle-x-CMYsN4ZH.js">
|
|
254
254
|
<link rel="modulepreload" crossorigin href="/assets/download-CFHyyHcw.js">
|
|
@@ -260,23 +260,24 @@
|
|
|
260
260
|
<link rel="modulepreload" crossorigin href="/assets/search-Cq_gE3Ik.js">
|
|
261
261
|
<link rel="modulepreload" crossorigin href="/assets/trash-2-BOU3WdZZ.js">
|
|
262
262
|
<link rel="modulepreload" crossorigin href="/assets/triangle-alert-H9T4jAmh.js">
|
|
263
|
-
<link rel="modulepreload" crossorigin href="/assets/jira-api-
|
|
263
|
+
<link rel="modulepreload" crossorigin href="/assets/jira-api-ChjVGJx_.js">
|
|
264
264
|
<link rel="modulepreload" crossorigin href="/assets/zap-CHo3aCD1.js">
|
|
265
265
|
<link rel="modulepreload" crossorigin href="/assets/clsx-CjueKrWZ.js">
|
|
266
266
|
<link rel="modulepreload" crossorigin href="/assets/utils-BC4DHlz2.js">
|
|
267
267
|
<link rel="modulepreload" crossorigin href="/assets/dialog-xXB2q5HJ.js">
|
|
268
268
|
<link rel="modulepreload" crossorigin href="/assets/lib-CGdyxIWg.js">
|
|
269
|
-
<link rel="modulepreload" crossorigin href="/assets/getTimezoneOffsetInMilliseconds-
|
|
270
|
-
<link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-
|
|
269
|
+
<link rel="modulepreload" crossorigin href="/assets/getTimezoneOffsetInMilliseconds-DxAkjvJb.js">
|
|
270
|
+
<link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-CeZv93iR.js">
|
|
271
271
|
<link rel="modulepreload" crossorigin href="/assets/button-Ccc9Dc-D.js">
|
|
272
|
-
<link rel="modulepreload" crossorigin href="/assets/project-repositories-
|
|
273
|
-
<link rel="modulepreload" crossorigin href="/assets/RepositoryScopeSelector-
|
|
274
|
-
<link rel="modulepreload" crossorigin href="/assets/provider-capabilities-
|
|
275
|
-
<link rel="modulepreload" crossorigin href="/assets/TicketDetailModalContext-
|
|
272
|
+
<link rel="modulepreload" crossorigin href="/assets/project-repositories-BvBj9XH9.js">
|
|
273
|
+
<link rel="modulepreload" crossorigin href="/assets/RepositoryScopeSelector-DnAFtjTz.js">
|
|
274
|
+
<link rel="modulepreload" crossorigin href="/assets/provider-capabilities-CXipwrXJ.js">
|
|
275
|
+
<link rel="modulepreload" crossorigin href="/assets/TicketDetailModalContext-B1X5glzV.js">
|
|
276
276
|
<link rel="modulepreload" crossorigin href="/assets/core-B3mKBIh5.js">
|
|
277
|
-
<link rel="modulepreload" crossorigin href="/assets/RepositoryDeliveries-
|
|
277
|
+
<link rel="modulepreload" crossorigin href="/assets/RepositoryDeliveries-BQm_3TO8.js">
|
|
278
|
+
<link rel="modulepreload" crossorigin href="/assets/format-duration-NJVPOPBG.js">
|
|
278
279
|
<link rel="modulepreload" crossorigin href="/assets/event-Bt7f3wXe.js">
|
|
279
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
280
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cfh9aaep.css">
|
|
280
281
|
</head>
|
|
281
282
|
<body>
|
|
282
283
|
<div id="specrails-splash" aria-hidden="false" role="status" aria-label="Loading Specrails">
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Desktop can hand implementation to Core's **runtime API 1**. Core runs architect, developer, deterministic verification, reviewer and archive as separate LangGraph phases. Desktop keeps project/worktree selection, rail lifecycle, logs, accounting and delivery ownership.
|
|
4
4
|
|
|
5
|
-
This is
|
|
5
|
+
This is the only implementation engine in the current source tree. It applies to implementation rail steps and their Core completion check. Mission chat and unrelated AI features keep their existing transports. Provider-native implementation prompts and skills are not invoked inside the programmatic phases.
|
|
6
6
|
|
|
7
7
|
## Build the paired source
|
|
8
8
|
|
|
@@ -53,16 +53,16 @@ Desktop negotiates `runtime api` and sends configuration to `runtime validate --
|
|
|
53
53
|
|
|
54
54
|
`SPECRAILS_CORE_RUNTIME_PATH` selects this execution module. The existing `SPECRAILS_CORE_BIN` controls the installation/lifecycle resolver and is a different setting. Prefer a paired source bundle when testing the complete installation and execution flow.
|
|
55
55
|
|
|
56
|
-
##
|
|
56
|
+
## Configure a project
|
|
57
57
|
|
|
58
58
|
1. Open **Project settings → Agent runtime** (its own section in the project dialog).
|
|
59
|
-
2. Choose a provider per role. The model dropdown lists each CLI's catalog with the default marked; turns, attempts and timeout show their defaults in the fields. Claude, Codex, Gemini and Kimi remain available; open **
|
|
59
|
+
2. Choose a provider per role. The model dropdown lists each CLI's catalog with the default marked; turns, attempts and timeout show their defaults in the fields. Claude, Codex, Gemini and Kimi remain available; open **General settings → Specrails Agents → Provider connections** to add an OpenAI-compatible endpoint for a local or remote model (local endpoints do not require a key; API providers need an explicit model).
|
|
60
60
|
3. Verification commands are optional. A project that never saved runtime settings is prefilled with the checks Desktop detects offline (`package.json` test/type-check/lint scripts with the right package manager, Cargo, Go, pytest, Gradle, Maven, .NET, Make); **Detect project checks** re-runs that detection. Each row is a repository plus one command line (`npm run test -- --strict`; quotes group arguments). Leave the list empty and the architect proposes the project's own checks on each run; repositories with no automated check are still reviewed and recorded as unverified in Core's receipt.
|
|
61
61
|
4. **Review gate** and **Architect confidence** are optional. Review thresholds (overall score and the five aspects: type correctness, pattern adherence, test coverage, security, architectural alignment) can only *tighten* Core's own gate: the floors are 70 overall, 75 for security and 60 for every other aspect, and Desktop rejects lower values before saving (`Review threshold review.minScore must be at least 70 (Core's own review gate)`). Empty fields omit the key so Core's defaults apply. `architect.onLowConfidence` decides what happens when, after one autonomous investigation pass, the architect's design is still low in confidence: `ask` (default) pauses the run with the architect's question; `proceed` continues on stated assumptions.
|
|
62
|
-
5.
|
|
62
|
+
5. Save the project settings. Implementation always uses the agent runtime.
|
|
63
63
|
6. Start an implementation through the normal rail flow.
|
|
64
64
|
|
|
65
|
-
Settings are saved at `<project execution .specrails directory>/agent-runtime.json`. Missing
|
|
65
|
+
Settings are saved at `<project execution .specrails directory>/agent-runtime.json`. Missing configuration uses the default agent runtime. The retired enabled flag cannot select another engine. Malformed configuration blocks admission; it is not ignored. Saving verifies that Core exposes the expected API. Connections are stored globally in `~/.specrails/runtime-providers.json`; project files retain role references, models, limits and verification. Existing embedded connections migrate once, with stable disambiguated IDs on endpoint conflicts. New runs freeze the resolved configuration, while saved runs retain their original snapshot.
|
|
66
66
|
|
|
67
67
|
Core owns role instructions and permissions. The developer role edits and runs commands inside its CLI sandbox (the same autonomy as the legacy Implement step); architect and reviewer are read-only. A legacy rail profile/model selection does not override the runtime's per-role provider configuration. The JSON schema is [server/schemas/agent-runtime.schema.json](../../server/schemas/agent-runtime.schema.json), mirrored from Core. For a complete configuration, custom executor examples, Kimi capabilities and API tooling details, see [Core's runtime guide](https://github.com/fjpulidop/specrails-core/blob/main/docs/agent-runtime.md) in the paired revision.
|
|
68
68
|
|
|
@@ -77,6 +77,7 @@ The rail creates a frozen Core execution context for its original repository/wor
|
|
|
77
77
|
| File | Purpose |
|
|
78
78
|
| --- | --- |
|
|
79
79
|
| `desktop-context.json` | Selected repositories, original worktrees, frozen scope and ownership |
|
|
80
|
+
| `desktop-runtime-config.json` | Admission snapshot of project settings with verification commands restricted to the selected repositories; project settings remain unchanged |
|
|
80
81
|
| `desktop-runtime-host.json` | Allowlisted host settings needed to reconstruct execution |
|
|
81
82
|
| `agent-runtime-request.json` | Core's frozen configuration and change name |
|
|
82
83
|
| `state.json`, `receipts/` | Core gates and verification receipts |
|
|
@@ -84,11 +85,13 @@ The rail creates a frozen Core execution context for its original repository/wor
|
|
|
84
85
|
|
|
85
86
|
A run pauses (Core exit code 2) either on an **approval** (`pendingApproval`, for example before archive) or on a **question** (`pendingQuestion: { stepId, requestedAt, question }`) when the architect is configured to ask on low confidence. A pending question can only be resumed together with an answer: the runs panel shows the question with a textarea and an **Answer and resume** action, which posts `{ "answer": "…" }` (nonempty, at most 20,000 characters). Resuming without an answer while a question is open is rejected with `400 answer_required`. Once Core records `answeredAt`, the question is history and ordinary resume applies again. The bridge reports a paused run's reason in the job log: awaiting approval, or the pending question text.
|
|
86
87
|
|
|
87
|
-
**
|
|
88
|
+
**Implementation cards and job detail** expose resume, archive approval, question answering, interrupted-step recovery and continuation cancellation actions next to the work. Cards query their latest job by original rail identity; job detail queries its exact run. Legacy jobs render no runtime panel. **Jobs → Saved executions** retains the cross-run history. Project settings contain runtime configuration only. A continuation resumes from the phase shown, in the original worktree, and writes its progress into that job's log (a `[runtime] continuation started from phase …` banner, tool activity, phase notes and the final outcome). Resuming a run that stopped at the developer attempt limit grants a fresh attempt budget. Wait for the original rail execution to settle before resuming. Active rail jobs are stopped through their job controls; the continuation's Cancel action owns only continuations started from this panel.
|
|
88
89
|
|
|
89
90
|
Resume retains valid completed phases and rechecks Core evidence. Changed code or environment requires fresh verification/review. An ambiguous interrupted write requires an explicit recovery action after inspecting partial changes. A changed frozen config/identity requires a new run. Missing original worktrees or mismatched execution manifests block recovery; the controller never invents a replacement worktree.
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
Saved executions link directly to the original job log. Continuations broadcast readable logs and raw runtime events to that job, publish `runtime.continuation` lifecycle updates, and reserve the original implementation card until settlement. Jobs list/detail project the continuation as running while it is active (including the running filter); the failed attempt remains in history. Job and card cancellation route to the continuation process. After a successful continuation, Desktop revalidates Core receipts, commits the exact preserved worktrees locally, and reconciles the job and repository delivery cards to completed / ready for review. A new completion event supersedes the earlier failed summary while retaining the failed attempt and its usage. The activity reservation then clears.
|
|
93
|
+
|
|
94
|
+
**A continuation prepares local delivery for review after Core succeeds; it does not create a PR or merge code.** Use the normal delivery card actions to review and publish. Older successful continuations expose **Prepare delivery**, which performs the same receipt validation and local settlement without invoking a model. This also applies when approval is granted after the original rail has settled. Core archive success is not a claim that a PR was created. The initial uninterrupted successful rail retains its normal host delivery flow.
|
|
92
95
|
|
|
93
96
|
Disabling project runtime settings affects future admission. Existing runs retain their frozen runtime request and remain available for explicit continuation; they do not switch back to a platform prompt.
|
|
94
97
|
|
|
@@ -122,8 +125,16 @@ A production release must:
|
|
|
122
125
|
|
|
123
126
|
Do not relabel an old 5.1.1 bundle or copy only `dist/agent-runtime`: LangGraph and the complete runtime dependency closure are required. Source assembly writes `source-bundle.json` with the Core version, runtime API and lock hash for traceability; it does not publish Core or update the production registry lock.
|
|
124
127
|
|
|
125
|
-
|
|
128
|
+
Verify role outputs, delivery ownership and saved-run recovery before releasing the paired app. Implementation has no legacy fallback; profile v1 remains only for other workflows. Core's programmatic archive writes reviewed **complete specification replacements**; it does not merge partial OpenSpec delta snippets. Preserve unchanged requirements in the architect's output and inspect that behavior during the pilot.
|
|
126
129
|
|
|
127
130
|
See [Core runtime selection and recovery](core-runtime-updates.md) for the separate framework-update lifecycle and [the original evaluation](agent-runtime-framework-evaluation.md) for the architecture rationale.
|
|
128
131
|
|
|
129
132
|
The [implementation verification record](programmatic-agent-runtime-validation.md) lists the completed checks and outstanding release validation.
|
|
133
|
+
|
|
134
|
+
## Efficiency metrics
|
|
135
|
+
|
|
136
|
+
Core's additive runtime metrics v1 are passed from compact status to each saved run's optional `metrics` field. The contextual run panels and Jobs history render a collapsed **Usage and time** panel with reported cost, active execution/agent time, calls and token/cache totals, plus per-phase attempts, duration, provider calls and cost. All eight locales include the panel labels.
|
|
137
|
+
|
|
138
|
+
Older Core versions continue to work without the panel. The server validates numeric fields and known phase IDs, drops unsupported/malformed metrics and projects only the supported fields; it never forwards arbitrary transcripts from the metrics object. Missing billing is displayed as unavailable rather than zero. Cache tokens are already included in input tokens, and agent duration already includes native tool work. The panel does not estimate savings or measure implementation quality.
|
|
139
|
+
|
|
140
|
+
The paired `agent-runtime-efficiency` OpenSpec change in specrails-core documents verification ownership, efficient API tools and the measurement contract. Core exposes the same report through `runtime status` / `runtime-result`, so a fixed set of real tasks can be compared without a Desktop database migration.
|
package/package.json
CHANGED
|
@@ -266,6 +266,11 @@ function codexMcpOverrides(entry) {
|
|
|
266
266
|
const args = [
|
|
267
267
|
...c(`mcp_servers.specrails.command=${JSON.stringify(entry.command)}`),
|
|
268
268
|
...c(`mcp_servers.specrails.args=[${entry.args.map((a) => JSON.stringify(a)).join(', ')}]`),
|
|
269
|
+
// This invocation owns a server-minted capability. Specrails authorizes
|
|
270
|
+
// each action against its mission tier; Codex's tool-level prompt cannot
|
|
271
|
+
// distinguish facade reads from writes and fails under approvalPolicy=never.
|
|
272
|
+
// Keep this scoped to the internal bridge, never external MCP servers.
|
|
273
|
+
...c('mcp_servers.specrails.default_tools_approval_mode="approve"'),
|
|
269
274
|
];
|
|
270
275
|
for (const [k, v] of Object.entries(entry.env)) {
|
|
271
276
|
args.push(...c(`mcp_servers.specrails.env.${k}=${JSON.stringify(v)}`));
|
|
@@ -79,8 +79,8 @@ repository members, and runs coordinated AI coding pipelines over their selected
|
|
|
79
79
|
routing). Supported by Claude and Kimi; forced to null on Codex/Gemini rails.
|
|
80
80
|
- **Provider / engine** — claude, codex, gemini or kimi. A project installs one
|
|
81
81
|
or more; AI-spawning actions may pick any installed one. Claude and Kimi
|
|
82
|
-
support profiles and Freestyle; Contract Refine
|
|
83
|
-
|
|
82
|
+
support profiles and Freestyle; Contract Refine supports Claude and Codex
|
|
83
|
+
(read-only). SMASH and persistent interactive jobs remain Claude-only.
|
|
84
84
|
Cost is authoritative on Claude, estimated (~) on Codex/Gemini, and
|
|
85
85
|
unavailable when Kimi does not report it.
|
|
86
86
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RUNTIME_HOST_ENV_KEYS = void 0;
|
|
4
|
-
exports.selectAgentRuntime = selectAgentRuntime;
|
|
5
4
|
exports.runtimeChangeName = runtimeChangeName;
|
|
6
5
|
exports.runAgentRuntimeInvocation = runAgentRuntimeInvocation;
|
|
6
|
+
const agent_runtime_repositories_1 = require("./agent-runtime-repositories");
|
|
7
|
+
const node_util_1 = require("node:util");
|
|
8
|
+
const providers_1 = require("./providers");
|
|
7
9
|
const node_child_process_1 = require("node:child_process");
|
|
8
10
|
const node_crypto_1 = require("node:crypto");
|
|
9
11
|
const node_fs_1 = require("node:fs");
|
|
@@ -13,14 +15,6 @@ const agent_runtime_loader_1 = require("./agent-runtime-loader");
|
|
|
13
15
|
const agent_runtime_settings_1 = require("./agent-runtime-settings");
|
|
14
16
|
const core_node_runtime_1 = require("./core-node-runtime");
|
|
15
17
|
const win_spawn_1 = require("./util/win-spawn");
|
|
16
|
-
/** The stored admission wins over configuration changes for an existing run. */
|
|
17
|
-
function selectAgentRuntime(configPath, contextPath) {
|
|
18
|
-
if (contextPath && (0, node_fs_1.existsSync)((0, node_path_1.join)((0, node_path_1.dirname)(contextPath), 'agent-runtime-request.json')))
|
|
19
|
-
return true;
|
|
20
|
-
if (!(0, node_fs_1.existsSync)(configPath))
|
|
21
|
-
return false;
|
|
22
|
-
return (0, agent_runtime_settings_1.validateAgentRuntimeConfig)(JSON.parse((0, node_fs_1.readFileSync)(configPath, 'utf8'))).enabled;
|
|
23
|
-
}
|
|
24
18
|
function runtimeChangeName(runId) {
|
|
25
19
|
return 'runtime-' + (0, node_crypto_1.createHash)('sha256').update(runId).digest('hex').slice(0, 20);
|
|
26
20
|
}
|
|
@@ -59,8 +53,55 @@ async function runAgentRuntimeInvocation(options) {
|
|
|
59
53
|
if (!options.resume)
|
|
60
54
|
saveHostContext(options);
|
|
61
55
|
const args = [cli, options.resume ? 'resume' : 'run', '--context', options.contextPath];
|
|
62
|
-
if (!options.resume)
|
|
63
|
-
|
|
56
|
+
if (!options.resume) {
|
|
57
|
+
if (!Array.isArray(admittedContext.repositories) || !admittedContext.repositories.length || admittedContext.repositories.some(repo => !repo || typeof repo.id !== 'string' || !repo.id))
|
|
58
|
+
throw new Error('Core context is missing its repository scope');
|
|
59
|
+
const config = (0, agent_runtime_settings_1.loadRuntimeConfigFile)(options.configPath, options.defaultProvider);
|
|
60
|
+
config.rolePrompts = { ...(await (0, agent_runtime_loader_1.loadCoreAgentRuntime)()).rolePromptDefaults(), ...(0, agent_runtime_settings_1.loadRuntimeRolePrompts)() };
|
|
61
|
+
if (options.defaultProvider) {
|
|
62
|
+
const provider = config.providers.find(entry => entry.id === options.defaultProvider);
|
|
63
|
+
if (!provider)
|
|
64
|
+
throw new Error(`Selected runtime provider is not configured: ${options.defaultProvider}`);
|
|
65
|
+
for (const role of Object.values(config.agents)) {
|
|
66
|
+
if (role.provider !== provider.id) {
|
|
67
|
+
role.provider = provider.id;
|
|
68
|
+
role.model = options.selectedModel ?? (provider.kind === 'cli' ? (0, providers_1.getAdapter)(provider.cli).defaultModel() : undefined);
|
|
69
|
+
if (!role.model)
|
|
70
|
+
throw new Error(`Selected runtime provider requires a model: ${provider.id}`);
|
|
71
|
+
}
|
|
72
|
+
if (!role.model && provider.kind === 'cli')
|
|
73
|
+
role.model = (0, providers_1.getAdapter)(provider.cli).defaultModel();
|
|
74
|
+
}
|
|
75
|
+
if (options.selectedModel)
|
|
76
|
+
config.agents.developer.model = options.selectedModel;
|
|
77
|
+
}
|
|
78
|
+
const selected = new Set(admittedContext.repositories.map(repo => repo.id));
|
|
79
|
+
// Project settings cover every repository; a ticket may select only a subset.
|
|
80
|
+
// Freeze that subset for admission without changing the project settings.
|
|
81
|
+
config.verification = config.verification.filter(command => selected.has(command.repositoryId));
|
|
82
|
+
const scopedPath = (0, node_path_1.join)((0, node_path_1.dirname)(options.contextPath), 'desktop-runtime-config.json');
|
|
83
|
+
const serialized = JSON.stringify(config, null, 2) + '\n';
|
|
84
|
+
try {
|
|
85
|
+
(0, node_fs_1.writeFileSync)(scopedPath, serialized, { flag: 'wx', mode: 0o600 });
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code !== 'EEXIST')
|
|
89
|
+
throw error;
|
|
90
|
+
if ((0, node_fs_1.readFileSync)(scopedPath, 'utf8') !== serialized)
|
|
91
|
+
throw new Error('Desktop runtime configuration changed; start a new run');
|
|
92
|
+
}
|
|
93
|
+
args.push('--config', scopedPath, '--change', options.change);
|
|
94
|
+
}
|
|
95
|
+
const frozenPath = (0, node_path_1.join)((0, node_path_1.dirname)(options.contextPath), 'desktop-runtime-config.json');
|
|
96
|
+
if ((0, node_fs_1.existsSync)(frozenPath)) {
|
|
97
|
+
const frozen = JSON.parse((0, node_fs_1.readFileSync)(frozenPath, 'utf8'));
|
|
98
|
+
for (const [role, assignment] of Object.entries(frozen.agents)) {
|
|
99
|
+
try {
|
|
100
|
+
options.onLine?.(`[runtime] ${role}: ${assignment.provider}/${assignment.model ?? 'provider default'}\n`);
|
|
101
|
+
}
|
|
102
|
+
catch { /* Log observers cannot prevent execution. */ }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
64
105
|
for (const [flag, values] of [['approve', options.approve], ['recover', options.recover], ['invalidate', options.invalidate]]) {
|
|
65
106
|
if (values?.length)
|
|
66
107
|
args.push('--' + flag, values.join(','));
|
|
@@ -113,7 +154,12 @@ async function runAgentRuntimeInvocation(options) {
|
|
|
113
154
|
invalidProtocol = true;
|
|
114
155
|
return;
|
|
115
156
|
}
|
|
116
|
-
|
|
157
|
+
if (event.type === 'agent-event' && event.event && typeof event.event === 'object' && event.event.kind === 'tool-start' && (admittedContext.repositories?.length ?? 0) > 1) {
|
|
158
|
+
const repositories = (0, agent_runtime_repositories_1.toolRepositories)(event.event, admittedContext.repositories, admittedContext.artifactRoot ?? options.cwd);
|
|
159
|
+
if (repositories.length)
|
|
160
|
+
event.repositories = repositories.map(repo => ({ id: repo.id, name: repo.name || repo.id }));
|
|
161
|
+
}
|
|
162
|
+
observe(() => options.onRawLine?.(event.repositories ? JSON.stringify(event) : line));
|
|
117
163
|
if (event.type === 'runtime-result') {
|
|
118
164
|
if (result)
|
|
119
165
|
invalidProtocol = true;
|
|
@@ -133,11 +179,13 @@ async function runAgentRuntimeInvocation(options) {
|
|
|
133
179
|
}
|
|
134
180
|
else if (payload?.kind === 'tool-start' && typeof payload.tool === 'string') {
|
|
135
181
|
// Live tool activity keeps a long developer turn from looking hung.
|
|
136
|
-
|
|
182
|
+
const repositories = event.repositories;
|
|
183
|
+
const scope = repositories?.length ? ` [${repositories.map(repo => repo.name).join(' + ')}]` : '';
|
|
184
|
+
observe(() => options.onLine?.(`[${role}]${scope} ${payload.tool}${payload.detail ? ' ' + payload.detail : ''}\n`));
|
|
137
185
|
}
|
|
138
186
|
}
|
|
139
187
|
else if (event.type === 'verification-output' && typeof event.text === 'string') {
|
|
140
|
-
observe(() => options.onLine?.(String(event.text)));
|
|
188
|
+
observe(() => options.onLine?.((0, node_util_1.stripVTControlCharacters)(String(event.text))));
|
|
141
189
|
}
|
|
142
190
|
else if (event.type === 'span') {
|
|
143
191
|
// Trace spans are telemetry; the raw line is already recorded for diagnostics.
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isRuntimeContinuationActive = isRuntimeContinuationActive;
|
|
4
|
+
exports.activeRuntimeContinuationIds = activeRuntimeContinuationIds;
|
|
5
|
+
exports.cancelRuntimeContinuation = cancelRuntimeContinuation;
|
|
3
6
|
exports.shutdownAgentRuntimeControls = shutdownAgentRuntimeControls;
|
|
4
7
|
exports.registerAgentRuntimeControlRoutes = registerAgentRuntimeControlRoutes;
|
|
8
|
+
const agent_runtime_paths_1 = require("./agent-runtime-paths");
|
|
5
9
|
const agent_runtime_controls_1 = require("./agent-runtime-controls");
|
|
6
10
|
const controllers = new WeakMap();
|
|
11
|
+
function isRuntimeContinuationActive(context, runId) { return controllers.get(context)?.isActive(runId) ?? false; }
|
|
12
|
+
function activeRuntimeContinuationIds(context) { return controllers.get(context)?.activeRunIds() ?? []; }
|
|
13
|
+
function cancelRuntimeContinuation(context, runId) {
|
|
14
|
+
const controller = controllers.get(context);
|
|
15
|
+
if (!controller?.isActive(runId))
|
|
16
|
+
return false;
|
|
17
|
+
controller.cancel(runId);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
7
20
|
function shutdownAgentRuntimeControls(context) { controllers.get(context)?.shutdown(); }
|
|
8
21
|
function registerAgentRuntimeControlRoutes({ router, ctx }) {
|
|
9
22
|
function controls(request) {
|
|
@@ -17,12 +30,32 @@ function registerAgentRuntimeControlRoutes({ router, ctx }) {
|
|
|
17
30
|
}
|
|
18
31
|
router.get('/:projectId/agent-runtime/runs', async (req, res) => {
|
|
19
32
|
try {
|
|
33
|
+
if (req.query.railIndex !== undefined) {
|
|
34
|
+
const railIndex = Number(req.query.railIndex);
|
|
35
|
+
if (!Number.isSafeInteger(railIndex) || railIndex < 0) {
|
|
36
|
+
res.status(400).json({ error: 'invalid_rail_index' });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const context = ctx(req);
|
|
40
|
+
const latest = context.db.prepare('SELECT id FROM loop_runs WHERE rail_index = ? ORDER BY started_at DESC, rowid DESC LIMIT 1').get(railIndex);
|
|
41
|
+
res.json({ runs: latest && (0, agent_runtime_paths_1.hasAgentRuntimeRequest)(context.project, latest.id) ? [await controls(req).summary(latest.id)] : [] });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
20
44
|
res.json({ runs: await controls(req).list() });
|
|
21
45
|
}
|
|
22
46
|
catch {
|
|
23
47
|
res.status(500).json({ error: 'runtime_status_failed', message: 'Could not read runtime executions' });
|
|
24
48
|
}
|
|
25
49
|
});
|
|
50
|
+
router.get('/:projectId/agent-runtime/runs/:runId', async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const context = ctx(req), runId = String(req.params.runId);
|
|
53
|
+
res.json({ runs: (0, agent_runtime_paths_1.hasAgentRuntimeRequest)(context.project, runId) ? [await controls(req).summary(runId)] : [] });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
res.status(500).json({ error: 'runtime_status_failed' });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
26
59
|
router.post('/:projectId/agent-runtime/runs/:runId/resume', async (req, res) => {
|
|
27
60
|
try {
|
|
28
61
|
await controls(req).resume(String(req.params.runId), (0, agent_runtime_controls_1.validateRuntimeResumeInput)(req.body));
|
|
@@ -32,6 +65,15 @@ function registerAgentRuntimeControlRoutes({ router, ctx }) {
|
|
|
32
65
|
res.status(error instanceof agent_runtime_controls_1.RuntimeControlError ? error.statusCode : 500).json({ error: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.code : 'runtime_resume_failed', message: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.message : 'Could not resume runtime execution' });
|
|
33
66
|
}
|
|
34
67
|
});
|
|
68
|
+
router.post('/:projectId/agent-runtime/runs/:runId/settle', async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
await controls(req).settle(String(req.params.runId));
|
|
71
|
+
res.json({ settled: true });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
res.status(error instanceof agent_runtime_controls_1.RuntimeControlError ? error.statusCode : 500).json({ message: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.message : 'Could not prepare delivery' });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
35
77
|
router.post('/:projectId/agent-runtime/runs/:runId/cancel', (req, res) => {
|
|
36
78
|
try {
|
|
37
79
|
controls(req).cancel(String(req.params.runId));
|
|
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.AgentRuntimeControls = exports.RuntimeControlError = void 0;
|
|
7
7
|
exports.validateRuntimeResumeInput = validateRuntimeResumeInput;
|
|
8
8
|
exports.readAgentRuntimeStatus = readAgentRuntimeStatus;
|
|
9
|
+
const agent_runtime_settlement_1 = require("./agent-runtime-settlement");
|
|
10
|
+
const agent_runtime_metrics_1 = require("./agent-runtime-metrics");
|
|
9
11
|
const node_child_process_1 = require("node:child_process");
|
|
10
12
|
const node_crypto_1 = require("node:crypto");
|
|
11
13
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -70,11 +72,13 @@ async function readAgentRuntimeStatus(contextPath, cwd, env) {
|
|
|
70
72
|
class AgentRuntimeControls {
|
|
71
73
|
ctx;
|
|
72
74
|
dependencies;
|
|
75
|
+
isActive(runId) { return this.active.has(runId); }
|
|
76
|
+
activeRunIds() { return [...this.active.keys()]; }
|
|
73
77
|
active = new Map();
|
|
74
78
|
errors = new Map();
|
|
75
79
|
statusCache = new Map();
|
|
76
80
|
disposed = false;
|
|
77
|
-
constructor(ctx, dependencies = { status: readAgentRuntimeStatus, execute: agent_runtime_bridge_1.runAgentRuntimeInvocation, kill: win_spawn_1.treeKillSafe }) {
|
|
81
|
+
constructor(ctx, dependencies = { status: readAgentRuntimeStatus, execute: agent_runtime_bridge_1.runAgentRuntimeInvocation, kill: win_spawn_1.treeKillSafe, settle: agent_runtime_settlement_1.settleRuntimeContinuation }) {
|
|
78
82
|
this.ctx = ctx;
|
|
79
83
|
this.dependencies = dependencies;
|
|
80
84
|
}
|
|
@@ -149,6 +153,8 @@ class AgentRuntimeControls {
|
|
|
149
153
|
return { runId, status: state.status === 'running' && !active ? 'interrupted' : state.status, nextStep: state.nextStep,
|
|
150
154
|
updatedAt: state.updatedAt, error: this.errors.get(runId) ?? state.error, pendingApproval: state.pendingApproval,
|
|
151
155
|
traceId: state.traceId, pendingQuestion: openQuestion(state),
|
|
156
|
+
metrics: (0, agent_runtime_metrics_1.readRuntimeEfficiency)(state.metrics),
|
|
157
|
+
canSettle: !active && state.status === 'succeeded' && this.ctx.db.prepare('SELECT status FROM jobs WHERE id = ?').get(runId)?.status !== 'completed',
|
|
152
158
|
recoverableSteps, active, canCancel: this.active.has(runId), canResume: !active && parent?.status === 'completed' && state.status !== 'succeeded' };
|
|
153
159
|
}
|
|
154
160
|
catch (error) {
|
|
@@ -161,6 +167,8 @@ class AgentRuntimeControls {
|
|
|
161
167
|
const parent = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
|
|
162
168
|
if (!parent || parent.status !== 'completed' || this.active.has(runId))
|
|
163
169
|
throw new RuntimeControlError(409, 'runtime_run_active', 'Wait for the original Desktop execution to settle before resuming');
|
|
170
|
+
if (parent.rail_index != null && [...(this.ctx.railLoopRuns?.values() ?? []), ...(this.ctx.railJobs?.values() ?? [])].some(meta => meta.railIndex === parent.rail_index))
|
|
171
|
+
throw new RuntimeControlError(409, 'runtime_rail_active', 'Wait for the implementation card to finish its active job before resuming');
|
|
164
172
|
const context = this.context(runId);
|
|
165
173
|
// Reserve before awaiting status so concurrent resume requests cannot race.
|
|
166
174
|
const active = { cancelled: false };
|
|
@@ -172,6 +180,8 @@ class AgentRuntimeControls {
|
|
|
172
180
|
const state = await this.dependencies.status(context.file, context.cwd, context.env);
|
|
173
181
|
if (!state || state.runId !== runId || state.status === 'succeeded')
|
|
174
182
|
throw new RuntimeControlError(409, 'runtime_not_resumable', 'This execution has no resumable workflow');
|
|
183
|
+
if (parent.rail_index != null && [...(this.ctx.railLoopRuns?.values() ?? []), ...(this.ctx.railJobs?.values() ?? [])].some(meta => meta.railIndex === parent.rail_index))
|
|
184
|
+
throw new RuntimeControlError(409, 'runtime_rail_active', 'The implementation card became active while checking the saved execution');
|
|
175
185
|
// A pending question resumes the architect only with the operator's answer.
|
|
176
186
|
if (openQuestion(state) && !input.answer)
|
|
177
187
|
throw new RuntimeControlError(400, 'answer_required', 'This execution is waiting for an answer to the architect\'s question');
|
|
@@ -206,7 +216,20 @@ class AgentRuntimeControls {
|
|
|
206
216
|
(0, db_1.appendEvent)(this.ctx.db, runId, ++sequence, { event_type: 'log', source, payload: JSON.stringify({ line: line.replace(/\n$/, '') }) });
|
|
207
217
|
}
|
|
208
218
|
catch { /* events table best-effort */ }
|
|
219
|
+
try {
|
|
220
|
+
this.ctx.broadcast?.({ type: 'log', source, line, timestamp: new Date().toISOString(), processId: runId });
|
|
221
|
+
}
|
|
222
|
+
catch { /* persisted log remains authoritative */ }
|
|
223
|
+
};
|
|
224
|
+
if (parent.rail_index != null)
|
|
225
|
+
this.ctx.railLoopRuns?.set(runId, { railIndex: parent.rail_index, ticketIds, requiresTerminalIntent: true });
|
|
226
|
+
const publishActivity = (running) => {
|
|
227
|
+
try {
|
|
228
|
+
this.ctx.broadcast?.({ type: 'runtime.continuation', projectId: this.ctx.project.id, jobId: runId, railIndex: parent.rail_index, active: running });
|
|
229
|
+
}
|
|
230
|
+
catch { /* durable log remains available */ }
|
|
209
231
|
};
|
|
232
|
+
publishActivity(true);
|
|
210
233
|
const requested = [...(input.approve?.length ? ['approve ' + input.approve.join(',')] : []), ...(input.recover?.length ? ['recover ' + input.recover.join(',')] : []), ...(input.invalidate?.length ? ['invalidate ' + input.invalidate.join(',')] : []), ...(input.answer ? ['answered question'] : [])];
|
|
211
234
|
logLine(`[runtime] continuation started from phase ${state.nextStep ?? 'end'}${requested.length ? ' (' + requested.join('; ') + ')' : ''}; the original worktree and frozen scope are reused`);
|
|
212
235
|
void this.dependencies.execute({
|
|
@@ -227,6 +250,10 @@ class AgentRuntimeControls {
|
|
|
227
250
|
(0, db_1.appendEvent)(this.ctx.db, runId, ++sequence, { event_type: eventType, source: 'stdout', payload: line });
|
|
228
251
|
(0, loop_runs_store_1.updateLoopStepActivityCheckpoint)(this.ctx.db, runId, stepKey, undefined, Date.now());
|
|
229
252
|
})();
|
|
253
|
+
try {
|
|
254
|
+
this.ctx.broadcast?.({ type: 'event', jobId: runId, event_type: eventType, source: 'stdout', payload: line, seq: sequence, timestamp: new Date().toISOString() });
|
|
255
|
+
}
|
|
256
|
+
catch { /* persisted event remains authoritative */ }
|
|
230
257
|
},
|
|
231
258
|
onSpawn: (child) => {
|
|
232
259
|
active.child = child;
|
|
@@ -235,7 +262,7 @@ class AgentRuntimeControls {
|
|
|
235
262
|
else if (active.cancelled)
|
|
236
263
|
this.cancel(runId);
|
|
237
264
|
},
|
|
238
|
-
}).then((result) => {
|
|
265
|
+
}).then(async (result) => {
|
|
239
266
|
if (this.disposed)
|
|
240
267
|
return;
|
|
241
268
|
logLine(result.failed ? `[runtime] continuation ended: ${result.errorText ?? 'failed'}` : '[runtime] continuation completed', result.failed ? 'stderr' : 'stdout');
|
|
@@ -243,14 +270,51 @@ class AgentRuntimeControls {
|
|
|
243
270
|
(0, loop_run_manager_1.recoverOrphanLoopStepAccounting)(this.ctx.db, new Date().toISOString(), runId);
|
|
244
271
|
if (result.errorText)
|
|
245
272
|
this.errors.set(runId, result.errorText);
|
|
246
|
-
|
|
247
|
-
|
|
273
|
+
if (!result.failed && !active.cancelled)
|
|
274
|
+
await this.dependencies.settle?.({ db: this.ctx.db, projectId: this.ctx.project.id, runId, contextPath: context.file, cwd: context.cwd, env: context.env, broadcast: this.ctx.broadcast });
|
|
275
|
+
}).catch(() => {
|
|
276
|
+
if (this.disposed)
|
|
277
|
+
return;
|
|
278
|
+
const message = 'Runtime continuation failed. Inspect the saved phase state before retrying.';
|
|
279
|
+
this.errors.set(runId, message);
|
|
280
|
+
logLine(`[runtime] ${message}`, 'stderr');
|
|
281
|
+
})
|
|
282
|
+
.finally(() => {
|
|
283
|
+
clearTimeout(active.forceKillTimer);
|
|
284
|
+
this.active.delete(runId);
|
|
285
|
+
this.ctx.railLoopRuns?.delete(runId);
|
|
286
|
+
this.statusCache.delete(runId);
|
|
287
|
+
publishActivity(false);
|
|
288
|
+
});
|
|
248
289
|
}
|
|
249
290
|
catch (error) {
|
|
250
291
|
this.active.delete(runId);
|
|
292
|
+
this.ctx.railLoopRuns?.delete(runId);
|
|
293
|
+
try {
|
|
294
|
+
this.ctx.broadcast?.({ type: 'runtime.continuation', projectId: this.ctx.project.id, jobId: runId, railIndex: parent.rail_index, active: false });
|
|
295
|
+
}
|
|
296
|
+
catch { /* admission failed */ }
|
|
251
297
|
throw error;
|
|
252
298
|
}
|
|
253
299
|
}
|
|
300
|
+
async settle(runId) {
|
|
301
|
+
if (this.active.has(runId) || (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId)?.status !== 'completed')
|
|
302
|
+
throw new RuntimeControlError(409, 'runtime_run_active', 'Wait for the execution to settle');
|
|
303
|
+
const context = this.context(runId);
|
|
304
|
+
this.active.set(runId, { cancelled: false });
|
|
305
|
+
try {
|
|
306
|
+
await (this.dependencies.settle ?? agent_runtime_settlement_1.settleRuntimeContinuation)({ db: this.ctx.db, projectId: this.ctx.project.id, runId, contextPath: context.file, cwd: context.cwd, env: context.env, broadcast: this.ctx.broadcast });
|
|
307
|
+
this.errors.delete(runId);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
throw new RuntimeControlError(409, 'runtime_settlement_blocked', error instanceof Error ? error.message : 'Could not prepare the recovered delivery');
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
this.active.delete(runId);
|
|
314
|
+
this.statusCache.delete(runId);
|
|
315
|
+
this.ctx.broadcast?.({ type: 'runtime.continuation', projectId: this.ctx.project.id, jobId: runId, railIndex: (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId)?.rail_index ?? null, active: false });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
254
318
|
cancel(runId) {
|
|
255
319
|
const active = this.active.get(runId);
|
|
256
320
|
if (!active)
|
|
@@ -68,6 +68,12 @@ async function loadCoreAgentRuntime() {
|
|
|
68
68
|
}
|
|
69
69
|
return {
|
|
70
70
|
RUNTIME_API_VERSION: 1,
|
|
71
|
+
rolePromptDefaults() {
|
|
72
|
+
const result = JSON.parse(invoke(['prompts']));
|
|
73
|
+
if (result.type !== 'runtime-role-prompts' || !['architect', 'developer', 'reviewer'].every(role => typeof result.defaults?.[role] === 'string' && result.defaults[role].trim()))
|
|
74
|
+
throw new Error('Core role prompt catalog is unavailable. Update the paired Core bundle.');
|
|
75
|
+
return result.defaults;
|
|
76
|
+
},
|
|
71
77
|
validateRuntimeConfig(input) {
|
|
72
78
|
const result = JSON.parse(invoke(['validate', '--stdin'], JSON.stringify(input)));
|
|
73
79
|
if (result.type !== 'runtime-config-valid')
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readRuntimeEfficiency = readRuntimeEfficiency;
|
|
4
|
+
const COUNTS = ['attempts', 'measuredAttempts', 'providerCalls', 'toolCalls', 'inputTokens', 'outputTokens', 'uncachedInputTokens', 'cacheReadInputTokens', 'cacheWriteInputTokens'];
|
|
5
|
+
const KEYS = [...COUNTS, 'durationMs', 'agentDurationMs', 'costUsd'];
|
|
6
|
+
const PHASES = ['architect', 'developer', 'verify', 'reviewer', 'archive'];
|
|
7
|
+
function object(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null; }
|
|
8
|
+
function counters(value) {
|
|
9
|
+
const input = object(value);
|
|
10
|
+
if (!input)
|
|
11
|
+
return null;
|
|
12
|
+
const output = {};
|
|
13
|
+
for (const key of KEYS) {
|
|
14
|
+
const number = input[key];
|
|
15
|
+
if (number === null && key !== 'attempts' && key !== 'measuredAttempts') {
|
|
16
|
+
output[key] = null;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (typeof number !== 'number' || !Number.isFinite(number) || number < 0 || (COUNTS.includes(key) && !Number.isSafeInteger(number)))
|
|
20
|
+
return null;
|
|
21
|
+
output[key] = number;
|
|
22
|
+
}
|
|
23
|
+
if (output.measuredAttempts > output.attempts)
|
|
24
|
+
return null;
|
|
25
|
+
return output;
|
|
26
|
+
}
|
|
27
|
+
/** Older Core versions omit metrics; malformed/unknown versions do not break run controls. */
|
|
28
|
+
function readRuntimeEfficiency(value) {
|
|
29
|
+
const input = object(value);
|
|
30
|
+
if (input?.schemaVersion !== 1 || !Array.isArray(input.phases) || input.phases.length > PHASES.length)
|
|
31
|
+
return undefined;
|
|
32
|
+
const total = counters(input.total);
|
|
33
|
+
if (!total)
|
|
34
|
+
return undefined;
|
|
35
|
+
const phases = [];
|
|
36
|
+
for (const raw of input.phases) {
|
|
37
|
+
const phase = object(raw), count = counters(phase);
|
|
38
|
+
if (!phase || !count || typeof phase.stepId !== 'string' || !PHASES.includes(phase.stepId) || phases.some(item => item.stepId === phase.stepId))
|
|
39
|
+
return undefined;
|
|
40
|
+
for (const key of ['providers', 'models'])
|
|
41
|
+
if (!Array.isArray(phase[key]) || phase[key].length > 50 || !phase[key].every((item) => typeof item === 'string' && item.length > 0 && item.length <= 256))
|
|
42
|
+
return undefined;
|
|
43
|
+
phases.push({ ...count, stepId: phase.stepId, providers: [...phase.providers], models: [...phase.models] });
|
|
44
|
+
}
|
|
45
|
+
return { schemaVersion: 1, total, phases };
|
|
46
|
+
}
|