@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,53 @@
1
+ /**
2
+ * capability-checker.js — Runtime capability violation detection (COMP-CAPS-ENFORCE).
3
+ *
4
+ * Compares observed tool names against agent template restrictions.
5
+ * Called after each step to identify policy violations for audit or enforcement.
6
+ */
7
+
8
+ import { resolveTemplate, validateCapabilities } from '../server/agent-templates.js';
9
+ import { parseAgentString } from './agent-string.js';
10
+
11
+ /**
12
+ * Check whether a tool use constitutes a capability violation under an agent's template.
13
+ *
14
+ * Violation = tool is in disallowedTools (explicit deny)
15
+ * Warning = tool is not in allowedTools and allowedTools is a non-null list
16
+ * (not explicitly permitted, but not explicitly denied)
17
+ * No issue = tool is permitted or template has no restrictions
18
+ *
19
+ * @param {string} toolName The tool that was observed being used
20
+ * @param {string|null|undefined} agentString Agent string, e.g. "claude:read-only-reviewer"
21
+ * @returns {{ violation: boolean, severity: 'violation'|'warning'|'none', reason: string }}
22
+ */
23
+ export function checkCapabilityViolation(toolName, agentString) {
24
+ const { template: templateName } = parseAgentString(agentString);
25
+ const template = resolveTemplate(templateName);
26
+
27
+ if (!template) {
28
+ return { violation: false, severity: 'none', reason: 'No template — all tools permitted' };
29
+ }
30
+
31
+ const { allowedTools, disallowedTools } = template;
32
+
33
+ // Explicit deny → violation
34
+ if (disallowedTools && disallowedTools.includes(toolName)) {
35
+ return {
36
+ violation: true,
37
+ severity: 'violation',
38
+ reason: `Tool "${toolName}" is in disallowedTools for template "${templateName}"`,
39
+ };
40
+ }
41
+
42
+ // Not in allowedTools (when allowedTools is set) → warning
43
+ if (allowedTools !== null && !allowedTools.includes(toolName)) {
44
+ return {
45
+ violation: true,
46
+ severity: 'warning',
47
+ reason: `Tool "${toolName}" is not in allowedTools for template "${templateName}"`,
48
+ };
49
+ }
50
+
51
+ const { reason } = validateCapabilities(template, toolName);
52
+ return { violation: false, severity: 'none', reason };
53
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * cert-inject.js — STRAT-CERT reasoning template injection for parallel dispatch tasks.
3
+ *
4
+ * Mirrors Python's inject_cert_instructions() from executor.py.
5
+ * Used by build.js to inject structured reasoning prompts into
6
+ * STRAT-REV lens task intents.
7
+ *
8
+ * See: docs/features/STRAT-CERT/design.md (Consumer Wiring addendum)
9
+ */
10
+
11
+ export const DEFAULT_CERT_SECTIONS = [
12
+ { id: 'premises', label: 'Premises', description: 'State every verifiable fact you are using. Each premise must cite a file:line.' },
13
+ { id: 'trace', label: 'Trace', description: 'Walk through the logic step by step. Reference premises by [P<n>] ID.' },
14
+ { id: 'conclusion', label: 'Conclusion', description: 'State your finding. Every claim must reference at least one premise.' },
15
+ ];
16
+
17
+ /**
18
+ * Inject structured reasoning certificate instructions into an intent string.
19
+ *
20
+ * @param {string} intent - The original task intent
21
+ * @param {object} template - reasoning_template object with optional sections[] and require_citations
22
+ * @returns {string} Intent with appended certificate structure instructions
23
+ */
24
+ export function injectCertInstructions(intent, template) {
25
+ if (!template) return intent;
26
+ const sections = template.sections?.length ? template.sections : DEFAULT_CERT_SECTIONS;
27
+ const requireCitations = template.require_citations || false;
28
+ const lines = [intent, '', '---', '', 'You MUST structure your response with these sections:', ''];
29
+ for (const [i, section] of sections.entries()) {
30
+ lines.push(`## ${section.label}`);
31
+ lines.push(section.description);
32
+ if (section.id === 'premises' && requireCitations) lines.push('Format each fact as: [P1] <fact, citing file:line>, [P2] ..., etc.');
33
+ else if (i > 0 && requireCitations) lines.push('Reference premises by their [P<n>] ID.');
34
+ lines.push('');
35
+ }
36
+ lines.push('Include your full structured reasoning in a `reasoning` field in your JSON output.');
37
+ return lines.join('\n');
38
+ }
@@ -0,0 +1,483 @@
1
+ /**
2
+ * cli-progress.js — Build progress output for compose build.
3
+ *
4
+ * Two modes:
5
+ * Collapsed (default): last 5 tool events + sticky key hints bar
6
+ * Expanded: all tool events printed as they arrive
7
+ *
8
+ * Keys: t=toggle s=skip r=retry Ctrl+C=abort
9
+ */
10
+
11
+ import { EventEmitter } from 'node:events';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // ANSI helpers
15
+ // ---------------------------------------------------------------------------
16
+
17
+ const ESC = '\x1b[';
18
+ const RESET = `${ESC}0m`;
19
+ const BOLD = `${ESC}1m`;
20
+ const DIM = `${ESC}2m`;
21
+ const CYAN = `${ESC}36m`;
22
+ const YELLOW = `${ESC}33m`;
23
+ const GREEN = `${ESC}32m`;
24
+ const RED = `${ESC}31m`;
25
+ const MAGENTA = `${ESC}35m`;
26
+ const GRAY = `${ESC}90m`;
27
+ const WHITE = `${ESC}37m`;
28
+
29
+ const ERASE_LINE = `${ESC}2K\r`;
30
+ const MOVE_UP = (n) => `${ESC}${n}A`;
31
+ const HIDE_CURSOR = `${ESC}?25l`;
32
+ const SHOW_CURSOR = `${ESC}?25h`;
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Tool color mapping
36
+ // ---------------------------------------------------------------------------
37
+
38
+ const TOOL_COLORS = {
39
+ Bash: CYAN, bash: CYAN,
40
+ Read: GREEN, read: GREEN,
41
+ Write: YELLOW, write: YELLOW,
42
+ Edit: YELLOW, edit: YELLOW,
43
+ Glob: MAGENTA, glob: MAGENTA,
44
+ Grep: MAGENTA, grep: MAGENTA,
45
+ Agent: BOLD + CYAN, agent: BOLD + CYAN,
46
+ };
47
+
48
+ function colorForTool(tool) {
49
+ return TOOL_COLORS[tool] ?? GRAY;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Constants
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const COLLAPSED_LINES = 5; // tool lines to show when collapsed
57
+ const HINT_BAR = `${DIM} keys: ${WHITE}t${GRAY}=toggle ${WHITE}s${GRAY}=skip ${WHITE}r${GRAY}=retry ${WHITE}Ctrl+C${GRAY}=abort${RESET}`;
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Pipeline step definitions (COMP-TUI-1)
61
+ // ---------------------------------------------------------------------------
62
+
63
+ const PIPELINE_STEPS = [
64
+ 'explore_design', 'scope', 'design_gate', 'prd', 'architecture',
65
+ 'blueprint', 'verification', 'plan_gate', 'decompose', 'execute',
66
+ 'review', 'codex_review', 'coverage', 'report', 'docs', 'ship_gate', 'ship',
67
+ ];
68
+
69
+ const STEP_LABELS = {
70
+ explore_design: 'explore', scope: 'scope', design_gate: 'design\u2193',
71
+ prd: 'prd', architecture: 'arch', blueprint: 'blueprint',
72
+ verification: 'verify', plan_gate: 'plan\u2193', decompose: 'decomp',
73
+ execute: 'execute', review: 'review', codex_review: 'codex',
74
+ coverage: 'coverage',
75
+ report: 'report', docs: 'docs', ship_gate: 'ship\u2193', ship: 'ship',
76
+ };
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // CliProgress
80
+ // ---------------------------------------------------------------------------
81
+
82
+ export class CliProgress extends EventEmitter {
83
+ #expanded = false;
84
+ #toolHistory = []; // all tool events for current step
85
+ #toolCount = 0;
86
+ #drawnCollapsedLines = 0; // how many collapsed lines are currently on screen
87
+ #stream;
88
+ #isTTY;
89
+ #onKey;
90
+ #wasRaw;
91
+ #listening = false;
92
+ #heartbeatTimer = null;
93
+ #stepStart = 0;
94
+ #currentStepId = '';
95
+ #pendingAction = null;
96
+ #stepHistory = []; // completed step IDs for pipeline bar
97
+
98
+ constructor({ stream, expanded = false } = {}) {
99
+ super();
100
+ this.#stream = stream ?? process.stderr;
101
+ this.#isTTY = this.#stream.isTTY ?? false;
102
+ this.#expanded = expanded;
103
+
104
+ this.#onKey = (key) => {
105
+ if (key === 't' || key === 'T') this.toggle();
106
+ if (key === 's' || key === 'S') {
107
+ this.#pendingAction = 'skip';
108
+ this.#clearCollapsed();
109
+ this.#stream.write(` ${YELLOW}⏭ Skip requested — interrupting current step...${RESET}\n`);
110
+ this.emit('interrupt');
111
+ }
112
+ if (key === 'r' || key === 'R') {
113
+ this.#pendingAction = 'retry';
114
+ this.#clearCollapsed();
115
+ this.#stream.write(` ${YELLOW}↻ Retry requested — interrupting current step...${RESET}\n`);
116
+ this.emit('interrupt');
117
+ }
118
+ if (key === '\x03') {
119
+ this.finish();
120
+ process.exit(130);
121
+ }
122
+ };
123
+
124
+ this.#startListening();
125
+ }
126
+
127
+ get expanded() { return this.#expanded; }
128
+
129
+ consumeAction() {
130
+ const action = this.#pendingAction;
131
+ this.#pendingAction = null;
132
+ return action;
133
+ }
134
+
135
+ get pendingAction() { return this.#pendingAction; }
136
+
137
+ // ── Key listener management ───────────────────────────────────────────
138
+
139
+ #startListening() {
140
+ if (this.#listening) return;
141
+ if (!this.#isTTY || !process.stdin.isTTY || !process.stdin.setRawMode) return;
142
+ this.#wasRaw = process.stdin.isRaw;
143
+ process.stdin.setRawMode(true);
144
+ process.stdin.resume();
145
+ process.stdin.setEncoding('utf-8');
146
+ process.stdin.on('data', this.#onKey);
147
+ this.#listening = true;
148
+ if (this.#isTTY) this.#stream.write(HIDE_CURSOR);
149
+ }
150
+
151
+ #stopListening() {
152
+ if (!this.#listening) return;
153
+ process.stdin.removeListener('data', this.#onKey);
154
+ if (process.stdin.setRawMode) {
155
+ try { process.stdin.setRawMode(this.#wasRaw ?? false); } catch { /* ignore */ }
156
+ }
157
+ process.stdin.pause();
158
+ this.#listening = false;
159
+ if (this.#isTTY) this.#stream.write(SHOW_CURSOR);
160
+ }
161
+
162
+ pause() {
163
+ this.#clearCollapsed();
164
+ this.#stopListening();
165
+ }
166
+
167
+ resume() {
168
+ this.#startListening();
169
+ if (!this.#expanded) this.#drawCollapsed();
170
+ }
171
+
172
+ toggle() {
173
+ this.#clearCollapsed();
174
+ this.#expanded = !this.#expanded;
175
+ if (this.#expanded) {
176
+ // Dump full tool history on expand
177
+ for (const t of this.#toolHistory) {
178
+ this.#stream.write(` ${colorForTool(t.tool)}↳ ${t.tool}${RESET}${t.detail ? ': ' + t.detail : ''}\n`);
179
+ }
180
+ } else {
181
+ this.#drawCollapsed();
182
+ }
183
+ }
184
+
185
+ // ── Event handlers ────────────────────────────────────────────────────
186
+
187
+ toolUse(tool, detail) {
188
+ this.#toolCount++;
189
+ this.#stepStart = Date.now();
190
+ const short = typeof detail === 'string' && detail.length > 60
191
+ ? detail.slice(0, 57) + '...' : (detail || '');
192
+
193
+ this.#toolHistory.push({ tool, detail: short });
194
+
195
+ if (this.#expanded) {
196
+ this.#stream.write(` ${colorForTool(tool)}↳ ${tool}${RESET}${short ? ': ' + short : ''}\n`);
197
+ } else {
198
+ this.#drawCollapsed();
199
+ }
200
+ }
201
+
202
+ toolSummary(summary) {
203
+ const short = summary.length > 80 ? summary.slice(0, 77) + '...' : summary;
204
+ this.#toolHistory.push({ tool: '✓', detail: short });
205
+
206
+ if (this.#expanded) {
207
+ this.#stream.write(` ${GREEN}✓${RESET} ${short}\n`);
208
+ } else {
209
+ this.#drawCollapsed();
210
+ }
211
+ }
212
+
213
+ toolProgress(tool, elapsed) {
214
+ this.#toolHistory.push({ tool, detail: `(${Math.round(elapsed)}s)` });
215
+
216
+ if (this.#expanded) {
217
+ this.#stream.write(` ${colorForTool(tool)}↳ ${tool}${RESET} ${DIM}(${Math.round(elapsed)}s)${RESET}\n`);
218
+ } else {
219
+ this.#drawCollapsed();
220
+ }
221
+ }
222
+
223
+ stepStart(stepNum, totalSteps, stepId) {
224
+ this.#clearCollapsed();
225
+ this.#stopHeartbeat();
226
+ this.#drawPipelineBar(stepId);
227
+ this.#stream.write(`${BOLD}[${stepNum}/${totalSteps}]${RESET} ${stepId}...\n`);
228
+ this.#toolCount = 0;
229
+ this.#toolHistory = [];
230
+ this.#stepStart = Date.now();
231
+ this.#currentStepId = stepId;
232
+ if (!this.#expanded) this.#drawCollapsed();
233
+ this.#startHeartbeat();
234
+ }
235
+
236
+ stepDone(stepId) {
237
+ if (!this.#stepHistory.includes(stepId)) {
238
+ this.#stepHistory.push(stepId);
239
+ }
240
+ }
241
+
242
+ // ── Pipeline bar (COMP-TUI-1) ────────────────────────────────────────
243
+
244
+ pipelineBar(stepNum, totalSteps, stepId, stepHistory) {
245
+ this.#stepHistory = (stepHistory ?? []).map(h => h.stepId ?? h);
246
+ this.#drawPipelineBar(stepId);
247
+ }
248
+
249
+ #drawPipelineBar(currentStepId) {
250
+ if (!this.#isTTY) return;
251
+
252
+ const doneSet = new Set(this.#stepHistory);
253
+ const currentIdx = PIPELINE_STEPS.indexOf(currentStepId);
254
+
255
+ // Build the visible window: all done + current + up to 3 pending
256
+ const cols = this.#stream.columns || 80;
257
+ let visibleSteps;
258
+
259
+ // Calculate full bar length to decide if we need a sliding window
260
+ const fullLen = PIPELINE_STEPS.reduce((sum, s, i) => {
261
+ const label = STEP_LABELS[s] || s;
262
+ return sum + label.length + 3 + (i < PIPELINE_STEPS.length - 1 ? 3 : 0); // icon + space + label + separator
263
+ }, 4); // 4 for leading indent
264
+
265
+ if (fullLen <= cols) {
266
+ visibleSteps = PIPELINE_STEPS;
267
+ } else {
268
+ // Sliding window: show done steps, current, and up to 3 pending
269
+ const start = 0;
270
+ const end = Math.min(PIPELINE_STEPS.length, (currentIdx < 0 ? 0 : currentIdx) + 4);
271
+ visibleSteps = PIPELINE_STEPS.slice(start, end);
272
+
273
+ // If window is still too wide, trim from the start (keep current visible)
274
+ while (visibleSteps.length > 3) {
275
+ const windowLen = visibleSteps.reduce((sum, s, i) => {
276
+ const label = STEP_LABELS[s] || s;
277
+ return sum + label.length + 3 + (i < visibleSteps.length - 1 ? 3 : 0);
278
+ }, 4);
279
+ if (windowLen <= cols) break;
280
+ visibleSteps.shift();
281
+ }
282
+ }
283
+
284
+ const parts = visibleSteps.map(s => {
285
+ const label = STEP_LABELS[s] || s;
286
+ if (s === currentStepId) {
287
+ return `${CYAN}${BOLD}\u25C9 ${label}${RESET}`;
288
+ } else if (doneSet.has(s)) {
289
+ return `${GREEN}\u25CF ${label}${RESET}`;
290
+ } else {
291
+ return `${DIM}\u25CB ${label}${RESET}`;
292
+ }
293
+ });
294
+
295
+ this.#stream.write(` ${parts.join(` ${DIM}\u2500${RESET} `)}\n`);
296
+ }
297
+
298
+ subFlowStep(flowName, stepId) {
299
+ this.#clearCollapsed();
300
+ this.#stopHeartbeat();
301
+ this.#stream.write(` ${CYAN}[${flowName}]${RESET} ${stepId}...\n`);
302
+ this.#toolHistory = [];
303
+ this.#stepStart = Date.now();
304
+ this.#currentStepId = `${flowName}/${stepId}`;
305
+ if (!this.#expanded) this.#drawCollapsed();
306
+ this.#startHeartbeat();
307
+ }
308
+
309
+ retry(flowName, stepId, agent) {
310
+ this.#clearCollapsed();
311
+ this.#stream.write(` ${YELLOW}[${flowName}] ↻ Retrying ${stepId}${agent ? ` (${agent})` : ''}${RESET}\n`);
312
+ if (!this.#expanded) this.#drawCollapsed();
313
+ }
314
+
315
+ fix(flowName, fixAgent, stepId) {
316
+ this.#clearCollapsed();
317
+ this.#stream.write(` ${YELLOW}[${flowName}] ↻ Fix (${fixAgent}) for ${stepId}${RESET}\n`);
318
+ if (!this.#expanded) this.#drawCollapsed();
319
+ }
320
+
321
+ warn(msg) {
322
+ this.#clearCollapsed();
323
+ this.#stream.write(` ${RED}⚠ ${msg}${RESET}\n`);
324
+ if (!this.#expanded) this.#drawCollapsed();
325
+ }
326
+
327
+ success(msg) {
328
+ this.#clearCollapsed();
329
+ this.#stream.write(` ${GREEN}✓${RESET} ${msg}\n`);
330
+ if (!this.#expanded) this.#drawCollapsed();
331
+ }
332
+
333
+ info(msg) {
334
+ this.#clearCollapsed();
335
+ this.#stream.write(`${msg}\n`);
336
+ if (!this.#expanded) this.#drawCollapsed();
337
+ }
338
+
339
+ debug(msg) {
340
+ if (!process.env.COMPOSE_DEBUG) return;
341
+ if (this.#expanded) {
342
+ this.#stream.write(` ${DIM}[debug] ${msg}${RESET}\n`);
343
+ }
344
+ }
345
+
346
+ finish() {
347
+ this.#clearCollapsed();
348
+ this.#stopHeartbeat();
349
+ this.#stopListening();
350
+ }
351
+
352
+ // ── Collapsed view: last N tools + heartbeat + hints ──────────────────
353
+
354
+ #drawCollapsed() {
355
+ if (!this.#isTTY) return;
356
+
357
+ // First erase any previously drawn collapsed block
358
+ this.#eraseCollapsedBlock();
359
+
360
+ const lines = [];
361
+
362
+ // Last N tool events
363
+ const recent = this.#toolHistory.slice(-COLLAPSED_LINES);
364
+ for (const t of recent) {
365
+ const color = colorForTool(t.tool);
366
+ lines.push(` ${color}↳ ${t.tool}${RESET}${t.detail ? ': ' + t.detail : ''}`);
367
+ }
368
+
369
+ // Heartbeat / status line
370
+ const elapsed = Math.round((Date.now() - this.#stepStart) / 1000);
371
+ const countStr = this.#toolCount > 0 ? `${this.#toolCount} calls` : 'waiting';
372
+ lines.push(` ${DIM}${this.#currentStepId} · ${elapsed}s · ${countStr}${RESET}`);
373
+
374
+ // Key hints
375
+ lines.push(HINT_BAR);
376
+
377
+ // Write all lines
378
+ for (const line of lines) {
379
+ this.#stream.write(line + '\n');
380
+ }
381
+
382
+ this.#drawnCollapsedLines = lines.length;
383
+ }
384
+
385
+ #clearCollapsed() {
386
+ this.#eraseCollapsedBlock();
387
+ }
388
+
389
+ #eraseCollapsedBlock() {
390
+ if (!this.#isTTY || this.#drawnCollapsedLines === 0) return;
391
+ // Move up and erase each line
392
+ for (let i = 0; i < this.#drawnCollapsedLines; i++) {
393
+ this.#stream.write(MOVE_UP(1) + ERASE_LINE);
394
+ }
395
+ this.#drawnCollapsedLines = 0;
396
+ }
397
+
398
+ // ── Heartbeat ─────────────────────────────────────────────────────────
399
+
400
+ #startHeartbeat() {
401
+ this.#stopHeartbeat();
402
+ this.#heartbeatTimer = setInterval(() => {
403
+ if (!this.#expanded) {
404
+ this.#drawCollapsed();
405
+ } else {
406
+ // In expanded mode, just print elapsed time
407
+ const elapsed = Math.round((Date.now() - this.#stepStart) / 1000);
408
+ this.#stream.write(` ${DIM}… ${this.#currentStepId} (${elapsed}s)${RESET}\n`);
409
+ }
410
+ }, 5_000);
411
+ }
412
+
413
+ #stopHeartbeat() {
414
+ if (this.#heartbeatTimer) {
415
+ clearInterval(this.#heartbeatTimer);
416
+ this.#heartbeatTimer = null;
417
+ }
418
+ }
419
+
420
+ // ── Findings table (COMP-TUI-3) ──────────────────────────────────────
421
+
422
+ findings(items) {
423
+ if (!items || items.length === 0) return;
424
+
425
+ const SEV_COLORS = { 'must-fix': RED, 'should-fix': YELLOW, nit: GRAY };
426
+ const SEV_ORDER = ['must-fix', 'should-fix', 'nit'];
427
+
428
+ // Parse violation strings into structured rows
429
+ const rows = items.map(item => {
430
+ if (typeof item === 'object' && item.severity) {
431
+ return { sev: item.severity, file: item.file ?? '', desc: item.description ?? item.message ?? '' };
432
+ }
433
+ const s = String(item);
434
+ // Try to extract file:line reference and severity
435
+ const fileMatch = s.match(/(\S+\.\w+:\d+)/);
436
+ const file = fileMatch ? fileMatch[1] : '';
437
+ let sev = 'nit';
438
+ if (/must.?fix|error|critical/i.test(s)) sev = 'must-fix';
439
+ else if (/should.?fix|warning/i.test(s)) sev = 'should-fix';
440
+ // Strip severity prefix, file reference, and connectors to get clean description
441
+ const desc = s
442
+ .replace(/^(must[- ]?fix|should[- ]?fix|nit|error|warning|critical)\s*[:]\s*/i, '')
443
+ .replace(fileMatch?.[0] ?? '', '')
444
+ .replace(/^\s*[-:—]\s*/, '')
445
+ .trim() || s;
446
+ return { sev, file, desc };
447
+ });
448
+
449
+ // Sort by severity
450
+ rows.sort((a, b) => SEV_ORDER.indexOf(a.sev) - SEV_ORDER.indexOf(b.sev));
451
+
452
+ // Column widths
453
+ const cols = this.#stream.columns || 100;
454
+ const sevW = Math.max(10, ...rows.map(r => r.sev.length + 2));
455
+ const fileW = Math.max(10, ...rows.map(r => r.file.length + 2));
456
+ const descW = Math.max(10, cols - sevW - fileW - 10); // account for borders + padding
457
+ const totalW = sevW + fileW + descW + 4; // 4 for border chars
458
+
459
+ const pad = (str, w) => str.length >= w ? str.slice(0, w) : str + ' '.repeat(w - str.length);
460
+ const title = ' Review Findings ';
461
+
462
+ this.#clearCollapsed();
463
+
464
+ // Top border
465
+ const topRule = '\u2500'.repeat(Math.max(0, totalW - 2 - title.length));
466
+ this.#stream.write(` \u250C\u2500${BOLD}${title}${RESET}${'\u2500'.repeat(Math.max(0, topRule.length))}\u2510\n`);
467
+
468
+ // Header
469
+ this.#stream.write(` \u2502 ${BOLD}${pad('SEV', sevW)}${RESET}\u2502 ${BOLD}${pad('FILE', fileW)}${RESET}\u2502 ${BOLD}${pad('FINDING', descW)}${RESET}\u2502\n`);
470
+ this.#stream.write(` \u251C${'\u2500'.repeat(sevW + 1)}\u253C${'\u2500'.repeat(fileW + 2)}\u253C${'\u2500'.repeat(descW + 2)}\u2524\n`);
471
+
472
+ // Data rows
473
+ for (const row of rows) {
474
+ const sevColor = SEV_COLORS[row.sev] ?? GRAY;
475
+ this.#stream.write(` \u2502 ${sevColor}${pad(row.sev, sevW)}${RESET}\u2502 ${pad(row.file, fileW)}\u2502 ${pad(row.desc, descW)}\u2502\n`);
476
+ }
477
+
478
+ // Bottom border
479
+ this.#stream.write(` \u2514${'\u2500'.repeat(sevW + 1)}\u2534${'\u2500'.repeat(fileW + 2)}\u2534${'\u2500'.repeat(descW + 2)}\u2518\n`);
480
+
481
+ if (!this.#expanded) this.#drawCollapsed();
482
+ }
483
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Shared constants for Compose CLI and frontend.
3
+ *
4
+ * Single canonical source for step/gate labels.
5
+ * Importable by both CLI (lib/build.js, lib/new.js) and frontend (if bundled).
6
+ */
7
+
8
+ /**
9
+ * Canonical labels for lifecycle steps and gates.
10
+ * Used in summary fallback chains and UI rendering.
11
+ */
12
+ export const STEP_LABELS = {
13
+ // Lifecycle steps
14
+ explore_design: 'Design',
15
+ prd: 'PRD',
16
+ architecture: 'Architecture',
17
+ blueprint: 'Blueprint',
18
+ verification: 'Verification',
19
+ plan: 'Plan',
20
+ execute: 'Execute',
21
+ report: 'Report',
22
+ docs: 'Docs',
23
+ ship: 'Ship',
24
+
25
+ // Gate steps
26
+ design_gate: 'Design Gate',
27
+ prd_gate: 'PRD Gate',
28
+ plan_gate: 'Plan Gate',
29
+ architecture_gate: 'Architecture Gate',
30
+ report_gate: 'Report Gate',
31
+ ship_gate: 'Ship Gate',
32
+ };
33
+
34
+ /**
35
+ * Map gate step IDs to their artifact filenames.
36
+ * Used to derive artifact paths for gate enrichment.
37
+ */
38
+ export const GATE_ARTIFACTS = {
39
+ design_gate: 'design.md',
40
+ prd_gate: 'prd.md',
41
+ architecture_gate: 'architecture.md',
42
+ plan_gate: 'plan.md',
43
+ report_gate: 'report.md',
44
+ };
45
+
46
+ /**
47
+ * Title-case a step ID for display when no label exists.
48
+ * e.g. "some_step" -> "Some Step"
49
+ */
50
+ export function titleCase(stepId) {
51
+ if (!stepId) return 'Unknown';
52
+ return stepId
53
+ .replace(/_/g, ' ')
54
+ .replace(/\b\w/g, c => c.toUpperCase());
55
+ }
56
+
57
+ /**
58
+ * Build a human-readable summary for a gate step.
59
+ * Fallback chain: response.summary -> STEP_LABELS + featureCode -> titleCase -> "Gate: stepId"
60
+ */
61
+ export function buildGateSummary(stepId, featureCode, responseSummary) {
62
+ if (responseSummary) return responseSummary;
63
+ const label = STEP_LABELS[stepId];
64
+ if (label && featureCode) return `${label} for ${featureCode}`;
65
+ if (label) return label;
66
+ const titled = titleCase(stepId);
67
+ if (titled !== 'Unknown') return titled;
68
+ return `Gate: ${stepId}`;
69
+ }