specrails-desktop 2.25.0 → 2.26.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 (119) hide show
  1. package/client/dist/assets/{ActivityFeedPage-BGRTOOhT.js → ActivityFeedPage-1eXnoNAo.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-DvDLxsUy.js → AgentBrowserCapture-BHW_6Vm0.js} +1 -1
  3. package/client/dist/assets/{AgentModeCodePane-DfwctjzN.js → AgentModeCodePane-C89SAJzI.js} +2 -2
  4. package/client/dist/assets/{AgentModeJobsPane-G3WrS-sE.js → AgentModeJobsPane-BG_4r4Dm.js} +1 -1
  5. package/client/dist/assets/{AgentsPage-DGFFijMC.js → AgentsPage-BrkWfc9s.js} +1 -1
  6. package/client/dist/assets/{AnalyticsPage-hoJmlEpk.js → AnalyticsPage-CMZRUPrT.js} +1 -1
  7. package/client/dist/assets/{BarChart-BEhAXVK-.js → BarChart-BtF3zJHa.js} +1 -1
  8. package/client/dist/assets/{CodePage-B4zB9DVC.js → CodePage-Cime5FRs.js} +1 -1
  9. package/client/dist/assets/{DesktopAnalyticsPage-CBX-Qpxd.js → DesktopAnalyticsPage-CSKHSBBF.js} +1 -1
  10. package/client/dist/assets/{DocsDialog-lTsnNdAM.js → DocsDialog-ClTq6Fzj.js} +1 -1
  11. package/client/dist/assets/{DocsPage-CkvdGroT.js → DocsPage-DTxfOpTu.js} +1 -1
  12. package/client/dist/assets/{ExportDropdown-Da98gR-P.js → ExportDropdown-Ho9zdPvK.js} +1 -1
  13. package/client/dist/assets/{IntegrationsPage-Cf9o4eZq.js → IntegrationsPage-DWNyo6jk.js} +1 -1
  14. package/client/dist/assets/{InteractiveJobComposer-C8N-Bbnn.js → InteractiveJobComposer-CYxXOKld.js} +1 -1
  15. package/client/dist/assets/{JobDetailModal-BZVwIIEx.js → JobDetailModal-DrNafHY5.js} +1 -1
  16. package/client/dist/assets/{JobDetailPage-CQewQ1zy.js → JobDetailPage-4VKw33GW.js} +1 -1
  17. package/client/dist/assets/{JobsPage-CB4wOxmE.js → JobsPage-cFr6wWaH.js} +1 -1
  18. package/client/dist/assets/{LoopBuilderPage-B9Y5gt08.js → LoopBuilderPage-Bk8zVvEX.js} +1 -1
  19. package/client/dist/assets/{LoopPreviewModal-kxQ5kV1t.js → LoopPreviewModal-XFSO2Vcs.js} +1 -1
  20. package/client/dist/assets/{LoopsPage-RysW-6sC.js → LoopsPage-DWMwDj9g.js} +1 -1
  21. package/client/dist/assets/{ProjectSettingsDialog-BHN5qsGT.js → ProjectSettingsDialog-DnSe_yWm.js} +1 -1
  22. package/client/dist/assets/{TemplatePreviewModal-DW6lZCIs.js → TemplatePreviewModal-B30-TY_v.js} +1 -1
  23. package/client/dist/assets/agent-6mhkV25_2.js +1 -0
  24. package/client/dist/assets/agent-BZqfQhI_.js +1 -0
  25. package/client/dist/assets/agent-Bjy1SlT5.js +1 -0
  26. package/client/dist/assets/agent-Bn6ZmjHj.js +1 -0
  27. package/client/dist/assets/{agent-G6Zn2gpr.js → agent-CRYdi9-t.js} +1 -1
  28. package/client/dist/assets/agent-D5QNn9np2.js +1 -0
  29. package/client/dist/assets/agent-DgFoLgrF2.js +1 -0
  30. package/client/dist/assets/agent-DgwPsxWQ.js +1 -0
  31. package/client/dist/assets/{brain-B9dkGuAN.js → brain-DcXkQwzO.js} +1 -1
  32. package/client/dist/assets/code-CwnBPPyA.js +1 -0
  33. package/client/dist/assets/{dashboard-4WX7agHF2.js → dashboard-CuGuy2__2.js} +1 -1
  34. package/client/dist/assets/{dashboard-IbEztmJ52.js → dashboard-CxP3TZ3I2.js} +1 -1
  35. package/client/dist/assets/{dashboard-5Z_esXIB.js → dashboard-DWHVfP7U.js} +1 -1
  36. package/client/dist/assets/{dashboard-DQ-po1MG2.js → dashboard-D_Pgs6QH2.js} +1 -1
  37. package/client/dist/assets/{dashboard-oqS7VRrK.js → dashboard-Dwnssayh.js} +1 -1
  38. package/client/dist/assets/{dashboard-ojWdDq6S.js → dashboard-S_pT1yxN.js} +1 -1
  39. package/client/dist/assets/{dashboard-zfoJcdFW.js → dashboard-tCxOFOxx.js} +1 -1
  40. package/client/dist/assets/{dashboard-nc-Raovv.js → dashboard-zefbBNyG.js} +1 -1
  41. package/client/dist/assets/{dist-js-BW5-IZTP.js → dist-js-6U6hyD6z.js} +1 -1
  42. package/client/dist/assets/{dist-js-Bbd7Wzdw.js → dist-js-BAWwLSIj.js} +1 -1
  43. package/client/dist/assets/index-DnkfABxY.js +156 -0
  44. package/client/dist/assets/index-cvOunQkD.css +2 -0
  45. package/client/dist/assets/{lib-EIkIPf26.js → lib-Bsny0zZp.js} +1 -1
  46. package/client/dist/assets/{loops-YYV2bwjd.js → loops-Brnx4Fji.js} +1 -1
  47. package/client/dist/assets/{loops-hXgncEpF2.js → loops-BvqQ-bHR2.js} +1 -1
  48. package/client/dist/assets/{loops-BvEenWBy.js → loops-CvOWsi5r.js} +1 -1
  49. package/client/dist/assets/{loops-DjOtVXRo.js → loops-D6D3XHjj.js} +1 -1
  50. package/client/dist/assets/{loops-HTHf-xu32.js → loops-DJNhp17p2.js} +1 -1
  51. package/client/dist/assets/{loops-kYbydDyN.js → loops-K5_HXfJa.js} +1 -1
  52. package/client/dist/assets/loops-LIgO_-eM.js +1 -0
  53. package/client/dist/assets/{loops-DNGiYKll2.js → loops-vhJdbcz92.js} +1 -1
  54. package/client/dist/assets/{settings-DZJeXoRV.js → settings-BWIf3vv4.js} +1 -1
  55. package/client/dist/assets/{settings-TNga5gIF2.js → settings-BhuXe7ek2.js} +1 -1
  56. package/client/dist/assets/{settings-D6qY9_UI.js → settings-DE8t6aYv.js} +1 -1
  57. package/client/dist/assets/{settings-BsJMsWGc.js → settings-DHd1lQ1k.js} +1 -1
  58. package/client/dist/assets/{settings-BOHOmHkb.js → settings-DTYQ5COE.js} +1 -1
  59. package/client/dist/assets/settings-DXgPh9-k.js +1 -0
  60. package/client/dist/assets/{settings-BlEA_7h02.js → settings-Dgv_q4qf2.js} +1 -1
  61. package/client/dist/assets/{settings-lFoaYT-s2.js → settings-FJXpJ3k92.js} +1 -1
  62. package/client/dist/assets/{setup-CmU6bTCV2.js → setup-cnqZsQSX2.js} +1 -1
  63. package/client/dist/assets/{upload-BCMl-nCg.js → upload-49vX83IZ.js} +1 -1
  64. package/client/dist/assets/{value-CG-amSq1.js → value-Bo8EiTZE.js} +1 -1
  65. package/client/dist/index.html +6 -6
  66. package/docs/agy-cli-provider-study.md +1 -1
  67. package/docs/codex.md +2 -2
  68. package/docs/gemini-cli-provider-study.md +2 -2
  69. package/docs/guide/de/pipeline/3-batch-implement-and-multi-feature.md +1 -1
  70. package/docs/guide/en/pipeline/3-batch-implement-and-multi-feature.md +1 -1
  71. package/docs/guide/es/pipeline/3-batch-implement-and-multi-feature.md +1 -1
  72. package/docs/guide/fr/pipeline/3-batch-implement-and-multi-feature.md +1 -1
  73. package/docs/guide/it/pipeline/3-batch-implement-and-multi-feature.md +1 -1
  74. package/docs/guide/ja/pipeline/3-batch-implement-and-multi-feature.md +1 -1
  75. package/docs/guide/pt/pipeline/3-batch-implement-and-multi-feature.md +1 -1
  76. package/docs/guide/pt/pipeline/5-the-loop-builder.md +1 -1
  77. package/docs/guide/zh/pipeline/3-batch-implement-and-multi-feature.md +1 -1
  78. package/docs/internals/api-reference.md +3 -3
  79. package/docs/internals/companion-rails-as-loops-contract.md +6 -6
  80. package/docs/internals/configuration.md +1 -1
  81. package/docs/internals/interactive-jobs.md +3 -3
  82. package/docs/internals/safe-pr-review-flow.md +1 -1
  83. package/docs/internals/visual-design-audit.md +2 -2
  84. package/docs/running-pipelines.md +1 -1
  85. package/package.json +1 -1
  86. package/server/dist/agent-chat-manager.js +16 -2
  87. package/server/dist/agent-chat-router.js +66 -1
  88. package/server/dist/agent-context-resolver.js +301 -0
  89. package/server/dist/agent-operator-prompt.js +14 -7
  90. package/server/dist/agent-store.js +24 -12
  91. package/server/dist/db.js +17 -17
  92. package/server/dist/desktop-db.js +6 -0
  93. package/server/dist/feature-flags.js +2 -2
  94. package/server/dist/index.js +1 -1
  95. package/server/dist/interactive-job-session.js +3 -3
  96. package/server/dist/loop-command-catalog.js +11 -11
  97. package/server/dist/loop-factory.js +7 -7
  98. package/server/dist/loop-graph.js +2 -2
  99. package/server/dist/loop-run-manager.js +1 -1
  100. package/server/dist/mcp/guide.js +8 -6
  101. package/server/dist/mcp/tools/rails.js +4 -3
  102. package/server/dist/mobile/mobile-router.js +1 -1
  103. package/server/dist/project-router-jobs.js +1 -1
  104. package/server/dist/project-router-loop-runs.js +1 -1
  105. package/server/dist/project-router-settings.js +5 -5
  106. package/server/dist/queue-manager.js +27 -27
  107. package/server/dist/rails-router.js +20 -20
  108. package/client/dist/assets/agent-4ARs0uac2.js +0 -1
  109. package/client/dist/assets/agent-C55wwsUX.js +0 -1
  110. package/client/dist/assets/agent-C_kqyrXo.js +0 -1
  111. package/client/dist/assets/agent-Df3lnHMu.js +0 -1
  112. package/client/dist/assets/agent-MQhun7cv.js +0 -1
  113. package/client/dist/assets/agent-NqlER4Yh2.js +0 -1
  114. package/client/dist/assets/agent-OTGI6t7e2.js +0 -1
  115. package/client/dist/assets/code-CqCcRThR.js +0 -1
  116. package/client/dist/assets/index-BtAks9nm.css +0 -2
  117. package/client/dist/assets/index-DC-REAcL.js +0 -155
  118. package/client/dist/assets/loops-CBiWd9Hf.js +0 -1
  119. package/client/dist/assets/settings-Mr7MnJbf.js +0 -1
@@ -14,7 +14,7 @@ Gemini supera a Codex en dos capacidades clave verificadas:
14
14
 
15
15
  **Entra en v1 (PR-B beta-gated):** jobs/rails básicos, Explore Spec multi-turno (spawn-per-turn con `--resume`), Quick spec, Analytics/coste vía rate-card, OTEL nativo, terminal CLI launch, detección + prerequisitos, selección de modelo, multi-provider per project.
16
16
 
17
- **NO entra en v1 (paridad Codex, se ocultan por intersección de capacidades):** Agent Profiles, SMASH, Contract Refine, Ultracode (+interactivo), plugins/Serena, persistent-stdin de Explore. Gemini se comporta byte-idéntico a Codex en estas superficies **siempre que declare las capacidades correctas y se ensanchen los tipos** — cero código nuevo en esas superficies.
17
+ **NO entra en v1 (paridad Codex, se ocultan por intersección de capacidades):** Agent Profiles, SMASH, Contract Refine, Freestyle (+interactivo), plugins/Serena, persistent-stdin de Explore. Gemini se comporta byte-idéntico a Codex en estas superficies **siempre que declare las capacidades correctas y se ensanchen los tipos** — cero código nuevo en esas superficies.
18
18
 
19
19
  **Diferido a follow-up explícito (decisiones tomadas, no implementadas en v1):** generalizar `provider-capabilities.ts` para *otorgar* a Gemini features hoy claude-only; bridge Windows multi-line argv; traducción slash-command para rails funcionales en Gemini (esto último es **bloqueante para rails reales**, ver §6).
20
20
 
@@ -238,7 +238,7 @@ Mergeable independiente, no cambia comportamiento observable.
238
238
  | **Integrations/plugins (MCP/Serena)** | **Bloqueado (paridad codex)** | Manifest Serena omite `providerSupport` → claude-only. Paridad: añadir `'gemini'` a `providerSupport` + entry MCP bajo `.gemini/settings.json`. |
239
239
  | **SMASH** | **Bloqueado (paridad codex)** | `isSmashCapable→'claude'`. `smash-runner.ts` spawnea `claude` directo sin adapter. Paridad: reescribir runner sobre `getAdapter()` + prompt/parser Gemini + flip del gate. |
240
240
  | **Contract Refine** | **Bloqueado (inherentemente anthropic en su forma actual)** | `--resume` a sesión Claude + slash `/specrails:contract-refine`. Paridad parcial vía el path no-resume (re-seed por system prompt, como `runContractRefineForQuick`) + de-hardcodear `getAdapter('claude')`. |
241
- | **Ultracode interactivo** | **Bloqueado (paridad codex)** | `mode==='ultracode' && provider!=='claude'→400`; `isUltracode=adapter.id==='claude'`. Paridad: quitar el 400 para gemini, generalizar el branch ultracode-prompt, `persistentStdin:true` para el interactivo. Mecanismo no-anthropic; factible. |
241
+ | **Freestyle interactivo** | **Bloqueado (paridad codex)** | `mode==='freestyle' && provider!=='claude'→400`; `isFreestyle=adapter.id==='claude'`. Paridad: quitar el 400 para gemini, generalizar el branch freestyle-prompt, `persistentStdin:true` para el interactivo. Mecanismo no-anthropic; factible. |
242
242
  | **Analytics/coste** | **Nativo (estimado)** | `nativeCostUsd:false` + filas pricing → coste estimado, `estimated=true`. Filtros engine ya data-driven. |
243
243
  | **OTEL/telemetry** | **Nativo (MEJOR que codex)** | `nativeOtelEnv:true` → env-injection (no bridge). Solo parametrizar el enable-var en `buildTelemetryEnv`. Caveat correlación (§4). |
244
244
  | **Terminal CLI launch** | **Nativo** | `CliLaunchMenu` itera providers; aparece tras ensanchar union + wiring binario. |
@@ -81,4 +81,4 @@ Es gibt kein globales Parallelitäts-Limit, das du einstellen musst. Öffne die
81
81
 
82
82
  - [Rails & Jobs](rails-and-jobs) — das Queue-Modell im Detail.
83
83
  - [Die Job-Detail-Ansicht](the-job-detail-view) — einem Batch-Lauf live zusehen.
84
- - [Engine pro Rail wählen](picking-an-engine-per-rail) — beachte: Batch läuft auf jedem Provider; Ultra gibt es nur bei Claude.
84
+ - [Engine pro Rail wählen](picking-an-engine-per-rail) — beachte: Batch läuft auf jedem Provider; Freestyle gibt es nur bei Claude.
@@ -83,4 +83,4 @@ There's no global concurrency limit to tune. Open the projects or rails you need
83
83
 
84
84
  - [Rails & jobs](rails-and-jobs) — the queue model in depth.
85
85
  - [The Job Detail view](the-job-detail-view) — watch a batch run live.
86
- - [Picking an engine per rail](picking-an-engine-per-rail) — note that Batch runs on any provider; Ultra is Claude-only.
86
+ - [Picking an engine per rail](picking-an-engine-per-rail) — note that Batch runs on any provider; Freestyle is Claude-only.
@@ -81,4 +81,4 @@ No hay límite global de concurrencia que ajustar. Abre los proyectos o rails qu
81
81
 
82
82
  - [Rails y jobs](rails-and-jobs) — el modelo de cola en profundidad.
83
83
  - [La vista de detalle del job](the-job-detail-view) — mira un batch ejecutarse en vivo.
84
- - [Elegir un motor por rail](picking-an-engine-per-rail) — ten en cuenta que Batch corre en cualquier proveedor; Ultra es solo de Claude.
84
+ - [Elegir un motor por rail](picking-an-engine-per-rail) — ten en cuenta que Batch corre en cualquier proveedor; Freestyle es solo de Claude.
@@ -81,4 +81,4 @@ Il n'y a aucune limite globale de concurrence à régler. Ouvrez les projets ou
81
81
 
82
82
  - [Rails et jobs](rails-and-jobs) — le modèle de file d'attente en détail.
83
83
  - [La vue détaillée du job](the-job-detail-view) — regarder un batch s'exécuter en direct.
84
- - [Choisir un moteur par rail](picking-an-engine-per-rail) — notez que Batch fonctionne sur n'importe quel fournisseur ; Ultra est réservé à Claude.
84
+ - [Choisir un moteur par rail](picking-an-engine-per-rail) — notez que Batch fonctionne sur n'importe quel fournisseur ; Freestyle est réservé à Claude.
@@ -81,4 +81,4 @@ Non c'è alcun limite globale di concorrenza da regolare. Apri i progetti o i ra
81
81
 
82
82
  - [Rail e job](rails-and-jobs) — il modello della coda in dettaglio.
83
83
  - [La vista Dettaglio job](the-job-detail-view) — guarda un batch in esecuzione dal vivo.
84
- - [Scegliere un engine per ogni rail](picking-an-engine-per-rail) — nota che il Batch gira su qualsiasi provider; Ultra è solo Claude.
84
+ - [Scegliere un engine per ogni rail](picking-an-engine-per-rail) — nota che il Batch gira su qualsiasi provider; Freestyle è solo Claude.
@@ -81,4 +81,4 @@ Batch モードは、関連するスペックを*順番に処理する*一番す
81
81
 
82
82
  - [レールとジョブ](rails-and-jobs) — キューモデルを掘り下げて。
83
83
  - [ジョブ詳細ビュー](the-job-detail-view) — バッチ実行をライブで見守る。
84
- - [レールごとのエンジン選択](picking-an-engine-per-rail) — Batch はどのプロバイダーでも動きますが、Ultra は Claude 専用である点に注意。
84
+ - [レールごとのエンジン選択](picking-an-engine-per-rail) — Batch はどのプロバイダーでも動きますが、Freestyle は Claude 専用である点に注意。
@@ -81,4 +81,4 @@ Não há limite global de concorrência para ajustar. Abra os projetos ou rails
81
81
 
82
82
  - [Rails e jobs](rails-and-jobs) — o modelo da fila em detalhe.
83
83
  - [A vista de detalhe do job](the-job-detail-view) — ver um batch a correr ao vivo.
84
- - [Escolher um motor por rail](picking-an-engine-per-rail) — note que o Batch corre em qualquer fornecedor; o Ultra é só Claude.
84
+ - [Escolher um motor por rail](picking-an-engine-per-rail) — note que o Batch corre em qualquer fornecedor; o Freestyle é só Claude.
@@ -57,7 +57,7 @@ Um loop que nunca para queimaria dinheiro para sempre, então toda execução te
57
57
  |-------|--------------|
58
58
  | **Max iterations** | Teto rígido de quantas vezes o Decider pode voltar atrás, independentemente do seu veredito. |
59
59
  | **Timeout (min)** | Limite de tempo de relógio para toda a execução. |
60
- | **Max cost ($)** | *Opcional.* Para o loop quando o custo acumulado cruza o seu orçamento. Verificado **entre passos** (o custo de um passo só é conhecido quando ele termina), então pode ultrapassar em um passo. No Claude o custo é exato; no Codex e no Gemini é uma estimativa. Deixe vazio para não ter teto. |
60
+ | **Max cost ($)** | *Opcional.* Para o loop quando o custo acumulado cruza o seu orçamento. Verificado **entre passos** (o custo de um passo só é conhecido quando ele termina), então pode freestylepassar em um passo. No Claude o custo é exato; no Codex e no Gemini é uma estimativa. Deixe vazio para não ter teto. |
61
61
 
62
62
  ## Construindo com confiança
63
63
 
@@ -81,4 +81,4 @@ Project B ▶ Rail running feature Y ┘
81
81
 
82
82
  - [Rail 与任务](rails-and-jobs)——深入理解队列模型。
83
83
  - [任务详情视图](the-job-detail-view)——实时观看一次批量运行。
84
- - [为每条 rail 选择引擎](picking-an-engine-per-rail)——注意 Batch 可在任意提供方上运行;Ultra 仅限 Claude。
84
+ - [为每条 rail 选择引擎](picking-an-engine-per-rail)——注意 Batch 可在任意提供方上运行;Freestyle 仅限 Claude。
@@ -199,7 +199,7 @@ Example — queue a command on a specific provider:
199
199
  | `POST` | `/jobs/:id/finalize` | Finalize a running interactive job (SIGTERM the resident child; final totals + terminal status are stamped when it closes). For a loop run this settles the **current step** and the loop advances. `202` scheduled, `403` interactive jobs disabled, `409` not an active interactive session |
200
200
  | `GET` | `/jobs/:jobId/diagnostic` | Stream a diagnostic ZIP (telemetry + profile + plugins snapshots) |
201
201
 
202
- The two interactive endpoints back the in-job chat that is **on by default for every Claude job** — QueueManager jobs (implement / batch / Freestyle / custom commands) and the loop engine's claude ai-steps alike. Two settle modes: Freestyle/ultracode jobs idle until an explicit finalize (`'finalize'`); everything else settles itself on quiescence (`'auto'` — a turn result with nothing queued), where finalize acts as "wrap up now" / "settle this step". Providers without persistent stdin (Codex, Gemini) run one-shot as before and 409 here. Both endpoints 403 when the server has `SPECRAILS_INTERACTIVE_JOBS=false`. As-built detail: [interactive-jobs.md](interactive-jobs.md).
202
+ The two interactive endpoints back the in-job chat that is **on by default for every Claude job** — QueueManager jobs (implement / batch / Freestyle / custom commands) and the loop engine's claude ai-steps alike. Two settle modes: Freestyle/freestyle jobs idle until an explicit finalize (`'finalize'`); everything else settles itself on quiescence (`'auto'` — a turn result with nothing queued), where finalize acts as "wrap up now" / "settle this step". Providers without persistent stdin (Codex, Gemini) run one-shot as before and 409 here. Both endpoints 403 when the server has `SPECRAILS_INTERACTIVE_JOBS=false`. As-built detail: [interactive-jobs.md](interactive-jobs.md).
203
203
 
204
204
  Example — fetch one job:
205
205
 
@@ -435,7 +435,7 @@ A non-developer-friendly file tree + Monaco viewer with plain-language AI summar
435
435
  | `PUT` | `/:railIndex/profile` | Set the rail's default profile |
436
436
  | `PUT` | `/:railIndex/engine` | Set the rail's AI engine override. Body `{ aiEngine }` (string — one of the project's providers — or `null` to clear) |
437
437
  | `PUT` | `/:railIndex/name` | Set the rail's display name. Body `{ name }` (string or `null` to clear back to the default label); 400 if longer than 60 characters |
438
- | `POST` | `/:railIndex/launch` | Launch the rail. Body `{ mode?, loopId?, profileName?, aiEngine?, model?, reasoning_effort?, originConversationId?, originSurface?, interactive? }`. `mode` is `implement` / `batch-implement` / `ultracode` / `loop`; a factory `loopId` (`factory:implement` etc.) maps to its legacy mode, and a **bare legacy mode with no `loopId`** (MCP / mobile / direct REST) derives the matching factory loop when Loops are enabled, so every launch door gets the same isolation + ask-first PR flow as the dashboard. `model` (haiku/sonnet/opus/fable) applies to ultracode only. The in-job chat is **on by default for every Claude job** — the legacy `interactive` param is accepted and **ignored** (wire compat; the spawn-time gate decides). Returns `202 { jobId, railIndex, mode }`; **`503`** when the Claude or Codex CLI binary is not found (a missing Gemini/other binary surfaces as `500`) |
438
+ | `POST` | `/:railIndex/launch` | Launch the rail. Body `{ mode?, loopId?, profileName?, aiEngine?, model?, reasoning_effort?, originConversationId?, originSurface?, interactive? }`. `mode` is `implement` / `batch-implement` / `freestyle` / `loop`; a factory `loopId` (`factory:implement` etc.) maps to its canonical rail mode, and a **bare mode with no `loopId`** (MCP / mobile / direct REST) derives the matching factory loop when Loops are enabled, so every launch door gets the same isolation + ask-first PR flow as the dashboard. `model` (haiku/sonnet/opus/fable) applies to freestyle only. The in-job chat is **on by default for every Claude job** — the `interactive` param is accepted and **ignored** (wire compat; the spawn-time gate decides). Returns `202 { jobId, railIndex, mode }`; **`503`** when the Claude or Codex CLI binary is not found (a missing Gemini/other binary surfaces as `500`) |
439
439
  | `POST` | `/:railIndex/stop` | Stop the rail's running job (cancels every job the rail registered) |
440
440
 
441
441
  ---
@@ -444,7 +444,7 @@ A non-developer-friendly file tree + Monaco viewer with plain-language AI summar
444
444
 
445
445
  Gated by `SPECRAILS_AGENTS_SECTION !== 'false'`. Rails force legacy (no-profile) mode whenever the chosen engine is not Claude, and the Agents section is hidden in multi-provider projects; profile env-injection itself works for any provider whose adapter advertises `profileEnvSupport`.
446
446
 
447
- > **Provider capability notes.** A few behaviours are not the same across all three providers: **Contract Refine** is Claude-only; **rails force legacy (no-profile) mode** for any non-Claude engine; **Ultracode rails** are Claude-only; the **Serena plugin** (and other `project-json` plugins) work for Claude and Gemini but are filtered out for Codex; and **cost is exact only for Claude** (estimated from a rate card for Codex and Gemini). See the [Codex guide](../codex.md) and [Gemini guide](../gemini.md).
447
+ > **Provider capability notes.** A few behaviours are not the same across all three providers: **Contract Refine** is Claude-only; **rails force no-profile mode** for any non-Claude engine; **Freestyle rails** are Claude-only; the **Serena plugin** (and other `project-json` plugins) work for Claude and Gemini but are filtered out for Codex; and **cost is exact only for Claude** (estimated from a rate card for Codex and Gemini). See the [Codex guide](../codex.md) and [Gemini guide](../gemini.md).
448
448
 
449
449
  ### Profile CRUD
450
450
 
@@ -45,10 +45,10 @@ The companion is a **light-control mirror**. It has no typed `Rail` model: rails
45
45
 
46
46
  ## 2. What rails-as-loops changed — the divergence list
47
47
 
48
- - **`loopId` is the new primary launch verb.** `POST /rails/:i/launch` now accepts `{mode='implement', profileName, aiEngine, model, interactive, loopId, reasoning_effort}` (`rails-router.ts:214-215`). `VALID_MODES` is now `{implement, batch-implement, ultracode, loop}` (`rails-router.ts:25`).
48
+ - **`loopId` is the new primary launch verb.** `POST /rails/:i/launch` now accepts `{mode='implement', profileName, aiEngine, model, interactive, loopId, reasoning_effort}` (`rails-router.ts:214-215`). `VALID_MODES` is now `{implement, batch-implement, freestyle, loop}` (`rails-router.ts:25`).
49
49
  - **The mode segmented-control was removed** from the desktop UI and replaced by `RailLoopSelector` (`RailControls.tsx:51-53` comment; `RailRow.tsx:414-421, 597-604`). `RailMode` still exists but is now **derived** from the loop via `deriveRailMode()` (`rail-loops.ts:34-39`), not user-picked.
50
- - **Factory→mode mapping (the compat layer).** Client factory ids `factory:implement|factory:batch|factory:ultracode` (`rail-loops.ts:8-25`); server mirror `loop-factory.ts:33-55`. `factoryLoopMode()` maps `factory:implement→implement`, `factory:batch→batch-implement`, `factory:ultracode→ultracode` (`rails-router.ts:220-224`); any custom loop → `mode='loop'`.
51
- - **Loop runs are NOT job-queue jobs.** When `isLoopsEnabled()` (default ON, `feature-flags.ts:51-52`, `!== 'false'`) AND a `loopId` is present (factory OR custom), the launch routes through **`LoopRunManager`**, not `QueueManager` (`rails-router.ts:301-381`). It returns `202 {loopRunIds, railIndex, mode}` with **no `jobId`** (`rails-router.ts:379`), and rows live in a **separate `loop_runs` table** (`loop-runs-store.ts:12-32`) keyed by `loopRunId`. The legacy ultracode/implement paths still return `{jobId, ...}` (`rails-router.ts:428, 453`).
50
+ - **Factory→mode mapping (the compat layer).** Client factory ids `factory:implement|factory:batch|factory:freestyle` (`rail-loops.ts:8-25`); server mirror `loop-factory.ts:33-55`. `factoryLoopMode()` maps `factory:implement→implement`, `factory:batch→batch-implement`, `factory:freestyle→freestyle` (`rails-router.ts:220-224`); any custom loop → `mode='loop'`.
51
+ - **Loop runs are NOT job-queue jobs.** When `isLoopsEnabled()` (default ON, `feature-flags.ts:51-52`, `!== 'false'`) AND a `loopId` is present (factory OR custom), the launch routes through **`LoopRunManager`**, not `QueueManager` (`rails-router.ts:301-381`). It returns `202 {loopRunIds, railIndex, mode}` with **no `jobId`** (`rails-router.ts:379`), and rows live in a **separate `loop_runs` table** (`loop-runs-store.ts:12-32`) keyed by `loopRunId`. QueueManager fallback freestyle/implement paths still return `{jobId, ...}` (`rails-router.ts:428, 453`).
52
52
  - **New WS lifecycle family.** `loop.run_started`/`loop.run_progress`/`loop.run_stopped`/`loop.run_completed` (`types.ts:756-790`), emitted by `LoopRunManager` (`loop-run-manager.ts:469`), NOT by the `project-registry.ts:543` `onJobFinished` path that emits `rail.job_completed`. Keying: `loop.run_started`/`loop.run_stopped`/`loop.run_completed` carry `railIndex` + `loopRunId`; **`loop.run_progress` carries only `loopRunId`/`iteration`/`activeNode`/`reasoning` — NO `railIndex`** (`types.ts:756-790`). `loop.run_completed.status ∈ {success, max-iterations, stopped, failed, blocked, stalled}` — `max-iterations` is a **successful** terminal state, not a failure; **`blocked`** (the Loop Decider flagged the run needs a human decision it can't make) and **`stalled`** (the non-convergence guard aborted after consecutive iterations left the working tree unchanged) are controlled HALTS: NOT in the success set, and the settle maps them to job status `canceled` (tickets revert to todo, no PR delivery). A naive `ok = status=='success'` already treats them as non-success — correct.
53
53
  - **`GET /rails` gained `activeLoopRuns`** → `{rails, activeJobs:{[idx]:{jobId,mode}}, activeLoopRuns:{[idx]:{loopRunId}}}` (`rails-router.ts:78-82`).
54
54
  - **`loopId`/`effort` are NOT persisted on the rail row.** `RailState` is still `{railIndex, ticketIds, mode, profileName, aiEngine, name}` (`rails-store.ts:3-13`); rails table cols are `rail_index, ticket_id, position, mode, profile_name, ai_engine` (`db.ts:236`), `name` in `rail_meta` (migration 28). `loopId` is purely a per-launch param; `reasoning_effort` persists only on `loop_runs.reasoning_effort`.
@@ -73,7 +73,7 @@ The list fetch never crashes (untyped maps, tolerant parsers). But a rail runnin
73
73
  Loop runs have a `loopRunId`, never a `jobId`, and are **not in the jobs/queue tables** (`loop-runs-store.ts:12-32`). The companion's "View running job log" routes by `jobId` (`rails_zone.dart:271-281`); `watch_job`/`log_batch`/`job_event` key on `jobId` (`mobile-ws.ts:196-243`). With no `jobId`, the job-tail surface never populates and the run is absent from Execution History (`JobHistoryScreen` reads queue jobs only). **Critically, a loop run produces NO tailable log at all** — its sole live heartbeat is `loop.run_progress` (`iteration`/`activeNode`), which is dropped at `topicFor()`. So even discounting the job-log routing, there is **no live-progress channel** for a running loop rail today: the running surface is a binary running/idle black box. For legacy non-loop launches the job-tail still WORKS.
74
74
 
75
75
  **(c) LAUNCH a rail — WORKS (factory/legacy only) / BROKEN (custom loops).**
76
- The companion sends bare `{mode}` (`rails_zone.dart:317` picker = `implement|batch-implement|ultracode`; `desktop_repository.dart:116-123`). This still validates server-side (`VALID_MODES` includes the three; default `mode='implement'` at `rails-router.ts:214`). Crucially, the gateway narrowing **strips `loopId`** (`mobile-router.ts:191-196`), so even a future companion that sent `loopId` would be silently ignored. Custom loops are unreachable (`mode='loop'` without `loopId` → 400, `rails-router.ts:385-386`). **Caveat:** the *running-state feedback* after launch is broken per (a)/(b) when the launch resolves to a `LoopRunManager` run (default-ON factory loops emit `loop.run_*`, no `rail.job_started`) — so the launch HTTP call succeeds (202) but the phone sees no live activity, no progress, and no completion notification.
76
+ The companion sends bare `{mode}` (`rails_zone.dart:317` picker = `implement|batch-implement|freestyle`; `desktop_repository.dart:116-123`). This still validates server-side (`VALID_MODES` includes the three; default `mode='implement'` at `rails-router.ts:214`). Crucially, the gateway narrowing **strips `loopId`** (`mobile-router.ts:191-196`), so even a future companion that sent `loopId` would be silently ignored. Custom loops are unreachable (`mode='loop'` without `loopId` → 400, `rails-router.ts:385-386`). **Caveat:** the *running-state feedback* after launch is broken per (a)/(b) when the launch resolves to a `LoopRunManager` run (default-ON factory loops emit `loop.run_*`, no `rail.job_started`) — so the launch HTTP call succeeds (202) but the phone sees no live activity, no progress, and no completion notification.
77
77
 
78
78
  **(d) Stop / assign tickets / engine — DEGRADED (stop) / WORKS (tickets) / WORKS-with-display-drift (engine).**
79
79
  - **Engine:** `PUT /rails/:i/engine {aiEngine}` WORKS end-to-end for *setting* the override (`desktop_repository.dart:127`; `mobile-router.ts:199-206`); `rail.updated` carries `aiEngine` (`types.ts:749`). The mutation needs no change. **But** the *displayed* engine drifts for loop-backed runs as noted in (a): the rail config `aiEngine` may be null while the actual run used the resolved provider/model from `loop_runs`, which is never forwarded.
@@ -133,7 +133,7 @@ Extending `Job.fromJson` tolerance (`models.dart:73-85`) is unnecessary (loop ru
133
133
  - Tile subtitle (`:73-74, :87`) and sheet "Mode:" line (`:151, :218`): render `rail['loopName'] ?? rail['mode']` so a loop-running rail shows the loop identity, not stale `implement`. For engine, prefer the forwarded `rail['provider']`/`rail['model']` (4.3) over the possibly-null config `aiEngine`.
134
134
  - Running detection (`:72`): include `rail['loopRunId'] != null` so loop-running rails show Cancel, not Launch.
135
135
  - **Live-progress view:** for loop-backed rails, render `iteration`/`activeNode` from `loop.run_progress` (forwarded per 4.4) and/or poll `GET /v1/.../loop-runs/:id` (4.2) — this is the replacement for the `jobId` log tail that loop runs will never have. Do NOT route "View running job log" (`:271-281`) to `JobDetailScreen` by a null `jobId`; disable it or route to the loop-run progress view.
136
- - Launch picker (`confirmLaunchRail`, `:308-354, :317`): replace the hardcoded `implement|batch-implement|ultracode` bottom-sheet with a **Loop picker** populated from the new loop-catalog endpoint (4.2). Send `loopId` (factory or custom). Keep a fallback to bare `mode` for old-desktop compat (see phasing). For factory loops, the picker can offer the three factory ids (`factory:implement|factory:batch|factory:ultracode`).
136
+ - Launch picker (`confirmLaunchRail`, `:308-354, :317`): replace the hardcoded `implement|batch-implement|freestyle` bottom-sheet with a **Loop picker** populated from the new loop-catalog endpoint (4.2). Send `loopId` (factory or custom). Keep a fallback to bare `mode` for old-desktop compat (see phasing). For factory loops, the picker can offer the three factory ids (`factory:implement|factory:batch|factory:freestyle`).
137
137
 
138
138
  **5.4 WS handlers for loop runs — `lib/features/project/project_shell.dart`.**
139
139
  `project_shell.dart:62-80` handles `rail.*` only. Add a `startsWith('loop.run_')` branch **inside** the existing inbound guard (`:62`, the `projectId`-filter) — loop runs ARE `projectId`-scoped (carried via `boundBroadcast`) so they pass the filter; read `m['projectId']` consistently so the branch isn't accidentally gated. The branch must: (a) invalidate `railsProvider`/`jobsProvider` (same as `rail.*` at `:67`); (b) on `loop.run_completed`, fire `NotificationService.jobOutcome` mapping the **success set `{success, max-iterations}`** → success (note `max-iterations` is a *successful* terminal state, NOT a failure — a naive `ok = status=='success'` mislabels it), mirroring the `rail.job_completed`/`status=='completed'` path at `:70-77`, using `loopRunId`/`ticketIds`.
@@ -196,4 +196,4 @@ Add the loop-catalog/loop-run read routes (4.2/5.5), the Loop picker UI (5.3), a
196
196
 
197
197
  - **Job-history blindness persists.** Loop runs live in `loop_runs`, not `jobs`/`queue` (`loop-runs-store.ts:12-32`), so they will not appear in the companion's Execution History or `Job`-model cost surfacing from WS fixes alone. The `GET /loop-runs/:id` read route (4.2) feeds the *running* surface; full history parity additionally needs a list route + a `LoopRun` model — flag as a follow-up beyond the mirror passes.
198
198
 
199
- - **Single point holding mobile launch alive.** The factory→legacy-mode mapping (`rails-router.ts:220-224`) plus the bare-`mode` default (`:214`) is the only thing keeping the v1 companion's launch working. If desktop ever removes bare-`mode` acceptance, the companion's `{mode}`-only `launchRail` (`desktop_repository.dart:116-123`) 400s. Treat bare-`mode` acceptance as frozen.
199
+ - **Single point holding mobile launch alive.** The factory→legacy-mode mapping (`rails-router.ts:220-224`) plus the bare-`mode` default (`:214`) is the only thing keeping the v1 companion's launch working. If desktop ever removes bare-`mode` acceptance, the companion's `{mode}`-only `launchRail` (`desktop_repository.dart:116-123`) 400s. Treat bare-`mode` acceptance as frozen.
@@ -66,7 +66,7 @@ Project settings apply to a single project. Open them from the project's **Setti
66
66
  |---------|------------------|
67
67
  | **Pipeline Telemetry** | Opt-in toggle that injects OpenTelemetry env vars into pipeline job spawns so they emit OTLP signals back to the app. Off by default. |
68
68
  | **Rail Pre-prompt** | Text prepended to every rail launch for this project. |
69
- | **Ultracode pre-prompt** | Text prepended to Ultracode-mode launches (the Claude-only autonomous rail mode). |
69
+ | **Freestyle pre-prompt** | Text prepended to Freestyle-mode launches (the Claude-only autonomous rail mode). |
70
70
  | **Budget** | Per-project daily spend cap (with queue auto-pause) and a per-job cost alert threshold. |
71
71
  | **Terminal Settings** | Per-project overrides for the terminal panel defaults (project override → app default → built-in). |
72
72
 
@@ -29,7 +29,7 @@ duplicate `result` frame can never be folded into the next turn's totals (BUG-IN
29
29
 
30
30
  ### The spike that unlocked the flip
31
31
 
32
- The original interactive path was ultracode-only because ultracode sends prose, not a slash
32
+ The original interactive path was freestyle-only because freestyle sends prose, not a slash
33
33
  command. Spike-verified 2026-07-03 against **claude 2.1.198**: the claude CLI expands slash
34
34
  commands arriving as stream-json stdin user frames **exactly like the argv `-p "/cmd"` path**
35
35
  (evidence pointer: the dated comment block above the interactive gate in
@@ -60,7 +60,7 @@ spawnInteractive = isInteractiveJobsEnabled() // SPECRAILS_INTERACTIV
60
60
 
61
61
  | | `'finalize'` | `'auto'` |
62
62
  |---|---|---|
63
- | Who gets it | ultracode/Freestyle QueueManager jobs (claude) | every other interactive job + ALL loop ai-steps |
63
+ | Who gets it | freestyle/Freestyle QueueManager jobs (claude) | every other interactive job + ALL loop ai-steps |
64
64
  | End of session | explicit human **Finalize** only (SIGTERM → 2s → SIGKILL) | **quiescence**: a turn `result` arrived, nothing queued, no write in flight |
65
65
  | Idle between turns | by design (awaiting the human) | only transiently (microtask window) |
66
66
  | Wedge detector | never armed | the queue's zombie-timeout budget, reset on any raw child output; silence for the whole budget → fold in-flight turn, settle `crashed` |
@@ -148,7 +148,7 @@ command didn't actually run, the settle is FAILED.**
148
148
  so the synthetic text would otherwise never appear in the log.
149
149
  - **QueueManager** (`_settleInteractiveJob`): a zero-work `'finalized'` settle stamps the job
150
150
  `'failed'` (exit code 1, failed `ai_invocations` row, dependents skipped) — **in both settle
151
- modes**, so an ultracode Finalize after only the synthetic frame is also a failure. A canceled
151
+ modes**, so an freestyle Finalize after only the synthetic frame is also a failure. A canceled
152
152
  job stays `'canceled'`.
153
153
  - **LoopRunManager** (ai-steps): a zero-work settle makes the step's `AiStepResult` `failed` and
154
154
  its `loop_step_end` `status:'failed'`, and it **routes exactly like a crashed step** — the run
@@ -247,7 +247,7 @@ EVERY completed job/run promotes its tickets `todo|in_progress → on_review`, n
247
247
  "Per-run settle" above) — covering shared-cwd rail launches, standalone loop runs, and the
248
248
  isolation-unavailable fallback.
249
249
  - **QueueManager jobs** (`project-registry.ts` `onJobFinished`): bare-mode launches, MCP
250
- `/spawn` jobs, ultracode Finalize, interactive auto-settles. `QueueManager._startJob` reads
250
+ `/spawn` jobs, freestyle Finalize, interactive auto-settles. `QueueManager._startJob` reads
251
251
  `isRailPrDeliveryEnabled()` ONCE per job at spawn — the SAME read that injects
252
252
  `SPECRAILS_GIT_AUTO=false` — records it in the in-memory `_jobPrDelivery` map (restart-durable
253
253
  by construction, like the interactive gate: a queued job surviving a restart recomputes at its
@@ -100,7 +100,7 @@ Prereq (fix #1): `npm i tw-animate-css` + `@import "tw-animate-css";` in `global
100
100
  6. `components/ProjectHealthWidget.tsx:51,92,110-124,141` + `components/HealthIndicatorBadge.tsx:28-31` — `#50fa7b/#f1fa8c/#ff5555` → `text-accent-success/text-accent-warning/text-destructive` (`var(--color-*)` for Recharts fills); ProjectHealthWidget:124 `AlertCircle` → `Ban`.
101
101
  7. `components/terminal/TerminalSearchOverlay.tsx:94,105-138` — `#f8f8f2/#6272a4/#44475a` → `text-foreground/bg-accent-primary/40/bg-muted`; `:107,115,126` `size={14}` → `className="w-3.5 h-3.5"`.
102
102
  8. `types/context-scope.ts:51-54` — `text-white` ×4 in `submitAccentForTier` → `text-background` (white on pastel accent-warning is unreadable).
103
- 9. `components/UltracodeLaunchDialog.tsx:91` — `bg-emerald-500 text-white hover:bg-emerald-400 focus-visible:ring-emerald-400` → `bg-accent-success text-background hover:bg-accent-success/90 focus-visible:ring-accent-success`; `:58` `h-4.5 w-4.5` → `h-4 w-4` (only off-scale icon in the app).
103
+ 9. `components/FreestyleLaunchDialog.tsx:91` — `bg-emerald-500 text-white hover:bg-emerald-400 focus-visible:ring-emerald-400` → `bg-accent-success text-background hover:bg-accent-success/90 focus-visible:ring-accent-success`; `:58` `h-4.5 w-4.5` → `h-4 w-4` (only off-scale icon in the app).
104
104
  10. `components/RailControls.tsx:78-85` — raw red/emerald/amber + `aurora-light:` → `text-destructive/text-accent-success/text-accent-warning`; `:42` `hsl(191_97%_77%/0.22)` → `color-mix(in srgb, var(--color-accent-info) 22%, transparent)`; `:88` add `disabled:opacity-40`; bump button `h-5 w-5`→`h-7 w-7`, icon `w-2.5`→`w-3.5`.
105
105
  11. `components/RailsBoard.tsx:167` — `text-emerald-400 bg-emerald-400/10` → `text-accent-success bg-accent-success/10`.
106
106
  12. `components/ActiveJobCard.tsx:78,83` — `border-blue-500/30 bg-blue-500/5` / `text-blue-400` → `border-accent-info/30 bg-accent-info/5` / `text-accent-info` (copy JobStatusPanel:189).
@@ -171,7 +171,7 @@ A single idle rail exposes **13 controls, six of them 10px native `<select>`s ex
171
171
  **Stop guard:** Stop cancels N billed jobs — replace bare kill with a 5s undo toast (`ThemedToaster` action pattern, `App.tsx:434`) or a one-line confirm; no new component needed.
172
172
 
173
173
  ## 3.4 Reused components (nothing new except one popover)
174
- `ui/select.tsx` + `ui/tooltip.tsx` + `ui/dropdown` primitives (Radix, already shipped) · `PipelineProgress` (add a `variant="mini"` — h-1.5 bar + phase label) · `Badge` (post fix #3, for ticket/status chips) · `AgentActivityChip` crossfade pattern (copy verbatim for phase/status text) · `glow-*` utilities (`globals.css:345-350`) · `TicketDetailModalProvider` (compact ticket-pill clicks, unchanged) · `MoveToRailPopover`, `UltracodeLaunchDialog` (kept as-is, minus emerald CTA per fix #9) · existing WS events + `GET /rails` enrichment (`activeLoopRuns.loopName/provider/model/iteration` — zero server work to show run identity) · shared `<EmptyState>` (new, but mandated app-wide by §1.5). Compact density is **retired as a separate design system**: it becomes the same card with the body collapsed — the compact branch is already the token-correct one, so normal density adopts its token language rather than maintaining two.
174
+ `ui/select.tsx` + `ui/tooltip.tsx` + `ui/dropdown` primitives (Radix, already shipped) · `PipelineProgress` (add a `variant="mini"` — h-1.5 bar + phase label) · `Badge` (post fix #3, for ticket/status chips) · `AgentActivityChip` crossfade pattern (copy verbatim for phase/status text) · `glow-*` utilities (`globals.css:345-350`) · `TicketDetailModalProvider` (compact ticket-pill clicks, unchanged) · `MoveToRailPopover`, `FreestyleLaunchDialog` (kept as-is, minus emerald CTA per fix #9) · existing WS events + `GET /rails` enrichment (`activeLoopRuns.loopName/provider/model/iteration` — zero server work to show run identity) · shared `<EmptyState>` (new, but mandated app-wide by §1.5). Compact density is **retired as a separate design system**: it becomes the same card with the body collapsed — the compact branch is already the token-correct one, so normal density adopts its token language rather than maintaining two.
175
175
 
176
176
  ## 3.5 Phased plan
177
177
 
@@ -80,7 +80,7 @@ In plain terms: the project's **agent profile** decides which AI agent handles e
80
80
 
81
81
  ### Freestyle
82
82
 
83
- `Freestyle` (mode value `ultracode` on the API — its original name) is a Claude-only loop that skips the Architect → Developer → Reviewer → Ship pipeline entirely. Instead of orchestrating the agent chain, it hands Claude a configurable pre-prompt plus the full spec text and lets it work autonomously with its native tools.
83
+ `Freestyle` (mode value `freestyle` on the API — its original name) is a Claude-only loop that skips the Architect → Developer → Reviewer → Ship pipeline entirely. Instead of orchestrating the agent chain, it hands Claude a configurable pre-prompt plus the full spec text and lets it work autonomously with its native tools.
84
84
 
85
85
  - **One job per spec.** If the rail has three specs, `Freestyle` launches three independent jobs.
86
86
  - **Variable cost.** Because the run is open-ended, pressing Play opens a confirmation dialog before anything spawns.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.25.0",
3
+ "version": "2.26.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,6 +15,7 @@ const agent_operator_prompt_1 = require("./agent-operator-prompt");
15
15
  const agent_mcp_config_1 = require("./agent-mcp-config");
16
16
  const agent_tier_1 = require("./agent-tier");
17
17
  const attachment_manager_1 = require("./attachment-manager");
18
+ const agent_context_resolver_1 = require("./agent-context-resolver");
18
19
  const agent_store_1 = require("./agent-store");
19
20
  const explore_draft_title_1 = require("./explore-draft-title");
20
21
  const types_1 = require("./mcp/tools/types");
@@ -22,6 +23,7 @@ class AgentChatManager {
22
23
  _broadcast;
23
24
  _db;
24
25
  _port;
26
+ _registry;
25
27
  _active = new Map();
26
28
  /** Conversations with a turn in-flight but not yet spawned. Closes the TOCTOU
27
29
  * window the attachment-extraction await opens between the busy guard and
@@ -37,10 +39,11 @@ class AgentChatManager {
37
39
  * shutdown() can tree-kill them instead of orphaning them. Self-removed on
38
40
  * 'close'/'error'. */
39
41
  _auxProcesses = new Set();
40
- constructor(broadcast, db, port) {
42
+ constructor(broadcast, db, port, registry) {
41
43
  this._broadcast = broadcast;
42
44
  this._db = db;
43
45
  this._port = port;
46
+ this._registry = registry ?? null;
44
47
  }
45
48
  /** True while a turn is streaming for this conversation. */
46
49
  isStreaming(conversationId) {
@@ -76,6 +79,7 @@ class AgentChatManager {
76
79
  conversationId,
77
80
  queueId,
78
81
  text: userText,
82
+ contextRefs: options.contextRefs ?? [],
79
83
  position: pending.length,
80
84
  timestamp: new Date().toISOString(),
81
85
  });
@@ -101,6 +105,7 @@ class AgentChatManager {
101
105
  conversationId,
102
106
  queueId: next.queueId,
103
107
  text: next.text,
108
+ contextRefs: next.options.contextRefs ?? [],
104
109
  timestamp: new Date().toISOString(),
105
110
  });
106
111
  await this._runTurnSafely(conv, next.text, next.options);
@@ -164,11 +169,19 @@ class AgentChatManager {
164
169
  console.error(`[agent-chat] attachment extraction failed (${conversationId}):`, err);
165
170
  }
166
171
  }
172
+ const contextBlock = (0, agent_context_resolver_1.buildResolvedAgentContextBlock)(options.contextRefs ?? [], {
173
+ desktopDb: this._db,
174
+ registry: this._registry,
175
+ fallbackProjectId: conversation.pinned_project_id ?? null,
176
+ });
177
+ if (contextBlock) {
178
+ userWithAttachments = `${userWithAttachments}\n\n${contextBlock}`;
179
+ }
167
180
  // The conversation may have been deleted while attachments were extracting
168
181
  // (DELETE aborts the child and drops the row) — inserting would violate the FK.
169
182
  if (!(0, agent_store_1.getAgentConversation)(this._db, conversationId))
170
183
  return;
171
- (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'user', content: userText, attachmentIds });
184
+ (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'user', content: userText, attachmentIds, contextRefs: options.contextRefs });
172
185
  this._autoTitle(conversationId, conversation.title);
173
186
  // Providers WITHOUT a --system-prompt flag (codex, gemini) drop opts.systemPrompt
174
187
  // for chat turns, so the attachment prompt-injection note would never reach them —
@@ -583,6 +596,7 @@ class AgentChatManager {
583
596
  conversationId,
584
597
  queueId,
585
598
  text,
599
+ contextRefs: item.options.contextRefs ?? [],
586
600
  timestamp: new Date().toISOString(),
587
601
  });
588
602
  return true;
@@ -26,6 +26,70 @@ function validProvider(provider) {
26
26
  return null;
27
27
  }
28
28
  }
29
+ const CONTEXT_KINDS = new Set(['project', 'spec', 'job', 'trace', 'conversation', 'file', 'alias', 'pr', 'action']);
30
+ function cleanContextString(value, max) {
31
+ if (typeof value !== 'string')
32
+ return null;
33
+ const clean = value.replace(/[\r\n"]/g, ' ').trim().slice(0, max);
34
+ return clean || null;
35
+ }
36
+ function cleanContextMetadata(value) {
37
+ if (!value || typeof value !== 'object' || Array.isArray(value))
38
+ return undefined;
39
+ const clean = {};
40
+ for (const [key, raw] of Object.entries(value).slice(0, 16)) {
41
+ const safeKey = cleanContextString(key, 80);
42
+ if (!safeKey)
43
+ continue;
44
+ if (raw === null || typeof raw === 'number' || typeof raw === 'boolean') {
45
+ clean[safeKey] = raw;
46
+ }
47
+ else if (typeof raw === 'string') {
48
+ const safeValue = cleanContextString(raw, 500);
49
+ if (safeValue !== null)
50
+ clean[safeKey] = safeValue;
51
+ }
52
+ else if (Array.isArray(raw)) {
53
+ clean[safeKey] = raw
54
+ .slice(0, 16)
55
+ .map((item) => (typeof item === 'string' ? cleanContextString(item, 240) :
56
+ typeof item === 'number' || typeof item === 'boolean' || item === null ? item :
57
+ undefined))
58
+ .filter((item) => item !== undefined);
59
+ }
60
+ }
61
+ return Object.keys(clean).length ? clean : undefined;
62
+ }
63
+ function sanitizeContextRefs(value) {
64
+ if (!Array.isArray(value))
65
+ return [];
66
+ const refs = [];
67
+ for (const raw of value.slice(0, 16)) {
68
+ if (!raw || typeof raw !== 'object')
69
+ continue;
70
+ const row = raw;
71
+ const kind = cleanContextString(row.kind, 40);
72
+ const id = cleanContextString(row.id, 160);
73
+ const label = cleanContextString(row.label, 220);
74
+ const token = cleanContextString(row.token, 120);
75
+ if (!kind || !CONTEXT_KINDS.has(kind) || !id || !label || !token)
76
+ continue;
77
+ const scopeRaw = row.scope && typeof row.scope === 'object' ? row.scope : null;
78
+ const projectId = cleanContextString(scopeRaw?.projectId, 160);
79
+ const projectName = cleanContextString(scopeRaw?.projectName, 180);
80
+ const status = cleanContextString(row.status, 80);
81
+ refs.push({
82
+ kind,
83
+ id,
84
+ label,
85
+ token,
86
+ scope: projectId || projectName ? { projectId, projectName } : undefined,
87
+ status,
88
+ metadata: cleanContextMetadata(row.metadata),
89
+ });
90
+ }
91
+ return refs;
92
+ }
29
93
  const attachmentUpload = (0, multer_1.default)({
30
94
  storage: multer_1.default.memoryStorage(),
31
95
  limits: { fileSize: 25 * 1024 * 1024 },
@@ -296,6 +360,7 @@ function createAgentChatRouter(deps) {
296
360
  attachmentIds = (rawAtt.ids).filter((x) => typeof x === 'string');
297
361
  }
298
362
  const queueId = typeof body.queueId === 'string' ? body.queueId : null;
363
+ const contextRefs = sanitizeContextRefs(body.contextRefs);
299
364
  // Fire-and-forget: the turn streams over WS. Persist the chosen tier first so
300
365
  // a refresh mid-turn restores the right level.
301
366
  if (tierLevel !== undefined)
@@ -304,7 +369,7 @@ function createAgentChatRouter(deps) {
304
369
  // synchronous frame (the enqueue happens before sendMessage's first await),
305
370
  // so the flag the client gets always matches what actually happened.
306
371
  const queued = manager.isBusy(conversation.id);
307
- void manager.sendMessage(conversation.id, text, { tierLevel, model, attachmentIds, queueId }).catch((e) => console.error('[agent-chat] send failed:', e));
372
+ void manager.sendMessage(conversation.id, text, { tierLevel, model, attachmentIds, queueId, contextRefs }).catch((e) => console.error('[agent-chat] send failed:', e));
308
373
  res.status(202).json({ accepted: true, queued });
309
374
  });
310
375
  // Edit a still-queued (not yet dispatched) message in place. 409 when the