@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,479 @@
1
+ /**
2
+ * compose-mcp-tools.js — Tool implementations for the Compose MCP server.
3
+ *
4
+ * Reads directly from disk (no HTTP, no daemon dependency) so the MCP server
5
+ * works even when the Compose server is not running.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import http from 'node:http';
10
+ import path from 'node:path';
11
+ import { ArtifactManager, ARTIFACT_SCHEMAS } from './artifact-manager.js';
12
+ import { getTargetRoot, getDataDir, resolveProjectPath } from './project-root.js';
13
+ import { ClaudeSDKConnector } from './connectors/claude-sdk-connector.js';
14
+ import { CodexConnector } from './connectors/codex-connector.js';
15
+
16
+ export const PROJECT_ROOT = getTargetRoot();
17
+ export const VISION_FILE = path.join(getDataDir(), 'vision-state.json');
18
+ export const SESSIONS_FILE = path.join(getDataDir(), 'sessions.json');
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Data access
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export function loadVisionState() {
25
+ try {
26
+ const raw = fs.readFileSync(VISION_FILE, 'utf-8');
27
+ const state = JSON.parse(raw);
28
+ if (Array.isArray(state.gates)) {
29
+ const seen = new Map();
30
+ for (const g of state.gates) seen.set(g.id, g);
31
+ state.gates = Array.from(seen.values());
32
+ }
33
+ return state;
34
+ } catch {
35
+ return { items: [], connections: [], gates: [] };
36
+ }
37
+ }
38
+
39
+ export function loadSessions() {
40
+ try {
41
+ const raw = fs.readFileSync(SESSIONS_FILE, 'utf-8');
42
+ const sessions = JSON.parse(raw);
43
+ return Array.isArray(sessions) ? sessions : [];
44
+ } catch {
45
+ return [];
46
+ }
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Tool implementations
51
+ // ---------------------------------------------------------------------------
52
+
53
+ export function toolGetVisionItems({ phase, status, type, keyword, limit = 30 }) {
54
+ const { items } = loadVisionState();
55
+
56
+ let results = items;
57
+ if (phase) results = results.filter(i => i.phase === phase);
58
+ if (status) {
59
+ const statuses = status.split(',').map(s => s.trim());
60
+ results = results.filter(i => statuses.includes(i.status));
61
+ }
62
+ if (type) results = results.filter(i => i.type === type);
63
+ if (keyword) {
64
+ const kw = keyword.toLowerCase();
65
+ results = results.filter(i =>
66
+ i.title?.toLowerCase().includes(kw) ||
67
+ i.description?.toLowerCase().includes(kw)
68
+ );
69
+ }
70
+
71
+ const sliced = results.slice(0, limit);
72
+ return {
73
+ count: results.length,
74
+ returned: sliced.length,
75
+ items: sliced.map(i => ({
76
+ id: i.id,
77
+ title: i.title,
78
+ type: i.type,
79
+ phase: i.phase,
80
+ status: i.status,
81
+ confidence: i.confidence ?? null,
82
+ description: i.description ?? null,
83
+ })),
84
+ };
85
+ }
86
+
87
+ export function toolGetItemDetail({ id }) {
88
+ const { items, connections } = loadVisionState();
89
+
90
+ const item = items.find(i => i.id === id || i.semanticId === id || i.slug === id);
91
+ if (!item) return { error: `Item not found: ${id}` };
92
+
93
+ const resolvedId = item.id;
94
+ const related = connections.filter(c => c.fromId === resolvedId || c.toId === resolvedId);
95
+ const connectionDetails = related.map(c => {
96
+ const other = items.find(i => i.id === (c.fromId === resolvedId ? c.toId : c.fromId));
97
+ return {
98
+ direction: c.fromId === resolvedId ? 'outgoing' : 'incoming',
99
+ type: c.type,
100
+ otherId: other?.id,
101
+ otherTitle: other?.title ?? '(unknown)',
102
+ otherStatus: other?.status,
103
+ };
104
+ });
105
+
106
+ return { ...item, connections: connectionDetails };
107
+ }
108
+
109
+ export function toolGetPhasesSummary({ phase }) {
110
+ const { items } = loadVisionState();
111
+
112
+ const scoped = phase ? items.filter(i => i.phase === phase) : items;
113
+ const byStatus = {};
114
+ const byType = {};
115
+ for (const item of scoped) {
116
+ const s = item.status || 'unknown';
117
+ byStatus[s] = (byStatus[s] || 0) + 1;
118
+ const t = item.type || 'unknown';
119
+ byType[t] = (byType[t] || 0) + 1;
120
+ }
121
+
122
+ const confidences = scoped.map(i => i.confidence).filter(c => typeof c === 'number');
123
+ const avgConfidence = confidences.length
124
+ ? Math.round((confidences.reduce((a, b) => a + b, 0) / confidences.length) * 100) / 100
125
+ : null;
126
+
127
+ return { phase: phase || 'all', total: scoped.length, byStatus, byType, avgConfidence };
128
+ }
129
+
130
+ export function toolGetBlockedItems() {
131
+ const { items, connections } = loadVisionState();
132
+ const itemMap = new Map(items.map(i => [i.id, i]));
133
+
134
+ const blocked = [];
135
+ for (const conn of connections) {
136
+ if (conn.type === 'blocks') {
137
+ const blocker = itemMap.get(conn.fromId);
138
+ const target = itemMap.get(conn.toId);
139
+ if (
140
+ blocker && target &&
141
+ blocker.status !== 'complete' &&
142
+ blocker.status !== 'killed'
143
+ ) {
144
+ blocked.push({
145
+ item: { id: target.id, title: target.title, status: target.status, phase: target.phase },
146
+ blockedBy: { id: blocker.id, title: blocker.title, status: blocker.status },
147
+ });
148
+ }
149
+ }
150
+ }
151
+
152
+ return { count: blocked.length, blocked };
153
+ }
154
+
155
+ export async function toolGetCurrentSession({ featureCode } = {}) {
156
+ if (featureCode) {
157
+ // Delegate to REST API for live session + lifecycle context
158
+ return new Promise((resolve, reject) => {
159
+ const url = new URL(`${_getComposeApi()}/api/session/current?featureCode=${encodeURIComponent(featureCode)}`);
160
+ const req = http.request(
161
+ { hostname: url.hostname, port: url.port, path: `${url.pathname}${url.search}`, method: 'GET' },
162
+ (res) => {
163
+ let buf = '';
164
+ res.on('data', chunk => buf += chunk);
165
+ res.on('end', () => {
166
+ try { resolve(JSON.parse(buf)); } catch { resolve({ session: null }); }
167
+ });
168
+ },
169
+ );
170
+ req.on('error', () => resolve({ session: null }));
171
+ req.end();
172
+ });
173
+ }
174
+ // Existing disk-read path (keep as-is)
175
+ const sessions = loadSessions();
176
+ if (sessions.length === 0) return { session: null };
177
+
178
+ const last = sessions[sessions.length - 1];
179
+ const allSummaries = [];
180
+ for (const [, acc] of Object.entries(last.items || {})) {
181
+ for (const s of acc.summaries || []) {
182
+ if (s) allSummaries.push(typeof s === 'string' ? { summary: s } : s);
183
+ }
184
+ }
185
+
186
+ return {
187
+ session: {
188
+ id: last.id,
189
+ startedAt: last.startedAt,
190
+ endedAt: last.endedAt ?? null,
191
+ source: last.source,
192
+ toolCount: last.toolCount,
193
+ blockCount: (last.blocks || []).length,
194
+ errorCount: (last.errors || []).length,
195
+ itemCount: Object.keys(last.items || {}).length,
196
+ recentSummaries: allSummaries.slice(-5),
197
+ },
198
+ };
199
+ }
200
+
201
+ export async function toolBindSession({ featureCode }) {
202
+ const postData = JSON.stringify({ featureCode });
203
+ return new Promise((resolve, reject) => {
204
+ const url = new URL(`${_getComposeApi()}/api/session/bind`);
205
+ const req = http.request(
206
+ { hostname: url.hostname, port: url.port, path: url.pathname, method: 'POST',
207
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } },
208
+ (res) => {
209
+ let buf = '';
210
+ res.on('data', chunk => buf += chunk);
211
+ res.on('end', () => {
212
+ let parsed;
213
+ try { parsed = JSON.parse(buf); } catch { parsed = { error: buf }; }
214
+ if (res.statusCode >= 400) {
215
+ reject(new Error(parsed.error || `HTTP ${res.statusCode}: ${buf}`));
216
+ } else {
217
+ resolve(parsed);
218
+ }
219
+ });
220
+ },
221
+ );
222
+ req.on('error', (err) => reject(new Error(`Compose server unreachable: ${err.message}`)));
223
+ req.write(postData);
224
+ req.end();
225
+ });
226
+ }
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // Lifecycle tools — read from disk, mutations delegate to Compose REST API
230
+ // ---------------------------------------------------------------------------
231
+
232
+ function _getComposeApi() {
233
+ return `http://127.0.0.1:${process.env.COMPOSE_PORT || process.env.PORT || 3001}`;
234
+ }
235
+
236
+ function _postLifecycle(itemId, action, body) {
237
+ return new Promise((resolve, reject) => {
238
+ const data = JSON.stringify(body);
239
+ const url = new URL(`${_getComposeApi()}/api/vision/items/${itemId}/lifecycle/${action}`);
240
+ const req = http.request(
241
+ { hostname: url.hostname, port: url.port, path: url.pathname, method: 'POST',
242
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
243
+ (res) => {
244
+ let buf = '';
245
+ res.on('data', (chunk) => buf += chunk);
246
+ res.on('end', () => {
247
+ let parsed;
248
+ try { parsed = JSON.parse(buf); }
249
+ catch { parsed = { error: buf }; }
250
+ if (res.statusCode >= 400) {
251
+ reject(new Error(parsed.error || `HTTP ${res.statusCode}: ${buf}`));
252
+ } else {
253
+ resolve(parsed);
254
+ }
255
+ });
256
+ },
257
+ );
258
+ req.on('error', (err) => reject(new Error(`Compose server unreachable: ${err.message}`)));
259
+ req.end(data);
260
+ });
261
+ }
262
+
263
+ function _postGate(gateId, action, body) {
264
+ return new Promise((resolve, reject) => {
265
+ const data = JSON.stringify(body);
266
+ const url = new URL(`${_getComposeApi()}/api/vision/gates/${gateId}/${action}`);
267
+ const req = http.request(
268
+ { hostname: url.hostname, port: url.port, path: url.pathname, method: 'POST',
269
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
270
+ (res) => {
271
+ let buf = '';
272
+ res.on('data', (chunk) => buf += chunk);
273
+ res.on('end', () => {
274
+ let parsed;
275
+ try { parsed = JSON.parse(buf); }
276
+ catch { parsed = { error: buf }; }
277
+ if (res.statusCode >= 400) {
278
+ reject(new Error(parsed.error || `HTTP ${res.statusCode}: ${buf}`));
279
+ } else {
280
+ resolve(parsed);
281
+ }
282
+ });
283
+ },
284
+ );
285
+ req.on('error', (err) => reject(new Error(`Compose server unreachable: ${err.message}`)));
286
+ req.end(data);
287
+ });
288
+ }
289
+
290
+ export function toolGetFeatureLifecycle({ id }) {
291
+ const { items } = loadVisionState();
292
+ const item = items.find(i => i.id === id || i.semanticId === id || i.slug === id);
293
+ if (!item) return { error: `Item not found: ${id}` };
294
+ if (!item.lifecycle) return { error: 'No lifecycle on this item' };
295
+ return item.lifecycle;
296
+ }
297
+
298
+ export async function toolKillFeature({ id, reason }) {
299
+ return _postLifecycle(id, 'kill', { reason });
300
+ }
301
+
302
+ export async function toolCompleteFeature({ id }) {
303
+ return _postLifecycle(id, 'complete', {});
304
+ }
305
+
306
+ export async function toolIterationStart({ id, loopType, maxIterations }) {
307
+ return _postLifecycle(id, 'iteration/start', { loopType, maxIterations });
308
+ }
309
+
310
+ export async function toolIterationReport({ id, result }) {
311
+ return _postLifecycle(id, 'iteration/report', { result });
312
+ }
313
+
314
+ export async function toolIterationAbort({ id, reason }) {
315
+ return _postLifecycle(id, 'iteration/abort', { reason });
316
+ }
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // Artifact tools — read/write directly (no REST delegation needed)
320
+ // ---------------------------------------------------------------------------
321
+
322
+ export function toolAssessFeatureArtifacts({ featureCode }) {
323
+ const featureRoot = resolveProjectPath('features');
324
+ if (!fs.existsSync(featureRoot)) {
325
+ // Return empty assessments — feature root hasn't been created yet
326
+ const empty = {};
327
+ for (const filename of Object.keys(ARTIFACT_SCHEMAS)) {
328
+ empty[filename] = { exists: false, wordCount: 0, meetsMinWordCount: false, sections: { found: [], missing: [], optional: [] }, completeness: 0, lastModified: null };
329
+ }
330
+ return { artifacts: empty };
331
+ }
332
+ const manager = new ArtifactManager(featureRoot);
333
+ return manager.assess(featureCode);
334
+ }
335
+
336
+ export function toolScaffoldFeature({ featureCode, only }) {
337
+ const featureRoot = resolveProjectPath('features');
338
+ const manager = new ArtifactManager(featureRoot);
339
+ return manager.scaffold(featureCode, only ? { only } : undefined);
340
+ }
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // Gate tools — mutations delegate to Compose REST API
344
+ // ---------------------------------------------------------------------------
345
+
346
+ export async function toolApproveGate({ gateId, outcome, comment }) {
347
+ return _postGate(gateId, 'resolve', { outcome, comment });
348
+ }
349
+
350
+ export function toolGetPendingGates({ itemId }) {
351
+ const { gates } = loadVisionState();
352
+ if (!gates) return { count: 0, gates: [] };
353
+ const pending = gates.filter(g => g.status === 'pending' && (!itemId || g.itemId === itemId));
354
+ return { count: pending.length, gates: pending };
355
+ }
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // Agent run — dispatch prompts to claude or codex
359
+ // ---------------------------------------------------------------------------
360
+
361
+ const VALID_AGENT_TYPES = new Set(['claude', 'codex']);
362
+
363
+ /**
364
+ * Build a context preamble for agent_run prompts.
365
+ * The spawned agent (especially codex via opencode run) has no project context —
366
+ * no CLAUDE.md, no feature folder awareness, no compose/stratum semantics.
367
+ * This function reads project files and prepends them so the agent can do useful work.
368
+ */
369
+ function _buildContext({ featureCode }) {
370
+ const sections = [];
371
+
372
+ // Project instructions
373
+ const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
374
+ if (fs.existsSync(claudeMd)) {
375
+ try {
376
+ const content = fs.readFileSync(claudeMd, 'utf-8');
377
+ sections.push(`## Project Instructions (CLAUDE.md)\n\n${content}`);
378
+ } catch { /* ignore read errors */ }
379
+ }
380
+
381
+ // Feature artifacts (if feature code detected)
382
+ // Cap total feature context to ~20KB to avoid exceeding model input limits.
383
+ // Prioritize design.md first, then most recent files by mtime.
384
+ const MAX_FEATURE_BYTES = 20_000;
385
+ if (featureCode) {
386
+ const featureRoot = resolveProjectPath('features');
387
+ const featureDir = path.join(featureRoot, featureCode);
388
+ if (fs.existsSync(featureDir)) {
389
+ const artifacts = [];
390
+ let totalBytes = 0;
391
+ try {
392
+ const files = fs.readdirSync(featureDir)
393
+ .filter(f => f.endsWith('.md') || f.endsWith('.json'))
394
+ .map(f => ({ name: f, path: path.join(featureDir, f), stat: fs.statSync(path.join(featureDir, f)) }))
395
+ .filter(f => f.stat.isFile())
396
+ // design.md first, then most recently modified
397
+ .sort((a, b) => {
398
+ if (a.name === 'design.md') return -1;
399
+ if (b.name === 'design.md') return 1;
400
+ return b.stat.mtimeMs - a.stat.mtimeMs;
401
+ });
402
+ for (const file of files) {
403
+ if (totalBytes >= MAX_FEATURE_BYTES) break;
404
+ try {
405
+ const content = fs.readFileSync(file.path, 'utf-8');
406
+ const trimmed = content.slice(0, MAX_FEATURE_BYTES - totalBytes);
407
+ artifacts.push(`### ${file.name}\n\n${trimmed}`);
408
+ totalBytes += trimmed.length;
409
+ } catch { /* skip unreadable files */ }
410
+ }
411
+ } catch { /* ignore readdir errors */ }
412
+ if (artifacts.length > 0) {
413
+ sections.push(`## Feature: ${featureCode}\n\n${artifacts.join('\n\n---\n\n')}`);
414
+ }
415
+ }
416
+ }
417
+
418
+ if (sections.length === 0) return '';
419
+ return `# Context\n\n${sections.join('\n\n---\n\n')}\n\n---\n\n`;
420
+ }
421
+
422
+ /**
423
+ * Extract a feature code from the prompt if one is referenced.
424
+ * Looks for common patterns like "FEAT-1", "AUTH-2", or feature folder paths.
425
+ */
426
+ function _extractFeatureCode(prompt) {
427
+ // Match uppercase CODE-N patterns (e.g. FEAT-1, AUTH-2, STRAT-COMP-3)
428
+ const codeMatch = prompt.match(/\b([A-Z][\w-]*-\d+)\b/);
429
+ if (codeMatch) return codeMatch[1];
430
+
431
+ // Match feature folder references
432
+ const pathMatch = prompt.match(/features\/([a-zA-Z][\w-]+)/);
433
+ if (pathMatch) return pathMatch[1];
434
+
435
+ return null;
436
+ }
437
+
438
+ export async function toolAgentRun({ type = 'claude', prompt, schema, modelID, cwd, featureCode }) {
439
+ if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
440
+ throw new Error('agent_run: prompt is required');
441
+ }
442
+ if (!VALID_AGENT_TYPES.has(type)) {
443
+ throw new Error(`agent_run: unknown type '${type}'. Valid: ${[...VALID_AGENT_TYPES].join(', ')}`);
444
+ }
445
+
446
+ // Resolve feature code from explicit param or prompt text
447
+ const resolvedFeature = featureCode || _extractFeatureCode(prompt);
448
+
449
+ // Build context preamble and prepend to prompt
450
+ const context = _buildContext({ featureCode: resolvedFeature });
451
+ const fullPrompt = context ? `${context}# Task\n\n${prompt}` : prompt;
452
+
453
+ const resolvedCwd = cwd || PROJECT_ROOT;
454
+ const connector = type === 'codex'
455
+ ? new CodexConnector({ modelID, cwd: resolvedCwd })
456
+ : new ClaudeSDKConnector({ model: modelID, cwd: resolvedCwd });
457
+
458
+ const parts = [];
459
+ for await (const event of connector.run(fullPrompt, { schema, modelID, cwd: resolvedCwd })) {
460
+ if (event.type === 'assistant' && event.content) {
461
+ parts.push(event.content);
462
+ } else if (event.type === 'error') {
463
+ throw new Error(`agent_run (${type}): ${event.message}`);
464
+ }
465
+ }
466
+
467
+ const text = parts.join('');
468
+
469
+ if (schema) {
470
+ try {
471
+ return { text, result: JSON.parse(text) };
472
+ } catch {
473
+ return { text, result: null, parseError: 'Response was not valid JSON' };
474
+ }
475
+ }
476
+
477
+ return { text };
478
+ }
479
+