@smartmemory/compose 0.1.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 (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1014 -0
  3. package/bin/compose.js +1515 -0
  4. package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
  5. package/dist/assets/arc-SxJ2J1sh.js +1 -0
  6. package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
  7. package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
  8. package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
  9. package/dist/assets/channel-DGElom1e.js +1 -0
  10. package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
  11. package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
  12. package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
  13. package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
  14. package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
  15. package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
  16. package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
  17. package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
  18. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
  19. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
  20. package/dist/assets/clone-DUJKJXd7.js +1 -0
  21. package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
  22. package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
  23. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  24. package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
  25. package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
  26. package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
  27. package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
  28. package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
  29. package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
  30. package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
  31. package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
  32. package/dist/assets/graph-D0Cfv00Y.js +1 -0
  33. package/dist/assets/index-CUd6pFGF.css +1 -0
  34. package/dist/assets/index-DReRlzZI.js +1144 -0
  35. package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
  36. package/dist/assets/init-Gi6I4Gst.js +1 -0
  37. package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
  38. package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
  39. package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
  40. package/dist/assets/katex-DkKDou_j.js +257 -0
  41. package/dist/assets/layout-Bj72wOEB.js +1 -0
  42. package/dist/assets/linear-BRFo114D.js +1 -0
  43. package/dist/assets/min-GCHnKlJS.js +1 -0
  44. package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
  45. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  46. package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
  47. package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
  48. package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
  49. package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
  50. package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
  51. package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
  52. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
  53. package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
  54. package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
  55. package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
  56. package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
  57. package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
  58. package/dist/index.html +30 -0
  59. package/lib/agent-chains.js +65 -0
  60. package/lib/agent-string.js +86 -0
  61. package/lib/budget-ledger.js +86 -0
  62. package/lib/build-all.js +162 -0
  63. package/lib/build-dag.js +120 -0
  64. package/lib/build-stream-writer.js +190 -0
  65. package/lib/build.js +2997 -0
  66. package/lib/capability-checker.js +53 -0
  67. package/lib/cert-inject.js +38 -0
  68. package/lib/cli-progress.js +483 -0
  69. package/lib/constants.js +69 -0
  70. package/lib/cross-layer-audit.js +84 -0
  71. package/lib/debug-discipline.js +173 -0
  72. package/lib/feature-json.js +106 -0
  73. package/lib/gate-prompt.js +291 -0
  74. package/lib/gate-tiers.js +194 -0
  75. package/lib/health-history.js +119 -0
  76. package/lib/health-score.js +227 -0
  77. package/lib/ideabox.js +570 -0
  78. package/lib/import.js +244 -0
  79. package/lib/migrate-roadmap.js +94 -0
  80. package/lib/model-pricing.js +67 -0
  81. package/lib/new.js +413 -0
  82. package/lib/pipeline-cli.js +489 -0
  83. package/lib/plan-parser.js +103 -0
  84. package/lib/qa-scoping.js +474 -0
  85. package/lib/questionnaire.js +200 -0
  86. package/lib/resolve-port.js +7 -0
  87. package/lib/result-normalizer.js +349 -0
  88. package/lib/review-lenses.js +166 -0
  89. package/lib/roadmap-gen.js +210 -0
  90. package/lib/roadmap-parser.js +176 -0
  91. package/lib/server-probe.js +23 -0
  92. package/lib/staleness.js +87 -0
  93. package/lib/step-prompt.js +260 -0
  94. package/lib/step-validator.js +49 -0
  95. package/lib/stratum-mcp-client.js +365 -0
  96. package/lib/team-flag.js +46 -0
  97. package/lib/test-bootstrap.js +401 -0
  98. package/lib/triage.js +274 -0
  99. package/lib/vision-writer.js +391 -0
  100. package/package.json +111 -0
  101. package/pipelines/bug-fix.stratum.yaml +230 -0
  102. package/pipelines/build.stratum.yaml +498 -0
  103. package/pipelines/content.stratum.yaml +112 -0
  104. package/pipelines/coverage-sweep.stratum.yaml +52 -0
  105. package/pipelines/refactor.stratum.yaml +169 -0
  106. package/pipelines/research.stratum.yaml +88 -0
  107. package/pipelines/review-fix.stratum.yaml +109 -0
  108. package/presets/team-feature.stratum.yaml +105 -0
  109. package/presets/team-research.stratum.yaml +108 -0
  110. package/presets/team-review.stratum.yaml +106 -0
  111. package/scripts/agent-activity-hook.sh +31 -0
  112. package/scripts/agent-error-hook.sh +28 -0
  113. package/scripts/analyze-orphans.mjs +50 -0
  114. package/scripts/find-orphans.mjs +26 -0
  115. package/scripts/fix-phases.mjs +49 -0
  116. package/scripts/generate-stratum-spec.mjs +137 -0
  117. package/scripts/import-roadmap.mjs +116 -0
  118. package/scripts/phase-audit.mjs +33 -0
  119. package/scripts/run-pipeline.mjs +314 -0
  120. package/scripts/session-end-hook.sh +18 -0
  121. package/scripts/session-start-hook.sh +38 -0
  122. package/scripts/vision-hook.sh +104 -0
  123. package/scripts/vision-track.mjs +554 -0
  124. package/scripts/wire-all-orphans.mjs +108 -0
  125. package/scripts/wire-orphans.mjs +164 -0
  126. package/server/activity-routes.js +123 -0
  127. package/server/agent-health.js +197 -0
  128. package/server/agent-hooks.js +102 -0
  129. package/server/agent-mcp.js +10 -0
  130. package/server/agent-registry.js +95 -0
  131. package/server/agent-server.js +290 -0
  132. package/server/agent-spawn.js +251 -0
  133. package/server/agent-templates.js +77 -0
  134. package/server/artifact-manager.js +247 -0
  135. package/server/artifact-templates/architecture.md +28 -0
  136. package/server/artifact-templates/blueprint.md +21 -0
  137. package/server/artifact-templates/design.md +36 -0
  138. package/server/artifact-templates/plan.md +25 -0
  139. package/server/artifact-templates/prd.md +43 -0
  140. package/server/artifact-templates/report.md +40 -0
  141. package/server/block-tracker.js +90 -0
  142. package/server/build-stream-bridge.js +502 -0
  143. package/server/coalescing-buffer.js +46 -0
  144. package/server/compose-mcp-tools.js +479 -0
  145. package/server/compose-mcp.js +324 -0
  146. package/server/connectors/agent-connector.js +78 -0
  147. package/server/connectors/claude-sdk-connector.js +198 -0
  148. package/server/connectors/codex-connector.js +240 -0
  149. package/server/connectors/connector-discovery.js +18 -0
  150. package/server/connectors/connector-runtime.js +13 -0
  151. package/server/connectors/opencode-connector.js +200 -0
  152. package/server/design-routes.js +540 -0
  153. package/server/design-session.js +161 -0
  154. package/server/feature-scan.js +593 -0
  155. package/server/file-watcher.js +284 -0
  156. package/server/find-root.js +29 -0
  157. package/server/graph-export.js +343 -0
  158. package/server/ideabox-cache.js +77 -0
  159. package/server/ideabox-routes.js +294 -0
  160. package/server/index.js +156 -0
  161. package/server/model-tiers.js +49 -0
  162. package/server/pipeline-routes.js +288 -0
  163. package/server/policy-evaluator.js +36 -0
  164. package/server/project-root.js +122 -0
  165. package/server/security.js +23 -0
  166. package/server/session-manager.js +403 -0
  167. package/server/session-routes.js +190 -0
  168. package/server/session-store.js +107 -0
  169. package/server/settings-routes.js +35 -0
  170. package/server/settings-store.js +234 -0
  171. package/server/stratum-api.js +102 -0
  172. package/server/stratum-client.js +192 -0
  173. package/server/stratum-sync.js +193 -0
  174. package/server/summarizer.js +139 -0
  175. package/server/supervisor.js +196 -0
  176. package/server/vision-routes.js +668 -0
  177. package/server/vision-server.js +393 -0
  178. package/server/vision-store.js +360 -0
  179. package/server/vision-utils.js +179 -0
  180. package/server/worktree-gc.js +137 -0
  181. package/templates/ROADMAP.md +46 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * server/ideabox-cache.js — JSON cache for ideabox data.
3
+ *
4
+ * Cache lives at .compose/data/ideabox-cache.json.
5
+ * Invalidated when the source file's mtime changes.
6
+ */
7
+
8
+ import fs from 'node:fs'
9
+ import path from 'node:path'
10
+ import { parseIdeabox } from '../lib/ideabox.js'
11
+
12
+ export class IdeaboxCache {
13
+ /**
14
+ * @param {string} dataDir — absolute path to .compose/data/
15
+ * @param {string} sourceFile — absolute path to ideabox.md
16
+ */
17
+ constructor(dataDir, sourceFile) {
18
+ this._dataDir = dataDir
19
+ this._sourceFile = sourceFile
20
+ this._cachePath = path.join(dataDir, 'ideabox-cache.json')
21
+ this._cachedMtime = null
22
+ this._cachedData = null
23
+ }
24
+
25
+ /**
26
+ * Return parsed ideabox data, using cache when source file is unchanged.
27
+ * @returns {{ ideas, killed, nextId, _mtime: number }}
28
+ */
29
+ get() {
30
+ let sourceMtime = null
31
+ try {
32
+ const stat = fs.statSync(this._sourceFile)
33
+ sourceMtime = stat.mtimeMs
34
+ } catch {
35
+ // Source file missing — return empty
36
+ return { ideas: [], killed: [], nextId: 1, _mtime: null }
37
+ }
38
+
39
+ // Cache hit
40
+ if (this._cachedData && this._cachedMtime === sourceMtime) {
41
+ return this._cachedData
42
+ }
43
+
44
+ // Load from disk cache if mtime matches
45
+ if (fs.existsSync(this._cachePath)) {
46
+ try {
47
+ const disk = JSON.parse(fs.readFileSync(this._cachePath, 'utf-8'))
48
+ if (disk._mtime === sourceMtime) {
49
+ this._cachedMtime = sourceMtime
50
+ this._cachedData = disk
51
+ return disk
52
+ }
53
+ } catch { /* stale or corrupt — reparse */ }
54
+ }
55
+
56
+ // Parse fresh
57
+ const markdown = fs.readFileSync(this._sourceFile, 'utf-8')
58
+ const parsed = parseIdeabox(markdown)
59
+ const entry = { ...parsed, _mtime: sourceMtime }
60
+
61
+ // Write to disk cache
62
+ try {
63
+ fs.mkdirSync(this._dataDir, { recursive: true })
64
+ fs.writeFileSync(this._cachePath, JSON.stringify(entry, null, 2))
65
+ } catch { /* non-fatal */ }
66
+
67
+ this._cachedMtime = sourceMtime
68
+ this._cachedData = entry
69
+ return entry
70
+ }
71
+
72
+ /** Invalidate in-memory cache (e.g., after a write). */
73
+ invalidate() {
74
+ this._cachedMtime = null
75
+ this._cachedData = null
76
+ }
77
+ }
@@ -0,0 +1,294 @@
1
+ /**
2
+ * server/ideabox-routes.js — REST API for the ideabox feature.
3
+ *
4
+ * Routes:
5
+ * GET /api/ideabox — return parsed ideabox JSON (cached)
6
+ * POST /api/ideabox/ideas — add new idea
7
+ * PATCH /api/ideabox/ideas/:id — update priority/status/etc.
8
+ * POST /api/ideabox/ideas/:id/promote — promote to feature
9
+ * POST /api/ideabox/ideas/:id/kill — kill with reason
10
+ * DELETE /api/ideabox/ideas/:id — not allowed (use kill)
11
+ */
12
+
13
+ import fs from 'node:fs'
14
+ import path from 'node:path'
15
+ import {
16
+ readIdeabox,
17
+ writeIdeabox,
18
+ addIdea,
19
+ promoteIdea,
20
+ killIdea,
21
+ resurrectIdea,
22
+ setPriority,
23
+ updateIdea,
24
+ addDiscussion,
25
+ } from '../lib/ideabox.js'
26
+ import { IdeaboxCache } from './ideabox-cache.js'
27
+
28
+ /**
29
+ * @param {object} app — Express app
30
+ * @param {{ getProjectRoot, getDataDir, broadcastMessage }} deps
31
+ */
32
+ export function attachIdeaboxRoutes(app, { getProjectRoot, getDataDir, broadcastMessage }) {
33
+ // Lazily created per project root — we need to handle project switches
34
+ let _cache = null
35
+ let _lastProjectRoot = null
36
+ let _lastDataDir = null
37
+
38
+ function getCache() {
39
+ const projectRoot = getProjectRoot()
40
+ const dataDir = getDataDir()
41
+ if (!_cache || _lastProjectRoot !== projectRoot || _lastDataDir !== dataDir) {
42
+ const config = loadConfig(projectRoot)
43
+ const ideaboxRel = config?.paths?.ideabox || 'docs/product/ideabox.md'
44
+ const sourceFile = path.join(projectRoot, ideaboxRel)
45
+ _cache = new IdeaboxCache(dataDir, sourceFile)
46
+ _lastProjectRoot = projectRoot
47
+ _lastDataDir = dataDir
48
+ }
49
+ return _cache
50
+ }
51
+
52
+ function getIdeaboxPath(projectRoot) {
53
+ const config = loadConfig(projectRoot)
54
+ return config?.paths?.ideabox || 'docs/product/ideabox.md'
55
+ }
56
+
57
+ function broadcastUpdate() {
58
+ if (broadcastMessage) {
59
+ broadcastMessage({ type: 'ideaboxUpdated', timestamp: new Date().toISOString() })
60
+ }
61
+ }
62
+
63
+ // GET /api/ideabox
64
+ app.get('/api/ideabox', (_req, res) => {
65
+ try {
66
+ const cache = getCache()
67
+ const data = cache.get()
68
+ res.json(data)
69
+ } catch (err) {
70
+ res.status(500).json({ error: err.message })
71
+ }
72
+ })
73
+
74
+ // POST /api/ideabox/ideas
75
+ app.post('/api/ideabox/ideas', (req, res) => {
76
+ try {
77
+ const { title, description, source, tags, cluster } = req.body || {}
78
+ if (!title) return res.status(400).json({ error: 'title is required' })
79
+
80
+ const projectRoot = getProjectRoot()
81
+ const ideaboxPath = getIdeaboxPath(projectRoot)
82
+ const parsed = readIdeabox(projectRoot, ideaboxPath)
83
+ addIdea(parsed, { title, description, source, tags, cluster })
84
+ writeIdeabox(projectRoot, ideaboxPath, parsed)
85
+ getCache().invalidate()
86
+ broadcastUpdate()
87
+ // Return the newly created idea
88
+ const newIdea = parsed.ideas[parsed.ideas.length - 1]
89
+ res.status(201).json(newIdea)
90
+ } catch (err) {
91
+ res.status(500).json({ error: err.message })
92
+ }
93
+ })
94
+
95
+ // PATCH /api/ideabox/ideas/:id
96
+ app.patch('/api/ideabox/ideas/:id', (req, res) => {
97
+ try {
98
+ const { id } = req.params
99
+ const fields = req.body || {}
100
+
101
+ const projectRoot = getProjectRoot()
102
+ const ideaboxPath = getIdeaboxPath(projectRoot)
103
+ const parsed = readIdeabox(projectRoot, ideaboxPath)
104
+
105
+ // Handle priority shortcut
106
+ if (fields.priority) {
107
+ setPriority(parsed, id, fields.priority)
108
+ delete fields.priority
109
+ }
110
+
111
+ // Reject status changes via PATCH — must use /promote or /kill endpoints
112
+ // to ensure proper transition logic (move to killed section, set fields, etc.)
113
+ if (fields.status !== undefined) {
114
+ return res.status(400).json({
115
+ error: 'Status changes must go through /promote or /kill endpoints, not PATCH'
116
+ })
117
+ }
118
+
119
+ // Handle remaining fields (no status)
120
+ const allowed = ['title', 'description', 'source', 'tags', 'cluster', 'mapsTo', 'effort', 'impact']
121
+ const safeFields = {}
122
+ for (const k of allowed) {
123
+ if (fields[k] !== undefined) safeFields[k] = fields[k]
124
+ }
125
+ // Validate enum fields
126
+ if (safeFields.effort !== undefined && safeFields.effort !== null && !['S', 'M', 'L'].includes(safeFields.effort)) {
127
+ return res.status(400).json({ error: 'effort must be S, M, L, or null' })
128
+ }
129
+ if (safeFields.impact !== undefined && safeFields.impact !== null && !['low', 'medium', 'high'].includes(safeFields.impact)) {
130
+ return res.status(400).json({ error: 'impact must be low, medium, high, or null' })
131
+ }
132
+ if (Object.keys(safeFields).length > 0) {
133
+ updateIdea(parsed, id, safeFields)
134
+ }
135
+
136
+ writeIdeabox(projectRoot, ideaboxPath, parsed)
137
+ getCache().invalidate()
138
+ broadcastUpdate()
139
+
140
+ // Find and return the updated idea
141
+ const upper = id.toUpperCase()
142
+ const updated = [...parsed.ideas, ...parsed.killed].find(i => i.id.toUpperCase() === upper)
143
+ res.json(updated || { id })
144
+ } catch (err) {
145
+ const status = err.message.includes('not found') ? 404 : 500
146
+ res.status(status).json({ error: err.message })
147
+ }
148
+ })
149
+
150
+ // POST /api/ideabox/ideas/:id/promote
151
+ app.post('/api/ideabox/ideas/:id/promote', (req, res) => {
152
+ try {
153
+ const { id } = req.params
154
+ const { featureCode } = req.body || {}
155
+
156
+ const projectRoot = getProjectRoot()
157
+ const ideaboxPath = getIdeaboxPath(projectRoot)
158
+ const parsed = readIdeabox(projectRoot, ideaboxPath)
159
+
160
+ // Find the idea before promoting (need title for feature folder seed)
161
+ const upper = id.toUpperCase()
162
+ const sourceIdea = parsed.ideas.find(i => i.id.toUpperCase() === upper)
163
+ if (!sourceIdea) {
164
+ return res.status(404).json({ error: `Idea not found: ${id}` })
165
+ }
166
+
167
+ // Resolve feature code: explicit, or derived from idea
168
+ let resolvedCode = featureCode
169
+ if (!resolvedCode) {
170
+ const slug = sourceIdea.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 20).replace(/-+$/, '')
171
+ resolvedCode = `IDEA-${sourceIdea.num}-${slug}`.toUpperCase()
172
+ }
173
+
174
+ // Create feature folder + feature.json (same logic as CLI promote)
175
+ const composeJsonPath = path.join(projectRoot, '.compose', 'compose.json')
176
+ let featuresRel = 'docs/features'
177
+ if (fs.existsSync(composeJsonPath)) {
178
+ try {
179
+ const cfg = JSON.parse(fs.readFileSync(composeJsonPath, 'utf-8'))
180
+ featuresRel = cfg?.paths?.features || 'docs/features'
181
+ } catch {}
182
+ }
183
+ const featuresDir = path.join(projectRoot, featuresRel, resolvedCode)
184
+ if (!fs.existsSync(featuresDir)) {
185
+ fs.mkdirSync(featuresDir, { recursive: true })
186
+ fs.writeFileSync(path.join(featuresDir, 'feature.json'), JSON.stringify({
187
+ code: resolvedCode,
188
+ description: sourceIdea.title,
189
+ status: 'PLANNED',
190
+ promotedFrom: sourceIdea.id,
191
+ createdAt: new Date().toISOString(),
192
+ }, null, 2))
193
+ }
194
+
195
+ promoteIdea(parsed, id, resolvedCode)
196
+ writeIdeabox(projectRoot, ideaboxPath, parsed)
197
+ getCache().invalidate()
198
+ broadcastUpdate()
199
+
200
+ const updated = parsed.ideas.find(i => i.id.toUpperCase() === upper)
201
+ res.json({ ...(updated || { id }), featureCode: resolvedCode, featurePath: `${featuresRel}/${resolvedCode}` })
202
+ } catch (err) {
203
+ const status = err.message.includes('not found') ? 404 : 500
204
+ res.status(status).json({ error: err.message })
205
+ }
206
+ })
207
+
208
+ // POST /api/ideabox/ideas/:id/kill
209
+ app.post('/api/ideabox/ideas/:id/kill', (req, res) => {
210
+ try {
211
+ const { id } = req.params
212
+ const { reason } = req.body || {}
213
+
214
+ const projectRoot = getProjectRoot()
215
+ const ideaboxPath = getIdeaboxPath(projectRoot)
216
+ const parsed = readIdeabox(projectRoot, ideaboxPath)
217
+ killIdea(parsed, id, reason || '')
218
+ writeIdeabox(projectRoot, ideaboxPath, parsed)
219
+ getCache().invalidate()
220
+ broadcastUpdate()
221
+
222
+ const upper = id.toUpperCase()
223
+ const killed = parsed.killed.find(i => i.id.toUpperCase() === upper)
224
+ res.json(killed || { id })
225
+ } catch (err) {
226
+ const status = err.message.includes('not found') ? 404 : 500
227
+ res.status(status).json({ error: err.message })
228
+ }
229
+ })
230
+
231
+ // POST /api/ideabox/ideas/:id/resurrect
232
+ app.post('/api/ideabox/ideas/:id/resurrect', (req, res) => {
233
+ try {
234
+ const { id } = req.params
235
+ const projectRoot = getProjectRoot()
236
+ const ideaboxPath = getIdeaboxPath(projectRoot)
237
+ const parsed = readIdeabox(projectRoot, ideaboxPath)
238
+ resurrectIdea(parsed, id)
239
+ writeIdeabox(projectRoot, ideaboxPath, parsed)
240
+ getCache().invalidate()
241
+ broadcastUpdate()
242
+
243
+ const upper = id.toUpperCase()
244
+ const restored = parsed.ideas.find(i => i.id.toUpperCase() === upper)
245
+ res.json(restored || { id })
246
+ } catch (err) {
247
+ const status = err.message.includes('not found') ? 404 : 500
248
+ res.status(status).json({ error: err.message })
249
+ }
250
+ })
251
+
252
+ // POST /api/ideabox/ideas/:id/discuss
253
+ app.post('/api/ideabox/ideas/:id/discuss', (req, res) => {
254
+ try {
255
+ const { id } = req.params
256
+ const { author, text } = req.body || {}
257
+ if (!author) return res.status(400).json({ error: 'author is required' })
258
+ if (!text) return res.status(400).json({ error: 'text is required' })
259
+
260
+ const projectRoot = getProjectRoot()
261
+ const ideaboxPath = getIdeaboxPath(projectRoot)
262
+ const parsed = readIdeabox(projectRoot, ideaboxPath)
263
+ addDiscussion(parsed, id, author, text)
264
+ writeIdeabox(projectRoot, ideaboxPath, parsed)
265
+ getCache().invalidate()
266
+ broadcastUpdate()
267
+
268
+ const upper = id.toUpperCase()
269
+ const updated = [...parsed.ideas, ...parsed.killed].find(i => i.id.toUpperCase() === upper)
270
+ res.status(201).json(updated || { id })
271
+ } catch (err) {
272
+ const status = err.message.includes('not found') ? 404 : 500
273
+ res.status(status).json({ error: err.message })
274
+ }
275
+ })
276
+
277
+ // DELETE /api/ideabox/ideas/:id — not allowed
278
+ app.delete('/api/ideabox/ideas/:id', (_req, res) => {
279
+ res.status(405).json({ error: 'Deletion not allowed. Use POST /api/ideabox/ideas/:id/kill instead.' })
280
+ })
281
+ }
282
+
283
+ // ---------------------------------------------------------------------------
284
+ // Internal: load compose.json config
285
+ // ---------------------------------------------------------------------------
286
+
287
+ function loadConfig(projectRoot) {
288
+ const configPath = path.join(projectRoot, '.compose', 'compose.json')
289
+ try {
290
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
291
+ } catch {
292
+ return null
293
+ }
294
+ }
@@ -0,0 +1,156 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import http from 'node:http';
4
+ import path from 'node:path';
5
+ import { execFileSync } from 'node:child_process';
6
+ import { FileWatcherServer } from './file-watcher.js';
7
+ import { VisionStore } from './vision-store.js';
8
+ import { VisionServer } from './vision-server.js';
9
+ import { SessionManager } from './session-manager.js';
10
+ import { scanFeatures, seedFeatures, scanSubPackages, seedSubPackages, seedFromRoadmapGraph } from './feature-scan.js';
11
+ import { attachGraphExportRoutes } from './graph-export.js';
12
+ import { getTargetRoot, getDataDir, ensureDataDir, loadProjectConfig, resolveProjectPath, switchProject } from './project-root.js';
13
+
14
+ // Load project config and verify stratum capability matches reality
15
+ const projectConfig = loadProjectConfig();
16
+ if (projectConfig.capabilities.stratum) {
17
+ try {
18
+ execFileSync('which', ['stratum-mcp'], { stdio: 'ignore' });
19
+ } catch {
20
+ console.error('[compose] stratum-mcp not found but capabilities.stratum=true');
21
+ console.error('[compose] Run: compose init (will auto-install) or compose init --no-stratum');
22
+ projectConfig.capabilities.stratum = false;
23
+ }
24
+ }
25
+
26
+ // Handle unexpected errors — fatal startup errors exit (supervisor retries),
27
+ // runtime errors keep the process alive to preserve PTY sessions
28
+ let serverListening = false;
29
+ process.on('uncaughtException', (err) => {
30
+ if (!serverListening && err.code === 'EADDRINUSE') {
31
+ console.error(`[compose] Port in use, exiting for supervisor retry: ${err.message}`);
32
+ process.exit(1);
33
+ }
34
+ console.error('[compose] Uncaught exception (process kept alive):', err.message);
35
+ console.error(err.stack);
36
+ });
37
+ process.on('unhandledRejection', (reason) => {
38
+ console.error('[compose] Unhandled rejection (process kept alive):', reason);
39
+ });
40
+ process.on('SIGTERM', () => {
41
+ console.log('[compose] SIGTERM received, shutting down gracefully');
42
+ process.exit(0);
43
+ });
44
+
45
+ const PORT = process.env.PORT || 4001;
46
+ const app = express();
47
+ app.use(cors({ origin: /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/ }));
48
+ app.use(express.json());
49
+
50
+ app.get('/api/health', (_req, res) => res.json({ ok: true }));
51
+ app.get('/api/status', (_req, res) => res.json({ session: 2, phase: '0.4-brainstorm', upSince: new Date().toISOString() }));
52
+
53
+ // Project info + switching
54
+ app.get('/api/project', (_req, res) => {
55
+ const root = getTargetRoot();
56
+ res.json({
57
+ targetRoot: root,
58
+ name: path.basename(root),
59
+ dataDir: getDataDir(),
60
+ });
61
+ });
62
+
63
+ app.post('/api/project/switch', (req, res) => {
64
+ const { path: projectPath } = req.body || {};
65
+ if (!projectPath) return res.status(400).json({ error: 'path is required' });
66
+ try {
67
+ const result = switchProject(projectPath);
68
+ ensureDataDir();
69
+ // Reload store from new data directory
70
+ visionStore.reloadFrom(result.dataDir);
71
+ // Re-scan features, sub-packages, and roadmap graph from new project
72
+ try {
73
+ const features = scanFeatures();
74
+ if (features.length > 0) seedFeatures(features, visionStore);
75
+ const packages = scanSubPackages();
76
+ if (packages.length > 0) seedSubPackages(packages, visionStore);
77
+ seedFromRoadmapGraph(visionStore);
78
+ } catch (err) {
79
+ console.error('[compose] Feature scan after switch:', err.message);
80
+ }
81
+ // Broadcast new state to all connected clients
82
+ visionServer.scheduleBroadcast();
83
+ res.json({ ok: true, targetRoot: result.targetRoot, name: path.basename(result.targetRoot) });
84
+ } catch (err) {
85
+ res.status(400).json({ error: err.message });
86
+ }
87
+ });
88
+
89
+ const server = http.createServer(app);
90
+ const fileWatcher = new FileWatcherServer();
91
+ fileWatcher.attach(server, app);
92
+ ensureDataDir();
93
+ const visionStore = new VisionStore(getDataDir());
94
+ const sessionManager = new SessionManager({
95
+ getFeaturePhase: (featureCode) => {
96
+ const item = visionStore.getItemByFeatureCode(featureCode);
97
+ return item?.lifecycle?.currentPhase || null;
98
+ },
99
+ featureRoot: resolveProjectPath('features'),
100
+ });
101
+ const visionServer = new VisionServer(visionStore, sessionManager, { config: projectConfig });
102
+ visionServer.attach(server, app);
103
+
104
+ // Seed feature folders, sub-packages, and roadmap graph into vision store on startup
105
+ try {
106
+ const features = scanFeatures();
107
+ if (features.length > 0) seedFeatures(features, visionStore);
108
+ const packages = scanSubPackages();
109
+ if (packages.length > 0) seedSubPackages(packages, visionStore);
110
+ seedFromRoadmapGraph(visionStore);
111
+ } catch (err) {
112
+ console.error('[compose] Feature scan startup error:', err.message);
113
+ }
114
+
115
+ // Wire feature folder changes → auto-reseed vision store
116
+ fileWatcher.onFeatureChanged = (_relativePath) => {
117
+ try {
118
+ const features = scanFeatures();
119
+ seedFeatures(features, visionStore);
120
+ visionServer.scheduleBroadcast();
121
+ } catch (err) {
122
+ console.error('[compose] Feature reseed error:', err.message);
123
+ }
124
+ };
125
+
126
+ // Wire build state changes → broadcast over /ws/vision
127
+ fileWatcher.onBuildStateChanged = (state) => {
128
+ if (state) {
129
+ // Broadcast flat payload per STRAT-COMP-4 contract
130
+ visionServer.broadcastMessage({ type: 'buildState', ...state });
131
+ }
132
+ };
133
+
134
+ // Manual WebSocket upgrade routing — avoids the ws library bug where multiple
135
+ // WebSocketServers on the same HTTP server write 400 on each other's connections
136
+ server.on('upgrade', (req, socket, head) => {
137
+ const { pathname } = new URL(req.url, 'http://localhost');
138
+ if (pathname === '/ws/files' && fileWatcher.wss) {
139
+ fileWatcher.wss.handleUpgrade(req, socket, head, (ws) => {
140
+ fileWatcher.wss.emit('connection', ws, req);
141
+ });
142
+ } else if (pathname === '/ws/vision' && visionServer.wss) {
143
+ visionServer.wss.handleUpgrade(req, socket, head, (ws) => {
144
+ visionServer.wss.emit('connection', ws, req);
145
+ });
146
+ } else {
147
+ socket.destroy();
148
+ }
149
+ });
150
+
151
+ server.listen(PORT, '127.0.0.1', () => {
152
+ serverListening = true;
153
+ console.log(`Compose server running on http://127.0.0.1:${PORT}`);
154
+ console.log(`File watcher WebSocket: ws://localhost:${PORT}/ws/files`);
155
+ console.log(`Vision WebSocket: ws://localhost:${PORT}/ws/vision`);
156
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * model-tiers.js — Model tier routing for STRAT-TIER.
3
+ *
4
+ * Maps symbolic tier names to concrete Anthropic model IDs.
5
+ * Tiers let pipeline specs declare intent (critical / standard / fast)
6
+ * without hard-coding model strings — the map here is the single source of truth.
7
+ */
8
+
9
+ /** @type {Record<string, string>} */
10
+ export const MODEL_TIERS = {
11
+ critical: 'claude-opus-4-7',
12
+ standard: 'claude-sonnet-4-6',
13
+ fast: 'claude-haiku-4-5-20251001',
14
+ };
15
+
16
+ /**
17
+ * Default thinking config per tier.
18
+ * - Opus 4.7 / Sonnet 4.6 support adaptive thinking and the effort parameter.
19
+ * - Haiku 4.5 doesn't accept the effort parameter (400 error), so fast tier stays off.
20
+ *
21
+ * @type {Record<string, { mode: 'adaptive'|'off', effort: 'low'|'medium'|'high'|'xhigh'|'max'|null }>}
22
+ */
23
+ export const TIER_THINKING = {
24
+ critical: { mode: 'adaptive', effort: 'xhigh' },
25
+ standard: { mode: 'adaptive', effort: 'high' },
26
+ fast: { mode: 'off', effort: null },
27
+ };
28
+
29
+ /**
30
+ * Resolve a tier name to a concrete model ID.
31
+ *
32
+ * @param {string|null|undefined} tier
33
+ * @returns {string|null} Model ID, or null if tier is unknown / not provided.
34
+ */
35
+ export function resolveTierModel(tier) {
36
+ if (!tier) return null;
37
+ return MODEL_TIERS[tier] ?? null;
38
+ }
39
+
40
+ /**
41
+ * Resolve a tier name to its default thinking config.
42
+ *
43
+ * @param {string|null|undefined} tier
44
+ * @returns {{ mode: string, effort: string|null }|null}
45
+ */
46
+ export function resolveTierThinking(tier) {
47
+ if (!tier) return null;
48
+ return TIER_THINKING[tier] ?? null;
49
+ }