@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,210 @@
1
+ /**
2
+ * roadmap-gen.js — Generate ROADMAP.md from feature.json files.
3
+ *
4
+ * feature.json is the source of truth. ROADMAP.md is a rendered view.
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { listFeatures } from './feature-json.js';
10
+
11
+ const STATUS_ORDER = ['IN_PROGRESS', 'PARTIAL', 'PLANNED', 'COMPLETE', 'SUPERSEDED', 'PARKED'];
12
+
13
+ /**
14
+ * Compute the aggregate status for a phase based on its features.
15
+ */
16
+ function phaseStatus(features) {
17
+ const statuses = new Set(features.map(f => f.status));
18
+ if (statuses.size === 1) return [...statuses][0];
19
+ if (statuses.has('IN_PROGRESS') || statuses.has('PARTIAL')) return 'PARTIAL';
20
+ if (statuses.has('PLANNED') && statuses.has('COMPLETE')) return 'PARTIAL';
21
+ return 'PLANNED';
22
+ }
23
+
24
+ /**
25
+ * Generate ROADMAP.md content from feature.json files.
26
+ *
27
+ * @param {string} cwd - Project root
28
+ * @param {object} [opts]
29
+ * @param {string} [opts.featuresDir] - Relative path to features dir
30
+ * @param {string} [opts.projectName] - Project name for header
31
+ * @param {string} [opts.projectDescription] - Project description for header
32
+ * @returns {string} - Generated ROADMAP.md content
33
+ */
34
+ export function generateRoadmap(cwd, opts = {}) {
35
+ const featuresDir = opts.featuresDir ?? 'docs/features';
36
+ const features = listFeatures(cwd, featuresDir);
37
+
38
+ // Read existing ROADMAP.md to preserve header/preamble
39
+ const preamble = readPreamble(cwd, opts);
40
+
41
+ // Group by phase
42
+ const phases = new Map();
43
+ const ungrouped = [];
44
+
45
+ for (const f of features) {
46
+ const phase = f.phase ?? null;
47
+ if (!phase) {
48
+ ungrouped.push(f);
49
+ continue;
50
+ }
51
+ if (!phases.has(phase)) phases.set(phase, []);
52
+ phases.get(phase).push(f);
53
+ }
54
+
55
+ const sections = [preamble.trimEnd()];
56
+
57
+ // Sort phases by the minimum position of their features (preserves ROADMAP order)
58
+ const sortedPhases = [...phases.entries()].sort((a, b) => {
59
+ const minA = Math.min(...a[1].map(f => f.position ?? 999));
60
+ const minB = Math.min(...b[1].map(f => f.position ?? 999));
61
+ return minA - minB;
62
+ });
63
+
64
+ // Render each phase
65
+ for (const [phase, phaseFeatures] of sortedPhases) {
66
+ const status = phaseStatus(phaseFeatures);
67
+ sections.push(renderPhase(phase, status, phaseFeatures));
68
+ }
69
+
70
+ // Render ungrouped features
71
+ if (ungrouped.length > 0) {
72
+ sections.push(renderPhase('Features', phaseStatus(ungrouped), ungrouped));
73
+ }
74
+
75
+ // Key documents section
76
+ const keyDocs = buildKeyDocs(features, featuresDir);
77
+ if (keyDocs) sections.push(keyDocs);
78
+
79
+ return sections.join('\n\n---\n\n') + '\n';
80
+ }
81
+
82
+ /**
83
+ * Read the preamble (everything before the first ## Phase/Feature section)
84
+ * from an existing ROADMAP.md, or generate a default one.
85
+ */
86
+ function readPreamble(cwd, opts) {
87
+ const roadmapPath = join(cwd, 'ROADMAP.md');
88
+ if (existsSync(roadmapPath)) {
89
+ const text = readFileSync(roadmapPath, 'utf-8');
90
+ // Find the first ## heading that looks like a phase/feature section
91
+ const match = text.match(/^(---\s*\n\s*)?(?=## )/m);
92
+ if (match) {
93
+ const idx = text.indexOf(match[0]);
94
+ const pre = text.slice(0, idx).trimEnd();
95
+ if (pre.length > 0) return pre;
96
+ }
97
+ }
98
+
99
+ // Default preamble
100
+ const name = opts.projectName ?? 'Project';
101
+ const desc = opts.projectDescription ?? '';
102
+ const today = new Date().toISOString().slice(0, 10);
103
+ return `# ${name} Roadmap
104
+
105
+ ${desc ? desc + '\n\n' : ''}<!-- Generated from feature.json — do not edit manually -->
106
+ <!-- Run: compose roadmap generate -->
107
+
108
+ **Last updated:** ${today}
109
+
110
+ ---
111
+
112
+ ## Roadmap Conventions
113
+
114
+ - **Status:** \`PLANNED\` | \`IN_PROGRESS\` | \`PARTIAL\` | \`COMPLETE\` | \`SUPERSEDED\` | \`PARKED\`
115
+ - Items are numbered sequentially. Never reuse a number.
116
+ - Cross-reference stable IDs (e.g. \`FEAT-1\`) not section headings.`;
117
+ }
118
+
119
+ /**
120
+ * Render a phase section with its feature table.
121
+ */
122
+ function renderPhase(phaseName, status, features) {
123
+ const lines = [`## ${phaseName} — ${status}`, ''];
124
+
125
+ // Phase description from the first feature's phaseDescription if available
126
+ const desc = features[0]?.phaseDescription;
127
+ if (desc) {
128
+ lines.push(desc, '');
129
+ }
130
+
131
+ // Determine table columns based on whether features have sub-items
132
+ const hasSubItems = features.some(f => f.items && f.items.length > 0);
133
+
134
+ if (hasSubItems) {
135
+ // Expanded: one row per sub-item
136
+ lines.push('| # | Feature | Item | Status |');
137
+ lines.push('|---|---------|------|--------|');
138
+ for (const f of features) {
139
+ if (f.items && f.items.length > 0) {
140
+ for (const item of f.items) {
141
+ const num = item.position ?? '—';
142
+ const desc = item.description ?? '';
143
+ const st = item.status ?? f.status;
144
+ lines.push(`| ${num} | ${f.code} | ${desc} | ${st} |`);
145
+ }
146
+ } else {
147
+ const num = f.position ?? '—';
148
+ lines.push(`| ${num} | ${f.code} | ${f.description} | ${f.status} |`);
149
+ }
150
+ }
151
+ } else {
152
+ // Simple: one row per feature
153
+ lines.push('| # | Feature | Description | Status |');
154
+ lines.push('|---|---------|-------------|--------|');
155
+ for (const f of features) {
156
+ const num = f.position ?? '—';
157
+ const desc = f.description.length > 80 ? f.description.slice(0, 77) + '...' : f.description;
158
+ lines.push(`| ${num} | ${f.code} | ${desc} | ${f.status} |`);
159
+ }
160
+ }
161
+
162
+ // Exit criteria
163
+ const exit = features[0]?.phaseExit;
164
+ if (exit) {
165
+ lines.push('', `**Exit:** ${exit}`);
166
+ }
167
+
168
+ // Links to design docs
169
+ const docsLinks = features
170
+ .filter(f => f.designDoc)
171
+ .map(f => `See \`${f.designDoc}\` for ${f.code} design.`);
172
+ if (docsLinks.length > 0) {
173
+ lines.push('', docsLinks.join('\n'));
174
+ }
175
+
176
+ return lines.join('\n');
177
+ }
178
+
179
+ /**
180
+ * Build a Key Documents section from features that have design docs.
181
+ */
182
+ function buildKeyDocs(features, featuresDir) {
183
+ const docs = features
184
+ .filter(f => existsSync || f.designDoc) // always include if designDoc is set
185
+ .filter(f => f.designDoc)
186
+ .map(f => `| \`${f.designDoc}\` | ${f.code} design |`);
187
+
188
+ if (docs.length === 0) return null;
189
+
190
+ return [
191
+ '## Key Documents',
192
+ '',
193
+ '| Document | What it is |',
194
+ '|---|---|',
195
+ ...docs,
196
+ ].join('\n');
197
+ }
198
+
199
+ /**
200
+ * Write the generated ROADMAP.md to disk.
201
+ *
202
+ * @param {string} cwd - Project root
203
+ * @param {object} [opts]
204
+ */
205
+ export function writeRoadmap(cwd, opts = {}) {
206
+ const content = generateRoadmap(cwd, opts);
207
+ const roadmapPath = join(cwd, 'ROADMAP.md');
208
+ writeFileSync(roadmapPath, content);
209
+ return roadmapPath;
210
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * roadmap-parser.js — Parse ROADMAP.md into structured feature entries.
3
+ *
4
+ * Extracts feature codes, descriptions, statuses, and phase membership
5
+ * from the markdown table format used by Compose roadmaps.
6
+ */
7
+
8
+ const SKIP_STATUSES = new Set(['COMPLETE', 'SUPERSEDED', 'PARKED']);
9
+
10
+ const PHASE_HEADING_RE = /^##\s+(.+?)(?:\s+—\s+(.+))?$/;
11
+ const MILESTONE_HEADING_RE = /^###\s+(.+?)(?:\s*:\s*(.+))?$/;
12
+ const TABLE_ROW_RE = /^\|(.+)\|$/;
13
+ const FEATURE_CODE_RE = /^[A-Z][\w-]*-\d+/;
14
+
15
+ /**
16
+ * @typedef {{ code: string, description: string, status: string, phaseId: string, position: number }} FeatureEntry
17
+ */
18
+
19
+ /**
20
+ * Parse ROADMAP.md text into an ordered list of feature entries.
21
+ * Anonymous rows (code === '—' or no code) are included for dependency chain
22
+ * purposes but can be filtered out for build lists.
23
+ *
24
+ * @param {string} text - Raw ROADMAP.md content
25
+ * @returns {FeatureEntry[]}
26
+ */
27
+ export function parseRoadmap(text) {
28
+ const lines = text.split('\n');
29
+ const entries = [];
30
+ let currentPhaseId = '';
31
+ let currentPhaseStatus = '';
32
+ let position = 0;
33
+ let inTable = false;
34
+ let columnLayout = null; // { codeCol, descCol, statusCol }
35
+
36
+ for (const line of lines) {
37
+ const trimmed = line.trim();
38
+
39
+ // Phase heading: ## Phase 0: Bootstrap — COMPLETE
40
+ const phaseMatch = trimmed.match(PHASE_HEADING_RE);
41
+ if (phaseMatch) {
42
+ currentPhaseId = phaseMatch[1].trim();
43
+ currentPhaseStatus = phaseMatch[2]?.trim() ?? '';
44
+ inTable = false;
45
+ columnLayout = null;
46
+ continue;
47
+ }
48
+
49
+ // Milestone heading: ### Milestone 1: Stratum Engine Complete
50
+ const milestoneMatch = trimmed.match(MILESTONE_HEADING_RE);
51
+ if (milestoneMatch) {
52
+ // Nest under parent phase
53
+ const milestoneLabel = milestoneMatch[1].trim();
54
+ if (currentPhaseId) {
55
+ currentPhaseId = `${currentPhaseId} > ${milestoneLabel}`;
56
+ } else {
57
+ currentPhaseId = milestoneLabel;
58
+ }
59
+ inTable = false;
60
+ columnLayout = null;
61
+ continue;
62
+ }
63
+
64
+ // Table row
65
+ if (!TABLE_ROW_RE.test(trimmed)) {
66
+ if (inTable) {
67
+ inTable = false;
68
+ columnLayout = null;
69
+ }
70
+ continue;
71
+ }
72
+
73
+ const cells = trimmed.split('|').slice(1, -1).map(c => c.trim());
74
+
75
+ // Skip separator rows (|---|---|---|)
76
+ if (cells.every(c => /^[-:]+$/.test(c))) {
77
+ continue;
78
+ }
79
+
80
+ // Detect header row
81
+ if (!inTable) {
82
+ inTable = true;
83
+ columnLayout = detectColumnLayout(cells);
84
+ continue;
85
+ }
86
+
87
+ if (!columnLayout) continue;
88
+
89
+ // Parse data row
90
+ const code = cells[columnLayout.codeCol] ?? '—';
91
+ const desc = cells[columnLayout.descCol] ?? '';
92
+ let status = cells[columnLayout.statusCol] ?? '';
93
+
94
+ // Clean up status (strip bold markers, etc.)
95
+ status = status.replace(/\*\*/g, '').trim();
96
+
97
+ // If the entire phase is COMPLETE, override individual statuses
98
+ if (SKIP_STATUSES.has(currentPhaseStatus)) {
99
+ status = currentPhaseStatus;
100
+ }
101
+
102
+ const isAnonymous = code === '—' || code === '-' || !FEATURE_CODE_RE.test(code);
103
+
104
+ entries.push({
105
+ code: isAnonymous ? `_anon_${position}` : code,
106
+ description: desc.replace(/\*\*/g, '').trim(),
107
+ status: status || 'PLANNED',
108
+ phaseId: currentPhaseId,
109
+ position: position++,
110
+ });
111
+ }
112
+
113
+ return entries;
114
+ }
115
+
116
+ /**
117
+ * Detect the column layout of a table header row.
118
+ *
119
+ * Supported layouts:
120
+ * 4-col: # | Feature | Item | Status → code=Feature, desc=Item
121
+ * 4-col: ID | Item | Location | Status → code=ID, desc=Item
122
+ * 3-col: ID | Feature | Status → code=ID, desc=Feature
123
+ * 3-col: ID | Item | Status → code=ID, desc=Item
124
+ * 3-col: # | Item | Status → anonymous (# is a row number, not a code)
125
+ */
126
+ function detectColumnLayout(headerCells) {
127
+ const lower = headerCells.map(c => c.toLowerCase());
128
+
129
+ // 4+ columns: look for a Feature column as the code source
130
+ if (lower.length >= 4) {
131
+ const featureIdx = lower.findIndex(c => c === 'feature');
132
+ if (featureIdx !== -1) {
133
+ return {
134
+ codeCol: featureIdx,
135
+ descCol: featureIdx + 1,
136
+ statusCol: lower.length - 1,
137
+ };
138
+ }
139
+ // No "feature" column — use first col as code, second as desc
140
+ return {
141
+ codeCol: 0,
142
+ descCol: 1,
143
+ statusCol: lower.length - 1,
144
+ };
145
+ }
146
+
147
+ // 3 columns: check if first column is "id" (contains feature codes)
148
+ if (lower.length === 3 && lower[0] === 'id') {
149
+ return {
150
+ codeCol: 0,
151
+ descCol: 1,
152
+ statusCol: 2,
153
+ };
154
+ }
155
+
156
+ // 3-column fallback: # | Item | Status (row-number tables, anonymous)
157
+ return {
158
+ codeCol: -1, // will yield undefined → anonymous
159
+ descCol: Math.min(1, lower.length - 2),
160
+ statusCol: lower.length - 1,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Return only features that should be built:
166
+ * - has a real feature code (not anonymous)
167
+ * - status is PLANNED or IN_PROGRESS or PARTIAL
168
+ *
169
+ * @param {FeatureEntry[]} entries
170
+ * @returns {FeatureEntry[]}
171
+ */
172
+ export function filterBuildable(entries) {
173
+ return entries.filter(e =>
174
+ !e.code.startsWith('_anon_') && !SKIP_STATUSES.has(e.status)
175
+ );
176
+ }
@@ -0,0 +1,23 @@
1
+ import { resolvePort } from './resolve-port.js';
2
+
3
+ /**
4
+ * Probe whether the Compose server is reachable.
5
+ * @param {number} [port] - Server port (default: resolvePort())
6
+ * @param {number} [timeoutMs=500] - Timeout in ms
7
+ * @returns {Promise<boolean>} true if server responds 2xx to GET /api/health
8
+ */
9
+ export async function probeServer(port, timeoutMs = 500) {
10
+ const p = port ?? resolvePort();
11
+ const controller = new AbortController();
12
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
13
+ try {
14
+ const res = await fetch(`http://localhost:${p}/api/health`, {
15
+ signal: controller.signal,
16
+ });
17
+ return res.ok;
18
+ } catch {
19
+ return false;
20
+ } finally {
21
+ clearTimeout(timer);
22
+ }
23
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * staleness.js — Artifact staleness detection for COMP-CTX (item 101).
3
+ *
4
+ * Artifacts embed a phase marker in their first 5 lines:
5
+ * <!-- phase: explore_design -->
6
+ *
7
+ * If the feature's current phase is past the artifact's written phase,
8
+ * the artifact is flagged stale.
9
+ */
10
+
11
+ import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+
14
+ // Canonical phase order — earlier index = earlier phase
15
+ const PHASE_ORDER = [
16
+ 'explore_design',
17
+ 'blueprint',
18
+ 'plan',
19
+ 'build',
20
+ 'ship',
21
+ 'done',
22
+ ];
23
+
24
+ /**
25
+ * Return numeric index of phase (lower = earlier).
26
+ * Unknown phases return -1 so they're never considered stale.
27
+ */
28
+ function phaseIndex(phase) {
29
+ return PHASE_ORDER.indexOf(phase);
30
+ }
31
+
32
+ /**
33
+ * Extract the <!-- phase: <name> --> marker from the first 5 lines of text.
34
+ * Returns the phase name or null if not found.
35
+ *
36
+ * @param {string} content
37
+ * @returns {string|null}
38
+ */
39
+ export function extractPhaseMarker(content) {
40
+ const lines = content.split('\n').slice(0, 5);
41
+ for (const line of lines) {
42
+ const m = line.match(/<!--\s*phase:\s*([\w_-]+)\s*-->/);
43
+ if (m) return m[1];
44
+ }
45
+ return null;
46
+ }
47
+
48
+ /**
49
+ * Check staleness of tracked artifacts in a feature folder.
50
+ *
51
+ * Scans for design.md, blueprint.md, plan.md in featureDir.
52
+ * For each file that exists and has a phase marker, compares its
53
+ * phase to currentPhase. If currentPhase is strictly later in
54
+ * PHASE_ORDER, the artifact is stale.
55
+ *
56
+ * @param {string} featureDir - Absolute path to the feature folder
57
+ * @param {string} currentPhase - Feature's current phase name
58
+ * @returns {Array<{ file: string, writtenPhase: string, currentPhase: string, stale: boolean }>}
59
+ */
60
+ export function checkStaleness(featureDir, currentPhase) {
61
+ const TRACKED = ['design.md', 'blueprint.md', 'plan.md'];
62
+ const results = [];
63
+
64
+ const currentIdx = phaseIndex(currentPhase);
65
+
66
+ for (const filename of TRACKED) {
67
+ const filePath = join(featureDir, filename);
68
+ if (!existsSync(filePath)) continue;
69
+
70
+ let content;
71
+ try {
72
+ content = readFileSync(filePath, 'utf-8');
73
+ } catch {
74
+ continue;
75
+ }
76
+
77
+ const writtenPhase = extractPhaseMarker(content);
78
+ if (!writtenPhase) continue;
79
+
80
+ const writtenIdx = phaseIndex(writtenPhase);
81
+ const stale = writtenIdx !== -1 && currentIdx !== -1 && currentIdx > writtenIdx;
82
+
83
+ results.push({ file: filename, writtenPhase, currentPhase, stale });
84
+ }
85
+
86
+ return results;
87
+ }