specrails-desktop 2.48.0 → 2.49.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-CGq6QhP5.js → ActivityFeedPage-FdZBSmTU.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-C4GZ-kdf.js → AgentBrowserCapture-Dr8kZotA.js} +1 -1
- package/client/dist/assets/{AgentModeAnalyticsPane-_IPmL0ZS.js → AgentModeAnalyticsPane-DXQSjYe-.js} +2 -2
- package/client/dist/assets/{AgentModeCodePane-B5WG5DOP.js → AgentModeCodePane-DcVV6EBy.js} +2 -2
- package/client/dist/assets/AgentModeJobsPane-a9f-g56c.js +1 -0
- package/client/dist/assets/AgentsPage-CTv4UVdp.js +87 -0
- package/client/dist/assets/{AnalyticsPage-BUMsKtsJ.js → AnalyticsPage-BbXPcz3a.js} +1 -1
- package/client/dist/assets/CodePage-CIigb_O0.js +3 -0
- package/client/dist/assets/DesktopAnalyticsPage-DZhN5jVh.js +1 -0
- package/client/dist/assets/{DocsDialog-ccQETHKB.js → DocsDialog-DzrqPxuj.js} +2 -2
- package/client/dist/assets/{DocsPage-DzEgplB3.js → DocsPage-B1AUuk0U.js} +1 -1
- package/client/dist/assets/{ExportDropdown-B8bBGopr.js → ExportDropdown-IPutNYvD.js} +1 -1
- package/client/dist/assets/InteractiveJobComposer-jg3xE_GX.js +19 -0
- package/client/dist/assets/{JobDetailModal-BYXmuTDe.js → JobDetailModal-BS-htwqZ.js} +1 -1
- package/client/dist/assets/JobDetailPage-CKYtQV5a.js +1 -0
- package/client/dist/assets/JobsPage-DFJm0bw0.js +1 -0
- package/client/dist/assets/{LoopBuilderPage-DQE-7Vby.js → LoopBuilderPage-ujkA2U0T.js} +2 -2
- package/client/dist/assets/LoopPreviewModal-BYOrnmcp.js +1 -0
- package/client/dist/assets/LoopsPage-DRcT1ikP.js +1 -0
- package/client/dist/assets/{MinimizedChatsContext-UpnqccPx.js → MinimizedChatsContext-CX1i2kQ8.js} +1 -1
- package/client/dist/assets/{PluginsPage-CjgVvmpW.js → PluginsPage-BR9ZTqxR.js} +2 -2
- package/client/dist/assets/{ProjectSettingsDialog-D6vBCoay.js → ProjectSettingsDialog-SAfCqXzC.js} +1 -1
- package/client/dist/assets/RepositoryDeliveries-DqmlG0Zs.js +5 -0
- package/client/dist/assets/{RepositoryScopeSelector-0Gz_fR1E.js → RepositoryScopeSelector-DG8KKPrm.js} +1 -1
- package/client/dist/assets/ReviewPacketPage-BoqahYNC.js +4 -0
- package/client/dist/assets/TemplatePreviewModal-CK5OoImj.js +1 -0
- package/client/dist/assets/TicketDetailModalContext-D2MEhRwJ.js +14 -0
- package/client/dist/assets/{Trans-9pHTy2fH.js → Trans-BLljsfHR.js} +1 -1
- package/client/dist/assets/agent-B76BhAyE.js +1 -0
- package/client/dist/assets/agent-BII69Nc0.js +1 -0
- package/client/dist/assets/agent-BIO4q6b0.js +1 -0
- package/client/dist/assets/agent-ButS6kwl.js +1 -0
- package/client/dist/assets/agent-CVeMLIu4.js +1 -0
- package/client/dist/assets/agent-CYFBDwFD.js +1 -0
- package/client/dist/assets/agent-D3jkMipn.js +1 -0
- package/client/dist/assets/agent-DSrflYFL.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-w_2U5pm0.js → formatDistanceToNow-CbQZkD8M.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BRymg1Kk.js → getTimezoneOffsetInMilliseconds-pBVK3rJC.js} +1 -1
- package/client/dist/assets/index-CjAPu3wb.js +80 -0
- package/client/dist/assets/index-UDzndadF.css +2 -0
- package/client/dist/assets/{jira-api-BqHbSdxK.js → jira-api-f-sYCIuX.js} +1 -1
- package/client/dist/assets/packet-BTRySHoQ.js +1 -0
- package/client/dist/assets/packet-BsRimSfZ.js +1 -0
- package/client/dist/assets/packet-C0pcMd6x.js +1 -0
- package/client/dist/assets/packet-C8qzvgrZ.js +1 -0
- package/client/dist/assets/packet-DF-iwqqY.js +1 -0
- package/client/dist/assets/packet-DQ_GnT2b.js +1 -0
- package/client/dist/assets/packet-Nb8BLI5h.js +1 -0
- package/client/dist/assets/packet-r7J14m89.js +1 -0
- package/client/dist/assets/{project-repositories-BsCs1PPA.js → project-repositories-DAjJbFhm.js} +1 -1
- package/client/dist/assets/{spending-hcgxHP4O.js → spending-DJBbdJyD.js} +1 -1
- package/client/dist/assets/{useDesktop-CAisjKWC.js → useDesktop-BJ8IRBRt.js} +1 -1
- package/client/dist/assets/useRuntimeRuns-Ch1KUc7k.js +1 -0
- package/client/dist/assets/{useSharedWebSocket-BHIfW4QT.js → useSharedWebSocket-BN3Lmehi.js} +2 -2
- package/client/dist/index.html +17 -17
- package/docs/guide/de/integrations/6-agent-chat.md +8 -0
- package/docs/guide/en/integrations/6-agent-chat.md +8 -0
- package/docs/guide/es/integrations/6-agent-chat.md +8 -0
- package/docs/guide/fr/integrations/6-agent-chat.md +8 -0
- package/docs/guide/it/integrations/6-agent-chat.md +8 -0
- package/docs/guide/ja/integrations/6-agent-chat.md +8 -0
- package/docs/guide/pt/integrations/6-agent-chat.md +8 -0
- package/docs/guide/zh/integrations/6-agent-chat.md +8 -0
- package/docs/internals/mission-rail-cards.md +130 -0
- package/docs/internals/review-packet.md +16 -3
- package/docs/mcp.md +24 -1
- package/package.json +1 -1
- package/server/dist/agent-chat-manager.js +97 -2
- package/server/dist/agent-chat-router.js +57 -1
- package/server/dist/agent-context-resolver.js +15 -0
- package/server/dist/agent-cwd-manager.js +1 -1
- package/server/dist/agent-failure-briefing.js +90 -0
- package/server/dist/agent-fence-promotion.js +8 -1
- package/server/dist/agent-operator-prompt.js +69 -1
- package/server/dist/agent-runtime-controls-router.js +17 -0
- package/server/dist/agent-store.js +40 -0
- package/server/dist/delivery-evidence.js +159 -0
- package/server/dist/desktop-db.js +11 -0
- package/server/dist/feature-flags.js +20 -0
- package/server/dist/mcp/guide.js +7 -0
- package/server/dist/mcp/tools/jobs.js +91 -4
- package/server/dist/mcp/tools/rails.js +2 -2
- package/server/dist/mission-run-notify.js +170 -0
- package/server/dist/rail-isolated-launch.js +47 -0
- package/server/dist/rail-launch-parser.js +182 -0
- package/server/dist/rail-pr-store.js +20 -0
- package/server/dist/rails-router.js +98 -5
- package/server/dist/review-packet.js +35 -2
- package/client/dist/assets/AgentModeJobsPane-w1N_HIXh.js +0 -1
- package/client/dist/assets/AgentsPage-CmwnnT46.js +0 -87
- package/client/dist/assets/CodePage-PjgywJPS.js +0 -3
- package/client/dist/assets/DesktopAnalyticsPage-BXY1pH4y.js +0 -1
- package/client/dist/assets/InteractiveJobComposer-CEwGkJ8F.js +0 -19
- package/client/dist/assets/JobDetailPage-CWp6MwJj.js +0 -1
- package/client/dist/assets/JobsPage-D1O1uyG8.js +0 -1
- package/client/dist/assets/LoopPreviewModal-Bk7zIXD5.js +0 -1
- package/client/dist/assets/LoopsPage-CqG7mx56.js +0 -1
- package/client/dist/assets/RepositoryDeliveries-C36ipKp_.js +0 -5
- package/client/dist/assets/ReviewPacketPage-Dr9RYs7q.js +0 -1
- package/client/dist/assets/TemplatePreviewModal-sCMHYFJo.js +0 -1
- package/client/dist/assets/TicketDetailModalContext-C5GgJlEt.js +0 -14
- package/client/dist/assets/agent-BdT0SB1D.js +0 -1
- package/client/dist/assets/agent-Bh9K7iEt.js +0 -1
- package/client/dist/assets/agent-C4ibdFk9.js +0 -1
- package/client/dist/assets/agent-CSFY22Zo.js +0 -1
- package/client/dist/assets/agent-DAnqxUn8.js +0 -1
- package/client/dist/assets/agent-DTQiu0Mi.js +0 -1
- package/client/dist/assets/agent-T54WuR_Y.js +0 -1
- package/client/dist/assets/agent-i75WDZOA.js +0 -1
- package/client/dist/assets/brain-DI9lHkKY.js +0 -1
- package/client/dist/assets/format-duration-NJVPOPBG.js +0 -1
- package/client/dist/assets/index-D0ScFZvR.css +0 -2
- package/client/dist/assets/index-DORCrgbI.js +0 -77
- package/client/dist/assets/packet-BNW8ODQw.js +0 -1
- package/client/dist/assets/packet-CXYvfdjy.js +0 -1
- package/client/dist/assets/packet-CtIlB8YE.js +0 -1
- package/client/dist/assets/packet-DA1SJkMH.js +0 -1
- package/client/dist/assets/packet-DYcWfBip.js +0 -1
- package/client/dist/assets/packet-DseX9yKC.js +0 -1
- package/client/dist/assets/packet-_m5aOy_d.js +0 -1
- package/client/dist/assets/packet-qOemUAm3.js +0 -1
package/server/dist/mcp/guide.js
CHANGED
|
@@ -335,6 +335,13 @@ can run Contract Refine require AI-spawn, including Explore conversions.
|
|
|
335
335
|
\`summary\`, \`provenance\`, \`diff\`. There is no MCP write path to files.
|
|
336
336
|
Start with content search to find behavior and tests, then read exact ranges.
|
|
337
337
|
Truncated scans or skipped files do not prove that a symbol is absent.
|
|
338
|
+
- **Run failures & recovery**: \`specrails_jobs(runtime_runs, jobId)\` reads
|
|
339
|
+
why a run stopped and what it offers (\`canResume\`, \`recoverableSteps\`,
|
|
340
|
+
\`pendingApproval\`, \`pendingQuestion\`); \`runtime_evidence\` the durable
|
|
341
|
+
evidence. Act only on the user's confirmation: \`runtime_resume\` /
|
|
342
|
+
\`runtime_recover\` (ai-spawn), \`runtime_approve\` / \`runtime_settle\` /
|
|
343
|
+
\`runtime_dismiss\` (write), \`runtime_cancel\` (destructive). In the in-app
|
|
344
|
+
mission, a failed run also updates its card and posts a briefing turn.
|
|
338
345
|
- **Execution evidence**: \`specrails_jobs(phase_breakdown)\` explains phases;
|
|
339
346
|
job events are paginated. \`specrails_rails(pr_candidates)\` finds existing
|
|
340
347
|
PR targets; \`review_packet(prDeliveryId)\` reads verification evidence.
|
|
@@ -37,15 +37,22 @@ function jobsTools() {
|
|
|
37
37
|
'pipeline (read a pipeline\'s jobs + statuses — use after composing dependent spawns), ' +
|
|
38
38
|
'activity (recent activity feed), stats, metrics, phase_breakdown (per-phase timing and token use for jobId), default_spec_model, ' +
|
|
39
39
|
'interactive_turn (ai-spawn — send a steering prompt to any running interactive job; claude jobs are interactive by default), ' +
|
|
40
|
-
'finalize (settle a running interactive job now — Freestyle jobs otherwise wait for it, others auto-settle)
|
|
40
|
+
'finalize (settle a running interactive job now — Freestyle jobs otherwise wait for it, others auto-settle), ' +
|
|
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
|
+
'runtime_evidence (read the durable runtime evidence of jobId), ' +
|
|
43
|
+
'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
|
+
'runtime_recover (ai-spawn — shorthand for runtime_resume with recover:[stepIds] over the recoverable steps), ' +
|
|
45
|
+
'runtime_approve (write — shorthand for runtime_resume with approve:[stepId] for a pending approval), ' +
|
|
46
|
+
'runtime_settle (write — prepare delivery for a succeeded-but-unsettled run), runtime_dismiss (write — dismiss a settled continuation from the rail card), ' +
|
|
47
|
+
'runtime_cancel (destructive — cancel an active runtime continuation).',
|
|
41
48
|
hintTier: 'read',
|
|
42
49
|
tier: (a) => {
|
|
43
50
|
const action = a.action;
|
|
44
|
-
if (['cancel', 'purge', 'background_start', 'background_kill'].includes(action))
|
|
51
|
+
if (['cancel', 'purge', 'background_start', 'background_kill', 'runtime_cancel'].includes(action))
|
|
45
52
|
return 'destructive';
|
|
46
|
-
if (['spawn', 'interactive_turn'].includes(action))
|
|
53
|
+
if (['spawn', 'interactive_turn', 'runtime_resume', 'runtime_recover'].includes(action))
|
|
47
54
|
return 'ai-spawn';
|
|
48
|
-
if (['pause', 'resume', 'reorder', 'priority', 'finalize'].includes(action))
|
|
55
|
+
if (['pause', 'resume', 'reorder', 'priority', 'finalize', 'runtime_approve', 'runtime_settle', 'runtime_dismiss'].includes(action))
|
|
49
56
|
return 'write';
|
|
50
57
|
return 'read';
|
|
51
58
|
},
|
|
@@ -78,6 +85,14 @@ function jobsTools() {
|
|
|
78
85
|
'background_list',
|
|
79
86
|
'background_logs',
|
|
80
87
|
'background_kill',
|
|
88
|
+
'runtime_runs',
|
|
89
|
+
'runtime_evidence',
|
|
90
|
+
'runtime_resume',
|
|
91
|
+
'runtime_recover',
|
|
92
|
+
'runtime_approve',
|
|
93
|
+
'runtime_settle',
|
|
94
|
+
'runtime_dismiss',
|
|
95
|
+
'runtime_cancel',
|
|
81
96
|
])
|
|
82
97
|
.describe('Operation to perform'),
|
|
83
98
|
projectId: zod_1.z.string().optional().describe('Project id (defaults to the active project)'),
|
|
@@ -129,6 +144,13 @@ function jobsTools() {
|
|
|
129
144
|
format: zod_1.z.enum(['json', 'csv']).optional().describe('Export format (default json)'),
|
|
130
145
|
// ── default_spec_model ──
|
|
131
146
|
provider: zod_1.z.string().optional().describe('Provider to resolve the default spec model for (default_spec_model)'),
|
|
147
|
+
// ── runtime_* (mission-rail-cards: the agent's eyes + hands on a failed run) ──
|
|
148
|
+
railIndex: zod_1.z.number().int().nonnegative().optional().describe('runtime_runs: only the latest continuation of this rail (0-based)'),
|
|
149
|
+
approve: zod_1.z.array(zod_1.z.string()).optional().describe('runtime_resume/runtime_approve: step ids to approve'),
|
|
150
|
+
recover: zod_1.z.array(zod_1.z.string()).optional().describe('runtime_resume/runtime_recover: step ids to recover (default for runtime_recover: every recoverableStep)'),
|
|
151
|
+
invalidate: zod_1.z.array(zod_1.z.string()).optional().describe('runtime_resume: step ids whose results must be discarded before continuing'),
|
|
152
|
+
answer: zod_1.z.string().optional().describe('runtime_resume: the answer to the run\'s pending question'),
|
|
153
|
+
stepId: zod_1.z.string().optional().describe('runtime_approve/runtime_recover: the single step id (alternative to the arrays)'),
|
|
132
154
|
},
|
|
133
155
|
async handler(ctx, args) {
|
|
134
156
|
const base = (0, types_1.projectPath)(ctx, args.projectId);
|
|
@@ -321,6 +343,71 @@ function jobsTools() {
|
|
|
321
343
|
throw new Error('finalize requires a "jobId".');
|
|
322
344
|
return (0, types_1.apiCall)(ctx, 'POST', `${base}/jobs/${encodeURIComponent(id)}/finalize`);
|
|
323
345
|
}
|
|
346
|
+
case 'runtime_runs': {
|
|
347
|
+
const id = args.jobId;
|
|
348
|
+
if (id)
|
|
349
|
+
return (0, types_1.apiCall)(ctx, 'GET', `${base}/agent-runtime/runs/${encodeURIComponent(id)}`);
|
|
350
|
+
const railIndex = args.railIndex;
|
|
351
|
+
return (0, types_1.apiCall)(ctx, 'GET', `${base}/agent-runtime/runs${typeof railIndex === 'number' ? `?railIndex=${railIndex}` : ''}`);
|
|
352
|
+
}
|
|
353
|
+
case 'runtime_evidence': {
|
|
354
|
+
const id = args.jobId;
|
|
355
|
+
if (!id)
|
|
356
|
+
throw new Error('runtime_evidence requires a "jobId".');
|
|
357
|
+
return (0, types_1.apiCall)(ctx, 'GET', `${base}/agent-runtime/runs/${encodeURIComponent(id)}/evidence`);
|
|
358
|
+
}
|
|
359
|
+
case 'runtime_resume':
|
|
360
|
+
case 'runtime_recover':
|
|
361
|
+
case 'runtime_approve': {
|
|
362
|
+
const id = args.jobId;
|
|
363
|
+
if (!id)
|
|
364
|
+
throw new Error(`${action} requires a "jobId".`);
|
|
365
|
+
const single = typeof args.stepId === 'string' && args.stepId.trim() ? [args.stepId.trim()] : undefined;
|
|
366
|
+
const body = {};
|
|
367
|
+
if (action === 'runtime_recover') {
|
|
368
|
+
let recover = args.recover ?? single;
|
|
369
|
+
if (!recover?.length) {
|
|
370
|
+
const state = await (0, types_1.apiCall)(ctx, 'GET', `${base}/agent-runtime/runs/${encodeURIComponent(id)}`);
|
|
371
|
+
recover = state.runs?.[0]?.recoverableSteps ?? [];
|
|
372
|
+
}
|
|
373
|
+
if (!recover.length)
|
|
374
|
+
throw new Error('runtime_recover: the run has no recoverable steps — read runtime_runs and use runtime_resume instead.');
|
|
375
|
+
body.recover = recover;
|
|
376
|
+
}
|
|
377
|
+
else if (action === 'runtime_approve') {
|
|
378
|
+
const approve = args.approve ?? single;
|
|
379
|
+
if (!approve?.length)
|
|
380
|
+
throw new Error('runtime_approve requires "stepId" or "approve".');
|
|
381
|
+
body.approve = approve;
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
for (const key of ['approve', 'recover', 'invalidate'])
|
|
385
|
+
if (Array.isArray(args[key]))
|
|
386
|
+
body[key] = args[key];
|
|
387
|
+
if (typeof args.answer === 'string')
|
|
388
|
+
body.answer = args.answer;
|
|
389
|
+
}
|
|
390
|
+
const r = await (0, types_1.apiCall)(ctx, 'POST', `${base}/agent-runtime/runs/${encodeURIComponent(id)}/resume`, body);
|
|
391
|
+
return { ...r, hint: 'Resume accepted (202) — it is not completion. Progress streams into the mission run card; verify with runtime_runs or specrails_watch.' };
|
|
392
|
+
}
|
|
393
|
+
case 'runtime_settle': {
|
|
394
|
+
const id = args.jobId;
|
|
395
|
+
if (!id)
|
|
396
|
+
throw new Error('runtime_settle requires a "jobId".');
|
|
397
|
+
return (0, types_1.apiCall)(ctx, 'POST', `${base}/agent-runtime/runs/${encodeURIComponent(id)}/settle`);
|
|
398
|
+
}
|
|
399
|
+
case 'runtime_dismiss': {
|
|
400
|
+
const id = args.jobId;
|
|
401
|
+
if (!id)
|
|
402
|
+
throw new Error('runtime_dismiss requires a "jobId".');
|
|
403
|
+
return (0, types_1.apiCall)(ctx, 'POST', `${base}/agent-runtime/runs/${encodeURIComponent(id)}/dismiss`);
|
|
404
|
+
}
|
|
405
|
+
case 'runtime_cancel': {
|
|
406
|
+
const id = args.jobId;
|
|
407
|
+
if (!id)
|
|
408
|
+
throw new Error('runtime_cancel requires a "jobId".');
|
|
409
|
+
return (0, types_1.apiCall)(ctx, 'POST', `${base}/agent-runtime/runs/${encodeURIComponent(id)}/cancel`);
|
|
410
|
+
}
|
|
324
411
|
case 'background_start': {
|
|
325
412
|
const command = args.command;
|
|
326
413
|
if (!command || !command.trim())
|
|
@@ -40,7 +40,7 @@ function railsTools() {
|
|
|
40
40
|
description: 'List and configure a project\'s rail launch slots, then launch or stop the AI pipeline for a rail. ' +
|
|
41
41
|
'A rail holds assigned ticket IDs plus a mode/profile/engine/name. Rails are DYNAMIC: create_rail adds a new slot (up to 12), so when every rail is busy or holds other work, create a fresh one and proceed — never wait for a slot. ' +
|
|
42
42
|
'Parallel launches normally run in isolated git worktrees; verify the returned isolated flag. Legacy or non-git runs may use shared project files and have no delivery card. ' +
|
|
43
|
-
'Actions: list (rails + active jobs/loop runs), pr_candidates (read compatible open PR targets for railIndex), review_packet (read durable evidence, verification and acceptance capability by prDeliveryId), create_rail (add a new rail slot; returns its railIndex), set_tickets (assign ticket IDs), set_profile (default agent profile, null=legacy), ' +
|
|
43
|
+
'Actions: list (rails + active jobs/loop runs; every rail carries availability free|busy|pending_decision|on_review — prefer a free rail when proposing a launch card, else create_rail), pr_candidates (read compatible open PR targets for railIndex), review_packet (read durable evidence, verification and acceptance capability by prDeliveryId), create_rail (add a new rail slot; returns its railIndex), set_tickets (assign ticket IDs), set_profile (default agent profile, null=legacy), ' +
|
|
44
44
|
'set_engine (provider override, null=primary), set_name (display label, null clears), ' +
|
|
45
45
|
'launch (ai-spawn — spawns the selected installed provider CLI job(s), including Kimi when installed, that WRITE CODE, RUN TESTS, COMMIT, and INCUR TOKEN COST; returns 202 with jobId/jobIds/loopRunIds), ' +
|
|
46
46
|
'launch_all (ai-spawn — launches EVERY rail that has tickets and no active run/uncontinuable pending PR decision, in parallel, using each rail\'s stored mode/engine/profile; returns per-rail outcomes with skip reasons), ' +
|
|
@@ -301,7 +301,7 @@ function railsTools() {
|
|
|
301
301
|
return {
|
|
302
302
|
...r,
|
|
303
303
|
railLabel,
|
|
304
|
-
hint: `Launch accepted (202) on ${railLabel}, but WORKTREE ISOLATION IS UNAVAILABLE because ${why}. The run proceeds on the SHARED working tree and writes changes DIRECTLY into the user's files — there is NO PR
|
|
304
|
+
hint: `Launch accepted (202) on ${railLabel}, but WORKTREE ISOLATION IS UNAVAILABLE because ${why}. The run proceeds on the SHARED working tree and writes changes DIRECTLY into the user's files — there is NO branch and NO PR phase — the mission shows a RUN card (progress, log, failure + recovery) but never a PR-decision card. Do NOT promise a PR card; tell them the run writes to their files in place, and explain why. ${fix} When it finishes, the spec parks at on_review — the user accepts it by moving it to Done on the board (the changes are already in their files) or reverts the spec's status (which does NOT undo the file changes).`,
|
|
305
305
|
};
|
|
306
306
|
}
|
|
307
307
|
if (r.isolated !== true) {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Mission run cards + the failure trigger (mission-rail-cards) ────────────
|
|
3
|
+
// ONE chokepoint through which every mission-originated run reports to the
|
|
4
|
+
// conversation that launched it:
|
|
5
|
+
// - `postRunCard` / `settleRunCard`: the run card for SHARED-CWD launches
|
|
6
|
+
// (no git repo / no commits — no `rail_pr_deliveries` row, so before this
|
|
7
|
+
// change the mission had NO object for the run at all). The card reuses
|
|
8
|
+
// the PR-decision envelope keyed on a synthetic `run:<runId>` id with
|
|
9
|
+
// `hasDelivery:false`, so cold-load / pinning / rendering are shared.
|
|
10
|
+
// - `notifyMissionRunFailure`: on failed / stalled / provider-limit / stuck
|
|
11
|
+
// outcomes: (a) update the card in place with the failure as TEXT,
|
|
12
|
+
// (b) insert the compact `run-failure` system row, (c) start ONE bounded
|
|
13
|
+
// automatic agent turn with the briefing. Untagged launches → no-op.
|
|
14
|
+
// Never throws — it runs inside settle paths.
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.RUN_CARD_ID_PREFIX = void 0;
|
|
17
|
+
exports.runCardId = runCardId;
|
|
18
|
+
exports.isRunCardId = isRunCardId;
|
|
19
|
+
exports.buildRunCardEnvelope = buildRunCardEnvelope;
|
|
20
|
+
exports.failureForLoopOutcome = failureForLoopOutcome;
|
|
21
|
+
exports.runtimeFromSummary = runtimeFromSummary;
|
|
22
|
+
exports.postRunCard = postRunCard;
|
|
23
|
+
exports.settleRunCard = settleRunCard;
|
|
24
|
+
exports.notifyMissionRunFailure = notifyMissionRunFailure;
|
|
25
|
+
const agent_chat_registry_1 = require("./agent-chat-registry");
|
|
26
|
+
const feature_flags_1 = require("./feature-flags");
|
|
27
|
+
const agent_failure_briefing_1 = require("./agent-failure-briefing");
|
|
28
|
+
exports.RUN_CARD_ID_PREFIX = 'run:';
|
|
29
|
+
function runCardId(runId) { return `${exports.RUN_CARD_ID_PREFIX}${runId}`; }
|
|
30
|
+
function isRunCardId(prDeliveryId) { return prDeliveryId.startsWith(exports.RUN_CARD_ID_PREFIX); }
|
|
31
|
+
/** The envelope of a delivery-less (shared-cwd) run card. */
|
|
32
|
+
function buildRunCardEnvelope(id, state) {
|
|
33
|
+
const now = new Date().toISOString();
|
|
34
|
+
const failed = state.decision === 'implementation_failed';
|
|
35
|
+
return {
|
|
36
|
+
kind: 'pr_decision',
|
|
37
|
+
prDeliveryId: runCardId(id.runId),
|
|
38
|
+
railIndex: id.railIndex,
|
|
39
|
+
projectId: id.projectId,
|
|
40
|
+
baseBranch: '',
|
|
41
|
+
ticketIds: id.ticketIds,
|
|
42
|
+
decision: state.decision,
|
|
43
|
+
implementationOutcome: state.decision === 'building' ? 'running' : state.decision === 'completed' ? 'succeeded' : failed ? 'failed' : 'unknown',
|
|
44
|
+
deliveryOutcome: 'not_started',
|
|
45
|
+
statusCode: state.statusCode ?? (failed ? 'implementation_failed' : state.decision === 'discarded' ? 'cancelled' : null),
|
|
46
|
+
statusDetail: state.statusDetail ?? null,
|
|
47
|
+
deliverySha: null,
|
|
48
|
+
isContinuation: false,
|
|
49
|
+
supersedesDeliveryId: null,
|
|
50
|
+
restoredFromDeliveryId: null,
|
|
51
|
+
operation: null,
|
|
52
|
+
cleanupWarnings: [],
|
|
53
|
+
safetyArchives: [],
|
|
54
|
+
units: id.ticketIds.map((ticketId) => ({ ticketId, branch: '', succeeded: state.decision === 'completed', runId: id.runId })),
|
|
55
|
+
prUrl: null,
|
|
56
|
+
prNumber: null,
|
|
57
|
+
prState: 'none',
|
|
58
|
+
branch: null,
|
|
59
|
+
runIds: [id.runId],
|
|
60
|
+
createdAt: now,
|
|
61
|
+
updatedAt: now,
|
|
62
|
+
hasDelivery: false,
|
|
63
|
+
phase: state.phase,
|
|
64
|
+
railName: id.railName ?? null,
|
|
65
|
+
runtime: state.runtime ?? null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Map a loop outcome (+ stall reason) to a card failure; null when not a failure. */
|
|
69
|
+
function failureForLoopOutcome(outcome, stallReason, detail) {
|
|
70
|
+
if (outcome === 'success')
|
|
71
|
+
return null;
|
|
72
|
+
if (stallReason === 'provider_limit')
|
|
73
|
+
return { code: 'provider_limit', detail: detail ?? null, stepId: null };
|
|
74
|
+
if (outcome === 'stalled')
|
|
75
|
+
return { code: 'stalled', detail: detail ?? (stallReason ? `reason: ${stallReason}` : null), stepId: null };
|
|
76
|
+
if (outcome === 'stopped')
|
|
77
|
+
return { code: 'cancelled', detail: detail ?? null, stepId: null };
|
|
78
|
+
if (outcome === 'blocked')
|
|
79
|
+
return { code: 'blocked', detail: detail ?? (stallReason ?? null), stepId: null };
|
|
80
|
+
return { code: 'implementation_failed', detail: detail ?? (stallReason ?? null), stepId: null };
|
|
81
|
+
}
|
|
82
|
+
function runtimeFromSummary(summary, failure) {
|
|
83
|
+
const status = summary
|
|
84
|
+
? (summary.active ? 'running' : summary.status === 'succeeded' ? 'succeeded' : summary.status === 'cancelled' ? 'cancelled' : summary.status === 'stalled' ? 'stalled' : failure ? 'failed' : 'unknown')
|
|
85
|
+
: failure ? (failure.code === 'stalled' ? 'stalled' : failure.code === 'cancelled' ? 'cancelled' : 'failed') : 'unknown';
|
|
86
|
+
return {
|
|
87
|
+
status,
|
|
88
|
+
currentStep: summary?.nextStep ?? null,
|
|
89
|
+
canResume: summary?.canResume ?? false,
|
|
90
|
+
recoverableSteps: summary?.recoverableSteps ?? [],
|
|
91
|
+
pendingApproval: Boolean(summary?.pendingApproval),
|
|
92
|
+
failure: failure ? { ...failure, stepId: failure.stepId ?? summary?.pendingApproval?.stepId ?? null } : null,
|
|
93
|
+
at: new Date().toISOString(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Post the run card at launch (shared-cwd path). */
|
|
97
|
+
function postRunCard(originConversationId, id) {
|
|
98
|
+
if (!originConversationId || !(0, feature_flags_1.isMissionRailCardsEnabled)())
|
|
99
|
+
return;
|
|
100
|
+
const mgr = (0, agent_chat_registry_1.getAgentChatManager)();
|
|
101
|
+
if (!mgr)
|
|
102
|
+
return;
|
|
103
|
+
try {
|
|
104
|
+
mgr.postPrDecisionCard(originConversationId, buildRunCardEnvelope(id, { phase: 'running', decision: 'building' }));
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.error('[mission-run] postRunCard failed:', err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Settle a shared-cwd run card: success → completed, cancel → discarded; failures go through notifyMissionRunFailure. */
|
|
111
|
+
function settleRunCard(originConversationId, id, outcome, summary = null) {
|
|
112
|
+
if (!originConversationId || !(0, feature_flags_1.isMissionRailCardsEnabled)())
|
|
113
|
+
return;
|
|
114
|
+
const mgr = (0, agent_chat_registry_1.getAgentChatManager)();
|
|
115
|
+
if (!mgr)
|
|
116
|
+
return;
|
|
117
|
+
try {
|
|
118
|
+
const decision = outcome === 'success' ? 'completed' : outcome === 'stopped' ? 'discarded' : 'implementation_failed';
|
|
119
|
+
const runtime = runtimeFromSummary(summary, failureForLoopOutcome(outcome));
|
|
120
|
+
if (outcome === 'success' && !summary)
|
|
121
|
+
runtime.status = 'succeeded';
|
|
122
|
+
mgr.updatePrDecisionCard(originConversationId, buildRunCardEnvelope(id, { phase: 'settled', decision, runtime }));
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
console.error('[mission-run] settleRunCard failed:', err);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const SKIPPED = { card: false, row: null, turn: 'skipped' };
|
|
129
|
+
/**
|
|
130
|
+
* The failure trigger job → card → agent. Resolves to what actually happened
|
|
131
|
+
* so callers/tests can assert; NEVER throws.
|
|
132
|
+
*/
|
|
133
|
+
async function notifyMissionRunFailure(input) {
|
|
134
|
+
if (!input.originConversationId || !(0, feature_flags_1.isMissionRailCardsEnabled)())
|
|
135
|
+
return SKIPPED;
|
|
136
|
+
const mgr = (0, agent_chat_registry_1.getAgentChatManager)();
|
|
137
|
+
if (!mgr)
|
|
138
|
+
return SKIPPED;
|
|
139
|
+
const origin = input.originConversationId;
|
|
140
|
+
const runtime = runtimeFromSummary(input.summary ?? null, input.failure);
|
|
141
|
+
const hasDelivery = input.hasDelivery ?? Boolean(input.envelope && input.envelope.hasDelivery !== false);
|
|
142
|
+
const envelope = input.envelope
|
|
143
|
+
? { ...input.envelope, phase: input.envelope.decision === 'building' ? 'running' : input.envelope.phase ?? 'settled', runtime, railName: input.railName ?? input.envelope.railName ?? null }
|
|
144
|
+
: buildRunCardEnvelope(input, { phase: 'settled', decision: input.failure.code === 'cancelled' ? 'discarded' : 'implementation_failed', runtime, statusCode: input.failure.code, statusDetail: input.failure.detail });
|
|
145
|
+
let card = false;
|
|
146
|
+
try {
|
|
147
|
+
mgr.updatePrDecisionCard(origin, envelope);
|
|
148
|
+
card = true;
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.error('[mission-run] card update failed:', err);
|
|
152
|
+
}
|
|
153
|
+
const rowPayload = {
|
|
154
|
+
kind: 'run-failure', runId: input.runId, railIndex: input.railIndex, projectId: input.projectId,
|
|
155
|
+
prDeliveryId: envelope.prDeliveryId, ticketIds: input.ticketIds,
|
|
156
|
+
code: input.failure.code, detail: input.failure.detail, stepId: runtime.failure?.stepId ?? null, at: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
const row = mgr.postRunFailureRow(origin, rowPayload);
|
|
159
|
+
// A cancelled run is the user's own decision — no briefing turn for it.
|
|
160
|
+
if (input.failure.code === 'cancelled')
|
|
161
|
+
return { card, row, turn: 'skipped' };
|
|
162
|
+
const briefing = (0, agent_failure_briefing_1.buildFailureBriefing)({
|
|
163
|
+
runId: input.runId, railIndex: input.railIndex, railName: input.railName, ticketIds: input.ticketIds, tickets: input.tickets,
|
|
164
|
+
failure: runtime.failure ?? input.failure, outputTail: input.outputTail,
|
|
165
|
+
recovery: { canResume: runtime.canResume, recoverableSteps: runtime.recoverableSteps, pendingApproval: runtime.pendingApproval, pendingQuestion: input.summary?.pendingQuestion?.question ?? null },
|
|
166
|
+
hasDelivery, prDeliveryId: hasDelivery ? envelope.prDeliveryId : null,
|
|
167
|
+
});
|
|
168
|
+
const turn = await mgr.startSystemTurn(origin, briefing, { runId: input.runId, ref: (0, agent_failure_briefing_1.failureBriefingRef)(input.runId, input.railIndex, input.failure.code) });
|
|
169
|
+
return { card, row, turn };
|
|
170
|
+
}
|
|
@@ -80,6 +80,9 @@ const multi_repo_execution_store_1 = require("./multi-repo-execution-store");
|
|
|
80
80
|
const rail_isolation_1 = require("./rail-isolation");
|
|
81
81
|
const rail_pr_store_1 = require("./rail-pr-store");
|
|
82
82
|
const agent_chat_registry_1 = require("./agent-chat-registry");
|
|
83
|
+
const mission_run_notify_1 = require("./mission-run-notify");
|
|
84
|
+
const rails_store_1 = require("./rails-store");
|
|
85
|
+
const agent_runtime_controls_router_1 = require("./agent-runtime-controls-router");
|
|
83
86
|
const rail_merge_orchestrator_1 = require("./rail-merge-orchestrator");
|
|
84
87
|
const loop_executors_1 = require("./loop-executors");
|
|
85
88
|
const worktree_overlay_1 = require("./worktree-overlay");
|
|
@@ -378,6 +381,14 @@ function branchRecords(results) {
|
|
|
378
381
|
* tearing down owned partial allocation. The router refuses shared-cwd
|
|
379
382
|
* fallback for these errors; execution stays bound to a verified worktree.
|
|
380
383
|
*/
|
|
384
|
+
function getRailName(ctx, railIndex) {
|
|
385
|
+
try {
|
|
386
|
+
return (0, rails_store_1.getRail)(ctx.db, railIndex).name ?? null;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
381
392
|
async function launchIsolatedRail(input, io = {}) {
|
|
382
393
|
const { ctx, railIndex, ticketIds, loopId, loopName, loopGraph, provider, model, effort, deciderEngine } = input;
|
|
383
394
|
let expectedRepositoryBaseSha = input.repositoryExecution?.expectedBaseSha;
|
|
@@ -467,6 +478,27 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
467
478
|
if (prDeliveryId)
|
|
468
479
|
syncDeliveryCard(prDeliveryId, verb);
|
|
469
480
|
};
|
|
481
|
+
// mission-rail-cards: the failure trigger for isolated launches. Reads the
|
|
482
|
+
// durable row (origin link + latest envelope) so the notification reflects
|
|
483
|
+
// the exact persisted state; fire-and-forget, never throws into settle.
|
|
484
|
+
const notifyOriginRunFailure = (deliveryId, failure, runId) => {
|
|
485
|
+
const row = (0, rail_pr_store_1.getPrDelivery)(ctx.db, deliveryId);
|
|
486
|
+
if (!row?.origin_conversation_id)
|
|
487
|
+
return;
|
|
488
|
+
const snapshot = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
|
|
489
|
+
const envelope = (0, rail_pr_store_1.toPrDecisionCardEnvelope)(ctx.project.id, snapshot);
|
|
490
|
+
const primaryRunId = runId ?? snapshot.runIds[0] ?? deliveryId;
|
|
491
|
+
void (async () => {
|
|
492
|
+
const summary = runId ? await (0, agent_runtime_controls_router_1.runtimeRunSummary)(ctx, runId).catch(() => null) : null;
|
|
493
|
+
await (0, mission_run_notify_1.notifyMissionRunFailure)({
|
|
494
|
+
originConversationId: row.origin_conversation_id,
|
|
495
|
+
projectId: ctx.project.id, runId: primaryRunId, railIndex, ticketIds: [...ticketIds],
|
|
496
|
+
railName: getRailName(ctx, railIndex),
|
|
497
|
+
failure, envelope, summary, hasDelivery: true, prDeliveryId: deliveryId,
|
|
498
|
+
tickets: ticketIds.map((id) => ({ id, title: ctx.getTicketSpec(id)?.title ?? null })),
|
|
499
|
+
});
|
|
500
|
+
})().catch((err) => console.error('[rail-isolated] mission failure notify failed:', err));
|
|
501
|
+
};
|
|
470
502
|
const closeFailedGeneration = (err) => {
|
|
471
503
|
if (!prDeliveryId || (0, rail_pr_store_1.getPrDelivery)(ctx.db, prDeliveryId)?.decision !== 'building')
|
|
472
504
|
return;
|
|
@@ -484,6 +516,7 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
484
516
|
emitPrDeliveryState(prDeliveryId, 'update');
|
|
485
517
|
}
|
|
486
518
|
catch { /* durable row is authoritative */ }
|
|
519
|
+
notifyOriginRunFailure(prDeliveryId, { code: 'launch_failed', detail: errorDetail(err), stepId: null }, null);
|
|
487
520
|
if (supersededDelivery) {
|
|
488
521
|
try {
|
|
489
522
|
emitPrDeliveryState(supersededDelivery.id, 'update');
|
|
@@ -1544,6 +1577,9 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
1544
1577
|
runId: result.run.runId,
|
|
1545
1578
|
worktreePath: result.run.handle.worktreePath,
|
|
1546
1579
|
loopId,
|
|
1580
|
+
// Programmatic-runtime evidence (host-run verification, reviewer
|
|
1581
|
+
// verdict) lives in the workspace pipeline dir, not the worktree.
|
|
1582
|
+
runtimeDir: path.join(resolveExecution(ctx.project).specrailsDir, 'pipeline', result.run.runId),
|
|
1547
1583
|
})));
|
|
1548
1584
|
}
|
|
1549
1585
|
catch (e) {
|
|
@@ -1600,6 +1636,17 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
1600
1636
|
// Completion driver: refresh the origin conversation's card in place —
|
|
1601
1637
|
// on_review asks the question, discarded informs the outcome.
|
|
1602
1638
|
syncOriginCard('update');
|
|
1639
|
+
// mission-rail-cards: an implementation failure also wakes the origin
|
|
1640
|
+
// mission (failure row + ONE bounded agent turn). Delivery failures
|
|
1641
|
+
// (pr_failed) keep the card's retry controls and stay silent.
|
|
1642
|
+
if (implementationOutcome === 'failed') {
|
|
1643
|
+
const failedUnit = results.find((result) => result.implementationOutcome === 'failed') ?? results[0];
|
|
1644
|
+
notifyOriginRunFailure(prDeliveryId, {
|
|
1645
|
+
code: 'implementation_failed',
|
|
1646
|
+
detail: statusDetail,
|
|
1647
|
+
stepId: null,
|
|
1648
|
+
}, failedUnit?.run.runId ?? null);
|
|
1649
|
+
}
|
|
1603
1650
|
}
|
|
1604
1651
|
return; // legacy merge-back never runs in PR mode
|
|
1605
1652
|
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Rail launch proposal protocol (mission-rail-cards) ──────────────────────
|
|
3
|
+
// The operator agent proposes "assign these specs to a rail and launch" as ONE
|
|
4
|
+
// fenced ```rail-launch JSON block instead of prose. The mission renders it as
|
|
5
|
+
// an editable launch card (client/src/components/agent-chat/AgentRailLaunchCard).
|
|
6
|
+
// This module is the parser HALF shared by server and client — the client copy
|
|
7
|
+
// lives at client/src/lib/rail-launch-draft.ts and MUST stay byte-identical
|
|
8
|
+
// except for its import path (a parity test guards that).
|
|
9
|
+
//
|
|
10
|
+
// Tolerance (spec-draft precedent + snapshot hardening): unknown keys are
|
|
11
|
+
// dropped, a malformed block is reported in `rejected[]` (never silent), a
|
|
12
|
+
// string-aware tolerant JSON repair runs before rejecting, an open trailing
|
|
13
|
+
// fence is cut while streaming (`pending`) and reported `truncated` once the
|
|
14
|
+
// turn settled. Every VALID block is kept in order (one card per block: a
|
|
15
|
+
// batch proposal is N blocks).
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.RAIL_LAUNCH_MODES = exports.RAIL_LAUNCH_PROPOSAL_VERSION = exports.RAIL_LAUNCH_FENCE = void 0;
|
|
18
|
+
exports.coerceRailLaunchProposal = coerceRailLaunchProposal;
|
|
19
|
+
exports.extractRailLaunchProposals = extractRailLaunchProposals;
|
|
20
|
+
exports.looksLikeRailLaunch = looksLikeRailLaunch;
|
|
21
|
+
const json_tolerant_1 = require("./json-tolerant");
|
|
22
|
+
exports.RAIL_LAUNCH_FENCE = 'rail-launch';
|
|
23
|
+
exports.RAIL_LAUNCH_PROPOSAL_VERSION = 1;
|
|
24
|
+
exports.RAIL_LAUNCH_MODES = ['implement', 'batch-implement', 'freestyle', 'loop'];
|
|
25
|
+
const FENCE_RE = /```rail-launch[^\S\n]*\n([\s\S]*?)\n[^\S\n]*```/g;
|
|
26
|
+
const OPEN_FENCE = '```rail-launch';
|
|
27
|
+
const EXCERPT_MAX = 160;
|
|
28
|
+
function isRecord(v) {
|
|
29
|
+
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
30
|
+
}
|
|
31
|
+
function optString(v, max = 200) {
|
|
32
|
+
if (typeof v !== 'string')
|
|
33
|
+
return null;
|
|
34
|
+
const t = v.trim();
|
|
35
|
+
return t ? t.slice(0, max) : null;
|
|
36
|
+
}
|
|
37
|
+
function optInt(v, min = 0) {
|
|
38
|
+
if (typeof v === 'number' && Number.isInteger(v) && v >= min)
|
|
39
|
+
return v;
|
|
40
|
+
if (typeof v === 'string' && /^\d+$/.test(v.trim())) {
|
|
41
|
+
const n = Number(v.trim());
|
|
42
|
+
return n >= min ? n : null;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function coerceTicketIds(v) {
|
|
47
|
+
if (!Array.isArray(v))
|
|
48
|
+
return [];
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const item of v) {
|
|
51
|
+
// Accept 12, "12" and "#12" — models mirror the user's `#12` vocabulary.
|
|
52
|
+
const n = typeof item === 'number' ? item : typeof item === 'string' ? Number(item.trim().replace(/^#/, '')) : NaN;
|
|
53
|
+
if (Number.isInteger(n) && n > 0 && !out.includes(n))
|
|
54
|
+
out.push(n);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function excerpt(raw) {
|
|
59
|
+
const t = raw.replace(/\s+/g, ' ').trim();
|
|
60
|
+
return t.length > EXCERPT_MAX ? `${t.slice(0, EXCERPT_MAX - 1)}…` : t;
|
|
61
|
+
}
|
|
62
|
+
/** Validate ONE parsed payload; returns the proposal or a rejection reason. */
|
|
63
|
+
function coerceRailLaunchProposal(value) {
|
|
64
|
+
if (!isRecord(value))
|
|
65
|
+
return { ok: false, reason: 'not_object' };
|
|
66
|
+
const version = optInt(value.version, 1) ?? optInt(value.blueprintVersion, 1);
|
|
67
|
+
if (version !== null && version !== exports.RAIL_LAUNCH_PROPOSAL_VERSION)
|
|
68
|
+
return { ok: false, reason: 'unsupported_version' };
|
|
69
|
+
const ticketIds = coerceTicketIds(value.ticketIds ?? value.specs ?? value.tickets);
|
|
70
|
+
if (ticketIds.length === 0)
|
|
71
|
+
return { ok: false, reason: 'no_tickets' };
|
|
72
|
+
const modeRaw = optString(value.mode)?.toLowerCase().replace(/_/g, '-') ?? 'implement';
|
|
73
|
+
const mode = modeRaw === 'batch' ? 'batch-implement' : modeRaw;
|
|
74
|
+
if (!exports.RAIL_LAUNCH_MODES.includes(mode))
|
|
75
|
+
return { ok: false, reason: 'invalid_mode' };
|
|
76
|
+
const newRailRaw = value.newRail;
|
|
77
|
+
const newRail = newRailRaw === true
|
|
78
|
+
? { name: null }
|
|
79
|
+
: isRecord(newRailRaw) ? { name: optString(newRailRaw.name, 80) } : null;
|
|
80
|
+
// The agent may say railIndex 1-based by mistake ("Rail 2"); we cannot know,
|
|
81
|
+
// so we trust the number and let the card reconcile against live rails.
|
|
82
|
+
const railIndex = newRail ? null : optInt(value.railIndex, 0);
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
proposal: {
|
|
86
|
+
version: 1,
|
|
87
|
+
railIndex,
|
|
88
|
+
newRail,
|
|
89
|
+
ticketIds,
|
|
90
|
+
mode: mode,
|
|
91
|
+
loopId: optString(value.loopId, 120),
|
|
92
|
+
aiEngine: optString(value.aiEngine ?? value.provider ?? value.engine, 80),
|
|
93
|
+
model: optString(value.model, 120),
|
|
94
|
+
reasoningEffort: optString(value.reasoningEffort ?? value.reasoning_effort ?? value.effort, 20),
|
|
95
|
+
profileName: optString(value.profileName ?? value.profile, 80),
|
|
96
|
+
targetPrNumber: optInt(value.targetPrNumber, 1),
|
|
97
|
+
baseBranch: optString(value.baseBranch, 200),
|
|
98
|
+
railName: optString(value.railName ?? value.name, 80),
|
|
99
|
+
rationale: optString(value.rationale ?? value.reason ?? value.why, 400),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function tidy(body) {
|
|
104
|
+
return body.replace(/\n{3,}/g, '\n\n').trim();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Extract every rail-launch block from assistant content.
|
|
108
|
+
* `streaming` cuts an open fence from the body instead of reporting truncation.
|
|
109
|
+
*/
|
|
110
|
+
function extractRailLaunchProposals(content, streaming = false) {
|
|
111
|
+
const empty = { body: content ?? '', proposals: [], rejected: [], pending: false, truncated: false, repaired: false };
|
|
112
|
+
if (!content || !content.includes(OPEN_FENCE))
|
|
113
|
+
return empty;
|
|
114
|
+
const proposals = [];
|
|
115
|
+
const rejected = [];
|
|
116
|
+
let repaired = false;
|
|
117
|
+
let stripped = '';
|
|
118
|
+
let cursor = 0;
|
|
119
|
+
FENCE_RE.lastIndex = 0;
|
|
120
|
+
let match;
|
|
121
|
+
const consume = (raw) => {
|
|
122
|
+
const parsed = (0, json_tolerant_1.parseJsonTolerant)(raw);
|
|
123
|
+
if (!parsed.ok) {
|
|
124
|
+
rejected.push({ reason: 'invalid_json', excerpt: excerpt(raw) });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (parsed.repaired)
|
|
128
|
+
repaired = true;
|
|
129
|
+
const result = coerceRailLaunchProposal(parsed.value);
|
|
130
|
+
if (result.ok)
|
|
131
|
+
proposals.push(result.proposal);
|
|
132
|
+
else
|
|
133
|
+
rejected.push({ reason: result.reason, excerpt: excerpt(raw) });
|
|
134
|
+
};
|
|
135
|
+
while ((match = FENCE_RE.exec(content)) !== null) {
|
|
136
|
+
stripped += content.slice(cursor, match.index);
|
|
137
|
+
cursor = match.index + match[0].length;
|
|
138
|
+
consume(match[1]);
|
|
139
|
+
}
|
|
140
|
+
stripped += content.slice(cursor);
|
|
141
|
+
let pending = false;
|
|
142
|
+
let truncated = false;
|
|
143
|
+
const openIdx = stripped.indexOf(OPEN_FENCE);
|
|
144
|
+
if (openIdx !== -1) {
|
|
145
|
+
const tail = stripped
|
|
146
|
+
.slice(openIdx + OPEN_FENCE.length)
|
|
147
|
+
.replace(/^[^\S\n]*\n?/, '')
|
|
148
|
+
.replace(/\s*(?:```)?\s*$/, '');
|
|
149
|
+
const parsed = tail.trim() ? (0, json_tolerant_1.parseJsonTolerant)(tail) : null;
|
|
150
|
+
if (parsed && parsed.ok) {
|
|
151
|
+
if (parsed.repaired)
|
|
152
|
+
repaired = true;
|
|
153
|
+
const result = coerceRailLaunchProposal(parsed.value);
|
|
154
|
+
if (result.ok)
|
|
155
|
+
proposals.push(result.proposal);
|
|
156
|
+
else
|
|
157
|
+
rejected.push({ reason: result.reason, excerpt: excerpt(tail) });
|
|
158
|
+
stripped = stripped.slice(0, openIdx);
|
|
159
|
+
}
|
|
160
|
+
else if (streaming) {
|
|
161
|
+
stripped = stripped.slice(0, openIdx);
|
|
162
|
+
pending = true;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Settled and unreadable: cut the raw JSON (never leak protocol text) and
|
|
166
|
+
// say so — the card shows the muted "unreadable" note.
|
|
167
|
+
stripped = stripped.slice(0, openIdx);
|
|
168
|
+
truncated = true;
|
|
169
|
+
rejected.push({ reason: 'invalid_json', excerpt: excerpt(tail) });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { body: tidy(stripped), proposals, rejected, pending, truncated, repaired };
|
|
173
|
+
}
|
|
174
|
+
/** Shape detector for the generic-fence promotion pass (small local models). */
|
|
175
|
+
function looksLikeRailLaunch(value) {
|
|
176
|
+
if (!isRecord(value))
|
|
177
|
+
return false;
|
|
178
|
+
const tickets = value.ticketIds ?? value.specs ?? value.tickets;
|
|
179
|
+
if (!Array.isArray(tickets) || tickets.length === 0)
|
|
180
|
+
return false;
|
|
181
|
+
return 'railIndex' in value || 'newRail' in value || 'mode' in value || 'loopId' in value || 'aiEngine' in value || 'railName' in value;
|
|
182
|
+
}
|
|
@@ -25,9 +25,11 @@ exports.transitionClaimedDecision = transitionClaimedDecision;
|
|
|
25
25
|
exports.claimPrDeliveryOperation = claimPrDeliveryOperation;
|
|
26
26
|
exports.releasePrDeliveryOperation = releasePrDeliveryOperation;
|
|
27
27
|
exports.clearOrphanedPrDeliveryOperations = clearOrphanedPrDeliveryOperations;
|
|
28
|
+
exports.updatePrDeliverySettleEvidence = updatePrDeliverySettleEvidence;
|
|
28
29
|
exports.toPrDeliverySnapshot = toPrDeliverySnapshot;
|
|
29
30
|
exports.toRailPrStateMessage = toRailPrStateMessage;
|
|
30
31
|
exports.toPrDecisionCardEnvelope = toPrDecisionCardEnvelope;
|
|
32
|
+
exports.missionPhaseForDecision = missionPhaseForDecision;
|
|
31
33
|
const ids_1 = require("./ids");
|
|
32
34
|
const multi_repo_execution_store_1 = require("./multi-repo-execution-store");
|
|
33
35
|
/** Defensive per-ticket cap; contract-layer specs run long but bounded. */
|
|
@@ -557,6 +559,10 @@ function parseJsonArray(raw) {
|
|
|
557
559
|
return [];
|
|
558
560
|
}
|
|
559
561
|
}
|
|
562
|
+
/** Rewrite ONLY the settle_evidence column (evidence heal) — no decision change. */
|
|
563
|
+
function updatePrDeliverySettleEvidence(db, id, evidence) {
|
|
564
|
+
return db.prepare('UPDATE rail_pr_deliveries SET settle_evidence = ? WHERE id = ?').run(JSON.stringify(evidence), id).changes > 0;
|
|
565
|
+
}
|
|
560
566
|
function toPrDeliverySnapshot(row) {
|
|
561
567
|
const units = parseJsonArray(row.branches);
|
|
562
568
|
return {
|
|
@@ -669,5 +675,19 @@ function toPrDecisionCardEnvelope(projectId, snap) {
|
|
|
669
675
|
runIds: snap.runIds,
|
|
670
676
|
createdAt: snap.createdAt,
|
|
671
677
|
updatedAt: snap.updatedAt,
|
|
678
|
+
// mission-rail-cards: coarse phase so the card renders the run header
|
|
679
|
+
// before the delivery controls; failure decisions carry a runtime snapshot
|
|
680
|
+
// with the reason as TEXT (the card used to hide statusDetail on discarded).
|
|
681
|
+
phase: missionPhaseForDecision(snap.decision),
|
|
682
|
+
...(snap.decision === 'implementation_failed' || (snap.decision === 'discarded' && snap.statusCode === 'delivery_failed')
|
|
683
|
+
? { runtime: { status: 'failed', currentStep: null, canResume: false, recoverableSteps: [], pendingApproval: false, at: snap.updatedAt ?? new Date().toISOString(), failure: { code: snap.statusCode ?? snap.decision, detail: snap.statusDetail, stepId: null } } }
|
|
684
|
+
: {}),
|
|
672
685
|
};
|
|
673
686
|
}
|
|
687
|
+
function missionPhaseForDecision(decision) {
|
|
688
|
+
if (decision === 'building')
|
|
689
|
+
return 'running';
|
|
690
|
+
if (decision === 'implementation_failed' || decision === 'discarded' || decision === 'completed')
|
|
691
|
+
return 'settled';
|
|
692
|
+
return 'delivery';
|
|
693
|
+
}
|