@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,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings-routes.js — Settings REST API.
|
|
3
|
+
*
|
|
4
|
+
* Routes:
|
|
5
|
+
* GET /api/settings — current merged settings
|
|
6
|
+
* PATCH /api/settings — partial update
|
|
7
|
+
* POST /api/settings/reset — reset all or a section
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {object} app — Express app
|
|
12
|
+
* @param {{ settingsStore: object, broadcastMessage: function }} deps
|
|
13
|
+
*/
|
|
14
|
+
export function attachSettingsRoutes(app, { settingsStore, broadcastMessage }) {
|
|
15
|
+
app.get('/api/settings', (_req, res) => {
|
|
16
|
+
res.json(settingsStore.get());
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
app.patch('/api/settings', (req, res) => {
|
|
20
|
+
try {
|
|
21
|
+
const updated = settingsStore.update(req.body);
|
|
22
|
+
broadcastMessage({ type: 'settingsUpdated', settings: updated });
|
|
23
|
+
res.json(updated);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
res.status(400).json({ error: err.message });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
app.post('/api/settings/reset', (req, res) => {
|
|
30
|
+
const section = req.body?.section || undefined;
|
|
31
|
+
const updated = settingsStore.reset(section);
|
|
32
|
+
broadcastMessage({ type: 'settingsUpdated', settings: updated });
|
|
33
|
+
res.json(updated);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Store — JSON-file-backed storage for user preferences.
|
|
3
|
+
* Loads from disk on startup, saves after every mutation.
|
|
4
|
+
* Returns merged view: contract defaults + user overrides.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { getDataDir as getDefaultDataDir } from './project-root.js';
|
|
10
|
+
|
|
11
|
+
const VALID_VIEWS = ['graph', 'tree', 'pipeline', 'gates', 'docs', 'design', 'sessions', 'settings'];
|
|
12
|
+
const VALID_THEMES = ['light', 'dark', 'system'];
|
|
13
|
+
const VALID_THINKING_MODES = ['adaptive', 'off'];
|
|
14
|
+
const VALID_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
15
|
+
|
|
16
|
+
export class SettingsStore {
|
|
17
|
+
constructor(dataDir, contract) {
|
|
18
|
+
const dir = dataDir || getDefaultDataDir();
|
|
19
|
+
this._file = path.join(dir, 'settings.json');
|
|
20
|
+
this._contract = contract;
|
|
21
|
+
this._userSettings = {};
|
|
22
|
+
this._load();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_load() {
|
|
26
|
+
try {
|
|
27
|
+
const raw = fs.readFileSync(this._file, 'utf-8');
|
|
28
|
+
this._userSettings = JSON.parse(raw);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err.code !== 'ENOENT') {
|
|
31
|
+
console.error('[settings] Failed to load settings, using defaults:', err.message);
|
|
32
|
+
}
|
|
33
|
+
this._userSettings = {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_save() {
|
|
38
|
+
try {
|
|
39
|
+
fs.mkdirSync(path.dirname(this._file), { recursive: true });
|
|
40
|
+
fs.writeFileSync(this._file, JSON.stringify(this._userSettings, null, 2), 'utf-8');
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error('[settings] Failed to save settings:', err.message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_defaults() {
|
|
47
|
+
return {
|
|
48
|
+
policies: Object.fromEntries(
|
|
49
|
+
this._contract.phases.map(p => [p.id, p.defaultPolicy])
|
|
50
|
+
),
|
|
51
|
+
iterations: { ...this._contract.iterationDefaults },
|
|
52
|
+
models: {
|
|
53
|
+
interactive: 'claude-sonnet-4-6',
|
|
54
|
+
agentRun: process.env.CLAUDE_MODEL || 'claude-sonnet-4-6',
|
|
55
|
+
summarizer: process.env.SUMMARIZER_MODEL || 'haiku',
|
|
56
|
+
},
|
|
57
|
+
ui: { theme: 'system', defaultView: 'graph' },
|
|
58
|
+
// COMP-CAPS-ENFORCE: runtime capability enforcement policy
|
|
59
|
+
capabilities: { enforcement: 'log' },
|
|
60
|
+
// COMP-HEALTH: quantified quality score settings
|
|
61
|
+
health: {
|
|
62
|
+
enabled: true,
|
|
63
|
+
gate_threshold: null, // null = off, number = min_score required
|
|
64
|
+
weights: {}, // dimension weight overrides (must sum to 1.0 ± 0.01)
|
|
65
|
+
},
|
|
66
|
+
// Claude thinking/effort controls. mode='tier' inherits the tier default
|
|
67
|
+
// (critical → adaptive+xhigh, standard → adaptive+high, fast → off).
|
|
68
|
+
// effort=null likewise inherits the tier default.
|
|
69
|
+
thinking: {
|
|
70
|
+
mode: 'tier', // 'tier' | 'adaptive' | 'off'
|
|
71
|
+
effort: null, // null (tier default) | 'low' | 'medium' | 'high' | 'xhigh' | 'max'
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Returns merged defaults + user overrides. */
|
|
77
|
+
get() {
|
|
78
|
+
const defaults = this._defaults();
|
|
79
|
+
const user = this._userSettings;
|
|
80
|
+
return {
|
|
81
|
+
policies: { ...defaults.policies, ...user.policies },
|
|
82
|
+
iterations: {
|
|
83
|
+
review: { ...defaults.iterations.review, ...user.iterations?.review },
|
|
84
|
+
coverage: { ...defaults.iterations.coverage, ...user.iterations?.coverage },
|
|
85
|
+
},
|
|
86
|
+
models: { ...defaults.models, ...user.models },
|
|
87
|
+
ui: { ...defaults.ui, ...user.ui },
|
|
88
|
+
capabilities: { ...defaults.capabilities, ...user.capabilities },
|
|
89
|
+
// COMP-HEALTH: merge health settings
|
|
90
|
+
health: {
|
|
91
|
+
...defaults.health,
|
|
92
|
+
...user.health,
|
|
93
|
+
weights: { ...defaults.health.weights, ...user.health?.weights },
|
|
94
|
+
},
|
|
95
|
+
thinking: { ...defaults.thinking, ...user.thinking },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Validate and apply a partial settings update. */
|
|
100
|
+
update(patch) {
|
|
101
|
+
this._validate(patch);
|
|
102
|
+
// Deep merge into user settings
|
|
103
|
+
for (const section of ['policies', 'iterations', 'models', 'ui', 'capabilities', 'health', 'thinking']) {
|
|
104
|
+
if (patch[section]) {
|
|
105
|
+
if (!this._userSettings[section]) this._userSettings[section] = {};
|
|
106
|
+
if (section === 'iterations') {
|
|
107
|
+
for (const [key, val] of Object.entries(patch.iterations)) {
|
|
108
|
+
this._userSettings.iterations[key] = { ...this._userSettings.iterations[key], ...val };
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
Object.assign(this._userSettings[section], patch[section]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
this._save();
|
|
116
|
+
return this.get();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Reset user overrides. If section given, reset only that section. */
|
|
120
|
+
reset(section) {
|
|
121
|
+
if (section) {
|
|
122
|
+
delete this._userSettings[section];
|
|
123
|
+
} else {
|
|
124
|
+
this._userSettings = {};
|
|
125
|
+
}
|
|
126
|
+
this._save();
|
|
127
|
+
return this.get();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_validate(patch) {
|
|
131
|
+
const validModes = new Set(this._contract.policyModes);
|
|
132
|
+
|
|
133
|
+
// Reject unknown top-level keys
|
|
134
|
+
for (const key of Object.keys(patch)) {
|
|
135
|
+
if (!['policies', 'iterations', 'models', 'ui', 'capabilities', 'health', 'thinking'].includes(key)) {
|
|
136
|
+
throw new Error(`Unknown settings section: ${key}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (patch.policies) {
|
|
141
|
+
for (const [phase, mode] of Object.entries(patch.policies)) {
|
|
142
|
+
if (mode !== null && !validModes.has(mode)) {
|
|
143
|
+
throw new Error(`Invalid policy mode for ${phase}: ${mode}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (patch.iterations) {
|
|
149
|
+
for (const [type, config] of Object.entries(patch.iterations)) {
|
|
150
|
+
if (config.maxIterations !== undefined) {
|
|
151
|
+
const n = config.maxIterations;
|
|
152
|
+
if (!Number.isInteger(n) || n < 1 || n > 100) {
|
|
153
|
+
throw new Error(`Invalid maxIterations for ${type}: must be integer 1-100`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (config.timeout !== undefined) {
|
|
157
|
+
const t = config.timeout;
|
|
158
|
+
if (!Number.isInteger(t) || t < 1 || t > 120) {
|
|
159
|
+
throw new Error(`Invalid timeout for ${type}: must be integer 1-120 (minutes)`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (config.maxTotal !== undefined) {
|
|
163
|
+
const m = config.maxTotal;
|
|
164
|
+
if (!Number.isInteger(m) || m < 1 || m > 200) {
|
|
165
|
+
throw new Error(`Invalid maxTotal for ${type}: must be integer 1-200`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (patch.models) {
|
|
172
|
+
for (const [key, val] of Object.entries(patch.models)) {
|
|
173
|
+
if (typeof val !== 'string' || val.length === 0) {
|
|
174
|
+
throw new Error(`Invalid model for ${key}: must be non-empty string`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (patch.ui) {
|
|
180
|
+
if (patch.ui.theme !== undefined && !VALID_THEMES.includes(patch.ui.theme)) {
|
|
181
|
+
throw new Error(`Invalid theme: ${patch.ui.theme} (must be ${VALID_THEMES.join(', ')})`);
|
|
182
|
+
}
|
|
183
|
+
if (patch.ui.defaultView !== undefined && !VALID_VIEWS.includes(patch.ui.defaultView)) {
|
|
184
|
+
throw new Error(`Invalid defaultView: ${patch.ui.defaultView} (must be ${VALID_VIEWS.join(', ')})`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (patch.capabilities) {
|
|
189
|
+
if (patch.capabilities.enforcement !== undefined) {
|
|
190
|
+
const VALID_ENFORCEMENT = ['log', 'block'];
|
|
191
|
+
if (!VALID_ENFORCEMENT.includes(patch.capabilities.enforcement)) {
|
|
192
|
+
throw new Error(`Invalid enforcement: ${patch.capabilities.enforcement} (must be ${VALID_ENFORCEMENT.join(', ')})`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (patch.thinking) {
|
|
198
|
+
if (patch.thinking.mode !== undefined) {
|
|
199
|
+
const m = patch.thinking.mode;
|
|
200
|
+
if (m !== 'tier' && !VALID_THINKING_MODES.includes(m)) {
|
|
201
|
+
throw new Error(`Invalid thinking.mode: ${m} (must be tier, ${VALID_THINKING_MODES.join(', ')})`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (patch.thinking.effort !== undefined && patch.thinking.effort !== null) {
|
|
205
|
+
if (!VALID_EFFORT_LEVELS.includes(patch.thinking.effort)) {
|
|
206
|
+
throw new Error(`Invalid thinking.effort: ${patch.thinking.effort} (must be null or ${VALID_EFFORT_LEVELS.join(', ')})`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (patch.health) {
|
|
212
|
+
// Validate gate_threshold: must be null or 0-100
|
|
213
|
+
if (patch.health.gate_threshold !== undefined && patch.health.gate_threshold !== null) {
|
|
214
|
+
const t = patch.health.gate_threshold;
|
|
215
|
+
if (typeof t !== 'number' || t < 0 || t > 100) {
|
|
216
|
+
throw new Error(`Invalid health.gate_threshold: must be null or a number 0-100`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Validate weights: each value must be a number; sum must be 1.0 ± 0.01
|
|
220
|
+
if (patch.health.weights !== undefined) {
|
|
221
|
+
const vals = Object.values(patch.health.weights);
|
|
222
|
+
if (vals.some(v => typeof v !== 'number' || v < 0)) {
|
|
223
|
+
throw new Error('Invalid health.weights: all values must be non-negative numbers');
|
|
224
|
+
}
|
|
225
|
+
if (vals.length > 0) {
|
|
226
|
+
const sum = vals.reduce((s, v) => s + v, 0);
|
|
227
|
+
if (Math.abs(sum - 1.0) > 0.01) {
|
|
228
|
+
throw new Error(`Invalid health.weights: values must sum to 1.0 ± 0.01 (got ${sum.toFixed(4)})`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stratum-api.js — Express router for Stratum pipeline monitor + gate UI.
|
|
3
|
+
*
|
|
4
|
+
* Pure transport adapter: calls stratum-client, maps results to HTTP.
|
|
5
|
+
* Zero domain logic — all gate semantics live in stratum.
|
|
6
|
+
*
|
|
7
|
+
* Routes:
|
|
8
|
+
* GET /api/stratum/flows
|
|
9
|
+
* GET /api/stratum/flows/:flowId
|
|
10
|
+
* GET /api/stratum/gates
|
|
11
|
+
* POST /api/stratum/gates/:flowId/:stepId/approve
|
|
12
|
+
* POST /api/stratum/gates/:flowId/:stepId/reject
|
|
13
|
+
* POST /api/stratum/gates/:flowId/:stepId/revise
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Router } from 'express';
|
|
17
|
+
import * as _defaultClient from './stratum-client.js';
|
|
18
|
+
|
|
19
|
+
/** Wrap an async route handler so rejections call next(err) instead of going unhandled. */
|
|
20
|
+
function ar(fn) {
|
|
21
|
+
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} [client] — override stratum-client (for tests only)
|
|
26
|
+
*/
|
|
27
|
+
export function createStratumRouter(client) {
|
|
28
|
+
const stratum = client ?? _defaultClient;
|
|
29
|
+
const router = Router();
|
|
30
|
+
|
|
31
|
+
// -- Read routes -----------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
router.get('/flows', ar(async (_req, res) => {
|
|
34
|
+
const result = await stratum.queryFlows();
|
|
35
|
+
if (result?.error) return res.status(_errorStatus(result.error.code)).json(result);
|
|
36
|
+
res.json(result);
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
router.get('/flows/:flowId', ar(async (req, res) => {
|
|
40
|
+
const result = await stratum.queryFlow(req.params.flowId);
|
|
41
|
+
if (result?.error) return res.status(_errorStatus(result.error.code)).json(result);
|
|
42
|
+
res.json(result);
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
router.get('/gates', ar(async (_req, res) => {
|
|
46
|
+
const result = await stratum.queryGates();
|
|
47
|
+
if (result?.error) return res.status(_errorStatus(result.error.code)).json(result);
|
|
48
|
+
res.json(result);
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// -- Gate mutation routes --------------------------------------------------
|
|
52
|
+
|
|
53
|
+
router.post('/gates/:flowId/:stepId/approve', ar(async (req, res) => {
|
|
54
|
+
const { flowId, stepId } = req.params;
|
|
55
|
+
const { note, resolvedBy } = req.body || {};
|
|
56
|
+
const result = await stratum.gateApprove(flowId, stepId, note, resolvedBy);
|
|
57
|
+
res.status(_mutationStatus(result)).json(result);
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
router.post('/gates/:flowId/:stepId/reject', ar(async (req, res) => {
|
|
61
|
+
const { flowId, stepId } = req.params;
|
|
62
|
+
const { note, resolvedBy } = req.body || {};
|
|
63
|
+
const result = await stratum.gateReject(flowId, stepId, note, resolvedBy);
|
|
64
|
+
res.status(_mutationStatus(result)).json(result);
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
router.post('/gates/:flowId/:stepId/revise', ar(async (req, res) => {
|
|
68
|
+
const { flowId, stepId } = req.params;
|
|
69
|
+
const { note, resolvedBy } = req.body || {};
|
|
70
|
+
const result = await stratum.gateRevise(flowId, stepId, note, resolvedBy);
|
|
71
|
+
res.status(_mutationStatus(result)).json(result);
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
// -- Error middleware (catches ENOENT, unexpected throws, etc.) -------------
|
|
75
|
+
// eslint-disable-next-line no-unused-vars
|
|
76
|
+
router.use((err, _req, res, _next) => {
|
|
77
|
+
console.error('[stratum-api] unhandled error:', err.message);
|
|
78
|
+
res.status(503).json({ error: { code: 'UNAVAILABLE', message: err.message, detail: '' } });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return router;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Status code mapping — error codes → HTTP status
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
function _errorStatus(code) {
|
|
89
|
+
switch (code) {
|
|
90
|
+
case 'NOT_FOUND': return 404;
|
|
91
|
+
case 'TIMEOUT': return 504;
|
|
92
|
+
case 'INVALID': return 400;
|
|
93
|
+
default: return 500;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _mutationStatus(result) {
|
|
98
|
+
if (result?.conflict) return 409;
|
|
99
|
+
if (result?.error) return _errorStatus(result.error.code);
|
|
100
|
+
if (result?.ok === true) return 200;
|
|
101
|
+
return 500;
|
|
102
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stratum-client.js — Single adapter for all stratum-mcp subprocess calls.
|
|
3
|
+
*
|
|
4
|
+
* This is the ONLY module in compose that spawns stratum-mcp processes.
|
|
5
|
+
* All query and mutation calls go through the exported functions below.
|
|
6
|
+
* No other file may call execFile/spawn with 'stratum-mcp' as the command.
|
|
7
|
+
*
|
|
8
|
+
* Contract:
|
|
9
|
+
* - Query calls: 5s timeout, 1 retry on timeout, no retry on error
|
|
10
|
+
* - Mutation calls: 10s timeout, no retry (mutations are not idempotent to retry)
|
|
11
|
+
* - Exit 0 → parse stdout as JSON, return result
|
|
12
|
+
* - Exit 2 → conflict (idempotency), return { conflict: true, ... }
|
|
13
|
+
* - Non-zero → log stderr internally, return { error: { code, message, detail } }
|
|
14
|
+
* - stderr is NEVER forwarded to callers
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { execFile as _execFileDefault } from 'node:child_process';
|
|
18
|
+
|
|
19
|
+
const STRATUM_BIN = 'stratum-mcp';
|
|
20
|
+
|
|
21
|
+
// Injected executor — replaced by tests only. Production code never calls this setter.
|
|
22
|
+
let _execFile = _execFileDefault;
|
|
23
|
+
export function _testOnly_setExecFile(fn) { _execFile = fn; }
|
|
24
|
+
const QUERY_TIMEOUT_MS = 5_000;
|
|
25
|
+
const MUTATION_TIMEOUT_MS = 10_000;
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Core subprocess runner
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Spawn stratum-mcp with args. Returns a Promise resolving to { stdout, code }.
|
|
33
|
+
* Rejects only on spawn failure (binary not found).
|
|
34
|
+
*
|
|
35
|
+
* @param {string[]} args
|
|
36
|
+
* @param {number} timeoutMs
|
|
37
|
+
* @returns {Promise<{ stdout: string, stderr: string, code: number }>}
|
|
38
|
+
*/
|
|
39
|
+
function spawnStratum(args, timeoutMs) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
let stdout = '';
|
|
42
|
+
let stderr = '';
|
|
43
|
+
|
|
44
|
+
const proc = _execFile(STRATUM_BIN, args, { timeout: timeoutMs }, (err, out, err2) => {
|
|
45
|
+
stdout = out || '';
|
|
46
|
+
stderr = err2 || '';
|
|
47
|
+
const code = err?.code === 'ETIMEDOUT' ? -1
|
|
48
|
+
: (typeof err?.code === 'number' ? err.code : 0);
|
|
49
|
+
resolve({ stdout, stderr, code });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
proc.on('error', (err) => {
|
|
53
|
+
if (err.code === 'ENOENT') {
|
|
54
|
+
reject(new Error(`stratum-mcp not found. Install with: pip install stratum-mcp`));
|
|
55
|
+
} else {
|
|
56
|
+
reject(err);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Run a query command (read-only). Retries once on timeout.
|
|
64
|
+
*
|
|
65
|
+
* @returns {Promise<any>} parsed JSON result, or throws StratumError
|
|
66
|
+
*/
|
|
67
|
+
async function runQuery(args) {
|
|
68
|
+
let result = await spawnStratum(args, QUERY_TIMEOUT_MS);
|
|
69
|
+
|
|
70
|
+
if (result.code === -1) {
|
|
71
|
+
// Retry once on timeout
|
|
72
|
+
result = await spawnStratum(args, QUERY_TIMEOUT_MS);
|
|
73
|
+
if (result.code === -1) {
|
|
74
|
+
return { error: { code: 'TIMEOUT', message: 'stratum-mcp query timed out', detail: '' } };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (result.code !== 0) {
|
|
79
|
+
console.error('[stratum-client] query error stderr:', result.stderr);
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(result.stdout);
|
|
82
|
+
} catch {
|
|
83
|
+
return { error: { code: 'UNKNOWN', message: 'stratum-mcp query failed', detail: '' } };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(result.stdout);
|
|
89
|
+
} catch {
|
|
90
|
+
return { error: { code: 'PARSE_ERROR', message: 'stratum-mcp returned invalid JSON', detail: '' } };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Run a mutation command (gate approve/reject/revise). No retry.
|
|
96
|
+
*
|
|
97
|
+
* @returns {Promise<any>} parsed JSON result, or { conflict }, or { error }
|
|
98
|
+
*/
|
|
99
|
+
async function runMutation(args) {
|
|
100
|
+
const result = await spawnStratum(args, MUTATION_TIMEOUT_MS);
|
|
101
|
+
|
|
102
|
+
if (result.code === -1) {
|
|
103
|
+
return { error: { code: 'TIMEOUT', message: 'stratum-mcp gate timed out', detail: '' } };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (result.code === 2) {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(result.stdout); // { conflict: true, ... }
|
|
109
|
+
} catch {
|
|
110
|
+
return { conflict: true, detail: '' };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (result.code !== 0) {
|
|
115
|
+
console.error('[stratum-client] mutation error stderr:', result.stderr);
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(result.stdout);
|
|
118
|
+
} catch {
|
|
119
|
+
return { error: { code: 'UNKNOWN', message: 'stratum-mcp gate failed', detail: '' } };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(result.stdout);
|
|
125
|
+
} catch {
|
|
126
|
+
return { error: { code: 'PARSE_ERROR', message: 'stratum-mcp returned invalid JSON', detail: '' } };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Public API
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/** List all persisted flows. @returns {Promise<FlowSummary[]|ErrorResult>} */
|
|
135
|
+
export async function queryFlows() {
|
|
136
|
+
return runQuery(['query', 'flows']);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Full state for a single flow. @returns {Promise<FlowState|ErrorResult>} */
|
|
140
|
+
export async function queryFlow(flowId) {
|
|
141
|
+
return runQuery(['query', 'flow', flowId]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** List all pending gate steps. @returns {Promise<PendingGate[]|ErrorResult>} */
|
|
145
|
+
export async function queryGates() {
|
|
146
|
+
return runQuery(['query', 'gates']);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Approve a gate step. Stratum is the mutation authority.
|
|
151
|
+
* @param {string} flowId
|
|
152
|
+
* @param {string} stepId
|
|
153
|
+
* @param {string} [note]
|
|
154
|
+
* @param {'human'|'agent'|'system'} [resolvedBy]
|
|
155
|
+
* @returns {Promise<GateMutationResult|ConflictResult|ErrorResult>}
|
|
156
|
+
*/
|
|
157
|
+
export async function gateApprove(flowId, stepId, note = '', resolvedBy = 'human') {
|
|
158
|
+
const args = ['gate', 'approve', flowId, stepId];
|
|
159
|
+
if (note) args.push('--note', note);
|
|
160
|
+
if (resolvedBy !== 'human') args.push('--resolved-by', resolvedBy);
|
|
161
|
+
return runMutation(args);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Reject (kill) a gate step.
|
|
166
|
+
* @param {string} flowId
|
|
167
|
+
* @param {string} stepId
|
|
168
|
+
* @param {string} [note]
|
|
169
|
+
* @param {'human'|'agent'|'system'} [resolvedBy]
|
|
170
|
+
* @returns {Promise<GateMutationResult|ConflictResult|ErrorResult>}
|
|
171
|
+
*/
|
|
172
|
+
export async function gateReject(flowId, stepId, note = '', resolvedBy = 'human') {
|
|
173
|
+
const args = ['gate', 'reject', flowId, stepId];
|
|
174
|
+
if (note) args.push('--note', note);
|
|
175
|
+
if (resolvedBy !== 'human') args.push('--resolved-by', resolvedBy);
|
|
176
|
+
return runMutation(args);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Send a gate step back for revision.
|
|
181
|
+
* @param {string} flowId
|
|
182
|
+
* @param {string} stepId
|
|
183
|
+
* @param {string} [note]
|
|
184
|
+
* @param {'human'|'agent'|'system'} [resolvedBy]
|
|
185
|
+
* @returns {Promise<GateMutationResult|ConflictResult|ErrorResult>}
|
|
186
|
+
*/
|
|
187
|
+
export async function gateRevise(flowId, stepId, note = '', resolvedBy = 'human') {
|
|
188
|
+
const args = ['gate', 'revise', flowId, stepId];
|
|
189
|
+
if (note) args.push('--note', note);
|
|
190
|
+
if (resolvedBy !== 'human') args.push('--resolved-by', resolvedBy);
|
|
191
|
+
return runMutation(args);
|
|
192
|
+
}
|