specrails-desktop 2.59.2 → 2.60.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 (177) hide show
  1. package/client/dist/assets/{ActivityFeedPage-vAY9C71W.js → ActivityFeedPage-3J7fR8yB.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-CpwjGXQm.js → AgentBrowserCapture-D4a3wYM1.js} +1 -1
  3. package/client/dist/assets/{AgentModeAnalyticsPane-BHeoJVxQ.js → AgentModeAnalyticsPane-Dps30tk9.js} +2 -2
  4. package/client/dist/assets/{AgentModeCodePane-B0Nf3YkN.js → AgentModeCodePane-CvLimey8.js} +2 -2
  5. package/client/dist/assets/{AgentModeJobsPane-CpbIItY_.js → AgentModeJobsPane-25tQlLpF.js} +1 -1
  6. package/client/dist/assets/{AgentsPage-BVJAFXn3.js → AgentsPage-CUDp_sfw.js} +2 -2
  7. package/client/dist/assets/{AnalyticsPage-DDWees-K.js → AnalyticsPage-DXWMGn8b.js} +1 -1
  8. package/client/dist/assets/{CodePage-CzV5tkvS.js → CodePage-D981zRHy.js} +1 -1
  9. package/client/dist/assets/{DesktopAnalyticsPage-DfRHNK7i.js → DesktopAnalyticsPage-BkFBq0E6.js} +1 -1
  10. package/client/dist/assets/{DocsDialog-BxduBYbh.js → DocsDialog-BL7uSbIJ.js} +1 -1
  11. package/client/dist/assets/{DocsPage-DOVeRJn_.js → DocsPage-CsDLbmZX.js} +1 -1
  12. package/client/dist/assets/{ExportDropdown-BnKVV8UK.js → ExportDropdown-B4Olg1Fk.js} +1 -1
  13. package/client/dist/assets/{InteractiveJobComposer-Da9dvM-0.js → InteractiveJobComposer-Z6fl4791.js} +2 -2
  14. package/client/dist/assets/JobDetailModal-DS_Owxs6.js +1 -0
  15. package/client/dist/assets/JobDetailPage-1TUQ9vlE.js +1 -0
  16. package/client/dist/assets/{JobsPage-0vdlbNik.js → JobsPage-BSrl8P7u.js} +1 -1
  17. package/client/dist/assets/LoopBuilderPage-Dh5uk2Qy.js +1 -0
  18. package/client/dist/assets/{LoopPreviewModal-Df-S8NGf.js → LoopPreviewModal-CH1r-KhU.js} +1 -1
  19. package/client/dist/assets/{LoopWindowSurface-sdb7QbSo.js → LoopWindowSurface-CiZdI6aw.js} +1 -1
  20. package/client/dist/assets/{LoopsPage-CDfCC9Px.js → LoopsPage-BwFujdGi.js} +1 -1
  21. package/client/dist/assets/{MinimizedChatsContext-DKlPf54Y.js → MinimizedChatsContext-BD2bsRBe.js} +1 -1
  22. package/client/dist/assets/PluginsPage-B8f49Mvn.js +2 -0
  23. package/client/dist/assets/{ProjectSettingsDialog-DOkjT7xv.js → ProjectSettingsDialog-D7q-O6h3.js} +1 -1
  24. package/client/dist/assets/{RepositoryDeliveries-CJVpOUpZ.js → RepositoryDeliveries-NNorcGzT.js} +1 -1
  25. package/client/dist/assets/{RepositoryScopeSelector-BeM-2gIZ.js → RepositoryScopeSelector-D-jb4bI_.js} +1 -1
  26. package/client/dist/assets/{ReviewPacketPage-EBCim2w_.js → ReviewPacketPage-DpZSzF-Q.js} +1 -1
  27. package/client/dist/assets/{TemplatePreviewModal-1-92UGwN.js → TemplatePreviewModal-5CZdAZ0G.js} +1 -1
  28. package/client/dist/assets/{TicketDetailModalContext-CkVXuHxU.js → TicketDetailModalContext-BHmtj1As.js} +1 -1
  29. package/client/dist/assets/{Trans-CzhPjnjB.js → Trans-wmTUDVyC.js} +1 -1
  30. package/client/dist/assets/{agentRuntime-TXPQowKJ.js → agentRuntime-7Ydcvr6P.js} +1 -1
  31. package/client/dist/assets/{agentRuntime-Bpx3yWHK.js → agentRuntime-BHVw9orA.js} +1 -1
  32. package/client/dist/assets/{agentRuntime-15Pkk6we.js → agentRuntime-DCPDaQcV.js} +1 -1
  33. package/client/dist/assets/{agentRuntime-C78n13To.js → agentRuntime-DKTw4Ajg.js} +1 -1
  34. package/client/dist/assets/{agentRuntime-CCI7XF4c.js → agentRuntime-DVhij_i_.js} +1 -1
  35. package/client/dist/assets/{agentRuntime-Ddqvp2bP.js → agentRuntime-Doss-T8o.js} +1 -1
  36. package/client/dist/assets/{agentRuntime-CKDHe2-a.js → agentRuntime-eZSMUlkM.js} +1 -1
  37. package/client/dist/assets/{agentRuntime-hqZRy08E.js → agentRuntime-qSSU7fQk.js} +1 -1
  38. package/client/dist/assets/agents-B6ZsnARo.js +1 -0
  39. package/client/dist/assets/agents-BORdhe0K.js +1 -0
  40. package/client/dist/assets/agents-C6aq9Oqs.js +1 -0
  41. package/client/dist/assets/agents-Cbsc4bcX.js +1 -0
  42. package/client/dist/assets/agents-D0uYR6hq.js +1 -0
  43. package/client/dist/assets/agents-Doe1sHY5.js +1 -0
  44. package/client/dist/assets/agents-DyNi_I9m.js +1 -0
  45. package/client/dist/assets/agents-StKxiTg0.js +1 -0
  46. package/client/dist/assets/agentstudio-7cYCXn-L.js +1 -0
  47. package/client/dist/assets/{agentstudio-QdAJIDef.js → agentstudio-BHWQh8G3.js} +1 -1
  48. package/client/dist/assets/agentstudio-BjnWHyRs.js +1 -0
  49. package/client/dist/assets/{agentstudio-fLXpRGIi.js → agentstudio-BkV8vcK4.js} +1 -1
  50. package/client/dist/assets/{agentstudio-BWAKeGgB.js → agentstudio-BzugqB_N.js} +1 -1
  51. package/client/dist/assets/{agentstudio-Cja8Sjcp.js → agentstudio-CWbp7KvH.js} +1 -1
  52. package/client/dist/assets/agentstudio-DG6plf5o.js +1 -0
  53. package/client/dist/assets/agentstudio-DkZVF-Cc.js +1 -0
  54. package/client/dist/assets/{formatDistanceToNow-DlQaXc7c.js → formatDistanceToNow-Dax4zqPF.js} +1 -1
  55. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-DxUFGcVq.js → getTimezoneOffsetInMilliseconds-dpIo42JZ.js} +1 -1
  56. package/client/dist/assets/{index-Czov72g7.js → index-g8S62tpS.js} +7 -7
  57. package/client/dist/assets/jira-B6OYw4CJ.js +1 -0
  58. package/client/dist/assets/jira-BTF6RaOe.js +1 -0
  59. package/client/dist/assets/jira-CEetIB2q.js +1 -0
  60. package/client/dist/assets/jira-CIzsZLw5.js +1 -0
  61. package/client/dist/assets/{jira-BOoaUXbb.js → jira-Cq6yvN4q.js} +1 -1
  62. package/client/dist/assets/jira-Daw9tmpI.js +1 -0
  63. package/client/dist/assets/jira-FXr7u2lh.js +1 -0
  64. package/client/dist/assets/jira-LaNQn7yl.js +1 -0
  65. package/client/dist/assets/{jira-api-CbJvbRYx.js → jira-api-CLrIvXFm.js} +1 -1
  66. package/client/dist/assets/{jobs-DZAbh1m2.js → jobs-ARwD85B9.js} +1 -1
  67. package/client/dist/assets/{jobs-BD47Ov11.js → jobs-BMF1w4WT.js} +1 -1
  68. package/client/dist/assets/{jobs-tWjcDF_6.js → jobs-CBfzGLwm.js} +1 -1
  69. package/client/dist/assets/{jobs-CSrRx5lD.js → jobs-CPjiluBY.js} +1 -1
  70. package/client/dist/assets/{jobs-epP7gt8W.js → jobs-CQQd58pY.js} +1 -1
  71. package/client/dist/assets/{jobs-1G3wodhl.js → jobs-De3EqaO0.js} +1 -1
  72. package/client/dist/assets/{jobs-BLOoVRRl.js → jobs-Du9-Ei9a.js} +1 -1
  73. package/client/dist/assets/{jobs-B4pu8TdF.js → jobs-qMHbnxwC.js} +1 -1
  74. package/client/dist/assets/{project-repositories-BkBD3rxx.js → project-repositories-BkyT8AjU.js} +1 -1
  75. package/client/dist/assets/{spending-CYotH-9N.js → spending-Oov7p52A.js} +1 -1
  76. package/client/dist/assets/{useDesktop-DpxVGfUC.js → useDesktop-DpiS8Uov.js} +1 -1
  77. package/client/dist/assets/{useRuntimeRuns-CfGRbhr9.js → useRuntimeRuns-DBm1mul0.js} +1 -1
  78. package/client/dist/assets/{useSharedWebSocket-C17TGqT8.js → useSharedWebSocket-BZxKaVNT.js} +2 -2
  79. package/client/dist/index.html +19 -19
  80. package/docs/getting-started.md +2 -2
  81. package/docs/guide/de/agents/1-meet-the-agents.md +1 -1
  82. package/docs/guide/de/agents/2-profiles-and-the-balanced-default.md +1 -1
  83. package/docs/guide/de/agents/3-customizing-models-per-agent.md +1 -1
  84. package/docs/guide/de/agents/4-custom-agents-catalog.md +3 -3
  85. package/docs/guide/en/agents/1-meet-the-agents.md +1 -1
  86. package/docs/guide/en/agents/2-profiles-and-the-balanced-default.md +1 -1
  87. package/docs/guide/en/agents/3-customizing-models-per-agent.md +1 -1
  88. package/docs/guide/en/agents/4-custom-agents-catalog.md +3 -3
  89. package/docs/guide/es/agents/1-meet-the-agents.md +1 -1
  90. package/docs/guide/es/agents/2-profiles-and-the-balanced-default.md +1 -1
  91. package/docs/guide/es/agents/3-customizing-models-per-agent.md +1 -1
  92. package/docs/guide/es/agents/4-custom-agents-catalog.md +3 -3
  93. package/docs/guide/fr/agents/1-meet-the-agents.md +1 -1
  94. package/docs/guide/fr/agents/2-profiles-and-the-balanced-default.md +1 -1
  95. package/docs/guide/fr/agents/3-customizing-models-per-agent.md +1 -1
  96. package/docs/guide/fr/agents/4-custom-agents-catalog.md +3 -3
  97. package/docs/guide/it/agents/1-meet-the-agents.md +1 -1
  98. package/docs/guide/it/agents/2-profiles-and-the-balanced-default.md +1 -1
  99. package/docs/guide/it/agents/3-customizing-models-per-agent.md +1 -1
  100. package/docs/guide/it/agents/4-custom-agents-catalog.md +3 -3
  101. package/docs/guide/ja/agents/1-meet-the-agents.md +1 -1
  102. package/docs/guide/ja/agents/2-profiles-and-the-balanced-default.md +1 -1
  103. package/docs/guide/ja/agents/3-customizing-models-per-agent.md +1 -1
  104. package/docs/guide/ja/agents/4-custom-agents-catalog.md +3 -3
  105. package/docs/guide/pt/agents/1-meet-the-agents.md +1 -1
  106. package/docs/guide/pt/agents/2-profiles-and-the-balanced-default.md +1 -1
  107. package/docs/guide/pt/agents/3-customizing-models-per-agent.md +1 -1
  108. package/docs/guide/pt/agents/4-custom-agents-catalog.md +3 -3
  109. package/docs/guide/zh/agents/1-meet-the-agents.md +1 -1
  110. package/docs/guide/zh/agents/2-profiles-and-the-balanced-default.md +1 -1
  111. package/docs/guide/zh/agents/3-customizing-models-per-agent.md +1 -1
  112. package/docs/guide/zh/agents/4-custom-agents-catalog.md +3 -3
  113. package/docs/internals/adding-a-provider.md +7 -6
  114. package/docs/internals/configuration.md +45 -0
  115. package/docs/internals/core-runtime-updates.md +19 -0
  116. package/docs/internals/loop-step-log-explorer.md +27 -0
  117. package/docs/internals/mission-rail-cards.md +7 -0
  118. package/docs/internals/profiles.md +17 -10
  119. package/docs/internals/review-packet.md +1 -1
  120. package/docs/internals/safe-pr-review-flow.md +9 -0
  121. package/docs/internals/source-map.md +6 -2
  122. package/docs/jira-integration-plan.md +7 -4
  123. package/docs/running-pipelines.md +2 -2
  124. package/package.json +1 -1
  125. package/server/dist/core-compat.js +7 -3
  126. package/server/dist/core-execution.js +5 -18
  127. package/server/dist/jira/jira-db.js +33 -6
  128. package/server/dist/jira/jira-materializer.js +11 -9
  129. package/server/dist/jira/jira-status-resolver.js +64 -22
  130. package/server/dist/jira/jira-sync-manager.js +40 -1
  131. package/server/dist/jira-router.js +8 -41
  132. package/server/dist/mcp/guide.js +4 -3
  133. package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +22 -70
  134. package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls.js +7 -4
  135. package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings.js +24 -1
  136. package/server/dist/modules/agent-runtime/runtime/agent-runtime-verification-log.js +252 -0
  137. package/server/dist/modules/agents/runtime/profiles-router.js +74 -36
  138. package/server/dist/modules/delivery/runtime/rail-relaunch.js +10 -2
  139. package/server/dist/modules/execution/runtime/queue-manager.js +7 -9
  140. package/server/dist/modules/loops/runtime/loop-agents.js +2 -5
  141. package/server/dist/modules/loops/runtime/loop-command-catalog.js +5 -5
  142. package/server/dist/modules/loops/runtime/loop-definition.js +1 -1
  143. package/server/dist/modules/loops/runtime/loop-executors.js +0 -2
  144. package/server/dist/modules/loops/runtime/loop-implement-recipe.js +5 -4
  145. package/server/dist/modules/loops/runtime/loop-run-manager.js +5 -3
  146. package/server/dist/modules/loops/runtime/loops-router.js +3 -1
  147. package/server/dist/path-resolver.js +41 -20
  148. package/server/dist/project-env.js +22 -6
  149. package/server/dist/providers/gemini-agent-ack.js +13 -10
  150. package/server/dist/setup-manager.js +28 -43
  151. package/server/dist/util/checkout-path.js +22 -0
  152. package/server/dist/workspace-manager.js +13 -71
  153. package/server/dist/worktree-node-modules.js +70 -34
  154. package/client/dist/assets/JobDetailModal-DhwKbZGe.js +0 -1
  155. package/client/dist/assets/JobDetailPage-MtPE-E_1.js +0 -1
  156. package/client/dist/assets/LoopBuilderPage-DCUHozXh.js +0 -1
  157. package/client/dist/assets/PluginsPage-iL0G3rQQ.js +0 -2
  158. package/client/dist/assets/agents-CA5eT1d0.js +0 -1
  159. package/client/dist/assets/agents-CAgy-5uS.js +0 -1
  160. package/client/dist/assets/agents-CNm6Qzlv.js +0 -1
  161. package/client/dist/assets/agents-CTwbx-fb.js +0 -1
  162. package/client/dist/assets/agents-DEGV1Vao.js +0 -1
  163. package/client/dist/assets/agents-HsDF1aQU.js +0 -1
  164. package/client/dist/assets/agents-IQjdA-z9.js +0 -1
  165. package/client/dist/assets/agents-RkGoteXh.js +0 -1
  166. package/client/dist/assets/agentstudio-B2CTtQgZ.js +0 -1
  167. package/client/dist/assets/agentstudio-DtfPcr99.js +0 -1
  168. package/client/dist/assets/agentstudio-X70BKf6u.js +0 -1
  169. package/client/dist/assets/agentstudio-qXKqVnWS.js +0 -1
  170. package/client/dist/assets/jira-BNZu8f9O.js +0 -1
  171. package/client/dist/assets/jira-BYVN4L2u.js +0 -1
  172. package/client/dist/assets/jira-CKJg3HZd.js +0 -1
  173. package/client/dist/assets/jira-DDDsZ4ub.js +0 -1
  174. package/client/dist/assets/jira-DXwYjVE4.js +0 -1
  175. package/client/dist/assets/jira-Dn6U2nRW.js +0 -1
  176. package/client/dist/assets/jira-j7hVolR5.js +0 -1
  177. package/server/dist/modules/loops/runtime/loop-agent-defaults.json +0 -6
@@ -9,8 +9,8 @@
9
9
  // 2. Category fallback anchored on `statusCategory.key` (never the localizable
10
10
  // status NAME), with a cancel/ship lexicon to disambiguate the `done`
11
11
  // category (e.g. `Won't Do` vs `Released`).
12
- // 3. BFS transition walk for forward-only workflows where there is no direct
13
- // edge to the target category.
12
+ // 3. Bounded category-monotonic transition walk where there is no direct edge
13
+ // to the target category. Unknown lateral paths are never explored by writes.
14
14
  //
15
15
  // This module is PURE: the walker takes async callbacks (getTransitions /
16
16
  // applyTransition) so it is fully testable without HTTP.
@@ -60,12 +60,14 @@ function nameMatches(name, lexicon) {
60
60
  function isCancelName(name) {
61
61
  return nameMatches(name, CANCEL_LEXICON);
62
62
  }
63
+ function matchesStatusTarget(id, name, target) {
64
+ return id === target || (name !== undefined && name.toLowerCase() === target.toLowerCase());
65
+ }
63
66
  /** Does this transition's destination match the user's explicitly configured
64
- * target (by status id, status name, or transition id)? Case-insensitive on name.
67
+ * target (by status id or status name)? Case-insensitive on name.
65
68
  * The explicit config ALWAYS wins over the cancel/ship lexicon heuristics. */
66
69
  function matchesExplicit(t, explicitTarget) {
67
- const e = explicitTarget.toLowerCase();
68
- return t.to.id === explicitTarget || t.to.name.toLowerCase() === e || t.id === explicitTarget;
70
+ return matchesStatusTarget(t.to.id, t.to.name, explicitTarget);
69
71
  }
70
72
  function transitionCategory(t) {
71
73
  const k = t.to.statusCategory?.key;
@@ -81,6 +83,8 @@ function transitionCategory(t) {
81
83
  */
82
84
  function pickDirectTransition(transitions, state, explicitTarget) {
83
85
  if (explicitTarget) {
86
+ // Values identify destination statuses, never transition IDs: the two ID
87
+ // namespaces can collide, including with an unrelated terminal transition.
84
88
  const t = transitions.find((tr) => matchesExplicit(tr, explicitTarget));
85
89
  if (t)
86
90
  return t;
@@ -117,9 +121,9 @@ function pickDirectTransition(transitions, state, explicitTarget) {
117
121
  }
118
122
  /**
119
123
  * Pick a transition that moves the issue closer to the target category (used by
120
- * the BFS walk when no direct transition exists). Returns the edge whose target
124
+ * the bounded walk when no direct transition exists). Returns the edge whose target
121
125
  * category is strictly closer (in rank distance) to the goal, never overshooting
122
- * past it, preferring the smallest forward step.
126
+ * past it, preferring the furthest progress in either direction.
123
127
  */
124
128
  function pickProgressTransition(transitions, currentCategory, targetCategory, visitedStatusIds, opts) {
125
129
  const goal = categoryRank(targetCategory);
@@ -189,26 +193,57 @@ function buildTransitionFields(transition, state) {
189
193
  }
190
194
  return Object.keys(out).length > 0 ? { fields: out } : {};
191
195
  }
196
+ function diagnosticValue(value, limit = 64) {
197
+ const compact = value.replace(/[\u0000-\u001f\u007f]/g, ' ').trim();
198
+ if (compact.length <= limit && JSON.stringify(compact).length <= limit)
199
+ return JSON.stringify(compact);
200
+ let preview = compact.slice(0, limit);
201
+ while (JSON.stringify(`${preview}…`).length > limit)
202
+ preview = preview.slice(0, -1);
203
+ return JSON.stringify(`${preview}…`);
204
+ }
205
+ function statusDiagnostic(name, id) {
206
+ return `${name ? diagnosticValue(name, 48) : '(unknown)'}${id ? ` (id ${diagnosticValue(id, 24)})` : ''}`;
207
+ }
208
+ function noPathReason(summary, currentName, currentId, explicitTarget, transitions) {
209
+ // Dead-letter storage retains at most 500 characters. Budget the entire
210
+ // message here so available destinations are never silently cut off there.
211
+ const prefix = `${summary.slice(0, 160)}; current status ${statusDiagnostic(currentName, currentId)}`
212
+ + (explicitTarget ? `; configured target ${diagnosticValue(explicitTarget)}` : '')
213
+ + '; available destinations: ';
214
+ if (transitions === undefined)
215
+ return `${prefix}not fetched for current status`;
216
+ const available = [];
217
+ for (const transition of transitions.slice(0, 8)) {
218
+ const candidate = [...available, statusDiagnostic(transition.to.name, transition.to.id)];
219
+ const omitted = transitions.length - candidate.length;
220
+ const suffix = omitted > 0 ? `; +${omitted} more` : '';
221
+ if (`${prefix}${candidate.join(', ')}${suffix}`.length > 500)
222
+ break;
223
+ available.push(candidate[candidate.length - 1]);
224
+ }
225
+ const omitted = transitions.length - available.length;
226
+ return `${prefix}${available.join(', ') || (omitted ? '(omitted)' : 'none')}${omitted > 0 ? `; +${omitted} more` : ''}`;
227
+ }
192
228
  /**
193
229
  * Walk the transition graph from the current category to the target category for
194
230
  * `state`, applying edges per hop (you can only see the current status's
195
- * outgoing edges). Idempotency-first: if already in the target category, no-op —
196
- * UNLESS an explicit target status is configured and the issue is not already
197
- * sitting on it (see `currentStatusName`).
231
+ * outgoing edges). An explicit destination's identity wins over the default
232
+ * category, including idempotency. Without a destination, the category suffices.
198
233
  */
199
234
  async function walkToCategory(args) {
200
235
  const target = targetCategoryFor(args.state);
201
- if (args.currentCategory === target) {
202
- const alreadyAtExplicit = !args.explicitTarget ||
203
- (args.currentStatusName !== undefined &&
204
- args.currentStatusName.toLowerCase() === args.explicitTarget.toLowerCase());
205
- if (alreadyAtExplicit)
206
- return { status: 'noop' };
207
- }
236
+ const alreadyAtTarget = args.explicitTarget
237
+ ? matchesStatusTarget(args.currentStatusId, args.currentStatusName, args.explicitTarget)
238
+ : args.currentCategory === target;
239
+ if (alreadyAtTarget)
240
+ return { status: 'noop' };
208
241
  const maxHops = args.maxHops ?? 5;
209
- const visited = new Set();
242
+ const visited = new Set(args.currentStatusId ? [args.currentStatusId] : []);
210
243
  const applied = [];
211
244
  let currentCategory = args.currentCategory;
245
+ let currentStatusName = args.currentStatusName;
246
+ let currentStatusId = args.currentStatusId;
212
247
  for (let hop = 0; hop < maxHops; hop++) {
213
248
  let transitions;
214
249
  try {
@@ -230,7 +265,7 @@ async function walkToCategory(args) {
230
265
  return { status: 'error', reason: err instanceof Error ? err.message : String(err) };
231
266
  }
232
267
  applied.push(direct.id);
233
- return { status: 'applied', finalCategory: target, transitions: applied };
268
+ return { status: 'applied', finalCategory: transitionCategory(direct), transitions: applied };
234
269
  }
235
270
  // No direct edge → step toward the target category. For any non-cancel walk
236
271
  // the step must avoid reject statuses so the issue never overshoots onto a
@@ -242,7 +277,7 @@ async function walkToCategory(args) {
242
277
  if (!step) {
243
278
  return {
244
279
  status: 'no_path',
245
- reason: `no workflow transition from category "${currentCategory}" toward "${target}"`,
280
+ reason: noPathReason(`no workflow transition from category "${currentCategory}" toward "${target}"`, currentStatusName, currentStatusId, args.explicitTarget, transitions),
246
281
  };
247
282
  }
248
283
  const plan = buildTransitionFields(step, args.state);
@@ -256,12 +291,19 @@ async function walkToCategory(args) {
256
291
  }
257
292
  applied.push(step.id);
258
293
  visited.add(step.to.id);
294
+ currentStatusName = step.to.name;
295
+ currentStatusId = step.to.id;
259
296
  const stepCat = transitionCategory(step);
260
297
  if (stepCat)
261
298
  currentCategory = stepCat;
262
- if (currentCategory === target) {
299
+ if (!args.explicitTarget && currentCategory === target) {
263
300
  return { status: 'applied', finalCategory: target, transitions: applied };
264
301
  }
265
302
  }
266
- return { status: 'no_path', reason: `target category "${target}" not reached within ${maxHops} hops` };
303
+ return {
304
+ status: 'no_path',
305
+ reason: noPathReason(args.explicitTarget
306
+ ? `configured status not reached within ${maxHops} hops`
307
+ : `target category "${target}" not reached within ${maxHops} hops`, currentStatusName, currentStatusId, args.explicitTarget, undefined),
308
+ };
267
309
  }
@@ -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
@@ -385,9 +385,10 @@ can run Contract Refine require AI-spawn, including Explore conversions.
385
385
  - **Plugins** (\`specrails_plugins\`): per-project MCP integrations; \`preview\`
386
386
  the diff before \`install\`; a degraded plugin never blocks rail launches.
387
387
  - **Profiles** (\`specrails_agents\`): per-project agent chains; the baseline
388
- trio \`sr-architect\`/\`sr-developer\`/\`sr-reviewer\` is required; routing rules
389
- end in a single terminal \`default: true\` rule; requires specrails-core
390
- 4.1.0 or newer.
388
+ trio \`sr-architect\`/\`sr-developer\`/\`sr-reviewer\` are role identifiers
389
+ defined by the specrails-core runtime (no installed files) and are required
390
+ in every chain; routing rules end in a single terminal \`default: true\` rule;
391
+ requires specrails-core 4.1.0 or newer.
391
392
  - **Setup** (\`specrails_setup\`): \`prerequisites\` → \`add_project\` →
392
393
  \`install_config\` per provider → ONE \`install\` (provisions all chosen
393
394
  providers; quick-only, offline) → poll \`checkpoints\` until
@@ -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',
@@ -208,14 +148,18 @@ async function runAgentRuntimeInvocation(options) {
208
148
  if (!Array.isArray(admittedContext.repositories) || !admittedContext.repositories.length || admittedContext.repositories.some(repo => !repo || typeof repo.id !== 'string' || !repo.id))
209
149
  throw new Error('Core context is missing its repository scope');
210
150
  const source = (0, node_fs_1.existsSync)(options.configPath) ? 'project-role' : 'default';
211
- const { config, origins } = (0, agent_runtime_effective_config_1.resolveEffectiveRuntimeConfig)(options.loopConfig ? (0, agent_runtime_settings_1.loadLoopRuntimeConfig)(options.configPath, options.loopConfig, options.providerOverride ?? { provider: options.defaultProvider ?? 'claude' }) : (0, agent_runtime_settings_1.loadRuntimeConfigFile)(options.configPath, options.defaultProvider), {
151
+ // The engine owns the factory role definitions; the user's global overrides sit on top. The Implement
152
+ // rail merges them under any project text, and loop recipes resolve their `inherit` definitions here.
153
+ const effectiveRolePrompts = { ...(await (0, agent_runtime_loader_1.loadCoreAgentRuntime)()).rolePromptDefaults(), ...(0, agent_runtime_settings_1.loadRuntimeRolePrompts)() };
154
+ const loopConfig = options.loopConfig && (0, agent_runtime_settings_1.resolveInheritedRolePrompts)(options.loopConfig, effectiveRolePrompts);
155
+ const { config, origins } = (0, agent_runtime_effective_config_1.resolveEffectiveRuntimeConfig)(loopConfig ? (0, agent_runtime_settings_1.loadLoopRuntimeConfig)(options.configPath, loopConfig, options.providerOverride ?? { provider: options.defaultProvider ?? 'claude' }) : (0, agent_runtime_settings_1.loadRuntimeConfigFile)(options.configPath, options.defaultProvider), {
212
156
  repositoryIds: admittedContext.repositories.map(repo => repo.id), source: options.loopConfig ? 'loop-role' : source, providerOverride: options.loopConfig ? undefined : options.providerOverride,
213
157
  });
214
158
  config.verification = scopedHostChecks(config.verification, admittedContext.repositories);
215
159
  if (definitionEngine)
216
160
  completeHostChecks(config, admittedContext.repositories);
217
- if (!options.loopConfig)
218
- config.rolePrompts = { ...(await (0, agent_runtime_loader_1.loadCoreAgentRuntime)()).rolePromptDefaults(), ...(0, agent_runtime_settings_1.loadRuntimeRolePrompts)(), ...config.rolePrompts };
161
+ if (!loopConfig)
162
+ config.rolePrompts = { ...effectiveRolePrompts, ...config.rolePrompts };
219
163
  // Core rejects unknown connection keys: drop the desktop-only local-engine
220
164
  // fields (label/defaultModel/rates/supportsReasoningEffort) before Core sees it.
221
165
  const runtime = await (0, agent_runtime_loader_1.loadCoreAgentRuntime)();
@@ -356,7 +300,13 @@ async function runAgentRuntimeInvocation(options) {
356
300
  callback?.();
357
301
  }
358
302
  catch { /* Logging cannot replay a workflow. */ } };
359
- const readableVerification = verificationLogFilter();
303
+ const readableVerification = (0, agent_runtime_verification_log_1.createVerificationLogProjection)();
304
+ const readableLines = (lines) => {
305
+ for (const line of lines)
306
+ observe(() => line.attemptId
307
+ ? options.onLine?.(line.text, 'stdout', { attemptId: line.attemptId })
308
+ : options.onLine?.(line.text));
309
+ };
360
310
  const lines = (0, node_readline_1.createInterface)({ input: child.stdout });
361
311
  lines.on('line', line => {
362
312
  if (line.length > 2_000_000) {
@@ -388,6 +338,10 @@ async function runAgentRuntimeInvocation(options) {
388
338
  (0, win_spawn_1.treeKillSafe)(child.pid, 'SIGKILL');
389
339
  return;
390
340
  }
341
+ const boundary = event.type === 'workflow-event' || event.type === 'runtime-result';
342
+ // Flush buffered output before the projection advances the current step.
343
+ if (boundary)
344
+ readableLines(readableVerification.project(event));
391
345
  try {
392
346
  options.onRuntimeEvent?.(event);
393
347
  }
@@ -400,6 +354,8 @@ async function runAgentRuntimeInvocation(options) {
400
354
  if (event.type === 'runtime-graph')
401
355
  graph = event;
402
356
  observe(() => options.onRawLine?.(event.repositories ? JSON.stringify(event) : line));
357
+ if (!boundary)
358
+ readableLines(readableVerification.project(event));
403
359
  if (event.type === 'runtime-result') {
404
360
  if (result)
405
361
  invalidProtocol = true;
@@ -435,11 +391,6 @@ async function runAgentRuntimeInvocation(options) {
435
391
  observe(() => options.onLine?.(`[${role}]${scope} ${payload.tool}${payload.detail ? ' ' + payload.detail : ''}\n`));
436
392
  }
437
393
  }
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
394
  else if (event.type === 'span') {
444
395
  // Trace spans are telemetry; the raw line is already recorded for diagnostics.
445
396
  }
@@ -461,6 +412,7 @@ async function runAgentRuntimeInvocation(options) {
461
412
  if (timer)
462
413
  clearTimeout(timer);
463
414
  lines.close();
415
+ readableLines(readableVerification.flush());
464
416
  const usage = result?.invocationUsage;
465
417
  const known = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
466
418
  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
  };
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.AgentRuntimeConfigError = exports.REVIEW_THRESHOLD_FLOORS = exports.MAX_OUTPUT_TOKENS_MAX = exports.MAX_OUTPUT_TOKENS_MIN = exports.CONTEXT_WINDOW_TOKENS_MAX = exports.CONTEXT_WINDOW_TOKENS_MIN = exports.RUNTIME_ROLE_ID = void 0;
6
+ exports.INHERIT_ROLE_PROMPT = exports.AgentRuntimeConfigError = exports.REVIEW_THRESHOLD_FLOORS = exports.MAX_OUTPUT_TOKENS_MAX = exports.MAX_OUTPUT_TOKENS_MIN = exports.CONTEXT_WINDOW_TOKENS_MAX = exports.CONTEXT_WINDOW_TOKENS_MIN = exports.RUNTIME_ROLE_ID = void 0;
7
7
  exports.stripDesktopConnectionFields = stripDesktopConnectionFields;
8
8
  exports.coreConnectionFieldGates = coreConnectionFieldGates;
9
9
  exports.validateLoopRuntimeSettings = validateLoopRuntimeSettings;
@@ -18,6 +18,8 @@ exports.saveRuntimeProviders = saveRuntimeProviders;
18
18
  exports.loadRuntimeConfigFile = loadRuntimeConfigFile;
19
19
  exports.loadAgentRuntimeConfig = loadAgentRuntimeConfig;
20
20
  exports.saveAgentRuntimeConfig = saveAgentRuntimeConfig;
21
+ exports.inheritedRolePrompt = inheritedRolePrompt;
22
+ exports.resolveInheritedRolePrompts = resolveInheritedRolePrompts;
21
23
  exports.validateRuntimeRolePrompts = validateRuntimeRolePrompts;
22
24
  exports.loadRuntimeRolePrompts = loadRuntimeRolePrompts;
23
25
  exports.saveRuntimeRolePrompts = saveRuntimeRolePrompts;
@@ -419,6 +421,27 @@ function saveAgentRuntimeConfig(project, input) {
419
421
  return config;
420
422
  }
421
423
  const PROMPT_ROLES = [...ROLES, 'fixer'];
424
+ /** A loop recipe definition that follows the engine: resolved at launch to the selected Core's factory definition plus the user's global override. */
425
+ exports.INHERIT_ROLE_PROMPT = 'inherit';
426
+ const INHERITED_ROLE_PROMPT = /^inherit:(architect|developer|reviewer|fixer)$/;
427
+ /** The prompt a declared recipe role carries when it follows a built-in definition (`inherit:<role>`), or the recipe's own text. */
428
+ function inheritedRolePrompt(prompts, role) {
429
+ const value = prompts?.[role];
430
+ return value === undefined || value === exports.INHERIT_ROLE_PROMPT ? `${exports.INHERIT_ROLE_PROMPT}:${role}` : value;
431
+ }
432
+ /** Replace `inherit` definitions (built-in roles) and `inherit:<role>` prompts (declared roles) with the effective engine definitions. Explicit recipe text is kept verbatim. */
433
+ function resolveInheritedRolePrompts(settings, effective) {
434
+ // A Core predating the fixer stance has no fixer default: the fixer then follows the developer definition.
435
+ const resolved = { ...effective, fixer: effective.fixer ?? effective.developer };
436
+ const lookup = (role) => resolved[role];
437
+ const rolePrompts = settings.rolePrompts && Object.fromEntries(Object.entries(settings.rolePrompts).map(([role, text]) => [role, text === exports.INHERIT_ROLE_PROMPT ? lookup(role) ?? text : text]));
438
+ const roles = settings.roles && Object.fromEntries(Object.entries(settings.roles).map(([id, role]) => {
439
+ const inherited = role.prompt?.match(INHERITED_ROLE_PROMPT);
440
+ const text = inherited ? lookup(inherited[1]) : undefined;
441
+ return [id, text ? { ...role, prompt: text } : role];
442
+ }));
443
+ return { ...settings, ...(rolePrompts ? { rolePrompts } : {}), ...(roles ? { roles } : {}) };
444
+ }
422
445
  function validateRuntimeRolePrompts(input, extraRoles = []) {
423
446
  if (!input || typeof input !== 'object' || Array.isArray(input))
424
447
  throw new AgentRuntimeConfigError('Role prompts must be an object');