stoa-mcp 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 +65 -0
- package/README.md +397 -0
- package/dist/cli/build.d.ts +39 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +288 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/review-loop.d.ts +2 -0
- package/dist/cli/review-loop.d.ts.map +1 -0
- package/dist/cli/review-loop.js +97 -0
- package/dist/cli/review-loop.js.map +1 -0
- package/dist/cli/scenarios-runner.d.ts +12 -0
- package/dist/cli/scenarios-runner.d.ts.map +1 -0
- package/dist/cli/scenarios-runner.js +158 -0
- package/dist/cli/scenarios-runner.js.map +1 -0
- package/dist/cli/verify.d.ts +13 -0
- package/dist/cli/verify.d.ts.map +1 -0
- package/dist/cli/verify.js +149 -0
- package/dist/cli/verify.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1135 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/parsers.d.ts +29 -0
- package/dist/core/parsers.d.ts.map +1 -0
- package/dist/core/parsers.js +296 -0
- package/dist/core/parsers.js.map +1 -0
- package/dist/core/parsers.test.d.ts +2 -0
- package/dist/core/parsers.test.d.ts.map +1 -0
- package/dist/core/parsers.test.js +198 -0
- package/dist/core/parsers.test.js.map +1 -0
- package/dist/core/prompts.d.ts +30 -0
- package/dist/core/prompts.d.ts.map +1 -0
- package/dist/core/prompts.js +346 -0
- package/dist/core/prompts.js.map +1 -0
- package/dist/core/refine.d.ts +38 -0
- package/dist/core/refine.d.ts.map +1 -0
- package/dist/core/refine.js +233 -0
- package/dist/core/refine.js.map +1 -0
- package/dist/core/spec-score.d.ts +17 -0
- package/dist/core/spec-score.d.ts.map +1 -0
- package/dist/core/spec-score.js +59 -0
- package/dist/core/spec-score.js.map +1 -0
- package/dist/formatters/index.d.ts +2 -0
- package/dist/formatters/index.d.ts.map +1 -0
- package/dist/formatters/index.js +2 -0
- package/dist/formatters/index.js.map +1 -0
- package/dist/formatters/stage-formatters.d.ts +10 -0
- package/dist/formatters/stage-formatters.d.ts.map +1 -0
- package/dist/formatters/stage-formatters.js +100 -0
- package/dist/formatters/stage-formatters.js.map +1 -0
- package/dist/formatters/stage-formatters.test.d.ts +2 -0
- package/dist/formatters/stage-formatters.test.d.ts.map +1 -0
- package/dist/formatters/stage-formatters.test.js +107 -0
- package/dist/formatters/stage-formatters.test.js.map +1 -0
- package/dist/guardrails/index.d.ts +2 -0
- package/dist/guardrails/index.d.ts.map +1 -0
- package/dist/guardrails/index.js +2 -0
- package/dist/guardrails/index.js.map +1 -0
- package/dist/guardrails/loader.d.ts +9 -0
- package/dist/guardrails/loader.d.ts.map +1 -0
- package/dist/guardrails/loader.js +56 -0
- package/dist/guardrails/loader.js.map +1 -0
- package/dist/guardrails/refine.d.ts +53 -0
- package/dist/guardrails/refine.d.ts.map +1 -0
- package/dist/guardrails/refine.js +184 -0
- package/dist/guardrails/refine.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +196 -0
- package/dist/index.js.map +1 -0
- package/dist/storage/change-detection.d.ts +2 -0
- package/dist/storage/change-detection.d.ts.map +1 -0
- package/dist/storage/change-detection.js +47 -0
- package/dist/storage/change-detection.js.map +1 -0
- package/dist/storage/change-detection.test.d.ts +2 -0
- package/dist/storage/change-detection.test.d.ts.map +1 -0
- package/dist/storage/change-detection.test.js +81 -0
- package/dist/storage/change-detection.test.js.map +1 -0
- package/dist/storage/index.d.ts +9 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +6 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/moodboard-describe.d.ts +14 -0
- package/dist/storage/moodboard-describe.d.ts.map +1 -0
- package/dist/storage/moodboard-describe.js +185 -0
- package/dist/storage/moodboard-describe.js.map +1 -0
- package/dist/storage/moodboard-sync.d.ts +11 -0
- package/dist/storage/moodboard-sync.d.ts.map +1 -0
- package/dist/storage/moodboard-sync.js +205 -0
- package/dist/storage/moodboard-sync.js.map +1 -0
- package/dist/storage/moodboard.d.ts +4 -0
- package/dist/storage/moodboard.d.ts.map +1 -0
- package/dist/storage/moodboard.js +68 -0
- package/dist/storage/moodboard.js.map +1 -0
- package/dist/storage/moodboard.test.d.ts +2 -0
- package/dist/storage/moodboard.test.d.ts.map +1 -0
- package/dist/storage/moodboard.test.js +133 -0
- package/dist/storage/moodboard.test.js.map +1 -0
- package/dist/storage/project-scan.d.ts +12 -0
- package/dist/storage/project-scan.d.ts.map +1 -0
- package/dist/storage/project-scan.js +118 -0
- package/dist/storage/project-scan.js.map +1 -0
- package/dist/storage/project.d.ts +10 -0
- package/dist/storage/project.d.ts.map +1 -0
- package/dist/storage/project.js +101 -0
- package/dist/storage/project.js.map +1 -0
- package/dist/storage/roles-refine.d.ts +59 -0
- package/dist/storage/roles-refine.d.ts.map +1 -0
- package/dist/storage/roles-refine.js +223 -0
- package/dist/storage/roles-refine.js.map +1 -0
- package/dist/storage/roles.d.ts +6 -0
- package/dist/storage/roles.d.ts.map +1 -0
- package/dist/storage/roles.js +41 -0
- package/dist/storage/roles.js.map +1 -0
- package/dist/storage/scenarios-refine.d.ts +47 -0
- package/dist/storage/scenarios-refine.d.ts.map +1 -0
- package/dist/storage/scenarios-refine.js +311 -0
- package/dist/storage/scenarios-refine.js.map +1 -0
- package/dist/storage/scenarios.d.ts +12 -0
- package/dist/storage/scenarios.d.ts.map +1 -0
- package/dist/storage/scenarios.js +37 -0
- package/dist/storage/scenarios.js.map +1 -0
- package/dist/storage/specs.d.ts +17 -0
- package/dist/storage/specs.d.ts.map +1 -0
- package/dist/storage/specs.js +104 -0
- package/dist/storage/specs.js.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/rerefine.d.ts +8 -0
- package/dist/tools/rerefine.d.ts.map +1 -0
- package/dist/tools/rerefine.js +153 -0
- package/dist/tools/rerefine.js.map +1 -0
- package/dist/tools/rerefine.test.d.ts +2 -0
- package/dist/tools/rerefine.test.d.ts.map +1 -0
- package/dist/tools/rerefine.test.js +123 -0
- package/dist/tools/rerefine.test.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/slug.d.ts +3 -0
- package/dist/utils/slug.d.ts.map +1 -0
- package/dist/utils/slug.js +33 -0
- package/dist/utils/slug.js.map +1 -0
- package/dist/utils/spec-helpers.d.ts +12 -0
- package/dist/utils/spec-helpers.d.ts.map +1 -0
- package/dist/utils/spec-helpers.js +95 -0
- package/dist/utils/spec-helpers.js.map +1 -0
- package/dist/utils/spec-helpers.test.d.ts +2 -0
- package/dist/utils/spec-helpers.test.d.ts.map +1 -0
- package/dist/utils/spec-helpers.test.js +114 -0
- package/dist/utils/spec-helpers.test.js.map +1 -0
- package/package.json +53 -0
- package/templates/guardrails/ask-when-unclear.md +3 -0
- package/templates/guardrails/dont-delete-code.md +3 -0
- package/templates/guardrails/explain-changes.md +3 -0
- package/templates/guardrails/run-tests.md +3 -0
- package/templates/guardrails/small-changes.md +3 -0
- package/templates/roles/builder.md +3 -0
- package/templates/roles/fixer.md +3 -0
- package/templates/roles/planner.md +3 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { readFile, writeFile, mkdir, access } from "node:fs/promises";
|
|
6
|
+
import { basename, join } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { existsSync, readFileSync, constants } from "node:fs";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { refinePipeline } from "./core/refine.js";
|
|
11
|
+
import { computeSpecScore } from "./core/spec-score.js";
|
|
12
|
+
import { initProject, loadConfig } from "./storage/project.js";
|
|
13
|
+
import { listGuardrails, addGuardrail, showGuardrail, removeGuardrail, } from "./guardrails/loader.js";
|
|
14
|
+
import { refinePipeline as refineGuardrail } from "./guardrails/refine.js";
|
|
15
|
+
import { refinePipeline as refineRole } from "./storage/roles-refine.js";
|
|
16
|
+
import { toSlug, resolveSlug } from "./utils/slug.js";
|
|
17
|
+
import { readMoodboard, writeSpecFiles, writeRefineMeta, listSpecs, showSpec } from "./storage/index.js";
|
|
18
|
+
import { listRoles, addRole, showRole, removeRole, loadRole, } from "./storage/roles.js";
|
|
19
|
+
import { listScenarios, showScenarios, addScenario, removeScenario, } from "./storage/scenarios.js";
|
|
20
|
+
import { generateScenarios } from "./storage/scenarios-refine.js";
|
|
21
|
+
import { resolveSpecName, snapshotSpecFiles } from "./utils/spec-helpers.js";
|
|
22
|
+
import { runReviewLoop } from "./cli/review-loop.js";
|
|
23
|
+
import { composePrompt, runBuild, parseSubtasks, promptSubtaskChoice } from "./cli/build.js";
|
|
24
|
+
import { runVerify } from "./cli/verify.js";
|
|
25
|
+
import { detectChanges } from "./storage/change-detection.js";
|
|
26
|
+
import { promptAndRerun } from "./tools/rerefine.js";
|
|
27
|
+
import { scanProject } from "./storage/project-scan.js";
|
|
28
|
+
import { syncMoodboard } from "./storage/moodboard-sync.js";
|
|
29
|
+
import { describeMoodboard } from "./storage/moodboard-describe.js";
|
|
30
|
+
import { runSpecScenarios } from "./cli/scenarios-runner.js";
|
|
31
|
+
import { spawn } from "node:child_process";
|
|
32
|
+
// ── Constants ─────────────────────────────────────────────────────────
|
|
33
|
+
const STAGE_NAMES = ["clarify", "structure", "score", "harden", "finalize"];
|
|
34
|
+
const STAGE_NAME_TO_NUMBER = {
|
|
35
|
+
clarify: 1,
|
|
36
|
+
structure: 2,
|
|
37
|
+
score: 3,
|
|
38
|
+
harden: 4,
|
|
39
|
+
finalize: 5,
|
|
40
|
+
};
|
|
41
|
+
const STAGE_DISPLAY_NAMES = {
|
|
42
|
+
1: "Problem Statement",
|
|
43
|
+
2: "Acceptance Criteria",
|
|
44
|
+
3: "Constraints",
|
|
45
|
+
4: "Decomposition",
|
|
46
|
+
5: "Evaluation Design",
|
|
47
|
+
};
|
|
48
|
+
const GLOBAL_CONFIG_DIR = join(homedir(), ".stoa");
|
|
49
|
+
const GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, "config.json");
|
|
50
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
51
|
+
function writeln(text = "") {
|
|
52
|
+
process.stdout.write(text + "\n");
|
|
53
|
+
}
|
|
54
|
+
function colorScore(score) {
|
|
55
|
+
if (score >= 4)
|
|
56
|
+
return chalk.green(`${score}`);
|
|
57
|
+
if (score >= 3)
|
|
58
|
+
return chalk.yellow(`${score}`);
|
|
59
|
+
return chalk.red(`${score}`);
|
|
60
|
+
}
|
|
61
|
+
function printScoreBadge(score) {
|
|
62
|
+
const label = ` Spec Score: ${colorScore(score)} / 5 `;
|
|
63
|
+
const rawLabel = ` Spec Score: ${score} / 5 `;
|
|
64
|
+
const width = rawLabel.length;
|
|
65
|
+
const top = "┌" + "─".repeat(width) + "┐";
|
|
66
|
+
const bottom = "└" + "─".repeat(width) + "┘";
|
|
67
|
+
writeln();
|
|
68
|
+
writeln(top);
|
|
69
|
+
writeln("│" + label + "│");
|
|
70
|
+
writeln(bottom);
|
|
71
|
+
}
|
|
72
|
+
function printStageHeader(index, total, stage) {
|
|
73
|
+
const line = "─".repeat(37);
|
|
74
|
+
writeln();
|
|
75
|
+
writeln(line);
|
|
76
|
+
writeln(chalk.bold(`Stage ${index} / ${total} — ${STAGE_DISPLAY_NAMES[stage] ?? `Stage ${stage}`}`));
|
|
77
|
+
writeln(line);
|
|
78
|
+
}
|
|
79
|
+
function formatStageOutput(output) {
|
|
80
|
+
if (output == null)
|
|
81
|
+
return "(no output)";
|
|
82
|
+
if (typeof output === "string") {
|
|
83
|
+
// Detect JSON array strings (e.g. Stage 2 acceptance criteria)
|
|
84
|
+
const trimmed = output.trim();
|
|
85
|
+
if (trimmed.startsWith("[")) {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(trimmed);
|
|
88
|
+
if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
|
|
89
|
+
return parsed.map((item, i) => `${i + 1}. ${item}`).join("\n");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Not valid JSON — fall through
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return output;
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(output) && output.every((item) => typeof item === "string")) {
|
|
99
|
+
return output.map((item, i) => `${i + 1}. ${item}`).join("\n");
|
|
100
|
+
}
|
|
101
|
+
return JSON.stringify(output, null, 2);
|
|
102
|
+
}
|
|
103
|
+
async function readConfig() {
|
|
104
|
+
try {
|
|
105
|
+
const raw = await readFile(GLOBAL_CONFIG_PATH, "utf-8");
|
|
106
|
+
return JSON.parse(raw);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function writeConfig(config) {
|
|
113
|
+
if (!existsSync(GLOBAL_CONFIG_DIR)) {
|
|
114
|
+
await mkdir(GLOBAL_CONFIG_DIR, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
await writeFile(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
117
|
+
}
|
|
118
|
+
function maskApiKey(value) {
|
|
119
|
+
if (value.length <= 4)
|
|
120
|
+
return value;
|
|
121
|
+
return value.slice(0, 3) + "..." + value.slice(-4);
|
|
122
|
+
}
|
|
123
|
+
function copyToClipboard(text) {
|
|
124
|
+
try {
|
|
125
|
+
const child = spawn("pbcopy", { stdio: ["pipe", "ignore", "ignore"] });
|
|
126
|
+
child.stdin.write(text);
|
|
127
|
+
child.stdin.end();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Clipboard unavailable — skip silently
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function printPostRefineOutput(slug, specScore) {
|
|
134
|
+
const specsDir = join(process.cwd(), ".stoa", "specs");
|
|
135
|
+
const descPath = join(specsDir, slug, "01-problem-statement.md");
|
|
136
|
+
const scenariosPath = join(specsDir, slug, "05-evaluation-design.md");
|
|
137
|
+
// Copy Stage 1 to clipboard
|
|
138
|
+
let copiedToClipboard = false;
|
|
139
|
+
try {
|
|
140
|
+
const descContent = await readFile(descPath, "utf-8");
|
|
141
|
+
copyToClipboard(descContent);
|
|
142
|
+
copiedToClipboard = true;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// description file missing — skip
|
|
146
|
+
}
|
|
147
|
+
// Check if scenarios exist
|
|
148
|
+
let hasScenarios = false;
|
|
149
|
+
try {
|
|
150
|
+
const scenariosContent = await readFile(scenariosPath, "utf-8");
|
|
151
|
+
hasScenarios = scenariosContent.trim().length > 0;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// no scenarios file
|
|
155
|
+
}
|
|
156
|
+
writeln();
|
|
157
|
+
writeln(chalk.cyan(`Spec saved to .stoa/specs/${slug}/`));
|
|
158
|
+
printScoreBadge(specScore);
|
|
159
|
+
if (copiedToClipboard) {
|
|
160
|
+
writeln();
|
|
161
|
+
writeln(chalk.green("→ Stage 1 description copied to clipboard"));
|
|
162
|
+
writeln(chalk.dim(" Paste into Lovable, Bolt, v0, or any AI tool"));
|
|
163
|
+
}
|
|
164
|
+
writeln();
|
|
165
|
+
writeln(chalk.green("→ Build prompt for Claude Code:"));
|
|
166
|
+
writeln(chalk.dim(` Read the spec in .stoa/specs/${slug}/ and build it. Follow all constraints and subtasks.`));
|
|
167
|
+
if (hasScenarios) {
|
|
168
|
+
writeln();
|
|
169
|
+
writeln(chalk.green(`→ Scenarios saved. Run: ${chalk.white(`stoa scenarios run ${slug}`)}`));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function resolveStages(stagesArg) {
|
|
173
|
+
const names = stagesArg.split(",").map((s) => s.trim());
|
|
174
|
+
return names.map((name) => {
|
|
175
|
+
const num = STAGE_NAME_TO_NUMBER[name];
|
|
176
|
+
if (!num) {
|
|
177
|
+
const valid = STAGE_NAMES.join(", ");
|
|
178
|
+
process.stderr.write(chalk.red(`Unknown stage: "${name}". Valid stages: ${valid}`) + "\n");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
return num;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// ── Read version from package.json ────────────────────────────────────
|
|
185
|
+
const __pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
186
|
+
const pkg = JSON.parse(readFileSync(__pkgPath, "utf-8"));
|
|
187
|
+
// ── Program ───────────────────────────────────────────────────────────
|
|
188
|
+
const program = new Command();
|
|
189
|
+
program
|
|
190
|
+
.name("stoa")
|
|
191
|
+
.description("Stoa — spec refinement CLI")
|
|
192
|
+
.version(pkg.version);
|
|
193
|
+
// ── stoa refine ───────────────────────────────────────────────────────
|
|
194
|
+
program
|
|
195
|
+
.command("refine")
|
|
196
|
+
.description("Run the spec refinement pipeline on a task description")
|
|
197
|
+
.argument("<description>", "Task description to refine")
|
|
198
|
+
.option("--stages <stages>", "Comma-separated stage names to run (e.g. clarify,structure)")
|
|
199
|
+
.option("--role <role>", "Role context for the pipeline (e.g. 'Backend Dev')")
|
|
200
|
+
.option("--mode <mode>", "Execution mode (clipboard|api|claude-code)")
|
|
201
|
+
.action(async (description, opts) => {
|
|
202
|
+
const validModes = ["clipboard", "api", "claude-code"];
|
|
203
|
+
if (opts.mode && !validModes.includes(opts.mode)) {
|
|
204
|
+
process.stderr.write(chalk.red(`Invalid mode: "${opts.mode}". Valid modes: ${validModes.join(", ")}`) + "\n");
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
const stageNumbers = opts.stages ? resolveStages(opts.stages) : undefined;
|
|
208
|
+
const totalStages = stageNumbers ? stageNumbers.length : 5;
|
|
209
|
+
let stageIndex = 0;
|
|
210
|
+
// Load guardrails
|
|
211
|
+
const guardrailItems = listGuardrails();
|
|
212
|
+
const guardrails = guardrailItems.map((g) => g.title);
|
|
213
|
+
// Resolve role
|
|
214
|
+
let roleName = opts.role;
|
|
215
|
+
if (!roleName) {
|
|
216
|
+
try {
|
|
217
|
+
const config = await loadConfig();
|
|
218
|
+
roleName = config.defaultRole;
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
if (err instanceof SyntaxError) {
|
|
222
|
+
process.stderr.write(chalk.red("Error: .stoa/config.json contains invalid JSON — fix or delete the file.") + "\n");
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
// File not found — roleName stays undefined
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
let roleContent;
|
|
229
|
+
if (roleName) {
|
|
230
|
+
try {
|
|
231
|
+
roleContent = loadRole(toSlug(roleName));
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
const rolePath = `.stoa/roles/${toSlug(roleName)}.md`;
|
|
235
|
+
process.stderr.write(chalk.red(`Error: Role file not found: ${rolePath}`) + "\n");
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Build prompt options
|
|
240
|
+
const promptOptions = {
|
|
241
|
+
...(guardrails.length > 0 ? { guardrails } : {}),
|
|
242
|
+
...(roleContent ? { role: roleContent } : {}),
|
|
243
|
+
};
|
|
244
|
+
const hasPromptOptions = guardrails.length > 0 || roleContent;
|
|
245
|
+
// Sync moodboard tokens before loading (ensures tokens.json is up to date)
|
|
246
|
+
try {
|
|
247
|
+
syncMoodboard(process.cwd());
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// Non-critical — notes.md may not exist
|
|
251
|
+
}
|
|
252
|
+
// Load moodboard design context (optional — continue without it on error)
|
|
253
|
+
let moodboard;
|
|
254
|
+
try {
|
|
255
|
+
const result = await readMoodboard(process.cwd());
|
|
256
|
+
if (result) {
|
|
257
|
+
moodboard = result;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// Non-critical — proceed without design context
|
|
262
|
+
}
|
|
263
|
+
// Scan project for context (optional — continue without it on error)
|
|
264
|
+
let projectCtx;
|
|
265
|
+
try {
|
|
266
|
+
projectCtx = scanProject(process.cwd());
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Non-critical — proceed without project context
|
|
270
|
+
}
|
|
271
|
+
const executionMode = opts.mode;
|
|
272
|
+
// In clipboard mode, skip spinner
|
|
273
|
+
if (executionMode === "clipboard") {
|
|
274
|
+
const stagesToRun = stageNumbers ?? [1, 2, 3, 4, 5];
|
|
275
|
+
try {
|
|
276
|
+
const result = await refinePipeline({
|
|
277
|
+
title: description,
|
|
278
|
+
description,
|
|
279
|
+
role: opts.role,
|
|
280
|
+
guardrails,
|
|
281
|
+
moodboard,
|
|
282
|
+
projectCtx,
|
|
283
|
+
promptOptions: hasPromptOptions ? promptOptions : undefined,
|
|
284
|
+
}, {
|
|
285
|
+
executionMode: "clipboard",
|
|
286
|
+
stages: stageNumbers,
|
|
287
|
+
onStageComplete: (stage, stageResult) => {
|
|
288
|
+
stageIndex++;
|
|
289
|
+
printStageHeader(stageIndex, totalStages, stage);
|
|
290
|
+
writeln(formatStageOutput(stageResult.output || stageResult.rawResponse));
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
// Write spec files
|
|
294
|
+
const specsDir = join(process.cwd(), ".stoa", "specs");
|
|
295
|
+
const slug = await resolveSlug(specsDir, toSlug(description));
|
|
296
|
+
const stagesRun = {};
|
|
297
|
+
for (const sr of result.stages) {
|
|
298
|
+
stagesRun[sr.stage] = sr.rawResponse;
|
|
299
|
+
}
|
|
300
|
+
await writeSpecFiles(specsDir, slug, stagesRun);
|
|
301
|
+
await writeRefineMeta(specsDir, slug, result.stages.map((s) => s.stage), result.executionMode, pkg.version);
|
|
302
|
+
await printPostRefineOutput(slug, result.finalSpecScore);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const spinner = ora({ text: "", color: "cyan" });
|
|
311
|
+
const stagesToRun = stageNumbers ?? [1, 2, 3, 4, 5];
|
|
312
|
+
spinner.start(`Running stage: ${STAGE_DISPLAY_NAMES[stagesToRun[0]]}...`);
|
|
313
|
+
try {
|
|
314
|
+
const result = await refinePipeline({
|
|
315
|
+
title: description,
|
|
316
|
+
description,
|
|
317
|
+
role: opts.role,
|
|
318
|
+
guardrails,
|
|
319
|
+
moodboard,
|
|
320
|
+
projectCtx,
|
|
321
|
+
promptOptions: hasPromptOptions ? promptOptions : undefined,
|
|
322
|
+
}, {
|
|
323
|
+
...(executionMode ? { executionMode } : {}),
|
|
324
|
+
stages: stageNumbers,
|
|
325
|
+
onStageComplete: (stage, stageResult) => {
|
|
326
|
+
spinner.stop();
|
|
327
|
+
stageIndex++;
|
|
328
|
+
printStageHeader(stageIndex, totalStages, stage);
|
|
329
|
+
writeln(formatStageOutput(stageResult.output || stageResult.rawResponse));
|
|
330
|
+
// Start spinner for next stage if there are more
|
|
331
|
+
if (stageIndex < totalStages) {
|
|
332
|
+
const nextStage = stagesToRun[stageIndex];
|
|
333
|
+
spinner.start(`Running stage: ${STAGE_DISPLAY_NAMES[nextStage]}...`);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
// Write spec files
|
|
338
|
+
const specsDir = join(process.cwd(), ".stoa", "specs");
|
|
339
|
+
const slug = await resolveSlug(specsDir, toSlug(description));
|
|
340
|
+
const stagesRun = {};
|
|
341
|
+
for (const sr of result.stages) {
|
|
342
|
+
stagesRun[sr.stage] = sr.rawResponse;
|
|
343
|
+
}
|
|
344
|
+
await writeSpecFiles(specsDir, slug, stagesRun);
|
|
345
|
+
await writeRefineMeta(specsDir, slug, result.stages.map((s) => s.stage), result.executionMode, pkg.version);
|
|
346
|
+
await printPostRefineOutput(slug, result.finalSpecScore);
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
spinner.fail(err instanceof Error ? err.message : String(err));
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
// ── stoa score ────────────────────────────────────────────────────────
|
|
354
|
+
program
|
|
355
|
+
.command("score")
|
|
356
|
+
.description("Score a task description without running the full pipeline")
|
|
357
|
+
.argument("<description>", "Task description to score")
|
|
358
|
+
.action((description) => {
|
|
359
|
+
const result = computeSpecScore({
|
|
360
|
+
hasDescription: description.length > 0,
|
|
361
|
+
descriptionLength: description.length,
|
|
362
|
+
wasRefined: false,
|
|
363
|
+
hasAcceptanceCriteria: false,
|
|
364
|
+
hasGuardrails: false,
|
|
365
|
+
hasRole: false,
|
|
366
|
+
hasScenarios: false,
|
|
367
|
+
hasSubtasks: false,
|
|
368
|
+
});
|
|
369
|
+
writeln(`Level: ${chalk.bold(result.level)}`);
|
|
370
|
+
printScoreBadge(result.score);
|
|
371
|
+
if (result.missing.length > 0) {
|
|
372
|
+
writeln();
|
|
373
|
+
writeln(chalk.dim("Missing:"));
|
|
374
|
+
for (const item of result.missing) {
|
|
375
|
+
writeln(chalk.dim(` - ${item}`));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
// ── stoa specs ────────────────────────────────────────────────────────
|
|
380
|
+
const specsCmd = program
|
|
381
|
+
.command("specs")
|
|
382
|
+
.description("Manage saved specs (.stoa/specs/)");
|
|
383
|
+
specsCmd
|
|
384
|
+
.command("list")
|
|
385
|
+
.description("List all saved specs with date")
|
|
386
|
+
.action(async () => {
|
|
387
|
+
const specsDir = join(process.cwd(), ".stoa", "specs");
|
|
388
|
+
const specs = await listSpecs(specsDir);
|
|
389
|
+
if (specs.length === 0) {
|
|
390
|
+
writeln(chalk.dim("No specs found. Run stoa refine <task> first."));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
for (const spec of specs) {
|
|
394
|
+
const dateStr = spec.date.toISOString().slice(0, 10);
|
|
395
|
+
writeln(`${chalk.white(spec.name)} ${chalk.dim(dateStr)} ${chalk.dim(`${spec.stages} stage(s)`)}`);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
specsCmd
|
|
399
|
+
.command("show")
|
|
400
|
+
.description("Show a saved spec's content")
|
|
401
|
+
.argument("<name>", "Spec name (slug)")
|
|
402
|
+
.action(async (name) => {
|
|
403
|
+
const specsDir = join(process.cwd(), ".stoa", "specs");
|
|
404
|
+
try {
|
|
405
|
+
const content = await showSpec(specsDir, name);
|
|
406
|
+
process.stdout.write(content);
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
// ── stoa config ───────────────────────────────────────────────────────
|
|
414
|
+
const configCmd = program
|
|
415
|
+
.command("config")
|
|
416
|
+
.description("Manage global Stoa configuration (~/.stoa/config.json)");
|
|
417
|
+
configCmd
|
|
418
|
+
.command("set")
|
|
419
|
+
.description("Set a configuration value")
|
|
420
|
+
.argument("<key>", "Configuration key")
|
|
421
|
+
.argument("<value>", "Configuration value")
|
|
422
|
+
.action(async (key, value) => {
|
|
423
|
+
const config = await readConfig();
|
|
424
|
+
config[key] = value;
|
|
425
|
+
await writeConfig(config);
|
|
426
|
+
writeln(chalk.green(`Set ${key}`));
|
|
427
|
+
});
|
|
428
|
+
configCmd
|
|
429
|
+
.command("get")
|
|
430
|
+
.description("Get a configuration value")
|
|
431
|
+
.argument("<key>", "Configuration key")
|
|
432
|
+
.action(async (key) => {
|
|
433
|
+
const config = await readConfig();
|
|
434
|
+
const value = config[key];
|
|
435
|
+
if (value === undefined) {
|
|
436
|
+
writeln(chalk.dim(`(not set)`));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (key.toLowerCase().endsWith("api_key")) {
|
|
440
|
+
writeln(maskApiKey(value));
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
writeln(value);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
// ── stoa edit ─────────────────────────────────────────────────────────
|
|
447
|
+
const EDITABLE_FILES = {
|
|
448
|
+
moodboard: ".stoa/moodboard/notes.md",
|
|
449
|
+
context: ".stoa/context.md",
|
|
450
|
+
lessons: ".stoa/lessons.md",
|
|
451
|
+
};
|
|
452
|
+
program
|
|
453
|
+
.command("edit")
|
|
454
|
+
.description("Open a .stoa/ file in your editor")
|
|
455
|
+
.argument("<file>", `File to edit (${Object.keys(EDITABLE_FILES).join(", ")})`)
|
|
456
|
+
.action((file) => {
|
|
457
|
+
const relativePath = EDITABLE_FILES[file];
|
|
458
|
+
if (!relativePath) {
|
|
459
|
+
process.stderr.write(chalk.red(`Unknown file: "${file}". Valid: ${Object.keys(EDITABLE_FILES).join(", ")}`) + "\n");
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
const fullPath = join(process.cwd(), relativePath);
|
|
463
|
+
if (!existsSync(fullPath)) {
|
|
464
|
+
process.stderr.write(chalk.red(`File not found: ${relativePath}. Run 'stoa init' first.`) + "\n");
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
const editor = process.env.EDITOR || "nano";
|
|
468
|
+
const child = spawn(editor, [fullPath], { stdio: "inherit" });
|
|
469
|
+
child.on("exit", (code) => {
|
|
470
|
+
process.exit(code ?? 0);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
// ── stoa init ─────────────────────────────────────────────────────────
|
|
474
|
+
program
|
|
475
|
+
.command("init")
|
|
476
|
+
.description("Initialize a .stoa/ directory in the current working directory")
|
|
477
|
+
.option("--name <name>", "Project name (defaults to current directory name)")
|
|
478
|
+
.option("--type <type>", "Project type", "generic")
|
|
479
|
+
.option("--no-templates", "Skip copying starter templates")
|
|
480
|
+
.action(async (opts) => {
|
|
481
|
+
const name = opts.name ?? basename(process.cwd());
|
|
482
|
+
const created = await initProject(name, opts.type, !opts.templates);
|
|
483
|
+
if (!created) {
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
writeln(chalk.green("Created .stoa/ with:"));
|
|
487
|
+
if (opts.templates) {
|
|
488
|
+
writeln(chalk.green(" 5 guardrails"));
|
|
489
|
+
writeln(chalk.green(" 3 roles (Builder, Fixer, Planner)"));
|
|
490
|
+
}
|
|
491
|
+
writeln(chalk.green(" moodboard/notes.md — design direction"));
|
|
492
|
+
writeln(chalk.green(" context.md — brand voice, dependencies, conventions"));
|
|
493
|
+
writeln(chalk.green(" lessons.md — project memory (grows automatically)"));
|
|
494
|
+
writeln();
|
|
495
|
+
writeln("Edit moodboard/notes.md and context.md, then run 'stoa refine \"your idea\"'");
|
|
496
|
+
});
|
|
497
|
+
// ── stoa guardrails ──────────────────────────────────────────────────
|
|
498
|
+
const guardrailsCmd = program
|
|
499
|
+
.command("guardrails")
|
|
500
|
+
.description("Manage project guardrails (.stoa/guardrails/)");
|
|
501
|
+
guardrailsCmd
|
|
502
|
+
.command("list")
|
|
503
|
+
.description("List all guardrails")
|
|
504
|
+
.action(() => {
|
|
505
|
+
const items = listGuardrails();
|
|
506
|
+
if (items.length === 0) {
|
|
507
|
+
writeln(chalk.dim("No guardrails found."));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
for (const item of items) {
|
|
511
|
+
writeln(`${item.slug}: ${item.title}`);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
guardrailsCmd
|
|
515
|
+
.command("add")
|
|
516
|
+
.description("Add a new guardrail")
|
|
517
|
+
.argument("<title>", "Guardrail title")
|
|
518
|
+
.action((title) => {
|
|
519
|
+
try {
|
|
520
|
+
addGuardrail(title);
|
|
521
|
+
writeln(chalk.green(`Added: ${toSlug(title)}`));
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
guardrailsCmd
|
|
529
|
+
.command("show")
|
|
530
|
+
.description("Show a guardrail's content")
|
|
531
|
+
.argument("<slug>", "Guardrail slug")
|
|
532
|
+
.action((slug) => {
|
|
533
|
+
try {
|
|
534
|
+
const content = showGuardrail(slug);
|
|
535
|
+
process.stdout.write(content);
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
guardrailsCmd
|
|
543
|
+
.command("remove")
|
|
544
|
+
.description("Remove a guardrail")
|
|
545
|
+
.argument("<slug>", "Guardrail slug")
|
|
546
|
+
.action(async (slug) => {
|
|
547
|
+
const { createInterface } = await import("node:readline");
|
|
548
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
549
|
+
rl.question(`Remove ${slug}? (y/N) `, (answer) => {
|
|
550
|
+
rl.close();
|
|
551
|
+
if (answer.toLowerCase() !== "y") {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
removeGuardrail(slug);
|
|
556
|
+
writeln(chalk.green(`Removed: ${slug}`));
|
|
557
|
+
}
|
|
558
|
+
catch (err) {
|
|
559
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
guardrailsCmd
|
|
565
|
+
.command("refine")
|
|
566
|
+
.description("Refine a guardrail through a 3-stage pipeline (clarify → verify → examples)")
|
|
567
|
+
.argument("<name>", "Guardrail slug name")
|
|
568
|
+
.option("--mode <mode>", "Execution mode (clipboard|api|claude-code)", "clipboard")
|
|
569
|
+
.action(async (name, opts) => {
|
|
570
|
+
const validModes = ["clipboard", "api", "claude-code"];
|
|
571
|
+
if (!validModes.includes(opts.mode)) {
|
|
572
|
+
process.stderr.write(chalk.red(`Invalid mode: "${opts.mode}". Valid modes: ${validModes.join(", ")}`) + "\n");
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
const mode = opts.mode;
|
|
576
|
+
if (mode === "clipboard") {
|
|
577
|
+
try {
|
|
578
|
+
const result = await refineGuardrail({
|
|
579
|
+
name,
|
|
580
|
+
mode,
|
|
581
|
+
onStageComplete: (i, label, output) => {
|
|
582
|
+
const line = "─".repeat(40);
|
|
583
|
+
writeln(`\n${line}`);
|
|
584
|
+
writeln(chalk.bold(label));
|
|
585
|
+
writeln(line);
|
|
586
|
+
writeln(output);
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
// result.prompts are printed via onStageComplete
|
|
590
|
+
void result;
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
// api or claude-code mode
|
|
599
|
+
const spinner = ora({ text: "", color: "cyan" });
|
|
600
|
+
spinner.start("Stage 1: Clarify & Tighten...");
|
|
601
|
+
try {
|
|
602
|
+
await refineGuardrail({
|
|
603
|
+
name,
|
|
604
|
+
mode,
|
|
605
|
+
onStageComplete: (i, label) => {
|
|
606
|
+
spinner.succeed(label);
|
|
607
|
+
if (i < 2) {
|
|
608
|
+
const nextLabels = [
|
|
609
|
+
"Stage 2: Add Verification...",
|
|
610
|
+
"Stage 3: Add Examples...",
|
|
611
|
+
];
|
|
612
|
+
spinner.start(nextLabels[i]);
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
writeln(chalk.green(`\nUpdated: .stoa/guardrails/${name}.md`));
|
|
617
|
+
}
|
|
618
|
+
catch (err) {
|
|
619
|
+
spinner.fail(err instanceof Error ? err.message : String(err));
|
|
620
|
+
process.exit(1);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
// ── stoa export ───────────────────────────────────────────────────────
|
|
624
|
+
program
|
|
625
|
+
.command("export")
|
|
626
|
+
.description("Export configuration in a specific format")
|
|
627
|
+
.argument("<format>", "Export format (e.g. claude-md)")
|
|
628
|
+
.action(async (format) => {
|
|
629
|
+
if (format !== "claude-md") {
|
|
630
|
+
process.stderr.write(chalk.red(`Unknown export format: "${format}". Supported: claude-md`) + "\n");
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
// Try local config first, then global
|
|
634
|
+
let config = {};
|
|
635
|
+
const localPath = join(process.cwd(), ".stoa", "config.json");
|
|
636
|
+
try {
|
|
637
|
+
const raw = await readFile(localPath, "utf-8");
|
|
638
|
+
config = JSON.parse(raw);
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
try {
|
|
642
|
+
const raw = await readFile(GLOBAL_CONFIG_PATH, "utf-8");
|
|
643
|
+
config = JSON.parse(raw);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
// empty config
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const lines = [
|
|
650
|
+
"# Stoa Configuration",
|
|
651
|
+
"",
|
|
652
|
+
];
|
|
653
|
+
if (config.stages) {
|
|
654
|
+
lines.push("## Pipeline Stages");
|
|
655
|
+
lines.push("");
|
|
656
|
+
const stages = config.stages;
|
|
657
|
+
for (const stage of stages) {
|
|
658
|
+
lines.push(`- ${stage}`);
|
|
659
|
+
}
|
|
660
|
+
lines.push("");
|
|
661
|
+
}
|
|
662
|
+
// Include any other config keys
|
|
663
|
+
for (const [key, value] of Object.entries(config)) {
|
|
664
|
+
if (key === "stages")
|
|
665
|
+
continue;
|
|
666
|
+
if (typeof key === "string" && key.toLowerCase().endsWith("api_key"))
|
|
667
|
+
continue;
|
|
668
|
+
lines.push(`## ${key}`);
|
|
669
|
+
lines.push("");
|
|
670
|
+
lines.push(typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
671
|
+
lines.push("");
|
|
672
|
+
}
|
|
673
|
+
// Read guardrails if present
|
|
674
|
+
const guardrailsDir = join(process.cwd(), ".stoa", "guardrails");
|
|
675
|
+
if (existsSync(guardrailsDir)) {
|
|
676
|
+
const { readdir } = await import("node:fs/promises");
|
|
677
|
+
const files = await readdir(guardrailsDir);
|
|
678
|
+
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
679
|
+
if (mdFiles.length > 0) {
|
|
680
|
+
lines.push("## Guardrails");
|
|
681
|
+
lines.push("");
|
|
682
|
+
for (const file of mdFiles) {
|
|
683
|
+
const content = await readFile(join(guardrailsDir, file), "utf-8");
|
|
684
|
+
lines.push(content);
|
|
685
|
+
lines.push("");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
process.stdout.write(lines.join("\n"));
|
|
690
|
+
});
|
|
691
|
+
// ── stoa roles ───────────────────────────────────────────────────────
|
|
692
|
+
const rolesCmd = program
|
|
693
|
+
.command("roles")
|
|
694
|
+
.description("Manage project roles (.stoa/roles/)");
|
|
695
|
+
rolesCmd
|
|
696
|
+
.command("list")
|
|
697
|
+
.description("List all roles")
|
|
698
|
+
.action(() => {
|
|
699
|
+
const slugs = listRoles();
|
|
700
|
+
for (const slug of slugs) {
|
|
701
|
+
writeln(slug);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
rolesCmd
|
|
705
|
+
.command("add")
|
|
706
|
+
.description("Add a new role")
|
|
707
|
+
.argument("<displayName>", "Role display name")
|
|
708
|
+
.action((displayName) => {
|
|
709
|
+
try {
|
|
710
|
+
addRole(displayName);
|
|
711
|
+
}
|
|
712
|
+
catch (err) {
|
|
713
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
714
|
+
process.exit(1);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
rolesCmd
|
|
718
|
+
.command("show")
|
|
719
|
+
.description("Show a role's content")
|
|
720
|
+
.argument("<slug>", "Role slug")
|
|
721
|
+
.action((slug) => {
|
|
722
|
+
try {
|
|
723
|
+
const content = showRole(slug);
|
|
724
|
+
process.stdout.write(content);
|
|
725
|
+
}
|
|
726
|
+
catch (err) {
|
|
727
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
rolesCmd
|
|
732
|
+
.command("remove")
|
|
733
|
+
.description("Remove a role")
|
|
734
|
+
.argument("<slug>", "Role slug")
|
|
735
|
+
.action((slug) => {
|
|
736
|
+
try {
|
|
737
|
+
removeRole(slug);
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
741
|
+
process.exit(1);
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
rolesCmd
|
|
745
|
+
.command("refine")
|
|
746
|
+
.description("Refine a role through a 3-stage pipeline (sharpen → boundaries → guardrails)")
|
|
747
|
+
.argument("<name>", "Role slug name")
|
|
748
|
+
.option("--mode <mode>", "Execution mode (clipboard|api|claude-code)", "clipboard")
|
|
749
|
+
.action(async (name, opts) => {
|
|
750
|
+
const validModes = ["clipboard", "api", "claude-code"];
|
|
751
|
+
if (!validModes.includes(opts.mode)) {
|
|
752
|
+
process.stderr.write(chalk.red(`Invalid mode: "${opts.mode}". Valid modes: ${validModes.join(", ")}`) + "\n");
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
const mode = opts.mode;
|
|
756
|
+
if (mode === "clipboard") {
|
|
757
|
+
try {
|
|
758
|
+
const result = await refineRole({
|
|
759
|
+
name,
|
|
760
|
+
mode,
|
|
761
|
+
onStageComplete: (i, label, output) => {
|
|
762
|
+
const line = "─".repeat(40);
|
|
763
|
+
writeln(`\n${line}`);
|
|
764
|
+
writeln(chalk.bold(label));
|
|
765
|
+
writeln(line);
|
|
766
|
+
writeln(output);
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
void result;
|
|
770
|
+
}
|
|
771
|
+
catch (err) {
|
|
772
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
773
|
+
process.exit(1);
|
|
774
|
+
}
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
// api or claude-code mode
|
|
778
|
+
const spinner = ora({ text: "", color: "cyan" });
|
|
779
|
+
spinner.start("Stage 1: Sharpen Identity...");
|
|
780
|
+
try {
|
|
781
|
+
const result = await refineRole({
|
|
782
|
+
name,
|
|
783
|
+
mode,
|
|
784
|
+
onStageComplete: (i, label, output) => {
|
|
785
|
+
spinner.succeed(label);
|
|
786
|
+
if (i === 0) {
|
|
787
|
+
spinner.start("Stage 2: Define Boundaries...");
|
|
788
|
+
}
|
|
789
|
+
else if (i === 1) {
|
|
790
|
+
writeln(chalk.green(`\nUpdated: .stoa/roles/${name}.md`));
|
|
791
|
+
spinner.start("Stage 3: Suggest Guardrails...");
|
|
792
|
+
}
|
|
793
|
+
else if (i === 2) {
|
|
794
|
+
// Stage 3: print guardrail suggestions to terminal only
|
|
795
|
+
const line = "─".repeat(40);
|
|
796
|
+
writeln(`\n${line}`);
|
|
797
|
+
writeln(chalk.bold("Suggested Guardrails (review & create manually):"));
|
|
798
|
+
writeln(line);
|
|
799
|
+
writeln(output);
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
void result;
|
|
804
|
+
}
|
|
805
|
+
catch (err) {
|
|
806
|
+
spinner.fail(err instanceof Error ? err.message : String(err));
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
// ── stoa scenarios ───────────────────────────────────────────────────
|
|
811
|
+
const scenariosCmd = program
|
|
812
|
+
.command("scenarios")
|
|
813
|
+
.description("Manage project scenarios (.stoa/scenarios/)");
|
|
814
|
+
scenariosCmd
|
|
815
|
+
.command("list")
|
|
816
|
+
.description("List all scenarios for a task")
|
|
817
|
+
.argument("<name>", "Scenario set name")
|
|
818
|
+
.action((name) => {
|
|
819
|
+
const scenarios = listScenarios(name);
|
|
820
|
+
for (let i = 0; i < scenarios.length; i++) {
|
|
821
|
+
writeln(`[${i}] title: ${scenarios[i].title}`);
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
scenariosCmd
|
|
825
|
+
.command("show")
|
|
826
|
+
.description("Show scenarios with details")
|
|
827
|
+
.argument("<name>", "Scenario set name")
|
|
828
|
+
.action((name) => {
|
|
829
|
+
try {
|
|
830
|
+
const scenarios = showScenarios(name);
|
|
831
|
+
for (let i = 0; i < scenarios.length; i++) {
|
|
832
|
+
writeln(`[${i}] GIVEN: ${scenarios[i].given}`);
|
|
833
|
+
writeln(` EXPECTED: ${scenarios[i].expected}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
catch (err) {
|
|
837
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
838
|
+
process.exit(1);
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
scenariosCmd
|
|
842
|
+
.command("add")
|
|
843
|
+
.description("Add a scenario")
|
|
844
|
+
.argument("<name>", "Scenario set name")
|
|
845
|
+
.requiredOption("--title <string>", "Scenario title")
|
|
846
|
+
.requiredOption("--given <string>", "Given condition")
|
|
847
|
+
.requiredOption("--expected <string>", "Expected outcome")
|
|
848
|
+
.action((name, opts) => {
|
|
849
|
+
addScenario(name, { title: opts.title, given: opts.given, expected: opts.expected });
|
|
850
|
+
});
|
|
851
|
+
scenariosCmd
|
|
852
|
+
.command("remove")
|
|
853
|
+
.description("Remove a scenario by index")
|
|
854
|
+
.argument("<name>", "Scenario set name")
|
|
855
|
+
.requiredOption("--index <number>", "Index to remove", parseInt)
|
|
856
|
+
.action((name, opts) => {
|
|
857
|
+
try {
|
|
858
|
+
removeScenario(name, opts.index);
|
|
859
|
+
}
|
|
860
|
+
catch (err) {
|
|
861
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
862
|
+
process.exit(1);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
scenariosCmd
|
|
866
|
+
.command("run")
|
|
867
|
+
.description("Interactively walk through scenarios for a spec")
|
|
868
|
+
.argument("[specName]", "Spec name (defaults to most recent)")
|
|
869
|
+
.action(async (specNameArg) => {
|
|
870
|
+
try {
|
|
871
|
+
await runSpecScenarios(specNameArg);
|
|
872
|
+
}
|
|
873
|
+
catch (err) {
|
|
874
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
875
|
+
process.exit(1);
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
scenariosCmd
|
|
879
|
+
.command("generate")
|
|
880
|
+
.description("Generate structured scenarios from a task spec (.stoa/specs/<taskName>.json)")
|
|
881
|
+
.argument("<taskName>", "Task name (matches spec filename)")
|
|
882
|
+
.option("--mode <mode>", "Execution mode (api|clipboard|cli)", "clipboard")
|
|
883
|
+
.action(async (taskName, opts) => {
|
|
884
|
+
const validModes = ["api", "clipboard", "cli"];
|
|
885
|
+
if (!validModes.includes(opts.mode)) {
|
|
886
|
+
process.stderr.write(chalk.red(`Invalid mode: "${opts.mode}". Valid modes: ${validModes.join(", ")}`) + "\n");
|
|
887
|
+
process.exit(1);
|
|
888
|
+
}
|
|
889
|
+
await generateScenarios(taskName, opts.mode);
|
|
890
|
+
});
|
|
891
|
+
// ── stoa moodboard ──────────────────────────────────────────────────
|
|
892
|
+
const moodboardCmd = program
|
|
893
|
+
.command("moodboard")
|
|
894
|
+
.description("Manage project moodboard (.stoa/moodboard/)");
|
|
895
|
+
moodboardCmd
|
|
896
|
+
.command("sync")
|
|
897
|
+
.description("Parse notes.md and generate tokens.json")
|
|
898
|
+
.action(() => {
|
|
899
|
+
try {
|
|
900
|
+
const tokens = syncMoodboard(process.cwd());
|
|
901
|
+
writeln(chalk.green("Generated .stoa/moodboard/tokens.json"));
|
|
902
|
+
writeln();
|
|
903
|
+
const entries = Object.entries(tokens);
|
|
904
|
+
if (entries.length === 0) {
|
|
905
|
+
writeln(chalk.dim("No values found. Add content to .stoa/moodboard/notes.md first."));
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
writeln("Extracted:");
|
|
909
|
+
for (const [key, value] of entries) {
|
|
910
|
+
if (typeof value === "string") {
|
|
911
|
+
writeln(` ${key}: ${value}`);
|
|
912
|
+
}
|
|
913
|
+
else if (value && typeof value === "object") {
|
|
914
|
+
const count = Object.keys(value).length;
|
|
915
|
+
writeln(` ${key}: ${count} value(s)`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
catch (err) {
|
|
921
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
moodboardCmd
|
|
926
|
+
.command("describe")
|
|
927
|
+
.description("AI-extract design system from screenshots in moodboard/")
|
|
928
|
+
.option("--mode <mode>", "Execution mode (api|claude-code|clipboard)")
|
|
929
|
+
.option("--overwrite", "Overwrite notes.md without prompting")
|
|
930
|
+
.action(async (opts) => {
|
|
931
|
+
const validModes = ["api", "claude-code", "clipboard"];
|
|
932
|
+
if (opts.mode && !validModes.includes(opts.mode)) {
|
|
933
|
+
process.stderr.write(chalk.red(`Invalid mode: "${opts.mode}". Valid: ${validModes.join(", ")}`) + "\n");
|
|
934
|
+
process.exit(1);
|
|
935
|
+
}
|
|
936
|
+
// Auto-detect mode if not specified
|
|
937
|
+
let mode;
|
|
938
|
+
if (opts.mode) {
|
|
939
|
+
mode = opts.mode;
|
|
940
|
+
}
|
|
941
|
+
else if (process.env.ANTHROPIC_API_KEY) {
|
|
942
|
+
mode = "api";
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
mode = "clipboard";
|
|
946
|
+
}
|
|
947
|
+
const spinner = mode === "api" ? ora({ text: "Analyzing screenshots...", color: "cyan" }) : null;
|
|
948
|
+
try {
|
|
949
|
+
if (spinner)
|
|
950
|
+
spinner.start();
|
|
951
|
+
const result = await describeMoodboard(process.cwd(), {
|
|
952
|
+
mode,
|
|
953
|
+
overwrite: opts.overwrite,
|
|
954
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
955
|
+
onConfirm: async () => {
|
|
956
|
+
if (spinner)
|
|
957
|
+
spinner.stop();
|
|
958
|
+
const { createInterface } = await import("node:readline");
|
|
959
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
960
|
+
return new Promise((resolve) => {
|
|
961
|
+
rl.question("notes.md has content. Overwrite? [y/N] ", (answer) => {
|
|
962
|
+
rl.close();
|
|
963
|
+
const yes = answer.trim().toLowerCase() === "y";
|
|
964
|
+
if (yes && spinner)
|
|
965
|
+
spinner.start();
|
|
966
|
+
resolve(yes);
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
},
|
|
970
|
+
});
|
|
971
|
+
if (spinner)
|
|
972
|
+
spinner.stop();
|
|
973
|
+
if (result.written) {
|
|
974
|
+
writeln(chalk.green(`Extracted design system from ${result.imageCount} image(s).`));
|
|
975
|
+
writeln(chalk.green("Updated notes.md and tokens.json."));
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
writeln(result.output);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
catch (err) {
|
|
982
|
+
if (spinner)
|
|
983
|
+
spinner.fail();
|
|
984
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
985
|
+
process.exit(1);
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
// ── stoa review ──────────────────────────────────────────────────────
|
|
989
|
+
program
|
|
990
|
+
.command("review")
|
|
991
|
+
.description("Review a refined spec interactively, then optionally re-run affected stages")
|
|
992
|
+
.argument("[specName]", "Spec name (defaults to most recent)")
|
|
993
|
+
.action(async (specNameArg) => {
|
|
994
|
+
let specName;
|
|
995
|
+
try {
|
|
996
|
+
specName = await resolveSpecName(specNameArg);
|
|
997
|
+
}
|
|
998
|
+
catch (err) {
|
|
999
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
const specDir = join(process.cwd(), ".stoa", "specs", specName);
|
|
1003
|
+
// 1. Capture snapshot
|
|
1004
|
+
const snapshot = await snapshotSpecFiles(specDir);
|
|
1005
|
+
// 2. Run interactive review loop
|
|
1006
|
+
await runReviewLoop(specDir);
|
|
1007
|
+
// 3. Detect changes made during review
|
|
1008
|
+
const affectedStages = await detectChanges(specDir, snapshot);
|
|
1009
|
+
// 4. Prompt user about re-running affected stages
|
|
1010
|
+
const success = await promptAndRerun(affectedStages, specDir);
|
|
1011
|
+
if (success) {
|
|
1012
|
+
// 5. Write .approved marker
|
|
1013
|
+
const approvedPath = join(specDir, ".approved");
|
|
1014
|
+
await writeFile(approvedPath, new Date().toISOString(), "utf-8");
|
|
1015
|
+
writeln(chalk.green(`Approved: ${specName}`));
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
process.stderr.write(chalk.red(`Review failed for spec "${specName}". Spec was not approved.`) + "\n");
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
// ── stoa build ───────────────────────────────────────────────────────
|
|
1023
|
+
program
|
|
1024
|
+
.command("build")
|
|
1025
|
+
.description("Launch a build from an approved spec")
|
|
1026
|
+
.argument("[specName]", "Spec name to build (defaults to most recent approved spec)")
|
|
1027
|
+
.option("--fix <number>", "Apply a fix spec before building (reads .stoa/specs/<name>/fixes/fix-<NNN>.md)")
|
|
1028
|
+
.action(async (specNameArg, opts) => {
|
|
1029
|
+
let specName;
|
|
1030
|
+
try {
|
|
1031
|
+
specName = await resolveSpecName(specNameArg);
|
|
1032
|
+
}
|
|
1033
|
+
catch (err) {
|
|
1034
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
1035
|
+
process.exit(1);
|
|
1036
|
+
}
|
|
1037
|
+
const approvedPath = join(process.cwd(), ".stoa", "specs", specName, ".approved");
|
|
1038
|
+
try {
|
|
1039
|
+
await access(approvedPath, constants.F_OK);
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
process.stderr.write(chalk.red(`Error: Spec "${specName}" has not been reviewed. Run \`stoa review ${specName}\` first.`) + "\n");
|
|
1043
|
+
process.exit(1);
|
|
1044
|
+
}
|
|
1045
|
+
// Handle --fix N: read the fix file and use it as the prompt
|
|
1046
|
+
let fixPrompt;
|
|
1047
|
+
if (opts.fix) {
|
|
1048
|
+
const fixNum = parseInt(opts.fix, 10);
|
|
1049
|
+
const padded = String(fixNum).padStart(3, "0");
|
|
1050
|
+
const fixPath = join(process.cwd(), ".stoa", "specs", specName, "fixes", `fix-${padded}.md`);
|
|
1051
|
+
if (!existsSync(fixPath)) {
|
|
1052
|
+
process.stderr.write(chalk.red(`Error: Fix file not found: .stoa/specs/${specName}/fixes/fix-${padded}.md`) + "\n");
|
|
1053
|
+
process.exit(1);
|
|
1054
|
+
}
|
|
1055
|
+
fixPrompt = readFileSync(fixPath, "utf-8");
|
|
1056
|
+
writeln(chalk.cyan(`Applying fix ${fixNum} for spec: ${chalk.white(specName)}`));
|
|
1057
|
+
}
|
|
1058
|
+
const config = await loadConfig();
|
|
1059
|
+
const role = config.defaultRole ?? "builder";
|
|
1060
|
+
writeln(chalk.cyan(`Building from spec: ${chalk.white(specName)}`));
|
|
1061
|
+
writeln(chalk.dim(`Role: ${role}`));
|
|
1062
|
+
const basePrompt = composePrompt(specName, { role });
|
|
1063
|
+
// If --fix, append fix content to the prompt
|
|
1064
|
+
const prompt = fixPrompt
|
|
1065
|
+
? basePrompt + "\n\n# Fix Spec\n" + fixPrompt
|
|
1066
|
+
: basePrompt;
|
|
1067
|
+
// Check for subtasks in the spec
|
|
1068
|
+
const subtasksPath = join(process.cwd(), ".stoa", "specs", specName, "04-decomposition.md");
|
|
1069
|
+
const subtasksContent = existsSync(subtasksPath) ? readFileSync(subtasksPath, "utf-8") : "";
|
|
1070
|
+
const subtasks = parseSubtasks(subtasksContent);
|
|
1071
|
+
try {
|
|
1072
|
+
if (fixPrompt || subtasks.length === 0) {
|
|
1073
|
+
// --fix mode or no subtasks: single build
|
|
1074
|
+
await runBuild(specName, prompt);
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
writeln(chalk.cyan(`\nFound ${subtasks.length} subtask(s):`));
|
|
1078
|
+
for (const st of subtasks) {
|
|
1079
|
+
writeln(chalk.dim(` ${st.index}. ${st.text.split("\n")[0]}`));
|
|
1080
|
+
}
|
|
1081
|
+
writeln("");
|
|
1082
|
+
const choice = await promptSubtaskChoice(subtasks);
|
|
1083
|
+
if (choice === "q") {
|
|
1084
|
+
process.exit(0);
|
|
1085
|
+
}
|
|
1086
|
+
else if (choice === "all") {
|
|
1087
|
+
for (const st of subtasks) {
|
|
1088
|
+
writeln(chalk.cyan(`\n── Subtask ${st.index}/${subtasks.length} ──\n`));
|
|
1089
|
+
await runBuild(specName, prompt + `\n\n# Current Subtask\n${st.text}`);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
const num = parseInt(choice, 10);
|
|
1094
|
+
const picked = subtasks.find((s) => s.index === num);
|
|
1095
|
+
if (!picked) {
|
|
1096
|
+
process.stderr.write(chalk.red(`Invalid choice: "${choice}". Expected all, 1-${subtasks.length}, or q.\n`));
|
|
1097
|
+
process.exit(1);
|
|
1098
|
+
}
|
|
1099
|
+
writeln(chalk.cyan(`\n── Subtask ${picked.index} ──\n`));
|
|
1100
|
+
await runBuild(specName, prompt + `\n\n# Current Subtask\n${picked.text}`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
// If --fix mode, run verify automatically after build completes
|
|
1104
|
+
if (fixPrompt) {
|
|
1105
|
+
writeln(chalk.cyan("\nRunning verification..."));
|
|
1106
|
+
await runVerify(specName);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
catch (err) {
|
|
1110
|
+
if (err instanceof Error && err.code === "ENOENT") {
|
|
1111
|
+
process.exit(1);
|
|
1112
|
+
}
|
|
1113
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
1114
|
+
process.exit(1);
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
// ── stoa verify ─────────────────────────────────────────────────────
|
|
1118
|
+
program
|
|
1119
|
+
.command("verify")
|
|
1120
|
+
.description("Interactively verify scenarios for a spec")
|
|
1121
|
+
.argument("[specName]", "Spec name to verify (defaults to most recent spec)")
|
|
1122
|
+
.action(async (specNameArg) => {
|
|
1123
|
+
let specName;
|
|
1124
|
+
try {
|
|
1125
|
+
specName = await resolveSpecName(specNameArg);
|
|
1126
|
+
}
|
|
1127
|
+
catch (err) {
|
|
1128
|
+
process.stderr.write(chalk.red(err instanceof Error ? err.message : String(err)) + "\n");
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
1131
|
+
await runVerify(specName);
|
|
1132
|
+
});
|
|
1133
|
+
// ── Parse ─────────────────────────────────────────────────────────────
|
|
1134
|
+
program.parse();
|
|
1135
|
+
//# sourceMappingURL=cli.js.map
|