@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.
- package/LICENSE +21 -0
- package/README.md +1014 -0
- package/bin/compose.js +1515 -0
- package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
- package/dist/assets/arc-SxJ2J1sh.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
- package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
- package/dist/assets/channel-DGElom1e.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
- package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
- package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
- package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
- package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
- package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
- package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
- package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
- package/dist/assets/clone-DUJKJXd7.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
- package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
- package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
- package/dist/assets/graph-D0Cfv00Y.js +1 -0
- package/dist/assets/index-CUd6pFGF.css +1 -0
- package/dist/assets/index-DReRlzZI.js +1144 -0
- package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
- package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
- package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-Bj72wOEB.js +1 -0
- package/dist/assets/linear-BRFo114D.js +1 -0
- package/dist/assets/min-GCHnKlJS.js +1 -0
- package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
- package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
- package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
- package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
- package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
- package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
- package/dist/index.html +30 -0
- package/lib/agent-chains.js +65 -0
- package/lib/agent-string.js +86 -0
- package/lib/budget-ledger.js +86 -0
- package/lib/build-all.js +162 -0
- package/lib/build-dag.js +120 -0
- package/lib/build-stream-writer.js +190 -0
- package/lib/build.js +2997 -0
- package/lib/capability-checker.js +53 -0
- package/lib/cert-inject.js +38 -0
- package/lib/cli-progress.js +483 -0
- package/lib/constants.js +69 -0
- package/lib/cross-layer-audit.js +84 -0
- package/lib/debug-discipline.js +173 -0
- package/lib/feature-json.js +106 -0
- package/lib/gate-prompt.js +291 -0
- package/lib/gate-tiers.js +194 -0
- package/lib/health-history.js +119 -0
- package/lib/health-score.js +227 -0
- package/lib/ideabox.js +570 -0
- package/lib/import.js +244 -0
- package/lib/migrate-roadmap.js +94 -0
- package/lib/model-pricing.js +67 -0
- package/lib/new.js +413 -0
- package/lib/pipeline-cli.js +489 -0
- package/lib/plan-parser.js +103 -0
- package/lib/qa-scoping.js +474 -0
- package/lib/questionnaire.js +200 -0
- package/lib/resolve-port.js +7 -0
- package/lib/result-normalizer.js +349 -0
- package/lib/review-lenses.js +166 -0
- package/lib/roadmap-gen.js +210 -0
- package/lib/roadmap-parser.js +176 -0
- package/lib/server-probe.js +23 -0
- package/lib/staleness.js +87 -0
- package/lib/step-prompt.js +260 -0
- package/lib/step-validator.js +49 -0
- package/lib/stratum-mcp-client.js +365 -0
- package/lib/team-flag.js +46 -0
- package/lib/test-bootstrap.js +401 -0
- package/lib/triage.js +274 -0
- package/lib/vision-writer.js +391 -0
- package/package.json +111 -0
- package/pipelines/bug-fix.stratum.yaml +230 -0
- package/pipelines/build.stratum.yaml +498 -0
- package/pipelines/content.stratum.yaml +112 -0
- package/pipelines/coverage-sweep.stratum.yaml +52 -0
- package/pipelines/refactor.stratum.yaml +169 -0
- package/pipelines/research.stratum.yaml +88 -0
- package/pipelines/review-fix.stratum.yaml +109 -0
- package/presets/team-feature.stratum.yaml +105 -0
- package/presets/team-research.stratum.yaml +108 -0
- package/presets/team-review.stratum.yaml +106 -0
- package/scripts/agent-activity-hook.sh +31 -0
- package/scripts/agent-error-hook.sh +28 -0
- package/scripts/analyze-orphans.mjs +50 -0
- package/scripts/find-orphans.mjs +26 -0
- package/scripts/fix-phases.mjs +49 -0
- package/scripts/generate-stratum-spec.mjs +137 -0
- package/scripts/import-roadmap.mjs +116 -0
- package/scripts/phase-audit.mjs +33 -0
- package/scripts/run-pipeline.mjs +314 -0
- package/scripts/session-end-hook.sh +18 -0
- package/scripts/session-start-hook.sh +38 -0
- package/scripts/vision-hook.sh +104 -0
- package/scripts/vision-track.mjs +554 -0
- package/scripts/wire-all-orphans.mjs +108 -0
- package/scripts/wire-orphans.mjs +164 -0
- package/server/activity-routes.js +123 -0
- package/server/agent-health.js +197 -0
- package/server/agent-hooks.js +102 -0
- package/server/agent-mcp.js +10 -0
- package/server/agent-registry.js +95 -0
- package/server/agent-server.js +290 -0
- package/server/agent-spawn.js +251 -0
- package/server/agent-templates.js +77 -0
- package/server/artifact-manager.js +247 -0
- package/server/artifact-templates/architecture.md +28 -0
- package/server/artifact-templates/blueprint.md +21 -0
- package/server/artifact-templates/design.md +36 -0
- package/server/artifact-templates/plan.md +25 -0
- package/server/artifact-templates/prd.md +43 -0
- package/server/artifact-templates/report.md +40 -0
- package/server/block-tracker.js +90 -0
- package/server/build-stream-bridge.js +502 -0
- package/server/coalescing-buffer.js +46 -0
- package/server/compose-mcp-tools.js +479 -0
- package/server/compose-mcp.js +324 -0
- package/server/connectors/agent-connector.js +78 -0
- package/server/connectors/claude-sdk-connector.js +198 -0
- package/server/connectors/codex-connector.js +240 -0
- package/server/connectors/connector-discovery.js +18 -0
- package/server/connectors/connector-runtime.js +13 -0
- package/server/connectors/opencode-connector.js +200 -0
- package/server/design-routes.js +540 -0
- package/server/design-session.js +161 -0
- package/server/feature-scan.js +593 -0
- package/server/file-watcher.js +284 -0
- package/server/find-root.js +29 -0
- package/server/graph-export.js +343 -0
- package/server/ideabox-cache.js +77 -0
- package/server/ideabox-routes.js +294 -0
- package/server/index.js +156 -0
- package/server/model-tiers.js +49 -0
- package/server/pipeline-routes.js +288 -0
- package/server/policy-evaluator.js +36 -0
- package/server/project-root.js +122 -0
- package/server/security.js +23 -0
- package/server/session-manager.js +403 -0
- package/server/session-routes.js +190 -0
- package/server/session-store.js +107 -0
- package/server/settings-routes.js +35 -0
- package/server/settings-store.js +234 -0
- package/server/stratum-api.js +102 -0
- package/server/stratum-client.js +192 -0
- package/server/stratum-sync.js +193 -0
- package/server/summarizer.js +139 -0
- package/server/supervisor.js +196 -0
- package/server/vision-routes.js +668 -0
- package/server/vision-server.js +393 -0
- package/server/vision-store.js +360 -0
- package/server/vision-utils.js +179 -0
- package/server/worktree-gc.js +137 -0
- 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
|
+
}
|
package/server/index.js
ADDED
|
@@ -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
|
+
}
|