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.
- package/client/dist/assets/{ActivityFeedPage-CDMzpGXd.js → ActivityFeedPage-AQgpr9qG.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-BVTacapZ.js → AgentBrowserCapture-I7a0usG7.js} +1 -1
- package/client/dist/assets/AgentModeCodePane-BqOVg15n.js +2 -0
- package/client/dist/assets/{AgentModeJobsPane-DAjN8Au5.js → AgentModeJobsPane-4SG6FF27.js} +1 -1
- package/client/dist/assets/{AgentsPage-CebykajE.js → AgentsPage-C3FgYlEi.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-BiPrVvgv.js → AnalyticsPage-BcMNByW5.js} +1 -1
- package/client/dist/assets/{CodePage-Dw9eQFRS.js → CodePage-CgdDkXp-.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-cCG_JAEi.js → DesktopAnalyticsPage-DPMcS88n.js} +1 -1
- package/client/dist/assets/{DocsDialog-C1kZ_mRP.js → DocsDialog-BC7574Zi.js} +1 -1
- package/client/dist/assets/{DocsPage-3GivxWJm.js → DocsPage-Dd9yilUg.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-KuZXGdr8.js → InteractiveJobComposer-CqC_0VQL.js} +1 -1
- package/client/dist/assets/{JobDetailModal-DWpRALkB.js → JobDetailModal-BFNXOhk1.js} +1 -1
- package/client/dist/assets/{JobDetailPage-mrXDKafj.js → JobDetailPage-DM-6zYpA.js} +1 -1
- package/client/dist/assets/{JobsPage-CUYXjcoc.js → JobsPage-1dkjPk1k.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-CwjBD5Gm.js → LoopBuilderPage-BPeqz6cG.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-CO9ton0-.js → LoopPreviewModal-EdkyYaGN.js} +1 -1
- package/client/dist/assets/{LoopsPage-CLL5UuvN.js → LoopsPage-nZfgiaK8.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext-DJAkRYps.js → MinimizedChatsContext-Cs34FPTz.js} +1 -1
- package/client/dist/assets/{PluginsPage-DZvCC5Ri.js → PluginsPage-CwFfFBzW.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-C7Hesadg.js → ProjectSettingsDialog-DHsxLFMT.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-DczCzUzP.js → TemplatePreviewModal-wcI3nGNr.js} +1 -1
- package/client/dist/assets/TicketDetailModal-7EloKP1C.js +14 -0
- package/client/dist/assets/{Trans-Cm9rhdnG.js → Trans-CPISVji8.js} +1 -1
- package/client/dist/assets/agent-BABjRt0P.js +1 -0
- package/client/dist/assets/agent-BuUpgqVx.js +1 -0
- package/client/dist/assets/agent-CCDZesgU.js +1 -0
- package/client/dist/assets/agent-CVn64zWH.js +1 -0
- package/client/dist/assets/agent-CWeOny0E.js +1 -0
- package/client/dist/assets/agent-DOysmad_.js +1 -0
- package/client/dist/assets/agent-DSsjTBhn.js +1 -0
- package/client/dist/assets/agent-DlZMxfM9.js +1 -0
- package/client/dist/assets/builder-BH8Fua-g.js +1 -0
- package/client/dist/assets/builder-BoV7DgKd.js +1 -0
- package/client/dist/assets/builder-Cc_Lf79C.js +1 -0
- package/client/dist/assets/builder-CwA4T7gP.js +1 -0
- package/client/dist/assets/builder-CwZ7Bme2.js +1 -0
- package/client/dist/assets/builder-CxW-JFsR.js +1 -0
- package/client/dist/assets/builder-D94lrV7x.js +1 -0
- package/client/dist/assets/builder-cqHpCyGa.js +1 -0
- package/client/dist/assets/{dist-js-BERxOT0X.js → dist-js-Bt2zW2YG.js} +1 -1
- package/client/dist/assets/{dist-js-Di8lflPo.js → dist-js-zvt5ap3o.js} +1 -1
- package/client/dist/assets/{formatDistanceToNow-BAXZRbEO.js → formatDistanceToNow-ByKli7Q_.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-DwzZXHtg.js → getTimezoneOffsetInMilliseconds-qrcBaLW1.js} +1 -1
- package/client/dist/assets/i18n-Bf9Ckd10.js +2 -0
- package/client/dist/assets/index-Dh9i4CnN.css +2 -0
- package/client/dist/assets/index-xkzK3E5i.js +76 -0
- package/client/dist/assets/setup-3832BJCo.js +1 -0
- package/client/dist/assets/setup-B8J-gECe.js +1 -0
- package/client/dist/assets/setup-BCIOxVsc.js +1 -0
- package/client/dist/assets/setup-BciLj_tU.js +1 -0
- package/client/dist/assets/setup-CITtPjed.js +1 -0
- package/client/dist/assets/setup-CTkE3cfh.js +1 -0
- package/client/dist/assets/setup-ajB4rYm-.js +1 -0
- package/client/dist/assets/setup-xFO1wNl0.js +1 -0
- package/client/dist/assets/{spending-BLb5pj6G.js → spending-DMnCXy84.js} +1 -1
- package/client/dist/assets/{useBrowserCaptureSession-DR1_jL56.js → useBrowserCaptureSession-CkBJShUw.js} +1 -1
- package/client/dist/assets/{useDesktop-D4DxBGPU.js → useDesktop-B4pSBIDd.js} +1 -1
- package/client/dist/index.html +14 -13
- package/docs/codex.md +2 -1
- package/docs/customizing.md +1 -1
- package/docs/internals/gemini-mcp-registration.md +1 -1
- package/docs/internals/global-artifacts-alignment-contract.md +4 -4
- package/docs/internals/project-builder.md +306 -0
- package/docs/internals/safe-pr-review-flow.md +29 -19
- package/package.json +1 -1
- package/server/dist/agent-operator-prompt.js +7 -1
- package/server/dist/blueprint-chat-manager.js +335 -0
- package/server/dist/blueprint-commit.js +335 -0
- package/server/dist/blueprint-draft-parser.js +168 -0
- package/server/dist/blueprint-operator-prompt.js +149 -0
- package/server/dist/blueprint-render.js +105 -0
- package/server/dist/blueprint-router.js +173 -0
- package/server/dist/blueprint-spec-quality.js +167 -0
- package/server/dist/blueprint-store.js +49 -0
- package/server/dist/blueprint-types.js +29 -0
- package/server/dist/builder-cwd-manager.js +57 -0
- package/server/dist/chat-manager.js +435 -23
- package/server/dist/context-scope.js +6 -3
- package/server/dist/contract-refine-runner.js +143 -72
- package/server/dist/desktop-db.js +27 -0
- package/server/dist/index.js +15 -0
- package/server/dist/offline-assemble.js +203 -0
- package/server/dist/pricing.js +5 -0
- package/server/dist/project-router-chat.js +14 -2
- package/server/dist/project-router.js +130 -7
- package/server/dist/providers/claude-adapter.js +8 -2
- package/server/dist/providers/codex-adapter.js +23 -10
- package/server/dist/providers/gemini-adapter.js +16 -7
- package/server/dist/rail-isolated-launch.js +11 -1
- package/server/dist/setup-manager.js +6 -54
- package/server/dist/spec-models.js +3 -0
- package/server/dist/workspace-manager.js +3 -1
- package/server/dist/worktree-overlay.js +233 -51
- package/client/dist/assets/AgentModeCodePane-D9bhFyuy.js +0 -2
- package/client/dist/assets/TicketDetailModal-CFh7GOv5.js +0 -14
- package/client/dist/assets/agent-BXc59ARK2.js +0 -1
- package/client/dist/assets/agent-BbIFXdDq.js +0 -1
- package/client/dist/assets/agent-BqEfA5u5.js +0 -1
- package/client/dist/assets/agent-BtGzgTC4.js +0 -1
- package/client/dist/assets/agent-CIhys_5d.js +0 -1
- package/client/dist/assets/agent-CJPaqsVR.js +0 -1
- package/client/dist/assets/agent-n-hY4GuL.js +0 -1
- package/client/dist/assets/agent-tJ-efa3_2.js +0 -1
- package/client/dist/assets/i18n-cK5pPbj8.js +0 -2
- package/client/dist/assets/index-BT6xDQjB.css +0 -2
- package/client/dist/assets/index-CCq_kIrF.js +0 -76
- package/client/dist/assets/setup-CGZjy2IF2.js +0 -1
- package/client/dist/assets/setup-CXa0Sx16.js +0 -1
- package/client/dist/assets/setup-CmK87LuS.js +0 -1
- package/client/dist/assets/setup-D2ktKKMF2.js +0 -1
- package/client/dist/assets/setup-DG2H7ha5.js +0 -1
- package/client/dist/assets/setup-DK5bHDUq2.js +0 -1
- package/client/dist/assets/setup-DrWlIQTZ.js +0 -1
- package/client/dist/assets/setup-rzHUVgAu.js +0 -1
- /package/client/dist/assets/{activity-C6kJpm9q2.js → activity-C6kJpm9q.js} +0 -0
- /package/client/dist/assets/{activity-DAyekdlt2.js → activity-DAyekdlt.js} +0 -0
- /package/client/dist/assets/{addspec-DbNBbrha2.js → addspec-DbNBbrha.js} +0 -0
- /package/client/dist/assets/{addspec-htHGZHOe2.js → addspec-htHGZHOe.js} +0 -0
- /package/client/dist/assets/{agents-6ZOJXWVk2.js → agents-6ZOJXWVk.js} +0 -0
- /package/client/dist/assets/{agents-Bz-thHkB2.js → agents-Bz-thHkB.js} +0 -0
- /package/client/dist/assets/{agentstudio-BLGvxWBx2.js → agentstudio-BLGvxWBx.js} +0 -0
- /package/client/dist/assets/{agentstudio-D9UKiWTj2.js → agentstudio-D9UKiWTj.js} +0 -0
- /package/client/dist/assets/{aiedit-BoG24nlg2.js → aiedit-BoG24nlg.js} +0 -0
- /package/client/dist/assets/{aiedit-olnv3QSt2.js → aiedit-olnv3QSt.js} +0 -0
- /package/client/dist/assets/{analytics-CdsAPAwZ2.js → analytics-CdsAPAwZ.js} +0 -0
- /package/client/dist/assets/{analytics-IEsKZvpW2.js → analytics-IEsKZvpW.js} +0 -0
- /package/client/dist/assets/{attachments-D0wXXt9T2.js → attachments-D0wXXt9T.js} +0 -0
- /package/client/dist/assets/{attachments-V_WZAYXx2.js → attachments-V_WZAYXx.js} +0 -0
- /package/client/dist/assets/{browser-CyR1Kp3m2.js → browser-CyR1Kp3m.js} +0 -0
- /package/client/dist/assets/{browser-DwZljcRh2.js → browser-DwZljcRh.js} +0 -0
- /package/client/dist/assets/{chat-BR8oe53x2.js → chat-BR8oe53x.js} +0 -0
- /package/client/dist/assets/{chat-DZyu3ImY2.js → chat-DZyu3ImY.js} +0 -0
- /package/client/dist/assets/{code-D20ef9Cf2.js → code-D20ef9Cf.js} +0 -0
- /package/client/dist/assets/{code-geHmknB92.js → code-geHmknB9.js} +0 -0
- /package/client/dist/assets/{commands-BEwbAjtD2.js → commands-BEwbAjtD.js} +0 -0
- /package/client/dist/assets/{commands-DTQBF_q52.js → commands-DTQBF_q5.js} +0 -0
- /package/client/dist/assets/{common-BNbnv8cE2.js → common-BNbnv8cE.js} +0 -0
- /package/client/dist/assets/{common-DBtTzeYm2.js → common-DBtTzeYm.js} +0 -0
- /package/client/dist/assets/{dashboard-Cqmmbh3l2.js → dashboard-Cqmmbh3l.js} +0 -0
- /package/client/dist/assets/{dashboard-Dy2Um-US2.js → dashboard-Dy2Um-US.js} +0 -0
- /package/client/dist/assets/{explore-C0TjprJW2.js → explore-C0TjprJW.js} +0 -0
- /package/client/dist/assets/{explore-DsXAgnmJ2.js → explore-DsXAgnmJ.js} +0 -0
- /package/client/dist/assets/{integrations-CH0zy1Mw2.js → integrations-CH0zy1Mw.js} +0 -0
- /package/client/dist/assets/{integrations-b8pH-vfl2.js → integrations-b8pH-vfl.js} +0 -0
- /package/client/dist/assets/{jira-CNeYi06w2.js → jira-CNeYi06w.js} +0 -0
- /package/client/dist/assets/{jira-pRaL7JQo2.js → jira-pRaL7JQo.js} +0 -0
- /package/client/dist/assets/{jobs-B-sDHEay2.js → jobs-B-sDHEay.js} +0 -0
- /package/client/dist/assets/{jobs-DzSIR8V62.js → jobs-DzSIR8V6.js} +0 -0
- /package/client/dist/assets/{loops-Crc9It8o2.js → loops-Crc9It8o.js} +0 -0
- /package/client/dist/assets/{loops-DH2hZcbK2.js → loops-DH2hZcbK.js} +0 -0
- /package/client/dist/assets/{loops-DS5TmvIU2.js → loops-DS5TmvIU.js} +0 -0
- /package/client/dist/assets/{mcp-Czd_iyp02.js → mcp-Czd_iyp0.js} +0 -0
- /package/client/dist/assets/{mcp-D4ZjtIx32.js → mcp-D4ZjtIx3.js} +0 -0
- /package/client/dist/assets/{mcp-DIfBUv2D2.js → mcp-DIfBUv2D.js} +0 -0
- /package/client/dist/assets/{nav-BRJBxaGt2.js → nav-BRJBxaGt.js} +0 -0
- /package/client/dist/assets/{nav-CNskRJKQ2.js → nav-CNskRJKQ.js} +0 -0
- /package/client/dist/assets/{nav-TvumAFD22.js → nav-TvumAFD2.js} +0 -0
- /package/client/dist/assets/{settings-DAZqjX2a2.js → settings-DAZqjX2a.js} +0 -0
- /package/client/dist/assets/{settings-DRh39v1S2.js → settings-DRh39v1S.js} +0 -0
- /package/client/dist/assets/{settings-Mj1OJbfd2.js → settings-Mj1OJbfd.js} +0 -0
- /package/client/dist/assets/{specs-BcyxaCwf2.js → specs-BcyxaCwf.js} +0 -0
- /package/client/dist/assets/{specs-CLZKysq-2.js → specs-CLZKysq-.js} +0 -0
- /package/client/dist/assets/{specs-dV3Mq5Mu2.js → specs-dV3Mq5Mu.js} +0 -0
- /package/client/dist/assets/{terminal-C2NAd7BW2.js → terminal-C2NAd7BW.js} +0 -0
- /package/client/dist/assets/{terminal-IahE4LW92.js → terminal-IahE4LW9.js} +0 -0
- /package/client/dist/assets/{terminal-QpXZU2Bn2.js → terminal-QpXZU2Bn.js} +0 -0
- /package/client/dist/assets/{tickets-Bs37scwY2.js → tickets-Bs37scwY.js} +0 -0
- /package/client/dist/assets/{tickets-DQp7JS2R2.js → tickets-DQp7JS2R.js} +0 -0
- /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
|
|
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 →
|
|
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`.
|
|
111
|
-
//
|
|
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
|
|
156
|
+
let env;
|
|
114
157
|
let cwd;
|
|
115
158
|
if (mcpEnabled) {
|
|
116
|
-
// Match the Explore mcp-on spawn cwd
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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:
|
|
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
|
|
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;
|
package/server/dist/index.js
CHANGED
|
@@ -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
|
+
}
|
package/server/dist/pricing.js
CHANGED
|
@@ -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
|
});
|