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.
- package/client/dist/assets/{ActivityFeedPage-BGRTOOhT.js → ActivityFeedPage-1eXnoNAo.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-DvDLxsUy.js → AgentBrowserCapture-BHW_6Vm0.js} +1 -1
- package/client/dist/assets/{AgentModeCodePane-DfwctjzN.js → AgentModeCodePane-C89SAJzI.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-G3WrS-sE.js → AgentModeJobsPane-BG_4r4Dm.js} +1 -1
- package/client/dist/assets/{AgentsPage-DGFFijMC.js → AgentsPage-BrkWfc9s.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-hoJmlEpk.js → AnalyticsPage-CMZRUPrT.js} +1 -1
- package/client/dist/assets/{BarChart-BEhAXVK-.js → BarChart-BtF3zJHa.js} +1 -1
- package/client/dist/assets/{CodePage-B4zB9DVC.js → CodePage-Cime5FRs.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-CBX-Qpxd.js → DesktopAnalyticsPage-CSKHSBBF.js} +1 -1
- package/client/dist/assets/{DocsDialog-lTsnNdAM.js → DocsDialog-ClTq6Fzj.js} +1 -1
- package/client/dist/assets/{DocsPage-CkvdGroT.js → DocsPage-DTxfOpTu.js} +1 -1
- package/client/dist/assets/{ExportDropdown-Da98gR-P.js → ExportDropdown-Ho9zdPvK.js} +1 -1
- package/client/dist/assets/{IntegrationsPage-Cf9o4eZq.js → IntegrationsPage-DWNyo6jk.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-C8N-Bbnn.js → InteractiveJobComposer-CYxXOKld.js} +1 -1
- package/client/dist/assets/{JobDetailModal-BZVwIIEx.js → JobDetailModal-DrNafHY5.js} +1 -1
- package/client/dist/assets/{JobDetailPage-CQewQ1zy.js → JobDetailPage-4VKw33GW.js} +1 -1
- package/client/dist/assets/{JobsPage-CB4wOxmE.js → JobsPage-cFr6wWaH.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-B9Y5gt08.js → LoopBuilderPage-Bk8zVvEX.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-kxQ5kV1t.js → LoopPreviewModal-XFSO2Vcs.js} +1 -1
- package/client/dist/assets/{LoopsPage-RysW-6sC.js → LoopsPage-DWMwDj9g.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-BHN5qsGT.js → ProjectSettingsDialog-DnSe_yWm.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-DW6lZCIs.js → TemplatePreviewModal-B30-TY_v.js} +1 -1
- package/client/dist/assets/agent-6mhkV25_2.js +1 -0
- package/client/dist/assets/agent-BZqfQhI_.js +1 -0
- package/client/dist/assets/agent-Bjy1SlT5.js +1 -0
- package/client/dist/assets/agent-Bn6ZmjHj.js +1 -0
- package/client/dist/assets/{agent-G6Zn2gpr.js → agent-CRYdi9-t.js} +1 -1
- package/client/dist/assets/agent-D5QNn9np2.js +1 -0
- package/client/dist/assets/agent-DgFoLgrF2.js +1 -0
- package/client/dist/assets/agent-DgwPsxWQ.js +1 -0
- package/client/dist/assets/{brain-B9dkGuAN.js → brain-DcXkQwzO.js} +1 -1
- package/client/dist/assets/code-CwnBPPyA.js +1 -0
- package/client/dist/assets/{dashboard-4WX7agHF2.js → dashboard-CuGuy2__2.js} +1 -1
- package/client/dist/assets/{dashboard-IbEztmJ52.js → dashboard-CxP3TZ3I2.js} +1 -1
- package/client/dist/assets/{dashboard-5Z_esXIB.js → dashboard-DWHVfP7U.js} +1 -1
- package/client/dist/assets/{dashboard-DQ-po1MG2.js → dashboard-D_Pgs6QH2.js} +1 -1
- package/client/dist/assets/{dashboard-oqS7VRrK.js → dashboard-Dwnssayh.js} +1 -1
- package/client/dist/assets/{dashboard-ojWdDq6S.js → dashboard-S_pT1yxN.js} +1 -1
- package/client/dist/assets/{dashboard-zfoJcdFW.js → dashboard-tCxOFOxx.js} +1 -1
- package/client/dist/assets/{dashboard-nc-Raovv.js → dashboard-zefbBNyG.js} +1 -1
- package/client/dist/assets/{dist-js-BW5-IZTP.js → dist-js-6U6hyD6z.js} +1 -1
- package/client/dist/assets/{dist-js-Bbd7Wzdw.js → dist-js-BAWwLSIj.js} +1 -1
- package/client/dist/assets/index-DnkfABxY.js +156 -0
- package/client/dist/assets/index-cvOunQkD.css +2 -0
- package/client/dist/assets/{lib-EIkIPf26.js → lib-Bsny0zZp.js} +1 -1
- package/client/dist/assets/{loops-YYV2bwjd.js → loops-Brnx4Fji.js} +1 -1
- package/client/dist/assets/{loops-hXgncEpF2.js → loops-BvqQ-bHR2.js} +1 -1
- package/client/dist/assets/{loops-BvEenWBy.js → loops-CvOWsi5r.js} +1 -1
- package/client/dist/assets/{loops-DjOtVXRo.js → loops-D6D3XHjj.js} +1 -1
- package/client/dist/assets/{loops-HTHf-xu32.js → loops-DJNhp17p2.js} +1 -1
- package/client/dist/assets/{loops-kYbydDyN.js → loops-K5_HXfJa.js} +1 -1
- package/client/dist/assets/loops-LIgO_-eM.js +1 -0
- package/client/dist/assets/{loops-DNGiYKll2.js → loops-vhJdbcz92.js} +1 -1
- package/client/dist/assets/{settings-DZJeXoRV.js → settings-BWIf3vv4.js} +1 -1
- package/client/dist/assets/{settings-TNga5gIF2.js → settings-BhuXe7ek2.js} +1 -1
- package/client/dist/assets/{settings-D6qY9_UI.js → settings-DE8t6aYv.js} +1 -1
- package/client/dist/assets/{settings-BsJMsWGc.js → settings-DHd1lQ1k.js} +1 -1
- package/client/dist/assets/{settings-BOHOmHkb.js → settings-DTYQ5COE.js} +1 -1
- package/client/dist/assets/settings-DXgPh9-k.js +1 -0
- package/client/dist/assets/{settings-BlEA_7h02.js → settings-Dgv_q4qf2.js} +1 -1
- package/client/dist/assets/{settings-lFoaYT-s2.js → settings-FJXpJ3k92.js} +1 -1
- package/client/dist/assets/{setup-CmU6bTCV2.js → setup-cnqZsQSX2.js} +1 -1
- package/client/dist/assets/{upload-BCMl-nCg.js → upload-49vX83IZ.js} +1 -1
- package/client/dist/assets/{value-CG-amSq1.js → value-Bo8EiTZE.js} +1 -1
- package/client/dist/index.html +6 -6
- package/docs/agy-cli-provider-study.md +1 -1
- package/docs/codex.md +2 -2
- package/docs/gemini-cli-provider-study.md +2 -2
- package/docs/guide/de/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/en/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/es/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/fr/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/it/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/ja/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/pt/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/guide/pt/pipeline/5-the-loop-builder.md +1 -1
- package/docs/guide/zh/pipeline/3-batch-implement-and-multi-feature.md +1 -1
- package/docs/internals/api-reference.md +3 -3
- package/docs/internals/companion-rails-as-loops-contract.md +6 -6
- package/docs/internals/configuration.md +1 -1
- package/docs/internals/interactive-jobs.md +3 -3
- package/docs/internals/safe-pr-review-flow.md +1 -1
- package/docs/internals/visual-design-audit.md +2 -2
- package/docs/running-pipelines.md +1 -1
- package/package.json +1 -1
- package/server/dist/agent-chat-manager.js +16 -2
- package/server/dist/agent-chat-router.js +66 -1
- package/server/dist/agent-context-resolver.js +301 -0
- package/server/dist/agent-operator-prompt.js +14 -7
- package/server/dist/agent-store.js +24 -12
- package/server/dist/db.js +17 -17
- package/server/dist/desktop-db.js +6 -0
- package/server/dist/feature-flags.js +2 -2
- package/server/dist/index.js +1 -1
- package/server/dist/interactive-job-session.js +3 -3
- package/server/dist/loop-command-catalog.js +11 -11
- package/server/dist/loop-factory.js +7 -7
- package/server/dist/loop-graph.js +2 -2
- package/server/dist/loop-run-manager.js +1 -1
- package/server/dist/mcp/guide.js +8 -6
- package/server/dist/mcp/tools/rails.js +4 -3
- package/server/dist/mobile/mobile-router.js +1 -1
- package/server/dist/project-router-jobs.js +1 -1
- package/server/dist/project-router-loop-runs.js +1 -1
- package/server/dist/project-router-settings.js +5 -5
- package/server/dist/queue-manager.js +27 -27
- package/server/dist/rails-router.js +20 -20
- package/client/dist/assets/agent-4ARs0uac2.js +0 -1
- package/client/dist/assets/agent-C55wwsUX.js +0 -1
- package/client/dist/assets/agent-C_kqyrXo.js +0 -1
- package/client/dist/assets/agent-Df3lnHMu.js +0 -1
- package/client/dist/assets/agent-MQhun7cv.js +0 -1
- package/client/dist/assets/agent-NqlER4Yh2.js +0 -1
- package/client/dist/assets/agent-OTGI6t7e2.js +0 -1
- package/client/dist/assets/code-CqCcRThR.js +0 -1
- package/client/dist/assets/index-BtAks9nm.css +0 -2
- package/client/dist/assets/index-DC-REAcL.js +0 -155
- package/client/dist/assets/loops-CBiWd9Hf.js +0 -1
- 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,
|
|
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
|
-
| **
|
|
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;
|
|
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;
|
|
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;
|
|
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 ;
|
|
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;
|
|
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 はどのプロバイダーでも動きますが、
|
|
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
|
|
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
|
|
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 可在任意提供方上运行;
|
|
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/
|
|
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` / `
|
|
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
|
|
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,
|
|
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:
|
|
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`.
|
|
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|
|
|
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|
|
|
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
|
-
| **
|
|
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
|
|
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 |
|
|
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
|
|
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,
|
|
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/
|
|
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`, `
|
|
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 `
|
|
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
|
@@ -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
|