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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Parses ```blueprint-draft fenced JSON blocks emitted during a Project
|
|
3
|
+
// Builder conversation (design D2). Unlike spec-draft (partial + merge), each
|
|
4
|
+
// blueprint-draft block is a FULL snapshot: the LAST syntactically valid
|
|
5
|
+
// block wins outright and there is no merging. Blocks are stripped from the
|
|
6
|
+
// chat content before it reaches the WS so the user never sees raw JSON.
|
|
7
|
+
//
|
|
8
|
+
// Streaming tail cut: the fence regex only matches CLOSED fences, so an
|
|
9
|
+
// unterminated trailing block is never parsed. `cutUnterminatedBlock` lets
|
|
10
|
+
// stream renderers hide the raw open fence while it is still arriving.
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.coerceBlueprint = coerceBlueprint;
|
|
13
|
+
exports.parseBlueprintDraftBlocks = parseBlueprintDraftBlocks;
|
|
14
|
+
exports.cutUnterminatedBlock = cutUnterminatedBlock;
|
|
15
|
+
const blueprint_types_1 = require("./blueprint-types");
|
|
16
|
+
const FENCE_RE = /```blueprint-draft\s*\n([\s\S]*?)\n\s*```/g;
|
|
17
|
+
const OPEN_FENCE_RE = /```blueprint-draft(?![\s\S]*?\n\s*```)/;
|
|
18
|
+
function asString(value) {
|
|
19
|
+
return typeof value === 'string' ? value : '';
|
|
20
|
+
}
|
|
21
|
+
function coerceStringArray(value) {
|
|
22
|
+
if (!Array.isArray(value))
|
|
23
|
+
return [];
|
|
24
|
+
return value.filter((item) => typeof item === 'string');
|
|
25
|
+
}
|
|
26
|
+
function coerceProduct(value) {
|
|
27
|
+
const obj = (value && typeof value === 'object' && !Array.isArray(value) ? value : {});
|
|
28
|
+
return { name: asString(obj.name), pitch: asString(obj.pitch), audience: asString(obj.audience) };
|
|
29
|
+
}
|
|
30
|
+
function coerceStack(value) {
|
|
31
|
+
const obj = (value && typeof value === 'object' && !Array.isArray(value) ? value : {});
|
|
32
|
+
const stack = {
|
|
33
|
+
language: asString(obj.language),
|
|
34
|
+
framework: asString(obj.framework),
|
|
35
|
+
db: asString(obj.db),
|
|
36
|
+
};
|
|
37
|
+
if (typeof obj.notes === 'string' && obj.notes !== '')
|
|
38
|
+
stack.notes = obj.notes;
|
|
39
|
+
return stack;
|
|
40
|
+
}
|
|
41
|
+
function coerceMilestones(value) {
|
|
42
|
+
if (!Array.isArray(value))
|
|
43
|
+
return [];
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const item of value) {
|
|
46
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
47
|
+
continue;
|
|
48
|
+
const obj = item;
|
|
49
|
+
const id = asString(obj.id);
|
|
50
|
+
const title = asString(obj.title);
|
|
51
|
+
if (id === '' && title === '')
|
|
52
|
+
continue;
|
|
53
|
+
const milestone = {
|
|
54
|
+
id,
|
|
55
|
+
title,
|
|
56
|
+
goal: asString(obj.goal),
|
|
57
|
+
status: (0, blueprint_types_1.isMilestoneStatus)(obj.status) ? obj.status : 'planned',
|
|
58
|
+
plannedSpecs: coerceStringArray(obj.plannedSpecs),
|
|
59
|
+
};
|
|
60
|
+
if (Array.isArray(obj.ticketIds)) {
|
|
61
|
+
const ids = obj.ticketIds.filter((n) => typeof n === 'number' && Number.isInteger(n));
|
|
62
|
+
if (ids.length > 0)
|
|
63
|
+
milestone.ticketIds = ids;
|
|
64
|
+
}
|
|
65
|
+
out.push(milestone);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function coerceM1Specs(value) {
|
|
70
|
+
if (!Array.isArray(value))
|
|
71
|
+
return [];
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const item of value) {
|
|
74
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
75
|
+
continue;
|
|
76
|
+
const obj = item;
|
|
77
|
+
const title = asString(obj.title);
|
|
78
|
+
if (title === '')
|
|
79
|
+
continue;
|
|
80
|
+
const spec = {
|
|
81
|
+
kind: (0, blueprint_types_1.isBlueprintSpecKind)(obj.kind) ? obj.kind : 'feature',
|
|
82
|
+
title,
|
|
83
|
+
shortSummary: asString(obj.shortSummary),
|
|
84
|
+
description: asString(obj.description),
|
|
85
|
+
acceptanceCriteria: coerceStringArray(obj.acceptanceCriteria),
|
|
86
|
+
priority: (0, blueprint_types_1.isBlueprintSpecPriority)(obj.priority) ? obj.priority : 'medium',
|
|
87
|
+
labels: coerceStringArray(obj.labels),
|
|
88
|
+
};
|
|
89
|
+
if (typeof obj.dependsOnIndex === 'number' && Number.isInteger(obj.dependsOnIndex) && obj.dependsOnIndex >= 0) {
|
|
90
|
+
spec.dependsOnIndex = obj.dependsOnIndex;
|
|
91
|
+
}
|
|
92
|
+
out.push(spec);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Validate + normalise one parsed JSON payload into a Blueprint snapshot.
|
|
98
|
+
* Unknown keys are dropped; missing sub-objects default to empty values. A
|
|
99
|
+
* missing or non-integer `blueprintVersion` rejects the block (spec gate).
|
|
100
|
+
*/
|
|
101
|
+
function coerceBlueprint(parsed) {
|
|
102
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
103
|
+
return null;
|
|
104
|
+
const obj = parsed;
|
|
105
|
+
if (typeof obj.blueprintVersion !== 'number' || !Number.isInteger(obj.blueprintVersion))
|
|
106
|
+
return null;
|
|
107
|
+
return {
|
|
108
|
+
blueprintVersion: obj.blueprintVersion,
|
|
109
|
+
product: coerceProduct(obj.product),
|
|
110
|
+
coreFlow: asString(obj.coreFlow),
|
|
111
|
+
platform: asString(obj.platform),
|
|
112
|
+
stack: coerceStack(obj.stack),
|
|
113
|
+
assumptions: coerceStringArray(obj.assumptions),
|
|
114
|
+
milestones: coerceMilestones(obj.milestones),
|
|
115
|
+
specsComplete: obj.specsComplete === true,
|
|
116
|
+
m1Specs: coerceM1Specs(obj.m1Specs),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Scan `text` for `blueprint-draft` fenced blocks. Returns the text with
|
|
121
|
+
* every block (valid or malformed) stripped, plus the LAST valid snapshot.
|
|
122
|
+
* Never throws.
|
|
123
|
+
*/
|
|
124
|
+
function parseBlueprintDraftBlocks(text) {
|
|
125
|
+
if (!text || !text.includes('```blueprint-draft')) {
|
|
126
|
+
return { stripped: text ?? '', blueprint: null, rawBlueprint: null, hadBlocks: false };
|
|
127
|
+
}
|
|
128
|
+
let blueprint = null;
|
|
129
|
+
let rawBlueprint = null;
|
|
130
|
+
let hadBlocks = false;
|
|
131
|
+
let stripped = '';
|
|
132
|
+
let cursor = 0;
|
|
133
|
+
FENCE_RE.lastIndex = 0;
|
|
134
|
+
let match;
|
|
135
|
+
while ((match = FENCE_RE.exec(text)) !== null) {
|
|
136
|
+
hadBlocks = true;
|
|
137
|
+
stripped += text.slice(cursor, match.index);
|
|
138
|
+
cursor = match.index + match[0].length;
|
|
139
|
+
let parsed;
|
|
140
|
+
try {
|
|
141
|
+
parsed = JSON.parse(match[1]);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
continue; // malformed; drop silently, keep the previous valid snapshot
|
|
145
|
+
}
|
|
146
|
+
const candidate = coerceBlueprint(parsed);
|
|
147
|
+
if (candidate) {
|
|
148
|
+
blueprint = candidate;
|
|
149
|
+
rawBlueprint = parsed;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
stripped += text.slice(cursor);
|
|
153
|
+
return { stripped, blueprint, rawBlueprint, hadBlocks };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* For live stream rendering: cut an UNTERMINATED trailing `blueprint-draft`
|
|
157
|
+
* fence (and everything after it) so raw JSON never flashes on screen while
|
|
158
|
+
* the block is still arriving. Complete blocks are untouched — run the full
|
|
159
|
+
* parser on the settled text instead.
|
|
160
|
+
*/
|
|
161
|
+
function cutUnterminatedBlock(text) {
|
|
162
|
+
if (!text || !text.includes('```blueprint-draft'))
|
|
163
|
+
return text ?? '';
|
|
164
|
+
const match = OPEN_FENCE_RE.exec(text);
|
|
165
|
+
if (!match)
|
|
166
|
+
return text;
|
|
167
|
+
return text.slice(0, match.index);
|
|
168
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Single source of truth for the Project Builder day-0 prompts ───────────
|
|
3
|
+
// BUILDER_INSTRUCTIONS is written to CLAUDE.md/AGENTS.md/GEMINI.md in
|
|
4
|
+
// ~/.specrails/builder-cwd/ (the channel that reaches every provider).
|
|
5
|
+
// BUILDER_SYSTEM_PROMPT is the compact equivalent passed via --system-prompt
|
|
6
|
+
// to providers that support a dedicated system-prompt argument.
|
|
7
|
+
//
|
|
8
|
+
// BYTE-STABILITY CONTRACT: both constants must stay static (no timestamps, no
|
|
9
|
+
// interpolation, no live data) so provider prompt caching can work on turn 2+.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.BUILDER_SYSTEM_PROMPT = exports.BUILDER_INSTRUCTIONS = void 0;
|
|
12
|
+
exports.BUILDER_INSTRUCTIONS = `# Specrails Project Builder
|
|
13
|
+
|
|
14
|
+
You are the Specrails Project Builder: a conversational product architect in
|
|
15
|
+
Specrails Desktop. Help the user turn a raw idea into an approved project
|
|
16
|
+
blueprint and then a rich Milestone-1 backlog. No project or repository exists
|
|
17
|
+
yet: you cannot inspect code, claim that paths exist, or create anything on
|
|
18
|
+
disk. The app performs the commit after the user reviews the result.
|
|
19
|
+
|
|
20
|
+
Conversational prose follows the user's language. Every generated spec field
|
|
21
|
+
(\`title\`, \`shortSummary\`, \`description\`, \`acceptanceCriteria\`, and
|
|
22
|
+
\`labels\`) is written in English.
|
|
23
|
+
|
|
24
|
+
## The blueprint-draft protocol
|
|
25
|
+
|
|
26
|
+
Communicate blueprint state exclusively through fenced \`blueprint-draft\` JSON
|
|
27
|
+
blocks. Rules:
|
|
28
|
+
|
|
29
|
+
- Every block is a FULL snapshot of the whole blueprint, never a delta. The
|
|
30
|
+
app uses the last valid block, so re-emit everything currently known.
|
|
31
|
+
- Emit at most one block per message and put it at the END of the message.
|
|
32
|
+
- Use valid JSON: double quotes, no comments, and no trailing commas.
|
|
33
|
+
- All keys below are required except \`stack.notes\` and \`dependsOnIndex\`:
|
|
34
|
+
|
|
35
|
+
\`\`\`json
|
|
36
|
+
{
|
|
37
|
+
"blueprintVersion": 1,
|
|
38
|
+
"product": { "name": "...", "pitch": "...", "audience": "..." },
|
|
39
|
+
"coreFlow": "one-sentence primary user journey",
|
|
40
|
+
"platform": "web | mobile | desktop | cli | api",
|
|
41
|
+
"stack": { "language": "...", "framework": "...", "db": "...", "notes": "optional" },
|
|
42
|
+
"assumptions": ["every default chosen for the user"],
|
|
43
|
+
"milestones": [
|
|
44
|
+
{ "id": "m1", "title": "...", "goal": "...", "status": "planned", "plannedSpecs": [] },
|
|
45
|
+
{ "id": "m2", "title": "...", "goal": "...", "status": "planned", "plannedSpecs": ["English title"] }
|
|
46
|
+
],
|
|
47
|
+
"specsComplete": false,
|
|
48
|
+
"m1Specs": [
|
|
49
|
+
{
|
|
50
|
+
"kind": "scaffold",
|
|
51
|
+
"title": "Scaffold the runnable project foundation",
|
|
52
|
+
"shortSummary": "Initialize a reproducible foundation that every later walking-skeleton slice can build on.",
|
|
53
|
+
"description": "## Problem Statement\\nThe approved project has a README and selected stack, but contributors cannot yet install, run, or verify a working application.\\n\\n## Proposed Solution\\nInitialize the selected stack with deterministic development, build, test, and CI commands while preserving the existing README.\\n\\n## Out of Scope\\n- Product features beyond a minimal runnable shell\\n- Production deployment and environment provisioning\\n\\n## Technical Considerations\\n- Pin compatible runtime and dependency versions for reproducible setup\\n- Keep the first application and test boundaries minimal but executable\\n\\n## Estimated Complexity\\nMedium - the foundation must align local development, automated tests, and CI without implementing product behavior.",
|
|
54
|
+
"acceptanceCriteria": ["A clean checkout installs successfully with the documented command.", "The development command starts the minimal application without an error.", "The automated test command passes from a clean checkout.", "CI executes the build and test commands on every proposed change."],
|
|
55
|
+
"priority": "high",
|
|
56
|
+
"labels": ["M1", "foundation"]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
62
|
+
## Interview and approval gate
|
|
63
|
+
|
|
64
|
+
- Ask questions for at most three assistant turns. After that, propose concrete
|
|
65
|
+
choices with short rationales and invite correction instead of interrogating.
|
|
66
|
+
- Before approval, fill product, core flow, platform, stack, assumptions, and
|
|
67
|
+
milestones, but ALWAYS emit \`m1Specs: []\` and \`specsComplete: false\`.
|
|
68
|
+
- "Surprise me" follows the same rule: decide all five blueprint dimensions in
|
|
69
|
+
one turn, record every default in \`assumptions\`, keep \`m1Specs\` empty, and
|
|
70
|
+
ask the user to approve or correct the blueprint.
|
|
71
|
+
- Generate detailed M1 specs only after explicit approval or a direct request
|
|
72
|
+
to generate the backlog/specs. Approval of the blueprint precedes generation;
|
|
73
|
+
do not create shallow placeholder specs during the interview.
|
|
74
|
+
- Keep prose brief because the adjacent panel renders the full snapshot.
|
|
75
|
+
|
|
76
|
+
## Milestone plan
|
|
77
|
+
|
|
78
|
+
- Milestone 1 is a walking skeleton: 5–10 detailed specs that together produce
|
|
79
|
+
the thinnest runnable, testable end-to-end version of the core flow.
|
|
80
|
+
- Milestones 2+ contain only shallow English \`plannedSpecs\` titles at day 0.
|
|
81
|
+
Never generate their descriptions or detailed payloads before that milestone
|
|
82
|
+
is opened against the real repository.
|
|
83
|
+
- If an idea is beyond the walking skeleton, park it as an M2+ title rather
|
|
84
|
+
than expanding M1 into a waterfall.
|
|
85
|
+
|
|
86
|
+
## Canonical detailed-spec contract
|
|
87
|
+
|
|
88
|
+
Every detailed M1 spec must satisfy all of these rules:
|
|
89
|
+
|
|
90
|
+
- \`kind\` is exactly \`scaffold\`, \`feature\`, or \`verification\`.
|
|
91
|
+
- \`title\` is concise, action-oriented, unique in the batch, and in English.
|
|
92
|
+
- \`shortSummary\` is one useful English sentence, no more than 240 characters.
|
|
93
|
+
- \`description\` is English markdown with exactly these five \`##\` headings,
|
|
94
|
+
once each and in this order:
|
|
95
|
+
1. \`## Problem Statement\` — who has the problem, when, why it matters, and
|
|
96
|
+
the user or product value of solving it.
|
|
97
|
+
2. \`## Proposed Solution\` — the concrete behavior and planned components or
|
|
98
|
+
contracts that deliver the thin outcome.
|
|
99
|
+
3. \`## Out of Scope\` — at least two honest bullets naming adjacent work that
|
|
100
|
+
this spec deliberately defers.
|
|
101
|
+
4. \`## Technical Considerations\` — at least two bullets covering the selected stack,
|
|
102
|
+
planned components and interfaces, risks, invariants, testing strategy,
|
|
103
|
+
and dependencies on earlier specs where relevant.
|
|
104
|
+
5. \`## Estimated Complexity\` — Low, Medium, High, or Very High plus one
|
|
105
|
+
sentence explaining what drives the estimate.
|
|
106
|
+
- Do NOT put an \`## Acceptance Criteria\` heading in \`description\`. Criteria
|
|
107
|
+
live only in the separate \`acceptanceCriteria\` array; the app folds them
|
|
108
|
+
into the ticket deterministically at commit time.
|
|
109
|
+
- \`acceptanceCriteria\` contains 4–10 non-empty, independent, testable outcomes,
|
|
110
|
+
not implementation steps. Cover the intended behavior, observable failure or
|
|
111
|
+
edge cases, and relevant automated verification.
|
|
112
|
+
- \`priority\` is exactly \`low\`, \`medium\`, \`high\`, or \`critical\` and
|
|
113
|
+
reflects delivery urgency/risk rather than implementation complexity.
|
|
114
|
+
- \`labels\` includes \`M1\` plus at least one concise domain label such as
|
|
115
|
+
\`frontend\`, \`api\`, \`data\`, \`auth\`, \`testing\`, or \`infra\`.
|
|
116
|
+
- \`m1Specs[0]\` is \`kind: "scaffold"\` and omits \`dependsOnIndex\`. It
|
|
117
|
+
states that the repository already contains a README and defines concrete
|
|
118
|
+
install, run, test, and CI outcomes for the selected stack.
|
|
119
|
+
- A later spec may use \`dependsOnIndex\` only when it truly depends on another
|
|
120
|
+
item, and the integer must point strictly backward to an earlier array index.
|
|
121
|
+
Never point forward or to the same spec.
|
|
122
|
+
|
|
123
|
+
## Day-0 grounding
|
|
124
|
+
|
|
125
|
+
There is no codebase to inspect. Ground technical details in the selected
|
|
126
|
+
stack, planned components, intended contracts/data shapes, known risks, and
|
|
127
|
+
inter-spec dependencies. Never invent or imply existing repository paths,
|
|
128
|
+
filenames, modules, functions, identifiers, migrations, or test files.
|
|
129
|
+
|
|
130
|
+
## Generation and completion
|
|
131
|
+
|
|
132
|
+
- After approval, generate the complete 5–10-spec walking skeleton in one
|
|
133
|
+
response and one FULL snapshot. Never expose a partial detailed-spec batch as
|
|
134
|
+
a result the user must manually ask you to continue.
|
|
135
|
+
- If the complete batch cannot fit or cannot pass validation, keep
|
|
136
|
+
\`m1Specs: []\` and \`specsComplete: false\`, explain what prevented completion,
|
|
137
|
+
and wait for a fresh generation request. Do not emit partially generated specs.
|
|
138
|
+
- Before setting \`specsComplete: true\`, self-audit the entire snapshot: schema
|
|
139
|
+
fields, exact five headings and order, English content, 4–10 criteria, valid
|
|
140
|
+
priority, M1 plus domain labels, unique titles, scaffold first, and strictly
|
|
141
|
+
backward dependencies. Set it true only if every spec passes. If anything
|
|
142
|
+
fails, keep it false and repair the snapshot first.
|
|
143
|
+
|
|
144
|
+
## Boundaries
|
|
145
|
+
|
|
146
|
+
- Never claim to have created files, repositories, tickets, or projects.
|
|
147
|
+
- Never include secrets, real API keys, or license-restricted code in specs.
|
|
148
|
+
`;
|
|
149
|
+
exports.BUILDER_SYSTEM_PROMPT = `You are the Specrails Project Builder. No repository exists yet. Converse in the user's language, but write every spec field in English. Emit at most one final fenced blueprint-draft JSON FULL snapshot per message. Before explicit blueprint approval or a direct request to generate specs, populate product/coreFlow/platform/stack/assumptions/milestones but ALWAYS keep m1Specs: [] and specsComplete: false; "surprise me" also proposes dimensions only and asks for approval. After approval generate the complete 5-10-spec M1 walking skeleton in one response and one FULL snapshot; never expose a partial batch the user must ask you to continue. If the complete batch cannot be produced and audited, emit m1Specs: [] with specsComplete: false instead of partial specs. Each spec has kind (scaffold|feature|verification), unique action-oriented title, one-sentence shortSummary <=240 characters, description with exactly these headings once and in order: ## Problem Statement, ## Proposed Solution, ## Out of Scope, ## Technical Considerations, ## Estimated Complexity; Out of Scope and Technical Considerations each contain at least two bullets; a separate acceptanceCriteria array of 4-10 independent testable outcomes (never an ## Acceptance Criteria section in description); priority low|medium|high|critical; labels including M1 and a domain label; and optional dependsOnIndex pointing strictly backward. m1Specs[0] is kind scaffold, omits dependsOnIndex, notes the repo already contains a README, and defines install/run/test/CI outcomes. Ground day-0 content in the selected stack, planned components/contracts, risks, tests, and spec dependencies; NEVER invent paths or existing identifiers. M2+ keeps title-only plannedSpecs. Set specsComplete true only after auditing all fields, five headings, English content, 4-10 criteria, labels, scaffold, unique titles, and dependencies. Never claim disk changes.`;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Deterministic rendering + persistence of the Blueprint pair (design D5).
|
|
3
|
+
//
|
|
4
|
+
// `blueprint.json` is the source of truth; `blueprint.md` is a deterministic
|
|
5
|
+
// render regenerated on EVERY json write and never hand-edited. Both live in
|
|
6
|
+
// `<workspace>/.specrails/` — never in the project repo (pristine-repo
|
|
7
|
+
// invariant). Writes are temp+rename so a crash never leaves a torn file.
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.BLUEPRINT_MD = exports.BLUEPRINT_JSON = void 0;
|
|
13
|
+
exports.renderBlueprintMarkdown = renderBlueprintMarkdown;
|
|
14
|
+
exports.blueprintJsonPath = blueprintJsonPath;
|
|
15
|
+
exports.writeBlueprintPair = writeBlueprintPair;
|
|
16
|
+
exports.readBlueprint = readBlueprint;
|
|
17
|
+
const fs_1 = __importDefault(require("fs"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
19
|
+
const blueprint_draft_parser_1 = require("./blueprint-draft-parser");
|
|
20
|
+
exports.BLUEPRINT_JSON = 'blueprint.json';
|
|
21
|
+
exports.BLUEPRINT_MD = 'blueprint.md';
|
|
22
|
+
function renderMilestone(m, index) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
lines.push(`### M${index + 1} — ${m.title}`);
|
|
25
|
+
lines.push('');
|
|
26
|
+
lines.push(`- **Status**: ${m.status}`);
|
|
27
|
+
if (m.goal !== '')
|
|
28
|
+
lines.push(`- **Goal**: ${m.goal}`);
|
|
29
|
+
if (m.plannedSpecs.length > 0) {
|
|
30
|
+
lines.push('- **Planned specs**:');
|
|
31
|
+
for (const spec of m.plannedSpecs)
|
|
32
|
+
lines.push(` - ${spec}`);
|
|
33
|
+
}
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Deterministic markdown render of a blueprint: same input bytes → same
|
|
38
|
+
* output bytes (no timestamps, no ordering surprises — arrays render in
|
|
39
|
+
* stored order).
|
|
40
|
+
*/
|
|
41
|
+
function renderBlueprintMarkdown(blueprint) {
|
|
42
|
+
const sections = [];
|
|
43
|
+
const name = blueprint.product.name !== '' ? blueprint.product.name : 'Untitled project';
|
|
44
|
+
sections.push(`# Blueprint — ${name}`);
|
|
45
|
+
const product = ['## Product', ''];
|
|
46
|
+
product.push(`- **Pitch**: ${blueprint.product.pitch !== '' ? blueprint.product.pitch : '_N/A_'}`);
|
|
47
|
+
product.push(`- **Audience**: ${blueprint.product.audience !== '' ? blueprint.product.audience : '_N/A_'}`);
|
|
48
|
+
product.push(`- **Core flow**: ${blueprint.coreFlow !== '' ? blueprint.coreFlow : '_N/A_'}`);
|
|
49
|
+
product.push(`- **Platform**: ${blueprint.platform !== '' ? blueprint.platform : '_N/A_'}`);
|
|
50
|
+
sections.push(product.join('\n'));
|
|
51
|
+
const stack = ['## Stack', ''];
|
|
52
|
+
stack.push(`- **Language**: ${blueprint.stack.language !== '' ? blueprint.stack.language : '_N/A_'}`);
|
|
53
|
+
stack.push(`- **Framework**: ${blueprint.stack.framework !== '' ? blueprint.stack.framework : '_N/A_'}`);
|
|
54
|
+
stack.push(`- **Database**: ${blueprint.stack.db !== '' ? blueprint.stack.db : '_N/A_'}`);
|
|
55
|
+
if (blueprint.stack.notes)
|
|
56
|
+
stack.push(`- **Notes**: ${blueprint.stack.notes}`);
|
|
57
|
+
sections.push(stack.join('\n'));
|
|
58
|
+
if (blueprint.assumptions.length > 0) {
|
|
59
|
+
sections.push(['## Assumptions', '', ...blueprint.assumptions.map((a) => `- ${a}`)].join('\n'));
|
|
60
|
+
}
|
|
61
|
+
if (blueprint.milestones.length > 0) {
|
|
62
|
+
sections.push(['## Milestones', '', blueprint.milestones.map(renderMilestone).join('\n\n')].join('\n'));
|
|
63
|
+
}
|
|
64
|
+
if (blueprint.m1Specs.length > 0) {
|
|
65
|
+
const specs = ['## Milestone 1 specs', ''];
|
|
66
|
+
blueprint.m1Specs.forEach((spec, i) => {
|
|
67
|
+
specs.push(`${i + 1}. **${spec.title}**${spec.labels.length > 0 ? ` (${spec.labels.join(', ')})` : ''}`);
|
|
68
|
+
});
|
|
69
|
+
sections.push(specs.join('\n'));
|
|
70
|
+
}
|
|
71
|
+
sections.push('---\n\n_Generated by Specrails Project Builder. Do not edit — regenerated from `blueprint.json`._');
|
|
72
|
+
return sections.join('\n\n') + '\n';
|
|
73
|
+
}
|
|
74
|
+
function writeAtomic(filePath, content) {
|
|
75
|
+
const tmp = `${filePath}.tmp`;
|
|
76
|
+
fs_1.default.writeFileSync(tmp, content, 'utf-8');
|
|
77
|
+
fs_1.default.renameSync(tmp, filePath);
|
|
78
|
+
}
|
|
79
|
+
/** Resolve the blueprint.json path inside a workspace. */
|
|
80
|
+
function blueprintJsonPath(workspaceDir) {
|
|
81
|
+
return path_1.default.join(workspaceDir, '.specrails', exports.BLUEPRINT_JSON);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Write the blueprint pair (json + regenerated md) into
|
|
85
|
+
* `<workspaceDir>/.specrails/`. Atomic per file (temp+rename).
|
|
86
|
+
*/
|
|
87
|
+
function writeBlueprintPair(workspaceDir, blueprint) {
|
|
88
|
+
const dir = path_1.default.join(workspaceDir, '.specrails');
|
|
89
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
90
|
+
writeAtomic(path_1.default.join(dir, exports.BLUEPRINT_JSON), JSON.stringify(blueprint, null, 2) + '\n');
|
|
91
|
+
writeAtomic(path_1.default.join(dir, exports.BLUEPRINT_MD), renderBlueprintMarkdown(blueprint));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Read a workspace's blueprint.json. Returns null when absent or unreadable
|
|
95
|
+
* (a torn/corrupt file must never crash a route).
|
|
96
|
+
*/
|
|
97
|
+
function readBlueprint(workspaceDir) {
|
|
98
|
+
try {
|
|
99
|
+
const raw = fs_1.default.readFileSync(blueprintJsonPath(workspaceDir), 'utf-8');
|
|
100
|
+
return (0, blueprint_draft_parser_1.coerceBlueprint)(JSON.parse(raw));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isProjectBuilderEnabled = isProjectBuilderEnabled;
|
|
4
|
+
exports.createBlueprintRouter = createBlueprintRouter;
|
|
5
|
+
const express_1 = require("express");
|
|
6
|
+
const providers_1 = require("./providers");
|
|
7
|
+
const blueprint_store_1 = require("./blueprint-store");
|
|
8
|
+
function isProjectBuilderEnabled() {
|
|
9
|
+
return process.env.SPECRAILS_PROJECT_BUILDER !== 'false';
|
|
10
|
+
}
|
|
11
|
+
function validProvider(provider) {
|
|
12
|
+
if (typeof provider !== 'string' || !provider)
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
(0, providers_1.getAdapter)(provider);
|
|
16
|
+
return provider;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function createBlueprintRouter(deps) {
|
|
23
|
+
const { manager, desktopDb, runCommit } = deps;
|
|
24
|
+
const router = (0, express_1.Router)();
|
|
25
|
+
router.use((_req, res, next) => {
|
|
26
|
+
if (!isProjectBuilderEnabled()) {
|
|
27
|
+
res.status(404).json({ error: 'project builder is disabled' });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
next();
|
|
31
|
+
});
|
|
32
|
+
router.get('/models', (req, res) => {
|
|
33
|
+
const provider = validProvider(req.query.provider) ?? 'claude';
|
|
34
|
+
const adapter = (0, providers_1.getAdapter)(provider);
|
|
35
|
+
res.json({
|
|
36
|
+
provider,
|
|
37
|
+
models: adapter.modelCatalog(),
|
|
38
|
+
defaultModel: adapter.defaultModel(),
|
|
39
|
+
efforts: adapter.capabilities.reasoningEfforts ?? [],
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
router.get('/conversations', (_req, res) => {
|
|
43
|
+
res.json({ conversations: (0, blueprint_store_1.listBlueprintConversations)(desktopDb) });
|
|
44
|
+
});
|
|
45
|
+
router.post('/conversations', (req, res) => {
|
|
46
|
+
const body = (req.body ?? {});
|
|
47
|
+
const provider = validProvider(body.provider) ?? 'claude';
|
|
48
|
+
const model = typeof body.model === 'string' && body.model ? body.model : null;
|
|
49
|
+
const conversation = (0, blueprint_store_1.createBlueprintConversation)(desktopDb, { provider, model });
|
|
50
|
+
res.status(201).json({ conversation });
|
|
51
|
+
});
|
|
52
|
+
router.get('/conversations/:id', (req, res) => {
|
|
53
|
+
const conversation = (0, blueprint_store_1.getBlueprintConversation)(desktopDb, String(req.params.id));
|
|
54
|
+
if (!conversation) {
|
|
55
|
+
res.status(404).json({ error: 'conversation not found' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
res.json({ conversation, messages: (0, blueprint_store_1.listBlueprintMessages)(desktopDb, conversation.id) });
|
|
59
|
+
});
|
|
60
|
+
router.patch('/conversations/:id', (req, res) => {
|
|
61
|
+
const conversation = (0, blueprint_store_1.getBlueprintConversation)(desktopDb, String(req.params.id));
|
|
62
|
+
if (!conversation) {
|
|
63
|
+
res.status(404).json({ error: 'conversation not found' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const body = (req.body ?? {});
|
|
67
|
+
const patch = {};
|
|
68
|
+
if (body.provider !== undefined) {
|
|
69
|
+
const provider = validProvider(body.provider);
|
|
70
|
+
if (!provider) {
|
|
71
|
+
res.status(400).json({ error: 'invalid provider' });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
patch.provider = provider;
|
|
75
|
+
if (provider !== conversation.provider) {
|
|
76
|
+
// A session id is provider-local; a switch must reset it and the model.
|
|
77
|
+
patch.session_id = null;
|
|
78
|
+
patch.model = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (body.model !== undefined) {
|
|
82
|
+
if (body.model !== null && typeof body.model !== 'string') {
|
|
83
|
+
res.status(400).json({ error: 'invalid model' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const effectiveProvider = patch.provider ?? conversation.provider;
|
|
87
|
+
if (typeof body.model === 'string' && body.model) {
|
|
88
|
+
const valid = new Set((0, providers_1.getAdapter)(effectiveProvider).modelCatalog().map((m) => m.value));
|
|
89
|
+
if (!valid.has(body.model)) {
|
|
90
|
+
res.status(400).json({ error: 'model not in provider catalog' });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
patch.model = body.model;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
patch.model = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (body.title !== undefined) {
|
|
100
|
+
if (body.title !== null && typeof body.title !== 'string') {
|
|
101
|
+
res.status(400).json({ error: 'invalid title' });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
patch.title = typeof body.title === 'string' ? body.title.slice(0, 200) : null;
|
|
105
|
+
}
|
|
106
|
+
const updated = (0, blueprint_store_1.updateBlueprintConversation)(desktopDb, conversation.id, patch);
|
|
107
|
+
res.json({ conversation: updated });
|
|
108
|
+
});
|
|
109
|
+
router.delete('/conversations/:id', (req, res) => {
|
|
110
|
+
const conversation = (0, blueprint_store_1.getBlueprintConversation)(desktopDb, String(req.params.id));
|
|
111
|
+
if (!conversation) {
|
|
112
|
+
res.status(404).json({ error: 'conversation not found' });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
manager.abort(conversation.id);
|
|
116
|
+
(0, blueprint_store_1.deleteBlueprintConversation)(desktopDb, conversation.id);
|
|
117
|
+
res.json({ ok: true });
|
|
118
|
+
});
|
|
119
|
+
router.post('/conversations/:id/send', (req, res) => {
|
|
120
|
+
const conversation = (0, blueprint_store_1.getBlueprintConversation)(desktopDb, String(req.params.id));
|
|
121
|
+
if (!conversation) {
|
|
122
|
+
res.status(404).json({ error: 'conversation not found' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const body = (req.body ?? {});
|
|
126
|
+
const text = typeof body.text === 'string' ? body.text.trim() : '';
|
|
127
|
+
if (!text) {
|
|
128
|
+
res.status(400).json({ error: 'text is required' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (manager.isStreaming(conversation.id)) {
|
|
132
|
+
res.status(409).json({ error: 'a turn is already streaming' });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const model = typeof body.model === 'string' && body.model ? body.model : undefined;
|
|
136
|
+
let reasoningEffort;
|
|
137
|
+
if (body.reasoning_effort !== undefined) {
|
|
138
|
+
const allowedEfforts = (0, providers_1.getAdapter)(conversation.provider).capabilities.reasoningEfforts ?? [];
|
|
139
|
+
if (typeof body.reasoning_effort !== 'string'
|
|
140
|
+
|| !allowedEfforts.includes(body.reasoning_effort)) {
|
|
141
|
+
res.status(400).json({ error: 'reasoning effort not in provider catalog' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
reasoningEffort = body.reasoning_effort;
|
|
145
|
+
}
|
|
146
|
+
void manager.sendMessage(conversation.id, text, { model, reasoningEffort });
|
|
147
|
+
res.status(202).json({ accepted: true });
|
|
148
|
+
});
|
|
149
|
+
router.post('/conversations/:id/abort', (req, res) => {
|
|
150
|
+
const conversation = (0, blueprint_store_1.getBlueprintConversation)(desktopDb, String(req.params.id));
|
|
151
|
+
if (!conversation) {
|
|
152
|
+
res.status(404).json({ error: 'conversation not found' });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
manager.abort(conversation.id);
|
|
156
|
+
res.json({ ok: true });
|
|
157
|
+
});
|
|
158
|
+
// Orchestrated commit (add-project-builder D3): validates synchronously,
|
|
159
|
+
// then 202 + streamed `blueprint.commit_progress` events. Delegation to
|
|
160
|
+
// `executeBlueprintCommit` keeps this handler thin and the orchestrator
|
|
161
|
+
// unit-testable via its DI bag.
|
|
162
|
+
router.post('/commit', (req, res) => {
|
|
163
|
+
const input = (req.body ?? {});
|
|
164
|
+
const validation = runCommit.validate(input);
|
|
165
|
+
if (!validation.ok) {
|
|
166
|
+
res.status(400).json({ error: validation.error, detail: validation.detail });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const commitId = runCommit.start(input);
|
|
170
|
+
res.status(202).json({ accepted: true, commitId });
|
|
171
|
+
});
|
|
172
|
+
return router;
|
|
173
|
+
}
|