@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,288 @@
1
+ /**
2
+ * pipeline-routes.js — Pipeline authoring REST routes.
3
+ *
4
+ * Routes:
5
+ * GET /api/pipeline/templates — list available pipeline templates
6
+ * GET /api/pipeline/templates/:id/spec — full YAML spec for a template
7
+ * POST /api/pipeline/draft — create a draft from template or inline spec
8
+ * GET /api/pipeline/draft — get current draft
9
+ * POST /api/pipeline/draft/approve — approve and persist current draft
10
+ * POST /api/pipeline/draft/reject — reject and discard current draft
11
+ */
12
+
13
+ import { readdirSync, readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { randomUUID } from 'node:crypto';
16
+ import YAML from 'yaml';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Read all *.stratum.yaml files from the pipelines dir and return those
24
+ * with a valid metadata block.
25
+ */
26
+ function loadTemplates(pipelinesDir) {
27
+ const templates = [];
28
+ let files;
29
+ try {
30
+ files = readdirSync(pipelinesDir).filter(f => f.endsWith('.stratum.yaml'));
31
+ } catch {
32
+ return templates;
33
+ }
34
+
35
+ for (const file of files) {
36
+ try {
37
+ const raw = readFileSync(join(pipelinesDir, file), 'utf-8');
38
+ const parsed = YAML.parse(raw);
39
+ if (parsed?.metadata?.id) {
40
+ templates.push({
41
+ id: parsed.metadata.id,
42
+ label: parsed.metadata.label || parsed.metadata.id,
43
+ description: parsed.metadata.description || '',
44
+ category: parsed.metadata.category || 'general',
45
+ steps: typeof parsed.metadata.steps === 'number' ? parsed.metadata.steps : 0,
46
+ estimated_minutes: typeof parsed.metadata.estimated_minutes === 'number' ? parsed.metadata.estimated_minutes : 0,
47
+ _file: file,
48
+ });
49
+ }
50
+ } catch {
51
+ // skip unparseable files
52
+ }
53
+ }
54
+ return templates;
55
+ }
56
+
57
+ /**
58
+ * Extract steps from a parsed spec, version-aware.
59
+ * v0.3: workflow.steps or flows.<name>.steps
60
+ * v0.1: flows.<name>.steps or functions (as step list)
61
+ */
62
+ function extractSteps(parsed) {
63
+ // v0.3 with workflow.steps
64
+ if (parsed?.workflow?.steps) {
65
+ return parsed.workflow.steps.map(s => ({ id: s.id, ...s }));
66
+ }
67
+
68
+ // flows.<name>.steps (both v0.1 and v0.3)
69
+ if (parsed?.flows) {
70
+ for (const flowName of Object.keys(parsed.flows)) {
71
+ const flow = parsed.flows[flowName];
72
+ if (Array.isArray(flow?.steps) && flow.steps.length > 0) {
73
+ return flow.steps.map(s => ({ id: s.id, ...s }));
74
+ }
75
+ }
76
+ }
77
+
78
+ // Fallback: list functions as pseudo-steps (v0.1 function-only specs)
79
+ if (parsed?.functions) {
80
+ return Object.keys(parsed.functions).map(id => ({ id }));
81
+ }
82
+
83
+ return [];
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // In-memory draft state (single draft at a time)
88
+ // ---------------------------------------------------------------------------
89
+
90
+ let currentDraft = null;
91
+
92
+ const DRAFT_FILE = 'pipeline-draft.json';
93
+
94
+ function saveDraft(dataDir, draft) {
95
+ currentDraft = draft;
96
+ mkdirSync(dataDir, { recursive: true });
97
+ writeFileSync(join(dataDir, DRAFT_FILE), JSON.stringify(draft, null, 2));
98
+ }
99
+
100
+ function loadDraft(dataDir) {
101
+ if (currentDraft) return currentDraft;
102
+ const p = join(dataDir, DRAFT_FILE);
103
+ if (!existsSync(p)) return null;
104
+ try {
105
+ currentDraft = JSON.parse(readFileSync(p, 'utf-8'));
106
+ return currentDraft;
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ function clearDraft(dataDir) {
113
+ currentDraft = null;
114
+ const p = join(dataDir, DRAFT_FILE);
115
+ try { if (existsSync(p)) unlinkSync(p); } catch { /* ignore */ }
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Route attachment
120
+ // ---------------------------------------------------------------------------
121
+
122
+ /**
123
+ * Attach pipeline authoring routes to an Express app.
124
+ *
125
+ * @param {object} app — Express app
126
+ * @param {{
127
+ * broadcastMessage: function,
128
+ * scheduleBroadcast: function,
129
+ * getDataDir: function,
130
+ * getPipelinesDir: function,
131
+ * stratumClient?: { validate: function } | null,
132
+ * }} deps
133
+ */
134
+ export function attachPipelineRoutes(app, { broadcastMessage, scheduleBroadcast, getDataDir, getPipelinesDir, stratumClient }) {
135
+
136
+ // GET /api/pipeline/templates
137
+ app.get('/api/pipeline/templates', (_req, res) => {
138
+ const templates = loadTemplates(getPipelinesDir());
139
+ // Strip internal _file field
140
+ const clean = templates.map(({ _file, ...rest }) => rest);
141
+ res.json({ templates: clean });
142
+ });
143
+
144
+ // GET /api/pipeline/templates/:id/spec
145
+ app.get('/api/pipeline/templates/:id/spec', (req, res) => {
146
+ const templates = loadTemplates(getPipelinesDir());
147
+ const tmpl = templates.find(t => t.id === req.params.id);
148
+ if (!tmpl) return res.status(404).json({ error: `Template "${req.params.id}" not found` });
149
+
150
+ const spec = readFileSync(join(getPipelinesDir(), tmpl._file), 'utf-8');
151
+ res.json({ spec });
152
+ });
153
+
154
+ // POST /api/pipeline/draft
155
+ app.post('/api/pipeline/draft', (req, res) => {
156
+ const { templateId, spec: inlineSpec, metadata: inlineMeta } = req.body || {};
157
+ const dataDir = getDataDir();
158
+
159
+ let spec;
160
+ let metadata;
161
+ let parsed;
162
+
163
+ if (templateId) {
164
+ // Load from template
165
+ const templates = loadTemplates(getPipelinesDir());
166
+ const tmpl = templates.find(t => t.id === templateId);
167
+ if (!tmpl) return res.status(404).json({ error: `Template "${templateId}" not found` });
168
+
169
+ spec = readFileSync(join(getPipelinesDir(), tmpl._file), 'utf-8');
170
+ try {
171
+ parsed = YAML.parse(spec);
172
+ } catch (e) {
173
+ return res.status(400).json({ error: `Failed to parse template: ${e.message}` });
174
+ }
175
+ metadata = parsed.metadata || tmpl;
176
+ } else if (inlineSpec) {
177
+ // Inline spec
178
+ spec = inlineSpec;
179
+ try {
180
+ parsed = YAML.parse(spec);
181
+ } catch (e) {
182
+ return res.status(400).json({ error: `Invalid YAML: ${e.message}` });
183
+ }
184
+ metadata = parsed?.metadata || inlineMeta;
185
+ if (!metadata || !metadata.id) {
186
+ return res.status(400).json({ error: 'Spec must include a metadata block with at least an id field, or metadata must be provided separately' });
187
+ }
188
+ } else {
189
+ return res.status(400).json({ error: 'Provide either templateId or spec + metadata' });
190
+ }
191
+
192
+ const steps = extractSteps(parsed);
193
+ const draftId = randomUUID();
194
+
195
+ const draft = {
196
+ draftId,
197
+ spec,
198
+ metadata,
199
+ steps,
200
+ createdAt: new Date().toISOString(),
201
+ status: 'pending',
202
+ };
203
+
204
+ saveDraft(dataDir, draft);
205
+
206
+ broadcastMessage({ type: 'pipelineDraft', ...draft });
207
+
208
+ res.json(draft);
209
+ });
210
+
211
+ // GET /api/pipeline/draft
212
+ app.get('/api/pipeline/draft', (_req, res) => {
213
+ const draft = loadDraft(getDataDir());
214
+ res.json({ draft: draft ?? null });
215
+ });
216
+
217
+ // POST /api/pipeline/draft/approve
218
+ app.post('/api/pipeline/draft/approve', async (req, res) => {
219
+ const { draftId } = req.body || {};
220
+ const dataDir = getDataDir();
221
+ const draft = loadDraft(dataDir);
222
+
223
+ if (!draft) return res.status(404).json({ error: 'No draft to approve' });
224
+ if (draft.draftId !== draftId) return res.status(409).json({ error: 'Draft ID mismatch' });
225
+
226
+ // Validate spec before approval
227
+ if (stratumClient && typeof stratumClient.validate === 'function') {
228
+ try {
229
+ const result = await stratumClient.validate(draft.spec);
230
+ if (result?.valid === false) {
231
+ return res.status(422).json({ error: `Spec validation failed: ${result.error || 'unknown error'}` });
232
+ }
233
+ } catch (err) {
234
+ // Stratum unavailable at runtime — fall back to YAML parse check
235
+ try {
236
+ YAML.parse(draft.spec);
237
+ } catch (parseErr) {
238
+ return res.status(422).json({ error: `Invalid YAML: ${parseErr.message}` });
239
+ }
240
+ }
241
+ } else {
242
+ // No stratum client — basic YAML parse check
243
+ try {
244
+ YAML.parse(draft.spec);
245
+ } catch (parseErr) {
246
+ return res.status(422).json({ error: `Invalid YAML: ${parseErr.message}` });
247
+ }
248
+ }
249
+
250
+ // Write approved spec to approved-specs/
251
+ const approvedDir = join(dataDir, 'approved-specs');
252
+ mkdirSync(approvedDir, { recursive: true });
253
+ const specPath = join(approvedDir, `${draftId}.yaml`);
254
+ writeFileSync(specPath, draft.spec);
255
+
256
+ // Only delete draft AFTER spec file written
257
+ clearDraft(dataDir);
258
+
259
+ broadcastMessage({
260
+ type: 'pipelineDraftResolved',
261
+ draftId,
262
+ outcome: 'approved',
263
+ specPath,
264
+ });
265
+
266
+ res.json({ ok: true, specPath });
267
+ });
268
+
269
+ // POST /api/pipeline/draft/reject
270
+ app.post('/api/pipeline/draft/reject', (req, res) => {
271
+ const { draftId } = req.body || {};
272
+ const dataDir = getDataDir();
273
+ const draft = loadDraft(dataDir);
274
+
275
+ if (!draft) return res.status(404).json({ error: 'No draft to reject' });
276
+ if (draft.draftId !== draftId) return res.status(409).json({ error: 'Draft ID mismatch' });
277
+
278
+ clearDraft(dataDir);
279
+
280
+ broadcastMessage({
281
+ type: 'pipelineDraftResolved',
282
+ draftId,
283
+ outcome: 'rejected',
284
+ });
285
+
286
+ res.json({ ok: true });
287
+ });
288
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Policy Evaluator — determines gate behavior for phase transitions.
3
+ *
4
+ * Pure function, no state, no side effects. Reads from the merged
5
+ * settings object (SettingsStore.get() shape).
6
+ *
7
+ * Policy modes:
8
+ * gate — human approval required (default)
9
+ * flag — auto-approve, emit stream event for audit
10
+ * skip — silent pass-through, no gate record
11
+ */
12
+
13
+ /**
14
+ * Evaluate the policy for a phase transition.
15
+ *
16
+ * @param {object} settings — merged settings (must have .policies object)
17
+ * @param {string} stepId — the Stratum step ID
18
+ * @param {object} [opts]
19
+ * @param {string} [opts.toPhase] — target phase (takes precedence over stepId for lookup)
20
+ * @param {string} [opts.fromPhase] — source phase (informational, not used for lookup)
21
+ * @returns {{ mode: 'gate'|'flag'|'skip', reason: string }}
22
+ */
23
+ export function evaluatePolicy(settings, stepId, opts = {}) {
24
+ const phase = opts.toPhase ?? stepId;
25
+ const mode = settings?.policies?.[phase] ?? null;
26
+
27
+ if (mode === null || mode === undefined) {
28
+ return { mode: 'gate', reason: `no policy for '${phase}', defaulting to gate` };
29
+ }
30
+
31
+ if (mode !== 'gate' && mode !== 'flag' && mode !== 'skip') {
32
+ return { mode: 'gate', reason: `unknown policy '${mode}' for '${phase}', defaulting to gate` };
33
+ }
34
+
35
+ return { mode, reason: `phase '${phase}' policy is '${mode}'` };
36
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * project-root.js — Resolve COMPOSE_HOME and TARGET_ROOT.
3
+ *
4
+ * COMPOSE_HOME: where Compose's own code lives (server/, node_modules/, etc.)
5
+ * TARGET_ROOT: the project being developed. Resolved by:
6
+ * 1. COMPOSE_TARGET env var (explicit override)
7
+ * 2. Walk up from cwd looking for .compose/, .stratum.yaml, or .git
8
+ * 3. Fall back to cwd
9
+ *
10
+ * All project paths are accessed via getTargetRoot() / getDataDir() so they
11
+ * update when switchProject() is called at runtime.
12
+ */
13
+
14
+ import path from 'node:path';
15
+ import fs from 'node:fs';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { findProjectRoot } from './find-root.js';
18
+
19
+ export { findProjectRoot } from './find-root.js';
20
+
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+
23
+ /** Where Compose's own code lives. Never changes. */
24
+ export const COMPOSE_HOME = path.resolve(__dirname, '..');
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Mutable project binding
28
+ // ---------------------------------------------------------------------------
29
+
30
+ let _targetRoot = (() => {
31
+ if (process.env.COMPOSE_TARGET) {
32
+ const resolved = path.resolve(process.env.COMPOSE_TARGET);
33
+ if (!fs.existsSync(resolved)) {
34
+ console.error(`[project-root] COMPOSE_TARGET=${process.env.COMPOSE_TARGET} does not exist`);
35
+ process.exit(1);
36
+ }
37
+ return resolved;
38
+ }
39
+ return findProjectRoot(process.cwd()) || process.cwd();
40
+ })();
41
+
42
+ let _dataDir = path.join(_targetRoot, '.compose', 'data');
43
+ let _configCache = null;
44
+
45
+ /** The target project being developed. */
46
+ export function getTargetRoot() { return _targetRoot; }
47
+
48
+ /** Data directory for Compose state. Lives in the target project. */
49
+ export function getDataDir() { return _dataDir; }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Switch project at runtime
53
+ // ---------------------------------------------------------------------------
54
+
55
+ const _switchListeners = [];
56
+
57
+ /**
58
+ * Register a callback for project switches.
59
+ * @param {(targetRoot: string, dataDir: string) => void} fn
60
+ */
61
+ export function onProjectSwitch(fn) {
62
+ _switchListeners.push(fn);
63
+ }
64
+
65
+ /**
66
+ * Switch to a different project directory.
67
+ * @param {string} newRoot — absolute path to the new project
68
+ * @returns {{ targetRoot: string, dataDir: string }}
69
+ */
70
+ export function switchProject(newRoot) {
71
+ const resolved = path.resolve(newRoot);
72
+ if (!fs.existsSync(resolved)) {
73
+ throw new Error(`Project path does not exist: ${resolved}`);
74
+ }
75
+ _targetRoot = resolved;
76
+ _dataDir = path.join(resolved, '.compose', 'data');
77
+ _configCache = null;
78
+ fs.mkdirSync(_dataDir, { recursive: true });
79
+ console.log(`[project-root] Switched to: ${resolved}`);
80
+ for (const fn of _switchListeners) {
81
+ try { fn(_targetRoot, _dataDir); } catch (e) { console.error('[project-root] Switch listener error:', e.message); }
82
+ }
83
+ return { targetRoot: _targetRoot, dataDir: _dataDir };
84
+ }
85
+
86
+ /** Ensure the data directory exists. */
87
+ export function ensureDataDir() {
88
+ fs.mkdirSync(getDataDir(), { recursive: true });
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Project config
93
+ // ---------------------------------------------------------------------------
94
+
95
+ const DEFAULT_CONFIG = Object.freeze({
96
+ version: 1,
97
+ capabilities: Object.freeze({ stratum: true, lifecycle: true }),
98
+ paths: Object.freeze({ docs: 'docs', features: 'docs/features', journal: 'docs/journal' }),
99
+ });
100
+
101
+ function cloneConfig(obj) {
102
+ return JSON.parse(JSON.stringify(obj));
103
+ }
104
+
105
+ export function loadProjectConfig() {
106
+ if (_configCache) return cloneConfig(_configCache);
107
+ const configPath = path.join(getTargetRoot(), '.compose', 'compose.json');
108
+ try {
109
+ _configCache = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
110
+ return cloneConfig(_configCache);
111
+ } catch {
112
+ _configCache = DEFAULT_CONFIG;
113
+ return cloneConfig(DEFAULT_CONFIG);
114
+ }
115
+ }
116
+
117
+ export function resolveProjectPath(key) {
118
+ const config = loadProjectConfig();
119
+ const rel = config.paths?.[key];
120
+ if (!rel) return path.join(getTargetRoot(), DEFAULT_CONFIG.paths[key] || key);
121
+ return path.join(getTargetRoot(), rel);
122
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared guard for sensitive local endpoints.
3
+ *
4
+ * Usage:
5
+ * 1) In normal dev flow, server/supervisor.js auto-generates COMPOSE_API_TOKEN.
6
+ * 2) If running servers directly, set COMPOSE_API_TOKEN in the environment.
7
+ * 3) Send header: x-compose-token: <COMPOSE_API_TOKEN>
8
+ */
9
+ export function requireSensitiveToken(req, res, next) {
10
+ const expected = process.env.COMPOSE_API_TOKEN;
11
+ if (!expected) {
12
+ return res.status(503).json({
13
+ error: 'Sensitive endpoint disabled: missing COMPOSE_API_TOKEN (run via supervisor or set it manually)',
14
+ });
15
+ }
16
+
17
+ const provided = req.get('x-compose-token');
18
+ if (!provided || provided !== expected) {
19
+ return res.status(401).json({ error: 'Unauthorized' });
20
+ }
21
+
22
+ next();
23
+ }