@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,84 @@
1
+ /**
2
+ * cross-layer-audit.js — Cross-layer scope detection for COMP-DEBUG-1.
3
+ *
4
+ * Detects when a bug fix spans multiple repos/layers and should trigger
5
+ * a grep audit before the fix step proceeds.
6
+ *
7
+ * See: docs/features/COMP-DEBUG-1/design.md
8
+ */
9
+
10
+ import { readFileSync, existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+
13
+ const DEFAULT_EXTENSIONS = ['*.py', '*.json', '*.ts', '*.tsx', '*.jsx', '*.yaml'];
14
+
15
+ const CROSS_LAYER_KEYWORDS = [
16
+ /openai/i, /groq/i, /anthropic/i, /gpt-4/i, /llama/i,
17
+ /config\.json/i, /\.env\b/i, /VITE_/,
18
+ /\brenamed?\b/i, /was previously/i, /changed from/i,
19
+ /\bcaddy\b/i, /\bproxy\b/i, /\bnginx\b/i, /\broute\b/i,
20
+ ];
21
+
22
+ /**
23
+ * Load debug discipline config from .compose/compose.json.
24
+ * Returns defaults if no config exists.
25
+ */
26
+ export function loadDebugConfig(cwd) {
27
+ const configPath = join(cwd, '.compose', 'compose.json');
28
+ const defaults = { cross_layer_repos: [], cross_layer_extensions: DEFAULT_EXTENSIONS };
29
+ try {
30
+ if (!existsSync(configPath)) return defaults;
31
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
32
+ const dd = config.debug_discipline ?? {};
33
+ return {
34
+ cross_layer_repos: dd.cross_layer_repos ?? defaults.cross_layer_repos,
35
+ cross_layer_extensions: dd.cross_layer_extensions ?? defaults.cross_layer_extensions,
36
+ };
37
+ } catch {
38
+ return defaults;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Detects whether a diagnose result indicates cross-layer changes
44
+ * that need a scope expansion audit.
45
+ */
46
+ export class CrossLayerAudit {
47
+ constructor(config) {
48
+ this.repos = config.cross_layer_repos ?? [];
49
+ this.extensions = config.cross_layer_extensions ?? DEFAULT_EXTENSIONS;
50
+ }
51
+
52
+ /**
53
+ * Check if the diagnose result warrants scope expansion.
54
+ * @param {object} diagnoseResult - output from diagnose step
55
+ * @returns {{ expand: boolean, trigger: string|null }}
56
+ */
57
+ shouldExpand(diagnoseResult) {
58
+ const hint = diagnoseResult?.scope_hint;
59
+
60
+ // Structured detection (primary)
61
+ if (hint === 'cross-layer') {
62
+ return { expand: true, trigger: 'scope_hint' };
63
+ }
64
+ if (hint === 'single') {
65
+ return { expand: false, trigger: null };
66
+ }
67
+
68
+ // Keyword fallback (when hint is 'unknown' or absent)
69
+ const text = [
70
+ diagnoseResult?.root_cause ?? '',
71
+ diagnoseResult?.summary ?? '',
72
+ ...(diagnoseResult?.affected_layers ?? []),
73
+ ].join(' ');
74
+
75
+ for (const kw of CROSS_LAYER_KEYWORDS) {
76
+ const match = text.match(kw);
77
+ if (match) {
78
+ return { expand: true, trigger: `keyword:${match[0]}` };
79
+ }
80
+ }
81
+
82
+ return { expand: false, trigger: null };
83
+ }
84
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * debug-discipline.js — Debug Discipline Engine for COMP-DEBUG-1.
3
+ *
4
+ * Detects fix-chain thrashing, validates trace evidence, tracks attempts,
5
+ * and audits cross-layer scope. Called from build.js during fix retry loops.
6
+ *
7
+ * See: docs/features/COMP-DEBUG-1/design.md
8
+ */
9
+
10
+ import { appendFileSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Fix-Chain Detector
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /**
18
+ * Tracks which files are modified across fix iterations.
19
+ * Detects thrashing: same file touched in multiple iterations.
20
+ */
21
+ export class FixChainDetector {
22
+ constructor() {
23
+ this.fileHits = new Map();
24
+ this.iteration = 0;
25
+ }
26
+
27
+ recordIteration(filesChanged) {
28
+ this.iteration++;
29
+ for (const file of filesChanged) {
30
+ this.fileHits.set(file, (this.fileHits.get(file) ?? 0) + 1);
31
+ }
32
+ }
33
+
34
+ detect() {
35
+ return [...this.fileHits.entries()]
36
+ .filter(([, count]) => count >= 2)
37
+ .map(([file, count]) => ({
38
+ file,
39
+ iterations: count,
40
+ level: count >= 3 ? 'critical' : 'warning',
41
+ }));
42
+ }
43
+
44
+ toJSON() {
45
+ return {
46
+ iteration: this.iteration,
47
+ fileHits: Object.fromEntries(this.fileHits),
48
+ };
49
+ }
50
+
51
+ static fromJSON(json) {
52
+ const d = new FixChainDetector();
53
+ d.iteration = json.iteration ?? 0;
54
+ d.fileHits = new Map(Object.entries(json.fileHits ?? {}));
55
+ return d;
56
+ }
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Attempt Counter
61
+ // ---------------------------------------------------------------------------
62
+
63
+ const VISUAL_EXTENSIONS = /\.(css|scss|jsx|tsx)$/i;
64
+
65
+ /**
66
+ * Tracks fix attempts and enforces thresholds with escalation.
67
+ * Visual bugs escalate at attempt 2; all bugs escalate at attempt 5.
68
+ */
69
+ export class AttemptCounter {
70
+ constructor() {
71
+ this.count = 0;
72
+ this.isVisual = false;
73
+ }
74
+
75
+ record({ filesChanged = [], isVisual = null }) {
76
+ this.count++;
77
+ if (isVisual !== null) {
78
+ this.isVisual = isVisual;
79
+ } else if (filesChanged.some(f => AttemptCounter.isVisualFile(f))) {
80
+ this.isVisual = true;
81
+ }
82
+ }
83
+
84
+ getIntervention() {
85
+ if (this.count >= 5) return 'escalate';
86
+ if (this.count >= 3 && !this.isVisual) return 'trace_refresh';
87
+ if (this.count >= 2 && this.isVisual) return 'escalate';
88
+ if (this.count >= 2) return 'trace_reminder';
89
+ return null;
90
+ }
91
+
92
+ static isVisualFile(file) {
93
+ return VISUAL_EXTENSIONS.test(file);
94
+ }
95
+
96
+ toJSON() {
97
+ return { count: this.count, isVisual: this.isVisual };
98
+ }
99
+
100
+ static fromJSON(json) {
101
+ const c = new AttemptCounter();
102
+ c.count = json.count ?? 0;
103
+ c.isVisual = json.isVisual ?? false;
104
+ return c;
105
+ }
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Trace Validator
110
+ // ---------------------------------------------------------------------------
111
+
112
+ const MIN_EVIDENCE_ITEMS = 2;
113
+ const MIN_OUTPUT_LENGTH = 5;
114
+
115
+ /**
116
+ * Validates diagnose step output meets trace evidence requirements.
117
+ * Rejects prose-only analysis without concrete command output.
118
+ */
119
+ export class TraceValidator {
120
+ static validate(diagnoseResult) {
121
+ const evidence = diagnoseResult?.trace_evidence;
122
+
123
+ if (!evidence) {
124
+ return { valid: false, reason: 'trace_evidence is missing or null' };
125
+ }
126
+ if (!Array.isArray(evidence) || evidence.length < MIN_EVIDENCE_ITEMS) {
127
+ return { valid: false, reason: `trace_evidence requires minimum ${MIN_EVIDENCE_ITEMS} items, got ${evidence?.length ?? 0}` };
128
+ }
129
+
130
+ for (const [i, e] of evidence.entries()) {
131
+ if (!e.command) {
132
+ return { valid: false, reason: `trace_evidence[${i}] missing command field` };
133
+ }
134
+ if (!e.actual_output) {
135
+ return { valid: false, reason: `trace_evidence[${i}] missing actual_output field` };
136
+ }
137
+ }
138
+
139
+ const hasSubstantialOutput = evidence.some(e =>
140
+ typeof e.actual_output === 'string' && e.actual_output.length > MIN_OUTPUT_LENGTH
141
+ );
142
+ if (!hasSubstantialOutput) {
143
+ return { valid: false, reason: `no trace evidence has output longer than ${MIN_OUTPUT_LENGTH} chars` };
144
+ }
145
+
146
+ if (!diagnoseResult.root_cause) {
147
+ return { valid: false, reason: 'root_cause is missing — connect evidence to a conclusion' };
148
+ }
149
+
150
+ return { valid: true };
151
+ }
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Debug Ledger (file-based, upgrades to COMP-HARNESS-9 later)
156
+ // ---------------------------------------------------------------------------
157
+
158
+ const LEDGER_FILE = 'debug-ledger.jsonl';
159
+
160
+ /**
161
+ * Append-only JSONL ledger for debug discipline events.
162
+ * Writes to .compose/debug-ledger.jsonl.
163
+ */
164
+ export class DebugLedger {
165
+ constructor(composeDir) {
166
+ this.path = join(composeDir, LEDGER_FILE);
167
+ }
168
+
169
+ record(entry) {
170
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...entry });
171
+ appendFileSync(this.path, line + '\n', 'utf-8');
172
+ }
173
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * feature-json.js — Read, write, and list feature.json files.
3
+ *
4
+ * Each feature lives at docs/features/<CODE>/feature.json.
5
+ * feature.json is the machine-readable source of truth.
6
+ * ROADMAP.md is generated from these files.
7
+ */
8
+
9
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
10
+ import { join, basename } from 'path';
11
+ import { readdirSync } from 'fs';
12
+
13
+ /**
14
+ * @typedef {object} FeatureJson
15
+ * @property {string} code
16
+ * @property {string} description
17
+ * @property {string} status - PLANNED | IN_PROGRESS | PARTIAL | COMPLETE | SUPERSEDED | PARKED
18
+ * @property {string} [parent] - Parent feature/phase code (e.g., "STRAT-1", "Phase 6")
19
+ * @property {string} [phase] - Phase heading for ROADMAP grouping
20
+ * @property {number} [position] - Sort order within phase
21
+ * @property {string} [complexity] - low | medium | high (from scope step)
22
+ * @property {object} [profile] - BuildProfile from scope step
23
+ * @property {string} [created] - ISO date
24
+ * @property {string} [updated] - ISO date
25
+ */
26
+
27
+ /**
28
+ * Read a single feature.json.
29
+ *
30
+ * @param {string} cwd - Project root
31
+ * @param {string} code - Feature code
32
+ * @param {string} [featuresDir] - Relative path to features dir (default: docs/features)
33
+ * @returns {FeatureJson|null}
34
+ */
35
+ export function readFeature(cwd, code, featuresDir = 'docs/features') {
36
+ const path = join(cwd, featuresDir, code, 'feature.json');
37
+ if (!existsSync(path)) return null;
38
+ try {
39
+ return JSON.parse(readFileSync(path, 'utf-8'));
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Write a feature.json, creating the directory if needed.
47
+ *
48
+ * @param {string} cwd - Project root
49
+ * @param {FeatureJson} feature
50
+ * @param {string} [featuresDir]
51
+ */
52
+ export function writeFeature(cwd, feature, featuresDir = 'docs/features') {
53
+ const dir = join(cwd, featuresDir, feature.code);
54
+ mkdirSync(dir, { recursive: true });
55
+ const path = join(dir, 'feature.json');
56
+ feature.updated = new Date().toISOString().slice(0, 10);
57
+ writeFileSync(path, JSON.stringify(feature, null, 2) + '\n');
58
+ }
59
+
60
+ /**
61
+ * List all feature.json files in the features directory.
62
+ *
63
+ * @param {string} cwd - Project root
64
+ * @param {string} [featuresDir]
65
+ * @returns {FeatureJson[]}
66
+ */
67
+ export function listFeatures(cwd, featuresDir = 'docs/features') {
68
+ const dir = join(cwd, featuresDir);
69
+ if (!existsSync(dir)) return [];
70
+
71
+ const features = [];
72
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
73
+ if (!entry.isDirectory()) continue;
74
+ const fjPath = join(dir, entry.name, 'feature.json');
75
+ if (!existsSync(fjPath)) continue;
76
+ try {
77
+ features.push(JSON.parse(readFileSync(fjPath, 'utf-8')));
78
+ } catch { /* skip malformed */ }
79
+ }
80
+
81
+ // Sort by phase, then position, then code
82
+ features.sort((a, b) => {
83
+ if (a.phase !== b.phase) return (a.phase ?? '').localeCompare(b.phase ?? '');
84
+ if ((a.position ?? 999) !== (b.position ?? 999)) return (a.position ?? 999) - (b.position ?? 999);
85
+ return a.code.localeCompare(b.code);
86
+ });
87
+
88
+ return features;
89
+ }
90
+
91
+ /**
92
+ * Update a feature's status (and optionally other fields).
93
+ *
94
+ * @param {string} cwd
95
+ * @param {string} code
96
+ * @param {Partial<FeatureJson>} updates
97
+ * @param {string} [featuresDir]
98
+ * @returns {FeatureJson|null} - Updated feature, or null if not found
99
+ */
100
+ export function updateFeature(cwd, code, updates, featuresDir = 'docs/features') {
101
+ const feature = readFeature(cwd, code, featuresDir);
102
+ if (!feature) return null;
103
+ Object.assign(feature, updates);
104
+ writeFeature(cwd, feature, featuresDir);
105
+ return feature;
106
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * gate-prompt.js — CLI readline interface for gate resolution.
3
+ *
4
+ * Prompts the user to approve, revise, or kill a gate dispatch,
5
+ * with interactive Q&A: typing a question dispatches it to an agent
6
+ * that reads the artifact and answers.
7
+ */
8
+ import { createInterface } from 'node:readline';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // ANSI helpers
12
+ // ---------------------------------------------------------------------------
13
+
14
+ const ESC = '\x1b[';
15
+ const RESET = `${ESC}0m`;
16
+ const BOLD = `${ESC}1m`;
17
+ const DIM = `${ESC}2m`;
18
+ const CYAN = `${ESC}36m`;
19
+ const GREEN = `${ESC}32m`;
20
+ const RED = `${ESC}31m`;
21
+ const YELLOW = `${ESC}33m`;
22
+ const GRAY = `${ESC}90m`;
23
+
24
+ const OUTCOME_MAP = {
25
+ a: 'approve',
26
+ approve: 'approve',
27
+ r: 'revise',
28
+ revise: 'revise',
29
+ k: 'kill',
30
+ kill: 'kill',
31
+ };
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // COMP-UX-3b: Generate a recommendation sentence and default action from the
35
+ // gate's artifact assessment (if present in gateExtras.artifactAssessment).
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Build a 1-sentence recommendation from gate metadata.
40
+ * Returns { sentence, defaultOutcome } where defaultOutcome is 'approve' or 'revise'.
41
+ */
42
+ function buildRecommendation(gateDispatch, gateExtras) {
43
+ const assessment = gateExtras?.artifactAssessment;
44
+ const summary = gateExtras?.summary;
45
+
46
+ // If there's an explicit summary from Stratum, use it
47
+ // Avoid false positives: "No critical findings" should NOT trigger revise
48
+ if (summary) {
49
+ const hasIssue = /(?<!\bno\s)(?<!\b0\s)\b(critical|error|fail|missing)\b/i.test(summary);
50
+ const defaultOutcome = hasIssue ? 'revise' : 'approve';
51
+ const action = defaultOutcome === 'approve' ? 'Ship it?' : 'Revise?';
52
+ return { sentence: `${summary} ${action}`, defaultOutcome };
53
+ }
54
+
55
+ if (!assessment) {
56
+ return { sentence: null, defaultOutcome: null };
57
+ }
58
+
59
+ // Missing artifact — never recommend approve
60
+ if (assessment.exists === false) {
61
+ return { sentence: 'Required artifact is missing. Revise?', defaultOutcome: 'revise' };
62
+ }
63
+
64
+ const { completeness, wordCount, sections, meetsMinWordCount, findings } = assessment;
65
+
66
+ // Count critical findings
67
+ const criticalCount = (findings ?? []).filter(
68
+ f => /critical|error|fatal/i.test(f.severity ?? f.level ?? '')
69
+ ).length;
70
+ const findingCount = (findings ?? []).length;
71
+ const missingCount = sections?.missing?.length ?? 0;
72
+
73
+ if (criticalCount > 0) {
74
+ return {
75
+ sentence: `${criticalCount} critical finding${criticalCount > 1 ? 's' : ''}. Revise?`,
76
+ defaultOutcome: 'revise',
77
+ };
78
+ }
79
+
80
+ if (!meetsMinWordCount && wordCount !== undefined) {
81
+ return {
82
+ sentence: `Artifact is thin (${wordCount} words). Revise?`,
83
+ defaultOutcome: 'revise',
84
+ };
85
+ }
86
+
87
+ if (missingCount > 0) {
88
+ return {
89
+ sentence: `Missing ${missingCount} section${missingCount > 1 ? 's' : ''} (${(sections.missing ?? []).slice(0, 2).join(', ')}). Revise?`,
90
+ defaultOutcome: 'revise',
91
+ };
92
+ }
93
+
94
+ if (findingCount > 0) {
95
+ return {
96
+ sentence: `${findingCount} finding${findingCount > 1 ? 's' : ''}, ${Math.round((completeness ?? 1) * 100)}% complete. Ship it?`,
97
+ defaultOutcome: 'approve',
98
+ };
99
+ }
100
+
101
+ const pct = completeness !== undefined ? `${Math.round(completeness * 100)}% complete` : null;
102
+ const wc = wordCount !== undefined ? `${wordCount} words` : null;
103
+ const detail = [pct, wc].filter(Boolean).join(', ');
104
+ return {
105
+ sentence: detail ? `${detail}. Ship it?` : 'Ready to proceed? Ship it?',
106
+ defaultOutcome: 'approve',
107
+ };
108
+ }
109
+
110
+ function ask(rl, question) {
111
+ return new Promise(resolve => rl.question(question, resolve));
112
+ }
113
+
114
+ /**
115
+ * Render a boxed gate panel with artifact, phase, and action info.
116
+ * COMP-UX-3b: Shows a recommendation sentence and accepts Enter as default.
117
+ */
118
+ function drawGatePanel(out, gateDispatch, { artifact, gateExtras } = {}) {
119
+ const { step_id, on_approve, on_revise, on_kill } = gateDispatch;
120
+ const cols = (out.columns ?? process.stdout.columns) || 80;
121
+ const innerW = Math.max(40, Math.min(cols - 6, 70));
122
+
123
+ const pad = (str, w) => {
124
+ const plainLen = str.replace(/\x1b\[[0-9;]*m/g, '').length;
125
+ return plainLen >= w ? str : str + ' '.repeat(w - plainLen);
126
+ };
127
+
128
+ const line = (content) => ` \u2502 ${pad(content, innerW)} \u2502`;
129
+ const empty = line('');
130
+ const topLabel = ` Gate: ${step_id} `;
131
+ const topRule = '\u2500'.repeat(Math.max(0, innerW + 2 - topLabel.length - 1));
132
+ const bottomRule = '\u2500'.repeat(innerW + 2);
133
+
134
+ // COMP-UX-3b: build recommendation
135
+ const { sentence: recSentence, defaultOutcome } = buildRecommendation(gateDispatch, gateExtras);
136
+ const recColor = defaultOutcome === 'approve' ? GREEN : YELLOW;
137
+ const defaultLabel = defaultOutcome === 'approve' ? 'a' : 'r';
138
+
139
+ out.write(` \u250C\u2500${BOLD}${CYAN}${topLabel}${RESET}${topRule}\u2510\n`);
140
+ out.write(empty + '\n');
141
+
142
+ if (artifact) {
143
+ out.write(line(`${DIM}Artifact:${RESET} ${artifact}`) + '\n');
144
+ }
145
+
146
+ if (gateExtras) {
147
+ const from = gateExtras.fromPhase ?? '?';
148
+ const to = gateExtras.toPhase ?? '?';
149
+ out.write(line(`${DIM}Phase:${RESET} ${from} \u2192 ${to}`) + '\n');
150
+ }
151
+
152
+ // Recommendation line
153
+ if (recSentence) {
154
+ out.write(empty + '\n');
155
+ out.write(line(`${recColor}${BOLD}${recSentence}${RESET}`) + '\n');
156
+ out.write(line(`${DIM}[Enter] = ${defaultOutcome} [d] = show details${RESET}`) + '\n');
157
+ }
158
+
159
+ out.write(empty + '\n');
160
+ out.write(line(`${GREEN}[a]${RESET} Approve \u2192 ${on_approve ?? '(complete)'}`) + '\n');
161
+ out.write(line(`${YELLOW}[r]${RESET} Revise \u2192 ${on_revise ?? '(kill)'}`) + '\n');
162
+ out.write(line(`${RED}[k]${RESET} Kill \u2192 ${on_kill ?? '(terminate)'}`) + '\n');
163
+ out.write(empty + '\n');
164
+ out.write(line(`${DIM}Type a question to ask the agent.${RESET}`) + '\n');
165
+ out.write(` \u2514${bottomRule}\u2518\n`);
166
+
167
+ return { defaultOutcome, defaultLabel };
168
+ }
169
+
170
+ /**
171
+ * Prompt the user to resolve a gate dispatch.
172
+ *
173
+ * @param {object} gateDispatch - Gate dispatch with step_id, on_approve, on_revise, on_kill
174
+ * @param {object} [options]
175
+ * @param {NodeJS.ReadableStream} [options.input]
176
+ * @param {NodeJS.WritableStream} [options.output]
177
+ * @param {string} [options.artifact] - Path to the artifact being reviewed
178
+ * @param {Function} [options.askAgent] - async (question, artifact) => answer string
179
+ * @param {object} [options.gateExtras] - { fromPhase, toPhase } for panel display
180
+ * @returns {Promise<{ outcome: string, rationale: string }>}
181
+ */
182
+ export async function promptGate(gateDispatch, { input, output, artifact, askAgent, gateExtras, nonInteractive } = {}) {
183
+ if (nonInteractive) {
184
+ return { outcome: 'approve', rationale: 'auto-approved (--all mode)' };
185
+ }
186
+
187
+ const rl = createInterface({
188
+ input: input ?? process.stdin,
189
+ output: output ?? process.stdout,
190
+ });
191
+
192
+ try {
193
+ const { step_id, on_approve, on_revise, on_kill } = gateDispatch;
194
+
195
+ // COMP-UX-3b: drawGatePanel returns recommendation context
196
+ const { defaultOutcome } = drawGatePanel(rl.output, gateDispatch, { artifact, gateExtras });
197
+
198
+ // Full assessment detail for 'd' key
199
+ const fullDetail = (() => {
200
+ const assessment = gateExtras?.artifactAssessment;
201
+ if (!assessment) return null;
202
+ const lines = [];
203
+ if (assessment.wordCount !== undefined) lines.push(`Words: ${assessment.wordCount}`);
204
+ if (assessment.completeness !== undefined) lines.push(`Completeness: ${Math.round(assessment.completeness * 100)}%`);
205
+ if (assessment.sections?.missing?.length) lines.push(`Missing sections: ${assessment.sections.missing.join(', ')}`);
206
+ if (!assessment.meetsMinWordCount) lines.push('Below minimum word count');
207
+ const findings = assessment.findings ?? [];
208
+ if (findings.length) {
209
+ lines.push(`Findings (${findings.length}):`);
210
+ for (const f of findings.slice(0, 5)) {
211
+ lines.push(` - ${f.severity ?? f.level ?? '?'}: ${f.message ?? f.text ?? JSON.stringify(f)}`);
212
+ }
213
+ if (findings.length > 5) lines.push(` ... and ${findings.length - 5} more`);
214
+ }
215
+ return lines.length ? lines.join('\n') : null;
216
+ })();
217
+
218
+ const notes = [];
219
+ let outcome;
220
+
221
+ while (!outcome) {
222
+ const raw = await ask(rl, '\n> ');
223
+ const trimmed = raw.trim();
224
+
225
+ // COMP-UX-3b: Enter alone → use recommended default action (only if recommendation was shown)
226
+ if (!trimmed && defaultOutcome) {
227
+ outcome = defaultOutcome;
228
+ rl.output.write(` (using recommended: ${defaultOutcome})\n`);
229
+ continue;
230
+ }
231
+ if (!trimmed) continue; // no recommendation — ignore bare Enter
232
+
233
+ const key = trimmed.toLowerCase();
234
+
235
+ // COMP-UX-3b: 'd' shows full artifact detail
236
+ if (key === 'd' || key === 'detail' || key === 'details') {
237
+ if (fullDetail) {
238
+ rl.output.write('\n Artifact detail:\n');
239
+ for (const l of fullDetail.split('\n')) rl.output.write(` ${l}\n`);
240
+ } else {
241
+ rl.output.write(' (no artifact assessment available)\n');
242
+ }
243
+ continue;
244
+ }
245
+
246
+ if (OUTCOME_MAP[key]) {
247
+ outcome = OUTCOME_MAP[key];
248
+ } else if (askAgent) {
249
+ // Dispatch question to agent
250
+ rl.output.write(' Asking agent...\n');
251
+ try {
252
+ const answer = await askAgent(trimmed, artifact);
253
+ rl.output.write(`\n ${answer}\n`);
254
+ } catch (err) {
255
+ rl.output.write(` (agent error: ${err.message})\n`);
256
+ }
257
+ notes.push(trimmed);
258
+ } else {
259
+ // No agent available — just collect as notes
260
+ notes.push(trimmed);
261
+ rl.output.write(' (noted — enter a/r/k when ready to decide)\n');
262
+ }
263
+ }
264
+
265
+ // Build rationale
266
+ let rationale;
267
+ if (notes.length > 0) {
268
+ rationale = notes.join('\n');
269
+ const addMore = await ask(rl, ' Additional rationale (or Enter to use notes): ');
270
+ if (addMore.trim()) {
271
+ rationale += '\n' + addMore.trim();
272
+ }
273
+ } else if (outcome === 'approve') {
274
+ rationale = 'approved';
275
+ } else {
276
+ while (!rationale) {
277
+ const raw = await ask(rl, 'Rationale: ');
278
+ const trimmed = raw.trim();
279
+ if (trimmed) {
280
+ rationale = trimmed;
281
+ } else {
282
+ rl.output.write('Rationale required.\n');
283
+ }
284
+ }
285
+ }
286
+
287
+ return { outcome, rationale };
288
+ } finally {
289
+ rl.close();
290
+ }
291
+ }