specrails-desktop 2.59.2 → 2.59.3

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 (87) hide show
  1. package/client/dist/assets/{ActivityFeedPage-vAY9C71W.js → ActivityFeedPage-BGEayjTY.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-CpwjGXQm.js → AgentBrowserCapture-Qvg4y5Tr.js} +1 -1
  3. package/client/dist/assets/{AgentModeAnalyticsPane-BHeoJVxQ.js → AgentModeAnalyticsPane-CR7aFeLJ.js} +2 -2
  4. package/client/dist/assets/{AgentModeCodePane-B0Nf3YkN.js → AgentModeCodePane-gAd88Gng.js} +2 -2
  5. package/client/dist/assets/{AgentModeJobsPane-CpbIItY_.js → AgentModeJobsPane-EjCDuTow.js} +1 -1
  6. package/client/dist/assets/{AgentsPage-BVJAFXn3.js → AgentsPage-CRHHcp1V.js} +1 -1
  7. package/client/dist/assets/{AnalyticsPage-DDWees-K.js → AnalyticsPage-a0b1fNEl.js} +1 -1
  8. package/client/dist/assets/{CodePage-CzV5tkvS.js → CodePage-BmV1m7l3.js} +1 -1
  9. package/client/dist/assets/{DesktopAnalyticsPage-DfRHNK7i.js → DesktopAnalyticsPage-B87WmXOQ.js} +1 -1
  10. package/client/dist/assets/{DocsDialog-BxduBYbh.js → DocsDialog-Co0O672q.js} +1 -1
  11. package/client/dist/assets/{DocsPage-DOVeRJn_.js → DocsPage-DxGI6A2T.js} +1 -1
  12. package/client/dist/assets/{ExportDropdown-BnKVV8UK.js → ExportDropdown-1Ce031vB.js} +1 -1
  13. package/client/dist/assets/{InteractiveJobComposer-Da9dvM-0.js → InteractiveJobComposer-DkqR77pO.js} +2 -2
  14. package/client/dist/assets/JobDetailModal-DVLDVgzz.js +1 -0
  15. package/client/dist/assets/JobDetailPage-RcP9N-D-.js +1 -0
  16. package/client/dist/assets/{JobsPage-0vdlbNik.js → JobsPage-R9M4xQnp.js} +1 -1
  17. package/client/dist/assets/{LoopBuilderPage-DCUHozXh.js → LoopBuilderPage-C4S-rkWp.js} +1 -1
  18. package/client/dist/assets/{LoopPreviewModal-Df-S8NGf.js → LoopPreviewModal-DA2JeKWX.js} +1 -1
  19. package/client/dist/assets/{LoopWindowSurface-sdb7QbSo.js → LoopWindowSurface-BkkeD0hx.js} +1 -1
  20. package/client/dist/assets/{LoopsPage-CDfCC9Px.js → LoopsPage-BHv--iI7.js} +1 -1
  21. package/client/dist/assets/{MinimizedChatsContext-DKlPf54Y.js → MinimizedChatsContext-Bc9xHByW.js} +1 -1
  22. package/client/dist/assets/PluginsPage-BEy62gxK.js +2 -0
  23. package/client/dist/assets/{ProjectSettingsDialog-DOkjT7xv.js → ProjectSettingsDialog-DDQ_f94T.js} +1 -1
  24. package/client/dist/assets/{RepositoryDeliveries-CJVpOUpZ.js → RepositoryDeliveries-DIWDFLRM.js} +1 -1
  25. package/client/dist/assets/{RepositoryScopeSelector-BeM-2gIZ.js → RepositoryScopeSelector-D3PlHIM-.js} +1 -1
  26. package/client/dist/assets/{ReviewPacketPage-EBCim2w_.js → ReviewPacketPage-BXdPwmdE.js} +1 -1
  27. package/client/dist/assets/{TemplatePreviewModal-1-92UGwN.js → TemplatePreviewModal-C7-G6lvI.js} +1 -1
  28. package/client/dist/assets/{TicketDetailModalContext-CkVXuHxU.js → TicketDetailModalContext-gGUv7gsQ.js} +1 -1
  29. package/client/dist/assets/{Trans-CzhPjnjB.js → Trans-Br_OIUKP.js} +1 -1
  30. package/client/dist/assets/{formatDistanceToNow-DlQaXc7c.js → formatDistanceToNow-Q7QPRFV4.js} +1 -1
  31. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-DxUFGcVq.js → getTimezoneOffsetInMilliseconds-Bq0CXBaJ.js} +1 -1
  32. package/client/dist/assets/{index-Czov72g7.js → index-DVkWHOR3.js} +7 -7
  33. package/client/dist/assets/jira-B6OYw4CJ.js +1 -0
  34. package/client/dist/assets/jira-BTF6RaOe.js +1 -0
  35. package/client/dist/assets/jira-CEetIB2q.js +1 -0
  36. package/client/dist/assets/jira-CIzsZLw5.js +1 -0
  37. package/client/dist/assets/{jira-BOoaUXbb.js → jira-Cq6yvN4q.js} +1 -1
  38. package/client/dist/assets/jira-Daw9tmpI.js +1 -0
  39. package/client/dist/assets/jira-FXr7u2lh.js +1 -0
  40. package/client/dist/assets/jira-LaNQn7yl.js +1 -0
  41. package/client/dist/assets/{jira-api-CbJvbRYx.js → jira-api-C2Tzc362.js} +1 -1
  42. package/client/dist/assets/{jobs-DZAbh1m2.js → jobs-ARwD85B9.js} +1 -1
  43. package/client/dist/assets/{jobs-BD47Ov11.js → jobs-BMF1w4WT.js} +1 -1
  44. package/client/dist/assets/{jobs-tWjcDF_6.js → jobs-CBfzGLwm.js} +1 -1
  45. package/client/dist/assets/{jobs-CSrRx5lD.js → jobs-CPjiluBY.js} +1 -1
  46. package/client/dist/assets/{jobs-epP7gt8W.js → jobs-CQQd58pY.js} +1 -1
  47. package/client/dist/assets/{jobs-1G3wodhl.js → jobs-De3EqaO0.js} +1 -1
  48. package/client/dist/assets/{jobs-BLOoVRRl.js → jobs-Du9-Ei9a.js} +1 -1
  49. package/client/dist/assets/{jobs-B4pu8TdF.js → jobs-qMHbnxwC.js} +1 -1
  50. package/client/dist/assets/{project-repositories-BkBD3rxx.js → project-repositories-DZKmieH3.js} +1 -1
  51. package/client/dist/assets/{spending-CYotH-9N.js → spending-EXaboRU0.js} +1 -1
  52. package/client/dist/assets/{useDesktop-DpxVGfUC.js → useDesktop-ChSmmJfc.js} +1 -1
  53. package/client/dist/assets/{useRuntimeRuns-CfGRbhr9.js → useRuntimeRuns-w9-5OzZl.js} +1 -1
  54. package/client/dist/assets/{useSharedWebSocket-C17TGqT8.js → useSharedWebSocket-g0nTHfTT.js} +2 -2
  55. package/client/dist/index.html +16 -16
  56. package/docs/internals/configuration.md +45 -0
  57. package/docs/internals/loop-step-log-explorer.md +27 -0
  58. package/docs/internals/mission-rail-cards.md +7 -0
  59. package/docs/internals/safe-pr-review-flow.md +9 -0
  60. package/docs/internals/source-map.md +5 -1
  61. package/docs/jira-integration-plan.md +7 -4
  62. package/package.json +1 -1
  63. package/server/dist/core-execution.js +5 -18
  64. package/server/dist/jira/jira-db.js +33 -6
  65. package/server/dist/jira/jira-materializer.js +11 -9
  66. package/server/dist/jira/jira-status-resolver.js +64 -22
  67. package/server/dist/jira/jira-sync-manager.js +40 -1
  68. package/server/dist/jira-router.js +8 -41
  69. package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +15 -67
  70. package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls.js +7 -4
  71. package/server/dist/modules/agent-runtime/runtime/agent-runtime-verification-log.js +252 -0
  72. package/server/dist/modules/delivery/runtime/rail-relaunch.js +10 -2
  73. package/server/dist/modules/loops/runtime/loop-run-manager.js +5 -3
  74. package/server/dist/path-resolver.js +41 -20
  75. package/server/dist/project-env.js +22 -6
  76. package/server/dist/util/checkout-path.js +22 -0
  77. package/server/dist/worktree-node-modules.js +70 -34
  78. package/client/dist/assets/JobDetailModal-DhwKbZGe.js +0 -1
  79. package/client/dist/assets/JobDetailPage-MtPE-E_1.js +0 -1
  80. package/client/dist/assets/PluginsPage-iL0G3rQQ.js +0 -2
  81. package/client/dist/assets/jira-BNZu8f9O.js +0 -1
  82. package/client/dist/assets/jira-BYVN4L2u.js +0 -1
  83. package/client/dist/assets/jira-CKJg3HZd.js +0 -1
  84. package/client/dist/assets/jira-DDDsZ4ub.js +0 -1
  85. package/client/dist/assets/jira-DXwYjVE4.js +0 -1
  86. package/client/dist/assets/jira-Dn6U2nRW.js +0 -1
  87. package/client/dist/assets/jira-j7hVolR5.js +0 -1
@@ -999,6 +999,11 @@ class JiraSyncManager {
999
999
  return;
1000
1000
  let changed = false;
1001
1001
  while (!this.authPaused) {
1002
+ // A failed transition may have acquired a newer intention while its HTTP
1003
+ // request was in flight. Reconcile before each claim, including legacy
1004
+ // manual retries whose newer transition has already completed.
1005
+ if ((0, jira_db_1.reconcileSupersededTransitions)(this.db) > 0)
1006
+ changed = true;
1002
1007
  const batch = (0, jira_db_1.claimDrainable)(this.db, MAX_DRAIN_BATCH);
1003
1008
  if (batch.length === 0)
1004
1009
  break;
@@ -1099,6 +1104,7 @@ class JiraSyncManager {
1099
1104
  // the SAME category (e.g. In Progress → In Review, both indeterminate)
1100
1105
  // while still noop-ing when the issue already sits on the target.
1101
1106
  currentStatusName: issue.data.fields.status?.name,
1107
+ currentStatusId: issue.data.fields.status?.id,
1102
1108
  explicitTarget,
1103
1109
  getTransitions: async () => {
1104
1110
  const res = await client.getTransitions(payload.jiraIssueId);
@@ -1231,6 +1237,7 @@ class JiraSyncManager {
1231
1237
  }
1232
1238
  }
1233
1239
  broadcastOutboxState() {
1240
+ (0, jira_db_1.reconcileSupersededTransitions)(this.db);
1234
1241
  const counts = (0, jira_db_1.countOutboxByState)(this.db);
1235
1242
  this.broadcast({ type: 'jira.outbox_changed', projectId: this.projectId, pending: counts.pending + counts.inflight, dead: counts.dead });
1236
1243
  }
@@ -1309,11 +1316,43 @@ class JiraSyncManager {
1309
1316
  return (0, jira_db_1.listLinks)(this.db);
1310
1317
  }
1311
1318
  listOutbox(state) {
1312
- return (0, jira_db_1.listOutbox)(this.db, state ? { state } : {});
1319
+ if ((0, jira_db_1.reconcileSupersededTransitions)(this.db) > 0)
1320
+ this.broadcastOutboxState();
1321
+ const links = new Map((0, jira_db_1.listLinks)(this.db).map((link) => [link.jiraIssueId, link.jiraKey]));
1322
+ const statusMap = (0, jira_db_1.getConnection)(this.db, this.projectId)?.statusMap;
1323
+ const logicalStates = ['todo', 'in_progress', 'on_review', 'done', 'cancelled'];
1324
+ return (0, jira_db_1.listOutbox)(this.db, state ? { state } : {}).map((row) => {
1325
+ let logicalState = null;
1326
+ let targetStatus = null;
1327
+ if (row.opType === 'transition') {
1328
+ try {
1329
+ const payload = JSON.parse(row.payload);
1330
+ if (payload && typeof payload === 'object') {
1331
+ if (logicalStates.includes(payload.logicalState))
1332
+ logicalState = payload.logicalState;
1333
+ if (typeof payload.targetStatus === 'string' && payload.targetStatus.trim())
1334
+ targetStatus = payload.targetStatus;
1335
+ }
1336
+ }
1337
+ catch {
1338
+ // Historical/corrupt payloads remain visible for diagnosis.
1339
+ }
1340
+ targetStatus ??= logicalState ? statusMap?.[logicalState] ?? null : null;
1341
+ }
1342
+ return { ...row, jiraKey: links.get(row.jiraIssueId) ?? null, logicalState, targetStatus };
1343
+ });
1313
1344
  }
1314
1345
  outboxCounts() {
1346
+ if ((0, jira_db_1.reconcileSupersededTransitions)(this.db) > 0)
1347
+ this.broadcastOutboxState();
1315
1348
  return (0, jira_db_1.countOutboxByState)(this.db);
1316
1349
  }
1350
+ retryOutbox(id) {
1351
+ const disposition = (0, jira_db_1.retryDeadOutbox)(this.db, id);
1352
+ if (disposition)
1353
+ this.broadcastOutboxState();
1354
+ return disposition;
1355
+ }
1317
1356
  }
1318
1357
  exports.JiraSyncManager = JiraSyncManager;
1319
1358
  // ─── Error envelope for thrown client failures inside the walk ─────────────────
@@ -1,37 +1,4 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.createJiraRouter = createJiraRouter;
37
4
  const express_1 = require("express");
@@ -201,26 +168,26 @@ function createJiraRouter() {
201
168
  router.get('/outbox', (req, res) => {
202
169
  const c = ctx(req);
203
170
  const state = req.query.state;
204
- const valid = ['pending', 'inflight', 'done', 'dead'];
171
+ const valid = ['pending', 'inflight', 'done', 'dead', 'superseded'];
205
172
  const filter = valid.includes(state) ? state : undefined;
206
173
  res.json({ ops: c.jiraSyncManager.listOutbox(filter), counts: c.jiraSyncManager.outboxCounts() });
207
174
  });
208
175
  // POST /outbox/:id/retry — re-queue a dead-lettered op for a manual retry.
209
176
  router.post('/outbox/:id/retry', async (req, res) => {
210
177
  const c = ctx(req);
211
- const id = parseInt(req.params.id, 10);
212
- if (Number.isNaN(id)) {
178
+ const id = Number(req.params.id);
179
+ if (!Number.isSafeInteger(id) || id <= 0) {
213
180
  res.status(400).json({ error: 'Invalid op id' });
214
181
  return;
215
182
  }
216
- const { retryDeadOutbox } = await Promise.resolve().then(() => __importStar(require('./jira/jira-db')));
217
- const ok = retryDeadOutbox(c.db, id);
218
- if (!ok) {
183
+ const disposition = c.jiraSyncManager.retryOutbox(id);
184
+ if (!disposition) {
219
185
  res.status(404).json({ error: 'Op not found or not in dead state' });
220
186
  return;
221
187
  }
222
- void c.jiraSyncManager.drainOnce().catch(() => undefined);
223
- res.json({ ok: true });
188
+ if (disposition === 'pending')
189
+ void c.jiraSyncManager.drainOnce().catch(() => undefined);
190
+ res.json({ ok: true, disposition });
224
191
  });
225
192
  // POST /specs — Add Spec when the project source is Jira: create the issue in
226
193
  // Jira and materialize it locally. Keeps the generic local ticket-create path
@@ -10,7 +10,7 @@ exports.runAgentRuntimeControl = runAgentRuntimeControl;
10
10
  const agent_runtime_verification_suggestions_1 = require("./agent-runtime-verification-suggestions");
11
11
  const agent_runtime_history_1 = require("./agent-runtime-history");
12
12
  const agent_runtime_repositories_1 = require("./agent-runtime-repositories");
13
- const node_util_1 = require("node:util");
13
+ const agent_runtime_verification_log_1 = require("./agent-runtime-verification-log");
14
14
  const agent_runtime_effective_config_1 = require("./agent-runtime-effective-config");
15
15
  const node_child_process_1 = require("node:child_process");
16
16
  const node_crypto_1 = require("node:crypto");
@@ -93,66 +93,6 @@ function requiresConfiguredChecks(value) {
93
93
  return Object.values(body.nodes ?? {}).some(node => node.kind === 'verify' && node.params?.commands === 'configured' && !node.params.additionalCommandsFrom && node.params.unverified !== true)
94
94
  || Object.values(body.components ?? {}).some(requiresConfiguredChecks);
95
95
  }
96
- /** Keep TAP failure diagnostics and totals in the readable log. The unfiltered
97
- * event stream remains available as runtime evidence. State is invocation-local. */
98
- function verificationLogFilter() {
99
- const streams = new Map();
100
- return text => (0, node_util_1.stripVTControlCharacters)(text).split(/(?<=\n)/).flatMap(line => {
101
- const match = line.match(/^(\[verification [^\]]+\] )?(.*?)(?:\n)?$/);
102
- if (!match)
103
- return [line];
104
- const key = match[1] ?? '', body = match[2].trim();
105
- const state = streams.get(key) ?? { tap: false, failed: false, yaml: false, inputLines: null };
106
- streams.set(key, state);
107
- if (/AssertionError\b.*\bInput:\s*$/.test(body))
108
- state.inputLines = 0;
109
- else if (state.inputLines !== null) {
110
- // Node prints assert.match's entire source input as quoted concatenation
111
- // lines. Bound only that block; stacks, expected values and other streams
112
- // remain visible and original events remain intact.
113
- if (/^["'`]/.test(body)) {
114
- state.inputLines++;
115
- if (state.inputLines > 2)
116
- return state.inputLines === 3 ? [key + '[Assertion input shortened; inspect raw evidence]\n'] : [];
117
- }
118
- else if (!body)
119
- return [];
120
- else
121
- state.inputLines = null;
122
- }
123
- if (/^[✔✓]\s/.test(body))
124
- return [];
125
- if (/^[✖✗]\s/.test(body)) {
126
- state.failed = true;
127
- return [line];
128
- }
129
- if (/^TAP version |^# Subtest:/.test(body)) {
130
- state.tap = true;
131
- return [];
132
- }
133
- if (/^(?:not )?ok \d+\b/.test(body)) {
134
- state.tap = true;
135
- state.failed = body.startsWith('not ok');
136
- state.yaml = false;
137
- return state.failed ? [line] : [];
138
- }
139
- if (!state.tap)
140
- return [line];
141
- if (body === '---') {
142
- state.yaml = true;
143
- return state.failed ? [line] : [];
144
- }
145
- if (body === '...') {
146
- state.yaml = false;
147
- return state.failed ? [line] : [];
148
- }
149
- if (state.yaml)
150
- return state.failed ? [line] : [];
151
- if (/^1\.\.\d+$/.test(body) || !body)
152
- return [];
153
- return [line];
154
- }).map(line => line.length > 2_000 ? line.slice(0, 1_900) + '\n[Long diagnostic line shortened; inspect raw evidence]\n' : line).join('');
155
- }
156
96
  exports.RUNTIME_HOST_ENV_KEYS = [
157
97
  'SPECRAILS_REPO_DIR', 'SPECRAILS_GIT_AUTO', 'SPECRAILS_REPO_MAP_PATH',
158
98
  'SPECRAILS_EXECUTION_CONTEXT', 'SPECRAILS_EXECUTION_MANIFEST', 'SPECRAILS_REPOSITORIES',
@@ -356,7 +296,13 @@ async function runAgentRuntimeInvocation(options) {
356
296
  callback?.();
357
297
  }
358
298
  catch { /* Logging cannot replay a workflow. */ } };
359
- const readableVerification = verificationLogFilter();
299
+ const readableVerification = (0, agent_runtime_verification_log_1.createVerificationLogProjection)();
300
+ const readableLines = (lines) => {
301
+ for (const line of lines)
302
+ observe(() => line.attemptId
303
+ ? options.onLine?.(line.text, 'stdout', { attemptId: line.attemptId })
304
+ : options.onLine?.(line.text));
305
+ };
360
306
  const lines = (0, node_readline_1.createInterface)({ input: child.stdout });
361
307
  lines.on('line', line => {
362
308
  if (line.length > 2_000_000) {
@@ -388,6 +334,10 @@ async function runAgentRuntimeInvocation(options) {
388
334
  (0, win_spawn_1.treeKillSafe)(child.pid, 'SIGKILL');
389
335
  return;
390
336
  }
337
+ const boundary = event.type === 'workflow-event' || event.type === 'runtime-result';
338
+ // Flush buffered output before the projection advances the current step.
339
+ if (boundary)
340
+ readableLines(readableVerification.project(event));
391
341
  try {
392
342
  options.onRuntimeEvent?.(event);
393
343
  }
@@ -400,6 +350,8 @@ async function runAgentRuntimeInvocation(options) {
400
350
  if (event.type === 'runtime-graph')
401
351
  graph = event;
402
352
  observe(() => options.onRawLine?.(event.repositories ? JSON.stringify(event) : line));
353
+ if (!boundary)
354
+ readableLines(readableVerification.project(event));
403
355
  if (event.type === 'runtime-result') {
404
356
  if (result)
405
357
  invalidProtocol = true;
@@ -435,11 +387,6 @@ async function runAgentRuntimeInvocation(options) {
435
387
  observe(() => options.onLine?.(`[${role}]${scope} ${payload.tool}${payload.detail ? ' ' + payload.detail : ''}\n`));
436
388
  }
437
389
  }
438
- else if (event.type === 'verification-output' && typeof event.text === 'string') {
439
- const readable = readableVerification(String(event.text));
440
- if (readable)
441
- observe(() => options.onLine?.(readable));
442
- }
443
390
  else if (event.type === 'span') {
444
391
  // Trace spans are telemetry; the raw line is already recorded for diagnostics.
445
392
  }
@@ -461,6 +408,7 @@ async function runAgentRuntimeInvocation(options) {
461
408
  if (timer)
462
409
  clearTimeout(timer);
463
410
  lines.close();
411
+ readableLines(readableVerification.flush());
464
412
  const usage = result?.invocationUsage;
465
413
  const known = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
466
414
  const cost = known(usage?.costUsd), tokensIn = known(usage?.inputTokens), tokensOut = known(usage?.outputTokens);
@@ -26,6 +26,7 @@ const agent_runtime_bridge_1 = require("./agent-runtime-bridge");
26
26
  const core_node_runtime_1 = require("../../../core-node-runtime");
27
27
  const win_spawn_1 = require("../../../util/win-spawn");
28
28
  const workspace_resolution_1 = require("../../../workspace-resolution");
29
+ const project_env_1 = require("../../../project-env");
29
30
  const loop_runs_store_1 = require("../../loops/runtime/loop-runs-store");
30
31
  const multi_repo_execution_store_1 = require("../../delivery/runtime/multi-repo-execution-store");
31
32
  const db_1 = require("../../../db");
@@ -309,7 +310,7 @@ class AgentRuntimeControls {
309
310
  const host = JSON.parse(node_fs_1.default.readFileSync(hostFile, 'utf8'));
310
311
  if (host.schemaVersion !== 1 || typeof host.cwd !== 'string' || ![frozen.backlogRoot, ...frozen.repositories.map((repository) => repository.path)].includes(host.cwd) || !host.env || typeof host.env !== 'object' || Array.isArray(host.env) || Object.entries(host.env).some(([key, value]) => !agent_runtime_bridge_1.RUNTIME_HOST_ENV_KEYS.includes(key) || typeof value !== 'string'))
311
312
  throw new RuntimeControlError(409, 'runtime_host_invalid', 'The saved runtime host settings are invalid');
312
- const env = { ...(0, workspace_resolution_1.resolveLoopBaseEnv)(this.ctx.project) };
313
+ const env = { ...(0, workspace_resolution_1.resolveLoopBaseEnv)(this.ctx.project, undefined, (0, project_env_1.applyWorktreeEnvPassthrough)(this.ctx.db, process.env)) };
313
314
  for (const key of agent_runtime_bridge_1.RUNTIME_HOST_ENV_KEYS)
314
315
  delete env[key];
315
316
  Object.assign(env, host.env, { SPECRAILS_EXECUTION_CONTEXT: file });
@@ -541,15 +542,17 @@ class AgentRuntimeControls {
541
542
  loopDurationBaseline: previous.total_duration_ms, completedDurationMs: 0,
542
543
  iterationCount: previous.iteration_count, activeTurnStartedAtMs: startedAtMs, lastActivityAtMs: startedAtMs,
543
544
  });
544
- const logLine = (line, source = 'stdout') => {
545
+ const logLine = (line, source = 'stdout', metadata) => {
545
546
  if (this.disposed)
546
547
  return;
548
+ const attemptId = metadata?.attemptId;
549
+ const attribution = typeof attemptId === 'string' && attemptId.trim() && attemptId.length <= 256 ? { attemptId } : {};
547
550
  try {
548
- (0, db_1.appendEvent)(this.ctx.db, runId, ++sequence, { event_type: 'log', source, payload: JSON.stringify({ line: line.replace(/\n$/, '') }) });
551
+ (0, db_1.appendEvent)(this.ctx.db, runId, ++sequence, { event_type: 'log', source, payload: JSON.stringify({ line: line.replace(/\n$/, ''), ...attribution }) });
549
552
  }
550
553
  catch { /* events table best-effort */ }
551
554
  try {
552
- this.ctx.broadcast?.({ type: 'log', source, line, timestamp: new Date().toISOString(), processId: runId });
555
+ this.ctx.broadcast?.({ type: 'log', source, line, timestamp: new Date().toISOString(), processId: runId, ...attribution });
553
556
  }
554
557
  catch { /* persisted log remains authoritative */ }
555
558
  };
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createVerificationLogProjection = createVerificationLogProjection;
4
+ const node_util_1 = require("node:util");
5
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
6
+ const text = (value) => typeof value === 'string' ? value : '';
7
+ const display = (value, limit = 512) => (0, node_util_1.stripVTControlCharacters)(value).replace(/[\r\n]/g, ' ').slice(0, limit);
8
+ const attemptId = (event) => typeof event.attemptId === 'string' && event.attemptId.length <= 256 && event.attemptId.trim() ? event.attemptId : undefined;
9
+ const output = (value, id) => ({ text: value, ...(id ? { attemptId: id } : {}) });
10
+ const scope = (event) => JSON.stringify([event.runId, event.scopeId, event.nodePath, event.attemptId, event.attempt, event.visit]
11
+ .map(value => typeof value === 'string' && value.length <= 512 || typeof value === 'number' && Number.isSafeInteger(value) ? value : null));
12
+ const reference = 'Details → Verification evidence (stdout/stderr; retained evidence may be truncated)';
13
+ const shortPrefix = (value) => value.length > 96 ? value.slice(0, 92) + '…] ' : value;
14
+ /** This is presentation only. Neither its heuristics nor check progress can
15
+ * certify a candidate; original runtime events and command evidence are untouched. */
16
+ function createVerificationLogProjection() {
17
+ const streams = new Map();
18
+ const active = new Map();
19
+ let overflow = false;
20
+ const binding = (event, repository) => JSON.stringify([scope(event), repository]);
21
+ function get(key, event, prefix) {
22
+ let state = streams.get(key);
23
+ if (!state && streams.size < 64) {
24
+ state = { scope: scope(event), attemptId: attemptId(event), prefix: shortPrefix(prefix), tap: false, failed: false, yaml: false, inputLines: null, inputShortened: false,
25
+ headLines: 0, headChars: 0, shown: new Set(), diagnostics: [[], [], [], []], summaries: new Map(), tail: [], previous: [], context: 0, omitted: 0, announced: false };
26
+ streams.set(key, state);
27
+ }
28
+ return state;
29
+ }
30
+ function omitted(state) {
31
+ state.omitted++;
32
+ if (state.announced)
33
+ return '';
34
+ state.announced = true;
35
+ return state.prefix + '[Verbose output omitted; ' + reference + ']\n';
36
+ }
37
+ function diagnostic(state, line, priority) {
38
+ const bounded = line.slice(0, priority === 3 ? 256 : 512), bucket = state.diagnostics[priority];
39
+ if (state.shown.has(line) || bucket.includes(bounded))
40
+ return;
41
+ // First fact plus three recent facts in each class; warnings cannot consume
42
+ // the compiler/assertion budget, and later failures remain represented.
43
+ if (bucket.length === (priority === 3 ? 2 : 4))
44
+ bucket.splice(1, 1);
45
+ bucket.push(bounded);
46
+ }
47
+ function priority(body) {
48
+ if (/^(?!['"`]|(?:actual|expected|source):).+(?:\(\d+,\d+\)|:\d+(?::\d+)?)(?::|\s+-)?\s+(?:fatal\s+)?error\b/i.test(body)
49
+ || /^(?:fatal\s+)?error\s+[A-Z]+\d+\s*:/.test(body)
50
+ || /^(?:AssertionError|TypeError|ReferenceError|SyntaxError|RangeError)\b/.test(body))
51
+ return 0;
52
+ if (/^(?:FAIL\s|not ok \d+\b|[✖✗]\s|(?:fatal\s+)?error(?:\s*\[[^\]]+\])?\s*:)/i.test(body)
53
+ && !/\b0 errors?\b/i.test(body))
54
+ return 1;
55
+ if (/^(?:expected|actual|operator|test at|location)\s*[: ]/i.test(body))
56
+ return 2;
57
+ return undefined;
58
+ }
59
+ function summary(body) {
60
+ if (/^Ran all test suites[.\s]*$/.test(body))
61
+ return 'ran';
62
+ const match = body.match(/^(Test Suites|Test Files|Tests|Snapshots|Time|Duration|Ran all test suites)\s*:?\s/i)
63
+ ?? body.match(/^(?:#|ℹ)\s*(tests|suites|pass|fail|cancelled|skipped|todo|duration_ms|duration)\b/i);
64
+ return match?.[1].toLowerCase();
65
+ }
66
+ function line(state, original, body) {
67
+ let value = original;
68
+ if (/AssertionError\b.*\bInput:\s*$/.test(body))
69
+ state.inputLines = 0;
70
+ else if (state.inputLines !== null) {
71
+ if (/^["'`]/.test(body)) {
72
+ state.inputLines++;
73
+ if (state.inputLines > 2) {
74
+ if (state.inputShortened)
75
+ return '';
76
+ state.inputShortened = true;
77
+ return state.prefix + '[Assertion input shortened; inspect raw evidence]\n';
78
+ }
79
+ }
80
+ else if (!body)
81
+ return '';
82
+ else
83
+ state.inputLines = null;
84
+ }
85
+ if (/^[✔✓]\s/.test(body))
86
+ return '';
87
+ if (/^PASS\s/.test(body))
88
+ return omitted(state);
89
+ if (/^[✖✗]\s/.test(body))
90
+ state.failed = true;
91
+ if (/^TAP version |^# Subtest:/.test(body)) {
92
+ state.tap = true;
93
+ return '';
94
+ }
95
+ if (/^(?:not )?ok \d+\b/.test(body)) {
96
+ state.tap = true;
97
+ state.failed = body.startsWith('not ok');
98
+ state.yaml = false;
99
+ if (!state.failed)
100
+ return '';
101
+ }
102
+ if (state.tap) {
103
+ if (body === '---') {
104
+ state.yaml = true;
105
+ if (!state.failed)
106
+ return '';
107
+ }
108
+ if (body === '...') {
109
+ state.yaml = false;
110
+ if (!state.failed)
111
+ return '';
112
+ }
113
+ if (state.yaml && !state.failed)
114
+ return '';
115
+ if (/^1\.\.\d+$/.test(body))
116
+ return '';
117
+ }
118
+ if (!body)
119
+ return '';
120
+ if (value.length > 2_000)
121
+ value = value.slice(0, 1_900) + '\n[Long diagnostic line shortened; inspect raw evidence]';
122
+ const total = summary(body);
123
+ if (total) {
124
+ // Known totals have a fixed key set, so repeated progress updates do not
125
+ // become another unbounded stream or evict diagnostics.
126
+ state.summaries.set(total, state.prefix + body.slice(0, 128));
127
+ return '';
128
+ }
129
+ const rank = priority(body.slice(0, 2_000));
130
+ if (state.headLines < 24 && state.headChars + value.length <= 3_500) {
131
+ state.headLines++;
132
+ state.headChars += value.length;
133
+ state.shown.add(value);
134
+ state.previous.push(value);
135
+ if (state.previous.length > 2)
136
+ state.previous.shift();
137
+ return value + '\n';
138
+ }
139
+ if (rank !== undefined) {
140
+ if (rank < 2) {
141
+ for (const preceding of state.previous)
142
+ diagnostic(state, preceding, 3);
143
+ state.context = 3;
144
+ }
145
+ diagnostic(state, value, rank);
146
+ }
147
+ else if (state.context > 0) {
148
+ diagnostic(state, value, 3);
149
+ state.context--;
150
+ }
151
+ state.previous.push(value);
152
+ if (state.previous.length > 2)
153
+ state.previous.shift();
154
+ state.tail.push(value.slice(0, 256));
155
+ if (state.tail.length > 6)
156
+ state.tail.shift();
157
+ return omitted(state);
158
+ }
159
+ function flush(key) {
160
+ const result = [];
161
+ for (const [id, state] of streams) {
162
+ if (key !== undefined && id !== key)
163
+ continue;
164
+ let content = '';
165
+ const pending = [...state.diagnostics.flat(), ...state.tail, ...state.summaries.values()];
166
+ const seen = new Set(state.shown);
167
+ for (const value of pending)
168
+ if (!seen.has(value)) {
169
+ seen.add(value);
170
+ content += value + '\n';
171
+ }
172
+ if (state.omitted)
173
+ content += state.prefix + `[${state.omitted} output lines compacted; ${reference}]\n`;
174
+ if (content)
175
+ result.push(output(content, state.attemptId));
176
+ streams.delete(id);
177
+ for (const [name, activeId] of active)
178
+ if (activeId === id)
179
+ active.delete(name);
180
+ }
181
+ return result;
182
+ }
183
+ function project(event) {
184
+ if (event.type === 'verification-output' && typeof event.text === 'string') {
185
+ const result = [];
186
+ for (const value of (0, node_util_1.stripVTControlCharacters)(event.text).split('\n')) {
187
+ if (!value)
188
+ continue;
189
+ const match = value.match(/^(\[verification ([^/\]]+)\/[^\]]+\] )?(.*)$/);
190
+ const prefix = match?.[1] ?? '', repository = match?.[2] ?? '', body = (match?.[3] ?? value).trim();
191
+ const key = active.get(binding(event, repository)) ?? JSON.stringify([scope(event), prefix.slice(0, 1024)]);
192
+ const state = get(key, event, prefix);
193
+ if (state) {
194
+ const content = line(state, shortPrefix(prefix) + (match?.[3] ?? value), body);
195
+ if (content)
196
+ result.push(output(content, state.attemptId));
197
+ }
198
+ else if (!overflow) {
199
+ overflow = true;
200
+ result.push(output(`[Additional verification output omitted; ${reference}]\n`, attemptId(event)));
201
+ }
202
+ }
203
+ return result;
204
+ }
205
+ if (event.type === 'runtime-efficiency-event') {
206
+ const data = object(event.payload) ? { ...event, ...event.payload } : event;
207
+ const kind = text(data.kind), id = text(data.executionId), repository = text(data.repositoryId), label = text(data.label);
208
+ if (!['check-started', 'check-finished', 'check-reused', 'check-invalidated'].includes(kind) || !id || id.length > 256 || !repository || repository.length > 256 || !label || label.length > 4096)
209
+ return [];
210
+ const prefix = `[verification ${display(repository)}/${display(label)}] `;
211
+ const key = JSON.stringify([scope(data), id]), name = binding(data, repository);
212
+ if (kind === 'check-started') {
213
+ if (active.get(name) === key)
214
+ return [];
215
+ const result = active.has(name) ? flush(active.get(name)) : [];
216
+ if (get(key, data, prefix))
217
+ active.set(name, key);
218
+ result.push(output(prefix + 'started\n', attemptId(data)));
219
+ return result;
220
+ }
221
+ if (kind === 'check-invalidated')
222
+ return [output(prefix + 'evidence invalidated' + (typeof data.reason === 'string' ? ': ' + display(data.reason) : '') + '\n', attemptId(data))];
223
+ if (!Number.isSafeInteger(data.exitCode) || typeof data.durationMs !== 'number' || !Number.isFinite(data.durationMs) || data.durationMs < 0)
224
+ return [];
225
+ const result = flush(key);
226
+ return [...result, output(prefix + `${kind === 'check-reused' ? 'reused' : data.exitCode === 0 ? 'command passed' : 'command failed'} (exit ${data.exitCode}, ${(data.durationMs / 1000).toFixed(1)} s)\n`, attemptId(data))];
227
+ }
228
+ if (event.type === 'runtime-result')
229
+ return flush();
230
+ if (event.type === 'workflow-event' && object(event.event)) {
231
+ const data = event.event, kind = text(data.type);
232
+ if (/^workflow_(?:succeeded|failed|cancelled|paused)$/.test(kind))
233
+ return flush();
234
+ if (!/^step_(?:started|succeeded|failed|cancelled|interrupted)$/.test(kind))
235
+ return [];
236
+ const result = [];
237
+ for (const [key, state] of streams) {
238
+ const identity = JSON.parse(state.scope);
239
+ // Older transports lack attempt identity. Flush them before a phase
240
+ // boundary; v2 flushes only the ending attempt, leaving siblings intact.
241
+ const legacy = identity[2] === null && identity[3] === null;
242
+ const sameAttempt = typeof data.attemptId === 'string' && identity[3] === data.attemptId;
243
+ const sameNode = !data.attemptId && (data.nodePath ?? data.stepId) === identity[2] && (!data.scopeId || data.scopeId === identity[1]);
244
+ if (legacy || (kind !== 'step_started' && (sameAttempt || sameNode)))
245
+ result.push(...flush(key));
246
+ }
247
+ return result;
248
+ }
249
+ return [];
250
+ }
251
+ return { project, flush };
252
+ }
@@ -104,8 +104,16 @@ function resolveRailRelaunch(c, railIndex, sourceId) {
104
104
  config.baseBranch = row.base_branch;
105
105
  if (delivery.executionManifest) {
106
106
  config.repositoryIds = delivery.executionManifest.selectedRepositoryIds;
107
- config.workspaceSelection = savedConfig?.workspaceSelection ?? Object.fromEntries(delivery.executionManifest.repositories
108
- .filter(repo => repo.selectedWorkspacePaths).map(repo => [repo.repositoryId, repo.selectedWorkspacePaths]));
107
+ if (savedConfig?.workspaceSelection === undefined) {
108
+ const entries = delivery.executionManifest.repositories
109
+ .filter(repo => repo.selectedWorkspacePaths !== undefined).map(repo => [repo.repositoryId, repo.selectedWorkspacePaths]);
110
+ // No recorded narrowing means registered defaults, represented by
111
+ // absence. Explicit values remain subject to ordinary launch validation.
112
+ if (entries.length)
113
+ config.workspaceSelection = Object.fromEntries(entries);
114
+ else
115
+ delete config.workspaceSelection;
116
+ }
109
117
  }
110
118
  }
111
119
  if (!Array.isArray(ticketIds) || !ticketIds.length || ticketIds.some(id => !Number.isSafeInteger(id) || id <= 0))
@@ -950,13 +950,15 @@ class LoopRunManager {
950
950
  openStep = null;
951
951
  emitRunEvent('loop_step_end', payload);
952
952
  };
953
- const logLine = (line, source = 'stdout') => {
953
+ const logLine = (line, source = 'stdout', metadata) => {
954
+ const attemptId = metadata?.attemptId;
955
+ const attribution = typeof attemptId === 'string' && attemptId.trim() && attemptId.length <= 256 ? { attemptId } : {};
954
956
  const s = takeSeq();
955
957
  try {
956
- (0, db_1.appendEvent)(this.db, runId, s, { event_type: 'log', source, payload: JSON.stringify({ line }) });
958
+ (0, db_1.appendEvent)(this.db, runId, s, { event_type: 'log', source, payload: JSON.stringify({ line, ...attribution }) });
957
959
  }
958
960
  catch { /* events table best-effort */ }
959
- this._emit({ type: 'log', source, line, timestamp: new Date(this.now()).toISOString(), processId: runId });
961
+ this._emit({ type: 'log', source, line, timestamp: new Date(this.now()).toISOString(), processId: runId, ...attribution });
960
962
  };
961
963
  // Forward a RAW provider stdout line (claude/codex JSONL) as an `event` —
962
964
  // identical to QueueManager — so JobStatusPanel parses real activity