corbat-coco 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +381 -0
- package/dist/cli/index.js +4887 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +3185 -0
- package/dist/index.js +8663 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,4887 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import chalk5 from 'chalk';
|
|
5
|
+
import fs4 from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import JSON5 from 'json5';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
11
|
+
|
|
12
|
+
// src/version.ts
|
|
13
|
+
var VERSION = "0.1.0";
|
|
14
|
+
async function createProjectStructure(projectPath, info) {
|
|
15
|
+
const cocoPath = path.join(projectPath, ".coco");
|
|
16
|
+
const directories = [
|
|
17
|
+
cocoPath,
|
|
18
|
+
path.join(cocoPath, "state"),
|
|
19
|
+
path.join(cocoPath, "checkpoints"),
|
|
20
|
+
path.join(cocoPath, "logs"),
|
|
21
|
+
path.join(cocoPath, "discovery"),
|
|
22
|
+
path.join(cocoPath, "spec"),
|
|
23
|
+
path.join(cocoPath, "architecture"),
|
|
24
|
+
path.join(cocoPath, "architecture", "adrs"),
|
|
25
|
+
path.join(cocoPath, "architecture", "diagrams"),
|
|
26
|
+
path.join(cocoPath, "planning"),
|
|
27
|
+
path.join(cocoPath, "planning", "epics"),
|
|
28
|
+
path.join(cocoPath, "execution"),
|
|
29
|
+
path.join(cocoPath, "versions"),
|
|
30
|
+
path.join(cocoPath, "reviews"),
|
|
31
|
+
path.join(cocoPath, "delivery")
|
|
32
|
+
];
|
|
33
|
+
for (const dir of directories) {
|
|
34
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
await createInitialConfig(cocoPath, info);
|
|
37
|
+
await createProjectState(cocoPath, info);
|
|
38
|
+
await createGitignore(cocoPath);
|
|
39
|
+
await createReadme(cocoPath, info);
|
|
40
|
+
}
|
|
41
|
+
async function createInitialConfig(cocoPath, info) {
|
|
42
|
+
const config = {
|
|
43
|
+
project: {
|
|
44
|
+
name: info.name,
|
|
45
|
+
version: "0.1.0",
|
|
46
|
+
description: info.description
|
|
47
|
+
},
|
|
48
|
+
stack: {
|
|
49
|
+
language: info.language,
|
|
50
|
+
framework: info.framework
|
|
51
|
+
},
|
|
52
|
+
provider: {
|
|
53
|
+
type: "anthropic",
|
|
54
|
+
model: "claude-sonnet-4-20250514"
|
|
55
|
+
},
|
|
56
|
+
quality: {
|
|
57
|
+
minScore: 85,
|
|
58
|
+
minCoverage: 80,
|
|
59
|
+
maxIterations: 10,
|
|
60
|
+
convergenceThreshold: 2
|
|
61
|
+
},
|
|
62
|
+
persistence: {
|
|
63
|
+
checkpointInterval: 3e5,
|
|
64
|
+
maxCheckpoints: 50
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
await fs4.writeFile(
|
|
68
|
+
path.join(cocoPath, "config.json"),
|
|
69
|
+
JSON.stringify(config, null, 2),
|
|
70
|
+
"utf-8"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
async function createProjectState(cocoPath, info) {
|
|
74
|
+
const state = {
|
|
75
|
+
id: `proj_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`,
|
|
76
|
+
name: info.name,
|
|
77
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
78
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
79
|
+
currentPhase: "idle",
|
|
80
|
+
phaseHistory: [],
|
|
81
|
+
currentTask: null,
|
|
82
|
+
completedTasks: [],
|
|
83
|
+
pendingTasks: [],
|
|
84
|
+
lastScores: null,
|
|
85
|
+
qualityHistory: [],
|
|
86
|
+
lastCheckpoint: null
|
|
87
|
+
};
|
|
88
|
+
await fs4.writeFile(
|
|
89
|
+
path.join(cocoPath, "state", "project.json"),
|
|
90
|
+
JSON.stringify(state, null, 2),
|
|
91
|
+
"utf-8"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
async function createGitignore(cocoPath) {
|
|
95
|
+
const content = `# Corbat-Coco
|
|
96
|
+
|
|
97
|
+
# Sensitive data
|
|
98
|
+
config.json
|
|
99
|
+
|
|
100
|
+
# Logs
|
|
101
|
+
logs/
|
|
102
|
+
|
|
103
|
+
# Checkpoints (can be large)
|
|
104
|
+
checkpoints/
|
|
105
|
+
|
|
106
|
+
# Session state
|
|
107
|
+
state/session.json
|
|
108
|
+
state/lock.json
|
|
109
|
+
`;
|
|
110
|
+
await fs4.writeFile(path.join(cocoPath, ".gitignore"), content, "utf-8");
|
|
111
|
+
}
|
|
112
|
+
async function createReadme(cocoPath, info) {
|
|
113
|
+
const content = `# Corbat-Coco Project: ${info.name}
|
|
114
|
+
|
|
115
|
+
This directory contains the Corbat-Coco project metadata and state.
|
|
116
|
+
|
|
117
|
+
## Structure
|
|
118
|
+
|
|
119
|
+
\`\`\`
|
|
120
|
+
.coco/
|
|
121
|
+
\u251C\u2500\u2500 config.json # Project configuration
|
|
122
|
+
\u251C\u2500\u2500 state/ # Current project state
|
|
123
|
+
\u2502 \u251C\u2500\u2500 project.json # Main state file
|
|
124
|
+
\u2502 \u2514\u2500\u2500 session.json # Active session
|
|
125
|
+
\u251C\u2500\u2500 checkpoints/ # Recovery checkpoints
|
|
126
|
+
\u251C\u2500\u2500 logs/ # Execution logs
|
|
127
|
+
\u251C\u2500\u2500 discovery/ # Discovery phase artifacts
|
|
128
|
+
\u251C\u2500\u2500 spec/ # Specification documents
|
|
129
|
+
\u251C\u2500\u2500 architecture/ # Architecture documents
|
|
130
|
+
\u2502 \u251C\u2500\u2500 adrs/ # Architecture Decision Records
|
|
131
|
+
\u2502 \u2514\u2500\u2500 diagrams/ # System diagrams
|
|
132
|
+
\u251C\u2500\u2500 planning/ # Planning phase artifacts
|
|
133
|
+
\u2502 \u2514\u2500\u2500 epics/ # Epic definitions
|
|
134
|
+
\u251C\u2500\u2500 execution/ # Execution tracking
|
|
135
|
+
\u251C\u2500\u2500 versions/ # Task version history
|
|
136
|
+
\u251C\u2500\u2500 reviews/ # Review documents
|
|
137
|
+
\u2514\u2500\u2500 delivery/ # Deployment artifacts
|
|
138
|
+
\`\`\`
|
|
139
|
+
|
|
140
|
+
## Commands
|
|
141
|
+
|
|
142
|
+
- \`coco status\` - Show current progress
|
|
143
|
+
- \`coco resume\` - Resume from last checkpoint
|
|
144
|
+
- \`coco plan\` - Run/update planning
|
|
145
|
+
- \`coco build\` - Execute tasks
|
|
146
|
+
|
|
147
|
+
## Configuration
|
|
148
|
+
|
|
149
|
+
Edit \`config.json\` to customize:
|
|
150
|
+
- LLM provider and model
|
|
151
|
+
- Quality thresholds
|
|
152
|
+
- Checkpoint settings
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
Generated by Corbat-Coco v0.1.0
|
|
156
|
+
`;
|
|
157
|
+
await fs4.writeFile(path.join(cocoPath, "README.md"), content, "utf-8");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/cli/commands/init.ts
|
|
161
|
+
function registerInitCommand(program2) {
|
|
162
|
+
program2.command("init").description("Initialize a new Corbat-Coco project").argument("[path]", "Project directory path", ".").option("-t, --template <template>", "Project template to use").option("-y, --yes", "Skip prompts and use defaults").option("--skip-discovery", "Skip the discovery phase (use existing spec)").action(async (path5, options) => {
|
|
163
|
+
await runInit(path5, options);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function runInit(projectPath, options) {
|
|
167
|
+
p.intro(chalk5.cyan("Welcome to Corbat-Coco!"));
|
|
168
|
+
const existingProject = await checkExistingProject(projectPath);
|
|
169
|
+
if (existingProject) {
|
|
170
|
+
const shouldContinue = await p.confirm({
|
|
171
|
+
message: "A Corbat-Coco project already exists here. Continue anyway?"
|
|
172
|
+
});
|
|
173
|
+
if (p.isCancel(shouldContinue) || !shouldContinue) {
|
|
174
|
+
p.cancel("Initialization cancelled.");
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
let projectInfo;
|
|
179
|
+
if (options.yes) {
|
|
180
|
+
projectInfo = getDefaultProjectInfo(projectPath);
|
|
181
|
+
} else {
|
|
182
|
+
const result = await gatherProjectInfo();
|
|
183
|
+
if (!result) {
|
|
184
|
+
p.cancel("Initialization cancelled.");
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
projectInfo = result;
|
|
188
|
+
}
|
|
189
|
+
const spinner4 = p.spinner();
|
|
190
|
+
spinner4.start("Creating project structure...");
|
|
191
|
+
try {
|
|
192
|
+
await createProjectStructure(projectPath, projectInfo);
|
|
193
|
+
spinner4.stop("Project structure created.");
|
|
194
|
+
} catch (error) {
|
|
195
|
+
spinner4.stop("Failed to create project structure.");
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
p.outro(chalk5.green("Project initialized successfully!"));
|
|
199
|
+
console.log("\nNext steps:");
|
|
200
|
+
console.log(chalk5.dim(" 1. ") + chalk5.cyan("coco plan") + chalk5.dim(" - Run discovery and create a development plan"));
|
|
201
|
+
console.log(chalk5.dim(" 2. ") + chalk5.cyan("coco build") + chalk5.dim(" - Start building the project"));
|
|
202
|
+
console.log(chalk5.dim(" 3. ") + chalk5.cyan("coco status") + chalk5.dim(" - Check current progress"));
|
|
203
|
+
}
|
|
204
|
+
async function gatherProjectInfo() {
|
|
205
|
+
const name = await p.text({
|
|
206
|
+
message: "What is your project name?",
|
|
207
|
+
placeholder: "my-awesome-project",
|
|
208
|
+
validate: (value) => {
|
|
209
|
+
if (!value) return "Project name is required";
|
|
210
|
+
if (!/^[a-z0-9-]+$/.test(value)) return "Use lowercase letters, numbers, and hyphens only";
|
|
211
|
+
return void 0;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
if (p.isCancel(name)) return null;
|
|
215
|
+
const description = await p.text({
|
|
216
|
+
message: "Describe your project in one sentence:",
|
|
217
|
+
placeholder: "A REST API for managing tasks"
|
|
218
|
+
});
|
|
219
|
+
if (p.isCancel(description)) return null;
|
|
220
|
+
const language = await p.select({
|
|
221
|
+
message: "What programming language?",
|
|
222
|
+
options: [
|
|
223
|
+
{ value: "typescript", label: "TypeScript", hint: "Recommended" },
|
|
224
|
+
{ value: "python", label: "Python" },
|
|
225
|
+
{ value: "go", label: "Go" },
|
|
226
|
+
{ value: "rust", label: "Rust" }
|
|
227
|
+
]
|
|
228
|
+
});
|
|
229
|
+
if (p.isCancel(language)) return null;
|
|
230
|
+
return {
|
|
231
|
+
name,
|
|
232
|
+
description: description || "",
|
|
233
|
+
language
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function getDefaultProjectInfo(path5) {
|
|
237
|
+
const name = path5 === "." ? "my-project" : path5.split("/").pop() || "my-project";
|
|
238
|
+
return {
|
|
239
|
+
name,
|
|
240
|
+
description: "",
|
|
241
|
+
language: "typescript"
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
async function checkExistingProject(path5) {
|
|
245
|
+
try {
|
|
246
|
+
const fs5 = await import('fs/promises');
|
|
247
|
+
await fs5.access(`${path5}/.coco`);
|
|
248
|
+
return true;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
var ProviderConfigSchema = z.object({
|
|
254
|
+
type: z.enum(["anthropic", "openai", "local"]).default("anthropic"),
|
|
255
|
+
apiKey: z.string().optional(),
|
|
256
|
+
model: z.string().default("claude-sonnet-4-20250514"),
|
|
257
|
+
maxTokens: z.number().min(1).max(2e5).default(8192),
|
|
258
|
+
temperature: z.number().min(0).max(2).default(0),
|
|
259
|
+
timeout: z.number().min(1e3).default(12e4)
|
|
260
|
+
});
|
|
261
|
+
var QualityConfigSchema = z.object({
|
|
262
|
+
minScore: z.number().min(0).max(100).default(85),
|
|
263
|
+
minCoverage: z.number().min(0).max(100).default(80),
|
|
264
|
+
maxIterations: z.number().min(1).max(20).default(10),
|
|
265
|
+
minIterations: z.number().min(1).max(10).default(2),
|
|
266
|
+
convergenceThreshold: z.number().min(0).max(10).default(2),
|
|
267
|
+
securityThreshold: z.number().min(0).max(100).default(100)
|
|
268
|
+
});
|
|
269
|
+
var PersistenceConfigSchema = z.object({
|
|
270
|
+
checkpointInterval: z.number().min(6e4).default(3e5),
|
|
271
|
+
// 5 min default
|
|
272
|
+
maxCheckpoints: z.number().min(1).max(100).default(50),
|
|
273
|
+
retentionDays: z.number().min(1).max(365).default(7),
|
|
274
|
+
compressOldCheckpoints: z.boolean().default(true)
|
|
275
|
+
});
|
|
276
|
+
var StackConfigSchema = z.object({
|
|
277
|
+
language: z.enum(["typescript", "python", "go", "rust", "java"]),
|
|
278
|
+
framework: z.string().optional(),
|
|
279
|
+
profile: z.string().optional()
|
|
280
|
+
// Custom profile path
|
|
281
|
+
});
|
|
282
|
+
var ProjectConfigSchema = z.object({
|
|
283
|
+
name: z.string().min(1),
|
|
284
|
+
version: z.string().default("0.1.0"),
|
|
285
|
+
description: z.string().optional()
|
|
286
|
+
});
|
|
287
|
+
var GitHubConfigSchema = z.object({
|
|
288
|
+
enabled: z.boolean().default(false),
|
|
289
|
+
token: z.string().optional(),
|
|
290
|
+
repo: z.string().optional(),
|
|
291
|
+
createPRs: z.boolean().default(true),
|
|
292
|
+
createIssues: z.boolean().default(true)
|
|
293
|
+
});
|
|
294
|
+
var IntegrationsConfigSchema = z.object({
|
|
295
|
+
github: GitHubConfigSchema.optional()
|
|
296
|
+
});
|
|
297
|
+
var CocoConfigSchema = z.object({
|
|
298
|
+
project: ProjectConfigSchema,
|
|
299
|
+
provider: ProviderConfigSchema.default({}),
|
|
300
|
+
quality: QualityConfigSchema.default({}),
|
|
301
|
+
persistence: PersistenceConfigSchema.default({}),
|
|
302
|
+
stack: StackConfigSchema.optional(),
|
|
303
|
+
integrations: IntegrationsConfigSchema.optional()
|
|
304
|
+
});
|
|
305
|
+
function createDefaultConfigObject(projectName, language = "typescript") {
|
|
306
|
+
return {
|
|
307
|
+
project: {
|
|
308
|
+
name: projectName,
|
|
309
|
+
version: "0.1.0"
|
|
310
|
+
},
|
|
311
|
+
provider: {
|
|
312
|
+
type: "anthropic",
|
|
313
|
+
model: "claude-sonnet-4-20250514",
|
|
314
|
+
maxTokens: 8192,
|
|
315
|
+
temperature: 0,
|
|
316
|
+
timeout: 12e4
|
|
317
|
+
},
|
|
318
|
+
quality: {
|
|
319
|
+
minScore: 85,
|
|
320
|
+
minCoverage: 80,
|
|
321
|
+
maxIterations: 10,
|
|
322
|
+
minIterations: 2,
|
|
323
|
+
convergenceThreshold: 2,
|
|
324
|
+
securityThreshold: 100
|
|
325
|
+
},
|
|
326
|
+
persistence: {
|
|
327
|
+
checkpointInterval: 3e5,
|
|
328
|
+
maxCheckpoints: 50,
|
|
329
|
+
retentionDays: 7,
|
|
330
|
+
compressOldCheckpoints: true
|
|
331
|
+
},
|
|
332
|
+
stack: {
|
|
333
|
+
language
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/config/loader.ts
|
|
339
|
+
async function loadConfig(configPath) {
|
|
340
|
+
const resolvedPath = configPath || findConfigPathSync();
|
|
341
|
+
try {
|
|
342
|
+
const content = await fs4.readFile(resolvedPath, "utf-8");
|
|
343
|
+
const parsed = JSON5.parse(content);
|
|
344
|
+
const result = CocoConfigSchema.safeParse(parsed);
|
|
345
|
+
if (!result.success) {
|
|
346
|
+
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
347
|
+
throw new Error(`Invalid configuration:
|
|
348
|
+
${issues}`);
|
|
349
|
+
}
|
|
350
|
+
return result.data;
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (error.code === "ENOENT") {
|
|
353
|
+
return createDefaultConfig("my-project");
|
|
354
|
+
}
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function createDefaultConfig(projectName, language = "typescript") {
|
|
359
|
+
return createDefaultConfigObject(projectName, language);
|
|
360
|
+
}
|
|
361
|
+
async function findConfigPath(cwd) {
|
|
362
|
+
const envPath = process.env["COCO_CONFIG_PATH"];
|
|
363
|
+
if (envPath) {
|
|
364
|
+
try {
|
|
365
|
+
await fs4.access(envPath);
|
|
366
|
+
return envPath;
|
|
367
|
+
} catch {
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const basePath = cwd || process.cwd();
|
|
371
|
+
const configPath = path.join(basePath, ".coco", "config.json");
|
|
372
|
+
try {
|
|
373
|
+
await fs4.access(configPath);
|
|
374
|
+
return configPath;
|
|
375
|
+
} catch {
|
|
376
|
+
return void 0;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function findConfigPathSync() {
|
|
380
|
+
const envPath = process.env["COCO_CONFIG_PATH"];
|
|
381
|
+
if (envPath) {
|
|
382
|
+
return envPath;
|
|
383
|
+
}
|
|
384
|
+
return path.join(process.cwd(), ".coco", "config.json");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/phases/converge/prompts.ts
|
|
388
|
+
var DISCOVERY_SYSTEM_PROMPT = `You are a senior software architect and requirements analyst. Your role is to help gather and clarify requirements for software projects.
|
|
389
|
+
|
|
390
|
+
Your goals:
|
|
391
|
+
1. Understand what the user wants to build
|
|
392
|
+
2. Extract clear, actionable requirements
|
|
393
|
+
3. Identify ambiguities and ask clarifying questions
|
|
394
|
+
4. Make reasonable assumptions when appropriate
|
|
395
|
+
5. Recommend technology choices when needed
|
|
396
|
+
|
|
397
|
+
Guidelines:
|
|
398
|
+
- Be thorough but not overwhelming
|
|
399
|
+
- Ask focused, specific questions
|
|
400
|
+
- Group related questions together
|
|
401
|
+
- Prioritize questions by importance
|
|
402
|
+
- Make assumptions for minor details
|
|
403
|
+
- Always explain your reasoning
|
|
404
|
+
|
|
405
|
+
You communicate in a professional but friendly manner. You use concrete examples to clarify abstract requirements.`;
|
|
406
|
+
var INITIAL_ANALYSIS_PROMPT = `Analyze the following project description and extract:
|
|
407
|
+
|
|
408
|
+
1. **Project Type**: What kind of software is this? (CLI, API, web app, library, service, etc.)
|
|
409
|
+
2. **Complexity**: How complex is this project? (simple, moderate, complex, enterprise)
|
|
410
|
+
3. **Completeness**: How complete is the description? (0-100%)
|
|
411
|
+
4. **Functional Requirements**: What should the system do?
|
|
412
|
+
5. **Non-Functional Requirements**: Performance, security, scalability needs
|
|
413
|
+
6. **Technical Constraints**: Any specified technologies or limitations
|
|
414
|
+
7. **Assumptions**: What must we assume to proceed?
|
|
415
|
+
8. **Critical Questions**: What must be clarified before proceeding?
|
|
416
|
+
9. **Technology Recommendations**: What tech stack would you recommend?
|
|
417
|
+
|
|
418
|
+
User's project description:
|
|
419
|
+
---
|
|
420
|
+
{{userInput}}
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
Respond in JSON format:
|
|
424
|
+
{
|
|
425
|
+
"projectType": "string",
|
|
426
|
+
"complexity": "simple|moderate|complex|enterprise",
|
|
427
|
+
"completeness": number,
|
|
428
|
+
"requirements": [
|
|
429
|
+
{
|
|
430
|
+
"category": "functional|non_functional|technical|constraint",
|
|
431
|
+
"priority": "must_have|should_have|could_have|wont_have",
|
|
432
|
+
"title": "string",
|
|
433
|
+
"description": "string",
|
|
434
|
+
"explicit": boolean,
|
|
435
|
+
"acceptanceCriteria": ["string"]
|
|
436
|
+
}
|
|
437
|
+
],
|
|
438
|
+
"assumptions": [
|
|
439
|
+
{
|
|
440
|
+
"category": "string",
|
|
441
|
+
"statement": "string",
|
|
442
|
+
"confidence": "high|medium|low",
|
|
443
|
+
"impactIfWrong": "string"
|
|
444
|
+
}
|
|
445
|
+
],
|
|
446
|
+
"questions": [
|
|
447
|
+
{
|
|
448
|
+
"category": "clarification|expansion|decision|confirmation|scope|priority",
|
|
449
|
+
"question": "string",
|
|
450
|
+
"context": "string",
|
|
451
|
+
"importance": "critical|important|helpful",
|
|
452
|
+
"options": ["string"] | null
|
|
453
|
+
}
|
|
454
|
+
],
|
|
455
|
+
"techRecommendations": [
|
|
456
|
+
{
|
|
457
|
+
"area": "language|framework|database|infrastructure|testing|ci_cd",
|
|
458
|
+
"decision": "string",
|
|
459
|
+
"alternatives": ["string"],
|
|
460
|
+
"rationale": "string"
|
|
461
|
+
}
|
|
462
|
+
]
|
|
463
|
+
}`;
|
|
464
|
+
var GENERATE_QUESTIONS_PROMPT = `Based on the current requirements and conversation, generate follow-up questions to clarify the project scope.
|
|
465
|
+
|
|
466
|
+
Current Requirements:
|
|
467
|
+
{{requirements}}
|
|
468
|
+
|
|
469
|
+
Previous Clarifications:
|
|
470
|
+
{{clarifications}}
|
|
471
|
+
|
|
472
|
+
Open Assumptions:
|
|
473
|
+
{{assumptions}}
|
|
474
|
+
|
|
475
|
+
Generate 1-3 focused questions that will:
|
|
476
|
+
1. Clarify the most important ambiguities
|
|
477
|
+
2. Confirm critical assumptions
|
|
478
|
+
3. Expand on underspecified areas
|
|
479
|
+
|
|
480
|
+
Prioritize questions by:
|
|
481
|
+
- Critical: Blocks further progress
|
|
482
|
+
- Important: Significantly affects design
|
|
483
|
+
- Helpful: Nice to know for completeness
|
|
484
|
+
|
|
485
|
+
Respond in JSON format:
|
|
486
|
+
{
|
|
487
|
+
"questions": [
|
|
488
|
+
{
|
|
489
|
+
"category": "clarification|expansion|decision|confirmation|scope|priority",
|
|
490
|
+
"question": "string",
|
|
491
|
+
"context": "Why this matters",
|
|
492
|
+
"importance": "critical|important|helpful",
|
|
493
|
+
"defaultAnswer": "string | null",
|
|
494
|
+
"options": ["string"] | null
|
|
495
|
+
}
|
|
496
|
+
],
|
|
497
|
+
"reasoning": "string"
|
|
498
|
+
}`;
|
|
499
|
+
var PROCESS_ANSWER_PROMPT = `The user answered a clarification question. Update the requirements based on their response.
|
|
500
|
+
|
|
501
|
+
Question Asked:
|
|
502
|
+
{{question}}
|
|
503
|
+
|
|
504
|
+
User's Answer:
|
|
505
|
+
{{answer}}
|
|
506
|
+
|
|
507
|
+
Current Requirements:
|
|
508
|
+
{{requirements}}
|
|
509
|
+
|
|
510
|
+
Determine:
|
|
511
|
+
1. Which requirements are affected by this answer
|
|
512
|
+
2. Whether new requirements should be added
|
|
513
|
+
3. Whether any requirements should be modified
|
|
514
|
+
4. Whether any assumptions can now be confirmed
|
|
515
|
+
|
|
516
|
+
Respond in JSON format:
|
|
517
|
+
{
|
|
518
|
+
"affectedRequirements": ["requirement_id"],
|
|
519
|
+
"modifications": [
|
|
520
|
+
{
|
|
521
|
+
"requirementId": "string",
|
|
522
|
+
"change": "string",
|
|
523
|
+
"newValue": "any"
|
|
524
|
+
}
|
|
525
|
+
],
|
|
526
|
+
"newRequirements": [
|
|
527
|
+
{
|
|
528
|
+
"category": "functional|non_functional|technical|constraint",
|
|
529
|
+
"priority": "must_have|should_have|could_have|wont_have",
|
|
530
|
+
"title": "string",
|
|
531
|
+
"description": "string",
|
|
532
|
+
"acceptanceCriteria": ["string"]
|
|
533
|
+
}
|
|
534
|
+
],
|
|
535
|
+
"confirmedAssumptions": ["assumption_id"],
|
|
536
|
+
"reasoning": "string"
|
|
537
|
+
}`;
|
|
538
|
+
var EXTRACT_REQUIREMENTS_PROMPT = `Extract requirements from the following conversation message.
|
|
539
|
+
|
|
540
|
+
Message:
|
|
541
|
+
{{message}}
|
|
542
|
+
|
|
543
|
+
Existing Requirements:
|
|
544
|
+
{{existingRequirements}}
|
|
545
|
+
|
|
546
|
+
Identify:
|
|
547
|
+
1. New explicit requirements stated by the user
|
|
548
|
+
2. Implicit requirements that can be inferred
|
|
549
|
+
3. Requirements that modify or contradict existing ones
|
|
550
|
+
4. Technology preferences or constraints mentioned
|
|
551
|
+
|
|
552
|
+
Respond in JSON format:
|
|
553
|
+
{
|
|
554
|
+
"newRequirements": [
|
|
555
|
+
{
|
|
556
|
+
"category": "functional|non_functional|technical|constraint",
|
|
557
|
+
"priority": "must_have|should_have|could_have|wont_have",
|
|
558
|
+
"title": "string",
|
|
559
|
+
"description": "string",
|
|
560
|
+
"explicit": boolean,
|
|
561
|
+
"acceptanceCriteria": ["string"]
|
|
562
|
+
}
|
|
563
|
+
],
|
|
564
|
+
"modifiedRequirements": [
|
|
565
|
+
{
|
|
566
|
+
"id": "string",
|
|
567
|
+
"modification": "string"
|
|
568
|
+
}
|
|
569
|
+
],
|
|
570
|
+
"techPreferences": [
|
|
571
|
+
{
|
|
572
|
+
"area": "language|framework|database|infrastructure",
|
|
573
|
+
"preference": "string",
|
|
574
|
+
"reason": "string"
|
|
575
|
+
}
|
|
576
|
+
]
|
|
577
|
+
}`;
|
|
578
|
+
var ARCHITECTURE_PROMPT = `Recommend an architecture for this project.
|
|
579
|
+
|
|
580
|
+
Project Type: {{projectType}}
|
|
581
|
+
Complexity: {{complexity}}
|
|
582
|
+
Requirements:
|
|
583
|
+
{{requirements}}
|
|
584
|
+
|
|
585
|
+
Technology Stack:
|
|
586
|
+
{{techStack}}
|
|
587
|
+
|
|
588
|
+
Consider:
|
|
589
|
+
1. Scalability needs
|
|
590
|
+
2. Maintainability
|
|
591
|
+
3. Team size (assumed: 1 developer + AI)
|
|
592
|
+
4. Deployment target
|
|
593
|
+
5. Future extensibility
|
|
594
|
+
|
|
595
|
+
Recommend:
|
|
596
|
+
1. Overall architecture pattern (layered, hexagonal, microservices, etc.)
|
|
597
|
+
2. Key components and their responsibilities
|
|
598
|
+
3. Data flow between components
|
|
599
|
+
4. External integrations approach
|
|
600
|
+
5. Testing strategy alignment
|
|
601
|
+
|
|
602
|
+
Respond in JSON format:
|
|
603
|
+
{
|
|
604
|
+
"pattern": "string",
|
|
605
|
+
"rationale": "string",
|
|
606
|
+
"components": [
|
|
607
|
+
{
|
|
608
|
+
"name": "string",
|
|
609
|
+
"responsibility": "string",
|
|
610
|
+
"technology": "string"
|
|
611
|
+
}
|
|
612
|
+
],
|
|
613
|
+
"dataFlow": "string description",
|
|
614
|
+
"integrationApproach": "string",
|
|
615
|
+
"testingStrategy": "string",
|
|
616
|
+
"diagramMermaid": "string (mermaid diagram code)"
|
|
617
|
+
}`;
|
|
618
|
+
function fillPrompt(template, variables) {
|
|
619
|
+
let result = template;
|
|
620
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
621
|
+
const placeholder = `{{${key}}}`;
|
|
622
|
+
const stringValue = typeof value === "string" ? value : typeof value === "number" ? String(value) : JSON.stringify(value, null, 2);
|
|
623
|
+
result = result.replaceAll(placeholder, stringValue);
|
|
624
|
+
}
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/utils/errors.ts
|
|
629
|
+
var CocoError = class _CocoError extends Error {
|
|
630
|
+
code;
|
|
631
|
+
context;
|
|
632
|
+
recoverable;
|
|
633
|
+
suggestion;
|
|
634
|
+
constructor(message, options) {
|
|
635
|
+
super(message, { cause: options.cause });
|
|
636
|
+
this.name = "CocoError";
|
|
637
|
+
this.code = options.code;
|
|
638
|
+
this.context = options.context ?? {};
|
|
639
|
+
this.recoverable = options.recoverable ?? false;
|
|
640
|
+
this.suggestion = options.suggestion;
|
|
641
|
+
Error.captureStackTrace(this, _CocoError);
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Convert to JSON for logging
|
|
645
|
+
*/
|
|
646
|
+
toJSON() {
|
|
647
|
+
return {
|
|
648
|
+
name: this.name,
|
|
649
|
+
code: this.code,
|
|
650
|
+
message: this.message,
|
|
651
|
+
context: this.context,
|
|
652
|
+
recoverable: this.recoverable,
|
|
653
|
+
suggestion: this.suggestion,
|
|
654
|
+
stack: this.stack,
|
|
655
|
+
cause: this.cause instanceof Error ? this.cause.message : this.cause
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
var FileSystemError = class extends CocoError {
|
|
660
|
+
constructor(message, options) {
|
|
661
|
+
super(message, {
|
|
662
|
+
code: "FILESYSTEM_ERROR",
|
|
663
|
+
context: { path: options.path, operation: options.operation },
|
|
664
|
+
recoverable: false,
|
|
665
|
+
suggestion: `Check that the path exists and you have permissions: ${options.path}`,
|
|
666
|
+
cause: options.cause
|
|
667
|
+
});
|
|
668
|
+
this.name = "FileSystemError";
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
var ProviderError = class extends CocoError {
|
|
672
|
+
provider;
|
|
673
|
+
statusCode;
|
|
674
|
+
constructor(message, options) {
|
|
675
|
+
super(message, {
|
|
676
|
+
code: "PROVIDER_ERROR",
|
|
677
|
+
context: { provider: options.provider, statusCode: options.statusCode },
|
|
678
|
+
recoverable: options.retryable ?? false,
|
|
679
|
+
suggestion: options.retryable ? "The request can be retried" : "Check your API key and provider configuration",
|
|
680
|
+
cause: options.cause
|
|
681
|
+
});
|
|
682
|
+
this.name = "ProviderError";
|
|
683
|
+
this.provider = options.provider;
|
|
684
|
+
this.statusCode = options.statusCode;
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
var PhaseError = class extends CocoError {
|
|
688
|
+
phase;
|
|
689
|
+
constructor(message, options) {
|
|
690
|
+
super(message, {
|
|
691
|
+
code: "PHASE_ERROR",
|
|
692
|
+
context: { phase: options.phase },
|
|
693
|
+
recoverable: options.recoverable ?? true,
|
|
694
|
+
suggestion: `Phase '${options.phase}' failed. Try 'coco resume' to continue.`,
|
|
695
|
+
cause: options.cause
|
|
696
|
+
});
|
|
697
|
+
this.name = "PhaseError";
|
|
698
|
+
this.phase = options.phase;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
function normalizeComplexity(value) {
|
|
702
|
+
const normalized = value?.toLowerCase();
|
|
703
|
+
if (normalized === "simple") return "simple";
|
|
704
|
+
if (normalized === "moderate") return "moderate";
|
|
705
|
+
if (normalized === "complex") return "complex";
|
|
706
|
+
if (normalized === "enterprise") return "enterprise";
|
|
707
|
+
return "moderate";
|
|
708
|
+
}
|
|
709
|
+
function normalizeCategory(value) {
|
|
710
|
+
const normalized = value?.toLowerCase();
|
|
711
|
+
if (normalized === "functional") return "functional";
|
|
712
|
+
if (normalized === "non_functional" || normalized === "nonfunctional")
|
|
713
|
+
return "non_functional";
|
|
714
|
+
if (normalized === "technical") return "technical";
|
|
715
|
+
if (normalized === "user_experience" || normalized === "ux")
|
|
716
|
+
return "user_experience";
|
|
717
|
+
if (normalized === "integration") return "integration";
|
|
718
|
+
if (normalized === "deployment") return "deployment";
|
|
719
|
+
if (normalized === "constraint") return "constraint";
|
|
720
|
+
return "functional";
|
|
721
|
+
}
|
|
722
|
+
function normalizePriority(value) {
|
|
723
|
+
const normalized = value?.toLowerCase();
|
|
724
|
+
if (normalized === "must_have" || normalized === "must") return "must_have";
|
|
725
|
+
if (normalized === "should_have" || normalized === "should")
|
|
726
|
+
return "should_have";
|
|
727
|
+
if (normalized === "could_have" || normalized === "could")
|
|
728
|
+
return "could_have";
|
|
729
|
+
if (normalized === "wont_have" || normalized === "wont") return "wont_have";
|
|
730
|
+
return "should_have";
|
|
731
|
+
}
|
|
732
|
+
function normalizeQuestionCategory(value) {
|
|
733
|
+
const normalized = value?.toLowerCase();
|
|
734
|
+
if (normalized === "clarification") return "clarification";
|
|
735
|
+
if (normalized === "expansion") return "expansion";
|
|
736
|
+
if (normalized === "decision") return "decision";
|
|
737
|
+
if (normalized === "confirmation") return "confirmation";
|
|
738
|
+
if (normalized === "scope") return "scope";
|
|
739
|
+
if (normalized === "priority") return "priority";
|
|
740
|
+
return "clarification";
|
|
741
|
+
}
|
|
742
|
+
function normalizeImportance(value) {
|
|
743
|
+
const normalized = value?.toLowerCase();
|
|
744
|
+
if (normalized === "critical") return "critical";
|
|
745
|
+
if (normalized === "important") return "important";
|
|
746
|
+
return "helpful";
|
|
747
|
+
}
|
|
748
|
+
function normalizeConfidence(value) {
|
|
749
|
+
const normalized = value?.toLowerCase();
|
|
750
|
+
if (normalized === "high") return "high";
|
|
751
|
+
if (normalized === "low") return "low";
|
|
752
|
+
return "medium";
|
|
753
|
+
}
|
|
754
|
+
function parseRequirements(data) {
|
|
755
|
+
return data.map((r) => ({
|
|
756
|
+
id: randomUUID(),
|
|
757
|
+
category: normalizeCategory(r.category),
|
|
758
|
+
priority: normalizePriority(r.priority),
|
|
759
|
+
title: r.title || "Untitled",
|
|
760
|
+
description: r.description || "",
|
|
761
|
+
sourceMessageId: "",
|
|
762
|
+
explicit: r.explicit ?? true,
|
|
763
|
+
acceptanceCriteria: r.acceptanceCriteria,
|
|
764
|
+
status: "draft"
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
767
|
+
function parseQuestions(data) {
|
|
768
|
+
return data.map((q) => ({
|
|
769
|
+
id: randomUUID(),
|
|
770
|
+
category: normalizeQuestionCategory(q.category),
|
|
771
|
+
question: q.question || "",
|
|
772
|
+
context: q.context || "",
|
|
773
|
+
importance: normalizeImportance(q.importance),
|
|
774
|
+
defaultAnswer: q.defaultAnswer || void 0,
|
|
775
|
+
options: q.options || void 0,
|
|
776
|
+
asked: false
|
|
777
|
+
}));
|
|
778
|
+
}
|
|
779
|
+
function parseAssumptions(data) {
|
|
780
|
+
return data.map((a) => ({
|
|
781
|
+
id: randomUUID(),
|
|
782
|
+
category: a.category || "general",
|
|
783
|
+
statement: a.statement || "",
|
|
784
|
+
confidence: normalizeConfidence(a.confidence),
|
|
785
|
+
confirmed: false,
|
|
786
|
+
impactIfWrong: a.impactIfWrong || ""
|
|
787
|
+
}));
|
|
788
|
+
}
|
|
789
|
+
function parseTechHints(data) {
|
|
790
|
+
return data.map((t) => ({
|
|
791
|
+
area: t.area,
|
|
792
|
+
decision: t.decision,
|
|
793
|
+
alternatives: t.alternatives || [],
|
|
794
|
+
rationale: t.rationale,
|
|
795
|
+
explicit: false
|
|
796
|
+
}));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/phases/converge/discovery.ts
|
|
800
|
+
var DEFAULT_DISCOVERY_CONFIG = {
|
|
801
|
+
maxQuestionsPerRound: 3,
|
|
802
|
+
minRequirements: 3,
|
|
803
|
+
autoConfirmLowConfidence: false,
|
|
804
|
+
defaultLanguage: "typescript",
|
|
805
|
+
includeDiagrams: true
|
|
806
|
+
};
|
|
807
|
+
var DiscoveryEngine = class {
|
|
808
|
+
session = null;
|
|
809
|
+
config;
|
|
810
|
+
llm;
|
|
811
|
+
constructor(llm, config = {}) {
|
|
812
|
+
this.llm = llm;
|
|
813
|
+
this.config = { ...DEFAULT_DISCOVERY_CONFIG, ...config };
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Start a new discovery session
|
|
817
|
+
*/
|
|
818
|
+
async startSession(initialInput) {
|
|
819
|
+
const sessionId = randomUUID();
|
|
820
|
+
const now = /* @__PURE__ */ new Date();
|
|
821
|
+
this.session = {
|
|
822
|
+
id: sessionId,
|
|
823
|
+
startedAt: now,
|
|
824
|
+
updatedAt: now,
|
|
825
|
+
status: "gathering",
|
|
826
|
+
initialInput,
|
|
827
|
+
conversation: [],
|
|
828
|
+
requirements: [],
|
|
829
|
+
openQuestions: [],
|
|
830
|
+
clarifications: [],
|
|
831
|
+
assumptions: [],
|
|
832
|
+
techDecisions: []
|
|
833
|
+
};
|
|
834
|
+
this.addMessage("user", initialInput);
|
|
835
|
+
const analysis = await this.analyzeInput(initialInput);
|
|
836
|
+
this.applyAnalysis(analysis);
|
|
837
|
+
this.updateSessionStatus();
|
|
838
|
+
return this.session;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Resume an existing session
|
|
842
|
+
*/
|
|
843
|
+
resumeSession(session) {
|
|
844
|
+
this.session = session;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Get the current session
|
|
848
|
+
*/
|
|
849
|
+
getSession() {
|
|
850
|
+
return this.session;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Analyze user input for requirements
|
|
854
|
+
*/
|
|
855
|
+
async analyzeInput(input) {
|
|
856
|
+
const prompt = fillPrompt(INITIAL_ANALYSIS_PROMPT, {
|
|
857
|
+
userInput: input
|
|
858
|
+
});
|
|
859
|
+
const response = await this.llm.chat([
|
|
860
|
+
{ role: "system", content: DISCOVERY_SYSTEM_PROMPT },
|
|
861
|
+
{ role: "user", content: prompt }
|
|
862
|
+
]);
|
|
863
|
+
try {
|
|
864
|
+
const content = response.content;
|
|
865
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
866
|
+
if (!jsonMatch) {
|
|
867
|
+
throw new Error("No JSON found in response");
|
|
868
|
+
}
|
|
869
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
870
|
+
return {
|
|
871
|
+
projectType: parsed.projectType || "unknown",
|
|
872
|
+
complexity: normalizeComplexity(parsed.complexity),
|
|
873
|
+
completeness: parsed.completeness || 0,
|
|
874
|
+
requirements: parseRequirements(parsed.requirements || []),
|
|
875
|
+
suggestedQuestions: parseQuestions(parsed.questions || []),
|
|
876
|
+
assumptions: parseAssumptions(parsed.assumptions || []),
|
|
877
|
+
techHints: parseTechHints(parsed.techRecommendations || [])
|
|
878
|
+
};
|
|
879
|
+
} catch {
|
|
880
|
+
throw new PhaseError(
|
|
881
|
+
"Failed to parse LLM response for input analysis",
|
|
882
|
+
{ phase: "converge" }
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Process a user's answer to a question
|
|
888
|
+
*/
|
|
889
|
+
async processAnswer(questionId, answer) {
|
|
890
|
+
if (!this.session) {
|
|
891
|
+
throw new PhaseError("No active discovery session", { phase: "converge" });
|
|
892
|
+
}
|
|
893
|
+
const question = this.session.openQuestions.find((q) => q.id === questionId);
|
|
894
|
+
if (!question) {
|
|
895
|
+
throw new PhaseError(`Question not found: ${questionId}`, { phase: "converge" });
|
|
896
|
+
}
|
|
897
|
+
question.asked = true;
|
|
898
|
+
question.answer = answer;
|
|
899
|
+
this.addMessage("user", answer);
|
|
900
|
+
const prompt = fillPrompt(PROCESS_ANSWER_PROMPT, {
|
|
901
|
+
question: JSON.stringify(question),
|
|
902
|
+
answer,
|
|
903
|
+
requirements: JSON.stringify(this.session.requirements)
|
|
904
|
+
});
|
|
905
|
+
const response = await this.llm.chat([
|
|
906
|
+
{ role: "system", content: DISCOVERY_SYSTEM_PROMPT },
|
|
907
|
+
{ role: "user", content: prompt }
|
|
908
|
+
]);
|
|
909
|
+
try {
|
|
910
|
+
const content = response.content;
|
|
911
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
912
|
+
if (!jsonMatch) {
|
|
913
|
+
throw new Error("No JSON found in response");
|
|
914
|
+
}
|
|
915
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
916
|
+
if (parsed.modifications) {
|
|
917
|
+
for (const mod of parsed.modifications) {
|
|
918
|
+
const req = this.session.requirements.find(
|
|
919
|
+
(r) => r.id === mod.requirementId
|
|
920
|
+
);
|
|
921
|
+
if (req && mod.change === "description" && typeof mod.newValue === "string") {
|
|
922
|
+
req.description = mod.newValue;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (parsed.newRequirements) {
|
|
927
|
+
const newReqs = parseRequirements(parsed.newRequirements);
|
|
928
|
+
for (const req of newReqs) {
|
|
929
|
+
req.sourceMessageId = this.session.conversation[this.session.conversation.length - 1]?.id || "";
|
|
930
|
+
this.session.requirements.push(req);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (parsed.confirmedAssumptions) {
|
|
934
|
+
for (const assumptionId of parsed.confirmedAssumptions) {
|
|
935
|
+
const assumption = this.session.assumptions.find(
|
|
936
|
+
(a) => a.id === assumptionId
|
|
937
|
+
);
|
|
938
|
+
if (assumption) {
|
|
939
|
+
assumption.confirmed = true;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const clarification = {
|
|
944
|
+
questionId,
|
|
945
|
+
answer,
|
|
946
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
947
|
+
affectedRequirements: parsed.affectedRequirements || [],
|
|
948
|
+
newRequirements: parsed.newRequirements?.map((r) => r.title || "") || []
|
|
949
|
+
};
|
|
950
|
+
this.session.clarifications.push(clarification);
|
|
951
|
+
this.session.openQuestions = this.session.openQuestions.filter(
|
|
952
|
+
(q) => q.id !== questionId
|
|
953
|
+
);
|
|
954
|
+
this.session.updatedAt = /* @__PURE__ */ new Date();
|
|
955
|
+
this.updateSessionStatus();
|
|
956
|
+
} catch {
|
|
957
|
+
throw new PhaseError("Failed to process answer", { phase: "converge" });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Generate follow-up questions based on current state
|
|
962
|
+
*/
|
|
963
|
+
async generateQuestions() {
|
|
964
|
+
if (!this.session) {
|
|
965
|
+
throw new PhaseError("No active discovery session", { phase: "converge" });
|
|
966
|
+
}
|
|
967
|
+
const prompt = fillPrompt(GENERATE_QUESTIONS_PROMPT, {
|
|
968
|
+
requirements: JSON.stringify(this.session.requirements),
|
|
969
|
+
clarifications: JSON.stringify(this.session.clarifications),
|
|
970
|
+
assumptions: JSON.stringify(
|
|
971
|
+
this.session.assumptions.filter((a) => !a.confirmed)
|
|
972
|
+
)
|
|
973
|
+
});
|
|
974
|
+
const response = await this.llm.chat([
|
|
975
|
+
{ role: "system", content: DISCOVERY_SYSTEM_PROMPT },
|
|
976
|
+
{ role: "user", content: prompt }
|
|
977
|
+
]);
|
|
978
|
+
try {
|
|
979
|
+
const content = response.content;
|
|
980
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
981
|
+
if (!jsonMatch) {
|
|
982
|
+
throw new Error("No JSON found in response");
|
|
983
|
+
}
|
|
984
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
985
|
+
const questions = parseQuestions(parsed.questions || []);
|
|
986
|
+
const limited = questions.slice(0, this.config.maxQuestionsPerRound);
|
|
987
|
+
for (const q of limited) {
|
|
988
|
+
if (!this.session.openQuestions.some((oq) => oq.question === q.question)) {
|
|
989
|
+
this.session.openQuestions.push(q);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return limited;
|
|
993
|
+
} catch {
|
|
994
|
+
throw new PhaseError("Failed to generate questions", { phase: "converge" });
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Process a free-form message from the user
|
|
999
|
+
*/
|
|
1000
|
+
async processMessage(message) {
|
|
1001
|
+
if (!this.session) {
|
|
1002
|
+
throw new PhaseError("No active discovery session", { phase: "converge" });
|
|
1003
|
+
}
|
|
1004
|
+
this.addMessage("user", message);
|
|
1005
|
+
const prompt = fillPrompt(EXTRACT_REQUIREMENTS_PROMPT, {
|
|
1006
|
+
message,
|
|
1007
|
+
existingRequirements: JSON.stringify(this.session.requirements)
|
|
1008
|
+
});
|
|
1009
|
+
const response = await this.llm.chat([
|
|
1010
|
+
{ role: "system", content: DISCOVERY_SYSTEM_PROMPT },
|
|
1011
|
+
{ role: "user", content: prompt }
|
|
1012
|
+
]);
|
|
1013
|
+
try {
|
|
1014
|
+
const content = response.content;
|
|
1015
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
1016
|
+
if (!jsonMatch) {
|
|
1017
|
+
throw new Error("No JSON found in response");
|
|
1018
|
+
}
|
|
1019
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1020
|
+
const newReqs = parseRequirements(parsed.newRequirements || []);
|
|
1021
|
+
const lastMsgId = this.session.conversation[this.session.conversation.length - 1]?.id || "";
|
|
1022
|
+
for (const req of newReqs) {
|
|
1023
|
+
req.sourceMessageId = lastMsgId;
|
|
1024
|
+
this.session.requirements.push(req);
|
|
1025
|
+
}
|
|
1026
|
+
if (parsed.techPreferences) {
|
|
1027
|
+
for (const pref of parsed.techPreferences) {
|
|
1028
|
+
if (pref.area && pref.preference) {
|
|
1029
|
+
const existing = this.session.techDecisions.find(
|
|
1030
|
+
(t) => t.area === pref.area
|
|
1031
|
+
);
|
|
1032
|
+
if (!existing) {
|
|
1033
|
+
this.session.techDecisions.push({
|
|
1034
|
+
id: randomUUID(),
|
|
1035
|
+
area: pref.area,
|
|
1036
|
+
decision: pref.preference,
|
|
1037
|
+
alternatives: [],
|
|
1038
|
+
rationale: pref.reason || "",
|
|
1039
|
+
explicit: true
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
this.session.updatedAt = /* @__PURE__ */ new Date();
|
|
1046
|
+
this.updateSessionStatus();
|
|
1047
|
+
let questions = [];
|
|
1048
|
+
if (this.session.status === "clarifying") {
|
|
1049
|
+
questions = await this.generateQuestions();
|
|
1050
|
+
}
|
|
1051
|
+
return { newRequirements: newReqs, questions };
|
|
1052
|
+
} catch {
|
|
1053
|
+
throw new PhaseError("Failed to process message", { phase: "converge" });
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Check if discovery is complete
|
|
1058
|
+
*/
|
|
1059
|
+
isComplete() {
|
|
1060
|
+
if (!this.session) return false;
|
|
1061
|
+
return this.session.status === "complete" || this.session.status === "spec_generated";
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Get unanswered questions
|
|
1065
|
+
*/
|
|
1066
|
+
getOpenQuestions() {
|
|
1067
|
+
if (!this.session) return [];
|
|
1068
|
+
return this.session.openQuestions.filter((q) => !q.asked);
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Get critical questions that must be answered
|
|
1072
|
+
*/
|
|
1073
|
+
getCriticalQuestions() {
|
|
1074
|
+
return this.getOpenQuestions().filter((q) => q.importance === "critical");
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Mark discovery as complete
|
|
1078
|
+
*/
|
|
1079
|
+
markComplete() {
|
|
1080
|
+
if (!this.session) {
|
|
1081
|
+
throw new PhaseError("No active discovery session", { phase: "converge" });
|
|
1082
|
+
}
|
|
1083
|
+
this.session.status = "complete";
|
|
1084
|
+
this.session.updatedAt = /* @__PURE__ */ new Date();
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Force complete with current requirements
|
|
1088
|
+
*/
|
|
1089
|
+
forceComplete() {
|
|
1090
|
+
if (!this.session) {
|
|
1091
|
+
throw new PhaseError("No active discovery session", { phase: "converge" });
|
|
1092
|
+
}
|
|
1093
|
+
if (this.config.autoConfirmLowConfidence) {
|
|
1094
|
+
for (const assumption of this.session.assumptions) {
|
|
1095
|
+
if (!assumption.confirmed && assumption.confidence !== "high") {
|
|
1096
|
+
assumption.confirmed = true;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
this.session.status = "complete";
|
|
1101
|
+
this.session.updatedAt = /* @__PURE__ */ new Date();
|
|
1102
|
+
}
|
|
1103
|
+
// Private helper methods
|
|
1104
|
+
addMessage(role, content) {
|
|
1105
|
+
if (!this.session) return;
|
|
1106
|
+
const message = {
|
|
1107
|
+
id: randomUUID(),
|
|
1108
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1109
|
+
role,
|
|
1110
|
+
content
|
|
1111
|
+
};
|
|
1112
|
+
this.session.conversation.push(message);
|
|
1113
|
+
this.session.updatedAt = /* @__PURE__ */ new Date();
|
|
1114
|
+
}
|
|
1115
|
+
applyAnalysis(analysis) {
|
|
1116
|
+
if (!this.session) return;
|
|
1117
|
+
const lastMsgId = this.session.conversation[this.session.conversation.length - 1]?.id || "";
|
|
1118
|
+
for (const req of analysis.requirements) {
|
|
1119
|
+
req.sourceMessageId = lastMsgId;
|
|
1120
|
+
this.session.requirements.push(req);
|
|
1121
|
+
}
|
|
1122
|
+
for (const assumption of analysis.assumptions) {
|
|
1123
|
+
this.session.assumptions.push(assumption);
|
|
1124
|
+
}
|
|
1125
|
+
for (const question of analysis.suggestedQuestions) {
|
|
1126
|
+
this.session.openQuestions.push(question);
|
|
1127
|
+
}
|
|
1128
|
+
for (const hint of analysis.techHints) {
|
|
1129
|
+
if (hint.area && hint.decision) {
|
|
1130
|
+
this.session.techDecisions.push({
|
|
1131
|
+
id: randomUUID(),
|
|
1132
|
+
area: hint.area,
|
|
1133
|
+
decision: hint.decision,
|
|
1134
|
+
alternatives: hint.alternatives || [],
|
|
1135
|
+
rationale: hint.rationale || "",
|
|
1136
|
+
explicit: hint.explicit ?? false
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
updateSessionStatus() {
|
|
1142
|
+
if (!this.session) return;
|
|
1143
|
+
const hasMinRequirements = this.session.requirements.length >= this.config.minRequirements;
|
|
1144
|
+
const hasCriticalQuestions = this.getCriticalQuestions().length > 0;
|
|
1145
|
+
const hasUnconfirmedHighImpact = this.session.assumptions.some(
|
|
1146
|
+
(a) => !a.confirmed && a.confidence === "low"
|
|
1147
|
+
);
|
|
1148
|
+
if (hasCriticalQuestions || hasUnconfirmedHighImpact) {
|
|
1149
|
+
this.session.status = "clarifying";
|
|
1150
|
+
} else if (hasMinRequirements) {
|
|
1151
|
+
this.session.status = "refining";
|
|
1152
|
+
} else {
|
|
1153
|
+
this.session.status = "gathering";
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
function createDiscoveryEngine(llm, config) {
|
|
1158
|
+
return new DiscoveryEngine(llm, config);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// src/phases/converge/specification-types.ts
|
|
1162
|
+
var DEFAULT_SPEC_CONFIG = {
|
|
1163
|
+
includeDiagrams: true,
|
|
1164
|
+
maxLength: 5e4,
|
|
1165
|
+
includeRisks: true,
|
|
1166
|
+
format: "markdown"
|
|
1167
|
+
};
|
|
1168
|
+
function extractProjectName(input) {
|
|
1169
|
+
const namePatterns = [
|
|
1170
|
+
/(?:called|named|create|build)\s+["']?([a-zA-Z][a-zA-Z0-9-_]+)["']?/i,
|
|
1171
|
+
/^([a-zA-Z][a-zA-Z0-9-_]+)\s*[-:]/,
|
|
1172
|
+
/project\s+["']?([a-zA-Z][a-zA-Z0-9-_]+)["']?/i
|
|
1173
|
+
];
|
|
1174
|
+
for (const pattern of namePatterns) {
|
|
1175
|
+
const match = input.match(pattern);
|
|
1176
|
+
if (match?.[1]) {
|
|
1177
|
+
return match[1];
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return "my-project";
|
|
1181
|
+
}
|
|
1182
|
+
function inferTargetUsers(session) {
|
|
1183
|
+
const users = [];
|
|
1184
|
+
const userPatterns = [
|
|
1185
|
+
/(?:for|by)\s+(developers?|users?|administrators?|customers?)/gi,
|
|
1186
|
+
/(developers?|users?|administrators?|customers?)\s+(?:can|will|should)/gi
|
|
1187
|
+
];
|
|
1188
|
+
const text4 = session.requirements.map((r) => r.description).join(" ");
|
|
1189
|
+
for (const pattern of userPatterns) {
|
|
1190
|
+
let match;
|
|
1191
|
+
while ((match = pattern.exec(text4)) !== null) {
|
|
1192
|
+
const user = match[1]?.toLowerCase();
|
|
1193
|
+
if (user && !users.includes(user)) {
|
|
1194
|
+
users.push(user);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (users.length === 0) {
|
|
1199
|
+
users.push("developers");
|
|
1200
|
+
}
|
|
1201
|
+
return users;
|
|
1202
|
+
}
|
|
1203
|
+
function inferProjectType(session) {
|
|
1204
|
+
const text4 = session.initialInput.toLowerCase();
|
|
1205
|
+
if (text4.includes("cli") || text4.includes("command line")) return "cli";
|
|
1206
|
+
if (text4.includes("api") || text4.includes("rest") || text4.includes("graphql"))
|
|
1207
|
+
return "api";
|
|
1208
|
+
if (text4.includes("web app") || text4.includes("frontend")) return "web_app";
|
|
1209
|
+
if (text4.includes("library") || text4.includes("package")) return "library";
|
|
1210
|
+
if (text4.includes("service") || text4.includes("daemon")) return "service";
|
|
1211
|
+
if (text4.includes("full stack") || text4.includes("fullstack"))
|
|
1212
|
+
return "full_stack";
|
|
1213
|
+
return "unknown";
|
|
1214
|
+
}
|
|
1215
|
+
function assessComplexity(session) {
|
|
1216
|
+
const reqCount = session.requirements.length;
|
|
1217
|
+
const hasIntegrations = session.requirements.some(
|
|
1218
|
+
(r) => r.category === "integration"
|
|
1219
|
+
);
|
|
1220
|
+
const hasSecurity = session.requirements.some(
|
|
1221
|
+
(r) => r.description.toLowerCase().includes("security")
|
|
1222
|
+
);
|
|
1223
|
+
if (reqCount > 20 || hasIntegrations && hasSecurity) return "enterprise";
|
|
1224
|
+
if (reqCount > 10 || hasIntegrations) return "complex";
|
|
1225
|
+
if (reqCount > 5) return "moderate";
|
|
1226
|
+
return "simple";
|
|
1227
|
+
}
|
|
1228
|
+
function extractIntegrations(session) {
|
|
1229
|
+
return session.requirements.filter((r) => r.category === "integration").map((r) => r.title);
|
|
1230
|
+
}
|
|
1231
|
+
function extractDeployment(session) {
|
|
1232
|
+
const deployReq = session.requirements.find(
|
|
1233
|
+
(r) => r.category === "deployment"
|
|
1234
|
+
);
|
|
1235
|
+
const deployTech = session.techDecisions.find(
|
|
1236
|
+
(t) => t.area === "infrastructure"
|
|
1237
|
+
);
|
|
1238
|
+
if (deployReq) return deployReq.description;
|
|
1239
|
+
if (deployTech) return deployTech.decision;
|
|
1240
|
+
return "Deployment strategy to be determined";
|
|
1241
|
+
}
|
|
1242
|
+
function extractOutOfScope(session) {
|
|
1243
|
+
return session.requirements.filter((r) => r.priority === "wont_have").map((r) => r.title);
|
|
1244
|
+
}
|
|
1245
|
+
function generateOverview(session) {
|
|
1246
|
+
const name = extractProjectName(session.initialInput);
|
|
1247
|
+
const description = session.initialInput.substring(0, 500);
|
|
1248
|
+
const goals = session.requirements.filter((r) => r.priority === "must_have").slice(0, 5).map((r) => r.title);
|
|
1249
|
+
const targetUsers = inferTargetUsers(session);
|
|
1250
|
+
const successCriteria = session.requirements.filter((r) => r.acceptanceCriteria && r.acceptanceCriteria.length > 0).flatMap((r) => r.acceptanceCriteria || []).slice(0, 10);
|
|
1251
|
+
return {
|
|
1252
|
+
name,
|
|
1253
|
+
description,
|
|
1254
|
+
goals,
|
|
1255
|
+
targetUsers,
|
|
1256
|
+
successCriteria
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
function generateRisksFromSession(session) {
|
|
1260
|
+
const risks = [];
|
|
1261
|
+
for (const assumption of session.assumptions.filter((a) => !a.confirmed)) {
|
|
1262
|
+
if (assumption.confidence === "low") {
|
|
1263
|
+
risks.push({
|
|
1264
|
+
id: randomUUID(),
|
|
1265
|
+
description: `Assumption may be incorrect: ${assumption.statement}`,
|
|
1266
|
+
probability: "medium",
|
|
1267
|
+
impact: assumption.impactIfWrong ? "high" : "medium",
|
|
1268
|
+
mitigation: "Validate assumption early in development"
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
const hasDatabase = session.techDecisions.some((t) => t.area === "database");
|
|
1273
|
+
const hasIntegrations = session.requirements.some(
|
|
1274
|
+
(r) => r.category === "integration"
|
|
1275
|
+
);
|
|
1276
|
+
if (hasDatabase) {
|
|
1277
|
+
risks.push({
|
|
1278
|
+
id: randomUUID(),
|
|
1279
|
+
description: "Data migration complexity",
|
|
1280
|
+
probability: "medium",
|
|
1281
|
+
impact: "medium",
|
|
1282
|
+
mitigation: "Plan data model carefully, use migrations"
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
if (hasIntegrations) {
|
|
1286
|
+
risks.push({
|
|
1287
|
+
id: randomUUID(),
|
|
1288
|
+
description: "Third-party API changes or unavailability",
|
|
1289
|
+
probability: "medium",
|
|
1290
|
+
impact: "high",
|
|
1291
|
+
mitigation: "Abstract integrations, implement circuit breakers"
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
return risks;
|
|
1295
|
+
}
|
|
1296
|
+
function formatPriority(priority) {
|
|
1297
|
+
switch (priority) {
|
|
1298
|
+
case "must_have":
|
|
1299
|
+
return "\u{1F534} Must";
|
|
1300
|
+
case "should_have":
|
|
1301
|
+
return "\u{1F7E0} Should";
|
|
1302
|
+
case "could_have":
|
|
1303
|
+
return "\u{1F7E2} Could";
|
|
1304
|
+
case "wont_have":
|
|
1305
|
+
return "\u26AA Won't";
|
|
1306
|
+
default:
|
|
1307
|
+
return priority;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/phases/converge/specification-markdown.ts
|
|
1312
|
+
function generateSimpleMarkdown(spec) {
|
|
1313
|
+
const sections = [];
|
|
1314
|
+
sections.push(`# ${spec.name}`);
|
|
1315
|
+
sections.push("");
|
|
1316
|
+
if (spec.description) {
|
|
1317
|
+
sections.push(spec.description);
|
|
1318
|
+
sections.push("");
|
|
1319
|
+
}
|
|
1320
|
+
sections.push("## Requirements");
|
|
1321
|
+
sections.push("");
|
|
1322
|
+
sections.push("### Functional");
|
|
1323
|
+
sections.push("");
|
|
1324
|
+
const functional = spec.requirements?.functional || [];
|
|
1325
|
+
if (functional.length > 0) {
|
|
1326
|
+
for (const req of functional) {
|
|
1327
|
+
sections.push(`- ${typeof req === "string" ? req : req}`);
|
|
1328
|
+
}
|
|
1329
|
+
} else {
|
|
1330
|
+
sections.push("*No functional requirements*");
|
|
1331
|
+
}
|
|
1332
|
+
sections.push("");
|
|
1333
|
+
sections.push("### Non-Functional");
|
|
1334
|
+
sections.push("");
|
|
1335
|
+
const nonFunctional = spec.requirements?.nonFunctional || [];
|
|
1336
|
+
if (nonFunctional.length > 0) {
|
|
1337
|
+
for (const req of nonFunctional) {
|
|
1338
|
+
sections.push(`- ${typeof req === "string" ? req : req}`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
sections.push("");
|
|
1342
|
+
sections.push("## Assumptions");
|
|
1343
|
+
sections.push("");
|
|
1344
|
+
if (spec.assumptions?.length) {
|
|
1345
|
+
for (const a of spec.assumptions) {
|
|
1346
|
+
sections.push(`- ${typeof a === "string" ? a : a}`);
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
sections.push("*No assumptions*");
|
|
1350
|
+
}
|
|
1351
|
+
sections.push("");
|
|
1352
|
+
sections.push("## Constraints");
|
|
1353
|
+
sections.push("");
|
|
1354
|
+
if (spec.constraints?.length) {
|
|
1355
|
+
for (const c of spec.constraints) {
|
|
1356
|
+
sections.push(`- ${typeof c === "string" ? c : c}`);
|
|
1357
|
+
}
|
|
1358
|
+
} else {
|
|
1359
|
+
sections.push("*No constraints*");
|
|
1360
|
+
}
|
|
1361
|
+
sections.push("");
|
|
1362
|
+
return sections.join("\n");
|
|
1363
|
+
}
|
|
1364
|
+
function addRequirementsTable(sections, requirements) {
|
|
1365
|
+
if (requirements.length === 0) {
|
|
1366
|
+
sections.push("*No requirements in this category*");
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
sections.push("| ID | Title | Priority | Description |");
|
|
1370
|
+
sections.push("|----|-------|----------|-------------|");
|
|
1371
|
+
for (const req of requirements) {
|
|
1372
|
+
const priority = formatPriority(req.priority);
|
|
1373
|
+
const desc = req.description.substring(0, 100).replace(/\|/g, "\\|");
|
|
1374
|
+
sections.push(
|
|
1375
|
+
`| ${req.id.substring(0, 8)} | ${req.title} | ${priority} | ${desc} |`
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
sections.push("");
|
|
1379
|
+
sections.push("### Details");
|
|
1380
|
+
sections.push("");
|
|
1381
|
+
for (const req of requirements) {
|
|
1382
|
+
sections.push(`#### ${req.title}`);
|
|
1383
|
+
sections.push("");
|
|
1384
|
+
sections.push(req.description);
|
|
1385
|
+
sections.push("");
|
|
1386
|
+
if (req.acceptanceCriteria && req.acceptanceCriteria.length > 0) {
|
|
1387
|
+
sections.push("**Acceptance Criteria:**");
|
|
1388
|
+
for (const ac of req.acceptanceCriteria) {
|
|
1389
|
+
sections.push(`- [ ] ${ac}`);
|
|
1390
|
+
}
|
|
1391
|
+
sections.push("");
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
function generateFullMarkdown(spec) {
|
|
1396
|
+
const sections = [];
|
|
1397
|
+
sections.push(`# ${spec.overview.name} - Project Specification`);
|
|
1398
|
+
sections.push("");
|
|
1399
|
+
sections.push(
|
|
1400
|
+
`> Generated: ${spec.generatedAt.toISOString()} | Version: ${spec.version}`
|
|
1401
|
+
);
|
|
1402
|
+
sections.push("");
|
|
1403
|
+
sections.push("## Table of Contents");
|
|
1404
|
+
sections.push("");
|
|
1405
|
+
sections.push("1. [Executive Summary](#executive-summary)");
|
|
1406
|
+
sections.push("2. [Goals & Success Criteria](#goals--success-criteria)");
|
|
1407
|
+
sections.push("3. [Functional Requirements](#functional-requirements)");
|
|
1408
|
+
sections.push(
|
|
1409
|
+
"4. [Non-Functional Requirements](#non-functional-requirements)"
|
|
1410
|
+
);
|
|
1411
|
+
sections.push("5. [Technical Constraints](#technical-constraints)");
|
|
1412
|
+
sections.push("6. [Technology Stack](#technology-stack)");
|
|
1413
|
+
sections.push("7. [Architecture](#architecture)");
|
|
1414
|
+
sections.push("8. [Assumptions & Risks](#assumptions--risks)");
|
|
1415
|
+
sections.push("9. [Out of Scope](#out-of-scope)");
|
|
1416
|
+
if (spec.openQuestions.length > 0) {
|
|
1417
|
+
sections.push("10. [Open Questions](#open-questions)");
|
|
1418
|
+
}
|
|
1419
|
+
sections.push("");
|
|
1420
|
+
sections.push("## Executive Summary");
|
|
1421
|
+
sections.push("");
|
|
1422
|
+
sections.push(spec.overview.description);
|
|
1423
|
+
sections.push("");
|
|
1424
|
+
sections.push("**Target Users:**");
|
|
1425
|
+
for (const user of spec.overview.targetUsers) {
|
|
1426
|
+
sections.push(`- ${user}`);
|
|
1427
|
+
}
|
|
1428
|
+
sections.push("");
|
|
1429
|
+
sections.push("## Goals & Success Criteria");
|
|
1430
|
+
sections.push("");
|
|
1431
|
+
sections.push("### Goals");
|
|
1432
|
+
sections.push("");
|
|
1433
|
+
for (const goal of spec.overview.goals) {
|
|
1434
|
+
sections.push(`- ${goal}`);
|
|
1435
|
+
}
|
|
1436
|
+
sections.push("");
|
|
1437
|
+
sections.push("### Success Criteria");
|
|
1438
|
+
sections.push("");
|
|
1439
|
+
for (const criteria of spec.overview.successCriteria) {
|
|
1440
|
+
sections.push(`- [ ] ${criteria}`);
|
|
1441
|
+
}
|
|
1442
|
+
sections.push("");
|
|
1443
|
+
sections.push("## Functional Requirements");
|
|
1444
|
+
sections.push("");
|
|
1445
|
+
addRequirementsTable(sections, spec.requirements.functional);
|
|
1446
|
+
sections.push("");
|
|
1447
|
+
sections.push("## Non-Functional Requirements");
|
|
1448
|
+
sections.push("");
|
|
1449
|
+
addRequirementsTable(sections, spec.requirements.nonFunctional);
|
|
1450
|
+
sections.push("");
|
|
1451
|
+
sections.push("## Technical Constraints");
|
|
1452
|
+
sections.push("");
|
|
1453
|
+
addRequirementsTable(sections, spec.requirements.constraints);
|
|
1454
|
+
sections.push("");
|
|
1455
|
+
sections.push("## Technology Stack");
|
|
1456
|
+
sections.push("");
|
|
1457
|
+
sections.push("| Area | Decision | Alternatives | Rationale |");
|
|
1458
|
+
sections.push("|------|----------|--------------|-----------|");
|
|
1459
|
+
for (const tech of spec.technical.stack) {
|
|
1460
|
+
sections.push(
|
|
1461
|
+
`| ${tech.area} | **${tech.decision}** | ${tech.alternatives.join(", ") || "-"} | ${tech.rationale} |`
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
sections.push("");
|
|
1465
|
+
sections.push("## Architecture");
|
|
1466
|
+
sections.push("");
|
|
1467
|
+
sections.push(spec.technical.architecture);
|
|
1468
|
+
sections.push("");
|
|
1469
|
+
if (spec.technical.integrations.length > 0) {
|
|
1470
|
+
sections.push("### Integrations");
|
|
1471
|
+
sections.push("");
|
|
1472
|
+
for (const integration of spec.technical.integrations) {
|
|
1473
|
+
sections.push(`- ${integration}`);
|
|
1474
|
+
}
|
|
1475
|
+
sections.push("");
|
|
1476
|
+
}
|
|
1477
|
+
if (spec.technical.deployment) {
|
|
1478
|
+
sections.push("### Deployment");
|
|
1479
|
+
sections.push("");
|
|
1480
|
+
sections.push(spec.technical.deployment);
|
|
1481
|
+
sections.push("");
|
|
1482
|
+
}
|
|
1483
|
+
sections.push("## Assumptions & Risks");
|
|
1484
|
+
sections.push("");
|
|
1485
|
+
sections.push("### Confirmed Assumptions");
|
|
1486
|
+
sections.push("");
|
|
1487
|
+
if (spec.assumptions.confirmed.length > 0) {
|
|
1488
|
+
for (const assumption of spec.assumptions.confirmed) {
|
|
1489
|
+
sections.push(`- \u2705 ${assumption.statement}`);
|
|
1490
|
+
}
|
|
1491
|
+
} else {
|
|
1492
|
+
sections.push("*No confirmed assumptions*");
|
|
1493
|
+
}
|
|
1494
|
+
sections.push("");
|
|
1495
|
+
sections.push("### Unconfirmed Assumptions");
|
|
1496
|
+
sections.push("");
|
|
1497
|
+
if (spec.assumptions.unconfirmed.length > 0) {
|
|
1498
|
+
for (const assumption of spec.assumptions.unconfirmed) {
|
|
1499
|
+
sections.push(
|
|
1500
|
+
`- \u26A0\uFE0F ${assumption.statement} (${assumption.confidence} confidence)`
|
|
1501
|
+
);
|
|
1502
|
+
if (assumption.impactIfWrong) {
|
|
1503
|
+
sections.push(` - *Impact if wrong:* ${assumption.impactIfWrong}`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
} else {
|
|
1507
|
+
sections.push("*No unconfirmed assumptions*");
|
|
1508
|
+
}
|
|
1509
|
+
sections.push("");
|
|
1510
|
+
if (spec.assumptions.risks.length > 0) {
|
|
1511
|
+
sections.push("### Risks");
|
|
1512
|
+
sections.push("");
|
|
1513
|
+
sections.push("| Risk | Probability | Impact | Mitigation |");
|
|
1514
|
+
sections.push("|------|------------|--------|------------|");
|
|
1515
|
+
for (const risk of spec.assumptions.risks) {
|
|
1516
|
+
sections.push(
|
|
1517
|
+
`| ${risk.description} | ${risk.probability} | ${risk.impact} | ${risk.mitigation} |`
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
sections.push("");
|
|
1521
|
+
}
|
|
1522
|
+
sections.push("## Out of Scope");
|
|
1523
|
+
sections.push("");
|
|
1524
|
+
if (spec.outOfScope.length > 0) {
|
|
1525
|
+
for (const item of spec.outOfScope) {
|
|
1526
|
+
sections.push(`- ${item}`);
|
|
1527
|
+
}
|
|
1528
|
+
} else {
|
|
1529
|
+
sections.push("*Nothing explicitly marked as out of scope*");
|
|
1530
|
+
}
|
|
1531
|
+
sections.push("");
|
|
1532
|
+
if (spec.openQuestions.length > 0) {
|
|
1533
|
+
sections.push("## Open Questions");
|
|
1534
|
+
sections.push("");
|
|
1535
|
+
for (const question of spec.openQuestions) {
|
|
1536
|
+
sections.push(`### ${question.question}`);
|
|
1537
|
+
sections.push("");
|
|
1538
|
+
sections.push(`*Context:* ${question.context}`);
|
|
1539
|
+
sections.push(`*Importance:* ${question.importance}`);
|
|
1540
|
+
if (question.defaultAnswer) {
|
|
1541
|
+
sections.push(`*Default answer:* ${question.defaultAnswer}`);
|
|
1542
|
+
}
|
|
1543
|
+
sections.push("");
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
sections.push("---");
|
|
1547
|
+
sections.push("");
|
|
1548
|
+
sections.push("*This specification was generated by Corbat-Coco*");
|
|
1549
|
+
return sections.join("\n");
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// src/phases/converge/specification.ts
|
|
1553
|
+
var SpecificationGenerator = class {
|
|
1554
|
+
llm;
|
|
1555
|
+
config;
|
|
1556
|
+
constructor(llm, config = {}) {
|
|
1557
|
+
this.llm = llm;
|
|
1558
|
+
this.config = { ...DEFAULT_SPEC_CONFIG, ...config };
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Generate a specification from a discovery session
|
|
1562
|
+
*/
|
|
1563
|
+
async generate(session) {
|
|
1564
|
+
if (session.status !== "complete" && session.status !== "refining") {
|
|
1565
|
+
throw new PhaseError(
|
|
1566
|
+
"Discovery session is not ready for specification",
|
|
1567
|
+
{ phase: "converge" }
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
const functional = session.requirements.filter(
|
|
1571
|
+
(r) => r.category === "functional"
|
|
1572
|
+
);
|
|
1573
|
+
const nonFunctional = session.requirements.filter(
|
|
1574
|
+
(r) => r.category === "non_functional" || r.category === "user_experience" || r.category === "deployment"
|
|
1575
|
+
);
|
|
1576
|
+
const constraints = session.requirements.filter(
|
|
1577
|
+
(r) => r.category === "constraint" || r.category === "technical"
|
|
1578
|
+
);
|
|
1579
|
+
const architecture = await this.generateArchitecture(session);
|
|
1580
|
+
const risks = this.config.includeRisks ? generateRisksFromSession(session) : [];
|
|
1581
|
+
const spec = {
|
|
1582
|
+
version: "1.0.0",
|
|
1583
|
+
generatedAt: /* @__PURE__ */ new Date(),
|
|
1584
|
+
overview: generateOverview(session),
|
|
1585
|
+
requirements: {
|
|
1586
|
+
functional,
|
|
1587
|
+
nonFunctional,
|
|
1588
|
+
constraints
|
|
1589
|
+
},
|
|
1590
|
+
technical: {
|
|
1591
|
+
stack: session.techDecisions,
|
|
1592
|
+
architecture,
|
|
1593
|
+
integrations: extractIntegrations(session),
|
|
1594
|
+
deployment: extractDeployment(session)
|
|
1595
|
+
},
|
|
1596
|
+
assumptions: {
|
|
1597
|
+
confirmed: session.assumptions.filter((a) => a.confirmed),
|
|
1598
|
+
unconfirmed: session.assumptions.filter((a) => !a.confirmed),
|
|
1599
|
+
risks
|
|
1600
|
+
},
|
|
1601
|
+
outOfScope: extractOutOfScope(session),
|
|
1602
|
+
openQuestions: session.openQuestions.filter((q) => !q.asked)
|
|
1603
|
+
};
|
|
1604
|
+
return spec;
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Generate a markdown document from the specification
|
|
1608
|
+
* Supports both full Specification and simplified test format
|
|
1609
|
+
*/
|
|
1610
|
+
toMarkdown(spec) {
|
|
1611
|
+
if ("name" in spec && !("overview" in spec)) {
|
|
1612
|
+
return generateSimpleMarkdown(spec);
|
|
1613
|
+
}
|
|
1614
|
+
return generateFullMarkdown(spec);
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Generate a markdown document from the specification (alias for toMarkdown)
|
|
1618
|
+
*/
|
|
1619
|
+
generateMarkdown(spec) {
|
|
1620
|
+
return generateFullMarkdown(spec);
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Generate JSON output
|
|
1624
|
+
*/
|
|
1625
|
+
generateJSON(spec) {
|
|
1626
|
+
return JSON.stringify(spec, null, 2);
|
|
1627
|
+
}
|
|
1628
|
+
// Private helper methods
|
|
1629
|
+
async generateArchitecture(session) {
|
|
1630
|
+
const projectType = inferProjectType(session);
|
|
1631
|
+
const complexity = assessComplexity(session);
|
|
1632
|
+
const prompt = fillPrompt(ARCHITECTURE_PROMPT, {
|
|
1633
|
+
projectType,
|
|
1634
|
+
complexity,
|
|
1635
|
+
requirements: JSON.stringify(session.requirements),
|
|
1636
|
+
techStack: JSON.stringify(session.techDecisions)
|
|
1637
|
+
});
|
|
1638
|
+
try {
|
|
1639
|
+
const response = await this.llm.chat([
|
|
1640
|
+
{ role: "system", content: DISCOVERY_SYSTEM_PROMPT },
|
|
1641
|
+
{ role: "user", content: prompt }
|
|
1642
|
+
]);
|
|
1643
|
+
const content = response.content;
|
|
1644
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
1645
|
+
if (jsonMatch) {
|
|
1646
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1647
|
+
let architecture = `### Pattern: ${parsed.pattern || "Layered Architecture"}
|
|
1648
|
+
|
|
1649
|
+
`;
|
|
1650
|
+
architecture += `${parsed.rationale || ""}
|
|
1651
|
+
|
|
1652
|
+
`;
|
|
1653
|
+
if (parsed.components && parsed.components.length > 0) {
|
|
1654
|
+
architecture += "### Components\n\n";
|
|
1655
|
+
for (const comp of parsed.components) {
|
|
1656
|
+
architecture += `- **${comp.name}**: ${comp.responsibility} (${comp.technology})
|
|
1657
|
+
`;
|
|
1658
|
+
}
|
|
1659
|
+
architecture += "\n";
|
|
1660
|
+
}
|
|
1661
|
+
if (parsed.dataFlow) {
|
|
1662
|
+
architecture += `### Data Flow
|
|
1663
|
+
|
|
1664
|
+
${parsed.dataFlow}
|
|
1665
|
+
|
|
1666
|
+
`;
|
|
1667
|
+
}
|
|
1668
|
+
if (this.config.includeDiagrams && parsed.diagramMermaid) {
|
|
1669
|
+
architecture += "### Diagram\n\n";
|
|
1670
|
+
architecture += "```mermaid\n";
|
|
1671
|
+
architecture += parsed.diagramMermaid;
|
|
1672
|
+
architecture += "\n```\n";
|
|
1673
|
+
}
|
|
1674
|
+
return architecture;
|
|
1675
|
+
}
|
|
1676
|
+
return "Architecture to be determined during ORCHESTRATE phase.";
|
|
1677
|
+
} catch {
|
|
1678
|
+
return "Architecture to be determined during ORCHESTRATE phase.";
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
function createSpecificationGenerator(llm, config) {
|
|
1683
|
+
return new SpecificationGenerator(llm, config);
|
|
1684
|
+
}
|
|
1685
|
+
function getPersistencePaths(projectPath) {
|
|
1686
|
+
const baseDir = path.join(projectPath, ".coco", "spec");
|
|
1687
|
+
return {
|
|
1688
|
+
baseDir,
|
|
1689
|
+
sessionFile: path.join(baseDir, "discovery-session.json"),
|
|
1690
|
+
specFile: path.join(baseDir, "spec.md"),
|
|
1691
|
+
conversationLog: path.join(baseDir, "conversation.jsonl"),
|
|
1692
|
+
checkpointFile: path.join(baseDir, "checkpoint.json")
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
var SessionPersistence = class {
|
|
1696
|
+
paths;
|
|
1697
|
+
constructor(projectPath) {
|
|
1698
|
+
this.paths = getPersistencePaths(projectPath);
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Ensure the persistence directory exists
|
|
1702
|
+
*/
|
|
1703
|
+
async ensureDir() {
|
|
1704
|
+
try {
|
|
1705
|
+
await fs4.mkdir(this.paths.baseDir, { recursive: true });
|
|
1706
|
+
} catch {
|
|
1707
|
+
throw new FileSystemError(
|
|
1708
|
+
`Failed to create persistence directory: ${this.paths.baseDir}`,
|
|
1709
|
+
{ path: this.paths.baseDir, operation: "write" }
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Save a discovery session
|
|
1715
|
+
*/
|
|
1716
|
+
async saveSession(session) {
|
|
1717
|
+
await this.ensureDir();
|
|
1718
|
+
try {
|
|
1719
|
+
const data = JSON.stringify(session, null, 2);
|
|
1720
|
+
await fs4.writeFile(this.paths.sessionFile, data, "utf-8");
|
|
1721
|
+
} catch {
|
|
1722
|
+
throw new FileSystemError(
|
|
1723
|
+
"Failed to save discovery session",
|
|
1724
|
+
{ path: this.paths.sessionFile, operation: "write" }
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Load a discovery session
|
|
1730
|
+
*/
|
|
1731
|
+
async loadSession() {
|
|
1732
|
+
try {
|
|
1733
|
+
const data = await fs4.readFile(this.paths.sessionFile, "utf-8");
|
|
1734
|
+
const parsed = JSON.parse(data);
|
|
1735
|
+
parsed.startedAt = new Date(parsed.startedAt);
|
|
1736
|
+
parsed.updatedAt = new Date(parsed.updatedAt);
|
|
1737
|
+
for (const msg of parsed.conversation) {
|
|
1738
|
+
msg.timestamp = new Date(msg.timestamp);
|
|
1739
|
+
}
|
|
1740
|
+
for (const clarification of parsed.clarifications) {
|
|
1741
|
+
clarification.timestamp = new Date(clarification.timestamp);
|
|
1742
|
+
}
|
|
1743
|
+
return parsed;
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
if (error.code === "ENOENT") {
|
|
1746
|
+
return null;
|
|
1747
|
+
}
|
|
1748
|
+
throw new FileSystemError(
|
|
1749
|
+
"Failed to load discovery session",
|
|
1750
|
+
{ path: this.paths.sessionFile, operation: "read" }
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Check if a session exists
|
|
1756
|
+
*/
|
|
1757
|
+
async hasSession() {
|
|
1758
|
+
try {
|
|
1759
|
+
await fs4.access(this.paths.sessionFile);
|
|
1760
|
+
return true;
|
|
1761
|
+
} catch {
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Delete a session
|
|
1767
|
+
*/
|
|
1768
|
+
async deleteSession() {
|
|
1769
|
+
try {
|
|
1770
|
+
await fs4.unlink(this.paths.sessionFile);
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
if (error.code !== "ENOENT") {
|
|
1773
|
+
throw new FileSystemError(
|
|
1774
|
+
"Failed to delete discovery session",
|
|
1775
|
+
{ path: this.paths.sessionFile, operation: "delete" }
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Save the specification markdown
|
|
1782
|
+
*/
|
|
1783
|
+
async saveSpecification(content) {
|
|
1784
|
+
await this.ensureDir();
|
|
1785
|
+
try {
|
|
1786
|
+
await fs4.writeFile(this.paths.specFile, content, "utf-8");
|
|
1787
|
+
} catch {
|
|
1788
|
+
throw new FileSystemError(
|
|
1789
|
+
"Failed to save specification",
|
|
1790
|
+
{ path: this.paths.specFile, operation: "write" }
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Load the specification markdown
|
|
1796
|
+
*/
|
|
1797
|
+
async loadSpecification() {
|
|
1798
|
+
try {
|
|
1799
|
+
return await fs4.readFile(this.paths.specFile, "utf-8");
|
|
1800
|
+
} catch {
|
|
1801
|
+
return null;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Append a message to the conversation log
|
|
1806
|
+
*/
|
|
1807
|
+
async appendConversation(role, content) {
|
|
1808
|
+
await this.ensureDir();
|
|
1809
|
+
const entry = {
|
|
1810
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1811
|
+
role,
|
|
1812
|
+
content
|
|
1813
|
+
};
|
|
1814
|
+
try {
|
|
1815
|
+
await fs4.appendFile(
|
|
1816
|
+
this.paths.conversationLog,
|
|
1817
|
+
JSON.stringify(entry) + "\n",
|
|
1818
|
+
"utf-8"
|
|
1819
|
+
);
|
|
1820
|
+
} catch {
|
|
1821
|
+
throw new FileSystemError(
|
|
1822
|
+
"Failed to append to conversation log",
|
|
1823
|
+
{ path: this.paths.conversationLog, operation: "write" }
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Load the full conversation log
|
|
1829
|
+
*/
|
|
1830
|
+
async loadConversationLog() {
|
|
1831
|
+
try {
|
|
1832
|
+
const data = await fs4.readFile(this.paths.conversationLog, "utf-8");
|
|
1833
|
+
const lines = data.trim().split("\n");
|
|
1834
|
+
return lines.filter((line) => line.trim()).map((line) => JSON.parse(line));
|
|
1835
|
+
} catch {
|
|
1836
|
+
return [];
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Save a checkpoint
|
|
1841
|
+
*/
|
|
1842
|
+
async saveCheckpoint(checkpoint) {
|
|
1843
|
+
await this.ensureDir();
|
|
1844
|
+
try {
|
|
1845
|
+
const data = JSON.stringify(checkpoint, null, 2);
|
|
1846
|
+
await fs4.writeFile(this.paths.checkpointFile, data, "utf-8");
|
|
1847
|
+
} catch {
|
|
1848
|
+
throw new FileSystemError(
|
|
1849
|
+
"Failed to save checkpoint",
|
|
1850
|
+
{ path: this.paths.checkpointFile, operation: "write" }
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Load a checkpoint
|
|
1856
|
+
*/
|
|
1857
|
+
async loadCheckpoint() {
|
|
1858
|
+
try {
|
|
1859
|
+
const data = await fs4.readFile(this.paths.checkpointFile, "utf-8");
|
|
1860
|
+
const parsed = JSON.parse(data);
|
|
1861
|
+
parsed.timestamp = new Date(parsed.timestamp);
|
|
1862
|
+
return parsed;
|
|
1863
|
+
} catch {
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Clear all persisted data
|
|
1869
|
+
*/
|
|
1870
|
+
async clearAll() {
|
|
1871
|
+
try {
|
|
1872
|
+
await fs4.rm(this.paths.baseDir, { recursive: true, force: true });
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
if (error.code !== "ENOENT") {
|
|
1875
|
+
throw new FileSystemError(
|
|
1876
|
+
"Failed to clear persistence data",
|
|
1877
|
+
{ path: this.paths.baseDir, operation: "delete" }
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Get the specification file path
|
|
1884
|
+
*/
|
|
1885
|
+
getSpecPath() {
|
|
1886
|
+
return this.paths.specFile;
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
function createCheckpoint(sessionId, step, progress, specGenerated = false, metadata = {}) {
|
|
1890
|
+
return {
|
|
1891
|
+
id: `converge-${Date.now()}`,
|
|
1892
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1893
|
+
step,
|
|
1894
|
+
sessionId,
|
|
1895
|
+
progress,
|
|
1896
|
+
specGenerated,
|
|
1897
|
+
metadata
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
var SessionManager = class {
|
|
1901
|
+
persistence;
|
|
1902
|
+
constructor(projectPath) {
|
|
1903
|
+
this.persistence = new SessionPersistence(projectPath);
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Get the persistence layer
|
|
1907
|
+
*/
|
|
1908
|
+
getPersistence() {
|
|
1909
|
+
return this.persistence;
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Save session with automatic checkpoint
|
|
1913
|
+
*/
|
|
1914
|
+
async saveWithCheckpoint(session, step, progress) {
|
|
1915
|
+
await this.persistence.saveSession(session);
|
|
1916
|
+
const checkpoint = createCheckpoint(
|
|
1917
|
+
session.id,
|
|
1918
|
+
step,
|
|
1919
|
+
progress,
|
|
1920
|
+
session.status === "spec_generated"
|
|
1921
|
+
);
|
|
1922
|
+
await this.persistence.saveCheckpoint(checkpoint);
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Resume from last checkpoint
|
|
1926
|
+
*/
|
|
1927
|
+
async resume() {
|
|
1928
|
+
const checkpoint = await this.persistence.loadCheckpoint();
|
|
1929
|
+
if (!checkpoint) return null;
|
|
1930
|
+
const session = await this.persistence.loadSession();
|
|
1931
|
+
if (!session) return null;
|
|
1932
|
+
return { session, checkpoint };
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Check if can resume
|
|
1936
|
+
*/
|
|
1937
|
+
async canResume() {
|
|
1938
|
+
const checkpoint = await this.persistence.loadCheckpoint();
|
|
1939
|
+
return checkpoint !== null && checkpoint.step !== "complete";
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Get resume info without loading full session
|
|
1943
|
+
*/
|
|
1944
|
+
async getResumeInfo() {
|
|
1945
|
+
const checkpoint = await this.persistence.loadCheckpoint();
|
|
1946
|
+
if (!checkpoint) return null;
|
|
1947
|
+
return {
|
|
1948
|
+
sessionId: checkpoint.sessionId,
|
|
1949
|
+
step: checkpoint.step,
|
|
1950
|
+
progress: checkpoint.progress,
|
|
1951
|
+
timestamp: checkpoint.timestamp
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* Complete the session and save specification
|
|
1956
|
+
*/
|
|
1957
|
+
async complete(session, specMarkdown) {
|
|
1958
|
+
session.status = "spec_generated";
|
|
1959
|
+
session.updatedAt = /* @__PURE__ */ new Date();
|
|
1960
|
+
await this.persistence.saveSession(session);
|
|
1961
|
+
await this.persistence.saveSpecification(specMarkdown);
|
|
1962
|
+
const checkpoint = createCheckpoint(
|
|
1963
|
+
session.id,
|
|
1964
|
+
"complete",
|
|
1965
|
+
100,
|
|
1966
|
+
true
|
|
1967
|
+
);
|
|
1968
|
+
await this.persistence.saveCheckpoint(checkpoint);
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
function createSessionManager(projectPath) {
|
|
1972
|
+
return new SessionManager(projectPath);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// src/phases/converge/executor.ts
|
|
1976
|
+
var DEFAULT_CONVERGE_CONFIG = {
|
|
1977
|
+
maxQuestionRounds: 3,
|
|
1978
|
+
maxQuestionsPerRound: 3,
|
|
1979
|
+
autoProceed: false,
|
|
1980
|
+
includeDiagrams: true
|
|
1981
|
+
};
|
|
1982
|
+
var ConvergeExecutor = class {
|
|
1983
|
+
name = "converge";
|
|
1984
|
+
description = "Gather requirements and generate specification";
|
|
1985
|
+
config;
|
|
1986
|
+
discovery = null;
|
|
1987
|
+
specGenerator = null;
|
|
1988
|
+
sessionManager = null;
|
|
1989
|
+
currentSession = null;
|
|
1990
|
+
llm = null;
|
|
1991
|
+
constructor(config = {}) {
|
|
1992
|
+
this.config = { ...DEFAULT_CONVERGE_CONFIG, ...config };
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Check if the phase can start
|
|
1996
|
+
*/
|
|
1997
|
+
canStart(_context) {
|
|
1998
|
+
return true;
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Execute the CONVERGE phase
|
|
2002
|
+
*/
|
|
2003
|
+
async execute(context) {
|
|
2004
|
+
const startTime = /* @__PURE__ */ new Date();
|
|
2005
|
+
const artifacts = [];
|
|
2006
|
+
try {
|
|
2007
|
+
await this.initialize(context);
|
|
2008
|
+
const resumeData = await this.sessionManager.resume();
|
|
2009
|
+
if (resumeData) {
|
|
2010
|
+
this.currentSession = resumeData.session;
|
|
2011
|
+
this.discovery.resumeSession(this.currentSession);
|
|
2012
|
+
this.reportProgress(
|
|
2013
|
+
resumeData.checkpoint.step,
|
|
2014
|
+
resumeData.checkpoint.progress,
|
|
2015
|
+
"Resuming from checkpoint"
|
|
2016
|
+
);
|
|
2017
|
+
} else {
|
|
2018
|
+
const initialInput = await this.getUserInput(
|
|
2019
|
+
"Please describe the project you want to build:",
|
|
2020
|
+
void 0
|
|
2021
|
+
);
|
|
2022
|
+
this.reportProgress("discovery", 10, "Starting discovery...");
|
|
2023
|
+
this.currentSession = await this.discovery.startSession(initialInput);
|
|
2024
|
+
await this.saveProgress("discovery", 15);
|
|
2025
|
+
}
|
|
2026
|
+
await this.runDiscoveryLoop();
|
|
2027
|
+
this.discovery.markComplete();
|
|
2028
|
+
this.reportProgress("spec_generation", 80, "Generating specification...");
|
|
2029
|
+
const spec = await this.specGenerator.generate(this.currentSession);
|
|
2030
|
+
const specMarkdown = this.specGenerator.generateMarkdown(spec);
|
|
2031
|
+
await this.sessionManager.complete(this.currentSession, specMarkdown);
|
|
2032
|
+
artifacts.push({
|
|
2033
|
+
type: "specification",
|
|
2034
|
+
path: this.sessionManager.getPersistence().getSpecPath(),
|
|
2035
|
+
description: "Project specification document"
|
|
2036
|
+
});
|
|
2037
|
+
this.reportProgress("complete", 100, "CONVERGE phase complete");
|
|
2038
|
+
const endTime = /* @__PURE__ */ new Date();
|
|
2039
|
+
return {
|
|
2040
|
+
phase: "converge",
|
|
2041
|
+
success: true,
|
|
2042
|
+
artifacts,
|
|
2043
|
+
metrics: {
|
|
2044
|
+
startTime,
|
|
2045
|
+
endTime,
|
|
2046
|
+
durationMs: endTime.getTime() - startTime.getTime(),
|
|
2047
|
+
llmCalls: this.currentSession.conversation.length,
|
|
2048
|
+
tokensUsed: 0
|
|
2049
|
+
// Would need to track this
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
return {
|
|
2054
|
+
phase: "converge",
|
|
2055
|
+
success: false,
|
|
2056
|
+
artifacts,
|
|
2057
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Check if the phase can complete
|
|
2063
|
+
*/
|
|
2064
|
+
canComplete(_context) {
|
|
2065
|
+
if (!this.discovery || !this.currentSession) return false;
|
|
2066
|
+
return this.discovery.isComplete() || this.discovery.getCriticalQuestions().length === 0;
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Create a checkpoint for recovery
|
|
2070
|
+
*/
|
|
2071
|
+
async checkpoint(_context) {
|
|
2072
|
+
const step = this.getCurrentStep();
|
|
2073
|
+
const progress = this.calculateProgress();
|
|
2074
|
+
if (this.currentSession && this.sessionManager) {
|
|
2075
|
+
await this.sessionManager.saveWithCheckpoint(
|
|
2076
|
+
this.currentSession,
|
|
2077
|
+
step,
|
|
2078
|
+
progress
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
return {
|
|
2082
|
+
phase: "converge",
|
|
2083
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2084
|
+
state: {
|
|
2085
|
+
artifacts: [],
|
|
2086
|
+
progress,
|
|
2087
|
+
checkpoint: null
|
|
2088
|
+
},
|
|
2089
|
+
resumePoint: step
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Restore from a checkpoint
|
|
2094
|
+
*/
|
|
2095
|
+
async restore(_checkpoint, context) {
|
|
2096
|
+
await this.initialize(context);
|
|
2097
|
+
const resumeData = await this.sessionManager.resume();
|
|
2098
|
+
if (resumeData) {
|
|
2099
|
+
this.currentSession = resumeData.session;
|
|
2100
|
+
this.discovery.resumeSession(this.currentSession);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
// Private methods
|
|
2104
|
+
async initialize(context) {
|
|
2105
|
+
this.llm = this.createLLMAdapter(context);
|
|
2106
|
+
this.discovery = createDiscoveryEngine(this.llm, {
|
|
2107
|
+
maxQuestionsPerRound: this.config.maxQuestionsPerRound
|
|
2108
|
+
});
|
|
2109
|
+
this.specGenerator = createSpecificationGenerator(this.llm, {
|
|
2110
|
+
includeDiagrams: this.config.includeDiagrams
|
|
2111
|
+
});
|
|
2112
|
+
this.sessionManager = createSessionManager(context.projectPath);
|
|
2113
|
+
}
|
|
2114
|
+
createLLMAdapter(context) {
|
|
2115
|
+
const llmContext = context.llm;
|
|
2116
|
+
return {
|
|
2117
|
+
id: "phase-adapter",
|
|
2118
|
+
name: "Phase LLM Adapter",
|
|
2119
|
+
async initialize() {
|
|
2120
|
+
},
|
|
2121
|
+
async chat(messages) {
|
|
2122
|
+
const adapted = messages.map((m) => ({
|
|
2123
|
+
role: m.role,
|
|
2124
|
+
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
|
|
2125
|
+
}));
|
|
2126
|
+
const response = await llmContext.chat(adapted);
|
|
2127
|
+
return {
|
|
2128
|
+
id: `chat-${Date.now()}`,
|
|
2129
|
+
content: response.content,
|
|
2130
|
+
stopReason: "end_turn",
|
|
2131
|
+
usage: {
|
|
2132
|
+
inputTokens: response.usage.inputTokens,
|
|
2133
|
+
outputTokens: response.usage.outputTokens
|
|
2134
|
+
},
|
|
2135
|
+
model: "phase-adapter"
|
|
2136
|
+
};
|
|
2137
|
+
},
|
|
2138
|
+
async chatWithTools(messages, options) {
|
|
2139
|
+
const adapted = messages.map((m) => ({
|
|
2140
|
+
role: m.role,
|
|
2141
|
+
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
|
|
2142
|
+
}));
|
|
2143
|
+
const tools = options.tools.map((t) => ({
|
|
2144
|
+
name: t.name,
|
|
2145
|
+
description: t.description,
|
|
2146
|
+
parameters: t.input_schema
|
|
2147
|
+
}));
|
|
2148
|
+
const response = await llmContext.chatWithTools(adapted, tools);
|
|
2149
|
+
return {
|
|
2150
|
+
id: `chat-${Date.now()}`,
|
|
2151
|
+
content: response.content,
|
|
2152
|
+
stopReason: "end_turn",
|
|
2153
|
+
usage: {
|
|
2154
|
+
inputTokens: response.usage.inputTokens,
|
|
2155
|
+
outputTokens: response.usage.outputTokens
|
|
2156
|
+
},
|
|
2157
|
+
model: "phase-adapter",
|
|
2158
|
+
toolCalls: (response.toolCalls || []).map((tc) => ({
|
|
2159
|
+
id: tc.name,
|
|
2160
|
+
name: tc.name,
|
|
2161
|
+
input: tc.arguments
|
|
2162
|
+
}))
|
|
2163
|
+
};
|
|
2164
|
+
},
|
|
2165
|
+
async *stream(messages) {
|
|
2166
|
+
const adapted = messages.map((m) => ({
|
|
2167
|
+
role: m.role,
|
|
2168
|
+
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
|
|
2169
|
+
}));
|
|
2170
|
+
const response = await llmContext.chat(adapted);
|
|
2171
|
+
yield {
|
|
2172
|
+
type: "text",
|
|
2173
|
+
text: response.content
|
|
2174
|
+
};
|
|
2175
|
+
yield {
|
|
2176
|
+
type: "done"
|
|
2177
|
+
};
|
|
2178
|
+
},
|
|
2179
|
+
countTokens(_text) {
|
|
2180
|
+
return Math.ceil(_text.length / 4);
|
|
2181
|
+
},
|
|
2182
|
+
getContextWindow() {
|
|
2183
|
+
return 2e5;
|
|
2184
|
+
},
|
|
2185
|
+
async isAvailable() {
|
|
2186
|
+
return true;
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
async runDiscoveryLoop() {
|
|
2191
|
+
let round = 0;
|
|
2192
|
+
while (round < this.config.maxQuestionRounds) {
|
|
2193
|
+
round++;
|
|
2194
|
+
const criticalQuestions = this.discovery.getCriticalQuestions();
|
|
2195
|
+
if (criticalQuestions.length === 0 && this.config.autoProceed) {
|
|
2196
|
+
break;
|
|
2197
|
+
}
|
|
2198
|
+
const openQuestions = this.discovery.getOpenQuestions();
|
|
2199
|
+
if (openQuestions.length === 0) {
|
|
2200
|
+
const newQuestions = await this.discovery.generateQuestions();
|
|
2201
|
+
if (newQuestions.length === 0) {
|
|
2202
|
+
break;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
const questions = this.discovery.getOpenQuestions();
|
|
2206
|
+
if (questions.length === 0) {
|
|
2207
|
+
break;
|
|
2208
|
+
}
|
|
2209
|
+
this.reportProgress(
|
|
2210
|
+
"clarification",
|
|
2211
|
+
30 + round * 15,
|
|
2212
|
+
`Asking clarification questions (round ${round})`
|
|
2213
|
+
);
|
|
2214
|
+
for (const question of questions) {
|
|
2215
|
+
const answer = await this.askQuestion(question);
|
|
2216
|
+
if (answer.toLowerCase() === "skip") {
|
|
2217
|
+
if (question.defaultAnswer) {
|
|
2218
|
+
await this.discovery.processAnswer(
|
|
2219
|
+
question.id,
|
|
2220
|
+
question.defaultAnswer
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
if (answer.toLowerCase() === "done") {
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
await this.discovery.processAnswer(question.id, answer);
|
|
2229
|
+
}
|
|
2230
|
+
await this.saveProgress("clarification", 30 + round * 15);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
async askQuestion(question) {
|
|
2234
|
+
let prompt = question.question;
|
|
2235
|
+
if (question.context) {
|
|
2236
|
+
prompt += `
|
|
2237
|
+
|
|
2238
|
+
Context: ${question.context}`;
|
|
2239
|
+
}
|
|
2240
|
+
if (question.options && question.options.length > 0) {
|
|
2241
|
+
prompt += "\n\nOptions:";
|
|
2242
|
+
for (let i = 0; i < question.options.length; i++) {
|
|
2243
|
+
prompt += `
|
|
2244
|
+
${i + 1}. ${question.options[i]}`;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
if (question.defaultAnswer) {
|
|
2248
|
+
prompt += `
|
|
2249
|
+
|
|
2250
|
+
(Default: ${question.defaultAnswer}, type 'skip' to use default)`;
|
|
2251
|
+
}
|
|
2252
|
+
prompt += "\n\n(Type 'done' to finish questions and proceed)";
|
|
2253
|
+
return this.getUserInput(prompt, question.options);
|
|
2254
|
+
}
|
|
2255
|
+
async getUserInput(prompt, options) {
|
|
2256
|
+
if (this.config.onUserInput) {
|
|
2257
|
+
return this.config.onUserInput(prompt, options);
|
|
2258
|
+
}
|
|
2259
|
+
throw new PhaseError(
|
|
2260
|
+
"No user input handler configured",
|
|
2261
|
+
{ phase: "converge" }
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2264
|
+
reportProgress(step, progress, message) {
|
|
2265
|
+
if (this.config.onProgress) {
|
|
2266
|
+
this.config.onProgress(step, progress, message);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
async saveProgress(step, progress) {
|
|
2270
|
+
if (this.currentSession && this.sessionManager) {
|
|
2271
|
+
await this.sessionManager.saveWithCheckpoint(
|
|
2272
|
+
this.currentSession,
|
|
2273
|
+
step,
|
|
2274
|
+
progress
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
getCurrentStep() {
|
|
2279
|
+
if (!this.currentSession) return "init";
|
|
2280
|
+
switch (this.currentSession.status) {
|
|
2281
|
+
case "gathering":
|
|
2282
|
+
return "discovery";
|
|
2283
|
+
case "clarifying":
|
|
2284
|
+
return "clarification";
|
|
2285
|
+
case "refining":
|
|
2286
|
+
return "refinement";
|
|
2287
|
+
case "complete":
|
|
2288
|
+
return "spec_generation";
|
|
2289
|
+
case "spec_generated":
|
|
2290
|
+
return "complete";
|
|
2291
|
+
default:
|
|
2292
|
+
return "discovery";
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
calculateProgress() {
|
|
2296
|
+
if (!this.currentSession) return 0;
|
|
2297
|
+
const step = this.getCurrentStep();
|
|
2298
|
+
const stepProgress = {
|
|
2299
|
+
init: 0,
|
|
2300
|
+
discovery: 20,
|
|
2301
|
+
clarification: 50,
|
|
2302
|
+
refinement: 70,
|
|
2303
|
+
spec_generation: 90,
|
|
2304
|
+
complete: 100
|
|
2305
|
+
};
|
|
2306
|
+
return stepProgress[step] || 0;
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
function createConvergeExecutor(config) {
|
|
2310
|
+
return new ConvergeExecutor(config);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
// src/phases/orchestrate/types.ts
|
|
2314
|
+
var DEFAULT_SPRINT_CONFIG = {
|
|
2315
|
+
sprintDuration: 14,
|
|
2316
|
+
targetVelocity: 20,
|
|
2317
|
+
maxStoriesPerSprint: 8,
|
|
2318
|
+
bufferPercentage: 20
|
|
2319
|
+
};
|
|
2320
|
+
var DEFAULT_ORCHESTRATE_CONFIG = {
|
|
2321
|
+
generateC4Diagrams: true,
|
|
2322
|
+
generateSequenceDiagrams: true,
|
|
2323
|
+
maxADRs: 10,
|
|
2324
|
+
sprint: DEFAULT_SPRINT_CONFIG,
|
|
2325
|
+
breakdownStrategy: "tdd",
|
|
2326
|
+
generateDeploymentDocs: true
|
|
2327
|
+
};
|
|
2328
|
+
|
|
2329
|
+
// src/phases/orchestrate/prompts.ts
|
|
2330
|
+
var ARCHITECT_SYSTEM_PROMPT = `You are a senior software architect with expertise in designing scalable, maintainable systems.
|
|
2331
|
+
|
|
2332
|
+
Your responsibilities:
|
|
2333
|
+
1. Design clear, modular architectures
|
|
2334
|
+
2. Document key decisions with ADRs
|
|
2335
|
+
3. Create actionable development plans
|
|
2336
|
+
4. Consider quality attributes (performance, security, maintainability)
|
|
2337
|
+
5. Apply appropriate design patterns
|
|
2338
|
+
|
|
2339
|
+
Guidelines:
|
|
2340
|
+
- Favor simplicity over complexity
|
|
2341
|
+
- Design for change and extensibility
|
|
2342
|
+
- Consider trade-offs explicitly
|
|
2343
|
+
- Document assumptions and risks
|
|
2344
|
+
- Use industry-standard patterns when appropriate
|
|
2345
|
+
|
|
2346
|
+
You produce structured, well-documented architectural artifacts.`;
|
|
2347
|
+
var GENERATE_ARCHITECTURE_PROMPT = `Based on the project specification, design a comprehensive software architecture.
|
|
2348
|
+
|
|
2349
|
+
Project Specification:
|
|
2350
|
+
{{specification}}
|
|
2351
|
+
|
|
2352
|
+
Technology Stack:
|
|
2353
|
+
{{techStack}}
|
|
2354
|
+
|
|
2355
|
+
Requirements Summary:
|
|
2356
|
+
- Functional: {{functionalCount}} requirements
|
|
2357
|
+
- Non-Functional: {{nonFunctionalCount}} requirements
|
|
2358
|
+
- Constraints: {{constraintCount}} constraints
|
|
2359
|
+
|
|
2360
|
+
Generate an architecture that:
|
|
2361
|
+
1. Addresses all functional requirements
|
|
2362
|
+
2. Satisfies non-functional requirements
|
|
2363
|
+
3. Respects technical constraints
|
|
2364
|
+
4. Is appropriately complex for the project scope
|
|
2365
|
+
|
|
2366
|
+
Respond in JSON format:
|
|
2367
|
+
{
|
|
2368
|
+
"overview": {
|
|
2369
|
+
"pattern": "layered|hexagonal|clean|microservices|event_driven|cqrs|modular_monolith|serverless",
|
|
2370
|
+
"description": "string",
|
|
2371
|
+
"principles": ["string"],
|
|
2372
|
+
"qualityAttributes": [
|
|
2373
|
+
{
|
|
2374
|
+
"name": "string",
|
|
2375
|
+
"description": "string",
|
|
2376
|
+
"priority": "high|medium|low",
|
|
2377
|
+
"tradeoffs": ["string"]
|
|
2378
|
+
}
|
|
2379
|
+
]
|
|
2380
|
+
},
|
|
2381
|
+
"components": [
|
|
2382
|
+
{
|
|
2383
|
+
"id": "string",
|
|
2384
|
+
"name": "string",
|
|
2385
|
+
"type": "service|controller|repository|adapter|port|domain|usecase|utility|external",
|
|
2386
|
+
"description": "string",
|
|
2387
|
+
"responsibilities": ["string"],
|
|
2388
|
+
"technology": "string",
|
|
2389
|
+
"layer": "string",
|
|
2390
|
+
"dependencies": ["component_id"]
|
|
2391
|
+
}
|
|
2392
|
+
],
|
|
2393
|
+
"relationships": [
|
|
2394
|
+
{
|
|
2395
|
+
"from": "component_id",
|
|
2396
|
+
"to": "component_id",
|
|
2397
|
+
"type": "uses|implements|extends|depends|calls|publishes|subscribes",
|
|
2398
|
+
"description": "string"
|
|
2399
|
+
}
|
|
2400
|
+
],
|
|
2401
|
+
"dataModels": [
|
|
2402
|
+
{
|
|
2403
|
+
"name": "string",
|
|
2404
|
+
"description": "string",
|
|
2405
|
+
"fields": [
|
|
2406
|
+
{
|
|
2407
|
+
"name": "string",
|
|
2408
|
+
"type": "string",
|
|
2409
|
+
"required": true,
|
|
2410
|
+
"description": "string"
|
|
2411
|
+
}
|
|
2412
|
+
],
|
|
2413
|
+
"relationships": [
|
|
2414
|
+
{
|
|
2415
|
+
"type": "one_to_one|one_to_many|many_to_many",
|
|
2416
|
+
"target": "model_name",
|
|
2417
|
+
"description": "string"
|
|
2418
|
+
}
|
|
2419
|
+
]
|
|
2420
|
+
}
|
|
2421
|
+
],
|
|
2422
|
+
"integrations": [
|
|
2423
|
+
{
|
|
2424
|
+
"name": "string",
|
|
2425
|
+
"type": "rest_api|graphql|grpc|database|message_queue|file_system|external_service",
|
|
2426
|
+
"description": "string",
|
|
2427
|
+
"endpoint": "string",
|
|
2428
|
+
"authentication": "string"
|
|
2429
|
+
}
|
|
2430
|
+
],
|
|
2431
|
+
"reasoning": "string"
|
|
2432
|
+
}`;
|
|
2433
|
+
var GENERATE_C4_DIAGRAMS_PROMPT = `Generate C4 model diagrams for the architecture.
|
|
2434
|
+
|
|
2435
|
+
Architecture Overview:
|
|
2436
|
+
{{architecture}}
|
|
2437
|
+
|
|
2438
|
+
Generate Mermaid diagrams for:
|
|
2439
|
+
1. Context diagram (system and external actors)
|
|
2440
|
+
2. Container diagram (deployable units)
|
|
2441
|
+
3. Component diagram (key components within containers)
|
|
2442
|
+
|
|
2443
|
+
Respond in JSON format:
|
|
2444
|
+
{
|
|
2445
|
+
"diagrams": [
|
|
2446
|
+
{
|
|
2447
|
+
"id": "c4_context",
|
|
2448
|
+
"type": "c4_context",
|
|
2449
|
+
"title": "System Context Diagram",
|
|
2450
|
+
"description": "string",
|
|
2451
|
+
"mermaid": "C4Context\\n..."
|
|
2452
|
+
},
|
|
2453
|
+
{
|
|
2454
|
+
"id": "c4_container",
|
|
2455
|
+
"type": "c4_container",
|
|
2456
|
+
"title": "Container Diagram",
|
|
2457
|
+
"description": "string",
|
|
2458
|
+
"mermaid": "C4Container\\n..."
|
|
2459
|
+
},
|
|
2460
|
+
{
|
|
2461
|
+
"id": "c4_component",
|
|
2462
|
+
"type": "c4_component",
|
|
2463
|
+
"title": "Component Diagram",
|
|
2464
|
+
"description": "string",
|
|
2465
|
+
"mermaid": "C4Component\\n..."
|
|
2466
|
+
}
|
|
2467
|
+
]
|
|
2468
|
+
}`;
|
|
2469
|
+
var GENERATE_SEQUENCE_DIAGRAMS_PROMPT = `Generate sequence diagrams for key user flows.
|
|
2470
|
+
|
|
2471
|
+
Architecture:
|
|
2472
|
+
{{architecture}}
|
|
2473
|
+
|
|
2474
|
+
Key Functional Requirements:
|
|
2475
|
+
{{functionalRequirements}}
|
|
2476
|
+
|
|
2477
|
+
Generate sequence diagrams for the 3-5 most important user flows.
|
|
2478
|
+
|
|
2479
|
+
Respond in JSON format:
|
|
2480
|
+
{
|
|
2481
|
+
"diagrams": [
|
|
2482
|
+
{
|
|
2483
|
+
"id": "string",
|
|
2484
|
+
"type": "sequence",
|
|
2485
|
+
"title": "string",
|
|
2486
|
+
"description": "string",
|
|
2487
|
+
"mermaid": "sequenceDiagram\\n..."
|
|
2488
|
+
}
|
|
2489
|
+
]
|
|
2490
|
+
}`;
|
|
2491
|
+
var GENERATE_ADRS_PROMPT = `Generate Architecture Decision Records for key decisions.
|
|
2492
|
+
|
|
2493
|
+
Architecture:
|
|
2494
|
+
{{architecture}}
|
|
2495
|
+
|
|
2496
|
+
Technology Stack:
|
|
2497
|
+
{{techStack}}
|
|
2498
|
+
|
|
2499
|
+
Identify the {{maxADRs}} most important architectural decisions and document them as ADRs.
|
|
2500
|
+
|
|
2501
|
+
Include ADRs for:
|
|
2502
|
+
1. Core architecture pattern choice
|
|
2503
|
+
2. Major technology selections
|
|
2504
|
+
3. Security approach
|
|
2505
|
+
4. Data storage strategy
|
|
2506
|
+
5. Integration patterns
|
|
2507
|
+
6. Testing strategy
|
|
2508
|
+
7. Deployment approach
|
|
2509
|
+
|
|
2510
|
+
Respond in JSON format:
|
|
2511
|
+
{
|
|
2512
|
+
"adrs": [
|
|
2513
|
+
{
|
|
2514
|
+
"number": 1,
|
|
2515
|
+
"title": "string",
|
|
2516
|
+
"status": "accepted",
|
|
2517
|
+
"context": "Detailed context explaining the situation and problem",
|
|
2518
|
+
"decision": "Clear statement of the decision made",
|
|
2519
|
+
"consequences": {
|
|
2520
|
+
"positive": ["string"],
|
|
2521
|
+
"negative": ["string"],
|
|
2522
|
+
"neutral": ["string"]
|
|
2523
|
+
},
|
|
2524
|
+
"alternatives": [
|
|
2525
|
+
{
|
|
2526
|
+
"option": "string",
|
|
2527
|
+
"pros": ["string"],
|
|
2528
|
+
"cons": ["string"],
|
|
2529
|
+
"reason": "Why not chosen"
|
|
2530
|
+
}
|
|
2531
|
+
],
|
|
2532
|
+
"references": ["string"]
|
|
2533
|
+
}
|
|
2534
|
+
]
|
|
2535
|
+
}`;
|
|
2536
|
+
var GENERATE_BACKLOG_PROMPT = `Create a complete development backlog from the architecture and requirements.
|
|
2537
|
+
|
|
2538
|
+
Architecture:
|
|
2539
|
+
{{architecture}}
|
|
2540
|
+
|
|
2541
|
+
Requirements:
|
|
2542
|
+
{{requirements}}
|
|
2543
|
+
|
|
2544
|
+
Breakdown Strategy: {{breakdownStrategy}}
|
|
2545
|
+
|
|
2546
|
+
Generate a backlog with:
|
|
2547
|
+
1. Epics (major features or components)
|
|
2548
|
+
2. User Stories (deliverable increments)
|
|
2549
|
+
3. Tasks (atomic work items)
|
|
2550
|
+
|
|
2551
|
+
Follow these guidelines:
|
|
2552
|
+
- Each task should be completable in 1-4 hours
|
|
2553
|
+
- Include tests for each feature task
|
|
2554
|
+
- Order by dependency and priority
|
|
2555
|
+
- Include infrastructure/setup tasks
|
|
2556
|
+
- Include documentation tasks
|
|
2557
|
+
|
|
2558
|
+
Respond in JSON format:
|
|
2559
|
+
{
|
|
2560
|
+
"epics": [
|
|
2561
|
+
{
|
|
2562
|
+
"id": "epic_001",
|
|
2563
|
+
"title": "string",
|
|
2564
|
+
"description": "string",
|
|
2565
|
+
"priority": 1-5,
|
|
2566
|
+
"dependencies": ["epic_id"],
|
|
2567
|
+
"status": "planned"
|
|
2568
|
+
}
|
|
2569
|
+
],
|
|
2570
|
+
"stories": [
|
|
2571
|
+
{
|
|
2572
|
+
"id": "story_001",
|
|
2573
|
+
"epicId": "epic_001",
|
|
2574
|
+
"title": "string",
|
|
2575
|
+
"asA": "role",
|
|
2576
|
+
"iWant": "feature",
|
|
2577
|
+
"soThat": "benefit",
|
|
2578
|
+
"acceptanceCriteria": ["string"],
|
|
2579
|
+
"points": 1|2|3|5|8|13,
|
|
2580
|
+
"status": "backlog"
|
|
2581
|
+
}
|
|
2582
|
+
],
|
|
2583
|
+
"tasks": [
|
|
2584
|
+
{
|
|
2585
|
+
"id": "task_001",
|
|
2586
|
+
"storyId": "story_001",
|
|
2587
|
+
"title": "string",
|
|
2588
|
+
"description": "string",
|
|
2589
|
+
"type": "feature|test|refactor|docs|infra|config",
|
|
2590
|
+
"files": ["expected/file/paths"],
|
|
2591
|
+
"dependencies": ["task_id"],
|
|
2592
|
+
"estimatedComplexity": "trivial|simple|moderate|complex",
|
|
2593
|
+
"status": "pending"
|
|
2594
|
+
}
|
|
2595
|
+
],
|
|
2596
|
+
"estimatedSprints": number,
|
|
2597
|
+
"warnings": ["string"]
|
|
2598
|
+
}`;
|
|
2599
|
+
var PLAN_SPRINT_PROMPT = `Plan the first sprint from the backlog.
|
|
2600
|
+
|
|
2601
|
+
Backlog Summary:
|
|
2602
|
+
- Epics: {{epicCount}}
|
|
2603
|
+
- Stories: {{storyCount}}
|
|
2604
|
+
- Tasks: {{taskCount}}
|
|
2605
|
+
|
|
2606
|
+
Sprint Configuration:
|
|
2607
|
+
- Duration: {{sprintDuration}} days
|
|
2608
|
+
- Target Velocity: {{targetVelocity}} points
|
|
2609
|
+
- Max Stories: {{maxStoriesPerSprint}}
|
|
2610
|
+
- Buffer: {{bufferPercentage}}%
|
|
2611
|
+
|
|
2612
|
+
Stories Available:
|
|
2613
|
+
{{availableStories}}
|
|
2614
|
+
|
|
2615
|
+
Select stories for Sprint 1 following these rules:
|
|
2616
|
+
1. Prioritize foundation/infrastructure stories
|
|
2617
|
+
2. Respect dependencies
|
|
2618
|
+
3. Stay within velocity target (with buffer)
|
|
2619
|
+
4. Ensure a coherent sprint goal
|
|
2620
|
+
|
|
2621
|
+
Respond in JSON format:
|
|
2622
|
+
{
|
|
2623
|
+
"sprint": {
|
|
2624
|
+
"id": "sprint_001",
|
|
2625
|
+
"name": "Sprint 1: Foundation",
|
|
2626
|
+
"goal": "string",
|
|
2627
|
+
"stories": ["story_id"],
|
|
2628
|
+
"plannedPoints": number,
|
|
2629
|
+
"status": "planning"
|
|
2630
|
+
},
|
|
2631
|
+
"reasoning": "string"
|
|
2632
|
+
}`;
|
|
2633
|
+
function fillPrompt2(template, variables) {
|
|
2634
|
+
let result = template;
|
|
2635
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
2636
|
+
const placeholder = `{{${key}}}`;
|
|
2637
|
+
const stringValue = typeof value === "string" ? value : typeof value === "number" ? String(value) : JSON.stringify(value, null, 2);
|
|
2638
|
+
result = result.replaceAll(placeholder, stringValue);
|
|
2639
|
+
}
|
|
2640
|
+
return result;
|
|
2641
|
+
}
|
|
2642
|
+
function parseOverview(data) {
|
|
2643
|
+
return {
|
|
2644
|
+
pattern: data?.pattern || "layered",
|
|
2645
|
+
description: data?.description || "System architecture",
|
|
2646
|
+
principles: data?.principles || [],
|
|
2647
|
+
qualityAttributes: (data?.qualityAttributes || []).map((qa) => ({
|
|
2648
|
+
name: qa.name || "",
|
|
2649
|
+
description: qa.description || "",
|
|
2650
|
+
priority: qa.priority || "medium",
|
|
2651
|
+
tradeoffs: qa.tradeoffs
|
|
2652
|
+
}))
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
function parseComponents(data) {
|
|
2656
|
+
return data.map((c) => ({
|
|
2657
|
+
id: c.id || randomUUID(),
|
|
2658
|
+
name: c.name || "Component",
|
|
2659
|
+
type: c.type || "service",
|
|
2660
|
+
description: c.description || "",
|
|
2661
|
+
responsibilities: c.responsibilities || [],
|
|
2662
|
+
technology: c.technology,
|
|
2663
|
+
layer: c.layer,
|
|
2664
|
+
dependencies: c.dependencies || []
|
|
2665
|
+
}));
|
|
2666
|
+
}
|
|
2667
|
+
function parseRelationships(data) {
|
|
2668
|
+
return data.map((r) => ({
|
|
2669
|
+
from: r.from || "",
|
|
2670
|
+
to: r.to || "",
|
|
2671
|
+
type: r.type || "uses",
|
|
2672
|
+
description: r.description
|
|
2673
|
+
}));
|
|
2674
|
+
}
|
|
2675
|
+
function parseDataModels(data) {
|
|
2676
|
+
return data.map((dm) => ({
|
|
2677
|
+
name: dm.name || "Model",
|
|
2678
|
+
description: dm.description || "",
|
|
2679
|
+
fields: (dm.fields || []).map((f) => ({
|
|
2680
|
+
name: f.name || "",
|
|
2681
|
+
type: f.type || "string",
|
|
2682
|
+
required: f.required ?? true,
|
|
2683
|
+
description: f.description
|
|
2684
|
+
})),
|
|
2685
|
+
relationships: (dm.relationships || []).map((r) => ({
|
|
2686
|
+
type: r.type || "one_to_many",
|
|
2687
|
+
target: r.target || "",
|
|
2688
|
+
description: r.description
|
|
2689
|
+
}))
|
|
2690
|
+
}));
|
|
2691
|
+
}
|
|
2692
|
+
function parseIntegrations(data) {
|
|
2693
|
+
return data.map((i) => ({
|
|
2694
|
+
name: i.name || "Integration",
|
|
2695
|
+
type: i.type || "rest_api",
|
|
2696
|
+
description: i.description || "",
|
|
2697
|
+
endpoint: i.endpoint,
|
|
2698
|
+
authentication: i.authentication
|
|
2699
|
+
}));
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// src/phases/orchestrate/architecture-markdown.ts
|
|
2703
|
+
function generateArchitectureMarkdown(doc) {
|
|
2704
|
+
const sections = [];
|
|
2705
|
+
sections.push("# Architecture Document");
|
|
2706
|
+
sections.push("");
|
|
2707
|
+
sections.push(`> Generated: ${doc.generatedAt.toISOString()} | Version: ${doc.version}`);
|
|
2708
|
+
sections.push("");
|
|
2709
|
+
sections.push("## Overview");
|
|
2710
|
+
sections.push("");
|
|
2711
|
+
sections.push(`**Pattern:** ${doc.overview.pattern}`);
|
|
2712
|
+
sections.push("");
|
|
2713
|
+
sections.push(doc.overview.description);
|
|
2714
|
+
sections.push("");
|
|
2715
|
+
if (doc.overview.principles.length > 0) {
|
|
2716
|
+
sections.push("### Design Principles");
|
|
2717
|
+
sections.push("");
|
|
2718
|
+
for (const principle of doc.overview.principles) {
|
|
2719
|
+
sections.push(`- ${principle}`);
|
|
2720
|
+
}
|
|
2721
|
+
sections.push("");
|
|
2722
|
+
}
|
|
2723
|
+
if (doc.overview.qualityAttributes.length > 0) {
|
|
2724
|
+
sections.push("### Quality Attributes");
|
|
2725
|
+
sections.push("");
|
|
2726
|
+
sections.push("| Attribute | Priority | Description |");
|
|
2727
|
+
sections.push("|-----------|----------|-------------|");
|
|
2728
|
+
for (const qa of doc.overview.qualityAttributes) {
|
|
2729
|
+
sections.push(`| ${qa.name} | ${qa.priority} | ${qa.description} |`);
|
|
2730
|
+
}
|
|
2731
|
+
sections.push("");
|
|
2732
|
+
}
|
|
2733
|
+
sections.push("## Components");
|
|
2734
|
+
sections.push("");
|
|
2735
|
+
for (const component of doc.components) {
|
|
2736
|
+
sections.push(`### ${component.name}`);
|
|
2737
|
+
sections.push("");
|
|
2738
|
+
sections.push(`**Type:** ${component.type}`);
|
|
2739
|
+
if (component.layer) sections.push(`**Layer:** ${component.layer}`);
|
|
2740
|
+
if (component.technology) sections.push(`**Technology:** ${component.technology}`);
|
|
2741
|
+
sections.push("");
|
|
2742
|
+
sections.push(component.description);
|
|
2743
|
+
sections.push("");
|
|
2744
|
+
if (component.responsibilities.length > 0) {
|
|
2745
|
+
sections.push("**Responsibilities:**");
|
|
2746
|
+
for (const resp of component.responsibilities) {
|
|
2747
|
+
sections.push(`- ${resp}`);
|
|
2748
|
+
}
|
|
2749
|
+
sections.push("");
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
if (doc.dataModels.length > 0) {
|
|
2753
|
+
sections.push("## Data Models");
|
|
2754
|
+
sections.push("");
|
|
2755
|
+
for (const model of doc.dataModels) {
|
|
2756
|
+
sections.push(`### ${model.name}`);
|
|
2757
|
+
sections.push("");
|
|
2758
|
+
sections.push(model.description);
|
|
2759
|
+
sections.push("");
|
|
2760
|
+
sections.push("| Field | Type | Required |");
|
|
2761
|
+
sections.push("|-------|------|----------|");
|
|
2762
|
+
for (const field of model.fields) {
|
|
2763
|
+
sections.push(`| ${field.name} | ${field.type} | ${field.required ? "Yes" : "No"} |`);
|
|
2764
|
+
}
|
|
2765
|
+
sections.push("");
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
if (doc.integrations.length > 0) {
|
|
2769
|
+
sections.push("## Integrations");
|
|
2770
|
+
sections.push("");
|
|
2771
|
+
for (const integration of doc.integrations) {
|
|
2772
|
+
sections.push(`### ${integration.name}`);
|
|
2773
|
+
sections.push("");
|
|
2774
|
+
sections.push(`**Type:** ${integration.type}`);
|
|
2775
|
+
sections.push(integration.description);
|
|
2776
|
+
sections.push("");
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
if (doc.diagrams.length > 0) {
|
|
2780
|
+
sections.push("## Diagrams");
|
|
2781
|
+
sections.push("");
|
|
2782
|
+
for (const diagram of doc.diagrams) {
|
|
2783
|
+
sections.push(`### ${diagram.title}`);
|
|
2784
|
+
sections.push("");
|
|
2785
|
+
sections.push(diagram.description);
|
|
2786
|
+
sections.push("");
|
|
2787
|
+
sections.push("```mermaid");
|
|
2788
|
+
sections.push(diagram.mermaid);
|
|
2789
|
+
sections.push("```");
|
|
2790
|
+
sections.push("");
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
sections.push("---");
|
|
2794
|
+
sections.push("");
|
|
2795
|
+
sections.push("*Generated by Corbat-Coco*");
|
|
2796
|
+
return sections.join("\n");
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
// src/phases/orchestrate/architecture.ts
|
|
2800
|
+
var ArchitectureGenerator = class {
|
|
2801
|
+
llm;
|
|
2802
|
+
config;
|
|
2803
|
+
constructor(llm, config) {
|
|
2804
|
+
this.llm = llm;
|
|
2805
|
+
this.config = config;
|
|
2806
|
+
}
|
|
2807
|
+
/**
|
|
2808
|
+
* Generate architecture from specification
|
|
2809
|
+
*/
|
|
2810
|
+
async generate(specification) {
|
|
2811
|
+
const baseArchitecture = await this.generateBaseArchitecture(specification);
|
|
2812
|
+
const diagrams = [];
|
|
2813
|
+
if (this.config.generateC4Diagrams) {
|
|
2814
|
+
const c4Diagrams = await this.generateC4Diagrams(baseArchitecture);
|
|
2815
|
+
diagrams.push(...c4Diagrams);
|
|
2816
|
+
}
|
|
2817
|
+
if (this.config.generateSequenceDiagrams) {
|
|
2818
|
+
const seqDiagrams = await this.generateSequenceDiagrams(
|
|
2819
|
+
baseArchitecture,
|
|
2820
|
+
specification
|
|
2821
|
+
);
|
|
2822
|
+
diagrams.push(...seqDiagrams);
|
|
2823
|
+
}
|
|
2824
|
+
return {
|
|
2825
|
+
...baseArchitecture,
|
|
2826
|
+
diagrams
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2829
|
+
/**
|
|
2830
|
+
* Generate base architecture
|
|
2831
|
+
*/
|
|
2832
|
+
async generateBaseArchitecture(specification) {
|
|
2833
|
+
const prompt = fillPrompt2(GENERATE_ARCHITECTURE_PROMPT, {
|
|
2834
|
+
specification: JSON.stringify(specification.overview),
|
|
2835
|
+
techStack: JSON.stringify(specification.technical.stack),
|
|
2836
|
+
functionalCount: specification.requirements.functional.length,
|
|
2837
|
+
nonFunctionalCount: specification.requirements.nonFunctional.length,
|
|
2838
|
+
constraintCount: specification.requirements.constraints.length
|
|
2839
|
+
});
|
|
2840
|
+
const response = await this.llm.chat([
|
|
2841
|
+
{ role: "system", content: ARCHITECT_SYSTEM_PROMPT },
|
|
2842
|
+
{ role: "user", content: prompt }
|
|
2843
|
+
]);
|
|
2844
|
+
try {
|
|
2845
|
+
const content = response.content;
|
|
2846
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
2847
|
+
if (!jsonMatch) {
|
|
2848
|
+
throw new Error("No JSON found in response");
|
|
2849
|
+
}
|
|
2850
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
2851
|
+
return {
|
|
2852
|
+
version: "1.0.0",
|
|
2853
|
+
generatedAt: /* @__PURE__ */ new Date(),
|
|
2854
|
+
overview: parseOverview(parsed.overview),
|
|
2855
|
+
components: parseComponents(parsed.components || []),
|
|
2856
|
+
relationships: parseRelationships(parsed.relationships || []),
|
|
2857
|
+
dataModels: parseDataModels(parsed.dataModels || []),
|
|
2858
|
+
integrations: parseIntegrations(parsed.integrations || []),
|
|
2859
|
+
diagrams: []
|
|
2860
|
+
};
|
|
2861
|
+
} catch {
|
|
2862
|
+
throw new PhaseError("Failed to generate architecture", { phase: "orchestrate" });
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Generate C4 diagrams
|
|
2867
|
+
*/
|
|
2868
|
+
async generateC4Diagrams(architecture) {
|
|
2869
|
+
const prompt = fillPrompt2(GENERATE_C4_DIAGRAMS_PROMPT, {
|
|
2870
|
+
architecture: JSON.stringify({
|
|
2871
|
+
overview: architecture.overview,
|
|
2872
|
+
components: architecture.components,
|
|
2873
|
+
relationships: architecture.relationships
|
|
2874
|
+
})
|
|
2875
|
+
});
|
|
2876
|
+
try {
|
|
2877
|
+
const response = await this.llm.chat([
|
|
2878
|
+
{ role: "system", content: ARCHITECT_SYSTEM_PROMPT },
|
|
2879
|
+
{ role: "user", content: prompt }
|
|
2880
|
+
]);
|
|
2881
|
+
const content = response.content;
|
|
2882
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
2883
|
+
if (!jsonMatch) {
|
|
2884
|
+
return this.generateFallbackC4Diagrams(architecture);
|
|
2885
|
+
}
|
|
2886
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
2887
|
+
return (parsed.diagrams || []).map((d) => ({
|
|
2888
|
+
id: d.id || randomUUID(),
|
|
2889
|
+
type: d.type || "c4_context",
|
|
2890
|
+
title: d.title || "Diagram",
|
|
2891
|
+
description: d.description || "",
|
|
2892
|
+
mermaid: d.mermaid || ""
|
|
2893
|
+
}));
|
|
2894
|
+
} catch {
|
|
2895
|
+
return this.generateFallbackC4Diagrams(architecture);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
/**
|
|
2899
|
+
* Generate sequence diagrams
|
|
2900
|
+
*/
|
|
2901
|
+
async generateSequenceDiagrams(architecture, specification) {
|
|
2902
|
+
const prompt = fillPrompt2(GENERATE_SEQUENCE_DIAGRAMS_PROMPT, {
|
|
2903
|
+
architecture: JSON.stringify({
|
|
2904
|
+
overview: architecture.overview,
|
|
2905
|
+
components: architecture.components
|
|
2906
|
+
}),
|
|
2907
|
+
functionalRequirements: JSON.stringify(
|
|
2908
|
+
specification.requirements.functional.slice(0, 5)
|
|
2909
|
+
)
|
|
2910
|
+
});
|
|
2911
|
+
try {
|
|
2912
|
+
const response = await this.llm.chat([
|
|
2913
|
+
{ role: "system", content: ARCHITECT_SYSTEM_PROMPT },
|
|
2914
|
+
{ role: "user", content: prompt }
|
|
2915
|
+
]);
|
|
2916
|
+
const content = response.content;
|
|
2917
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
2918
|
+
if (!jsonMatch) {
|
|
2919
|
+
return [];
|
|
2920
|
+
}
|
|
2921
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
2922
|
+
return (parsed.diagrams || []).map((d) => ({
|
|
2923
|
+
id: d.id || randomUUID(),
|
|
2924
|
+
type: "sequence",
|
|
2925
|
+
title: d.title || "Sequence Diagram",
|
|
2926
|
+
description: d.description || "",
|
|
2927
|
+
mermaid: d.mermaid || ""
|
|
2928
|
+
}));
|
|
2929
|
+
} catch {
|
|
2930
|
+
return [];
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
/**
|
|
2934
|
+
* Generate fallback C4 diagrams if LLM fails
|
|
2935
|
+
*/
|
|
2936
|
+
generateFallbackC4Diagrams(architecture) {
|
|
2937
|
+
const diagrams = [];
|
|
2938
|
+
let contextMermaid = "C4Context\n";
|
|
2939
|
+
contextMermaid += ` title System Context Diagram
|
|
2940
|
+
`;
|
|
2941
|
+
contextMermaid += ` Person(user, "User", "Primary system user")
|
|
2942
|
+
`;
|
|
2943
|
+
contextMermaid += ` System(system, "${architecture.overview.description.substring(0, 30)}...", "The system")
|
|
2944
|
+
`;
|
|
2945
|
+
for (const integration of architecture.integrations) {
|
|
2946
|
+
contextMermaid += ` System_Ext(${integration.name.replace(/\s/g, "_")}, "${integration.name}", "${integration.type}")
|
|
2947
|
+
`;
|
|
2948
|
+
}
|
|
2949
|
+
contextMermaid += ` Rel(user, system, "Uses")
|
|
2950
|
+
`;
|
|
2951
|
+
diagrams.push({
|
|
2952
|
+
id: "c4_context",
|
|
2953
|
+
type: "c4_context",
|
|
2954
|
+
title: "System Context Diagram",
|
|
2955
|
+
description: "High-level view of the system and its environment",
|
|
2956
|
+
mermaid: contextMermaid
|
|
2957
|
+
});
|
|
2958
|
+
let containerMermaid = "C4Container\n";
|
|
2959
|
+
containerMermaid += ` title Container Diagram
|
|
2960
|
+
`;
|
|
2961
|
+
const layers = new Set(architecture.components.map((c) => c.layer).filter(Boolean));
|
|
2962
|
+
for (const layer of layers) {
|
|
2963
|
+
containerMermaid += ` Container_Boundary(${layer?.replace(/\s/g, "_")}, "${layer}") {
|
|
2964
|
+
`;
|
|
2965
|
+
for (const component of architecture.components.filter((c) => c.layer === layer)) {
|
|
2966
|
+
containerMermaid += ` Container(${component.id}, "${component.name}", "${component.technology || ""}")
|
|
2967
|
+
`;
|
|
2968
|
+
}
|
|
2969
|
+
containerMermaid += ` }
|
|
2970
|
+
`;
|
|
2971
|
+
}
|
|
2972
|
+
diagrams.push({
|
|
2973
|
+
id: "c4_container",
|
|
2974
|
+
type: "c4_container",
|
|
2975
|
+
title: "Container Diagram",
|
|
2976
|
+
description: "Shows the major containers/deployable units",
|
|
2977
|
+
mermaid: containerMermaid
|
|
2978
|
+
});
|
|
2979
|
+
return diagrams;
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2982
|
+
var ADRGenerator = class {
|
|
2983
|
+
llm;
|
|
2984
|
+
config;
|
|
2985
|
+
constructor(llm, config) {
|
|
2986
|
+
this.llm = llm;
|
|
2987
|
+
this.config = config;
|
|
2988
|
+
}
|
|
2989
|
+
/**
|
|
2990
|
+
* Generate ADRs from architecture and specification
|
|
2991
|
+
*/
|
|
2992
|
+
async generate(architecture, specification) {
|
|
2993
|
+
const prompt = fillPrompt2(GENERATE_ADRS_PROMPT, {
|
|
2994
|
+
architecture: JSON.stringify({
|
|
2995
|
+
overview: architecture.overview,
|
|
2996
|
+
components: architecture.components.map((c) => ({
|
|
2997
|
+
name: c.name,
|
|
2998
|
+
type: c.type,
|
|
2999
|
+
technology: c.technology
|
|
3000
|
+
}))
|
|
3001
|
+
}),
|
|
3002
|
+
techStack: JSON.stringify(specification.technical.stack),
|
|
3003
|
+
maxADRs: this.config.maxADRs
|
|
3004
|
+
});
|
|
3005
|
+
const response = await this.llm.chat([
|
|
3006
|
+
{ role: "system", content: ARCHITECT_SYSTEM_PROMPT },
|
|
3007
|
+
{ role: "user", content: prompt }
|
|
3008
|
+
]);
|
|
3009
|
+
try {
|
|
3010
|
+
const content = response.content;
|
|
3011
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
3012
|
+
if (!jsonMatch) {
|
|
3013
|
+
throw new Error("No JSON found in response");
|
|
3014
|
+
}
|
|
3015
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
3016
|
+
return (parsed.adrs || []).map((adr, index) => this.parseADR(adr, index));
|
|
3017
|
+
} catch {
|
|
3018
|
+
throw new PhaseError("Failed to generate ADRs", {
|
|
3019
|
+
phase: "orchestrate"
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* Parse a single ADR from LLM response
|
|
3025
|
+
*/
|
|
3026
|
+
parseADR(data, index) {
|
|
3027
|
+
return {
|
|
3028
|
+
id: randomUUID(),
|
|
3029
|
+
number: data.number || index + 1,
|
|
3030
|
+
title: data.title || `Decision ${index + 1}`,
|
|
3031
|
+
date: /* @__PURE__ */ new Date(),
|
|
3032
|
+
status: data.status || "accepted",
|
|
3033
|
+
context: data.context || "",
|
|
3034
|
+
decision: data.decision || "",
|
|
3035
|
+
consequences: {
|
|
3036
|
+
positive: data.consequences?.positive || [],
|
|
3037
|
+
negative: data.consequences?.negative || [],
|
|
3038
|
+
neutral: data.consequences?.neutral
|
|
3039
|
+
},
|
|
3040
|
+
alternatives: (data.alternatives || []).map((alt) => ({
|
|
3041
|
+
option: alt.option || "",
|
|
3042
|
+
pros: alt.pros || [],
|
|
3043
|
+
cons: alt.cons || [],
|
|
3044
|
+
reason: alt.reason || ""
|
|
3045
|
+
})),
|
|
3046
|
+
references: data.references
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
};
|
|
3050
|
+
function generateADRMarkdown(adr) {
|
|
3051
|
+
const sections = [];
|
|
3052
|
+
const paddedNumber = String(adr.number).padStart(3, "0");
|
|
3053
|
+
sections.push(`# ADR ${paddedNumber}: ${adr.title}`);
|
|
3054
|
+
sections.push("");
|
|
3055
|
+
sections.push(`**Date:** ${adr.date.toISOString().split("T")[0]}`);
|
|
3056
|
+
sections.push(`**Status:** ${adr.status}`);
|
|
3057
|
+
sections.push("");
|
|
3058
|
+
sections.push("## Context");
|
|
3059
|
+
sections.push("");
|
|
3060
|
+
sections.push(adr.context);
|
|
3061
|
+
sections.push("");
|
|
3062
|
+
sections.push("## Decision");
|
|
3063
|
+
sections.push("");
|
|
3064
|
+
sections.push(adr.decision);
|
|
3065
|
+
sections.push("");
|
|
3066
|
+
sections.push("## Consequences");
|
|
3067
|
+
sections.push("");
|
|
3068
|
+
if (adr.consequences.positive.length > 0) {
|
|
3069
|
+
sections.push("### Positive");
|
|
3070
|
+
sections.push("");
|
|
3071
|
+
for (const consequence of adr.consequences.positive) {
|
|
3072
|
+
sections.push(`- \u2705 ${consequence}`);
|
|
3073
|
+
}
|
|
3074
|
+
sections.push("");
|
|
3075
|
+
}
|
|
3076
|
+
if (adr.consequences.negative.length > 0) {
|
|
3077
|
+
sections.push("### Negative");
|
|
3078
|
+
sections.push("");
|
|
3079
|
+
for (const consequence of adr.consequences.negative) {
|
|
3080
|
+
sections.push(`- \u26A0\uFE0F ${consequence}`);
|
|
3081
|
+
}
|
|
3082
|
+
sections.push("");
|
|
3083
|
+
}
|
|
3084
|
+
if (adr.consequences.neutral && adr.consequences.neutral.length > 0) {
|
|
3085
|
+
sections.push("### Neutral");
|
|
3086
|
+
sections.push("");
|
|
3087
|
+
for (const consequence of adr.consequences.neutral) {
|
|
3088
|
+
sections.push(`- ${consequence}`);
|
|
3089
|
+
}
|
|
3090
|
+
sections.push("");
|
|
3091
|
+
}
|
|
3092
|
+
if (adr.alternatives && adr.alternatives.length > 0) {
|
|
3093
|
+
sections.push("## Alternatives Considered");
|
|
3094
|
+
sections.push("");
|
|
3095
|
+
for (const alt of adr.alternatives) {
|
|
3096
|
+
sections.push(`### ${alt.option}`);
|
|
3097
|
+
sections.push("");
|
|
3098
|
+
if (alt.pros.length > 0) {
|
|
3099
|
+
sections.push("**Pros:**");
|
|
3100
|
+
for (const pro of alt.pros) {
|
|
3101
|
+
sections.push(`- ${pro}`);
|
|
3102
|
+
}
|
|
3103
|
+
sections.push("");
|
|
3104
|
+
}
|
|
3105
|
+
if (alt.cons.length > 0) {
|
|
3106
|
+
sections.push("**Cons:**");
|
|
3107
|
+
for (const con of alt.cons) {
|
|
3108
|
+
sections.push(`- ${con}`);
|
|
3109
|
+
}
|
|
3110
|
+
sections.push("");
|
|
3111
|
+
}
|
|
3112
|
+
sections.push(`**Why not chosen:** ${alt.reason}`);
|
|
3113
|
+
sections.push("");
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
if (adr.references && adr.references.length > 0) {
|
|
3117
|
+
sections.push("## References");
|
|
3118
|
+
sections.push("");
|
|
3119
|
+
for (const ref of adr.references) {
|
|
3120
|
+
sections.push(`- ${ref}`);
|
|
3121
|
+
}
|
|
3122
|
+
sections.push("");
|
|
3123
|
+
}
|
|
3124
|
+
return sections.join("\n");
|
|
3125
|
+
}
|
|
3126
|
+
function generateADRIndexMarkdown(adrs) {
|
|
3127
|
+
const sections = [];
|
|
3128
|
+
sections.push("# Architecture Decision Records");
|
|
3129
|
+
sections.push("");
|
|
3130
|
+
sections.push("This directory contains all Architecture Decision Records (ADRs) for this project.");
|
|
3131
|
+
sections.push("");
|
|
3132
|
+
sections.push("## Index");
|
|
3133
|
+
sections.push("");
|
|
3134
|
+
sections.push("| # | Title | Status | Date |");
|
|
3135
|
+
sections.push("|---|-------|--------|------|");
|
|
3136
|
+
for (const adr of adrs) {
|
|
3137
|
+
const paddedNumber = String(adr.number).padStart(3, "0");
|
|
3138
|
+
const filename = `${paddedNumber}-${slugify(adr.title)}.md`;
|
|
3139
|
+
const dateStr = adr.date.toISOString().split("T")[0];
|
|
3140
|
+
sections.push(
|
|
3141
|
+
`| ${adr.number} | [${adr.title}](./${filename}) | ${adr.status} | ${dateStr} |`
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
sections.push("");
|
|
3145
|
+
sections.push("## About ADRs");
|
|
3146
|
+
sections.push("");
|
|
3147
|
+
sections.push("ADRs are short documents that capture important architectural decisions.");
|
|
3148
|
+
sections.push("Each ADR describes:");
|
|
3149
|
+
sections.push("- The context and problem being addressed");
|
|
3150
|
+
sections.push("- The decision made");
|
|
3151
|
+
sections.push("- The consequences (positive and negative)");
|
|
3152
|
+
sections.push("- Alternatives considered");
|
|
3153
|
+
sections.push("");
|
|
3154
|
+
sections.push("For more information, see [ADR GitHub](https://adr.github.io/).");
|
|
3155
|
+
return sections.join("\n");
|
|
3156
|
+
}
|
|
3157
|
+
function getADRFilename(adr) {
|
|
3158
|
+
const paddedNumber = String(adr.number).padStart(3, "0");
|
|
3159
|
+
return `${paddedNumber}-${slugify(adr.title)}.md`;
|
|
3160
|
+
}
|
|
3161
|
+
function slugify(str) {
|
|
3162
|
+
return str.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3163
|
+
}
|
|
3164
|
+
var BacklogGenerator = class {
|
|
3165
|
+
llm;
|
|
3166
|
+
config;
|
|
3167
|
+
constructor(llm, config) {
|
|
3168
|
+
this.llm = llm;
|
|
3169
|
+
this.config = config;
|
|
3170
|
+
}
|
|
3171
|
+
/**
|
|
3172
|
+
* Generate complete backlog from architecture and specification
|
|
3173
|
+
*/
|
|
3174
|
+
async generate(architecture, specification) {
|
|
3175
|
+
const prompt = fillPrompt2(GENERATE_BACKLOG_PROMPT, {
|
|
3176
|
+
architecture: JSON.stringify({
|
|
3177
|
+
pattern: architecture.overview.pattern,
|
|
3178
|
+
components: architecture.components.map((c) => c.name),
|
|
3179
|
+
dataModels: architecture.dataModels.map((d) => d.name)
|
|
3180
|
+
}),
|
|
3181
|
+
requirements: JSON.stringify({
|
|
3182
|
+
functional: specification.requirements.functional.map((r) => ({
|
|
3183
|
+
title: r.title,
|
|
3184
|
+
priority: r.priority
|
|
3185
|
+
})),
|
|
3186
|
+
nonFunctional: specification.requirements.nonFunctional.map((r) => r.title)
|
|
3187
|
+
}),
|
|
3188
|
+
breakdownStrategy: this.config.breakdownStrategy
|
|
3189
|
+
});
|
|
3190
|
+
const response = await this.llm.chat([
|
|
3191
|
+
{ role: "system", content: ARCHITECT_SYSTEM_PROMPT },
|
|
3192
|
+
{ role: "user", content: prompt }
|
|
3193
|
+
]);
|
|
3194
|
+
try {
|
|
3195
|
+
const content = response.content;
|
|
3196
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
3197
|
+
if (!jsonMatch) {
|
|
3198
|
+
throw new Error("No JSON found in response");
|
|
3199
|
+
}
|
|
3200
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
3201
|
+
const epics = this.parseEpics(parsed.epics || []);
|
|
3202
|
+
const stories = this.parseStories(parsed.stories || []);
|
|
3203
|
+
const tasks = this.parseTasks(parsed.tasks || []);
|
|
3204
|
+
const totalPoints = stories.reduce((sum, s) => sum + s.points, 0);
|
|
3205
|
+
const estimatedSprints = parsed.estimatedSprints || Math.ceil(totalPoints / this.config.sprint.targetVelocity);
|
|
3206
|
+
return {
|
|
3207
|
+
backlog: {
|
|
3208
|
+
epics,
|
|
3209
|
+
stories,
|
|
3210
|
+
tasks,
|
|
3211
|
+
currentSprint: null,
|
|
3212
|
+
completedSprints: []
|
|
3213
|
+
},
|
|
3214
|
+
estimatedSprints,
|
|
3215
|
+
estimatedVelocity: this.config.sprint.targetVelocity,
|
|
3216
|
+
warnings: parsed.warnings || []
|
|
3217
|
+
};
|
|
3218
|
+
} catch {
|
|
3219
|
+
throw new PhaseError("Failed to generate backlog", { phase: "orchestrate" });
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
/**
|
|
3223
|
+
* Plan the first sprint from the backlog
|
|
3224
|
+
*/
|
|
3225
|
+
async planFirstSprint(backlog) {
|
|
3226
|
+
const availableStories = backlog.stories.filter(
|
|
3227
|
+
(s) => s.status === "backlog" || s.status === "ready"
|
|
3228
|
+
);
|
|
3229
|
+
const readyStories = availableStories.filter((story) => {
|
|
3230
|
+
const epic = backlog.epics.find((e) => e.id === story.epicId);
|
|
3231
|
+
if (!epic) return true;
|
|
3232
|
+
const depsMet = epic.dependencies.every((depId) => {
|
|
3233
|
+
const depEpic = backlog.epics.find((e) => e.id === depId);
|
|
3234
|
+
return depEpic?.status === "done";
|
|
3235
|
+
});
|
|
3236
|
+
return depsMet || epic.dependencies.length === 0;
|
|
3237
|
+
});
|
|
3238
|
+
const prompt = fillPrompt2(PLAN_SPRINT_PROMPT, {
|
|
3239
|
+
epicCount: backlog.epics.length,
|
|
3240
|
+
storyCount: backlog.stories.length,
|
|
3241
|
+
taskCount: backlog.tasks.length,
|
|
3242
|
+
sprintDuration: this.config.sprint.sprintDuration,
|
|
3243
|
+
targetVelocity: this.config.sprint.targetVelocity,
|
|
3244
|
+
maxStoriesPerSprint: this.config.sprint.maxStoriesPerSprint,
|
|
3245
|
+
bufferPercentage: this.config.sprint.bufferPercentage,
|
|
3246
|
+
availableStories: JSON.stringify(
|
|
3247
|
+
readyStories.slice(0, 20).map((s) => ({
|
|
3248
|
+
id: s.id,
|
|
3249
|
+
title: s.title,
|
|
3250
|
+
points: s.points,
|
|
3251
|
+
epicId: s.epicId
|
|
3252
|
+
}))
|
|
3253
|
+
)
|
|
3254
|
+
});
|
|
3255
|
+
try {
|
|
3256
|
+
const response = await this.llm.chat([
|
|
3257
|
+
{ role: "system", content: ARCHITECT_SYSTEM_PROMPT },
|
|
3258
|
+
{ role: "user", content: prompt }
|
|
3259
|
+
]);
|
|
3260
|
+
const content = response.content;
|
|
3261
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
3262
|
+
if (jsonMatch) {
|
|
3263
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
3264
|
+
if (parsed.sprint) {
|
|
3265
|
+
return {
|
|
3266
|
+
id: parsed.sprint.id || `sprint_${randomUUID().substring(0, 8)}`,
|
|
3267
|
+
name: parsed.sprint.name || "Sprint 1",
|
|
3268
|
+
goal: parsed.sprint.goal || "Initial foundation",
|
|
3269
|
+
startDate: /* @__PURE__ */ new Date(),
|
|
3270
|
+
stories: parsed.sprint.stories || [],
|
|
3271
|
+
status: "planning"
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
return this.autoSelectSprint(backlog, readyStories);
|
|
3276
|
+
} catch {
|
|
3277
|
+
return this.autoSelectSprint(backlog, readyStories);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
3281
|
+
* Auto-select stories for a sprint based on priority and velocity
|
|
3282
|
+
*/
|
|
3283
|
+
autoSelectSprint(backlog, availableStories) {
|
|
3284
|
+
const targetVelocity = this.config.sprint.targetVelocity;
|
|
3285
|
+
const bufferFactor = 1 - this.config.sprint.bufferPercentage / 100;
|
|
3286
|
+
const maxPoints = targetVelocity * bufferFactor;
|
|
3287
|
+
const sorted = [...availableStories].sort((a, b) => {
|
|
3288
|
+
const epicA = backlog.epics.find((e) => e.id === a.epicId);
|
|
3289
|
+
const epicB = backlog.epics.find((e) => e.id === b.epicId);
|
|
3290
|
+
const priorityDiff = (epicA?.priority || 5) - (epicB?.priority || 5);
|
|
3291
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
3292
|
+
return a.points - b.points;
|
|
3293
|
+
});
|
|
3294
|
+
const selectedStories = [];
|
|
3295
|
+
let currentPoints = 0;
|
|
3296
|
+
for (const story of sorted) {
|
|
3297
|
+
if (selectedStories.length >= this.config.sprint.maxStoriesPerSprint) break;
|
|
3298
|
+
if (currentPoints + story.points > maxPoints) continue;
|
|
3299
|
+
selectedStories.push(story.id);
|
|
3300
|
+
currentPoints += story.points;
|
|
3301
|
+
}
|
|
3302
|
+
return {
|
|
3303
|
+
id: `sprint_${randomUUID().substring(0, 8)}`,
|
|
3304
|
+
name: "Sprint 1: Foundation",
|
|
3305
|
+
goal: "Set up project foundation and core infrastructure",
|
|
3306
|
+
startDate: /* @__PURE__ */ new Date(),
|
|
3307
|
+
stories: selectedStories,
|
|
3308
|
+
status: "planning"
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
// Parse helpers
|
|
3312
|
+
parseEpics(data) {
|
|
3313
|
+
return data.map((e) => ({
|
|
3314
|
+
id: e.id || `epic_${randomUUID().substring(0, 8)}`,
|
|
3315
|
+
title: e.title || "Epic",
|
|
3316
|
+
description: e.description || "",
|
|
3317
|
+
stories: [],
|
|
3318
|
+
priority: e.priority || 3,
|
|
3319
|
+
dependencies: e.dependencies || [],
|
|
3320
|
+
status: e.status || "planned"
|
|
3321
|
+
}));
|
|
3322
|
+
}
|
|
3323
|
+
parseStories(data) {
|
|
3324
|
+
return data.map((s) => ({
|
|
3325
|
+
id: s.id || `story_${randomUUID().substring(0, 8)}`,
|
|
3326
|
+
epicId: s.epicId || "",
|
|
3327
|
+
title: s.title || "Story",
|
|
3328
|
+
asA: s.asA || "user",
|
|
3329
|
+
iWant: s.iWant || "",
|
|
3330
|
+
soThat: s.soThat || "",
|
|
3331
|
+
acceptanceCriteria: s.acceptanceCriteria || [],
|
|
3332
|
+
tasks: [],
|
|
3333
|
+
points: this.normalizePoints(s.points),
|
|
3334
|
+
status: s.status || "backlog"
|
|
3335
|
+
}));
|
|
3336
|
+
}
|
|
3337
|
+
parseTasks(data) {
|
|
3338
|
+
return data.map((t) => ({
|
|
3339
|
+
id: t.id || `task_${randomUUID().substring(0, 8)}`,
|
|
3340
|
+
storyId: t.storyId || "",
|
|
3341
|
+
title: t.title || "Task",
|
|
3342
|
+
description: t.description || "",
|
|
3343
|
+
type: t.type || "feature",
|
|
3344
|
+
files: t.files || [],
|
|
3345
|
+
dependencies: t.dependencies || [],
|
|
3346
|
+
estimatedComplexity: t.estimatedComplexity || "simple",
|
|
3347
|
+
status: "pending"
|
|
3348
|
+
}));
|
|
3349
|
+
}
|
|
3350
|
+
normalizePoints(value) {
|
|
3351
|
+
const fibonacciPoints = [1, 2, 3, 5, 8, 13];
|
|
3352
|
+
if (!value) return 3;
|
|
3353
|
+
const closest = fibonacciPoints.reduce(
|
|
3354
|
+
(prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev
|
|
3355
|
+
);
|
|
3356
|
+
return closest;
|
|
3357
|
+
}
|
|
3358
|
+
};
|
|
3359
|
+
function generateBacklogMarkdown(backlog) {
|
|
3360
|
+
const sections = [];
|
|
3361
|
+
sections.push("# Project Backlog");
|
|
3362
|
+
sections.push("");
|
|
3363
|
+
sections.push("## Summary");
|
|
3364
|
+
sections.push("");
|
|
3365
|
+
sections.push(`- **Epics:** ${backlog.epics.length}`);
|
|
3366
|
+
sections.push(`- **Stories:** ${backlog.stories.length}`);
|
|
3367
|
+
sections.push(`- **Tasks:** ${backlog.tasks.length}`);
|
|
3368
|
+
sections.push(
|
|
3369
|
+
`- **Total Points:** ${backlog.stories.reduce((sum, s) => sum + s.points, 0)}`
|
|
3370
|
+
);
|
|
3371
|
+
sections.push("");
|
|
3372
|
+
sections.push("## Epics");
|
|
3373
|
+
sections.push("");
|
|
3374
|
+
for (const epic of backlog.epics) {
|
|
3375
|
+
sections.push(`### ${epic.title}`);
|
|
3376
|
+
sections.push("");
|
|
3377
|
+
sections.push(`**Priority:** ${epic.priority} | **Status:** ${epic.status}`);
|
|
3378
|
+
sections.push("");
|
|
3379
|
+
sections.push(epic.description);
|
|
3380
|
+
sections.push("");
|
|
3381
|
+
const epicStories = backlog.stories.filter((s) => s.epicId === epic.id);
|
|
3382
|
+
if (epicStories.length > 0) {
|
|
3383
|
+
sections.push("#### Stories");
|
|
3384
|
+
sections.push("");
|
|
3385
|
+
sections.push("| ID | Title | Points | Status |");
|
|
3386
|
+
sections.push("|----|-------|--------|--------|");
|
|
3387
|
+
for (const story of epicStories) {
|
|
3388
|
+
sections.push(
|
|
3389
|
+
`| ${story.id} | ${story.title} | ${story.points} | ${story.status} |`
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
3392
|
+
sections.push("");
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
sections.push("## Story Details");
|
|
3396
|
+
sections.push("");
|
|
3397
|
+
for (const story of backlog.stories) {
|
|
3398
|
+
sections.push(`### ${story.title}`);
|
|
3399
|
+
sections.push("");
|
|
3400
|
+
sections.push(`**As a** ${story.asA}`);
|
|
3401
|
+
sections.push(`**I want** ${story.iWant}`);
|
|
3402
|
+
sections.push(`**So that** ${story.soThat}`);
|
|
3403
|
+
sections.push("");
|
|
3404
|
+
sections.push(`**Points:** ${story.points} | **Status:** ${story.status}`);
|
|
3405
|
+
sections.push("");
|
|
3406
|
+
if (story.acceptanceCriteria.length > 0) {
|
|
3407
|
+
sections.push("**Acceptance Criteria:**");
|
|
3408
|
+
for (const ac of story.acceptanceCriteria) {
|
|
3409
|
+
sections.push(`- [ ] ${ac}`);
|
|
3410
|
+
}
|
|
3411
|
+
sections.push("");
|
|
3412
|
+
}
|
|
3413
|
+
const storyTasks = backlog.tasks.filter((t) => t.storyId === story.id);
|
|
3414
|
+
if (storyTasks.length > 0) {
|
|
3415
|
+
sections.push("**Tasks:**");
|
|
3416
|
+
sections.push("");
|
|
3417
|
+
sections.push("| ID | Title | Type | Complexity |");
|
|
3418
|
+
sections.push("|----|-------|------|------------|");
|
|
3419
|
+
for (const task of storyTasks) {
|
|
3420
|
+
sections.push(
|
|
3421
|
+
`| ${task.id} | ${task.title} | ${task.type} | ${task.estimatedComplexity} |`
|
|
3422
|
+
);
|
|
3423
|
+
}
|
|
3424
|
+
sections.push("");
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
sections.push("---");
|
|
3428
|
+
sections.push("");
|
|
3429
|
+
sections.push("*Generated by Corbat-Coco*");
|
|
3430
|
+
return sections.join("\n");
|
|
3431
|
+
}
|
|
3432
|
+
function generateSprintMarkdown(sprint, backlog) {
|
|
3433
|
+
const sections = [];
|
|
3434
|
+
sections.push(`# ${sprint.name}`);
|
|
3435
|
+
sections.push("");
|
|
3436
|
+
sections.push(`**Start Date:** ${sprint.startDate.toISOString().split("T")[0]}`);
|
|
3437
|
+
sections.push(`**Status:** ${sprint.status}`);
|
|
3438
|
+
sections.push("");
|
|
3439
|
+
sections.push("## Goal");
|
|
3440
|
+
sections.push("");
|
|
3441
|
+
sections.push(sprint.goal);
|
|
3442
|
+
sections.push("");
|
|
3443
|
+
const sprintStories = backlog.stories.filter(
|
|
3444
|
+
(s) => sprint.stories.includes(s.id)
|
|
3445
|
+
);
|
|
3446
|
+
const totalPoints = sprintStories.reduce((sum, s) => sum + s.points, 0);
|
|
3447
|
+
sections.push("## Stories");
|
|
3448
|
+
sections.push("");
|
|
3449
|
+
sections.push(`**Total Points:** ${totalPoints}`);
|
|
3450
|
+
sections.push("");
|
|
3451
|
+
sections.push("| Story | Points | Status |");
|
|
3452
|
+
sections.push("|-------|--------|--------|");
|
|
3453
|
+
for (const story of sprintStories) {
|
|
3454
|
+
sections.push(`| ${story.title} | ${story.points} | ${story.status} |`);
|
|
3455
|
+
}
|
|
3456
|
+
sections.push("");
|
|
3457
|
+
sections.push("## Tasks");
|
|
3458
|
+
sections.push("");
|
|
3459
|
+
for (const story of sprintStories) {
|
|
3460
|
+
const storyTasks = backlog.tasks.filter((t) => t.storyId === story.id);
|
|
3461
|
+
if (storyTasks.length > 0) {
|
|
3462
|
+
sections.push(`### ${story.title}`);
|
|
3463
|
+
sections.push("");
|
|
3464
|
+
for (const task of storyTasks) {
|
|
3465
|
+
const checkbox = task.status === "completed" ? "[x]" : "[ ]";
|
|
3466
|
+
sections.push(`- ${checkbox} ${task.title} (${task.type})`);
|
|
3467
|
+
}
|
|
3468
|
+
sections.push("");
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
return sections.join("\n");
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
// src/phases/orchestrate/executor.ts
|
|
3475
|
+
var OrchestrateExecutor = class {
|
|
3476
|
+
name = "orchestrate";
|
|
3477
|
+
description = "Design architecture, create ADRs, and generate backlog";
|
|
3478
|
+
config;
|
|
3479
|
+
constructor(config = {}) {
|
|
3480
|
+
this.config = { ...DEFAULT_ORCHESTRATE_CONFIG, ...config };
|
|
3481
|
+
}
|
|
3482
|
+
/**
|
|
3483
|
+
* Check if the phase can start
|
|
3484
|
+
*/
|
|
3485
|
+
canStart(_context) {
|
|
3486
|
+
return true;
|
|
3487
|
+
}
|
|
3488
|
+
/**
|
|
3489
|
+
* Execute the ORCHESTRATE phase
|
|
3490
|
+
*/
|
|
3491
|
+
async execute(context) {
|
|
3492
|
+
const startTime = /* @__PURE__ */ new Date();
|
|
3493
|
+
const artifacts = [];
|
|
3494
|
+
try {
|
|
3495
|
+
const specification = await this.loadSpecification(context.projectPath);
|
|
3496
|
+
const llm = this.createLLMAdapter(context);
|
|
3497
|
+
const archGenerator = new ArchitectureGenerator(llm, this.config);
|
|
3498
|
+
const adrGenerator = new ADRGenerator(llm, this.config);
|
|
3499
|
+
const backlogGenerator = new BacklogGenerator(llm, this.config);
|
|
3500
|
+
const architecture = await archGenerator.generate(specification);
|
|
3501
|
+
const archPath = await this.saveArchitecture(context.projectPath, architecture);
|
|
3502
|
+
artifacts.push({
|
|
3503
|
+
type: "architecture",
|
|
3504
|
+
path: archPath,
|
|
3505
|
+
description: "Architecture documentation"
|
|
3506
|
+
});
|
|
3507
|
+
const adrs = await adrGenerator.generate(architecture, specification);
|
|
3508
|
+
const adrPaths = await this.saveADRs(context.projectPath, adrs);
|
|
3509
|
+
for (const adrPath of adrPaths) {
|
|
3510
|
+
artifacts.push({
|
|
3511
|
+
type: "adr",
|
|
3512
|
+
path: adrPath,
|
|
3513
|
+
description: "Architecture Decision Record"
|
|
3514
|
+
});
|
|
3515
|
+
}
|
|
3516
|
+
const backlogResult = await backlogGenerator.generate(
|
|
3517
|
+
architecture,
|
|
3518
|
+
specification
|
|
3519
|
+
);
|
|
3520
|
+
const backlogPath = await this.saveBacklog(context.projectPath, backlogResult);
|
|
3521
|
+
artifacts.push({
|
|
3522
|
+
type: "backlog",
|
|
3523
|
+
path: backlogPath,
|
|
3524
|
+
description: "Project backlog"
|
|
3525
|
+
});
|
|
3526
|
+
const firstSprint = await backlogGenerator.planFirstSprint(
|
|
3527
|
+
backlogResult.backlog
|
|
3528
|
+
);
|
|
3529
|
+
const sprintPath = await this.saveSprint(
|
|
3530
|
+
context.projectPath,
|
|
3531
|
+
firstSprint,
|
|
3532
|
+
backlogResult
|
|
3533
|
+
);
|
|
3534
|
+
artifacts.push({
|
|
3535
|
+
type: "backlog",
|
|
3536
|
+
path: sprintPath,
|
|
3537
|
+
description: "Sprint 1 plan"
|
|
3538
|
+
});
|
|
3539
|
+
for (const diagram of architecture.diagrams) {
|
|
3540
|
+
const diagramPath = await this.saveDiagram(
|
|
3541
|
+
context.projectPath,
|
|
3542
|
+
diagram.id,
|
|
3543
|
+
diagram.mermaid
|
|
3544
|
+
);
|
|
3545
|
+
artifacts.push({
|
|
3546
|
+
type: "diagram",
|
|
3547
|
+
path: diagramPath,
|
|
3548
|
+
description: diagram.title
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3551
|
+
const endTime = /* @__PURE__ */ new Date();
|
|
3552
|
+
return {
|
|
3553
|
+
phase: "orchestrate",
|
|
3554
|
+
success: true,
|
|
3555
|
+
artifacts,
|
|
3556
|
+
metrics: {
|
|
3557
|
+
startTime,
|
|
3558
|
+
endTime,
|
|
3559
|
+
durationMs: endTime.getTime() - startTime.getTime(),
|
|
3560
|
+
llmCalls: adrs.length + 3,
|
|
3561
|
+
// Approximate
|
|
3562
|
+
tokensUsed: 0
|
|
3563
|
+
// Would need tracking
|
|
3564
|
+
}
|
|
3565
|
+
};
|
|
3566
|
+
} catch (error) {
|
|
3567
|
+
return {
|
|
3568
|
+
phase: "orchestrate",
|
|
3569
|
+
success: false,
|
|
3570
|
+
artifacts,
|
|
3571
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
/**
|
|
3576
|
+
* Check if the phase can complete
|
|
3577
|
+
*/
|
|
3578
|
+
canComplete(_context) {
|
|
3579
|
+
return true;
|
|
3580
|
+
}
|
|
3581
|
+
/**
|
|
3582
|
+
* Create a checkpoint
|
|
3583
|
+
*/
|
|
3584
|
+
async checkpoint(_context) {
|
|
3585
|
+
return {
|
|
3586
|
+
phase: "orchestrate",
|
|
3587
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3588
|
+
state: {
|
|
3589
|
+
artifacts: [],
|
|
3590
|
+
progress: 0,
|
|
3591
|
+
checkpoint: null
|
|
3592
|
+
},
|
|
3593
|
+
resumePoint: "start"
|
|
3594
|
+
};
|
|
3595
|
+
}
|
|
3596
|
+
/**
|
|
3597
|
+
* Restore from checkpoint
|
|
3598
|
+
*/
|
|
3599
|
+
async restore(_checkpoint, _context) {
|
|
3600
|
+
}
|
|
3601
|
+
// Private methods
|
|
3602
|
+
createLLMAdapter(context) {
|
|
3603
|
+
const llmContext = context.llm;
|
|
3604
|
+
return {
|
|
3605
|
+
id: "phase-adapter",
|
|
3606
|
+
name: "Phase LLM Adapter",
|
|
3607
|
+
async initialize() {
|
|
3608
|
+
},
|
|
3609
|
+
async chat(messages) {
|
|
3610
|
+
const adapted = messages.map((m) => ({
|
|
3611
|
+
role: m.role,
|
|
3612
|
+
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
|
|
3613
|
+
}));
|
|
3614
|
+
const response = await llmContext.chat(adapted);
|
|
3615
|
+
return {
|
|
3616
|
+
id: `chat-${Date.now()}`,
|
|
3617
|
+
content: response.content,
|
|
3618
|
+
stopReason: "end_turn",
|
|
3619
|
+
usage: {
|
|
3620
|
+
inputTokens: response.usage.inputTokens,
|
|
3621
|
+
outputTokens: response.usage.outputTokens
|
|
3622
|
+
},
|
|
3623
|
+
model: "phase-adapter"
|
|
3624
|
+
};
|
|
3625
|
+
},
|
|
3626
|
+
async chatWithTools(messages, options) {
|
|
3627
|
+
const adapted = messages.map((m) => ({
|
|
3628
|
+
role: m.role,
|
|
3629
|
+
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
|
|
3630
|
+
}));
|
|
3631
|
+
const tools = options.tools.map((t) => ({
|
|
3632
|
+
name: t.name,
|
|
3633
|
+
description: t.description,
|
|
3634
|
+
parameters: t.input_schema
|
|
3635
|
+
}));
|
|
3636
|
+
const response = await llmContext.chatWithTools(adapted, tools);
|
|
3637
|
+
return {
|
|
3638
|
+
id: `chat-${Date.now()}`,
|
|
3639
|
+
content: response.content,
|
|
3640
|
+
stopReason: "end_turn",
|
|
3641
|
+
usage: {
|
|
3642
|
+
inputTokens: response.usage.inputTokens,
|
|
3643
|
+
outputTokens: response.usage.outputTokens
|
|
3644
|
+
},
|
|
3645
|
+
model: "phase-adapter",
|
|
3646
|
+
toolCalls: (response.toolCalls || []).map((tc) => ({
|
|
3647
|
+
id: tc.name,
|
|
3648
|
+
name: tc.name,
|
|
3649
|
+
input: tc.arguments
|
|
3650
|
+
}))
|
|
3651
|
+
};
|
|
3652
|
+
},
|
|
3653
|
+
async *stream(messages) {
|
|
3654
|
+
const adapted = messages.map((m) => ({
|
|
3655
|
+
role: m.role,
|
|
3656
|
+
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
|
|
3657
|
+
}));
|
|
3658
|
+
const response = await llmContext.chat(adapted);
|
|
3659
|
+
yield {
|
|
3660
|
+
type: "text",
|
|
3661
|
+
text: response.content
|
|
3662
|
+
};
|
|
3663
|
+
yield {
|
|
3664
|
+
type: "done"
|
|
3665
|
+
};
|
|
3666
|
+
},
|
|
3667
|
+
countTokens(_text) {
|
|
3668
|
+
return Math.ceil(_text.length / 4);
|
|
3669
|
+
},
|
|
3670
|
+
getContextWindow() {
|
|
3671
|
+
return 2e5;
|
|
3672
|
+
},
|
|
3673
|
+
async isAvailable() {
|
|
3674
|
+
return true;
|
|
3675
|
+
}
|
|
3676
|
+
};
|
|
3677
|
+
}
|
|
3678
|
+
async loadSpecification(projectPath) {
|
|
3679
|
+
try {
|
|
3680
|
+
const jsonPath = path.join(projectPath, ".coco", "spec", "spec.json");
|
|
3681
|
+
const jsonContent = await fs4.readFile(jsonPath, "utf-8");
|
|
3682
|
+
return JSON.parse(jsonContent);
|
|
3683
|
+
} catch {
|
|
3684
|
+
return this.createMinimalSpec(projectPath);
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
createMinimalSpec(projectPath) {
|
|
3688
|
+
return {
|
|
3689
|
+
version: "1.0.0",
|
|
3690
|
+
generatedAt: /* @__PURE__ */ new Date(),
|
|
3691
|
+
overview: {
|
|
3692
|
+
name: path.basename(projectPath),
|
|
3693
|
+
description: "Project specification",
|
|
3694
|
+
goals: [],
|
|
3695
|
+
targetUsers: ["developers"],
|
|
3696
|
+
successCriteria: []
|
|
3697
|
+
},
|
|
3698
|
+
requirements: {
|
|
3699
|
+
functional: [],
|
|
3700
|
+
nonFunctional: [],
|
|
3701
|
+
constraints: []
|
|
3702
|
+
},
|
|
3703
|
+
technical: {
|
|
3704
|
+
stack: [],
|
|
3705
|
+
architecture: "",
|
|
3706
|
+
integrations: [],
|
|
3707
|
+
deployment: ""
|
|
3708
|
+
},
|
|
3709
|
+
assumptions: {
|
|
3710
|
+
confirmed: [],
|
|
3711
|
+
unconfirmed: [],
|
|
3712
|
+
risks: []
|
|
3713
|
+
},
|
|
3714
|
+
outOfScope: [],
|
|
3715
|
+
openQuestions: []
|
|
3716
|
+
};
|
|
3717
|
+
}
|
|
3718
|
+
async saveArchitecture(projectPath, architecture) {
|
|
3719
|
+
const dir = path.join(projectPath, ".coco", "architecture");
|
|
3720
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
3721
|
+
const mdPath = path.join(dir, "ARCHITECTURE.md");
|
|
3722
|
+
await fs4.writeFile(mdPath, generateArchitectureMarkdown(architecture), "utf-8");
|
|
3723
|
+
const jsonPath = path.join(dir, "architecture.json");
|
|
3724
|
+
await fs4.writeFile(jsonPath, JSON.stringify(architecture, null, 2), "utf-8");
|
|
3725
|
+
return mdPath;
|
|
3726
|
+
}
|
|
3727
|
+
async saveADRs(projectPath, adrs) {
|
|
3728
|
+
const dir = path.join(projectPath, ".coco", "architecture", "adrs");
|
|
3729
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
3730
|
+
const paths = [];
|
|
3731
|
+
const indexPath = path.join(dir, "README.md");
|
|
3732
|
+
await fs4.writeFile(indexPath, generateADRIndexMarkdown(adrs), "utf-8");
|
|
3733
|
+
paths.push(indexPath);
|
|
3734
|
+
for (const adr of adrs) {
|
|
3735
|
+
const filename = getADRFilename(adr);
|
|
3736
|
+
const adrPath = path.join(dir, filename);
|
|
3737
|
+
await fs4.writeFile(adrPath, generateADRMarkdown(adr), "utf-8");
|
|
3738
|
+
paths.push(adrPath);
|
|
3739
|
+
}
|
|
3740
|
+
return paths;
|
|
3741
|
+
}
|
|
3742
|
+
async saveBacklog(projectPath, backlogResult) {
|
|
3743
|
+
const dir = path.join(projectPath, ".coco", "planning");
|
|
3744
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
3745
|
+
const mdPath = path.join(dir, "BACKLOG.md");
|
|
3746
|
+
await fs4.writeFile(
|
|
3747
|
+
mdPath,
|
|
3748
|
+
generateBacklogMarkdown(backlogResult.backlog),
|
|
3749
|
+
"utf-8"
|
|
3750
|
+
);
|
|
3751
|
+
const jsonPath = path.join(dir, "backlog.json");
|
|
3752
|
+
await fs4.writeFile(jsonPath, JSON.stringify(backlogResult, null, 2), "utf-8");
|
|
3753
|
+
return mdPath;
|
|
3754
|
+
}
|
|
3755
|
+
async saveSprint(projectPath, sprint, backlogResult) {
|
|
3756
|
+
const dir = path.join(projectPath, ".coco", "planning", "sprints");
|
|
3757
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
3758
|
+
const filename = `${sprint.id}.md`;
|
|
3759
|
+
const sprintPath = path.join(dir, filename);
|
|
3760
|
+
await fs4.writeFile(
|
|
3761
|
+
sprintPath,
|
|
3762
|
+
generateSprintMarkdown(sprint, backlogResult.backlog),
|
|
3763
|
+
"utf-8"
|
|
3764
|
+
);
|
|
3765
|
+
const jsonPath = path.join(dir, `${sprint.id}.json`);
|
|
3766
|
+
await fs4.writeFile(jsonPath, JSON.stringify(sprint, null, 2), "utf-8");
|
|
3767
|
+
return sprintPath;
|
|
3768
|
+
}
|
|
3769
|
+
async saveDiagram(projectPath, id, mermaid) {
|
|
3770
|
+
const dir = path.join(projectPath, ".coco", "architecture", "diagrams");
|
|
3771
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
3772
|
+
const diagramPath = path.join(dir, `${id}.mmd`);
|
|
3773
|
+
await fs4.writeFile(diagramPath, mermaid, "utf-8");
|
|
3774
|
+
return diagramPath;
|
|
3775
|
+
}
|
|
3776
|
+
};
|
|
3777
|
+
function createOrchestrateExecutor(config) {
|
|
3778
|
+
return new OrchestrateExecutor(config);
|
|
3779
|
+
}
|
|
3780
|
+
var DEFAULT_MODEL = "claude-sonnet-4-20250514";
|
|
3781
|
+
var CONTEXT_WINDOWS = {
|
|
3782
|
+
"claude-sonnet-4-20250514": 2e5,
|
|
3783
|
+
"claude-opus-4-20250514": 2e5,
|
|
3784
|
+
"claude-3-5-sonnet-20241022": 2e5,
|
|
3785
|
+
"claude-3-5-haiku-20241022": 2e5,
|
|
3786
|
+
"claude-3-opus-20240229": 2e5,
|
|
3787
|
+
"claude-3-sonnet-20240229": 2e5,
|
|
3788
|
+
"claude-3-haiku-20240307": 2e5
|
|
3789
|
+
};
|
|
3790
|
+
var AnthropicProvider = class {
|
|
3791
|
+
id = "anthropic";
|
|
3792
|
+
name = "Anthropic Claude";
|
|
3793
|
+
client = null;
|
|
3794
|
+
config = {};
|
|
3795
|
+
/**
|
|
3796
|
+
* Initialize the provider
|
|
3797
|
+
*/
|
|
3798
|
+
async initialize(config) {
|
|
3799
|
+
this.config = config;
|
|
3800
|
+
const apiKey = config.apiKey ?? process.env["ANTHROPIC_API_KEY"];
|
|
3801
|
+
if (!apiKey) {
|
|
3802
|
+
throw new ProviderError("Anthropic API key not provided", {
|
|
3803
|
+
provider: this.id
|
|
3804
|
+
});
|
|
3805
|
+
}
|
|
3806
|
+
this.client = new Anthropic({
|
|
3807
|
+
apiKey,
|
|
3808
|
+
baseURL: config.baseUrl,
|
|
3809
|
+
timeout: config.timeout ?? 12e4
|
|
3810
|
+
});
|
|
3811
|
+
}
|
|
3812
|
+
/**
|
|
3813
|
+
* Send a chat message
|
|
3814
|
+
*/
|
|
3815
|
+
async chat(messages, options) {
|
|
3816
|
+
this.ensureInitialized();
|
|
3817
|
+
try {
|
|
3818
|
+
const response = await this.client.messages.create({
|
|
3819
|
+
model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
|
|
3820
|
+
max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
|
|
3821
|
+
temperature: options?.temperature ?? this.config.temperature ?? 0,
|
|
3822
|
+
system: options?.system,
|
|
3823
|
+
messages: this.convertMessages(messages),
|
|
3824
|
+
stop_sequences: options?.stopSequences
|
|
3825
|
+
});
|
|
3826
|
+
return {
|
|
3827
|
+
id: response.id,
|
|
3828
|
+
content: this.extractTextContent(response.content),
|
|
3829
|
+
stopReason: this.mapStopReason(response.stop_reason),
|
|
3830
|
+
usage: {
|
|
3831
|
+
inputTokens: response.usage.input_tokens,
|
|
3832
|
+
outputTokens: response.usage.output_tokens
|
|
3833
|
+
},
|
|
3834
|
+
model: response.model
|
|
3835
|
+
};
|
|
3836
|
+
} catch (error) {
|
|
3837
|
+
throw this.handleError(error);
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
/**
|
|
3841
|
+
* Send a chat message with tool use
|
|
3842
|
+
*/
|
|
3843
|
+
async chatWithTools(messages, options) {
|
|
3844
|
+
this.ensureInitialized();
|
|
3845
|
+
try {
|
|
3846
|
+
const response = await this.client.messages.create({
|
|
3847
|
+
model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
|
|
3848
|
+
max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
|
|
3849
|
+
temperature: options?.temperature ?? this.config.temperature ?? 0,
|
|
3850
|
+
system: options?.system,
|
|
3851
|
+
messages: this.convertMessages(messages),
|
|
3852
|
+
tools: this.convertTools(options.tools),
|
|
3853
|
+
tool_choice: options.toolChoice ? this.convertToolChoice(options.toolChoice) : void 0
|
|
3854
|
+
});
|
|
3855
|
+
const toolCalls = this.extractToolCalls(response.content);
|
|
3856
|
+
return {
|
|
3857
|
+
id: response.id,
|
|
3858
|
+
content: this.extractTextContent(response.content),
|
|
3859
|
+
stopReason: this.mapStopReason(response.stop_reason),
|
|
3860
|
+
usage: {
|
|
3861
|
+
inputTokens: response.usage.input_tokens,
|
|
3862
|
+
outputTokens: response.usage.output_tokens
|
|
3863
|
+
},
|
|
3864
|
+
model: response.model,
|
|
3865
|
+
toolCalls
|
|
3866
|
+
};
|
|
3867
|
+
} catch (error) {
|
|
3868
|
+
throw this.handleError(error);
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
/**
|
|
3872
|
+
* Stream a chat response
|
|
3873
|
+
*/
|
|
3874
|
+
async *stream(messages, options) {
|
|
3875
|
+
this.ensureInitialized();
|
|
3876
|
+
try {
|
|
3877
|
+
const stream = await this.client.messages.stream({
|
|
3878
|
+
model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
|
|
3879
|
+
max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
|
|
3880
|
+
temperature: options?.temperature ?? this.config.temperature ?? 0,
|
|
3881
|
+
system: options?.system,
|
|
3882
|
+
messages: this.convertMessages(messages)
|
|
3883
|
+
});
|
|
3884
|
+
for await (const event of stream) {
|
|
3885
|
+
if (event.type === "content_block_delta") {
|
|
3886
|
+
const delta = event.delta;
|
|
3887
|
+
if (delta.type === "text_delta" && delta.text) {
|
|
3888
|
+
yield { type: "text", text: delta.text };
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
yield { type: "done" };
|
|
3893
|
+
} catch (error) {
|
|
3894
|
+
throw this.handleError(error);
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Count tokens (approximate)
|
|
3899
|
+
*/
|
|
3900
|
+
countTokens(text4) {
|
|
3901
|
+
return Math.ceil(text4.length / 4);
|
|
3902
|
+
}
|
|
3903
|
+
/**
|
|
3904
|
+
* Get context window size
|
|
3905
|
+
*/
|
|
3906
|
+
getContextWindow() {
|
|
3907
|
+
const model = this.config.model ?? DEFAULT_MODEL;
|
|
3908
|
+
return CONTEXT_WINDOWS[model] ?? 2e5;
|
|
3909
|
+
}
|
|
3910
|
+
/**
|
|
3911
|
+
* Check if provider is available
|
|
3912
|
+
*/
|
|
3913
|
+
async isAvailable() {
|
|
3914
|
+
if (!this.client) return false;
|
|
3915
|
+
try {
|
|
3916
|
+
await this.client.messages.create({
|
|
3917
|
+
model: this.config.model ?? DEFAULT_MODEL,
|
|
3918
|
+
max_tokens: 1,
|
|
3919
|
+
messages: [{ role: "user", content: "hi" }]
|
|
3920
|
+
});
|
|
3921
|
+
return true;
|
|
3922
|
+
} catch {
|
|
3923
|
+
return false;
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
/**
|
|
3927
|
+
* Ensure client is initialized
|
|
3928
|
+
*/
|
|
3929
|
+
ensureInitialized() {
|
|
3930
|
+
if (!this.client) {
|
|
3931
|
+
throw new ProviderError("Provider not initialized. Call initialize() first.", {
|
|
3932
|
+
provider: this.id
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
/**
|
|
3937
|
+
* Convert messages to Anthropic format
|
|
3938
|
+
*/
|
|
3939
|
+
convertMessages(messages) {
|
|
3940
|
+
return messages.filter((m) => m.role !== "system").map((m) => ({
|
|
3941
|
+
role: m.role,
|
|
3942
|
+
content: this.convertContent(m.content)
|
|
3943
|
+
}));
|
|
3944
|
+
}
|
|
3945
|
+
/**
|
|
3946
|
+
* Convert message content to Anthropic format
|
|
3947
|
+
*/
|
|
3948
|
+
convertContent(content) {
|
|
3949
|
+
if (typeof content === "string") {
|
|
3950
|
+
return content;
|
|
3951
|
+
}
|
|
3952
|
+
return content.map((block) => {
|
|
3953
|
+
if (block.type === "text") {
|
|
3954
|
+
return { type: "text", text: block.text };
|
|
3955
|
+
}
|
|
3956
|
+
if (block.type === "tool_use") {
|
|
3957
|
+
const toolUse = block;
|
|
3958
|
+
return {
|
|
3959
|
+
type: "tool_use",
|
|
3960
|
+
id: toolUse.id,
|
|
3961
|
+
name: toolUse.name,
|
|
3962
|
+
input: toolUse.input
|
|
3963
|
+
};
|
|
3964
|
+
}
|
|
3965
|
+
if (block.type === "tool_result") {
|
|
3966
|
+
const toolResult = block;
|
|
3967
|
+
return {
|
|
3968
|
+
type: "tool_result",
|
|
3969
|
+
tool_use_id: toolResult.tool_use_id,
|
|
3970
|
+
content: toolResult.content,
|
|
3971
|
+
is_error: toolResult.is_error
|
|
3972
|
+
};
|
|
3973
|
+
}
|
|
3974
|
+
return { type: "text", text: "" };
|
|
3975
|
+
});
|
|
3976
|
+
}
|
|
3977
|
+
/**
|
|
3978
|
+
* Convert tools to Anthropic format
|
|
3979
|
+
*/
|
|
3980
|
+
convertTools(tools) {
|
|
3981
|
+
return tools.map((tool) => ({
|
|
3982
|
+
name: tool.name,
|
|
3983
|
+
description: tool.description,
|
|
3984
|
+
input_schema: tool.input_schema
|
|
3985
|
+
}));
|
|
3986
|
+
}
|
|
3987
|
+
/**
|
|
3988
|
+
* Convert tool choice to Anthropic format
|
|
3989
|
+
*/
|
|
3990
|
+
convertToolChoice(choice) {
|
|
3991
|
+
if (choice === "auto") return { type: "auto" };
|
|
3992
|
+
if (choice === "any") return { type: "any" };
|
|
3993
|
+
if (typeof choice === "object" && choice.type === "tool") {
|
|
3994
|
+
return { type: "tool", name: choice.name };
|
|
3995
|
+
}
|
|
3996
|
+
return { type: "auto" };
|
|
3997
|
+
}
|
|
3998
|
+
/**
|
|
3999
|
+
* Extract text content from response
|
|
4000
|
+
*/
|
|
4001
|
+
extractTextContent(content) {
|
|
4002
|
+
return content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
4003
|
+
}
|
|
4004
|
+
/**
|
|
4005
|
+
* Extract tool calls from response
|
|
4006
|
+
*/
|
|
4007
|
+
extractToolCalls(content) {
|
|
4008
|
+
return content.filter((block) => block.type === "tool_use").map((block) => ({
|
|
4009
|
+
id: block.id,
|
|
4010
|
+
name: block.name,
|
|
4011
|
+
input: block.input
|
|
4012
|
+
}));
|
|
4013
|
+
}
|
|
4014
|
+
/**
|
|
4015
|
+
* Map stop reason to our format
|
|
4016
|
+
*/
|
|
4017
|
+
mapStopReason(reason) {
|
|
4018
|
+
switch (reason) {
|
|
4019
|
+
case "end_turn":
|
|
4020
|
+
return "end_turn";
|
|
4021
|
+
case "max_tokens":
|
|
4022
|
+
return "max_tokens";
|
|
4023
|
+
case "stop_sequence":
|
|
4024
|
+
return "stop_sequence";
|
|
4025
|
+
case "tool_use":
|
|
4026
|
+
return "tool_use";
|
|
4027
|
+
default:
|
|
4028
|
+
return "end_turn";
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
/**
|
|
4032
|
+
* Handle API errors
|
|
4033
|
+
*/
|
|
4034
|
+
handleError(error) {
|
|
4035
|
+
if (error instanceof Anthropic.APIError) {
|
|
4036
|
+
const retryable = error.status === 429 || error.status >= 500;
|
|
4037
|
+
throw new ProviderError(error.message, {
|
|
4038
|
+
provider: this.id,
|
|
4039
|
+
statusCode: error.status,
|
|
4040
|
+
retryable,
|
|
4041
|
+
cause: error
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4044
|
+
throw new ProviderError(
|
|
4045
|
+
error instanceof Error ? error.message : String(error),
|
|
4046
|
+
{
|
|
4047
|
+
provider: this.id,
|
|
4048
|
+
cause: error instanceof Error ? error : void 0
|
|
4049
|
+
}
|
|
4050
|
+
);
|
|
4051
|
+
}
|
|
4052
|
+
};
|
|
4053
|
+
|
|
4054
|
+
// src/providers/index.ts
|
|
4055
|
+
async function createProvider(type, config = {}) {
|
|
4056
|
+
let provider;
|
|
4057
|
+
switch (type) {
|
|
4058
|
+
case "anthropic":
|
|
4059
|
+
provider = new AnthropicProvider();
|
|
4060
|
+
break;
|
|
4061
|
+
case "openai":
|
|
4062
|
+
throw new ProviderError("OpenAI provider not yet implemented", {
|
|
4063
|
+
provider: "openai"
|
|
4064
|
+
});
|
|
4065
|
+
case "local":
|
|
4066
|
+
throw new ProviderError("Local provider not yet implemented", {
|
|
4067
|
+
provider: "local"
|
|
4068
|
+
});
|
|
4069
|
+
default:
|
|
4070
|
+
throw new ProviderError(`Unknown provider type: ${type}`, {
|
|
4071
|
+
provider: type
|
|
4072
|
+
});
|
|
4073
|
+
}
|
|
4074
|
+
await provider.initialize(config);
|
|
4075
|
+
return provider;
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
// src/cli/commands/plan.ts
|
|
4079
|
+
function registerPlanCommand(program2) {
|
|
4080
|
+
program2.command("plan").description("Run discovery and create a development plan").option("-i, --interactive", "Run in interactive mode (default)").option("--skip-discovery", "Skip discovery, use existing specification").option("--dry-run", "Generate plan without saving").option("--auto", "Run without confirmations").action(async (options) => {
|
|
4081
|
+
try {
|
|
4082
|
+
const result = await runPlan({ ...options, cwd: process.cwd() });
|
|
4083
|
+
if (!result.success) {
|
|
4084
|
+
p.log.error(result.error || "Planning failed");
|
|
4085
|
+
process.exit(1);
|
|
4086
|
+
}
|
|
4087
|
+
} catch (error) {
|
|
4088
|
+
p.log.error(
|
|
4089
|
+
error instanceof Error ? error.message : "An error occurred"
|
|
4090
|
+
);
|
|
4091
|
+
process.exit(1);
|
|
4092
|
+
}
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
async function createCliPhaseContext(projectPath, _onUserInput) {
|
|
4096
|
+
let llm;
|
|
4097
|
+
try {
|
|
4098
|
+
const provider = await createProvider("anthropic", {
|
|
4099
|
+
apiKey: process.env.ANTHROPIC_API_KEY
|
|
4100
|
+
});
|
|
4101
|
+
llm = {
|
|
4102
|
+
async chat(messages) {
|
|
4103
|
+
const adapted = messages.map((m) => ({
|
|
4104
|
+
role: m.role,
|
|
4105
|
+
content: m.content
|
|
4106
|
+
}));
|
|
4107
|
+
const response = await provider.chat(adapted);
|
|
4108
|
+
return {
|
|
4109
|
+
content: response.content,
|
|
4110
|
+
usage: response.usage
|
|
4111
|
+
};
|
|
4112
|
+
},
|
|
4113
|
+
async chatWithTools(messages, tools) {
|
|
4114
|
+
const adapted = messages.map((m) => ({
|
|
4115
|
+
role: m.role,
|
|
4116
|
+
content: m.content
|
|
4117
|
+
}));
|
|
4118
|
+
const adaptedTools = tools.map((t) => ({
|
|
4119
|
+
name: t.name,
|
|
4120
|
+
description: t.description,
|
|
4121
|
+
input_schema: t.parameters
|
|
4122
|
+
}));
|
|
4123
|
+
const response = await provider.chatWithTools(adapted, { tools: adaptedTools });
|
|
4124
|
+
return {
|
|
4125
|
+
content: response.content,
|
|
4126
|
+
usage: response.usage,
|
|
4127
|
+
toolCalls: response.toolCalls?.map((tc) => ({
|
|
4128
|
+
name: tc.name,
|
|
4129
|
+
arguments: tc.input
|
|
4130
|
+
}))
|
|
4131
|
+
};
|
|
4132
|
+
}
|
|
4133
|
+
};
|
|
4134
|
+
} catch {
|
|
4135
|
+
llm = {
|
|
4136
|
+
async chat() {
|
|
4137
|
+
return { content: "{}", usage: { inputTokens: 0, outputTokens: 0 } };
|
|
4138
|
+
},
|
|
4139
|
+
async chatWithTools() {
|
|
4140
|
+
return { content: "{}", usage: { inputTokens: 0, outputTokens: 0 } };
|
|
4141
|
+
}
|
|
4142
|
+
};
|
|
4143
|
+
}
|
|
4144
|
+
return {
|
|
4145
|
+
projectPath,
|
|
4146
|
+
config: {
|
|
4147
|
+
quality: {
|
|
4148
|
+
minScore: 85,
|
|
4149
|
+
minCoverage: 80,
|
|
4150
|
+
maxIterations: 10,
|
|
4151
|
+
convergenceThreshold: 2
|
|
4152
|
+
},
|
|
4153
|
+
timeouts: {
|
|
4154
|
+
phaseTimeout: 36e5,
|
|
4155
|
+
taskTimeout: 6e5,
|
|
4156
|
+
llmTimeout: 12e4
|
|
4157
|
+
}
|
|
4158
|
+
},
|
|
4159
|
+
state: {
|
|
4160
|
+
artifacts: [],
|
|
4161
|
+
progress: 0,
|
|
4162
|
+
checkpoint: null
|
|
4163
|
+
},
|
|
4164
|
+
tools: {
|
|
4165
|
+
file: {
|
|
4166
|
+
async read(path5) {
|
|
4167
|
+
const fs5 = await import('fs/promises');
|
|
4168
|
+
return fs5.readFile(path5, "utf-8");
|
|
4169
|
+
},
|
|
4170
|
+
async write(path5, content) {
|
|
4171
|
+
const fs5 = await import('fs/promises');
|
|
4172
|
+
const nodePath = await import('path');
|
|
4173
|
+
await fs5.mkdir(nodePath.dirname(path5), { recursive: true });
|
|
4174
|
+
await fs5.writeFile(path5, content, "utf-8");
|
|
4175
|
+
},
|
|
4176
|
+
async exists(path5) {
|
|
4177
|
+
const fs5 = await import('fs/promises');
|
|
4178
|
+
try {
|
|
4179
|
+
await fs5.access(path5);
|
|
4180
|
+
return true;
|
|
4181
|
+
} catch {
|
|
4182
|
+
return false;
|
|
4183
|
+
}
|
|
4184
|
+
},
|
|
4185
|
+
async glob(pattern) {
|
|
4186
|
+
const { glob } = await import('glob');
|
|
4187
|
+
return glob(pattern, { cwd: projectPath });
|
|
4188
|
+
}
|
|
4189
|
+
},
|
|
4190
|
+
bash: {
|
|
4191
|
+
async exec(command, options = {}) {
|
|
4192
|
+
const { execa } = await import('execa');
|
|
4193
|
+
try {
|
|
4194
|
+
const result = await execa(command, {
|
|
4195
|
+
shell: true,
|
|
4196
|
+
cwd: options.cwd || projectPath,
|
|
4197
|
+
timeout: options.timeout,
|
|
4198
|
+
env: options.env
|
|
4199
|
+
});
|
|
4200
|
+
return {
|
|
4201
|
+
stdout: result.stdout,
|
|
4202
|
+
stderr: result.stderr,
|
|
4203
|
+
exitCode: result.exitCode ?? 0
|
|
4204
|
+
};
|
|
4205
|
+
} catch (error) {
|
|
4206
|
+
const err = error;
|
|
4207
|
+
return {
|
|
4208
|
+
stdout: err.stdout || "",
|
|
4209
|
+
stderr: err.stderr || "",
|
|
4210
|
+
exitCode: err.exitCode || 1
|
|
4211
|
+
};
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
},
|
|
4215
|
+
git: {
|
|
4216
|
+
async status() {
|
|
4217
|
+
return { branch: "main", clean: true, staged: [], unstaged: [], untracked: [] };
|
|
4218
|
+
},
|
|
4219
|
+
async commit() {
|
|
4220
|
+
},
|
|
4221
|
+
async push() {
|
|
4222
|
+
}
|
|
4223
|
+
},
|
|
4224
|
+
test: {
|
|
4225
|
+
async run() {
|
|
4226
|
+
return { passed: 0, failed: 0, skipped: 0, duration: 0, failures: [] };
|
|
4227
|
+
},
|
|
4228
|
+
async coverage() {
|
|
4229
|
+
return { lines: 0, branches: 0, functions: 0, statements: 0 };
|
|
4230
|
+
}
|
|
4231
|
+
},
|
|
4232
|
+
quality: {
|
|
4233
|
+
async lint() {
|
|
4234
|
+
return { errors: 0, warnings: 0, issues: [] };
|
|
4235
|
+
},
|
|
4236
|
+
async complexity() {
|
|
4237
|
+
return { averageComplexity: 0, maxComplexity: 0, files: [] };
|
|
4238
|
+
},
|
|
4239
|
+
async security() {
|
|
4240
|
+
return { vulnerabilities: 0, issues: [] };
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
},
|
|
4244
|
+
llm
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
4247
|
+
async function runPlan(options = {}) {
|
|
4248
|
+
const cwd = options.cwd || process.cwd();
|
|
4249
|
+
const configPath = await findConfigPath(cwd);
|
|
4250
|
+
if (!configPath) {
|
|
4251
|
+
return {
|
|
4252
|
+
success: false,
|
|
4253
|
+
error: 'No Corbat-Coco config found. Run "coco init" first.'
|
|
4254
|
+
};
|
|
4255
|
+
}
|
|
4256
|
+
await loadConfig(configPath);
|
|
4257
|
+
if (!options.auto) {
|
|
4258
|
+
p.intro(chalk5.cyan("Corbat-Coco Planning"));
|
|
4259
|
+
}
|
|
4260
|
+
const onUserInput = options.auto ? void 0 : async (prompt, opts) => {
|
|
4261
|
+
if (opts && opts.length > 0) {
|
|
4262
|
+
const result = await p.select({
|
|
4263
|
+
message: prompt,
|
|
4264
|
+
options: opts.map((o) => ({ value: o, label: o }))
|
|
4265
|
+
});
|
|
4266
|
+
if (p.isCancel(result)) {
|
|
4267
|
+
throw new Error("Cancelled by user");
|
|
4268
|
+
}
|
|
4269
|
+
return result;
|
|
4270
|
+
} else {
|
|
4271
|
+
const result = await p.text({ message: prompt });
|
|
4272
|
+
if (p.isCancel(result)) {
|
|
4273
|
+
throw new Error("Cancelled by user");
|
|
4274
|
+
}
|
|
4275
|
+
return result;
|
|
4276
|
+
}
|
|
4277
|
+
};
|
|
4278
|
+
let convergeResult;
|
|
4279
|
+
if (!options.skipDiscovery) {
|
|
4280
|
+
if (!options.auto) {
|
|
4281
|
+
const shouldProceed = await p.confirm({
|
|
4282
|
+
message: "Ready to start planning. Continue?"
|
|
4283
|
+
});
|
|
4284
|
+
if (p.isCancel(shouldProceed) || shouldProceed === false) {
|
|
4285
|
+
return { success: false, error: "Planning cancelled by user" };
|
|
4286
|
+
}
|
|
4287
|
+
p.log.info("Phase 1: Converge - Understanding your requirements");
|
|
4288
|
+
}
|
|
4289
|
+
const convergeExecutor = createConvergeExecutor({
|
|
4290
|
+
onUserInput,
|
|
4291
|
+
onProgress: (step, progress, message) => {
|
|
4292
|
+
if (!options.auto) {
|
|
4293
|
+
p.log.step(`[${step}] ${progress}% - ${message}`);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
});
|
|
4297
|
+
const context2 = await createCliPhaseContext(cwd);
|
|
4298
|
+
convergeResult = await convergeExecutor.execute(context2);
|
|
4299
|
+
if (!convergeResult.success) {
|
|
4300
|
+
return {
|
|
4301
|
+
success: false,
|
|
4302
|
+
error: convergeResult.error || "CONVERGE phase failed"
|
|
4303
|
+
};
|
|
4304
|
+
}
|
|
4305
|
+
} else {
|
|
4306
|
+
convergeResult = { phase: "converge", success: true, artifacts: [] };
|
|
4307
|
+
}
|
|
4308
|
+
if (!options.auto) {
|
|
4309
|
+
p.log.info("Phase 2: Orchestrate - Creating development plan");
|
|
4310
|
+
}
|
|
4311
|
+
const orchestrateExecutor = createOrchestrateExecutor();
|
|
4312
|
+
const context = await createCliPhaseContext(cwd);
|
|
4313
|
+
const orchestrateResult = await orchestrateExecutor.execute(context);
|
|
4314
|
+
if (!orchestrateResult.success) {
|
|
4315
|
+
return {
|
|
4316
|
+
success: false,
|
|
4317
|
+
error: orchestrateResult.error || "ORCHESTRATE phase failed"
|
|
4318
|
+
};
|
|
4319
|
+
}
|
|
4320
|
+
if (!options.auto) {
|
|
4321
|
+
console.log("\n" + chalk5.bold("Planning Summary:"));
|
|
4322
|
+
console.log(
|
|
4323
|
+
chalk5.dim(" Artifacts generated: ") + orchestrateResult.artifacts.length
|
|
4324
|
+
);
|
|
4325
|
+
for (const artifact of orchestrateResult.artifacts) {
|
|
4326
|
+
console.log(chalk5.dim(` - ${artifact.type}: ${artifact.description}`));
|
|
4327
|
+
}
|
|
4328
|
+
p.outro(
|
|
4329
|
+
chalk5.green("Planning complete! Run 'coco build' to start development.")
|
|
4330
|
+
);
|
|
4331
|
+
}
|
|
4332
|
+
return {
|
|
4333
|
+
success: true
|
|
4334
|
+
};
|
|
4335
|
+
}
|
|
4336
|
+
function registerBuildCommand(program2) {
|
|
4337
|
+
program2.command("build").description("Execute tasks and build the project").option("-t, --task <task-id>", "Build a specific task only").option("-s, --sprint <sprint-id>", "Build a specific sprint only").option("--no-review", "Skip self-review iterations").option("--max-iterations <n>", "Maximum iterations per task", "10").option("--min-quality <n>", "Minimum quality score", "85").action(async (options) => {
|
|
4338
|
+
await runBuild(options);
|
|
4339
|
+
});
|
|
4340
|
+
}
|
|
4341
|
+
async function runBuild(options) {
|
|
4342
|
+
p.intro(chalk5.cyan("Corbat-Coco Build"));
|
|
4343
|
+
const projectState = await checkProjectState();
|
|
4344
|
+
if (!projectState.hasProject) {
|
|
4345
|
+
p.log.error("No Corbat-Coco project found. Run 'coco init' first.");
|
|
4346
|
+
process.exit(1);
|
|
4347
|
+
}
|
|
4348
|
+
if (!projectState.hasPlan) {
|
|
4349
|
+
p.log.error("No development plan found. Run 'coco plan' first.");
|
|
4350
|
+
process.exit(1);
|
|
4351
|
+
}
|
|
4352
|
+
const maxIterations = parseInt(options.maxIterations || "10", 10);
|
|
4353
|
+
const minQuality = parseInt(options.minQuality || "85", 10);
|
|
4354
|
+
p.log.step(`Phase 3: Complete - Building with quality threshold ${minQuality}`);
|
|
4355
|
+
const tasks = await loadTasks();
|
|
4356
|
+
p.log.info(`Found ${tasks.length} tasks to complete`);
|
|
4357
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
4358
|
+
const task = tasks[i];
|
|
4359
|
+
if (!task) continue;
|
|
4360
|
+
console.log("\n" + chalk5.bold(`Task ${i + 1}/${tasks.length}: ${task.title}`));
|
|
4361
|
+
await executeTask(task, {
|
|
4362
|
+
maxIterations,
|
|
4363
|
+
minQuality,
|
|
4364
|
+
skipReview: !options.review
|
|
4365
|
+
});
|
|
4366
|
+
}
|
|
4367
|
+
p.outro(chalk5.green("Build complete!"));
|
|
4368
|
+
}
|
|
4369
|
+
async function executeTask(_task, options) {
|
|
4370
|
+
const spinner4 = p.spinner();
|
|
4371
|
+
let iteration = 1;
|
|
4372
|
+
let score = 0;
|
|
4373
|
+
let previousScore = 0;
|
|
4374
|
+
do {
|
|
4375
|
+
spinner4.start(`Iteration ${iteration}: Generating code...`);
|
|
4376
|
+
await simulateDelay(2e3);
|
|
4377
|
+
spinner4.stop(`Iteration ${iteration}: Code generated.`);
|
|
4378
|
+
spinner4.start(`Iteration ${iteration}: Running tests...`);
|
|
4379
|
+
await simulateDelay(1500);
|
|
4380
|
+
spinner4.stop(`Iteration ${iteration}: Tests passed.`);
|
|
4381
|
+
spinner4.start(`Iteration ${iteration}: Evaluating quality...`);
|
|
4382
|
+
await simulateDelay(1e3);
|
|
4383
|
+
previousScore = score;
|
|
4384
|
+
score = Math.min(100, 70 + iteration * 8 + Math.random() * 5);
|
|
4385
|
+
spinner4.stop(`Iteration ${iteration}: Quality score: ${score.toFixed(0)}/100`);
|
|
4386
|
+
if (score >= options.minQuality) {
|
|
4387
|
+
const converged = Math.abs(score - previousScore) < 2;
|
|
4388
|
+
if (converged || options.skipReview) {
|
|
4389
|
+
p.log.success(`Task completed in ${iteration} iterations with score ${score.toFixed(0)}`);
|
|
4390
|
+
break;
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
if (iteration < options.maxIterations) {
|
|
4394
|
+
spinner4.start(`Iteration ${iteration}: Analyzing improvements...`);
|
|
4395
|
+
await simulateDelay(1e3);
|
|
4396
|
+
spinner4.stop(`Iteration ${iteration}: Improvements identified.`);
|
|
4397
|
+
}
|
|
4398
|
+
iteration++;
|
|
4399
|
+
} while (iteration <= options.maxIterations);
|
|
4400
|
+
if (score < options.minQuality) {
|
|
4401
|
+
p.log.warn(`Task completed with score ${score.toFixed(0)} (below threshold ${options.minQuality})`);
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
async function loadTasks(_options) {
|
|
4405
|
+
return [
|
|
4406
|
+
{ id: "task-001", title: "Create user entity", description: "Create the User entity with validation" },
|
|
4407
|
+
{ id: "task-002", title: "Implement registration", description: "Create registration endpoint" },
|
|
4408
|
+
{ id: "task-003", title: "Add authentication", description: "Implement JWT authentication" }
|
|
4409
|
+
];
|
|
4410
|
+
}
|
|
4411
|
+
async function checkProjectState() {
|
|
4412
|
+
const fs5 = await import('fs/promises');
|
|
4413
|
+
let hasProject = false;
|
|
4414
|
+
let hasPlan = false;
|
|
4415
|
+
try {
|
|
4416
|
+
await fs5.access(".coco");
|
|
4417
|
+
hasProject = true;
|
|
4418
|
+
} catch {
|
|
4419
|
+
}
|
|
4420
|
+
try {
|
|
4421
|
+
await fs5.access(".coco/planning/backlog.json");
|
|
4422
|
+
hasPlan = true;
|
|
4423
|
+
} catch {
|
|
4424
|
+
}
|
|
4425
|
+
return { hasProject, hasPlan };
|
|
4426
|
+
}
|
|
4427
|
+
function simulateDelay(ms) {
|
|
4428
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4429
|
+
}
|
|
4430
|
+
function registerStatusCommand(program2) {
|
|
4431
|
+
program2.command("status").description("Show current project status and progress").option("-d, --detailed", "Show detailed status").option("-v, --verbose", "Show verbose output including checkpoints").option("--json", "Output as JSON").action(async (options) => {
|
|
4432
|
+
try {
|
|
4433
|
+
await runStatus({ ...options, cwd: process.cwd() });
|
|
4434
|
+
} catch (error) {
|
|
4435
|
+
p.log.error(
|
|
4436
|
+
error instanceof Error ? error.message : "An error occurred"
|
|
4437
|
+
);
|
|
4438
|
+
process.exit(1);
|
|
4439
|
+
}
|
|
4440
|
+
});
|
|
4441
|
+
}
|
|
4442
|
+
async function runStatus(options = {}) {
|
|
4443
|
+
const cwd = options.cwd || process.cwd();
|
|
4444
|
+
const configPath = await findConfigPath(cwd);
|
|
4445
|
+
if (!configPath) {
|
|
4446
|
+
p.log.warning("No project found. Run 'coco init' first.");
|
|
4447
|
+
return {
|
|
4448
|
+
project: "",
|
|
4449
|
+
phase: "idle",
|
|
4450
|
+
progress: 0,
|
|
4451
|
+
checkpoints: []
|
|
4452
|
+
};
|
|
4453
|
+
}
|
|
4454
|
+
const config = await loadConfig(configPath);
|
|
4455
|
+
const state = await loadProjectState(cwd, config);
|
|
4456
|
+
if (options.json) {
|
|
4457
|
+
console.log(JSON.stringify(state, null, 2));
|
|
4458
|
+
return {
|
|
4459
|
+
project: state.name,
|
|
4460
|
+
phase: state.currentPhase,
|
|
4461
|
+
progress: state.progress,
|
|
4462
|
+
sprint: state.sprint,
|
|
4463
|
+
metrics: state.metrics,
|
|
4464
|
+
checkpoints: state.checkpoints
|
|
4465
|
+
};
|
|
4466
|
+
}
|
|
4467
|
+
p.log.info(chalk5.bold(`Project: ${state.name}`));
|
|
4468
|
+
p.log.info(`Phase: ${formatPhaseStatus(state.currentPhase, getPhaseStatusForPhase(state.currentPhase))}`);
|
|
4469
|
+
p.log.info(`Progress: ${formatProgress(state.progress)}`);
|
|
4470
|
+
if (state.sprint) {
|
|
4471
|
+
p.log.info(`Sprint: ${state.sprint.name} (${state.sprint.completed}/${state.sprint.total} tasks)`);
|
|
4472
|
+
}
|
|
4473
|
+
if (options.verbose && state.metrics) {
|
|
4474
|
+
p.log.info(`Average Quality: ${state.metrics.averageQuality}/100`);
|
|
4475
|
+
p.log.info(`Test Coverage: ${state.metrics.testCoverage}%`);
|
|
4476
|
+
p.log.info(`Security Issues: ${state.metrics.securityIssues}`);
|
|
4477
|
+
}
|
|
4478
|
+
if (options.verbose && state.checkpoints.length > 0) {
|
|
4479
|
+
p.log.info(`Checkpoints: ${state.checkpoints.length} available (checkpoint: ${state.checkpoints[0]})`);
|
|
4480
|
+
}
|
|
4481
|
+
return {
|
|
4482
|
+
project: state.name,
|
|
4483
|
+
phase: state.currentPhase,
|
|
4484
|
+
progress: state.progress,
|
|
4485
|
+
sprint: state.sprint,
|
|
4486
|
+
metrics: state.metrics,
|
|
4487
|
+
checkpoints: state.checkpoints
|
|
4488
|
+
};
|
|
4489
|
+
}
|
|
4490
|
+
function formatPhaseStatus(phase, status) {
|
|
4491
|
+
const icons = {
|
|
4492
|
+
pending: "\u25CB",
|
|
4493
|
+
in_progress: "\u2192",
|
|
4494
|
+
completed: "\u2713",
|
|
4495
|
+
failed: "\u2717"
|
|
4496
|
+
};
|
|
4497
|
+
const colors = {
|
|
4498
|
+
pending: chalk5.gray,
|
|
4499
|
+
in_progress: chalk5.yellow,
|
|
4500
|
+
completed: chalk5.green,
|
|
4501
|
+
failed: chalk5.red
|
|
4502
|
+
};
|
|
4503
|
+
const icon = icons[status];
|
|
4504
|
+
const color = colors[status];
|
|
4505
|
+
const phaseName = phase.charAt(0).toUpperCase() + phase.slice(1);
|
|
4506
|
+
return color(`${icon} ${phaseName}`);
|
|
4507
|
+
}
|
|
4508
|
+
function formatProgress(progress) {
|
|
4509
|
+
const percentage = Math.round(progress * 100);
|
|
4510
|
+
const barLength = 20;
|
|
4511
|
+
const filled = Math.round(barLength * progress);
|
|
4512
|
+
const empty = barLength - filled;
|
|
4513
|
+
const bar = chalk5.green("\u2588".repeat(filled)) + chalk5.dim("\u2591".repeat(empty));
|
|
4514
|
+
return `[${bar}] ${percentage}%`;
|
|
4515
|
+
}
|
|
4516
|
+
function getPhaseStatusForPhase(phase) {
|
|
4517
|
+
if (phase === "idle") return "pending";
|
|
4518
|
+
return "in_progress";
|
|
4519
|
+
}
|
|
4520
|
+
async function loadProjectState(cwd, config) {
|
|
4521
|
+
const fs5 = await import('fs/promises');
|
|
4522
|
+
const path5 = await import('path');
|
|
4523
|
+
const statePath = path5.join(cwd, ".coco", "state.json");
|
|
4524
|
+
const backlogPath = path5.join(cwd, ".coco", "planning", "backlog.json");
|
|
4525
|
+
const checkpointDir = path5.join(cwd, ".coco", "checkpoints");
|
|
4526
|
+
let currentPhase = "idle";
|
|
4527
|
+
let metrics;
|
|
4528
|
+
let sprint;
|
|
4529
|
+
let checkpoints = [];
|
|
4530
|
+
try {
|
|
4531
|
+
const stateContent = await fs5.readFile(statePath, "utf-8");
|
|
4532
|
+
const stateData = JSON.parse(stateContent);
|
|
4533
|
+
currentPhase = stateData.currentPhase || "idle";
|
|
4534
|
+
metrics = stateData.metrics;
|
|
4535
|
+
} catch {
|
|
4536
|
+
}
|
|
4537
|
+
try {
|
|
4538
|
+
const backlogContent = await fs5.readFile(backlogPath, "utf-8");
|
|
4539
|
+
const backlogData = JSON.parse(backlogContent);
|
|
4540
|
+
if (backlogData.currentSprint) {
|
|
4541
|
+
const tasks = backlogData.tasks || [];
|
|
4542
|
+
const completedTasks2 = tasks.filter((t) => t.status === "completed");
|
|
4543
|
+
sprint = {
|
|
4544
|
+
id: backlogData.currentSprint.id,
|
|
4545
|
+
name: backlogData.currentSprint.name,
|
|
4546
|
+
completed: completedTasks2.length,
|
|
4547
|
+
total: tasks.length,
|
|
4548
|
+
avgQuality: 0,
|
|
4549
|
+
tasks: tasks.map((t) => ({
|
|
4550
|
+
id: t.id,
|
|
4551
|
+
title: t.title || t.id,
|
|
4552
|
+
status: t.status,
|
|
4553
|
+
score: t.score
|
|
4554
|
+
}))
|
|
4555
|
+
};
|
|
4556
|
+
}
|
|
4557
|
+
} catch {
|
|
4558
|
+
}
|
|
4559
|
+
try {
|
|
4560
|
+
const files = await fs5.readdir(checkpointDir);
|
|
4561
|
+
checkpoints = files.filter((f) => f.endsWith(".json")).sort().reverse();
|
|
4562
|
+
} catch {
|
|
4563
|
+
}
|
|
4564
|
+
const totalTasks = sprint?.total || 0;
|
|
4565
|
+
const completedTasks = sprint?.completed || 0;
|
|
4566
|
+
const progress = totalTasks > 0 ? completedTasks / totalTasks : 0;
|
|
4567
|
+
return {
|
|
4568
|
+
name: config.project?.name || "my-project",
|
|
4569
|
+
currentPhase,
|
|
4570
|
+
progress,
|
|
4571
|
+
sprint,
|
|
4572
|
+
metrics,
|
|
4573
|
+
checkpoints,
|
|
4574
|
+
lastCheckpoint: checkpoints.length > 0 ? {
|
|
4575
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4576
|
+
canResume: true
|
|
4577
|
+
} : void 0
|
|
4578
|
+
};
|
|
4579
|
+
}
|
|
4580
|
+
function registerResumeCommand(program2) {
|
|
4581
|
+
program2.command("resume").description("Resume from the last checkpoint after an interruption").option("-c, --checkpoint <id>", "Resume from a specific checkpoint").option("--list", "List available checkpoints").option("--force", "Force resume even if state is inconsistent").action(async (options) => {
|
|
4582
|
+
await runResume(options);
|
|
4583
|
+
});
|
|
4584
|
+
}
|
|
4585
|
+
async function runResume(options) {
|
|
4586
|
+
p.intro(chalk5.cyan("Corbat-Coco Resume"));
|
|
4587
|
+
const hasProject = await checkProjectExists();
|
|
4588
|
+
if (!hasProject) {
|
|
4589
|
+
p.log.error("No Corbat-Coco project found.");
|
|
4590
|
+
process.exit(1);
|
|
4591
|
+
}
|
|
4592
|
+
if (options.list) {
|
|
4593
|
+
await listCheckpoints();
|
|
4594
|
+
return;
|
|
4595
|
+
}
|
|
4596
|
+
const checkpoint = options.checkpoint ? await loadCheckpoint(options.checkpoint) : await findLatestCheckpoint();
|
|
4597
|
+
if (!checkpoint) {
|
|
4598
|
+
p.log.error("No checkpoint found to resume from.");
|
|
4599
|
+
process.exit(1);
|
|
4600
|
+
}
|
|
4601
|
+
console.log(chalk5.bold("\nCheckpoint Information:"));
|
|
4602
|
+
console.log(chalk5.dim(" ID: ") + checkpoint.id);
|
|
4603
|
+
console.log(chalk5.dim(" Created: ") + checkpoint.timestamp);
|
|
4604
|
+
console.log(chalk5.dim(" Phase: ") + checkpoint.phase);
|
|
4605
|
+
console.log(chalk5.dim(" Task: ") + (checkpoint.currentTask || "None"));
|
|
4606
|
+
const validation = await validateCheckpoint();
|
|
4607
|
+
if (!validation.valid && !options.force) {
|
|
4608
|
+
p.log.error("Checkpoint validation failed:");
|
|
4609
|
+
for (const issue of validation.issues) {
|
|
4610
|
+
console.log(chalk5.red(" - " + issue));
|
|
4611
|
+
}
|
|
4612
|
+
console.log(chalk5.dim("\nUse --force to resume anyway (may cause issues)."));
|
|
4613
|
+
process.exit(1);
|
|
4614
|
+
}
|
|
4615
|
+
const shouldResume = await p.confirm({
|
|
4616
|
+
message: `Resume from checkpoint ${checkpoint.id}?`
|
|
4617
|
+
});
|
|
4618
|
+
if (p.isCancel(shouldResume) || !shouldResume) {
|
|
4619
|
+
p.cancel("Resume cancelled.");
|
|
4620
|
+
process.exit(0);
|
|
4621
|
+
}
|
|
4622
|
+
const spinner4 = p.spinner();
|
|
4623
|
+
spinner4.start("Restoring state from checkpoint...");
|
|
4624
|
+
try {
|
|
4625
|
+
await restoreFromCheckpoint(checkpoint);
|
|
4626
|
+
spinner4.stop("State restored successfully.");
|
|
4627
|
+
} catch (error) {
|
|
4628
|
+
spinner4.stop("Failed to restore state.");
|
|
4629
|
+
throw error;
|
|
4630
|
+
}
|
|
4631
|
+
p.log.success(`Resuming from phase: ${checkpoint.phase}`);
|
|
4632
|
+
p.outro(chalk5.green("Ready to continue. Run 'coco build' to proceed."));
|
|
4633
|
+
}
|
|
4634
|
+
async function listCheckpoints() {
|
|
4635
|
+
const checkpoints = [
|
|
4636
|
+
{
|
|
4637
|
+
id: "cp-2024-01-15-001",
|
|
4638
|
+
timestamp: "2024-01-15T10:30:00Z",
|
|
4639
|
+
phase: "complete",
|
|
4640
|
+
currentTask: "task-003",
|
|
4641
|
+
completedTasks: ["task-001", "task-002"],
|
|
4642
|
+
canResume: true
|
|
4643
|
+
},
|
|
4644
|
+
{
|
|
4645
|
+
id: "cp-2024-01-15-002",
|
|
4646
|
+
timestamp: "2024-01-15T11:00:00Z",
|
|
4647
|
+
phase: "complete",
|
|
4648
|
+
currentTask: "task-003",
|
|
4649
|
+
completedTasks: ["task-001", "task-002"],
|
|
4650
|
+
canResume: true
|
|
4651
|
+
}
|
|
4652
|
+
];
|
|
4653
|
+
console.log(chalk5.bold("\nAvailable Checkpoints:"));
|
|
4654
|
+
console.log("");
|
|
4655
|
+
for (const cp of checkpoints) {
|
|
4656
|
+
const status = cp.canResume ? chalk5.green("") : chalk5.red("");
|
|
4657
|
+
console.log(` ${status} ${chalk5.cyan(cp.id)}`);
|
|
4658
|
+
console.log(chalk5.dim(` Created: ${cp.timestamp}`));
|
|
4659
|
+
console.log(chalk5.dim(` Phase: ${cp.phase}`));
|
|
4660
|
+
console.log(chalk5.dim(` Task: ${cp.currentTask || "None"}`));
|
|
4661
|
+
console.log("");
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
async function loadCheckpoint(id) {
|
|
4665
|
+
return {
|
|
4666
|
+
id,
|
|
4667
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4668
|
+
phase: "complete",
|
|
4669
|
+
currentTask: "task-003",
|
|
4670
|
+
completedTasks: ["task-001", "task-002"],
|
|
4671
|
+
canResume: true
|
|
4672
|
+
};
|
|
4673
|
+
}
|
|
4674
|
+
async function findLatestCheckpoint() {
|
|
4675
|
+
return {
|
|
4676
|
+
id: "cp-2024-01-15-002",
|
|
4677
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4678
|
+
phase: "complete",
|
|
4679
|
+
currentTask: "task-003",
|
|
4680
|
+
completedTasks: ["task-001", "task-002"],
|
|
4681
|
+
canResume: true
|
|
4682
|
+
};
|
|
4683
|
+
}
|
|
4684
|
+
async function validateCheckpoint(_checkpoint) {
|
|
4685
|
+
const issues = [];
|
|
4686
|
+
return {
|
|
4687
|
+
valid: issues.length === 0,
|
|
4688
|
+
issues
|
|
4689
|
+
};
|
|
4690
|
+
}
|
|
4691
|
+
async function restoreFromCheckpoint(_checkpoint) {
|
|
4692
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
4693
|
+
}
|
|
4694
|
+
async function checkProjectExists() {
|
|
4695
|
+
try {
|
|
4696
|
+
const fs5 = await import('fs/promises');
|
|
4697
|
+
await fs5.access(".coco");
|
|
4698
|
+
return true;
|
|
4699
|
+
} catch {
|
|
4700
|
+
return false;
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
function registerConfigCommand(program2) {
|
|
4704
|
+
const configCmd = program2.command("config").description("Manage Corbat-Coco configuration");
|
|
4705
|
+
configCmd.command("get <key>").description("Get a configuration value").action(async (key) => {
|
|
4706
|
+
await runConfigGet(key);
|
|
4707
|
+
});
|
|
4708
|
+
configCmd.command("set <key> <value>").description("Set a configuration value").action(async (key, value) => {
|
|
4709
|
+
await runConfigSet(key, value);
|
|
4710
|
+
});
|
|
4711
|
+
configCmd.command("list").description("List all configuration values").option("--json", "Output as JSON").action(async (options) => {
|
|
4712
|
+
await runConfigList(options);
|
|
4713
|
+
});
|
|
4714
|
+
configCmd.command("init").description("Initialize configuration interactively").action(async () => {
|
|
4715
|
+
await runConfigInit();
|
|
4716
|
+
});
|
|
4717
|
+
}
|
|
4718
|
+
async function runConfigGet(key) {
|
|
4719
|
+
const config = await loadConfig2();
|
|
4720
|
+
const value = getNestedValue(config, key);
|
|
4721
|
+
if (value === void 0) {
|
|
4722
|
+
p.log.error(`Configuration key '${key}' not found.`);
|
|
4723
|
+
process.exit(1);
|
|
4724
|
+
}
|
|
4725
|
+
console.log(typeof value === "object" ? JSON.stringify(value, null, 2) : String(value));
|
|
4726
|
+
}
|
|
4727
|
+
async function runConfigSet(key, value) {
|
|
4728
|
+
const config = await loadConfig2();
|
|
4729
|
+
let parsedValue;
|
|
4730
|
+
try {
|
|
4731
|
+
parsedValue = JSON.parse(value);
|
|
4732
|
+
} catch {
|
|
4733
|
+
parsedValue = value;
|
|
4734
|
+
}
|
|
4735
|
+
setNestedValue(config, key, parsedValue);
|
|
4736
|
+
await saveConfig(config);
|
|
4737
|
+
p.log.success(`Set ${key} = ${value}`);
|
|
4738
|
+
}
|
|
4739
|
+
async function runConfigList(options) {
|
|
4740
|
+
const config = await loadConfig2();
|
|
4741
|
+
if (options.json) {
|
|
4742
|
+
console.log(JSON.stringify(config, null, 2));
|
|
4743
|
+
return;
|
|
4744
|
+
}
|
|
4745
|
+
console.log(chalk5.bold("\nCorbat-Coco Configuration:\n"));
|
|
4746
|
+
printConfig(config, "");
|
|
4747
|
+
}
|
|
4748
|
+
async function runConfigInit() {
|
|
4749
|
+
p.intro(chalk5.cyan("Corbat-Coco Configuration Setup"));
|
|
4750
|
+
const apiKey = await p.text({
|
|
4751
|
+
message: "Enter your Anthropic API key:",
|
|
4752
|
+
placeholder: "sk-ant-...",
|
|
4753
|
+
validate: (value) => {
|
|
4754
|
+
if (!value) return "API key is required";
|
|
4755
|
+
if (!value.startsWith("sk-ant-")) return "Invalid API key format";
|
|
4756
|
+
return void 0;
|
|
4757
|
+
}
|
|
4758
|
+
});
|
|
4759
|
+
if (p.isCancel(apiKey)) {
|
|
4760
|
+
p.cancel("Configuration cancelled.");
|
|
4761
|
+
process.exit(0);
|
|
4762
|
+
}
|
|
4763
|
+
const model = await p.select({
|
|
4764
|
+
message: "Select the default model:",
|
|
4765
|
+
options: [
|
|
4766
|
+
{ value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4", hint: "Recommended for coding" },
|
|
4767
|
+
{ value: "claude-opus-4-20250514", label: "Claude Opus 4", hint: "Most capable" },
|
|
4768
|
+
{ value: "claude-3-5-sonnet-20241022", label: "Claude 3.5 Sonnet", hint: "Fast and capable" }
|
|
4769
|
+
]
|
|
4770
|
+
});
|
|
4771
|
+
if (p.isCancel(model)) {
|
|
4772
|
+
p.cancel("Configuration cancelled.");
|
|
4773
|
+
process.exit(0);
|
|
4774
|
+
}
|
|
4775
|
+
const quality = await p.text({
|
|
4776
|
+
message: "Minimum quality score (0-100):",
|
|
4777
|
+
placeholder: "85",
|
|
4778
|
+
initialValue: "85",
|
|
4779
|
+
validate: (value) => {
|
|
4780
|
+
const num = parseInt(value, 10);
|
|
4781
|
+
if (isNaN(num) || num < 0 || num > 100) return "Must be a number between 0 and 100";
|
|
4782
|
+
return void 0;
|
|
4783
|
+
}
|
|
4784
|
+
});
|
|
4785
|
+
if (p.isCancel(quality)) {
|
|
4786
|
+
p.cancel("Configuration cancelled.");
|
|
4787
|
+
process.exit(0);
|
|
4788
|
+
}
|
|
4789
|
+
const config = {
|
|
4790
|
+
provider: {
|
|
4791
|
+
type: "anthropic",
|
|
4792
|
+
apiKey,
|
|
4793
|
+
model
|
|
4794
|
+
},
|
|
4795
|
+
quality: {
|
|
4796
|
+
minScore: parseInt(quality, 10),
|
|
4797
|
+
minCoverage: 80,
|
|
4798
|
+
maxIterations: 10
|
|
4799
|
+
}
|
|
4800
|
+
};
|
|
4801
|
+
await saveConfig(config);
|
|
4802
|
+
p.outro(chalk5.green("Configuration saved to .coco/config.json"));
|
|
4803
|
+
}
|
|
4804
|
+
async function loadConfig2() {
|
|
4805
|
+
return {
|
|
4806
|
+
provider: {
|
|
4807
|
+
type: "anthropic",
|
|
4808
|
+
model: "claude-sonnet-4-20250514"
|
|
4809
|
+
},
|
|
4810
|
+
quality: {
|
|
4811
|
+
minScore: 85,
|
|
4812
|
+
minCoverage: 80,
|
|
4813
|
+
maxIterations: 10
|
|
4814
|
+
},
|
|
4815
|
+
persistence: {
|
|
4816
|
+
checkpointInterval: 3e5,
|
|
4817
|
+
maxCheckpoints: 50
|
|
4818
|
+
}
|
|
4819
|
+
};
|
|
4820
|
+
}
|
|
4821
|
+
async function saveConfig(config) {
|
|
4822
|
+
const fs5 = await import('fs/promises');
|
|
4823
|
+
await fs5.mkdir(".coco", { recursive: true });
|
|
4824
|
+
await fs5.writeFile(".coco/config.json", JSON.stringify(config, null, 2));
|
|
4825
|
+
}
|
|
4826
|
+
function getNestedValue(obj, path5) {
|
|
4827
|
+
const keys = path5.split(".");
|
|
4828
|
+
let current = obj;
|
|
4829
|
+
for (const key of keys) {
|
|
4830
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
4831
|
+
return void 0;
|
|
4832
|
+
}
|
|
4833
|
+
current = current[key];
|
|
4834
|
+
}
|
|
4835
|
+
return current;
|
|
4836
|
+
}
|
|
4837
|
+
function setNestedValue(obj, path5, value) {
|
|
4838
|
+
const keys = path5.split(".");
|
|
4839
|
+
let current = obj;
|
|
4840
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
4841
|
+
const key = keys[i];
|
|
4842
|
+
if (!key) continue;
|
|
4843
|
+
if (!(key in current) || typeof current[key] !== "object") {
|
|
4844
|
+
current[key] = {};
|
|
4845
|
+
}
|
|
4846
|
+
current = current[key];
|
|
4847
|
+
}
|
|
4848
|
+
const lastKey = keys[keys.length - 1];
|
|
4849
|
+
if (lastKey) {
|
|
4850
|
+
current[lastKey] = value;
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
function printConfig(obj, prefix) {
|
|
4854
|
+
if (obj === null || obj === void 0) {
|
|
4855
|
+
console.log(chalk5.dim(prefix) + chalk5.yellow("null"));
|
|
4856
|
+
return;
|
|
4857
|
+
}
|
|
4858
|
+
if (typeof obj !== "object") {
|
|
4859
|
+
console.log(chalk5.dim(prefix) + String(obj));
|
|
4860
|
+
return;
|
|
4861
|
+
}
|
|
4862
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
4863
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
4864
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
4865
|
+
printConfig(value, fullKey);
|
|
4866
|
+
} else {
|
|
4867
|
+
const displayValue = typeof value === "string" && value.startsWith("sk-") ? chalk5.dim("[hidden]") : chalk5.cyan(JSON.stringify(value));
|
|
4868
|
+
console.log(` ${chalk5.dim(fullKey + ":")} ${displayValue}`);
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
|
|
4873
|
+
// src/cli/index.ts
|
|
4874
|
+
var program = new Command();
|
|
4875
|
+
program.name("coco").description("Corbat-Coco: Autonomous Coding Agent with Self-Review and Quality Convergence").version(VERSION, "-v, --version", "Output the current version");
|
|
4876
|
+
registerInitCommand(program);
|
|
4877
|
+
registerPlanCommand(program);
|
|
4878
|
+
registerBuildCommand(program);
|
|
4879
|
+
registerStatusCommand(program);
|
|
4880
|
+
registerResumeCommand(program);
|
|
4881
|
+
registerConfigCommand(program);
|
|
4882
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
4883
|
+
console.error("Fatal error:", error);
|
|
4884
|
+
process.exit(1);
|
|
4885
|
+
});
|
|
4886
|
+
//# sourceMappingURL=index.js.map
|
|
4887
|
+
//# sourceMappingURL=index.js.map
|