@smartmemory/compose 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1014 -0
- package/bin/compose.js +1515 -0
- package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
- package/dist/assets/arc-SxJ2J1sh.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
- package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
- package/dist/assets/channel-DGElom1e.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
- package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
- package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
- package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
- package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
- package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
- package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
- package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
- package/dist/assets/clone-DUJKJXd7.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
- package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
- package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
- package/dist/assets/graph-D0Cfv00Y.js +1 -0
- package/dist/assets/index-CUd6pFGF.css +1 -0
- package/dist/assets/index-DReRlzZI.js +1144 -0
- package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
- package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
- package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-Bj72wOEB.js +1 -0
- package/dist/assets/linear-BRFo114D.js +1 -0
- package/dist/assets/min-GCHnKlJS.js +1 -0
- package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
- package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
- package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
- package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
- package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
- package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
- package/dist/index.html +30 -0
- package/lib/agent-chains.js +65 -0
- package/lib/agent-string.js +86 -0
- package/lib/budget-ledger.js +86 -0
- package/lib/build-all.js +162 -0
- package/lib/build-dag.js +120 -0
- package/lib/build-stream-writer.js +190 -0
- package/lib/build.js +2997 -0
- package/lib/capability-checker.js +53 -0
- package/lib/cert-inject.js +38 -0
- package/lib/cli-progress.js +483 -0
- package/lib/constants.js +69 -0
- package/lib/cross-layer-audit.js +84 -0
- package/lib/debug-discipline.js +173 -0
- package/lib/feature-json.js +106 -0
- package/lib/gate-prompt.js +291 -0
- package/lib/gate-tiers.js +194 -0
- package/lib/health-history.js +119 -0
- package/lib/health-score.js +227 -0
- package/lib/ideabox.js +570 -0
- package/lib/import.js +244 -0
- package/lib/migrate-roadmap.js +94 -0
- package/lib/model-pricing.js +67 -0
- package/lib/new.js +413 -0
- package/lib/pipeline-cli.js +489 -0
- package/lib/plan-parser.js +103 -0
- package/lib/qa-scoping.js +474 -0
- package/lib/questionnaire.js +200 -0
- package/lib/resolve-port.js +7 -0
- package/lib/result-normalizer.js +349 -0
- package/lib/review-lenses.js +166 -0
- package/lib/roadmap-gen.js +210 -0
- package/lib/roadmap-parser.js +176 -0
- package/lib/server-probe.js +23 -0
- package/lib/staleness.js +87 -0
- package/lib/step-prompt.js +260 -0
- package/lib/step-validator.js +49 -0
- package/lib/stratum-mcp-client.js +365 -0
- package/lib/team-flag.js +46 -0
- package/lib/test-bootstrap.js +401 -0
- package/lib/triage.js +274 -0
- package/lib/vision-writer.js +391 -0
- package/package.json +111 -0
- package/pipelines/bug-fix.stratum.yaml +230 -0
- package/pipelines/build.stratum.yaml +498 -0
- package/pipelines/content.stratum.yaml +112 -0
- package/pipelines/coverage-sweep.stratum.yaml +52 -0
- package/pipelines/refactor.stratum.yaml +169 -0
- package/pipelines/research.stratum.yaml +88 -0
- package/pipelines/review-fix.stratum.yaml +109 -0
- package/presets/team-feature.stratum.yaml +105 -0
- package/presets/team-research.stratum.yaml +108 -0
- package/presets/team-review.stratum.yaml +106 -0
- package/scripts/agent-activity-hook.sh +31 -0
- package/scripts/agent-error-hook.sh +28 -0
- package/scripts/analyze-orphans.mjs +50 -0
- package/scripts/find-orphans.mjs +26 -0
- package/scripts/fix-phases.mjs +49 -0
- package/scripts/generate-stratum-spec.mjs +137 -0
- package/scripts/import-roadmap.mjs +116 -0
- package/scripts/phase-audit.mjs +33 -0
- package/scripts/run-pipeline.mjs +314 -0
- package/scripts/session-end-hook.sh +18 -0
- package/scripts/session-start-hook.sh +38 -0
- package/scripts/vision-hook.sh +104 -0
- package/scripts/vision-track.mjs +554 -0
- package/scripts/wire-all-orphans.mjs +108 -0
- package/scripts/wire-orphans.mjs +164 -0
- package/server/activity-routes.js +123 -0
- package/server/agent-health.js +197 -0
- package/server/agent-hooks.js +102 -0
- package/server/agent-mcp.js +10 -0
- package/server/agent-registry.js +95 -0
- package/server/agent-server.js +290 -0
- package/server/agent-spawn.js +251 -0
- package/server/agent-templates.js +77 -0
- package/server/artifact-manager.js +247 -0
- package/server/artifact-templates/architecture.md +28 -0
- package/server/artifact-templates/blueprint.md +21 -0
- package/server/artifact-templates/design.md +36 -0
- package/server/artifact-templates/plan.md +25 -0
- package/server/artifact-templates/prd.md +43 -0
- package/server/artifact-templates/report.md +40 -0
- package/server/block-tracker.js +90 -0
- package/server/build-stream-bridge.js +502 -0
- package/server/coalescing-buffer.js +46 -0
- package/server/compose-mcp-tools.js +479 -0
- package/server/compose-mcp.js +324 -0
- package/server/connectors/agent-connector.js +78 -0
- package/server/connectors/claude-sdk-connector.js +198 -0
- package/server/connectors/codex-connector.js +240 -0
- package/server/connectors/connector-discovery.js +18 -0
- package/server/connectors/connector-runtime.js +13 -0
- package/server/connectors/opencode-connector.js +200 -0
- package/server/design-routes.js +540 -0
- package/server/design-session.js +161 -0
- package/server/feature-scan.js +593 -0
- package/server/file-watcher.js +284 -0
- package/server/find-root.js +29 -0
- package/server/graph-export.js +343 -0
- package/server/ideabox-cache.js +77 -0
- package/server/ideabox-routes.js +294 -0
- package/server/index.js +156 -0
- package/server/model-tiers.js +49 -0
- package/server/pipeline-routes.js +288 -0
- package/server/policy-evaluator.js +36 -0
- package/server/project-root.js +122 -0
- package/server/security.js +23 -0
- package/server/session-manager.js +403 -0
- package/server/session-routes.js +190 -0
- package/server/session-store.js +107 -0
- package/server/settings-routes.js +35 -0
- package/server/settings-store.js +234 -0
- package/server/stratum-api.js +102 -0
- package/server/stratum-client.js +192 -0
- package/server/stratum-sync.js +193 -0
- package/server/summarizer.js +139 -0
- package/server/supervisor.js +196 -0
- package/server/vision-routes.js +668 -0
- package/server/vision-server.js +393 -0
- package/server/vision-store.js +360 -0
- package/server/vision-utils.js +179 -0
- package/server/worktree-gc.js +137 -0
- package/templates/ROADMAP.md +46 -0
|
@@ -0,0 +1,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
|
+
}
|