specrails-desktop 2.29.4 → 2.30.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 (169) hide show
  1. package/client/dist/assets/{ActivityFeedPage-CDMzpGXd.js → ActivityFeedPage-AQgpr9qG.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-BVTacapZ.js → AgentBrowserCapture-I7a0usG7.js} +1 -1
  3. package/client/dist/assets/AgentModeCodePane-BqOVg15n.js +2 -0
  4. package/client/dist/assets/{AgentModeJobsPane-DAjN8Au5.js → AgentModeJobsPane-4SG6FF27.js} +1 -1
  5. package/client/dist/assets/{AgentsPage-CebykajE.js → AgentsPage-C3FgYlEi.js} +1 -1
  6. package/client/dist/assets/{AnalyticsPage-BiPrVvgv.js → AnalyticsPage-BcMNByW5.js} +1 -1
  7. package/client/dist/assets/{CodePage-Dw9eQFRS.js → CodePage-CgdDkXp-.js} +1 -1
  8. package/client/dist/assets/{DesktopAnalyticsPage-cCG_JAEi.js → DesktopAnalyticsPage-DPMcS88n.js} +1 -1
  9. package/client/dist/assets/{DocsDialog-C1kZ_mRP.js → DocsDialog-BC7574Zi.js} +1 -1
  10. package/client/dist/assets/{DocsPage-3GivxWJm.js → DocsPage-Dd9yilUg.js} +1 -1
  11. package/client/dist/assets/{InteractiveJobComposer-KuZXGdr8.js → InteractiveJobComposer-CqC_0VQL.js} +1 -1
  12. package/client/dist/assets/{JobDetailModal-DWpRALkB.js → JobDetailModal-BFNXOhk1.js} +1 -1
  13. package/client/dist/assets/{JobDetailPage-mrXDKafj.js → JobDetailPage-DM-6zYpA.js} +1 -1
  14. package/client/dist/assets/{JobsPage-CUYXjcoc.js → JobsPage-1dkjPk1k.js} +1 -1
  15. package/client/dist/assets/{LoopBuilderPage-CwjBD5Gm.js → LoopBuilderPage-BPeqz6cG.js} +1 -1
  16. package/client/dist/assets/{LoopPreviewModal-CO9ton0-.js → LoopPreviewModal-EdkyYaGN.js} +1 -1
  17. package/client/dist/assets/{LoopsPage-CLL5UuvN.js → LoopsPage-nZfgiaK8.js} +1 -1
  18. package/client/dist/assets/{MinimizedChatsContext-DJAkRYps.js → MinimizedChatsContext-Cs34FPTz.js} +1 -1
  19. package/client/dist/assets/{PluginsPage-DZvCC5Ri.js → PluginsPage-CwFfFBzW.js} +1 -1
  20. package/client/dist/assets/{ProjectSettingsDialog-C7Hesadg.js → ProjectSettingsDialog-DHsxLFMT.js} +1 -1
  21. package/client/dist/assets/{TemplatePreviewModal-DczCzUzP.js → TemplatePreviewModal-wcI3nGNr.js} +1 -1
  22. package/client/dist/assets/TicketDetailModal-7EloKP1C.js +14 -0
  23. package/client/dist/assets/{Trans-Cm9rhdnG.js → Trans-CPISVji8.js} +1 -1
  24. package/client/dist/assets/agent-BABjRt0P.js +1 -0
  25. package/client/dist/assets/agent-BuUpgqVx.js +1 -0
  26. package/client/dist/assets/agent-CCDZesgU.js +1 -0
  27. package/client/dist/assets/agent-CVn64zWH.js +1 -0
  28. package/client/dist/assets/agent-CWeOny0E.js +1 -0
  29. package/client/dist/assets/agent-DOysmad_.js +1 -0
  30. package/client/dist/assets/agent-DSsjTBhn.js +1 -0
  31. package/client/dist/assets/agent-DlZMxfM9.js +1 -0
  32. package/client/dist/assets/builder-BH8Fua-g.js +1 -0
  33. package/client/dist/assets/builder-BoV7DgKd.js +1 -0
  34. package/client/dist/assets/builder-Cc_Lf79C.js +1 -0
  35. package/client/dist/assets/builder-CwA4T7gP.js +1 -0
  36. package/client/dist/assets/builder-CwZ7Bme2.js +1 -0
  37. package/client/dist/assets/builder-CxW-JFsR.js +1 -0
  38. package/client/dist/assets/builder-D94lrV7x.js +1 -0
  39. package/client/dist/assets/builder-cqHpCyGa.js +1 -0
  40. package/client/dist/assets/{dist-js-BERxOT0X.js → dist-js-Bt2zW2YG.js} +1 -1
  41. package/client/dist/assets/{dist-js-Di8lflPo.js → dist-js-zvt5ap3o.js} +1 -1
  42. package/client/dist/assets/{formatDistanceToNow-BAXZRbEO.js → formatDistanceToNow-ByKli7Q_.js} +1 -1
  43. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-DwzZXHtg.js → getTimezoneOffsetInMilliseconds-qrcBaLW1.js} +1 -1
  44. package/client/dist/assets/i18n-Bf9Ckd10.js +2 -0
  45. package/client/dist/assets/index-Dh9i4CnN.css +2 -0
  46. package/client/dist/assets/index-xkzK3E5i.js +76 -0
  47. package/client/dist/assets/setup-3832BJCo.js +1 -0
  48. package/client/dist/assets/setup-B8J-gECe.js +1 -0
  49. package/client/dist/assets/setup-BCIOxVsc.js +1 -0
  50. package/client/dist/assets/setup-BciLj_tU.js +1 -0
  51. package/client/dist/assets/setup-CITtPjed.js +1 -0
  52. package/client/dist/assets/setup-CTkE3cfh.js +1 -0
  53. package/client/dist/assets/setup-ajB4rYm-.js +1 -0
  54. package/client/dist/assets/setup-xFO1wNl0.js +1 -0
  55. package/client/dist/assets/{spending-BLb5pj6G.js → spending-DMnCXy84.js} +1 -1
  56. package/client/dist/assets/{useBrowserCaptureSession-DR1_jL56.js → useBrowserCaptureSession-CkBJShUw.js} +1 -1
  57. package/client/dist/assets/{useDesktop-D4DxBGPU.js → useDesktop-B4pSBIDd.js} +1 -1
  58. package/client/dist/index.html +14 -13
  59. package/docs/codex.md +2 -1
  60. package/docs/customizing.md +1 -1
  61. package/docs/internals/gemini-mcp-registration.md +1 -1
  62. package/docs/internals/global-artifacts-alignment-contract.md +4 -4
  63. package/docs/internals/project-builder.md +306 -0
  64. package/docs/internals/safe-pr-review-flow.md +29 -19
  65. package/package.json +1 -1
  66. package/server/dist/agent-operator-prompt.js +7 -1
  67. package/server/dist/blueprint-chat-manager.js +335 -0
  68. package/server/dist/blueprint-commit.js +335 -0
  69. package/server/dist/blueprint-draft-parser.js +168 -0
  70. package/server/dist/blueprint-operator-prompt.js +149 -0
  71. package/server/dist/blueprint-render.js +105 -0
  72. package/server/dist/blueprint-router.js +173 -0
  73. package/server/dist/blueprint-spec-quality.js +167 -0
  74. package/server/dist/blueprint-store.js +49 -0
  75. package/server/dist/blueprint-types.js +29 -0
  76. package/server/dist/builder-cwd-manager.js +57 -0
  77. package/server/dist/chat-manager.js +435 -23
  78. package/server/dist/context-scope.js +6 -3
  79. package/server/dist/contract-refine-runner.js +143 -72
  80. package/server/dist/desktop-db.js +27 -0
  81. package/server/dist/index.js +15 -0
  82. package/server/dist/offline-assemble.js +203 -0
  83. package/server/dist/pricing.js +5 -0
  84. package/server/dist/project-router-chat.js +14 -2
  85. package/server/dist/project-router.js +130 -7
  86. package/server/dist/providers/claude-adapter.js +8 -2
  87. package/server/dist/providers/codex-adapter.js +23 -10
  88. package/server/dist/providers/gemini-adapter.js +16 -7
  89. package/server/dist/rail-isolated-launch.js +11 -1
  90. package/server/dist/setup-manager.js +6 -54
  91. package/server/dist/spec-models.js +3 -0
  92. package/server/dist/workspace-manager.js +3 -1
  93. package/server/dist/worktree-overlay.js +233 -51
  94. package/client/dist/assets/AgentModeCodePane-D9bhFyuy.js +0 -2
  95. package/client/dist/assets/TicketDetailModal-CFh7GOv5.js +0 -14
  96. package/client/dist/assets/agent-BXc59ARK2.js +0 -1
  97. package/client/dist/assets/agent-BbIFXdDq.js +0 -1
  98. package/client/dist/assets/agent-BqEfA5u5.js +0 -1
  99. package/client/dist/assets/agent-BtGzgTC4.js +0 -1
  100. package/client/dist/assets/agent-CIhys_5d.js +0 -1
  101. package/client/dist/assets/agent-CJPaqsVR.js +0 -1
  102. package/client/dist/assets/agent-n-hY4GuL.js +0 -1
  103. package/client/dist/assets/agent-tJ-efa3_2.js +0 -1
  104. package/client/dist/assets/i18n-cK5pPbj8.js +0 -2
  105. package/client/dist/assets/index-BT6xDQjB.css +0 -2
  106. package/client/dist/assets/index-CCq_kIrF.js +0 -76
  107. package/client/dist/assets/setup-CGZjy2IF2.js +0 -1
  108. package/client/dist/assets/setup-CXa0Sx16.js +0 -1
  109. package/client/dist/assets/setup-CmK87LuS.js +0 -1
  110. package/client/dist/assets/setup-D2ktKKMF2.js +0 -1
  111. package/client/dist/assets/setup-DG2H7ha5.js +0 -1
  112. package/client/dist/assets/setup-DK5bHDUq2.js +0 -1
  113. package/client/dist/assets/setup-DrWlIQTZ.js +0 -1
  114. package/client/dist/assets/setup-rzHUVgAu.js +0 -1
  115. /package/client/dist/assets/{activity-C6kJpm9q2.js → activity-C6kJpm9q.js} +0 -0
  116. /package/client/dist/assets/{activity-DAyekdlt2.js → activity-DAyekdlt.js} +0 -0
  117. /package/client/dist/assets/{addspec-DbNBbrha2.js → addspec-DbNBbrha.js} +0 -0
  118. /package/client/dist/assets/{addspec-htHGZHOe2.js → addspec-htHGZHOe.js} +0 -0
  119. /package/client/dist/assets/{agents-6ZOJXWVk2.js → agents-6ZOJXWVk.js} +0 -0
  120. /package/client/dist/assets/{agents-Bz-thHkB2.js → agents-Bz-thHkB.js} +0 -0
  121. /package/client/dist/assets/{agentstudio-BLGvxWBx2.js → agentstudio-BLGvxWBx.js} +0 -0
  122. /package/client/dist/assets/{agentstudio-D9UKiWTj2.js → agentstudio-D9UKiWTj.js} +0 -0
  123. /package/client/dist/assets/{aiedit-BoG24nlg2.js → aiedit-BoG24nlg.js} +0 -0
  124. /package/client/dist/assets/{aiedit-olnv3QSt2.js → aiedit-olnv3QSt.js} +0 -0
  125. /package/client/dist/assets/{analytics-CdsAPAwZ2.js → analytics-CdsAPAwZ.js} +0 -0
  126. /package/client/dist/assets/{analytics-IEsKZvpW2.js → analytics-IEsKZvpW.js} +0 -0
  127. /package/client/dist/assets/{attachments-D0wXXt9T2.js → attachments-D0wXXt9T.js} +0 -0
  128. /package/client/dist/assets/{attachments-V_WZAYXx2.js → attachments-V_WZAYXx.js} +0 -0
  129. /package/client/dist/assets/{browser-CyR1Kp3m2.js → browser-CyR1Kp3m.js} +0 -0
  130. /package/client/dist/assets/{browser-DwZljcRh2.js → browser-DwZljcRh.js} +0 -0
  131. /package/client/dist/assets/{chat-BR8oe53x2.js → chat-BR8oe53x.js} +0 -0
  132. /package/client/dist/assets/{chat-DZyu3ImY2.js → chat-DZyu3ImY.js} +0 -0
  133. /package/client/dist/assets/{code-D20ef9Cf2.js → code-D20ef9Cf.js} +0 -0
  134. /package/client/dist/assets/{code-geHmknB92.js → code-geHmknB9.js} +0 -0
  135. /package/client/dist/assets/{commands-BEwbAjtD2.js → commands-BEwbAjtD.js} +0 -0
  136. /package/client/dist/assets/{commands-DTQBF_q52.js → commands-DTQBF_q5.js} +0 -0
  137. /package/client/dist/assets/{common-BNbnv8cE2.js → common-BNbnv8cE.js} +0 -0
  138. /package/client/dist/assets/{common-DBtTzeYm2.js → common-DBtTzeYm.js} +0 -0
  139. /package/client/dist/assets/{dashboard-Cqmmbh3l2.js → dashboard-Cqmmbh3l.js} +0 -0
  140. /package/client/dist/assets/{dashboard-Dy2Um-US2.js → dashboard-Dy2Um-US.js} +0 -0
  141. /package/client/dist/assets/{explore-C0TjprJW2.js → explore-C0TjprJW.js} +0 -0
  142. /package/client/dist/assets/{explore-DsXAgnmJ2.js → explore-DsXAgnmJ.js} +0 -0
  143. /package/client/dist/assets/{integrations-CH0zy1Mw2.js → integrations-CH0zy1Mw.js} +0 -0
  144. /package/client/dist/assets/{integrations-b8pH-vfl2.js → integrations-b8pH-vfl.js} +0 -0
  145. /package/client/dist/assets/{jira-CNeYi06w2.js → jira-CNeYi06w.js} +0 -0
  146. /package/client/dist/assets/{jira-pRaL7JQo2.js → jira-pRaL7JQo.js} +0 -0
  147. /package/client/dist/assets/{jobs-B-sDHEay2.js → jobs-B-sDHEay.js} +0 -0
  148. /package/client/dist/assets/{jobs-DzSIR8V62.js → jobs-DzSIR8V6.js} +0 -0
  149. /package/client/dist/assets/{loops-Crc9It8o2.js → loops-Crc9It8o.js} +0 -0
  150. /package/client/dist/assets/{loops-DH2hZcbK2.js → loops-DH2hZcbK.js} +0 -0
  151. /package/client/dist/assets/{loops-DS5TmvIU2.js → loops-DS5TmvIU.js} +0 -0
  152. /package/client/dist/assets/{mcp-Czd_iyp02.js → mcp-Czd_iyp0.js} +0 -0
  153. /package/client/dist/assets/{mcp-D4ZjtIx32.js → mcp-D4ZjtIx3.js} +0 -0
  154. /package/client/dist/assets/{mcp-DIfBUv2D2.js → mcp-DIfBUv2D.js} +0 -0
  155. /package/client/dist/assets/{nav-BRJBxaGt2.js → nav-BRJBxaGt.js} +0 -0
  156. /package/client/dist/assets/{nav-CNskRJKQ2.js → nav-CNskRJKQ.js} +0 -0
  157. /package/client/dist/assets/{nav-TvumAFD22.js → nav-TvumAFD2.js} +0 -0
  158. /package/client/dist/assets/{settings-DAZqjX2a2.js → settings-DAZqjX2a.js} +0 -0
  159. /package/client/dist/assets/{settings-DRh39v1S2.js → settings-DRh39v1S.js} +0 -0
  160. /package/client/dist/assets/{settings-Mj1OJbfd2.js → settings-Mj1OJbfd.js} +0 -0
  161. /package/client/dist/assets/{specs-BcyxaCwf2.js → specs-BcyxaCwf.js} +0 -0
  162. /package/client/dist/assets/{specs-CLZKysq-2.js → specs-CLZKysq-.js} +0 -0
  163. /package/client/dist/assets/{specs-dV3Mq5Mu2.js → specs-dV3Mq5Mu.js} +0 -0
  164. /package/client/dist/assets/{terminal-C2NAd7BW2.js → terminal-C2NAd7BW.js} +0 -0
  165. /package/client/dist/assets/{terminal-IahE4LW92.js → terminal-IahE4LW9.js} +0 -0
  166. /package/client/dist/assets/{terminal-QpXZU2Bn2.js → terminal-QpXZU2Bn.js} +0 -0
  167. /package/client/dist/assets/{tickets-Bs37scwY2.js → tickets-Bs37scwY.js} +0 -0
  168. /package/client/dist/assets/{tickets-DQp7JS2R2.js → tickets-DQp7JS2R.js} +0 -0
  169. /package/client/dist/assets/{tickets-DpmdE8tv2.js → tickets-DpmdE8tv.js} +0 -0
@@ -5,8 +5,9 @@
5
5
  * Standalone runner that spawns a single Claude turn to produce the
6
6
  * Contract Layer for a just-committed Explore Spec ticket. Lives outside the
7
7
  * ChatManager lifecycle for now (design.md D3 — "thin sibling helper" option):
8
- * the refine is fire-and-forget, single-attempt, 60 s budget, no idle-kill /
9
- * crash-respawn semantics.
8
+ * the refine is fire-and-forget with a 60 s budget per invocation and no
9
+ * idle-kill / crash-respawn semantics. Only the exact relocated missing-session
10
+ * diagnostic permits one fresh compatibility retry.
10
11
  *
11
12
  * See openspec/changes/explore-spec-contract-refine.
12
13
  */
@@ -35,6 +36,7 @@ const workspace_resolution_1 = require("./workspace-resolution");
35
36
  const process_admission_1 = require("./process-admission");
36
37
  const transient_children_1 = require("./transient-children");
37
38
  const REFINE_TIMEOUT_MS = 60_000;
39
+ const CLAUDE_MISSING_SESSION_DIAGNOSTIC = 'No conversation found with session ID';
38
40
  /**
39
41
  * Relocate-artifacts: resolve the ticket store path honouring the gate.
40
42
  * Relocated ⇒ the registry entry's tickets path (workspace); legacy ⇒
@@ -83,6 +85,46 @@ function buildRefineArgs(model, systemPrompt, sessionId) {
83
85
  '-p', explore_contract_refine_1.CONTRACT_MARKER_USER_MESSAGE,
84
86
  ];
85
87
  }
88
+ /** Build the pure-output, no-resume invocation used by Quick Refine and by the
89
+ * one-time compatibility recovery for cwd-scoped Explore sessions. */
90
+ function buildFreshRefineArgs(model, title, description) {
91
+ const systemPrompt = [
92
+ (0, explore_contract_refine_1.buildContractRefineSystemPrompt)(),
93
+ '',
94
+ '## Spec under refinement',
95
+ '',
96
+ '### Title',
97
+ title,
98
+ '',
99
+ '### Description',
100
+ description,
101
+ ].join('\n');
102
+ return [
103
+ '--model', model,
104
+ '--tools', '__none__',
105
+ '--output-format', 'stream-json',
106
+ '--verbose',
107
+ '--system-prompt', systemPrompt,
108
+ '--max-turns', '1',
109
+ '-p', explore_contract_refine_1.CONTRACT_MARKER_USER_MESSAGE,
110
+ ];
111
+ }
112
+ /** Match only Claude's missing cwd-scoped session diagnostic. The CLI has
113
+ * emitted it both as stderr and inside structured result payloads, so inspect
114
+ * all captured provider output while keeping unrelated failures ineligible. */
115
+ function containsMissingClaudeSessionDiagnostic(value) {
116
+ if (typeof value === 'string') {
117
+ return value.includes(CLAUDE_MISSING_SESSION_DIAGNOSTIC);
118
+ }
119
+ if (value == null)
120
+ return false;
121
+ try {
122
+ return JSON.stringify(value).includes(CLAUDE_MISSING_SESSION_DIAGNOSTIC);
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ }
86
128
  /**
87
129
  * Build the spawn argv + cwd for the refine turn. Exported for tests.
88
130
  */
@@ -103,19 +145,26 @@ function prepareContractRefineSpawn(deps, conversation) {
103
145
  // different per-cwd session store and fails with "No conversation found with
104
146
  // session ID …" (the Desktop-tier bug). chat-manager `_resolveSpawnCwd` for an
105
147
  // Explore turn uses:
106
- // - mcp ON → `project.path` (NOT the relocated workspace),
148
+ // - mcp ON → the relocate-artifacts gate (workspace when relocated, else
149
+ // `project.path`); `SPECRAILS_EXPLORE_LEGACY_CWD=1` forces `project.path`,
107
150
  // - mcp OFF → the app-managed explore-cwd.
108
151
  // We mirror that EXACTLY here. The refine uses no tools (`--tools __none__`)
109
152
  // and its prompt rides on `--system-prompt`, so it needs
110
- // neither the workspace's `.mcp.json` nor its `.claude/commands`. Env still
111
- // carries SPECRAILS_REPO_DIR when relocated (harmless; no tools consume it).
153
+ // neither the workspace's `.mcp.json` nor its `.claude/commands`. The env is
154
+ // mirrored too so the legacy-cwd escape hatch remains byte-identical.
112
155
  const exec = (0, workspace_resolution_1.resolveProjectExecution)({ slug: deps.projectSlug, path: deps.projectPath });
113
- let env = exec.relocated ? { ...process.env, ...exec.env } : undefined;
156
+ let env;
114
157
  let cwd;
115
158
  if (mcpEnabled) {
116
- // Match the Explore mcp-on spawn cwd (`this._cwd` = project.path), even when
117
- // relocated (where exec.cwd would be the workspace ≠ project.path).
118
- cwd = deps.projectPath;
159
+ // Match the Explore mcp-on spawn cwd through the same gate + escape hatch.
160
+ if (process.env.SPECRAILS_EXPLORE_LEGACY_CWD === '1') {
161
+ cwd = deps.projectPath;
162
+ }
163
+ else {
164
+ cwd = exec.cwd;
165
+ if (exec.relocated)
166
+ env = { ...process.env, ...exec.env };
167
+ }
119
168
  }
120
169
  else {
121
170
  try {
@@ -126,7 +175,9 @@ function prepareContractRefineSpawn(deps, conversation) {
126
175
  });
127
176
  }
128
177
  catch {
129
- cwd = exec.cwd;
178
+ // Match ChatManager's fail-closed fallback for an unavailable
179
+ // app-managed Explore cwd.
180
+ cwd = deps.projectPath;
130
181
  }
131
182
  }
132
183
  const args = buildRefineArgs(conversation.model ?? 'sonnet', systemPrompt, conversation.session_id ?? '');
@@ -280,7 +331,8 @@ function applyContractLayerToTicket(filePath, ticketId, layer, nowIso) {
280
331
  return updated;
281
332
  }
282
333
  /**
283
- * Fire a single Contract Refine attempt for the given conversation + ticket.
334
+ * Run Contract Refine for the given conversation + ticket (normally one
335
+ * invocation; at most two for the narrow missing-session compatibility path).
284
336
  *
285
337
  * Returns a Promise that resolves with the outcome. Side effects:
286
338
  * - On success: patches the ticket's description, broadcasts `ticket_updated`
@@ -353,59 +405,97 @@ async function runContractRefine(deps, conversationId, ticketId) {
353
405
  const startedAt = now().toISOString();
354
406
  // Spawn/stream/timeout/settlement is owned by the shared spawn-lifecycle; the
355
407
  // contract-refine-specific raw parse (fullText from assistant text blocks,
356
- // the raw result event) and ALL finalize/record/broadcast logic stay here,
357
- // byte-for-byte, so behaviour is unchanged (it still records via the legacy
358
- // recordSafely path).
359
- let fullText = '';
360
- let resultEvent = null;
361
- const run = await (0, spawn_lifecycle_1.runAiCliInvocation)({
362
- adapter: (0, registry_1.getAdapter)('claude'),
363
- binary: 'claude',
364
- argv: args,
365
- cwd,
366
- env: refineEnv ?? process.env,
367
- spawn,
368
- timeoutMs,
369
- onSpawn: (child) => (0, transient_children_1.trackTransientChild)(deps.projectId, child),
370
- onStdoutLine: (line) => {
371
- let parsed = null;
372
- try {
373
- parsed = JSON.parse(line);
374
- }
375
- catch {
376
- return;
377
- }
378
- if (!parsed)
379
- return;
380
- const type = parsed.type;
381
- if (type === 'result') {
382
- resultEvent = parsed;
383
- }
384
- else if (type === 'assistant') {
385
- const message = parsed.message;
386
- for (const b of (message?.content ?? [])) {
387
- if (b.type === 'text' && typeof b.text === 'string')
388
- fullText += b.text;
408
+ // the raw result event) and all finalize/record/broadcast logic stay here.
409
+ // Keeping the invocation result in one shape also lets the narrowly-gated
410
+ // missing-session recovery re-enter the exact same finalization path.
411
+ const invoke = async (invocationArgs) => {
412
+ let fullText = '';
413
+ let resultEvent = null;
414
+ const run = await (0, spawn_lifecycle_1.runAiCliInvocation)({
415
+ adapter: (0, registry_1.getAdapter)('claude'),
416
+ binary: 'claude',
417
+ argv: invocationArgs,
418
+ cwd,
419
+ env: refineEnv ?? process.env,
420
+ spawn,
421
+ timeoutMs,
422
+ onSpawn: (child) => (0, transient_children_1.trackTransientChild)(deps.projectId, child),
423
+ onStdoutLine: (line) => {
424
+ let parsed = null;
425
+ try {
426
+ parsed = JSON.parse(line);
389
427
  }
390
- }
391
- },
392
- });
428
+ catch {
429
+ return;
430
+ }
431
+ if (!parsed)
432
+ return;
433
+ const type = parsed.type;
434
+ if (type === 'result') {
435
+ resultEvent = parsed;
436
+ }
437
+ else if (type === 'assistant') {
438
+ const message = parsed.message;
439
+ for (const b of (message?.content ?? [])) {
440
+ if (b.type === 'text' && typeof b.text === 'string')
441
+ fullText += b.text;
442
+ }
443
+ }
444
+ },
445
+ });
446
+ return {
447
+ fullText,
448
+ resultEvent,
449
+ code: run.code,
450
+ timedOut: run.timedOut,
451
+ spawnFailed: run.spawnFailed,
452
+ stderrTail: run.stderrTail,
453
+ };
454
+ };
455
+ let result = await invoke(args);
393
456
  if (!admission.isCurrent()) {
394
457
  return { ok: false, reason: 'aborted', ticketId, conversationId };
395
458
  }
396
- if (run.spawnFailed) {
397
- recordSafely(deps, conversationId, ticketId, conversation.model, startedAt, now().toISOString(), 'failed', null);
459
+ // A pre-relocation Explore session was created under the repo cwd and cannot
460
+ // be resumed after the corrected mcp=true route moves to the workspace.
461
+ // Recover only that exact provider failure, once, and never by returning to
462
+ // the repo. The fresh turn is explicitly seeded because it has no resumed
463
+ // conversation context.
464
+ const resumedFromRelocatedWorkspace = refineEnv?.SPECRAILS_WORKSPACE_DIR === cwd && cwd !== deps.projectPath;
465
+ const resumeFailed = !result.spawnFailed &&
466
+ !result.timedOut &&
467
+ (result.code !== 0 || !result.resultEvent || isResultErrorEvent(result.resultEvent));
468
+ const missingSessionDiagnostic = containsMissingClaudeSessionDiagnostic(result.fullText) ||
469
+ containsMissingClaudeSessionDiagnostic(result.resultEvent) ||
470
+ containsMissingClaudeSessionDiagnostic(result.stderrTail);
471
+ if (resumedFromRelocatedWorkspace && resumeFailed && missingSessionDiagnostic) {
472
+ let ticket = null;
473
+ try {
474
+ ticket = (0, ticket_store_1.readStore)(resolveContractTicketsPath(deps.projectPath)).tickets[String(ticketId)] ?? null;
475
+ }
476
+ catch (err) {
477
+ console.error('[contract-refine-runner] unable to read ticket for missing-session recovery:', err);
478
+ }
479
+ if (ticket) {
480
+ console.warn(`[contract-refine-runner] resume session unavailable; retrying once fresh from workspace ticket=${ticketId}`);
481
+ result = await invoke(buildFreshRefineArgs(conversation.model ?? 'sonnet', ticket.title, ticket.description));
482
+ if (!admission.isCurrent()) {
483
+ return { ok: false, reason: 'aborted', ticketId, conversationId };
484
+ }
485
+ }
486
+ }
487
+ const finishedAt = now().toISOString();
488
+ if (result.spawnFailed) {
489
+ recordSafely(deps, conversationId, ticketId, conversation.model, startedAt, finishedAt, 'failed', null);
398
490
  deps.broadcast({
399
491
  type: 'explore.contract_refine_failed',
400
492
  projectId: deps.projectId,
401
493
  ticketId,
402
494
  reason: 'crashed',
403
- timestamp: now().toISOString(),
495
+ timestamp: finishedAt,
404
496
  });
405
497
  return { ok: false, reason: 'crashed', ticketId, conversationId };
406
498
  }
407
- const result = { fullText, resultEvent, code: run.code, timedOut: run.timedOut };
408
- const finishedAt = now().toISOString();
409
499
  console.log(`[contract-refine-runner] spawn done code=${result.code} timedOut=${result.timedOut} hasResult=${!!result.resultEvent} textBytes=${result.fullText.length}`);
410
500
  if (result.timedOut) {
411
501
  recordSafely(deps, conversationId, ticketId, conversation.model, startedAt, finishedAt, 'aborted', result.resultEvent);
@@ -515,26 +605,7 @@ async function runContractRefineForQuick(deps, ticketId, generatedTitle, generat
515
605
  return { ok: false, reason: 'disabled', ticketId, conversationId: '' };
516
606
  }
517
607
  const admission = (0, process_admission_1.captureProcessAdmission)(deps.projectId);
518
- const systemPrompt = [
519
- (0, explore_contract_refine_1.buildContractRefineSystemPrompt)(),
520
- '',
521
- '## Spec under refinement',
522
- '',
523
- `### Title`,
524
- generatedTitle,
525
- '',
526
- `### Description`,
527
- generatedDescription,
528
- ].join('\n');
529
- const args = [
530
- '--model', model ?? 'sonnet',
531
- '--tools', '__none__',
532
- '--output-format', 'stream-json',
533
- '--verbose',
534
- '--system-prompt', systemPrompt,
535
- '--max-turns', '1',
536
- '-p', explore_contract_refine_1.CONTRACT_MARKER_USER_MESSAGE,
537
- ];
608
+ const args = buildFreshRefineArgs(model ?? 'sonnet', generatedTitle, generatedDescription);
538
609
  const startedAt = now().toISOString();
539
610
  deps.broadcast({
540
611
  type: 'explore.contract_refine_started',
@@ -436,6 +436,33 @@ function applyDesktopMigrations(db) {
436
436
  () => {
437
437
  db.exec(`ALTER TABLE agent_messages ADD COLUMN context_refs TEXT;`);
438
438
  },
439
+ // 22: Project Builder day-0 conversations (add-project-builder D1). The
440
+ // Builder runs BEFORE any project exists, so its conversations live in the
441
+ // app registry DB — same shape as agent_conversations but a separate pair
442
+ // of tables: the Builder has no tier ladder and no pinned project, and its
443
+ // lifecycle (discarded after commit or abandoned) should never mix with
444
+ // agent-chat history.
445
+ () => {
446
+ db.exec(`
447
+ CREATE TABLE IF NOT EXISTS blueprint_conversations (
448
+ id TEXT PRIMARY KEY,
449
+ title TEXT,
450
+ provider TEXT NOT NULL DEFAULT 'claude',
451
+ model TEXT,
452
+ session_id TEXT,
453
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
454
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
455
+ );
456
+ CREATE TABLE IF NOT EXISTS blueprint_messages (
457
+ id TEXT PRIMARY KEY,
458
+ conversation_id TEXT NOT NULL REFERENCES blueprint_conversations(id) ON DELETE CASCADE,
459
+ role TEXT NOT NULL,
460
+ content TEXT NOT NULL,
461
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
462
+ );
463
+ CREATE INDEX IF NOT EXISTS idx_blueprint_messages_conv ON blueprint_messages(conversation_id, created_at);
464
+ `);
465
+ },
439
466
  ];
440
467
  for (let i = 0; i < migrations.length; i++) {
441
468
  const version = i + 1;
@@ -26,6 +26,9 @@ const mobile_1 = require("./mobile");
26
26
  const mcp_1 = require("./mcp");
27
27
  const agent_chat_manager_1 = require("./agent-chat-manager");
28
28
  const agent_chat_router_1 = require("./agent-chat-router");
29
+ const blueprint_chat_manager_1 = require("./blueprint-chat-manager");
30
+ const blueprint_router_1 = require("./blueprint-router");
31
+ const blueprint_commit_1 = require("./blueprint-commit");
29
32
  const agent_chat_registry_1 = require("./agent-chat-registry");
30
33
  const browser_playwright_1 = require("./browser-playwright");
31
34
  const telemetry_receiver_1 = require("./telemetry-receiver");
@@ -254,6 +257,7 @@ let _registry = null;
254
257
  let _mobileGateway = null;
255
258
  let _mcpManager = null;
256
259
  let _agentChatManager = null;
260
+ let _blueprintChatManager = null;
257
261
  let _headroomManager = null;
258
262
  server.on('upgrade', (request, socket, head) => {
259
263
  const urlStr = request.url ?? '/';
@@ -579,6 +583,16 @@ function applyPtyWsRateLimiting(ws) {
579
583
  // disabled — the rails callers are null-safe and simply skip the card.
580
584
  (0, agent_chat_registry_1.setAgentChatManager)((0, agent_chat_router_1.isAgentChatEnabled)() ? agentChatManager : null);
581
585
  app.use('/api/agent', (0, agent_chat_router_1.createAgentChatRouter)({ manager: agentChatManager, desktopDb: registry.desktopDb }));
586
+ // Project Builder day-0 chat + orchestrated commit (add-project-builder).
587
+ // App-level like agent chat — a Builder conversation runs before any project
588
+ // exists. Route-gated by SPECRAILS_PROJECT_BUILDER inside the router.
589
+ const blueprintChatManager = new blueprint_chat_manager_1.BlueprintChatManager(broadcast, registry.desktopDb);
590
+ _blueprintChatManager = blueprintChatManager;
591
+ app.use('/api/blueprint', (0, blueprint_router_1.createBlueprintRouter)({
592
+ manager: blueprintChatManager,
593
+ desktopDb: registry.desktopDb,
594
+ runCommit: (0, blueprint_commit_1.createBlueprintCommitRunner)({ registry, broadcast }),
595
+ }));
582
596
  // App-level routes. CRITICAL mount order: the desktop router is mounted at
583
597
  // '/api' BEFORE the project router below so its exact routes (e.g.
584
598
  // GET /api/projects, DELETE /api/projects/:id) are handled here, while
@@ -689,6 +703,7 @@ async function shutdown() {
689
703
  Promise.resolve().then(() => _mobileGateway?.stop()),
690
704
  Promise.resolve().then(() => _mcpManager?.stop()),
691
705
  Promise.resolve().then(() => _agentChatManager?.shutdown()),
706
+ Promise.resolve().then(() => _blueprintChatManager?.shutdown()),
692
707
  ]);
693
708
  try {
694
709
  await _headroomManager?.shutdown();
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.spawnBundledCoreInit = spawnBundledCoreInit;
4
+ exports.hasOfflineCore = hasOfflineCore;
5
+ exports.spawnNpxCoreInit = spawnNpxCoreInit;
6
+ exports.canAssembleProject = canAssembleProject;
7
+ exports.assembleProjectOffline = assembleProjectOffline;
8
+ const child_process_1 = require("child_process");
9
+ const fs_1 = require("fs");
10
+ const path_1 = require("path");
11
+ const win_spawn_1 = require("./util/win-spawn");
12
+ const bundled_core_1 = require("./bundled-core");
13
+ const bundled_openspec_1 = require("./bundled-openspec");
14
+ const path_resolver_1 = require("./path-resolver");
15
+ const framework_manager_1 = require("./framework-manager");
16
+ const artifact_registry_1 = require("./artifact-registry");
17
+ const install_config_path_1 = require("./install-config-path");
18
+ const workspace_manager_1 = require("./workspace-manager");
19
+ const core_package_1 = require("./core-package");
20
+ // ─── Offline workspace assemble (add-project-builder D4) ─────────────────────
21
+ //
22
+ // The single shared "assemble a project workspace from the BUNDLED framework"
23
+ // primitive, extracted from SetupManager's bundled-core fast path so the
24
+ // Project Builder's orchestrated commit can assemble WITHOUT the wizard's
25
+ // streaming/phase shell. Extract-don't-fork: SetupManager keeps its shell and
26
+ // its npx fallback. The Builder PREFERS the bundle (offline day 0) but falls
27
+ // back to `npx specrails-core` when no bundle is present — so `npm run dev` and
28
+ // any runtimes-less build still work. Only a DESKTOP build with a MISSING/
29
+ // corrupted bundle hard-fails (see `canAssembleProject`).
30
+ /**
31
+ * Spawn the BUNDLED specrails-core `init` (offline framework + assemble).
32
+ * Runs `node <bundled-cli> init <args>` with `SPECRAILS_CORE_SCRIPT_DIR`
33
+ * pointed at the bundle so core's template/command sources resolve from the
34
+ * app bundle, NOT a global install or npm registry. Forces RELOCATION (the
35
+ * desktop keeps the user's repo pristine). When the app also ships
36
+ * @fission-ai/openspec, `SPECRAILS_OPENSPEC_BIN`/`_NODE` make the last network
37
+ * step offline too.
38
+ *
39
+ * Returns null when no bundled core is present. Callers may then use the npx
40
+ * fallback outside the packaged desktop app.
41
+ */
42
+ function spawnBundledCoreInit(args, cwd) {
43
+ const cli = (0, bundled_core_1.getBundledCoreCli)();
44
+ const coreRoot = (0, bundled_core_1.getBundledCoreRoot)();
45
+ if (!cli || !coreRoot)
46
+ return null;
47
+ const fullArgs = [cli, 'init', ...args];
48
+ const env = { ...(0, win_spawn_1.windowsSpawnEnv)(), SPECRAILS_CORE_SCRIPT_DIR: coreRoot, SPECRAILS_RELOCATE: '1' };
49
+ const openspecCli = (0, bundled_openspec_1.getBundledOpenspecCli)();
50
+ if (openspecCli) {
51
+ env.SPECRAILS_OPENSPEC_BIN = openspecCli;
52
+ // MUST be a REAL node executable — NOT process.execPath (the packaged
53
+ // `specrails-server` pkg binary cannot run openspec's ESM CLI).
54
+ env.SPECRAILS_OPENSPEC_NODE = (0, path_resolver_1.resolveBundledNodeExe)() ?? 'node';
55
+ }
56
+ // Run the core CLI with the REAL bundled node — process.execPath only in
57
+ // dev/tests where it IS node.
58
+ const nodeBin = (0, path_resolver_1.resolveBundledNodeExe)() ?? process.execPath;
59
+ console.log(`[offline-assemble] spawning BUNDLED core: ${nodeBin} ${fullArgs.join(' ')} (cwd=${cwd})` +
60
+ (openspecCli ? ' [bundled openspec: offline]' : ' [openspec: npx fallback]'));
61
+ return (0, win_spawn_1.spawnCli)(nodeBin, fullArgs, {
62
+ cwd,
63
+ env,
64
+ stdio: ['ignore', 'pipe', 'pipe'],
65
+ });
66
+ }
67
+ /** True when the bundled specrails-core is present (offline assemble possible). */
68
+ function hasOfflineCore() {
69
+ return (0, bundled_core_1.getBundledCoreCli)() !== null;
70
+ }
71
+ const WHICH_CMD = process.platform === 'win32' ? 'where' : 'which';
72
+ /** Resolve a core binary override to an absolute path (mirrors SetupManager). */
73
+ function resolveCoreBinary(bin) {
74
+ if ((0, path_1.isAbsolute)(bin))
75
+ return bin;
76
+ if (bin.includes('/') || bin.includes('\\'))
77
+ return (0, path_1.resolve)(bin);
78
+ const result = (0, child_process_1.spawnSync)(WHICH_CMD, [bin], {
79
+ env: (0, win_spawn_1.windowsSpawnEnv)(), shell: process.platform === 'win32', encoding: 'utf-8', timeout: 5_000,
80
+ });
81
+ if (result.error || (result.status ?? 1) !== 0)
82
+ return bin;
83
+ const first = `${result.stdout ?? ''}`.trim().split(/\r?\n/)[0]?.trim();
84
+ return first && first.length > 0 ? first : bin;
85
+ }
86
+ /**
87
+ * Spawn `npx specrails-core init` (or the `SPECRAILS_CORE_BIN` override) —
88
+ * the network fallback SetupManager uses when no bundle is present. Forces
89
+ * RELOCATION so the user's repo stays pristine.
90
+ */
91
+ function spawnNpxCoreInit(args, cwd) {
92
+ const override = process.env.SPECRAILS_CORE_BIN;
93
+ const bin = override ? resolveCoreBinary(override) : 'npx';
94
+ const fullArgs = override ? ['init', ...args] : ['--yes', '--prefer-online', core_package_1.CORE_PACKAGE_SPEC, 'init', ...args];
95
+ console.log(`[offline-assemble] spawning npx core: ${bin} ${fullArgs.join(' ')} (cwd=${cwd})`);
96
+ return (0, win_spawn_1.spawnCli)(bin, fullArgs, {
97
+ cwd,
98
+ env: { ...(0, win_spawn_1.windowsSpawnEnv)(), SPECRAILS_RELOCATE: '1' },
99
+ stdio: ['ignore', 'pipe', 'pipe'],
100
+ });
101
+ }
102
+ /**
103
+ * Whether the Project Builder can assemble a workspace at all. True when the
104
+ * bundle is present (offline path) OR we're NOT in a packaged desktop build
105
+ * (dev / runtimes-less server → npx fallback). Only a DESKTOP build with a
106
+ * missing/corrupted bundle returns false → the "reinstall the app" error.
107
+ */
108
+ function canAssembleProject() {
109
+ return (0, bundled_core_1.getBundledCoreCli)() !== null || process.env.SPECRAILS_IS_DESKTOP !== '1';
110
+ }
111
+ function buildQuickInstallConfigYaml(provider, defaultModel) {
112
+ // Matches serializeInstallConfigYaml's shape (specrails-core tui-installer
113
+ // format) for a quick/template install with the default agent set.
114
+ return [
115
+ '# specrails install config — generated by Specrails Project Builder',
116
+ 'version: 1',
117
+ `provider: ${provider}`,
118
+ 'tier: quick',
119
+ 'agents:',
120
+ ' selected: []',
121
+ ' excluded: []',
122
+ 'models:',
123
+ ' preset: balanced',
124
+ ` defaults: { model: ${defaultModel} }`,
125
+ ' overrides: {}',
126
+ 'agent_teams: false',
127
+ '',
128
+ ].join('\n');
129
+ }
130
+ /**
131
+ * Assemble a project workspace from the bundled framework when available, or
132
+ * from the npx fallback in development/runtimes-less builds: registry mirror
133
+ * → framework materialize (idempotent) → one `init` per provider. Throws on
134
+ * any failure — the caller surfaces it as a failed step; nothing here registers
135
+ * the project.
136
+ */
137
+ async function assembleProjectOffline(opts) {
138
+ const { projectPath, slug, desktopProjectId, providers } = opts;
139
+ if (!canAssembleProject() && !opts.io?.spawnInit) {
140
+ throw new Error('specrails-core is not available — reinstall the Specrails app');
141
+ }
142
+ if (providers.length === 0)
143
+ throw new Error('at least one provider is required');
144
+ // Bundle present ⇒ offline bundled init; otherwise (dev / no bundle) ⇒ npx.
145
+ const useBundled = hasOfflineCore();
146
+ // Registry entry BEFORE core's init so core resolves its workspace from the
147
+ // registry and relocates under the desktop's slug (same reasoning as
148
+ // SetupManager.startInstall's pre-install mirror).
149
+ (0, artifact_registry_1.mirrorProjectEntry)({
150
+ repoPath: projectPath,
151
+ slug,
152
+ providers,
153
+ primaryProvider: providers[0],
154
+ desktopProjectId,
155
+ });
156
+ // Materialize the bundled framework for the selected providers (idempotent —
157
+ // core's ensureFramework skips when already present). Best-effort: the
158
+ // bundled init re-runs ensureFramework, so it self-heals.
159
+ const materialize = opts.io?.materialize ?? ((provs) => {
160
+ try {
161
+ const fm = new framework_manager_1.FrameworkManager({ broadcast: () => { } });
162
+ const mat = fm.materialize((0, bundled_core_1.getBundledCoreVersion)() ?? undefined, provs);
163
+ if (mat.ran && mat.errors.length > 0) {
164
+ console.warn(`[offline-assemble] framework materialize had errors: ${mat.errors.map((e) => `${e.provider}: ${e.message}`).join('; ')}`);
165
+ }
166
+ }
167
+ catch (err) {
168
+ console.warn(`[offline-assemble] framework materialize threw (non-fatal): ${err.message}`);
169
+ }
170
+ });
171
+ materialize(providers);
172
+ const spawnInit = opts.io?.spawnInit ?? (useBundled ? spawnBundledCoreInit : spawnNpxCoreInit);
173
+ const coreLabel = useBundled ? 'bundled core' : 'npx core';
174
+ for (const provider of providers) {
175
+ const configPath = (0, install_config_path_1.installConfigPathForProvider)({ slug, path: projectPath }, provider);
176
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(configPath), { recursive: true });
177
+ (0, fs_1.writeFileSync)(configPath, buildQuickInstallConfigYaml(provider, opts.defaultModel ?? 'sonnet'), 'utf-8');
178
+ const child = spawnInit(['--yes', '--from-config', configPath], projectPath);
179
+ if (!child) {
180
+ throw new Error('specrails-core is not available — reinstall the Specrails app');
181
+ }
182
+ const stderrChunks = [];
183
+ child.stderr?.on('data', (chunk) => {
184
+ stderrChunks.push(chunk.toString());
185
+ });
186
+ child.stdout?.resume(); // drain so the child never blocks on a full pipe
187
+ const code = await new Promise((resolve, reject) => {
188
+ child.once('error', (err) => reject(new Error(`failed to launch ${coreLabel}: ${err.message}`)));
189
+ child.once('close', (exitCode) => resolve(exitCode));
190
+ });
191
+ if (code !== 0) {
192
+ const tail = stderrChunks.join('').split('\n').filter(Boolean).slice(-5).join(' ').slice(0, 500);
193
+ throw new Error(`${coreLabel} init failed for ${provider} (exit ${code})${tail ? `: ${tail}` : ''}`);
194
+ }
195
+ }
196
+ // The activation gate (workspace-resolution) requires a populated workspace;
197
+ // verify assemble actually produced one so a silent core no-op cannot leave
198
+ // a half-created project that would spawn from a repo cwd.
199
+ const workspace = (0, workspace_manager_1.workspacePathFor)(slug);
200
+ if (!(0, fs_1.existsSync)(workspace)) {
201
+ throw new Error(`assemble completed but workspace was not created at ${workspace}`);
202
+ }
203
+ }
@@ -29,6 +29,11 @@ const registry_1 = require("./providers/registry");
29
29
  /** Key shape: `<providerId>:<model>` (case-sensitive). */
30
30
  exports.PRICING = {
31
31
  // Codex (OpenAI). Reference: https://openai.com/pricing
32
+ // GPT-5.6 family (GA 2026-07-09): cache reads keep the 90% discount; 5.6+
33
+ // ALSO bills cache WRITES at 1.25x the uncached input rate (new semantics).
34
+ 'codex:gpt-5.6-sol': { inputPer1M: 5.00, outputPer1M: 30.00, cacheReadPer1M: 0.50, cacheWritePer1M: 6.25, lastReviewedAt: '2026-07-17' },
35
+ 'codex:gpt-5.6-terra': { inputPer1M: 2.50, outputPer1M: 15.00, cacheReadPer1M: 0.25, cacheWritePer1M: 3.125, lastReviewedAt: '2026-07-17' },
36
+ 'codex:gpt-5.6-luna': { inputPer1M: 1.00, outputPer1M: 6.00, cacheReadPer1M: 0.10, cacheWritePer1M: 1.25, lastReviewedAt: '2026-07-17' },
32
37
  'codex:gpt-5.5': { inputPer1M: 1.25, outputPer1M: 10.00, cacheReadPer1M: 0.125, lastReviewedAt: '2026-05-17' },
33
38
  'codex:gpt-5.4': { inputPer1M: 2.50, outputPer1M: 10.00, cacheReadPer1M: 0.25, lastReviewedAt: '2026-05-17' },
34
39
  'codex:gpt-5.4-mini': { inputPer1M: 0.25, outputPer1M: 2.00, cacheReadPer1M: 0.025, lastReviewedAt: '2026-05-17' },
@@ -45,13 +45,25 @@ function registerChatRoutes(deps) {
45
45
  return;
46
46
  }
47
47
  const rawKind = req.body?.kind;
48
- const kind = rawKind === 'explore' ? 'explore' : 'sidebar';
48
+ const kind = rawKind === 'explore' ? 'explore' : rawKind === 'milestone' ? 'milestone' : 'sidebar';
49
49
  const id = (0, ids_1.newId)();
50
50
  const rawScope = req.body?.contextScope;
51
51
  if (rawScope !== undefined && kind !== 'explore') {
52
52
  res.status(400).json({ error: 'contextScope is only allowed for kind=explore' });
53
53
  return;
54
54
  }
55
+ // Milestone generation (add-project-builder D7): the target milestone id
56
+ // rides context_scope as a minimal `{ milestone }` object so ChatManager
57
+ // can seed its milestone system prompt from the workspace blueprint.
58
+ let milestoneScope;
59
+ if (kind === 'milestone') {
60
+ const rawMilestone = req.body?.milestone;
61
+ if (typeof rawMilestone !== 'string' || !/^m[0-9]{1,3}$/i.test(rawMilestone)) {
62
+ res.status(400).json({ error: 'milestone (e.g. "m2") is required for kind=milestone' });
63
+ return;
64
+ }
65
+ milestoneScope = { milestone: rawMilestone.toLowerCase() };
66
+ }
55
67
  let scope;
56
68
  if (kind === 'explore') {
57
69
  const fallback = (0, context_scope_1.getLastContextScope)(db, 'explore');
@@ -69,7 +81,7 @@ function registerChatRoutes(deps) {
69
81
  // Only persist provider when the project is multi-provider; single-provider
70
82
  // projects leave it NULL so behaviour is byte-identical to before.
71
83
  const persistProvider = (0, provider_selection_1.isMultiProvider)(project) ? provider : null;
72
- (0, db_1.createConversation)(db, { id, model, kind, contextScope: scope, provider: persistProvider });
84
+ (0, db_1.createConversation)(db, { id, model, kind, contextScope: scope ?? milestoneScope, provider: persistProvider });
73
85
  const conversation = (0, db_1.getConversation)(db, id);
74
86
  res.status(201).json({ conversation });
75
87
  });