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
@@ -0,0 +1,335 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BlueprintChatManager = void 0;
7
+ const crypto_1 = require("crypto");
8
+ const tree_kill_1 = __importDefault(require("tree-kill"));
9
+ const providers_1 = require("./providers");
10
+ const spawn_lifecycle_1 = require("./spawn-lifecycle");
11
+ const cli_prompt_1 = require("./util/cli-prompt");
12
+ const result_event_1 = require("./result-event");
13
+ const desktop_db_1 = require("./desktop-db");
14
+ const builder_cwd_manager_1 = require("./builder-cwd-manager");
15
+ const blueprint_operator_prompt_1 = require("./blueprint-operator-prompt");
16
+ const blueprint_draft_parser_1 = require("./blueprint-draft-parser");
17
+ const explore_draft_title_1 = require("./explore-draft-title");
18
+ const blueprint_store_1 = require("./blueprint-store");
19
+ class BlueprintChatManager {
20
+ _broadcast;
21
+ _db;
22
+ _active = new Map();
23
+ _abortedTurns = new Set();
24
+ _terminationTimers = new Map();
25
+ _disposed = false;
26
+ _resolveDisposed;
27
+ _disposedSignal = new Promise((resolve) => {
28
+ this._resolveDisposed = resolve;
29
+ });
30
+ constructor(broadcast, db) {
31
+ this._broadcast = broadcast;
32
+ this._db = db;
33
+ }
34
+ async _awaitWhileLive(work) {
35
+ if (this._disposed)
36
+ return { disposed: true };
37
+ return Promise.race([
38
+ work.then((value) => ({ disposed: false, value })),
39
+ this._disposedSignal.then(() => ({ disposed: true })),
40
+ ]);
41
+ }
42
+ /** Own a dedicated POSIX process group so descendants remain killable after
43
+ * the root CLI exits (mirrors AgentChatManager). */
44
+ _spawnOwned(binary, args, options = {}) {
45
+ if (process.platform === 'win32')
46
+ return (0, cli_prompt_1.spawnAiCli)(binary, args, options);
47
+ return (0, cli_prompt_1.spawnAiCli)(binary, args, { ...options, detached: true });
48
+ }
49
+ _terminate(child) {
50
+ if (this._terminationTimers.has(child))
51
+ return;
52
+ try {
53
+ if (!child.pid) {
54
+ child.kill('SIGTERM');
55
+ return;
56
+ }
57
+ const pid = child.pid;
58
+ (0, tree_kill_1.default)(pid, 'SIGTERM');
59
+ const timer = setTimeout(() => {
60
+ this._terminationTimers.delete(child);
61
+ if (process.platform !== 'win32') {
62
+ try {
63
+ process.kill(-pid, 'SIGKILL');
64
+ }
65
+ catch { /* group already gone */ }
66
+ }
67
+ else {
68
+ try {
69
+ (0, tree_kill_1.default)(pid, 'SIGKILL', () => { });
70
+ }
71
+ catch { /* gone */ }
72
+ }
73
+ }, 2000);
74
+ timer.unref?.();
75
+ this._terminationTimers.set(child, timer);
76
+ }
77
+ catch {
78
+ /* already gone */
79
+ }
80
+ }
81
+ isStreaming(conversationId) {
82
+ return !this._disposed && this._active.has(conversationId);
83
+ }
84
+ /** Stop the in-flight turn (partial text is kept; never surfaced as error). */
85
+ abort(conversationId) {
86
+ const child = this._active.get(conversationId);
87
+ if (!child)
88
+ return;
89
+ this._abortedTurns.add(conversationId);
90
+ this._active.delete(conversationId);
91
+ this._terminate(child);
92
+ }
93
+ _emitError(conversationId, error) {
94
+ if (this._disposed)
95
+ return;
96
+ this._broadcast({ type: 'blueprint.error', conversationId, error, timestamp: new Date().toISOString() });
97
+ }
98
+ /**
99
+ * Runs one Builder turn: persists the user message, spawns the AI CLI from
100
+ * the builder cwd, streams deltas as `blueprint.stream`, then persists the
101
+ * STRIPPED assistant reply (blueprint-draft fences removed) and broadcasts
102
+ * `blueprint.done` with the last valid blueprint snapshot.
103
+ */
104
+ async sendMessage(conversationId, userText, options = {}) {
105
+ if (this._disposed)
106
+ return;
107
+ const conversation = (0, blueprint_store_1.getBlueprintConversation)(this._db, conversationId);
108
+ if (!conversation) {
109
+ this._emitError(conversationId, 'Unknown conversation');
110
+ return;
111
+ }
112
+ if (this._active.has(conversationId)) {
113
+ this._emitError(conversationId, 'A turn is already streaming for this conversation.');
114
+ return;
115
+ }
116
+ try {
117
+ await this._runTurn(conversation, userText, options);
118
+ }
119
+ catch (err) {
120
+ if (this._disposed)
121
+ return;
122
+ console.error(`[blueprint-chat] turn failed (${conversationId}):`, err);
123
+ this._emitError(conversationId, err instanceof Error ? err.message : 'The Builder turn failed.');
124
+ }
125
+ finally {
126
+ this._abortedTurns.delete(conversationId);
127
+ }
128
+ }
129
+ async _runTurn(conversation, userText, options) {
130
+ const conversationId = conversation.id;
131
+ const adapter = (0, providers_1.getAdapter)(conversation.provider);
132
+ const catalog = new Set(adapter.modelCatalog().map((m) => m.value));
133
+ const requested = options.model || conversation.model;
134
+ const model = requested && catalog.has(requested) ? requested : adapter.defaultModel();
135
+ // Reasoning effort: only for providers with the knob, only when the value
136
+ // is in their catalog (else undefined = provider default).
137
+ const efforts = adapter.capabilities.reasoningEfforts ?? [];
138
+ const requestedEffort = options.reasoningEffort;
139
+ const reasoningEffort = requestedEffort
140
+ && efforts.includes(requestedEffort)
141
+ ? requestedEffort
142
+ : undefined;
143
+ const cwd = (0, builder_cwd_manager_1.ensureBuilderCwd)();
144
+ const timestamp = () => new Date().toISOString();
145
+ (0, blueprint_store_1.addBlueprintMessage)(this._db, { conversationId, role: 'user', content: userText });
146
+ // Deterministic title from the first user prompt (no AI spend).
147
+ if (!conversation.title) {
148
+ const title = (0, explore_draft_title_1.generateAutoTitle)([{ role: 'user', content: userText }]);
149
+ if (title) {
150
+ (0, blueprint_store_1.updateBlueprintConversation)(this._db, conversationId, { title });
151
+ }
152
+ }
153
+ const invoke = async (useResume) => {
154
+ const action = useResume ? 'chat-resume' : 'chat-turn';
155
+ const startedAt = new Date().toISOString();
156
+ let streamed = '';
157
+ let capturedSessionId = useResume ? conversation.session_id ?? null : null;
158
+ let capturedError = null;
159
+ console.log(`[blueprint-chat] turn start conv=${conversationId} provider=${adapter.id} action=${action} model=${model}`);
160
+ const invocation = this._awaitWhileLive((0, spawn_lifecycle_1.runAiCliInvocation)({
161
+ adapter,
162
+ action,
163
+ cwd,
164
+ env: { ...process.env },
165
+ spawn: this._spawnOwned.bind(this),
166
+ buildOpts: {
167
+ prompt: userText,
168
+ systemPrompt: adapter.capabilities.systemPromptArg ? blueprint_operator_prompt_1.BUILDER_SYSTEM_PROMPT : undefined,
169
+ model,
170
+ sessionId: useResume ? conversation.session_id ?? undefined : undefined,
171
+ reasoning_effort: reasoningEffort,
172
+ },
173
+ onSpawn: (child) => {
174
+ if (this._disposed) {
175
+ this._terminate(child);
176
+ return;
177
+ }
178
+ this._active.set(conversationId, child);
179
+ },
180
+ onEvent: (ev) => {
181
+ if (this._disposed)
182
+ return;
183
+ switch (ev.kind) {
184
+ case 'text-delta':
185
+ streamed += ev.text;
186
+ // Suppress broadcasts once the child left _active (abort keeps
187
+ // flushing buffered stdout) so stragglers can't resurrect
188
+ // client-side streaming state.
189
+ if (this._active.has(conversationId)) {
190
+ this._broadcast({ type: 'blueprint.stream', conversationId, delta: ev.text, timestamp: timestamp() });
191
+ }
192
+ break;
193
+ case 'session-started':
194
+ if (ev.sessionId)
195
+ capturedSessionId = ev.sessionId;
196
+ break;
197
+ case 'result': {
198
+ const sid = ev.payload.session_id;
199
+ if (sid)
200
+ capturedSessionId = sid;
201
+ break;
202
+ }
203
+ case 'error':
204
+ capturedError = ev.message;
205
+ break;
206
+ default:
207
+ break;
208
+ }
209
+ },
210
+ }));
211
+ const invocationResult = await invocation;
212
+ if (invocationResult.disposed) {
213
+ this._active.delete(conversationId);
214
+ return { disposed: true, text: '', sessionId: capturedSessionId, error: null, code: null, spawnFailed: false, stderrTail: '', events: [], startedAt };
215
+ }
216
+ const result = invocationResult.value;
217
+ this._active.delete(conversationId);
218
+ const text = streamed.trim();
219
+ if (result.stderrTail && (result.spawnFailed || (result.code ?? 0) !== 0 || !text)) {
220
+ console.error(`[blueprint-chat] ${adapter.id} stderr:\n${result.stderrTail}`);
221
+ }
222
+ return { disposed: false, text, sessionId: capturedSessionId, error: capturedError, code: result.code, spawnFailed: result.spawnFailed, stderrTail: result.stderrTail, events: result.events, startedAt };
223
+ };
224
+ const persistSession = (sessionId) => {
225
+ if (this._disposed)
226
+ return;
227
+ if ((0, blueprint_store_1.getBlueprintConversation)(this._db, conversationId)) {
228
+ (0, blueprint_store_1.updateBlueprintConversation)(this._db, conversationId, { session_id: sessionId });
229
+ }
230
+ };
231
+ // One agent_invocations row per settled turn, project_id NULL (day-0 spend).
232
+ const record = (outcome, status) => {
233
+ if (this._disposed || outcome.disposed)
234
+ return;
235
+ this._recordTurn(conversation, adapter, model, outcome, status);
236
+ };
237
+ const settleText = (r) => {
238
+ const { stripped, blueprint, rawBlueprint } = (0, blueprint_draft_parser_1.parseBlueprintDraftBlocks)(r.text);
239
+ const content = stripped.trim();
240
+ if ((0, blueprint_store_1.getBlueprintConversation)(this._db, conversationId)) {
241
+ (0, blueprint_store_1.addBlueprintMessage)(this._db, { conversationId, role: 'assistant', content: content || r.text });
242
+ persistSession(r.sessionId);
243
+ }
244
+ this._broadcast({
245
+ type: 'blueprint.done',
246
+ conversationId,
247
+ fullText: content || r.text,
248
+ blueprint,
249
+ rawBlueprint,
250
+ timestamp: timestamp(),
251
+ });
252
+ };
253
+ const canResume = !!conversation.session_id && adapter.capabilities.nativeResume;
254
+ let r = await invoke(canResume);
255
+ if (this._disposed || r.disposed)
256
+ return;
257
+ if (!(0, blueprint_store_1.getBlueprintConversation)(this._db, conversationId)) {
258
+ record(r, 'aborted'); // deleted mid-turn; the spawn still billed
259
+ return;
260
+ }
261
+ if (this._abortedTurns.delete(conversationId)) {
262
+ if (r.text)
263
+ settleText(r);
264
+ record(r, 'aborted');
265
+ return;
266
+ }
267
+ // Auto-heal a stale session: a resume with no text retries once fresh.
268
+ if (canResume && !r.spawnFailed && !r.text) {
269
+ (0, blueprint_store_1.updateBlueprintConversation)(this._db, conversationId, { session_id: null });
270
+ r = await invoke(false);
271
+ if (this._disposed || r.disposed)
272
+ return;
273
+ if (!(0, blueprint_store_1.getBlueprintConversation)(this._db, conversationId)) {
274
+ record(r, 'aborted');
275
+ return;
276
+ }
277
+ if (this._abortedTurns.delete(conversationId)) {
278
+ if (r.text)
279
+ settleText(r);
280
+ record(r, 'aborted');
281
+ return;
282
+ }
283
+ }
284
+ if (r.spawnFailed) {
285
+ this._emitError(conversationId, `Failed to launch ${adapter.binary}. Is it installed and on PATH?`);
286
+ record(r, 'failed');
287
+ return;
288
+ }
289
+ if (r.text) {
290
+ settleText(r);
291
+ record(r, 'success');
292
+ return;
293
+ }
294
+ persistSession(null);
295
+ const reason = r.error ||
296
+ (r.stderrTail ? r.stderrTail.split('\n').filter(Boolean).slice(-3).join(' ').slice(0, 300) : '') ||
297
+ (r.code !== 0 ? `${adapter.binary} exited with code ${r.code}` : 'The Builder returned no output.');
298
+ this._emitError(conversationId, reason);
299
+ record(r, 'failed');
300
+ }
301
+ _recordTurn(conversation, adapter, model, outcome, status) {
302
+ if (this._disposed)
303
+ return;
304
+ try {
305
+ const { result, estimated } = (0, result_event_1.finaliseInvocationResult)(adapter, outcome.events, {
306
+ fallbackModel: model,
307
+ });
308
+ (0, desktop_db_1.recordAgentInvocation)(this._db, {
309
+ id: (0, crypto_1.randomUUID)(),
310
+ conversation_id: conversation.id,
311
+ project_id: null,
312
+ provider: adapter.id,
313
+ status,
314
+ started_at: outcome.startedAt,
315
+ finished_at: new Date().toISOString(),
316
+ total_cost_usd_estimated: estimated,
317
+ ...result,
318
+ });
319
+ }
320
+ catch (err) {
321
+ console.error(`[blueprint-chat] recordAgentInvocation failed (${conversation.id}):`, err);
322
+ }
323
+ }
324
+ /** Kills all live children and permanently gates DB access. */
325
+ shutdown() {
326
+ if (this._disposed)
327
+ return;
328
+ this._disposed = true;
329
+ this._resolveDisposed();
330
+ for (const child of this._active.values())
331
+ this._terminate(child);
332
+ this._active.clear();
333
+ }
334
+ }
335
+ exports.BlueprintChatManager = BlueprintChatManager;
@@ -0,0 +1,335 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.classifyGhCreateError = classifyGhCreateError;
7
+ exports.renderReadme = renderReadme;
8
+ exports.createBlueprintCommitRunner = createBlueprintCommitRunner;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const crypto_1 = require("crypto");
13
+ const win_spawn_1 = require("./util/win-spawn");
14
+ const desktop_db_1 = require("./desktop-db");
15
+ const providers_1 = require("./providers");
16
+ const blueprint_draft_parser_1 = require("./blueprint-draft-parser");
17
+ const blueprint_types_1 = require("./blueprint-types");
18
+ const blueprint_spec_quality_1 = require("./blueprint-spec-quality");
19
+ const blueprint_render_1 = require("./blueprint-render");
20
+ const offline_assemble_1 = require("./offline-assemble");
21
+ const artifact_registry_1 = require("./artifact-registry");
22
+ const workspace_manager_1 = require("./workspace-manager");
23
+ const ticket_store_1 = require("./ticket-store");
24
+ const project_router_helpers_1 = require("./project-router-helpers");
25
+ function execCli(bin, args, cwd) {
26
+ return new Promise((resolve) => {
27
+ const opts = { cwd, env: (0, win_spawn_1.windowsSpawnEnv)(), stdio: ['ignore', 'pipe', 'pipe'] };
28
+ const child = (0, win_spawn_1.spawnCli)(bin, args, opts);
29
+ const stderrChunks = [];
30
+ child.stderr?.on('data', (chunk) => stderrChunks.push(chunk.toString()));
31
+ child.stdout?.resume();
32
+ child.once('error', (err) => resolve({ code: null, stderr: err.message }));
33
+ child.once('close', (code) => {
34
+ resolve({ code, stderr: stderrChunks.join('').split('\n').filter(Boolean).slice(-5).join(' ').slice(0, 500) });
35
+ });
36
+ });
37
+ }
38
+ /** Classify `gh repo create` stderr into a stable code the client can i18n. */
39
+ function classifyGhCreateError(stderr) {
40
+ const s = stderr.toLowerCase();
41
+ if (/not logged in|gh auth login|authentication|http 401/.test(s))
42
+ return 'gh_not_authenticated';
43
+ if (/http 403|forbidden|scope|permission/.test(s))
44
+ return 'gh_scope';
45
+ if (/already exists/.test(s))
46
+ return 'gh_repo_exists';
47
+ if (/could not resolve|dial tcp|timeout|connection|network|no such host/.test(s))
48
+ return 'gh_network';
49
+ return 'gh_failed';
50
+ }
51
+ function expandLocation(raw) {
52
+ const home = process.env.SPECRAILS_REGISTRY_HOME || os_1.default.homedir();
53
+ const expanded = raw === '~' ? home : raw.startsWith('~/') || raw.startsWith('~\\') ? path_1.default.join(home, raw.slice(2)) : raw;
54
+ return path_1.default.resolve(expanded);
55
+ }
56
+ /** Deterministic README from the blueprint pitch — the commit must stay
57
+ * offline-capable, so no AI call here (design open question, pinned). */
58
+ function renderReadme(blueprint, name) {
59
+ const lines = [`# ${name}`, ''];
60
+ if (blueprint.product.pitch)
61
+ lines.push(blueprint.product.pitch, '');
62
+ if (blueprint.coreFlow)
63
+ lines.push(`**Core flow:** ${blueprint.coreFlow}`, '');
64
+ const stackBits = [blueprint.stack.language, blueprint.stack.framework, blueprint.stack.db].filter(Boolean);
65
+ if (stackBits.length > 0)
66
+ lines.push(`**Stack:** ${stackBits.join(' · ')}`, '');
67
+ lines.push('---', '', '_Bootstrapped with the Specrails Project Builder._', '');
68
+ return lines.join('\n');
69
+ }
70
+ /** Mirrors desktop-router's slugify/allocateSlug (kept local — same contract). */
71
+ function slugifyName(name) {
72
+ return name
73
+ .toLowerCase()
74
+ .normalize('NFKD')
75
+ .replace(/[̀-ͯ]/g, '')
76
+ .replace(/[^a-z0-9]+/g, '-')
77
+ .replace(/^-+|-+$/g, '');
78
+ }
79
+ function allocateSlug(name, existing) {
80
+ let base = slugifyName(name);
81
+ if (base === '')
82
+ base = 'project';
83
+ if (!existing.has(base))
84
+ return base;
85
+ let n = 2;
86
+ while (existing.has(`${base}-${n}`))
87
+ n += 1;
88
+ return `${base}-${n}`;
89
+ }
90
+ function dirMissingOrEmpty(dir) {
91
+ try {
92
+ const entries = fs_1.default.readdirSync(dir);
93
+ return entries.length === 0;
94
+ }
95
+ catch (err) {
96
+ return err.code === 'ENOENT';
97
+ }
98
+ }
99
+ function createBlueprintCommitRunner(deps) {
100
+ const io = {
101
+ hasCore: deps.io?.hasCore ?? offline_assemble_1.canAssembleProject,
102
+ mkdir: deps.io?.mkdir ?? ((dir) => fs_1.default.mkdirSync(dir, { recursive: true })),
103
+ exec: deps.io?.exec ?? execCli,
104
+ assemble: deps.io?.assemble ?? (async (opts) => (0, offline_assemble_1.assembleProjectOffline)(opts)),
105
+ writePair: deps.io?.writePair ?? blueprint_render_1.writeBlueprintPair,
106
+ mutateTickets: deps.io?.mutateTickets ?? ((filePath, fn) => { (0, ticket_store_1.mutateStore)(filePath, fn); }),
107
+ registerProject: deps.io?.registerProject ??
108
+ ((opts) => {
109
+ const ctx = deps.registry.addProject({
110
+ id: opts.id,
111
+ slug: opts.slug,
112
+ name: opts.name,
113
+ path: opts.path,
114
+ provider: opts.providers[0],
115
+ providers: opts.providers,
116
+ });
117
+ deps.broadcast({
118
+ type: 'desktop.project_added',
119
+ project: ctx.project,
120
+ timestamp: new Date().toISOString(),
121
+ });
122
+ }),
123
+ };
124
+ const validate = (input) => {
125
+ const name = typeof input.name === 'string' ? input.name.trim() : '';
126
+ if (!name)
127
+ return { ok: false, error: 'invalid_name' };
128
+ if (typeof input.location !== 'string' || !input.location.trim()) {
129
+ return { ok: false, error: 'invalid_location' };
130
+ }
131
+ const location = expandLocation(input.location.trim());
132
+ const providers = Array.isArray(input.providers)
133
+ ? [...new Set(input.providers.filter((p) => typeof p === 'string' && p.length > 0))]
134
+ : [];
135
+ if (providers.length === 0)
136
+ return { ok: false, error: 'providers_required' };
137
+ for (const p of providers) {
138
+ if (!(0, providers_1.hasAdapter)(p))
139
+ return { ok: false, error: 'unknown_provider', detail: p };
140
+ }
141
+ const blueprint = (0, blueprint_draft_parser_1.coerceBlueprint)(input.blueprint);
142
+ if (!blueprint)
143
+ return { ok: false, error: 'invalid_blueprint' };
144
+ if (blueprint.m1Specs.length === 0) {
145
+ return { ok: false, error: 'm1_specs_required' };
146
+ }
147
+ if (blueprint.m1Specs.length > blueprint_types_1.M1_SPECS_MAX) {
148
+ return { ok: false, error: 'm1_specs_over_cap', detail: `max ${blueprint_types_1.M1_SPECS_MAX}` };
149
+ }
150
+ const rawBlueprint = input.blueprint;
151
+ const quality = (0, blueprint_spec_quality_1.analyzeBuilderSpecBatch)({
152
+ specsComplete: rawBlueprint.specsComplete,
153
+ specs: Array.isArray(rawBlueprint.m1Specs) ? rawBlueprint.m1Specs : [],
154
+ }, { milestoneLabel: 'M1', minSpecs: blueprint_types_1.M1_SPECS_MIN, maxSpecs: blueprint_types_1.M1_SPECS_MAX, requireScaffold: true });
155
+ if (!quality.valid) {
156
+ return { ok: false, error: 'm1_spec_quality_invalid', detail: (0, blueprint_spec_quality_1.firstBuilderSpecQualityDetail)(quality) };
157
+ }
158
+ if (!io.hasCore()) {
159
+ // Only a packaged desktop build with a missing/corrupted bundle lands
160
+ // here (dev + runtimes-less servers fall back to npx — see
161
+ // canAssembleProject). Keep the "reinstall" message for that case.
162
+ return { ok: false, error: 'bundled_framework_missing', detail: 'The bundled framework is missing — reinstall the Specrails app.' };
163
+ }
164
+ if (!dirMissingOrEmpty(location)) {
165
+ return { ok: false, error: 'location_not_empty', detail: location };
166
+ }
167
+ // No existing registry entry may map this repo path (an entry means a
168
+ // workspace already belongs to some other project for this realpath).
169
+ try {
170
+ if (fs_1.default.existsSync(location) && (0, artifact_registry_1.resolveArtifacts)(location).entry) {
171
+ return { ok: false, error: 'location_already_registered', detail: location };
172
+ }
173
+ }
174
+ catch {
175
+ /* unreadable registry — never block on the projection; addProject re-mirrors */
176
+ }
177
+ return { ok: true, blueprint, name, location, providers, createGithubRepo: input.createGithubRepo === true };
178
+ };
179
+ const start = (input) => {
180
+ const validated = validate(input);
181
+ if (!validated.ok)
182
+ throw new Error(`invalid commit input: ${validated.error}`);
183
+ const commitId = (0, crypto_1.randomUUID)();
184
+ void run(commitId, validated);
185
+ return commitId;
186
+ };
187
+ const emit = (commitId, step, status, detail, code) => {
188
+ deps.broadcast({
189
+ type: 'blueprint.commit_progress',
190
+ commitId,
191
+ step,
192
+ status,
193
+ ...(detail ? { detail } : {}),
194
+ ...(code ? { code } : {}),
195
+ timestamp: new Date().toISOString(),
196
+ });
197
+ };
198
+ const run = async (commitId, v) => {
199
+ const { blueprint, name, location, providers } = v;
200
+ const projectId = (0, crypto_1.randomUUID)();
201
+ const existingSlugs = new Set((0, desktop_db_1.listProjects)(deps.registry.desktopDb).map((p) => p.slug));
202
+ const slug = allocateSlug(name, existingSlugs);
203
+ let step = 'create-dir';
204
+ try {
205
+ // 1. create target directory
206
+ emit(commitId, step, 'running');
207
+ io.mkdir(location);
208
+ emit(commitId, step, 'done');
209
+ // 2. git init -b main + deterministic README + initial commit
210
+ step = 'git-init';
211
+ emit(commitId, step, 'running');
212
+ const init = await io.exec('git', ['init', '-b', 'main'], location);
213
+ if (init.code !== 0)
214
+ throw new Error(`git init failed: ${init.stderr || `exit ${init.code}`}`);
215
+ fs_1.default.writeFileSync(path_1.default.join(location, 'README.md'), renderReadme(blueprint, name), 'utf-8');
216
+ const add = await io.exec('git', ['add', 'README.md'], location);
217
+ if (add.code !== 0)
218
+ throw new Error(`git add failed: ${add.stderr || `exit ${add.code}`}`);
219
+ const commit = await io.exec('git', ['-c', 'user.name=Specrails', '-c', 'user.email=builder@specrails.dev', 'commit', '-m', 'Initial commit'], location);
220
+ if (commit.code !== 0)
221
+ throw new Error(`git commit failed: ${commit.stderr || `exit ${commit.code}`}`);
222
+ emit(commitId, step, 'done');
223
+ // 3. registry allocation + offline workspace assemble (per provider)
224
+ step = 'assemble';
225
+ emit(commitId, step, 'running');
226
+ await io.assemble({ projectPath: location, slug, desktopProjectId: projectId, providers });
227
+ emit(commitId, step, 'done');
228
+ const workspace = (0, workspace_manager_1.workspacePathFor)(slug);
229
+ // 4. blueprint pair into the workspace (repo stays pristine)
230
+ step = 'blueprint';
231
+ emit(commitId, step, 'running');
232
+ io.writePair(workspace, blueprint);
233
+ emit(commitId, step, 'done');
234
+ // 5. insert M1 tickets (todo, label M1, scaffold first, advisory ids back)
235
+ step = 'tickets';
236
+ emit(commitId, step, 'running');
237
+ const insertedIds = [];
238
+ const ticketsPath = path_1.default.join(workspace, '.specrails', 'local-tickets.json');
239
+ io.mutateTickets(ticketsPath, (store) => {
240
+ const indexToId = new Map();
241
+ blueprint.m1Specs.forEach((spec, index) => {
242
+ const id = store.next_id;
243
+ store.next_id += 1;
244
+ const now = new Date().toISOString();
245
+ const labels = spec.labels.includes('M1') ? spec.labels.slice() : [...spec.labels, 'M1'];
246
+ const prereqIndex = spec.dependsOnIndex;
247
+ const prerequisites = prereqIndex !== undefined && indexToId.has(prereqIndex) ? [indexToId.get(prereqIndex)] : [];
248
+ const ticket = {
249
+ id,
250
+ title: spec.title,
251
+ description: (0, project_router_helpers_1.formatDescriptionWithCriteria)(spec.description, spec.acceptanceCriteria),
252
+ status: 'todo',
253
+ priority: spec.priority,
254
+ labels,
255
+ assignee: null,
256
+ prerequisites,
257
+ metadata: {},
258
+ origin_conversation_id: null,
259
+ is_epic: false,
260
+ parent_epic_id: null,
261
+ execution_order: index + 1,
262
+ short_summary: spec.shortSummary,
263
+ created_at: now,
264
+ updated_at: now,
265
+ created_by: 'project-builder',
266
+ source: 'project-builder',
267
+ };
268
+ store.tickets[String(id)] = ticket;
269
+ indexToId.set(index, id);
270
+ insertedIds.push(id);
271
+ });
272
+ });
273
+ // Record advisory ids + flip M1 → committed, then re-render the pair.
274
+ const m1 = blueprint.milestones.find((m) => m.id === 'm1') ?? blueprint.milestones[0];
275
+ if (m1) {
276
+ m1.status = 'committed';
277
+ m1.ticketIds = insertedIds;
278
+ }
279
+ io.writePair(workspace, blueprint);
280
+ emit(commitId, step, 'done');
281
+ // 6. register the project — LAST mutation before the optional remote
282
+ step = 'register';
283
+ emit(commitId, step, 'running');
284
+ io.registerProject({ id: projectId, slug, name, path: location, providers });
285
+ emit(commitId, step, 'done');
286
+ // 7. best-effort GitHub remote — never aborts. Pre-flight guards against
287
+ // a stale client cache sending createGithubRepo with gh absent/unauthed.
288
+ if (v.createGithubRepo) {
289
+ step = 'github';
290
+ emit(commitId, step, 'running');
291
+ try {
292
+ const auth = await io.exec('gh', ['auth', 'token'], location);
293
+ if (auth.code === null) {
294
+ emit(commitId, step, 'warning', auth.stderr || 'gh not found', 'gh_not_installed');
295
+ }
296
+ else if (auth.code !== 0) {
297
+ emit(commitId, step, 'warning', auth.stderr || `gh auth token exited with ${auth.code}`, 'gh_not_authenticated');
298
+ }
299
+ else {
300
+ const gh = await io.exec('gh', ['repo', 'create', slug, '--private', '--source', '.', '--push'], location);
301
+ if (gh.code === 0) {
302
+ emit(commitId, step, 'done');
303
+ }
304
+ else {
305
+ const detail = gh.stderr || `gh exited with ${gh.code}`;
306
+ emit(commitId, step, 'warning', detail, classifyGhCreateError(detail));
307
+ }
308
+ }
309
+ }
310
+ catch (err) {
311
+ emit(commitId, step, 'warning', err.message, 'gh_failed');
312
+ }
313
+ }
314
+ deps.broadcast({
315
+ type: 'blueprint.commit_done',
316
+ commitId,
317
+ projectId,
318
+ timestamp: new Date().toISOString(),
319
+ });
320
+ }
321
+ catch (err) {
322
+ const message = err instanceof Error ? err.message : String(err);
323
+ console.error(`[blueprint-commit] failed at ${step}:`, message);
324
+ emit(commitId, step, 'failed', message);
325
+ deps.broadcast({
326
+ type: 'blueprint.commit_failed',
327
+ commitId,
328
+ step,
329
+ error: message,
330
+ timestamp: new Date().toISOString(),
331
+ });
332
+ }
333
+ };
334
+ return { validate, start };
335
+ }