specrails-desktop 2.55.1 → 2.56.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-DmJcQpNe.js → ActivityFeedPage-DdMNmVBk.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-NxBHrU-V.js → AgentBrowserCapture-CeerUf--.js} +1 -1
- package/client/dist/assets/{AgentModeAnalyticsPane-CGAKTJfU.js → AgentModeAnalyticsPane-BjGb-8rh.js} +2 -2
- package/client/dist/assets/{AgentModeCodePane-DNSs5_oe.js → AgentModeCodePane-8XRkQEo_.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-DLoVYDnG.js → AgentModeJobsPane-BN3dgJOA.js} +1 -1
- package/client/dist/assets/{AgentsPage-CAzMn9Gg.js → AgentsPage-DfHWgP_x.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-C06KT4Y0.js → AnalyticsPage-CyfhYiBn.js} +1 -1
- package/client/dist/assets/{CodePage-aX1mxGyT.js → CodePage-C2Nf-Q5W.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-BtscfiQD.js → DesktopAnalyticsPage-tc0p4jJW.js} +1 -1
- package/client/dist/assets/{DocsDialog-CNOcXhp-.js → DocsDialog-Br3bczCp.js} +1 -1
- package/client/dist/assets/{DocsPage-C2BugxaI.js → DocsPage-BcDr2cBm.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-CCncBqNu.js → InteractiveJobComposer-DFPpdg2D.js} +1 -1
- package/client/dist/assets/{JobDetailModal-Cn_k3-RM.js → JobDetailModal-BdOv-l-M.js} +1 -1
- package/client/dist/assets/{JobDetailPage-BogJFBsk.js → JobDetailPage-vBbNeRuz.js} +1 -1
- package/client/dist/assets/{JobsPage-DL4E8ZyU.js → JobsPage-CJCtPhIz.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-9L6URQLY.js → LoopBuilderPage-BemvUOxy.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-CAPqTIgL.js → LoopPreviewModal-C6hNAeYN.js} +1 -1
- package/client/dist/assets/{LoopsPage-aU8dYw7L.js → LoopsPage-BZr0quvh.js} +1 -1
- package/client/dist/assets/{PluginsPage-D_tsW9B5.js → PluginsPage-DOpTV75p.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-_ZbNuspP.js → ProjectSettingsDialog-Dkwrn0z1.js} +1 -1
- package/client/dist/assets/{ReviewPacketPage-CG-Dv0El.js → ReviewPacketPage-DcOq-UTA.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-C19H2b2o.js → TemplatePreviewModal-Cejcquah.js} +1 -1
- package/client/dist/assets/{index-BcligJ3-.js → index-Cxb288Rj.js} +8 -8
- package/client/dist/index.html +1 -1
- package/docs/internals/adding-a-provider.md +7 -0
- package/docs/internals/core-runtime-updates.md +5 -0
- package/docs/internals/programmatic-agent-runtime.md +66 -0
- package/docs/internals/source-map.md +3 -1
- package/package.json +1 -1
- package/server/dist/mcp/guide.js +16 -2
- package/server/dist/mcp/tools/catalog.js +2 -0
- package/server/dist/mcp/tools/jobs.js +22 -4
- package/server/dist/mcp/tools/recovery.js +35 -0
- package/server/dist/mcp/tools/support.js +4 -1
- package/server/dist/modules/accounting/runtime/pricing.js +9 -2
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls-router.js +16 -0
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls.js +93 -1
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-recovery.js +53 -0
- package/server/dist/modules/missions/runtime/agent-failure-briefing.js +4 -2
- package/server/dist/modules/missions/runtime/agent-operator-prompt.js +58 -5
- package/server/dist/modules/missions/runtime/mission-run-notify.js +4 -1
- package/server/dist/providers/claude-adapter.js +3 -2
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-Cxb288Rj.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">
|
|
@@ -397,3 +397,10 @@ bundled `local-runner` process, which speaks claude-shaped stream-json so the
|
|
|
397
397
|
spawn contract is unchanged. If you add another endpoint-backed provider, reuse
|
|
398
398
|
that adapter factory + the runner rather than teaching managers a new
|
|
399
399
|
transport. See `docs/internals/local-agent-runner.md`.
|
|
400
|
+
|
|
401
|
+
### Claude Opus generation
|
|
402
|
+
|
|
403
|
+
The `opus` catalog value selects Claude Opus 5.5 (`claude-opus-5-5`). Keep the
|
|
404
|
+
Claude adapter pin, selector labels, Core CLI executor and cost estimates aligned
|
|
405
|
+
when advancing this alias. Concrete historical model IDs in usage events retain
|
|
406
|
+
their existing estimates; provider-reported cost remains authoritative.
|
|
@@ -12,6 +12,11 @@ framework is rejected rather than silently replacing an update.
|
|
|
12
12
|
|
|
13
13
|
## Persistence and publication
|
|
14
14
|
|
|
15
|
+
The release bundle pins Core 5.6.0 in `desktop-release.yml` and
|
|
16
|
+
`scripts/assemble-bundled-core.lock.json`. This includes scoped runtime recovery
|
|
17
|
+
and the Opus 5.5 alias. Update both pins together and check compatibility against
|
|
18
|
+
the staged published package; retained runs still use their original runtime.
|
|
19
|
+
|
|
15
20
|
Desktop updates retain the complete npm installation, including dependencies,
|
|
16
21
|
under `~/.specrails/core/<version>/`. The registry home override applies to this
|
|
17
22
|
directory. Only the managed package corresponding to the active framework is
|
|
@@ -138,3 +138,69 @@ Core's additive runtime metrics v1 are passed from compact status to each saved
|
|
|
138
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
139
|
|
|
140
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.
|
|
141
|
+
|
|
142
|
+
## Diagnose before retrying
|
|
143
|
+
|
|
144
|
+
`GET /agent-runtime/runs/:runId/diagnosis` (MCP: `specrails_jobs runtime_diagnose`)
|
|
145
|
+
returns the original scope, historical completed steps, up to eight recent
|
|
146
|
+
failed/interrupted attempts, verification and acceptance reasons, and a recovery
|
|
147
|
+
recommendation. It performs no provider calls or mutations. Repeated matching
|
|
148
|
+
step/error pairs recommend repairing the precondition before retrying. A missing
|
|
149
|
+
history on an older retained Core is unknown, not zero failures.
|
|
150
|
+
|
|
151
|
+
MCP `runtime_evidence` accepts `evidenceId`, `section`, `sourceId`, `cursor` and
|
|
152
|
+
`limit` to inspect the actual evidence beyond its index. `canResume` only means
|
|
153
|
+
resume is available. Changed receipts can still cause verification and review
|
|
154
|
+
to repeat; no zero-cost promise is made. A succeeded run awaiting settlement
|
|
155
|
+
should use `runtime_settle`, preserving the existing implementation.
|
|
156
|
+
|
|
157
|
+
Paired Core now preserves OpenSpec's output when archive exits successfully
|
|
158
|
+
without creating its destination, and distinguishes that from multiple matching
|
|
159
|
+
destinations. Retrying a failed archive keeps valid verification/review receipts;
|
|
160
|
+
changed candidate files or environment still require fresh evidence. These Core
|
|
161
|
+
fixes require a newly bundled/released runtime; retained original runtimes are
|
|
162
|
+
not silently replaced or migrated. Diagnosis does not add arbitrary file-write
|
|
163
|
+
access: repairs without a supported scoped tool are reported as concrete manual
|
|
164
|
+
steps instead of being disguised as a reason to relaunch.
|
|
165
|
+
|
|
166
|
+
## Repair the original worktree
|
|
167
|
+
|
|
168
|
+
`specrails_recovery` calls `POST /agent-runtime/runs/:runId/recovery` with a
|
|
169
|
+
strict action-specific request. It never constructs a new context/worktree.
|
|
170
|
+
|
|
171
|
+
| Action | Inputs and permission | Result |
|
|
172
|
+
| --- | --- | --- |
|
|
173
|
+
| inspect | read | Original repository IDs, registered check definitions, recent attempts |
|
|
174
|
+
| list_files / read_file / diff | repositoryId + relative path; read | Bounded directory/line/diff output; read_file includes SHA-256 |
|
|
175
|
+
| history | optional offset; read | Latest attempts first, pages of 20 with nextOffset |
|
|
176
|
+
| patch | repositoryId, path, expectedHash, oldText, newText, operationId UUID, reason; write | One unique replacement in an existing file, atomic publication |
|
|
177
|
+
| check | kind openspec or verification; saved checkId for verification; operationId + reason; destructive | Real validation evidence, bounded output and durable outcome |
|
|
178
|
+
|
|
179
|
+
Patches are limited to 16 KiB fragments and files readable within 128 KiB.
|
|
180
|
+
Runtime/provider metadata, secret paths, frozen OpenSpec changes/config/archives,
|
|
181
|
+
agent instructions, traversal, symlinks and platform aliases are rejected. New
|
|
182
|
+
files, deletions and arbitrary commands are not supported. Explicit repository IDs
|
|
183
|
+
prevent cross-project resolution. Verification runs one registered command with a
|
|
184
|
+
45-second deadline; full acceptance remains the normal resume/settlement path.
|
|
185
|
+
|
|
186
|
+
Desktop blocks admission while project executions are active and reserves the
|
|
187
|
+
run/rail. Core takes the same cross-process lease as Resume. An interrupted write
|
|
188
|
+
requires `acknowledgeInterrupted` after inspecting partial changes. Completed or
|
|
189
|
+
archived runs cannot be patched or checked through recovery. Inspection remains
|
|
190
|
+
available. This is an application boundary, not an OS sandbox against unrelated
|
|
191
|
+
processes modifying the worktree concurrently.
|
|
192
|
+
|
|
193
|
+
Core stores at most 100 attempts in `recovery-history.json` beside the pipeline
|
|
194
|
+
state, separate from checkpoints. Each mutation has a write-ahead operation ID,
|
|
195
|
+
request fingerprint, cause, before/after hashes where relevant and final outcome.
|
|
196
|
+
Reuse the exact request/ID after transport uncertainty. A pending patch whose
|
|
197
|
+
after-hash is present is reconciled; other pending operations become interrupted
|
|
198
|
+
and are not rerun. Repeating a failed check on unchanged candidate code requires
|
|
199
|
+
a documented `changedPrecondition`; this records the operator's explanation, not
|
|
200
|
+
an independent proof that an external prerequisite changed.
|
|
201
|
+
|
|
202
|
+
Resume still enforces verification, review and acceptance. Scoped verification
|
|
203
|
+
uses Core's existing evidence/receipt store and cannot mark workflow phases done.
|
|
204
|
+
OpenSpec checks validate real files without copying or archiving them. Old retained
|
|
205
|
+
Core packages lacking `scopedRecovery` are not upgraded/migrated behind the run's
|
|
206
|
+
back: the tool returns a manual-repair limitation rather than recommending Relaunch.
|
|
@@ -5,7 +5,7 @@ architectural dependency declaration. Search a heading or filename and read only
|
|
|
5
5
|
the relevant source and tests. Feature prefixes group the legacy server files;
|
|
6
6
|
new bounded modules live under `server/modules/`.
|
|
7
7
|
|
|
8
|
-
Includes
|
|
8
|
+
Includes 942 source/build files. Nearby tests are linked where names
|
|
9
9
|
match; integration suites may live elsewhere. Use `rg` to find other consumers.
|
|
10
10
|
|
|
11
11
|
## cli
|
|
@@ -979,6 +979,7 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
|
|
|
979
979
|
- [plugins.ts](../../server/mcp/tools/plugins.ts) · [test](../../server/mcp/tools/plugins.test.ts)
|
|
980
980
|
- [projects.ts](../../server/mcp/tools/projects.ts)
|
|
981
981
|
- [rails.ts](../../server/mcp/tools/rails.ts)
|
|
982
|
+
- [recovery.ts](../../server/mcp/tools/recovery.ts) · [test](../../server/mcp/tools/recovery.test.ts)
|
|
982
983
|
- [setup.ts](../../server/mcp/tools/setup.ts) · [test](../../server/mcp/tools/setup.test.ts)
|
|
983
984
|
- [specs.ts](../../server/mcp/tools/specs.ts) · [test](../../server/mcp/tools/specs.test.ts)
|
|
984
985
|
- [support.ts](../../server/mcp/tools/support.ts)
|
|
@@ -1031,6 +1032,7 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
|
|
|
1031
1032
|
- [agent-runtime-metrics.ts](../../server/modules/agent-runtime/runtime/agent-runtime-metrics.ts) · [test](../../server/modules/agent-runtime/runtime/agent-runtime-metrics.test.ts)
|
|
1032
1033
|
- [agent-runtime-package.ts](../../server/modules/agent-runtime/runtime/agent-runtime-package.ts) · [test](../../server/modules/agent-runtime/runtime/agent-runtime-package.test.ts)
|
|
1033
1034
|
- [agent-runtime-paths.ts](../../server/modules/agent-runtime/runtime/agent-runtime-paths.ts)
|
|
1035
|
+
- [agent-runtime-recovery.ts](../../server/modules/agent-runtime/runtime/agent-runtime-recovery.ts) · [test](../../server/modules/agent-runtime/runtime/agent-runtime-recovery.test.ts)
|
|
1034
1036
|
- [agent-runtime-repositories.ts](../../server/modules/agent-runtime/runtime/agent-runtime-repositories.ts) · [test](../../server/modules/agent-runtime/runtime/agent-runtime-repositories.test.ts)
|
|
1035
1037
|
- [agent-runtime-settings-router.ts](../../server/modules/agent-runtime/runtime/agent-runtime-settings-router.ts)
|
|
1036
1038
|
- [agent-runtime-settings.ts](../../server/modules/agent-runtime/runtime/agent-runtime-settings.ts) · [test](../../server/modules/agent-runtime/runtime/agent-runtime-settings.test.ts)
|
package/package.json
CHANGED
package/server/dist/mcp/guide.js
CHANGED
|
@@ -342,16 +342,30 @@ can run Contract Refine require AI-spawn, including Explore conversions.
|
|
|
342
342
|
trace or import is usually relative to a subdirectory), \`search\` (literal
|
|
343
343
|
content search with line numbers and bounded snippets), \`read_file\`
|
|
344
344
|
(bounded line ranges with continuation metadata),
|
|
345
|
-
\`summary\`, \`provenance\`, \`diff\`.
|
|
345
|
+
\`summary\`, \`provenance\`, \`diff\`. General Code Explorer remains read-only; stopped-run repairs use specrails_recovery.
|
|
346
346
|
Start with content search to find behavior and tests, then read exact ranges.
|
|
347
347
|
Truncated scans or skipped files do not prove that a symbol is absent.
|
|
348
|
-
- **Run failures & recovery**: \`specrails_jobs(
|
|
348
|
+
- **Run failures & recovery**: Start with \`specrails_jobs(runtime_diagnose, jobId)\`
|
|
349
|
+
for original scope, completed steps, repeated failures and receipt validity.
|
|
350
|
+
Availability of Resume is not a repair recommendation. After the same failure,
|
|
351
|
+
identify and validate a changed precondition before retrying; do not default
|
|
352
|
+
to Relaunch. Evidence can be paged with evidenceId/section/sourceId/cursor/limit.
|
|
353
|
+
\`specrails_jobs(runtime_runs, jobId)\` reads
|
|
349
354
|
why a run stopped and what it offers (\`canResume\`, \`recoverableSteps\`,
|
|
350
355
|
\`pendingApproval\`, \`pendingQuestion\`); \`runtime_evidence\` the durable
|
|
351
356
|
evidence. Act only on the user's confirmation: \`runtime_resume\` /
|
|
352
357
|
\`runtime_recover\` (ai-spawn), \`runtime_approve\` / \`runtime_settle\` /
|
|
353
358
|
\`runtime_dismiss\` (write), \`runtime_cancel\` (destructive). In the in-app
|
|
354
359
|
mission, a failed run also updates its card and posts a briefing turn.
|
|
360
|
+
- **Scoped repair**: \`specrails_recovery\` inspects/list_files/read_file/diff/history
|
|
361
|
+
in the original run scope. \`patch\` requires write permission, a current
|
|
362
|
+
expectedHash, a unique fragment, reason and operationId. \`check\` requires
|
|
363
|
+
destructive permission because registered verification executes project code;
|
|
364
|
+
select a saved checkId from inspect or kind openspec. No arbitrary commands,
|
|
365
|
+
frozen plan edits, checkpoint changes, deletes or automatic resume. Read the
|
|
366
|
+
durable outcome; reuse operationId on uncertain transport and explain a real
|
|
367
|
+
changedPrecondition before retrying a failed check. Old retained Core packages
|
|
368
|
+
may lack this capability; never silently migrate them.
|
|
355
369
|
- **Execution evidence**: \`specrails_jobs(phase_breakdown)\` explains phases;
|
|
356
370
|
job events are paginated. \`specrails_rails(pr_candidates)\` finds existing
|
|
357
371
|
PR targets; \`review_packet(prDeliveryId)\` reads verification evidence.
|
|
@@ -8,6 +8,7 @@ const meta_1 = require("./meta");
|
|
|
8
8
|
const specs_1 = require("./specs");
|
|
9
9
|
const rails_1 = require("./rails");
|
|
10
10
|
const jobs_1 = require("./jobs");
|
|
11
|
+
const recovery_1 = require("./recovery");
|
|
11
12
|
const chat_1 = require("./chat");
|
|
12
13
|
const agents_1 = require("./agents");
|
|
13
14
|
const plugins_1 = require("./plugins");
|
|
@@ -38,6 +39,7 @@ function buildToolSpecs() {
|
|
|
38
39
|
specs.push(...(0, specs_1.specsTools)());
|
|
39
40
|
specs.push(...(0, rails_1.railsTools)());
|
|
40
41
|
specs.push(...(0, jobs_1.jobsTools)());
|
|
42
|
+
specs.push(...(0, recovery_1.recoveryTools)());
|
|
41
43
|
specs.push(...(0, chat_1.chatTools)());
|
|
42
44
|
specs.push(...(0, agents_1.agentsTools)());
|
|
43
45
|
specs.push(...(0, plugins_1.pluginsTools)());
|
|
@@ -39,7 +39,8 @@ function jobsTools() {
|
|
|
39
39
|
'interactive_turn (ai-spawn — send a steering prompt to any running interactive job; claude jobs are interactive by default), ' +
|
|
40
40
|
'finalize (settle a running interactive job now — Freestyle jobs otherwise wait for it, others auto-settle), ' +
|
|
41
41
|
'runtime_runs (read the programmatic runtime state of jobId — or every run when omitted — status, nextStep, canResume, recoverableSteps, pendingApproval, pendingQuestion; THIS is how you learn why a run failed and what recovery it offers), ' +
|
|
42
|
-
'
|
|
42
|
+
'runtime_diagnose (read-only recovery assessment for jobId: original scope, completed steps, repeated failures and verification invalidation reasons; use before recommending retry or relaunch), ' +
|
|
43
|
+
'runtime_evidence (read the durable runtime evidence of jobId; page with evidenceId/section/sourceId/cursor/limit), ' +
|
|
43
44
|
'runtime_resume (ai-spawn — continue a resumable run: optional approve/recover/invalidate step-id lists + answer for a pending question; act ONLY after the user confirmed on the card), ' +
|
|
44
45
|
'runtime_recover (ai-spawn — shorthand for runtime_resume with recover:[stepIds] over the recoverable steps), ' +
|
|
45
46
|
'runtime_approve (write — shorthand for runtime_resume with approve:[stepId] for a pending approval), ' +
|
|
@@ -86,6 +87,7 @@ function jobsTools() {
|
|
|
86
87
|
'background_logs',
|
|
87
88
|
'background_kill',
|
|
88
89
|
'runtime_runs',
|
|
90
|
+
'runtime_diagnose',
|
|
89
91
|
'runtime_evidence',
|
|
90
92
|
'runtime_resume',
|
|
91
93
|
'runtime_recover',
|
|
@@ -102,7 +104,7 @@ function jobsTools() {
|
|
|
102
104
|
.optional()
|
|
103
105
|
.describe('Job id (for get / cancel / priority / diagnostic / interactive_turn / finalize)'),
|
|
104
106
|
// ── list / export pagination + filters ──
|
|
105
|
-
limit: zod_1.z.number().int().positive().max(200).optional().describe('Page size for list (1-200, default 50) / activity (1-100, default 50); background_logs returns the last N buffered log lines'),
|
|
107
|
+
limit: zod_1.z.number().int().positive().max(200).optional().describe('Page size for list (1-200, default 50) / activity (1-100, default 50) / runtime_evidence (1-100, default 25); background_logs returns the last N buffered log lines'),
|
|
106
108
|
offset: zod_1.z.number().int().nonnegative().optional().describe('Page offset for list/background_list'),
|
|
107
109
|
eventLimit: zod_1.z.number().int().positive().max(200).optional().describe('get: maximum persisted events to return (default 50, latest events by default)'),
|
|
108
110
|
eventOffset: zod_1.z.number().int().nonnegative().optional().describe('get: chronological event offset; omit for the latest eventLimit events'),
|
|
@@ -151,6 +153,10 @@ function jobsTools() {
|
|
|
151
153
|
invalidate: zod_1.z.array(zod_1.z.string()).optional().describe('runtime_resume: step ids whose results must be discarded before continuing'),
|
|
152
154
|
answer: zod_1.z.string().optional().describe('runtime_resume: the answer to the run\'s pending question'),
|
|
153
155
|
stepId: zod_1.z.string().optional().describe('runtime_approve/runtime_recover: the single step id (alternative to the arrays)'),
|
|
156
|
+
evidenceId: zod_1.z.string().max(1024).optional().describe('runtime_evidence: verification evidence id returned by the index'),
|
|
157
|
+
section: zod_1.z.enum(['summary', 'stdout', 'stderr', 'source']).optional().describe('runtime_evidence: summary index, command output or recorded source'),
|
|
158
|
+
sourceId: zod_1.z.string().max(1024).optional().describe('runtime_evidence: source id returned by the evidence index'),
|
|
159
|
+
cursor: zod_1.z.string().max(1024).optional().describe('runtime_evidence: continuation cursor returned by the previous page'),
|
|
154
160
|
},
|
|
155
161
|
async handler(ctx, args) {
|
|
156
162
|
const base = (0, types_1.projectPath)(ctx, args.projectId);
|
|
@@ -350,11 +356,23 @@ function jobsTools() {
|
|
|
350
356
|
const railIndex = args.railIndex;
|
|
351
357
|
return (0, types_1.apiCall)(ctx, 'GET', `${base}/agent-runtime/runs${typeof railIndex === 'number' ? `?railIndex=${railIndex}` : ''}`);
|
|
352
358
|
}
|
|
359
|
+
case 'runtime_diagnose': {
|
|
360
|
+
const id = args.jobId;
|
|
361
|
+
if (!id)
|
|
362
|
+
throw new Error('runtime_diagnose requires a "jobId".');
|
|
363
|
+
return (0, types_1.apiCall)(ctx, 'GET', `${base}/agent-runtime/runs/${encodeURIComponent(id)}/diagnosis`);
|
|
364
|
+
}
|
|
353
365
|
case 'runtime_evidence': {
|
|
354
366
|
const id = args.jobId;
|
|
355
367
|
if (!id)
|
|
356
368
|
throw new Error('runtime_evidence requires a "jobId".');
|
|
357
|
-
|
|
369
|
+
const query = new URLSearchParams();
|
|
370
|
+
if (typeof args.evidenceId === 'string')
|
|
371
|
+
query.set('id', args.evidenceId);
|
|
372
|
+
for (const key of ['section', 'sourceId', 'cursor', 'limit'])
|
|
373
|
+
if (args[key] !== undefined)
|
|
374
|
+
query.set(key, String(args[key]));
|
|
375
|
+
return (0, types_1.apiCall)(ctx, 'GET', `${base}/agent-runtime/runs/${encodeURIComponent(id)}/evidence${query.size ? '?' + query.toString() : ''}`);
|
|
358
376
|
}
|
|
359
377
|
case 'runtime_resume':
|
|
360
378
|
case 'runtime_recover':
|
|
@@ -371,7 +389,7 @@ function jobsTools() {
|
|
|
371
389
|
recover = state.runs?.[0]?.recoverableSteps ?? [];
|
|
372
390
|
}
|
|
373
391
|
if (!recover.length)
|
|
374
|
-
throw new Error('runtime_recover: the run has no
|
|
392
|
+
throw new Error('runtime_recover: the run has no interrupted steps to recover — read runtime_diagnose before deciding whether a repair or resume is appropriate.');
|
|
375
393
|
body.recover = recover;
|
|
376
394
|
}
|
|
377
395
|
else if (action === 'runtime_approve') {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recoveryTools = recoveryTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const agent_runtime_recovery_1 = require("../../modules/agent-runtime/runtime/agent-runtime-recovery");
|
|
6
|
+
const types_1 = require("./types");
|
|
7
|
+
function recoveryTools() {
|
|
8
|
+
return [{
|
|
9
|
+
name: 'specrails_recovery', title: 'Repair a stopped run', hintTier: 'read',
|
|
10
|
+
description: 'Inspect and repair the ORIGINAL saved run worktree without relaunching implementation. Actions: inspect (repositories, registered checks, recent repairs), list_files, read_file (paged lines + SHA-256), diff, history, patch (one exact unique replacement in an existing file, mandatory expectedHash and operationId), check (OpenSpec validation or ONE checkId returned by inspect). Patch requires write permission. Check executes project code and requires destructive permission. No arbitrary shell, checkpoint edits, frozen plan edits, file deletion or automatic resume. Reuse operationId after transport uncertainty; never blindly repeat failed checks. Read history and state a changedPrecondition before repeating a failed check on unchanged code. After successful repair/check use specrails_jobs runtime_resume under its normal authorization and verify the terminal outcome.',
|
|
11
|
+
tier: args => args.action === 'check' ? 'destructive' : args.action === 'patch' ? 'write' : 'read',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
action: zod_1.z.enum(['inspect', 'list_files', 'read_file', 'diff', 'history', 'patch', 'check']),
|
|
14
|
+
projectId: zod_1.z.string().optional(), jobId: zod_1.z.string().min(1),
|
|
15
|
+
repositoryId: zod_1.z.string().optional().describe('Exact membership returned by inspect; required for file actions'),
|
|
16
|
+
path: zod_1.z.string().optional().describe('Repository-relative path; list_files accepts .'),
|
|
17
|
+
startLine: zod_1.z.number().int().positive().optional(), endLine: zod_1.z.number().int().positive().optional(),
|
|
18
|
+
offset: zod_1.z.number().int().nonnegative().optional().describe('history only: nextOffset from the previous page; newest attempts first'),
|
|
19
|
+
expectedHash: zod_1.z.string().optional().describe('Full-file hash returned by read_file, mandatory for patch'),
|
|
20
|
+
oldText: zod_1.z.string().optional(), newText: zod_1.z.string().optional(),
|
|
21
|
+
operationId: zod_1.z.string().uuid().optional().describe('New UUID per patch/check; reuse with the exact same request after uncertain transport'),
|
|
22
|
+
reason: zod_1.z.string().optional().describe('Specific diagnosed cause and purpose of the repair/check'),
|
|
23
|
+
kind: zod_1.z.enum(['openspec', 'verification']).optional(), checkId: zod_1.z.string().optional(),
|
|
24
|
+
changedPrecondition: zod_1.z.string().optional().describe('What changed since the failed check; required before repeating an unchanged candidate'),
|
|
25
|
+
acknowledgeInterrupted: zod_1.z.boolean().optional().describe('Only after inspecting and accepting the partial writes of an interrupted run'),
|
|
26
|
+
},
|
|
27
|
+
async handler(ctx, args) {
|
|
28
|
+
const { projectId, jobId, ...request } = args;
|
|
29
|
+
const body = agent_runtime_recovery_1.recoveryRequestSchema.parse(request);
|
|
30
|
+
if (typeof jobId !== 'string' || !jobId)
|
|
31
|
+
throw new Error('Scoped recovery requires jobId');
|
|
32
|
+
return (0, types_1.apiCall)(ctx, 'POST', `${(0, types_1.projectPath)(ctx, projectId)}/agent-runtime/runs/${encodeURIComponent(jobId)}/recovery`, body);
|
|
33
|
+
},
|
|
34
|
+
}];
|
|
35
|
+
}
|
|
@@ -244,7 +244,10 @@ function nextSteps(topic) {
|
|
|
244
244
|
];
|
|
245
245
|
case 'rails_jobs':
|
|
246
246
|
return [
|
|
247
|
-
'
|
|
247
|
+
'Read specrails_jobs runtime_diagnose for the failed job before recommending recovery; inspect runtime_evidence and original-worktree scope, not just the error label.',
|
|
248
|
+
'If a step fails repeatedly, identify the prerequisite or artifact that must change and validate the smallest repair before retrying. canResume is not proof a retry will work. Relaunch is only justified by evidence that the original run cannot continue safely.',
|
|
249
|
+
'Use specrails_recovery inspect/read_file/diff, then a hash-guarded patch and registered check for authorized small repairs in the original worktree. Read history and preserve operationId on uncertain transport.',
|
|
250
|
+
'Preserve completed implementation. Use runtime_settle for a succeeded run awaiting delivery; name unsupported repair capabilities instead of proposing a fresh implementation.',
|
|
248
251
|
'For a failed or stuck job, inspect the Job Detail logs and report the real failure.',
|
|
249
252
|
'Do not relaunch or stop anything without confirmation and the required permission level.',
|
|
250
253
|
];
|
|
@@ -69,9 +69,13 @@ exports.PRICING = {
|
|
|
69
69
|
// usage is the sole signal. Keys are the CLI's short aliases; full model ids
|
|
70
70
|
// (e.g. per-event `message.model` like "claude-sonnet-4-6") are collapsed by
|
|
71
71
|
// family via `resolvePriceEntry`. Cache write = 1.25x input (5-minute-TTL
|
|
72
|
-
// default)
|
|
72
|
+
// default); cache reads are model-specific (Opus 5.5 uses 0.05x input).
|
|
73
|
+
// Anthropic usage semantics: input_tokens
|
|
73
74
|
// EXCLUDES cache reads/writes → inputIncludesCacheReads: false.
|
|
74
|
-
'claude:opus': { inputPer1M: 5.00, outputPer1M: 25.00, cacheReadPer1M: 0.50, cacheWritePer1M: 6.25, inputIncludesCacheReads: false, lastReviewedAt: '2026-07-02' },
|
|
75
|
+
'claude:claude-opus-5': { inputPer1M: 5.00, outputPer1M: 25.00, cacheReadPer1M: 0.50, cacheWritePer1M: 6.25, inputIncludesCacheReads: false, lastReviewedAt: '2026-07-02' },
|
|
76
|
+
// Opus 5.5: https://platform.claude.com/docs/en/models/opus-5-5/overview
|
|
77
|
+
'claude:opus': { inputPer1M: 4.00, outputPer1M: 20.00, cacheReadPer1M: 0.20, cacheWritePer1M: 5.00, inputIncludesCacheReads: false, lastReviewedAt: '2026-09-24' },
|
|
78
|
+
'claude:claude-opus-5-5': { inputPer1M: 4.00, outputPer1M: 20.00, cacheReadPer1M: 0.20, cacheWritePer1M: 5.00, inputIncludesCacheReads: false, lastReviewedAt: '2026-09-24' },
|
|
75
79
|
'claude:sonnet': { inputPer1M: 3.00, outputPer1M: 15.00, cacheReadPer1M: 0.30, cacheWritePer1M: 3.75, inputIncludesCacheReads: false, lastReviewedAt: '2026-07-02' },
|
|
76
80
|
'claude:haiku': { inputPer1M: 1.00, outputPer1M: 5.00, cacheReadPer1M: 0.10, cacheWritePer1M: 1.25, inputIncludesCacheReads: false, lastReviewedAt: '2026-07-02' },
|
|
77
81
|
'claude:fable': { inputPer1M: 10.00, outputPer1M: 50.00, cacheReadPer1M: 1.00, cacheWritePer1M: 12.50, inputIncludesCacheReads: false, lastReviewedAt: '2026-07-02' },
|
|
@@ -89,6 +93,9 @@ function resolvePriceEntry(providerId, model) {
|
|
|
89
93
|
if (exact)
|
|
90
94
|
return exact;
|
|
91
95
|
if (providerId === 'claude') {
|
|
96
|
+
// Keep the existing estimates for older concrete Opus generations.
|
|
97
|
+
if (/^claude-opus-(?:4(?:-|$)|5$)/.test(model))
|
|
98
|
+
return exports.PRICING['claude:claude-opus-5'];
|
|
92
99
|
const family = CLAUDE_MODEL_FAMILY.exec(model);
|
|
93
100
|
if (family)
|
|
94
101
|
return exports.PRICING[`claude:${family[1]}`];
|
|
@@ -83,6 +83,14 @@ function registerAgentRuntimeControlRoutes({ router, ctx }) {
|
|
|
83
83
|
res.status(error instanceof agent_runtime_controls_1.RuntimeControlError ? error.statusCode : 503).json({ error: 'evidence_unavailable', message: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.message : 'The original runtime evidence is unavailable' });
|
|
84
84
|
}
|
|
85
85
|
});
|
|
86
|
+
router.get('/:projectId/agent-runtime/runs/:runId/diagnosis', async (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
res.json(await controls(req).diagnose(String(req.params.runId)));
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
res.status(error instanceof agent_runtime_controls_1.RuntimeControlError ? error.statusCode : 503).json({ error: 'diagnosis_unavailable', message: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.message : 'Could not inspect the original runtime execution' });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
86
94
|
router.post('/:projectId/agent-runtime/runs/:runId/resume', async (req, res) => {
|
|
87
95
|
try {
|
|
88
96
|
await controls(req).resume(String(req.params.runId), (0, agent_runtime_controls_1.validateRuntimeResumeInput)(req.body));
|
|
@@ -92,6 +100,14 @@ function registerAgentRuntimeControlRoutes({ router, ctx }) {
|
|
|
92
100
|
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' });
|
|
93
101
|
}
|
|
94
102
|
});
|
|
103
|
+
router.post('/:projectId/agent-runtime/runs/:runId/recovery', async (req, res) => {
|
|
104
|
+
try {
|
|
105
|
+
res.json(await controls(req).recovery(String(req.params.runId), req.body));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
res.status(error instanceof agent_runtime_controls_1.RuntimeControlError ? error.statusCode : 500).json({ error: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.code : 'runtime_recovery_failed', message: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.message : 'Could not perform scoped recovery' });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
95
111
|
router.post('/:projectId/agent-runtime/runs/:runId/settle', async (req, res) => {
|
|
96
112
|
try {
|
|
97
113
|
await controls(req).settle(String(req.params.runId));
|
|
@@ -25,6 +25,7 @@ const loop_runs_store_1 = require("../../loops/runtime/loop-runs-store");
|
|
|
25
25
|
const multi_repo_execution_store_1 = require("../../delivery/runtime/multi-repo-execution-store");
|
|
26
26
|
const db_1 = require("../../../db");
|
|
27
27
|
const loop_run_manager_1 = require("../../loops/runtime/loop-run-manager");
|
|
28
|
+
const agent_runtime_recovery_1 = require("./agent-runtime-recovery");
|
|
28
29
|
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
29
30
|
const STEP_IDS = ['architect', 'developer', 'fixer', 'verify', 'reviewer', 'archive'];
|
|
30
31
|
const ANSWER_LIMIT = 20_000;
|
|
@@ -94,7 +95,7 @@ async function readAgentRuntimeStatus(contextPath, cwd, env) {
|
|
|
94
95
|
selection = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(node_path_1.default.dirname(contextPath), 'desktop-runtime-selection.json'), 'utf8'));
|
|
95
96
|
}
|
|
96
97
|
catch { /* Original hosts may not record selection origins. */ }
|
|
97
|
-
return result.state ? { ...result.state, efficiencySummary: (0, agent_runtime_metrics_1.applyRuntimeSelectionOrigins)((0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(result.efficiencySummary), selection, result.state.runId) } : null;
|
|
98
|
+
return result.state ? { ...result.state, inspection: result.pipeline, efficiencySummary: (0, agent_runtime_metrics_1.applyRuntimeSelectionOrigins)((0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(result.efficiencySummary), selection, result.state.runId) } : null;
|
|
98
99
|
}
|
|
99
100
|
/** One controller per ProjectContext; Core's durable lease remains the final
|
|
100
101
|
* cross-process guard. Resume never constructs a new context or worktree. */
|
|
@@ -165,6 +166,97 @@ class AgentRuntimeControls {
|
|
|
165
166
|
throw new RuntimeControlError(503, 'evidence_unavailable', 'Core evidence is unavailable or incompatible');
|
|
166
167
|
return result;
|
|
167
168
|
}
|
|
169
|
+
async recovery(runId, input) {
|
|
170
|
+
const parsed = agent_runtime_recovery_1.recoveryRequestSchema.safeParse(input);
|
|
171
|
+
if (!parsed.success)
|
|
172
|
+
throw new RuntimeControlError(400, 'invalid_recovery_request', parsed.error.message);
|
|
173
|
+
if (this.disposed)
|
|
174
|
+
throw new RuntimeControlError(503, 'runtime_shutting_down', 'Project runtime is shutting down');
|
|
175
|
+
const parent = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
|
|
176
|
+
const idle = () => parent?.status === 'completed' && !this.ctx.railLoopRuns?.size && !this.ctx.railJobs?.size
|
|
177
|
+
&& !this.ctx.db.prepare("SELECT 1 FROM jobs WHERE status = 'running' LIMIT 1").get();
|
|
178
|
+
if (!idle() || this.active.size)
|
|
179
|
+
throw new RuntimeControlError(409, 'runtime_run_active', 'Wait for project executions to settle before scoped recovery');
|
|
180
|
+
const context = this.context(runId);
|
|
181
|
+
const active = { cancelled: false };
|
|
182
|
+
this.active.set(runId, active);
|
|
183
|
+
if (parent?.rail_index != null)
|
|
184
|
+
this.ctx.railLoopRuns?.set(runId, { railIndex: parent.rail_index, ticketIds: [], requiresTerminalIntent: true });
|
|
185
|
+
try {
|
|
186
|
+
const result = await (this.dependencies.recovery ?? agent_runtime_recovery_1.invokeRuntimeRecovery)({ contextPath: context.file, cwd: context.cwd, env: context.env, request: parsed.data,
|
|
187
|
+
onSpawn: child => { active.child = child; if (active.cancelled || this.disposed)
|
|
188
|
+
this.cancel(runId); },
|
|
189
|
+
});
|
|
190
|
+
if (parsed.data.action === 'patch' || parsed.data.action === 'check') {
|
|
191
|
+
this.statusCache.delete(runId);
|
|
192
|
+
this.ctx.db.transaction(() => {
|
|
193
|
+
const seq = this.ctx.db.prepare('SELECT COALESCE(MAX(seq), -1) + 1 AS seq FROM events WHERE job_id = ?').get(runId).seq;
|
|
194
|
+
(0, db_1.appendEvent)(this.ctx.db, runId, seq, { event_type: 'runtime-recovery', source: 'stdout', payload: JSON.stringify({ action: parsed.data.action, result }) });
|
|
195
|
+
})();
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (error instanceof RuntimeControlError)
|
|
201
|
+
throw error;
|
|
202
|
+
throw new RuntimeControlError(409, 'runtime_recovery_blocked', error instanceof Error ? error.message : 'Recovery failed; inspect history before retrying');
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
clearTimeout(active.forceKillTimer);
|
|
206
|
+
this.active.delete(runId);
|
|
207
|
+
this.ctx.railLoopRuns?.delete(runId);
|
|
208
|
+
this.statusCache.delete(runId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Read-only recovery assessment. canResume is admission, not a repair claim. */
|
|
212
|
+
async diagnose(runId) {
|
|
213
|
+
const summary = await this.summary(runId);
|
|
214
|
+
if (summary.historical || summary.status === 'unavailable')
|
|
215
|
+
return {
|
|
216
|
+
runId, summary, recommendation: 'inspect_saved_evidence',
|
|
217
|
+
reason: 'Live original runtime scope is unavailable. Preserve saved work and inspect the missing scope before proposing a fresh run.',
|
|
218
|
+
repeatFailureCount: null, recentFailures: [],
|
|
219
|
+
};
|
|
220
|
+
const { file, frozen, cwd, env } = this.context(runId);
|
|
221
|
+
const state = await this.dependencies.status(file, cwd, env);
|
|
222
|
+
if (!state || state.runId !== runId)
|
|
223
|
+
throw new RuntimeControlError(409, 'runtime_state_unavailable', 'No saved workflow state is available');
|
|
224
|
+
const failures = (state.recentFailures ?? []).slice(-8).map(item => ({ stepId: item.stepId, status: item.status, at: item.at, error: item.error?.slice(-6000) }));
|
|
225
|
+
const matching = failures.filter(item => item.status === 'failed' && item.stepId === state.nextStep && !!item.error && item.error === state.error?.slice(-6000));
|
|
226
|
+
const repeated = matching.length > 1;
|
|
227
|
+
const recommendation = summary.active ? 'wait_for_active_run'
|
|
228
|
+
: summary.canSettle ? 'prepare_delivery'
|
|
229
|
+
: summary.status === 'succeeded' ? 'inspect_delivery'
|
|
230
|
+
: summary.pendingQuestion ? 'answer_question'
|
|
231
|
+
: summary.pendingApproval ? 'inspect_and_approve'
|
|
232
|
+
: repeated ? 'repair_before_retry'
|
|
233
|
+
: summary.recoverableSteps.length ? 'inspect_interrupted_writes'
|
|
234
|
+
: 'diagnose_before_retry';
|
|
235
|
+
return {
|
|
236
|
+
runId, summary, recommendation,
|
|
237
|
+
reason: {
|
|
238
|
+
wait_for_active_run: 'The original run is still active. Inspect its progress instead of starting a competing execution.',
|
|
239
|
+
prepare_delivery: 'Core succeeded but Desktop settlement is still pending. Prepare delivery of the existing implementation.',
|
|
240
|
+
inspect_delivery: 'Core already succeeded. Inspect the delivery state rather than restarting implementation.',
|
|
241
|
+
answer_question: 'The run is waiting for the recorded question to be answered.',
|
|
242
|
+
inspect_and_approve: 'The run is waiting for approval of the recorded candidate; inspect its evidence first.',
|
|
243
|
+
repair_before_retry: 'The same step failed with the same error more than once. Identify and verify a changed precondition before spending on another attempt.',
|
|
244
|
+
inspect_interrupted_writes: 'An interrupted step may have partially changed files. Inspect the original worktree before explicitly recovering it.',
|
|
245
|
+
diagnose_before_retry: 'Determine the failing precondition from the saved evidence. A resumable run may still need a repair; a fresh run is not a diagnosis.',
|
|
246
|
+
}[recommendation],
|
|
247
|
+
originalScope: { artifactRoot: frozen.artifactRoot, repositories: frozen.repositories },
|
|
248
|
+
completedSteps: Object.entries(state.steps).filter(([, step]) => step.status === 'succeeded').map(([id]) => id),
|
|
249
|
+
recentFailures: failures, repeatFailureCount: state.recentFailures ? matching.length : null,
|
|
250
|
+
verification: state.inspection?.verification ? { valid: state.inspection.verification.valid, reasons: state.inspection.verification.reasons } : null,
|
|
251
|
+
acceptance: state.inspection?.acceptance ? { valid: state.inspection.acceptance.valid, reasons: state.inspection.acceptance.reasons } : null,
|
|
252
|
+
resumePhase: state.inspection?.resumePhase ?? null,
|
|
253
|
+
limitations: [
|
|
254
|
+
'Recent failure history is bounded to eight attempts; null means the retained Core does not expose it.',
|
|
255
|
+
'Completed steps are historical outcomes. Resume revalidates receipts and may repeat verification/review after changes; do not promise zero cost.',
|
|
256
|
+
'This diagnostic does not execute repairs, resume, relaunch, publish or discard work.',
|
|
257
|
+
],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
168
260
|
async list() {
|
|
169
261
|
const directory = node_path_1.default.join((0, workspace_resolution_1.resolveProjectExecution)(this.ctx.project).specrailsDir, 'pipeline');
|
|
170
262
|
if (!node_fs_1.default.existsSync(directory))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recoveryRequestSchema = void 0;
|
|
4
|
+
exports.invokeRuntimeRecovery = invokeRuntimeRecovery;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
const agent_runtime_package_1 = require("./agent-runtime-package");
|
|
8
|
+
const core_node_runtime_1 = require("../../../core-node-runtime");
|
|
9
|
+
const win_spawn_1 = require("../../../util/win-spawn");
|
|
10
|
+
// Desktop validates its HTTP contract; retained Core independently validates
|
|
11
|
+
// the same operation at the filesystem/lease boundary.
|
|
12
|
+
const file = { repositoryId: zod_1.z.string().min(1).max(128), path: zod_1.z.string().min(1).max(1024) };
|
|
13
|
+
const operation = { operationId: zod_1.z.string().uuid(), reason: zod_1.z.string().trim().min(1).max(2000), acknowledgeInterrupted: zod_1.z.boolean().optional(), changedPrecondition: zod_1.z.string().trim().min(1).max(2000).optional() };
|
|
14
|
+
exports.recoveryRequestSchema = zod_1.z.discriminatedUnion('action', [
|
|
15
|
+
zod_1.z.object({ action: zod_1.z.literal('inspect') }).strict(),
|
|
16
|
+
zod_1.z.object({ action: zod_1.z.literal('history'), offset: zod_1.z.number().int().nonnegative().optional() }).strict(),
|
|
17
|
+
zod_1.z.object({ action: zod_1.z.literal('list_files'), ...file }).strict(),
|
|
18
|
+
zod_1.z.object({ action: zod_1.z.literal('read_file'), ...file, startLine: zod_1.z.number().int().positive().optional(), endLine: zod_1.z.number().int().positive().optional() }).strict(),
|
|
19
|
+
zod_1.z.object({ action: zod_1.z.literal('diff'), ...file }).strict(),
|
|
20
|
+
zod_1.z.object({ action: zod_1.z.literal('patch'), ...file, ...operation, expectedHash: zod_1.z.string().regex(/^[a-f0-9]{64}$/), oldText: zod_1.z.string().min(1).max(16_384), newText: zod_1.z.string().max(16_384) }).strict(),
|
|
21
|
+
zod_1.z.object({ action: zod_1.z.literal('check'), ...operation, kind: zod_1.z.enum(['openspec', 'verification']), checkId: zod_1.z.string().min(1).max(256).optional() }).strict(),
|
|
22
|
+
]);
|
|
23
|
+
async function invokeRuntimeRecovery(input) {
|
|
24
|
+
const cli = (0, agent_runtime_package_1.resolveRetainedAgentRuntime)(input.contextPath);
|
|
25
|
+
const invoke = (args, stdin) => new Promise((resolve, reject) => {
|
|
26
|
+
const child = (0, node_child_process_1.execFile)((0, core_node_runtime_1.resolveCoreNodeRuntime)(), [cli, ...args], {
|
|
27
|
+
cwd: input.cwd, env: (0, win_spawn_1.windowsSpawnEnv)(input.env), windowsHide: true,
|
|
28
|
+
timeout: stdin ? 65_000 : 15_000, maxBuffer: 2 * 1024 * 1024, encoding: 'utf8',
|
|
29
|
+
}, (error, stdout) => {
|
|
30
|
+
if (!error) {
|
|
31
|
+
resolve(stdout);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let detail = 'Recovery did not complete; inspect its history before retrying with the same operationId.';
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(stdout);
|
|
37
|
+
if (typeof parsed.error === 'string')
|
|
38
|
+
detail = parsed.error.slice(-6000);
|
|
39
|
+
}
|
|
40
|
+
catch { /* Do not misrepresent an uncertain transport outcome. */ }
|
|
41
|
+
reject(new Error(detail));
|
|
42
|
+
});
|
|
43
|
+
input.onSpawn?.(child);
|
|
44
|
+
child.stdin?.end(stdin ?? '');
|
|
45
|
+
});
|
|
46
|
+
const api = JSON.parse(await invoke(['api']));
|
|
47
|
+
if (api.capabilities?.scopedRecovery !== 1)
|
|
48
|
+
throw new Error('The retained original Core does not support scoped recovery. Preserve this run; use a manual repair in its original worktree. Do not replace its runtime or relaunch to bypass this limit.');
|
|
49
|
+
const response = JSON.parse(await invoke(['recovery', '--context', input.contextPath, '--stdin'], JSON.stringify(input.request)));
|
|
50
|
+
if (response.type !== 'runtime-recovery' || response.schemaVersion !== 1 || !('result' in response))
|
|
51
|
+
throw new Error('Invalid Core recovery response; inspect recovery history before retrying');
|
|
52
|
+
return response.result;
|
|
53
|
+
}
|
|
@@ -48,7 +48,9 @@ function buildFailureBriefing(input) {
|
|
|
48
48
|
const lines = [];
|
|
49
49
|
lines.push('[Specrails run-failure briefing — automatic, not typed by the user]');
|
|
50
50
|
lines.push('');
|
|
51
|
-
|
|
51
|
+
const description = input.failure.code === 'implementation_failed' && input.failure.stepId
|
|
52
|
+
? `the ${input.failure.stepId} step failed` : describeFailureCode(input.failure.code);
|
|
53
|
+
lines.push(`${railLabel(input.railIndex, input.railName)} · run ${input.runId} stopped: ${description}.`);
|
|
52
54
|
lines.push(`Specs: ${specs.join(', ') || 'none'}.`);
|
|
53
55
|
if (input.failure.stepId)
|
|
54
56
|
lines.push(`Failed step: ${input.failure.stepId}.`);
|
|
@@ -81,7 +83,7 @@ function buildFailureBriefing(input) {
|
|
|
81
83
|
if (input.prDeliveryId)
|
|
82
84
|
lines.push(`Delivery id: ${input.prDeliveryId}.`);
|
|
83
85
|
lines.push('');
|
|
84
|
-
lines.push('Conduct: explain the failure in at most 6 short lines, name the ONE next action you recommend and why, and stop. Do NOT relaunch, resume or discard by yourself — the user decides on the card.
|
|
86
|
+
lines.push('Conduct: explain the failure in at most 6 short lines, name the ONE next action you recommend and why, and stop. Do NOT relaunch, resume or discard by yourself — the user decides on the card. Read specrails_jobs runtime_diagnose and, as needed, get / runtime_evidence before recommending recovery. canResume is not proof that retry fixes the cause. The next action may be diagnosis or a targeted repair, not a card button. If the same failure recurred, identify what must change before another attempt; do not default to Relaunch. This six-line limit applies only to this automatic turn.');
|
|
85
87
|
return lines.join('\n');
|
|
86
88
|
}
|
|
87
89
|
/** The context ref persisted on the briefing row so the client renders it compactly. */
|