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.
Files changed (121) hide show
  1. package/client/dist/assets/{ActivityFeedPage-CGq6QhP5.js → ActivityFeedPage-FdZBSmTU.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-C4GZ-kdf.js → AgentBrowserCapture-Dr8kZotA.js} +1 -1
  3. package/client/dist/assets/{AgentModeAnalyticsPane-_IPmL0ZS.js → AgentModeAnalyticsPane-DXQSjYe-.js} +2 -2
  4. package/client/dist/assets/{AgentModeCodePane-B5WG5DOP.js → AgentModeCodePane-DcVV6EBy.js} +2 -2
  5. package/client/dist/assets/AgentModeJobsPane-a9f-g56c.js +1 -0
  6. package/client/dist/assets/AgentsPage-CTv4UVdp.js +87 -0
  7. package/client/dist/assets/{AnalyticsPage-BUMsKtsJ.js → AnalyticsPage-BbXPcz3a.js} +1 -1
  8. package/client/dist/assets/CodePage-CIigb_O0.js +3 -0
  9. package/client/dist/assets/DesktopAnalyticsPage-DZhN5jVh.js +1 -0
  10. package/client/dist/assets/{DocsDialog-ccQETHKB.js → DocsDialog-DzrqPxuj.js} +2 -2
  11. package/client/dist/assets/{DocsPage-DzEgplB3.js → DocsPage-B1AUuk0U.js} +1 -1
  12. package/client/dist/assets/{ExportDropdown-B8bBGopr.js → ExportDropdown-IPutNYvD.js} +1 -1
  13. package/client/dist/assets/InteractiveJobComposer-jg3xE_GX.js +19 -0
  14. package/client/dist/assets/{JobDetailModal-BYXmuTDe.js → JobDetailModal-BS-htwqZ.js} +1 -1
  15. package/client/dist/assets/JobDetailPage-CKYtQV5a.js +1 -0
  16. package/client/dist/assets/JobsPage-DFJm0bw0.js +1 -0
  17. package/client/dist/assets/{LoopBuilderPage-DQE-7Vby.js → LoopBuilderPage-ujkA2U0T.js} +2 -2
  18. package/client/dist/assets/LoopPreviewModal-BYOrnmcp.js +1 -0
  19. package/client/dist/assets/LoopsPage-DRcT1ikP.js +1 -0
  20. package/client/dist/assets/{MinimizedChatsContext-UpnqccPx.js → MinimizedChatsContext-CX1i2kQ8.js} +1 -1
  21. package/client/dist/assets/{PluginsPage-CjgVvmpW.js → PluginsPage-BR9ZTqxR.js} +2 -2
  22. package/client/dist/assets/{ProjectSettingsDialog-D6vBCoay.js → ProjectSettingsDialog-SAfCqXzC.js} +1 -1
  23. package/client/dist/assets/RepositoryDeliveries-DqmlG0Zs.js +5 -0
  24. package/client/dist/assets/{RepositoryScopeSelector-0Gz_fR1E.js → RepositoryScopeSelector-DG8KKPrm.js} +1 -1
  25. package/client/dist/assets/ReviewPacketPage-BoqahYNC.js +4 -0
  26. package/client/dist/assets/TemplatePreviewModal-CK5OoImj.js +1 -0
  27. package/client/dist/assets/TicketDetailModalContext-D2MEhRwJ.js +14 -0
  28. package/client/dist/assets/{Trans-9pHTy2fH.js → Trans-BLljsfHR.js} +1 -1
  29. package/client/dist/assets/agent-B76BhAyE.js +1 -0
  30. package/client/dist/assets/agent-BII69Nc0.js +1 -0
  31. package/client/dist/assets/agent-BIO4q6b0.js +1 -0
  32. package/client/dist/assets/agent-ButS6kwl.js +1 -0
  33. package/client/dist/assets/agent-CVeMLIu4.js +1 -0
  34. package/client/dist/assets/agent-CYFBDwFD.js +1 -0
  35. package/client/dist/assets/agent-D3jkMipn.js +1 -0
  36. package/client/dist/assets/agent-DSrflYFL.js +1 -0
  37. package/client/dist/assets/{formatDistanceToNow-w_2U5pm0.js → formatDistanceToNow-CbQZkD8M.js} +1 -1
  38. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BRymg1Kk.js → getTimezoneOffsetInMilliseconds-pBVK3rJC.js} +1 -1
  39. package/client/dist/assets/index-CjAPu3wb.js +80 -0
  40. package/client/dist/assets/index-UDzndadF.css +2 -0
  41. package/client/dist/assets/{jira-api-BqHbSdxK.js → jira-api-f-sYCIuX.js} +1 -1
  42. package/client/dist/assets/packet-BTRySHoQ.js +1 -0
  43. package/client/dist/assets/packet-BsRimSfZ.js +1 -0
  44. package/client/dist/assets/packet-C0pcMd6x.js +1 -0
  45. package/client/dist/assets/packet-C8qzvgrZ.js +1 -0
  46. package/client/dist/assets/packet-DF-iwqqY.js +1 -0
  47. package/client/dist/assets/packet-DQ_GnT2b.js +1 -0
  48. package/client/dist/assets/packet-Nb8BLI5h.js +1 -0
  49. package/client/dist/assets/packet-r7J14m89.js +1 -0
  50. package/client/dist/assets/{project-repositories-BsCs1PPA.js → project-repositories-DAjJbFhm.js} +1 -1
  51. package/client/dist/assets/{spending-hcgxHP4O.js → spending-DJBbdJyD.js} +1 -1
  52. package/client/dist/assets/{useDesktop-CAisjKWC.js → useDesktop-BJ8IRBRt.js} +1 -1
  53. package/client/dist/assets/useRuntimeRuns-Ch1KUc7k.js +1 -0
  54. package/client/dist/assets/{useSharedWebSocket-BHIfW4QT.js → useSharedWebSocket-BN3Lmehi.js} +2 -2
  55. package/client/dist/index.html +17 -17
  56. package/docs/guide/de/integrations/6-agent-chat.md +8 -0
  57. package/docs/guide/en/integrations/6-agent-chat.md +8 -0
  58. package/docs/guide/es/integrations/6-agent-chat.md +8 -0
  59. package/docs/guide/fr/integrations/6-agent-chat.md +8 -0
  60. package/docs/guide/it/integrations/6-agent-chat.md +8 -0
  61. package/docs/guide/ja/integrations/6-agent-chat.md +8 -0
  62. package/docs/guide/pt/integrations/6-agent-chat.md +8 -0
  63. package/docs/guide/zh/integrations/6-agent-chat.md +8 -0
  64. package/docs/internals/mission-rail-cards.md +130 -0
  65. package/docs/internals/review-packet.md +16 -3
  66. package/docs/mcp.md +24 -1
  67. package/package.json +1 -1
  68. package/server/dist/agent-chat-manager.js +97 -2
  69. package/server/dist/agent-chat-router.js +57 -1
  70. package/server/dist/agent-context-resolver.js +15 -0
  71. package/server/dist/agent-cwd-manager.js +1 -1
  72. package/server/dist/agent-failure-briefing.js +90 -0
  73. package/server/dist/agent-fence-promotion.js +8 -1
  74. package/server/dist/agent-operator-prompt.js +69 -1
  75. package/server/dist/agent-runtime-controls-router.js +17 -0
  76. package/server/dist/agent-store.js +40 -0
  77. package/server/dist/delivery-evidence.js +159 -0
  78. package/server/dist/desktop-db.js +11 -0
  79. package/server/dist/feature-flags.js +20 -0
  80. package/server/dist/mcp/guide.js +7 -0
  81. package/server/dist/mcp/tools/jobs.js +91 -4
  82. package/server/dist/mcp/tools/rails.js +2 -2
  83. package/server/dist/mission-run-notify.js +170 -0
  84. package/server/dist/rail-isolated-launch.js +47 -0
  85. package/server/dist/rail-launch-parser.js +182 -0
  86. package/server/dist/rail-pr-store.js +20 -0
  87. package/server/dist/rails-router.js +98 -5
  88. package/server/dist/review-packet.js +35 -2
  89. package/client/dist/assets/AgentModeJobsPane-w1N_HIXh.js +0 -1
  90. package/client/dist/assets/AgentsPage-CmwnnT46.js +0 -87
  91. package/client/dist/assets/CodePage-PjgywJPS.js +0 -3
  92. package/client/dist/assets/DesktopAnalyticsPage-BXY1pH4y.js +0 -1
  93. package/client/dist/assets/InteractiveJobComposer-CEwGkJ8F.js +0 -19
  94. package/client/dist/assets/JobDetailPage-CWp6MwJj.js +0 -1
  95. package/client/dist/assets/JobsPage-D1O1uyG8.js +0 -1
  96. package/client/dist/assets/LoopPreviewModal-Bk7zIXD5.js +0 -1
  97. package/client/dist/assets/LoopsPage-CqG7mx56.js +0 -1
  98. package/client/dist/assets/RepositoryDeliveries-C36ipKp_.js +0 -5
  99. package/client/dist/assets/ReviewPacketPage-Dr9RYs7q.js +0 -1
  100. package/client/dist/assets/TemplatePreviewModal-sCMHYFJo.js +0 -1
  101. package/client/dist/assets/TicketDetailModalContext-C5GgJlEt.js +0 -14
  102. package/client/dist/assets/agent-BdT0SB1D.js +0 -1
  103. package/client/dist/assets/agent-Bh9K7iEt.js +0 -1
  104. package/client/dist/assets/agent-C4ibdFk9.js +0 -1
  105. package/client/dist/assets/agent-CSFY22Zo.js +0 -1
  106. package/client/dist/assets/agent-DAnqxUn8.js +0 -1
  107. package/client/dist/assets/agent-DTQiu0Mi.js +0 -1
  108. package/client/dist/assets/agent-T54WuR_Y.js +0 -1
  109. package/client/dist/assets/agent-i75WDZOA.js +0 -1
  110. package/client/dist/assets/brain-DI9lHkKY.js +0 -1
  111. package/client/dist/assets/format-duration-NJVPOPBG.js +0 -1
  112. package/client/dist/assets/index-D0ScFZvR.css +0 -2
  113. package/client/dist/assets/index-DORCrgbI.js +0 -77
  114. package/client/dist/assets/packet-BNW8ODQw.js +0 -1
  115. package/client/dist/assets/packet-CXYvfdjy.js +0 -1
  116. package/client/dist/assets/packet-CtIlB8YE.js +0 -1
  117. package/client/dist/assets/packet-DA1SJkMH.js +0 -1
  118. package/client/dist/assets/packet-DYcWfBip.js +0 -1
  119. package/client/dist/assets/packet-DseX9yKC.js +0 -1
  120. package/client/dist/assets/packet-_m5aOy_d.js +0 -1
  121. package/client/dist/assets/packet-qOemUAm3.js +0 -1
@@ -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-decision/implementation card and NO branch. Do NOT tell the user to look for 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).`,
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
+ }