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.
Files changed (167) hide show
  1. package/LICENSE +65 -0
  2. package/README.md +397 -0
  3. package/dist/cli/build.d.ts +39 -0
  4. package/dist/cli/build.d.ts.map +1 -0
  5. package/dist/cli/build.js +288 -0
  6. package/dist/cli/build.js.map +1 -0
  7. package/dist/cli/review-loop.d.ts +2 -0
  8. package/dist/cli/review-loop.d.ts.map +1 -0
  9. package/dist/cli/review-loop.js +97 -0
  10. package/dist/cli/review-loop.js.map +1 -0
  11. package/dist/cli/scenarios-runner.d.ts +12 -0
  12. package/dist/cli/scenarios-runner.d.ts.map +1 -0
  13. package/dist/cli/scenarios-runner.js +158 -0
  14. package/dist/cli/scenarios-runner.js.map +1 -0
  15. package/dist/cli/verify.d.ts +13 -0
  16. package/dist/cli/verify.d.ts.map +1 -0
  17. package/dist/cli/verify.js +149 -0
  18. package/dist/cli/verify.js.map +1 -0
  19. package/dist/cli.d.ts +3 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +1135 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/core/index.d.ts +3 -0
  24. package/dist/core/index.d.ts.map +1 -0
  25. package/dist/core/index.js +2 -0
  26. package/dist/core/index.js.map +1 -0
  27. package/dist/core/parsers.d.ts +29 -0
  28. package/dist/core/parsers.d.ts.map +1 -0
  29. package/dist/core/parsers.js +296 -0
  30. package/dist/core/parsers.js.map +1 -0
  31. package/dist/core/parsers.test.d.ts +2 -0
  32. package/dist/core/parsers.test.d.ts.map +1 -0
  33. package/dist/core/parsers.test.js +198 -0
  34. package/dist/core/parsers.test.js.map +1 -0
  35. package/dist/core/prompts.d.ts +30 -0
  36. package/dist/core/prompts.d.ts.map +1 -0
  37. package/dist/core/prompts.js +346 -0
  38. package/dist/core/prompts.js.map +1 -0
  39. package/dist/core/refine.d.ts +38 -0
  40. package/dist/core/refine.d.ts.map +1 -0
  41. package/dist/core/refine.js +233 -0
  42. package/dist/core/refine.js.map +1 -0
  43. package/dist/core/spec-score.d.ts +17 -0
  44. package/dist/core/spec-score.d.ts.map +1 -0
  45. package/dist/core/spec-score.js +59 -0
  46. package/dist/core/spec-score.js.map +1 -0
  47. package/dist/formatters/index.d.ts +2 -0
  48. package/dist/formatters/index.d.ts.map +1 -0
  49. package/dist/formatters/index.js +2 -0
  50. package/dist/formatters/index.js.map +1 -0
  51. package/dist/formatters/stage-formatters.d.ts +10 -0
  52. package/dist/formatters/stage-formatters.d.ts.map +1 -0
  53. package/dist/formatters/stage-formatters.js +100 -0
  54. package/dist/formatters/stage-formatters.js.map +1 -0
  55. package/dist/formatters/stage-formatters.test.d.ts +2 -0
  56. package/dist/formatters/stage-formatters.test.d.ts.map +1 -0
  57. package/dist/formatters/stage-formatters.test.js +107 -0
  58. package/dist/formatters/stage-formatters.test.js.map +1 -0
  59. package/dist/guardrails/index.d.ts +2 -0
  60. package/dist/guardrails/index.d.ts.map +1 -0
  61. package/dist/guardrails/index.js +2 -0
  62. package/dist/guardrails/index.js.map +1 -0
  63. package/dist/guardrails/loader.d.ts +9 -0
  64. package/dist/guardrails/loader.d.ts.map +1 -0
  65. package/dist/guardrails/loader.js +56 -0
  66. package/dist/guardrails/loader.js.map +1 -0
  67. package/dist/guardrails/refine.d.ts +53 -0
  68. package/dist/guardrails/refine.d.ts.map +1 -0
  69. package/dist/guardrails/refine.js +184 -0
  70. package/dist/guardrails/refine.js.map +1 -0
  71. package/dist/index.d.ts +6 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +196 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/storage/change-detection.d.ts +2 -0
  76. package/dist/storage/change-detection.d.ts.map +1 -0
  77. package/dist/storage/change-detection.js +47 -0
  78. package/dist/storage/change-detection.js.map +1 -0
  79. package/dist/storage/change-detection.test.d.ts +2 -0
  80. package/dist/storage/change-detection.test.d.ts.map +1 -0
  81. package/dist/storage/change-detection.test.js +81 -0
  82. package/dist/storage/change-detection.test.js.map +1 -0
  83. package/dist/storage/index.d.ts +9 -0
  84. package/dist/storage/index.d.ts.map +1 -0
  85. package/dist/storage/index.js +6 -0
  86. package/dist/storage/index.js.map +1 -0
  87. package/dist/storage/moodboard-describe.d.ts +14 -0
  88. package/dist/storage/moodboard-describe.d.ts.map +1 -0
  89. package/dist/storage/moodboard-describe.js +185 -0
  90. package/dist/storage/moodboard-describe.js.map +1 -0
  91. package/dist/storage/moodboard-sync.d.ts +11 -0
  92. package/dist/storage/moodboard-sync.d.ts.map +1 -0
  93. package/dist/storage/moodboard-sync.js +205 -0
  94. package/dist/storage/moodboard-sync.js.map +1 -0
  95. package/dist/storage/moodboard.d.ts +4 -0
  96. package/dist/storage/moodboard.d.ts.map +1 -0
  97. package/dist/storage/moodboard.js +68 -0
  98. package/dist/storage/moodboard.js.map +1 -0
  99. package/dist/storage/moodboard.test.d.ts +2 -0
  100. package/dist/storage/moodboard.test.d.ts.map +1 -0
  101. package/dist/storage/moodboard.test.js +133 -0
  102. package/dist/storage/moodboard.test.js.map +1 -0
  103. package/dist/storage/project-scan.d.ts +12 -0
  104. package/dist/storage/project-scan.d.ts.map +1 -0
  105. package/dist/storage/project-scan.js +118 -0
  106. package/dist/storage/project-scan.js.map +1 -0
  107. package/dist/storage/project.d.ts +10 -0
  108. package/dist/storage/project.d.ts.map +1 -0
  109. package/dist/storage/project.js +101 -0
  110. package/dist/storage/project.js.map +1 -0
  111. package/dist/storage/roles-refine.d.ts +59 -0
  112. package/dist/storage/roles-refine.d.ts.map +1 -0
  113. package/dist/storage/roles-refine.js +223 -0
  114. package/dist/storage/roles-refine.js.map +1 -0
  115. package/dist/storage/roles.d.ts +6 -0
  116. package/dist/storage/roles.d.ts.map +1 -0
  117. package/dist/storage/roles.js +41 -0
  118. package/dist/storage/roles.js.map +1 -0
  119. package/dist/storage/scenarios-refine.d.ts +47 -0
  120. package/dist/storage/scenarios-refine.d.ts.map +1 -0
  121. package/dist/storage/scenarios-refine.js +311 -0
  122. package/dist/storage/scenarios-refine.js.map +1 -0
  123. package/dist/storage/scenarios.d.ts +12 -0
  124. package/dist/storage/scenarios.d.ts.map +1 -0
  125. package/dist/storage/scenarios.js +37 -0
  126. package/dist/storage/scenarios.js.map +1 -0
  127. package/dist/storage/specs.d.ts +17 -0
  128. package/dist/storage/specs.d.ts.map +1 -0
  129. package/dist/storage/specs.js +104 -0
  130. package/dist/storage/specs.js.map +1 -0
  131. package/dist/tools/index.d.ts +2 -0
  132. package/dist/tools/index.d.ts.map +1 -0
  133. package/dist/tools/index.js +2 -0
  134. package/dist/tools/index.js.map +1 -0
  135. package/dist/tools/rerefine.d.ts +8 -0
  136. package/dist/tools/rerefine.d.ts.map +1 -0
  137. package/dist/tools/rerefine.js +153 -0
  138. package/dist/tools/rerefine.js.map +1 -0
  139. package/dist/tools/rerefine.test.d.ts +2 -0
  140. package/dist/tools/rerefine.test.d.ts.map +1 -0
  141. package/dist/tools/rerefine.test.js +123 -0
  142. package/dist/tools/rerefine.test.js.map +1 -0
  143. package/dist/utils/index.d.ts +3 -0
  144. package/dist/utils/index.d.ts.map +1 -0
  145. package/dist/utils/index.js +3 -0
  146. package/dist/utils/index.js.map +1 -0
  147. package/dist/utils/slug.d.ts +3 -0
  148. package/dist/utils/slug.d.ts.map +1 -0
  149. package/dist/utils/slug.js +33 -0
  150. package/dist/utils/slug.js.map +1 -0
  151. package/dist/utils/spec-helpers.d.ts +12 -0
  152. package/dist/utils/spec-helpers.d.ts.map +1 -0
  153. package/dist/utils/spec-helpers.js +95 -0
  154. package/dist/utils/spec-helpers.js.map +1 -0
  155. package/dist/utils/spec-helpers.test.d.ts +2 -0
  156. package/dist/utils/spec-helpers.test.d.ts.map +1 -0
  157. package/dist/utils/spec-helpers.test.js +114 -0
  158. package/dist/utils/spec-helpers.test.js.map +1 -0
  159. package/package.json +53 -0
  160. package/templates/guardrails/ask-when-unclear.md +3 -0
  161. package/templates/guardrails/dont-delete-code.md +3 -0
  162. package/templates/guardrails/explain-changes.md +3 -0
  163. package/templates/guardrails/run-tests.md +3 -0
  164. package/templates/guardrails/small-changes.md +3 -0
  165. package/templates/roles/builder.md +3 -0
  166. package/templates/roles/fixer.md +3 -0
  167. package/templates/roles/planner.md +3 -0
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Role Refine Pipeline.
3
+ * Runs a 3-stage refinement pipeline that transforms raw role definitions
4
+ * into precise, bounded, guardrail-ready specifications.
5
+ *
6
+ * Stage 1 — Sharpen Identity: Rewrite the role persona to be specific and unambiguous.
7
+ * Stage 2 — Define Boundaries: Generate explicit boundary rules (file scope, tech limits, escalation).
8
+ * Stage 3 — Suggest Guardrails: Print 2–4 guardrail suggestions to stdout only (no file writes).
9
+ */
10
+ import Anthropic from "@anthropic-ai/sdk";
11
+ import { spawn } from "child_process";
12
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ const SPAWN_TIMEOUT_MS = 300_000;
15
+ function rolesDir() {
16
+ return join(process.cwd(), ".stoa", "roles");
17
+ }
18
+ // ── Stage names ──────────────────────────────────────────────────────
19
+ const STAGE_LABELS = [
20
+ "Stage 1: Sharpen Identity",
21
+ "Stage 2: Define Boundaries",
22
+ "Stage 3: Suggest Guardrails",
23
+ ];
24
+ // ── Pure prompt builders ─────────────────────────────────────────────
25
+ /**
26
+ * Build the Stage 1 prompt: Sharpen Identity.
27
+ * Rewrites the role persona to be specific, unambiguous, and self-contained.
28
+ * @param seed - The raw role markdown content.
29
+ * @returns The full prompt string for Stage 1.
30
+ */
31
+ export function buildStage1Prompt(seed) {
32
+ return [
33
+ "You are a technical writing editor specializing in agent role definitions for software engineering teams.",
34
+ "",
35
+ "Rewrite the following role definition so it is specific, unambiguous, and self-contained.",
36
+ "The output must make it clear:",
37
+ "- What the role does (primary responsibilities)",
38
+ "- What its primary decisions are (what it chooses, prioritizes, or trades off)",
39
+ "- What communication style it uses (tone, verbosity, formality)",
40
+ "",
41
+ "Remove vague language. Replace subjective terms with concrete descriptions.",
42
+ "Preserve the original intent and any technical details.",
43
+ "Output only the rewritten role definition in markdown format — no preamble, no commentary.",
44
+ "Keep the original heading (# line) intact.",
45
+ "",
46
+ "---",
47
+ "",
48
+ seed,
49
+ ].join("\n");
50
+ }
51
+ /**
52
+ * Build the Stage 2 prompt: Define Boundaries.
53
+ * Generates explicit boundary rules covering file scope, technology limits, and escalation triggers.
54
+ * @param stage1Output - The sharpened role definition from Stage 1.
55
+ * @returns The full prompt string for Stage 2.
56
+ */
57
+ export function buildStage2Prompt(stage1Output) {
58
+ return [
59
+ "You are a technical writing editor specializing in agent role definitions for software engineering teams.",
60
+ "",
61
+ "The following is a sharpened role definition. Append a `## Boundaries` section at the end.",
62
+ "The section must cover three areas:",
63
+ "",
64
+ "1. **File Scope** — Which directories and file types the role is authorized to read or modify.",
65
+ " Use advisory language (e.g. \"Focus on: [...]\", \"Avoid: [...]\").",
66
+ "",
67
+ "2. **Technology Limits** — Specific libraries, languages, frameworks, or APIs the role should and should not use.",
68
+ "",
69
+ "3. **Escalation Triggers** — Conditions under which the role should pause and ask for human input",
70
+ " rather than proceed autonomously (e.g. breaking changes, security-sensitive operations, ambiguous requirements).",
71
+ "",
72
+ "Each sub-area should be a subsection (### heading) within ## Boundaries.",
73
+ "Be specific and actionable — no vague advice.",
74
+ "Output the complete role definition with the new section appended — no preamble, no commentary.",
75
+ "",
76
+ "---",
77
+ "",
78
+ stage1Output,
79
+ ].join("\n");
80
+ }
81
+ /**
82
+ * Build the Stage 3 prompt: Suggest Guardrails.
83
+ * Generates 2–4 guardrail rule suggestions based on the sharpened identity and defined boundaries.
84
+ * @param stage2Output - The role definition with boundaries from Stage 2.
85
+ * @returns The full prompt string for Stage 3.
86
+ */
87
+ export function buildStage3Prompt(stage2Output) {
88
+ return [
89
+ "You are a technical writing editor specializing in software engineering guardrails and coding standards.",
90
+ "",
91
+ "Given the following role definition (with identity and boundaries), suggest 2–4 guardrail rules",
92
+ "that would make this role safer and more reliable when operating autonomously.",
93
+ "",
94
+ "For each guardrail:",
95
+ "- Give it a short, descriptive title",
96
+ "- Write 3–5 checklist items (- [ ]) that are concrete and verifiable",
97
+ "- Include at least one BAD and one GOOD code/config example where relevant",
98
+ "",
99
+ "Format the output as markdown with each guardrail as a ## heading.",
100
+ "This output is advisory — it will be shown to a human for review.",
101
+ "Do not include any preamble or commentary outside the guardrail definitions.",
102
+ "",
103
+ "---",
104
+ "",
105
+ stage2Output,
106
+ ].join("\n");
107
+ }
108
+ // ── Execution helpers ────────────────────────────────────────────────
109
+ async function executeStage(prompt, mode, model, apiKey) {
110
+ if (mode === "api") {
111
+ const client = new Anthropic({
112
+ apiKey: apiKey || process.env.ANTHROPIC_API_KEY,
113
+ });
114
+ const response = await client.messages.create({
115
+ model,
116
+ max_tokens: 4096,
117
+ messages: [{ role: "user", content: prompt }],
118
+ });
119
+ const textBlock = response.content.find((block) => block.type === "text");
120
+ return textBlock ? textBlock.text : "";
121
+ }
122
+ // claude-code mode — pipe prompt via stdin (the -p flag hangs as subprocess)
123
+ return new Promise((resolve, reject) => {
124
+ const child = spawn("claude", ["--print"], {
125
+ stdio: ["pipe", "pipe", "pipe"],
126
+ });
127
+ let stdout = "";
128
+ let resolved = false;
129
+ let dataTimer = null;
130
+ const finish = () => {
131
+ if (resolved)
132
+ return;
133
+ resolved = true;
134
+ clearTimeout(overallTimer);
135
+ if (dataTimer)
136
+ clearTimeout(dataTimer);
137
+ child.kill();
138
+ resolve(stdout.trim());
139
+ };
140
+ child.stdout.on("data", (chunk) => {
141
+ stdout += chunk.toString();
142
+ if (dataTimer)
143
+ clearTimeout(dataTimer);
144
+ dataTimer = setTimeout(finish, 2000);
145
+ });
146
+ child.on("close", () => {
147
+ finish();
148
+ });
149
+ const overallTimer = setTimeout(() => {
150
+ if (!resolved) {
151
+ resolved = true;
152
+ if (dataTimer)
153
+ clearTimeout(dataTimer);
154
+ child.kill("SIGKILL");
155
+ reject(new Error(`Claude Code timed out after ${SPAWN_TIMEOUT_MS / 1000}s`));
156
+ }
157
+ }, SPAWN_TIMEOUT_MS);
158
+ child.on("error", (err) => {
159
+ if (!resolved) {
160
+ resolved = true;
161
+ clearTimeout(overallTimer);
162
+ if (dataTimer)
163
+ clearTimeout(dataTimer);
164
+ reject(err);
165
+ }
166
+ });
167
+ // Write prompt to stdin and close it
168
+ child.stdin.write(prompt);
169
+ child.stdin.end();
170
+ });
171
+ }
172
+ /**
173
+ * Run the 3-stage role refinement pipeline.
174
+ *
175
+ * - clipboard: Builds all 3 stage prompts and returns them via onStageComplete callback.
176
+ * Does not call any AI API. Does not modify files.
177
+ * - api: Executes all 3 stages via Anthropic API. Writes stage 1+2 output to file.
178
+ * Stage 3 output is returned but not written.
179
+ * - claude-code: Executes via Claude Code CLI. Writes stage 1+2 output to file.
180
+ * Stage 3 output is returned but not written.
181
+ *
182
+ * @param options - Pipeline configuration including name, mode, and optional model/apiKey.
183
+ * @returns The prompts used and the final output.
184
+ * @throws If the role file does not exist.
185
+ */
186
+ export async function refinePipeline(options) {
187
+ const filePath = join(rolesDir(), `${options.name}.md`);
188
+ if (!existsSync(filePath)) {
189
+ throw new Error(`Role not found: .stoa/roles/${options.name}.md`);
190
+ }
191
+ const seed = readFileSync(filePath, "utf-8");
192
+ const model = options.model ?? "claude-sonnet-4-6";
193
+ const builders = [buildStage1Prompt, buildStage2Prompt, buildStage3Prompt];
194
+ const prompts = [];
195
+ if (options.mode === "clipboard") {
196
+ // Clipboard mode: build prompts sequentially, no AI calls, no file writes
197
+ let currentInput = seed;
198
+ for (let i = 0; i < builders.length; i++) {
199
+ const prompt = builders[i](currentInput);
200
+ prompts.push(prompt);
201
+ options.onStageComplete?.(i, STAGE_LABELS[i], prompt);
202
+ // In clipboard mode, the "output" of each stage is the prompt itself
203
+ // (user will paste it manually). Chain the seed forward unchanged.
204
+ currentInput = prompt;
205
+ }
206
+ return { prompts, finalOutput: "" };
207
+ }
208
+ // api or claude-code mode
209
+ let currentInput = seed;
210
+ for (let i = 0; i < builders.length; i++) {
211
+ const prompt = builders[i](currentInput);
212
+ prompts.push(prompt);
213
+ currentInput = await executeStage(prompt, options.mode, model, options.apiKey);
214
+ options.onStageComplete?.(i, STAGE_LABELS[i], currentInput);
215
+ // After stage 2, write the result back to the role file (stages 1+2 combined)
216
+ if (i === 1) {
217
+ writeFileSync(filePath, currentInput, "utf-8");
218
+ }
219
+ }
220
+ // Stage 3 output is NOT written to any file — it's advisory only
221
+ return { prompts, finalOutput: currentInput };
222
+ }
223
+ //# sourceMappingURL=roles-refine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles-refine.js","sourceRoot":"","sources":["../../src/storage/roles-refine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export declare function listRoles(): string[];
2
+ export declare function addRole(displayName: string): void;
3
+ export declare function showRole(slug: string): string;
4
+ export declare function removeRole(slug: string): void;
5
+ export declare function loadRole(slug: string): string;
6
+ //# sourceMappingURL=roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../../src/storage/roles.ts"],"names":[],"mappings":"AAQA,wBAAgB,SAAS,IAAI,MAAM,EAAE,CAOpC;AAED,wBAAgB,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAUjD;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQ7C;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQ7C;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C"}
@@ -0,0 +1,41 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { toSlug } from "../utils/slug.js";
4
+ function rolesDir() {
5
+ return join(process.cwd(), ".stoa", "roles");
6
+ }
7
+ export function listRoles() {
8
+ if (!existsSync(rolesDir()))
9
+ return [];
10
+ return readdirSync(rolesDir())
11
+ .filter((f) => f.endsWith(".md"))
12
+ .sort()
13
+ .map((f) => f.replace(/\.md$/, ""));
14
+ }
15
+ export function addRole(displayName) {
16
+ const slug = toSlug(displayName);
17
+ const filePath = join(rolesDir(), `${slug}.md`);
18
+ if (existsSync(filePath)) {
19
+ throw new Error(`Role already exists: ${slug}`);
20
+ }
21
+ mkdirSync(rolesDir(), { recursive: true });
22
+ writeFileSync(filePath, `# ${displayName}\n\nYou are a ${displayName}.\n`, "utf-8");
23
+ }
24
+ export function showRole(slug) {
25
+ const filePath = join(rolesDir(), `${slug}.md`);
26
+ if (!existsSync(filePath)) {
27
+ throw new Error(`Role not found: ${slug}`);
28
+ }
29
+ return readFileSync(filePath, "utf-8");
30
+ }
31
+ export function removeRole(slug) {
32
+ const filePath = join(rolesDir(), `${slug}.md`);
33
+ if (!existsSync(filePath)) {
34
+ throw new Error(`Role not found: ${slug}`);
35
+ }
36
+ unlinkSync(filePath);
37
+ }
38
+ export function loadRole(slug) {
39
+ return showRole(slug);
40
+ }
41
+ //# sourceMappingURL=roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.js","sourceRoot":"","sources":["../../src/storage/roles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACtG,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,SAAS,QAAQ;IACf,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;QAAE,OAAO,EAAE,CAAC;IAEvC,OAAO,WAAW,CAAC,QAAQ,EAAE,CAAC;SAC3B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SAChC,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,WAAmB;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;IACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC;IAEhD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,wBAAwB,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,SAAS,CAAC,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,aAAa,CAAC,QAAQ,EAAE,KAAK,WAAW,iBAAiB,WAAW,KAAK,EAAE,OAAO,CAAC,CAAC;AACtF,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC;IAEhD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,OAAO,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,IAAI,KAAK,CAAC,CAAC;IAEhD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,UAAU,CAAC,QAAQ,CAAC,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Scenario Refine Pipeline.
3
+ * Runs a 3-stage refinement pipeline that transforms raw scenario hints from
4
+ * a spec into structured, validated scenario definitions.
5
+ *
6
+ * Stage 1 — Structure: Normalize scenario hints into canonical { given, expected } format.
7
+ * Stage 2 — Edge Cases: Print advisory edge-case suggestions to stdout (no file writes).
8
+ * Stage 3 — Validation Commands: Attach a shell snippet to each scenario for verification.
9
+ */
10
+ /**
11
+ * Build the Stage 1 prompt: Structure.
12
+ * Transforms raw scenario hints into canonical { given, expected } format.
13
+ * @param scenarioHints - JSON string of raw scenario hints from the spec.
14
+ * @param specTitle - The task/spec title for context.
15
+ * @returns The full prompt string for Stage 1.
16
+ */
17
+ export declare function buildStructurePrompt(scenarioHints: string, specTitle: string): string;
18
+ /**
19
+ * Build the Stage 2 prompt: Edge Cases.
20
+ * Generates advisory edge-case suggestions based on structured scenarios.
21
+ * @param structuredScenarios - JSON string of structured scenarios from Stage 1.
22
+ * @param specTitle - The task/spec title for context.
23
+ * @returns The full prompt string for Stage 2.
24
+ */
25
+ export declare function buildEdgeCasesPrompt(structuredScenarios: string, specTitle: string): string;
26
+ /**
27
+ * Build the Stage 3 prompt: Validation Commands.
28
+ * Generates a shell command for each scenario to validate expected outcomes.
29
+ * @param structuredScenarios - JSON string of structured scenarios from Stage 1.
30
+ * @param specTitle - The task/spec title for context.
31
+ * @returns The full prompt string for Stage 3.
32
+ */
33
+ export declare function buildValidationCommandPrompt(structuredScenarios: string, specTitle: string): string;
34
+ /**
35
+ * Run the 3-stage scenario refinement pipeline.
36
+ *
37
+ * 1. Reads `.stoa/specs/<taskName>.json` for scenario hints.
38
+ * 2. Stage 1: Structure — normalizes hints into { given, expected }.
39
+ * 3. Stage 2: Edge Cases — prints advisory suggestions to stdout.
40
+ * 4. Stage 3: Validation Commands — attaches a bash snippet to each scenario.
41
+ * 5. Writes final output to `.stoa/scenarios/<taskName>.json` (api/cli mode only).
42
+ *
43
+ * @param taskName - The task name (matches the spec filename without extension).
44
+ * @param mode - Execution mode: "api", "clipboard", or "cli".
45
+ */
46
+ export declare function generateScenarios(taskName: string, mode: "api" | "clipboard" | "cli"): Promise<void>;
47
+ //# sourceMappingURL=scenarios-refine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scenarios-refine.d.ts","sourceRoot":"","sources":["../../src/storage/scenarios-refine.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA8CH;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAoBrF;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,mBAAmB,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAmB3F;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,mBAAmB,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAqBnG;AAuGD;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,KAAK,GAAG,WAAW,GAAG,KAAK,GAChC,OAAO,CAAC,IAAI,CAAC,CA0Hf"}
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Scenario Refine Pipeline.
3
+ * Runs a 3-stage refinement pipeline that transforms raw scenario hints from
4
+ * a spec into structured, validated scenario definitions.
5
+ *
6
+ * Stage 1 — Structure: Normalize scenario hints into canonical { given, expected } format.
7
+ * Stage 2 — Edge Cases: Print advisory edge-case suggestions to stdout (no file writes).
8
+ * Stage 3 — Validation Commands: Attach a shell snippet to each scenario for verification.
9
+ */
10
+ import Anthropic from "@anthropic-ai/sdk";
11
+ import { spawn } from "child_process";
12
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
13
+ import { dirname, join } from "node:path";
14
+ const SPAWN_TIMEOUT_MS = 300_000;
15
+ // ── Stage labels ─────────────────────────────────────────────────────
16
+ const STAGE_LABELS = [
17
+ "Stage 1: Structure",
18
+ "Stage 2: Edge Cases",
19
+ "Stage 3: Validation Commands",
20
+ ];
21
+ // ── Pure prompt builders ─────────────────────────────────────────────
22
+ /**
23
+ * Build the Stage 1 prompt: Structure.
24
+ * Transforms raw scenario hints into canonical { given, expected } format.
25
+ * @param scenarioHints - JSON string of raw scenario hints from the spec.
26
+ * @param specTitle - The task/spec title for context.
27
+ * @returns The full prompt string for Stage 1.
28
+ */
29
+ export function buildStructurePrompt(scenarioHints, specTitle) {
30
+ return [
31
+ "You are a QA engineer specializing in test scenario design.",
32
+ "",
33
+ "Given the following raw scenario hints from a task specification,",
34
+ "normalize each one into a structured JSON array of objects with exactly two fields:",
35
+ '- "given": A clear description of the initial condition or setup.',
36
+ '- "expected": A clear description of the expected outcome or behavior.',
37
+ "",
38
+ "Rules:",
39
+ "- Preserve the original intent of each scenario.",
40
+ "- If a hint has a title but no given/expected split, infer them from the title.",
41
+ "- If a hint is vague, make it concrete and testable.",
42
+ "- Output ONLY a valid JSON array — no preamble, no commentary.",
43
+ "",
44
+ `Task: ${specTitle}`,
45
+ "",
46
+ "Scenario hints:",
47
+ scenarioHints,
48
+ ].join("\n");
49
+ }
50
+ /**
51
+ * Build the Stage 2 prompt: Edge Cases.
52
+ * Generates advisory edge-case suggestions based on structured scenarios.
53
+ * @param structuredScenarios - JSON string of structured scenarios from Stage 1.
54
+ * @param specTitle - The task/spec title for context.
55
+ * @returns The full prompt string for Stage 2.
56
+ */
57
+ export function buildEdgeCasesPrompt(structuredScenarios, specTitle) {
58
+ return [
59
+ "You are a QA engineer specializing in edge-case discovery.",
60
+ "",
61
+ "Given the following structured test scenarios for a task,",
62
+ "suggest additional edge cases that are NOT already covered.",
63
+ "",
64
+ "Rules:",
65
+ "- Output a plain numbered list of edge-case descriptions.",
66
+ "- Each item should be a single sentence describing the edge case.",
67
+ "- Focus on boundary conditions, error paths, and unusual inputs.",
68
+ "- Do NOT output JSON — just a numbered list.",
69
+ "- Do NOT repeat scenarios already covered.",
70
+ "",
71
+ `Task: ${specTitle}`,
72
+ "",
73
+ "Existing scenarios:",
74
+ structuredScenarios,
75
+ ].join("\n");
76
+ }
77
+ /**
78
+ * Build the Stage 3 prompt: Validation Commands.
79
+ * Generates a shell command for each scenario to validate expected outcomes.
80
+ * @param structuredScenarios - JSON string of structured scenarios from Stage 1.
81
+ * @param specTitle - The task/spec title for context.
82
+ * @returns The full prompt string for Stage 3.
83
+ */
84
+ export function buildValidationCommandPrompt(structuredScenarios, specTitle) {
85
+ return [
86
+ "You are a DevOps engineer writing validation scripts.",
87
+ "",
88
+ "Given the following structured test scenarios for a task,",
89
+ "generate a single bash command (or short pipeline) for each scenario",
90
+ "that can be run to validate whether the expected outcome holds.",
91
+ "",
92
+ "Rules:",
93
+ "- Output a valid JSON array of objects, one per scenario, in the same order as the input.",
94
+ '- Each object must have: "given", "expected", "validationCommand".',
95
+ '- "validationCommand" is a single bash string that tests the expected behavior.',
96
+ "- Commands should be self-contained and runnable from the project root.",
97
+ "- Use common CLI tools (grep, jq, curl, test, diff, etc.).",
98
+ "- Output ONLY a valid JSON array — no preamble, no commentary.",
99
+ "",
100
+ `Task: ${specTitle}`,
101
+ "",
102
+ "Scenarios:",
103
+ structuredScenarios,
104
+ ].join("\n");
105
+ }
106
+ // ── Execution helper ─────────────────────────────────────────────────
107
+ async function executeStage(prompt, mode, model, apiKey) {
108
+ if (mode === "api") {
109
+ const client = new Anthropic({
110
+ apiKey: apiKey || process.env.ANTHROPIC_API_KEY,
111
+ });
112
+ const response = await client.messages.create({
113
+ model,
114
+ max_tokens: 4096,
115
+ messages: [{ role: "user", content: prompt }],
116
+ });
117
+ const textBlock = response.content.find((block) => block.type === "text");
118
+ return textBlock ? textBlock.text : "";
119
+ }
120
+ // cli mode — pipe prompt via stdin (the -p flag hangs as subprocess)
121
+ return new Promise((resolve, reject) => {
122
+ const child = spawn("claude", ["--print"], {
123
+ stdio: ["pipe", "pipe", "pipe"],
124
+ });
125
+ let stdout = "";
126
+ let resolved = false;
127
+ let dataTimer = null;
128
+ const finish = () => {
129
+ if (resolved)
130
+ return;
131
+ resolved = true;
132
+ clearTimeout(overallTimer);
133
+ if (dataTimer)
134
+ clearTimeout(dataTimer);
135
+ child.kill();
136
+ resolve(stdout.trim());
137
+ };
138
+ child.stdout.on("data", (chunk) => {
139
+ stdout += chunk.toString();
140
+ if (dataTimer)
141
+ clearTimeout(dataTimer);
142
+ dataTimer = setTimeout(finish, 2000);
143
+ });
144
+ child.on("close", () => {
145
+ finish();
146
+ });
147
+ const overallTimer = setTimeout(() => {
148
+ if (!resolved) {
149
+ resolved = true;
150
+ if (dataTimer)
151
+ clearTimeout(dataTimer);
152
+ child.kill("SIGKILL");
153
+ reject(new Error(`Claude Code timed out after ${SPAWN_TIMEOUT_MS / 1000}s`));
154
+ }
155
+ }, SPAWN_TIMEOUT_MS);
156
+ child.on("error", (err) => {
157
+ if (!resolved) {
158
+ resolved = true;
159
+ clearTimeout(overallTimer);
160
+ if (dataTimer)
161
+ clearTimeout(dataTimer);
162
+ reject(err);
163
+ }
164
+ });
165
+ // Write prompt to stdin and close it
166
+ child.stdin.write(prompt);
167
+ child.stdin.end();
168
+ });
169
+ }
170
+ // ── JSON extraction helper ───────────────────────────────────────────
171
+ function extractFirstJsonArray(raw) {
172
+ const start = raw.indexOf("[");
173
+ if (start === -1)
174
+ return null;
175
+ let depth = 0;
176
+ for (let i = start; i < raw.length; i++) {
177
+ if (raw[i] === "[")
178
+ depth++;
179
+ else if (raw[i] === "]")
180
+ depth--;
181
+ if (depth === 0) {
182
+ try {
183
+ const parsed = JSON.parse(raw.slice(start, i + 1));
184
+ if (Array.isArray(parsed))
185
+ return parsed;
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ return null;
191
+ }
192
+ }
193
+ return null;
194
+ }
195
+ // ── Pipeline ─────────────────────────────────────────────────────────
196
+ /**
197
+ * Run the 3-stage scenario refinement pipeline.
198
+ *
199
+ * 1. Reads `.stoa/specs/<taskName>.json` for scenario hints.
200
+ * 2. Stage 1: Structure — normalizes hints into { given, expected }.
201
+ * 3. Stage 2: Edge Cases — prints advisory suggestions to stdout.
202
+ * 4. Stage 3: Validation Commands — attaches a bash snippet to each scenario.
203
+ * 5. Writes final output to `.stoa/scenarios/<taskName>.json` (api/cli mode only).
204
+ *
205
+ * @param taskName - The task name (matches the spec filename without extension).
206
+ * @param mode - Execution mode: "api", "clipboard", or "cli".
207
+ */
208
+ export async function generateScenarios(taskName, mode) {
209
+ const baseDir = process.cwd();
210
+ const specPath = join(baseDir, ".stoa", "specs", `${taskName}.json`);
211
+ if (!existsSync(specPath)) {
212
+ process.stderr.write(`Error: Spec file not found: .stoa/specs/${taskName}.json\n`);
213
+ process.exit(1);
214
+ }
215
+ const specRaw = readFileSync(specPath, "utf-8");
216
+ const spec = JSON.parse(specRaw);
217
+ // Extract scenario hints — look for scenarios field from Stage 5 output
218
+ const scenarioHints = spec.scenarios ?? [];
219
+ const specTitle = spec.title ?? taskName;
220
+ if (scenarioHints.length === 0) {
221
+ process.stderr.write(`Warning: No scenario hints found in spec. The pipeline will generate from the spec context.\n`);
222
+ }
223
+ const hintsJson = JSON.stringify(scenarioHints, null, 2);
224
+ // ── Clipboard mode ──────────────────────────────────────────────
225
+ if (mode === "clipboard") {
226
+ const prompts = [
227
+ { label: STAGE_LABELS[0], prompt: buildStructurePrompt(hintsJson, specTitle) },
228
+ { label: STAGE_LABELS[1], prompt: buildEdgeCasesPrompt(hintsJson, specTitle) },
229
+ { label: STAGE_LABELS[2], prompt: buildValidationCommandPrompt(hintsJson, specTitle) },
230
+ ];
231
+ for (const { label, prompt } of prompts) {
232
+ const line = "─".repeat(40);
233
+ process.stdout.write(`\n${line}\n`);
234
+ process.stdout.write(`${label}\n`);
235
+ process.stdout.write(`${line}\n`);
236
+ process.stdout.write(`${prompt}\n`);
237
+ }
238
+ // Copy last prompt to clipboard
239
+ try {
240
+ const { execFileSync } = await import("node:child_process");
241
+ const allPrompts = prompts.map((p) => `--- ${p.label} ---\n${p.prompt}`).join("\n\n");
242
+ execFileSync("pbcopy", { input: allPrompts });
243
+ process.stdout.write(`\nPrompts copied to clipboard.\n`);
244
+ }
245
+ catch {
246
+ // Clipboard copy failed silently — prompts are already on stdout
247
+ }
248
+ return;
249
+ }
250
+ // ── API or CLI mode ─────────────────────────────────────────────
251
+ const model = "claude-sonnet-4-6";
252
+ const execMode = mode === "cli" ? "cli" : "api";
253
+ // Stage 1: Structure
254
+ process.stderr.write(`${STAGE_LABELS[0]}...\n`);
255
+ const stage1Prompt = buildStructurePrompt(hintsJson, specTitle);
256
+ const stage1Raw = await executeStage(stage1Prompt, execMode, model);
257
+ const stage1Parsed = extractFirstJsonArray(stage1Raw);
258
+ const structured = [];
259
+ if (stage1Parsed) {
260
+ for (const item of stage1Parsed) {
261
+ const obj = item;
262
+ if (typeof obj.given === "string" && typeof obj.expected === "string") {
263
+ structured.push({ given: obj.given, expected: obj.expected });
264
+ }
265
+ }
266
+ }
267
+ if (structured.length === 0) {
268
+ process.stderr.write("Error: Stage 1 produced no valid structured scenarios.\n");
269
+ process.exit(1);
270
+ }
271
+ process.stderr.write(` → ${structured.length} scenarios structured.\n`);
272
+ // Stage 2: Edge Cases (advisory only — print to stdout, no writes)
273
+ process.stderr.write(`${STAGE_LABELS[1]}...\n`);
274
+ const structuredJson = JSON.stringify(structured, null, 2);
275
+ const stage2Prompt = buildEdgeCasesPrompt(structuredJson, specTitle);
276
+ const stage2Raw = await executeStage(stage2Prompt, execMode, model);
277
+ const line = "─".repeat(40);
278
+ process.stdout.write(`\n${line}\n`);
279
+ process.stdout.write(`Suggested Edge Cases (advisory only):\n`);
280
+ process.stdout.write(`${line}\n`);
281
+ process.stdout.write(`${stage2Raw}\n`);
282
+ // Stage 3: Validation Commands
283
+ process.stderr.write(`${STAGE_LABELS[2]}...\n`);
284
+ const stage3Prompt = buildValidationCommandPrompt(structuredJson, specTitle);
285
+ const stage3Raw = await executeStage(stage3Prompt, execMode, model);
286
+ const stage3Parsed = extractFirstJsonArray(stage3Raw);
287
+ const finalScenarios = [];
288
+ if (stage3Parsed) {
289
+ for (let i = 0; i < stage3Parsed.length; i++) {
290
+ const obj = stage3Parsed[i];
291
+ const base = structured[i] ?? { given: "", expected: "" };
292
+ finalScenarios.push({
293
+ given: typeof obj.given === "string" ? obj.given : base.given,
294
+ expected: typeof obj.expected === "string" ? obj.expected : base.expected,
295
+ validationCommand: typeof obj.validationCommand === "string" ? obj.validationCommand : "",
296
+ });
297
+ }
298
+ }
299
+ else {
300
+ // Fallback: use structured scenarios with empty validation commands
301
+ for (const s of structured) {
302
+ finalScenarios.push({ ...s, validationCommand: "" });
303
+ }
304
+ }
305
+ // Write output
306
+ const outPath = join(baseDir, ".stoa", "scenarios", `${taskName}.json`);
307
+ mkdirSync(dirname(outPath), { recursive: true });
308
+ writeFileSync(outPath, JSON.stringify(finalScenarios, null, 2) + "\n", "utf-8");
309
+ process.stderr.write(`\nWritten: .stoa/scenarios/${taskName}.json (${finalScenarios.length} scenarios)\n`);
310
+ }
311
+ //# sourceMappingURL=scenarios-refine.js.map