chainlesschain 0.37.10 → 0.37.12
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/README.md +166 -10
- package/package.json +1 -1
- package/src/commands/a2a.js +374 -0
- package/src/commands/bi.js +240 -0
- package/src/commands/cowork.js +317 -0
- package/src/commands/economy.js +375 -0
- package/src/commands/evolution.js +398 -0
- package/src/commands/hmemory.js +273 -0
- package/src/commands/hook.js +260 -0
- package/src/commands/init.js +184 -0
- package/src/commands/lowcode.js +320 -0
- package/src/commands/plugin.js +55 -2
- package/src/commands/sandbox.js +366 -0
- package/src/commands/skill.js +254 -201
- package/src/commands/workflow.js +359 -0
- package/src/commands/zkp.js +277 -0
- package/src/index.js +44 -0
- package/src/lib/a2a-protocol.js +371 -0
- package/src/lib/agent-coordinator.js +273 -0
- package/src/lib/agent-economy.js +369 -0
- package/src/lib/app-builder.js +377 -0
- package/src/lib/bi-engine.js +299 -0
- package/src/lib/cowork/ab-comparator-cli.js +180 -0
- package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
- package/src/lib/cowork/debate-review-cli.js +144 -0
- package/src/lib/cowork/decision-kb-cli.js +153 -0
- package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
- package/src/lib/cowork-adapter.js +106 -0
- package/src/lib/evolution-system.js +508 -0
- package/src/lib/hierarchical-memory.js +471 -0
- package/src/lib/hook-manager.js +387 -0
- package/src/lib/plugin-manager.js +118 -0
- package/src/lib/project-detector.js +53 -0
- package/src/lib/sandbox-v2.js +503 -0
- package/src/lib/service-container.js +183 -0
- package/src/lib/skill-loader.js +274 -0
- package/src/lib/workflow-engine.js +503 -0
- package/src/lib/zkp-engine.js +241 -0
- package/src/repl/agent-repl.js +117 -112
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A/B Comparator for CLI
|
|
3
|
+
*
|
|
4
|
+
* Generates N solution variants for a prompt using different agent configurations,
|
|
5
|
+
* then scores and ranks them against specified criteria.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createChatFn } from "../cowork-adapter.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_CRITERIA = ["quality", "performance", "readability"];
|
|
11
|
+
|
|
12
|
+
const VARIANT_PROFILES = [
|
|
13
|
+
{
|
|
14
|
+
name: "conservative",
|
|
15
|
+
system:
|
|
16
|
+
"You are a conservative software engineer who favors proven patterns, stability, and minimal dependencies. Prefer simple, well-tested approaches over cutting-edge solutions.",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "innovative",
|
|
20
|
+
system:
|
|
21
|
+
"You are an innovative software engineer who favors modern patterns, new APIs, and elegant abstractions. Prioritize developer experience and future-proofing.",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "pragmatic",
|
|
25
|
+
system:
|
|
26
|
+
"You are a pragmatic software engineer who balances simplicity with capability. Choose the approach that ships fastest while maintaining acceptable quality.",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "performance-focused",
|
|
30
|
+
system:
|
|
31
|
+
"You are a performance-oriented engineer. Optimize for speed, memory efficiency, and minimal overhead. Accept complexity if it yields measurable performance gains.",
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate and compare N solution variants
|
|
37
|
+
*
|
|
38
|
+
* @param {object} params
|
|
39
|
+
* @param {string} params.prompt - The task/problem description
|
|
40
|
+
* @param {number} [params.variants=3] - Number of variants to generate
|
|
41
|
+
* @param {string[]} [params.criteria] - Scoring criteria
|
|
42
|
+
* @param {object} [params.llmOptions] - LLM provider options
|
|
43
|
+
* @returns {Promise<object>} Comparison result with ranked variants
|
|
44
|
+
*/
|
|
45
|
+
export async function compare({
|
|
46
|
+
prompt,
|
|
47
|
+
variants = 3,
|
|
48
|
+
criteria = DEFAULT_CRITERIA,
|
|
49
|
+
llmOptions = {},
|
|
50
|
+
}) {
|
|
51
|
+
const chat = createChatFn(llmOptions);
|
|
52
|
+
const numVariants = Math.min(variants, VARIANT_PROFILES.length);
|
|
53
|
+
const profiles = VARIANT_PROFILES.slice(0, numVariants);
|
|
54
|
+
const generatedVariants = [];
|
|
55
|
+
|
|
56
|
+
// Phase 1: Generate variants
|
|
57
|
+
for (const profile of profiles) {
|
|
58
|
+
const messages = [
|
|
59
|
+
{ role: "system", content: profile.system },
|
|
60
|
+
{
|
|
61
|
+
role: "user",
|
|
62
|
+
content: `Provide a solution for the following task. Include code if applicable.\n\nTask: ${prompt}\n\nProvide your solution with:\n1. Approach summary (1-2 sentences)\n2. Implementation (code or detailed steps)\n3. Trade-offs (pros and cons of this approach)`,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const response = await chat(messages, { maxTokens: 2000 });
|
|
68
|
+
generatedVariants.push({
|
|
69
|
+
name: profile.name,
|
|
70
|
+
profile: profile.system,
|
|
71
|
+
solution: response,
|
|
72
|
+
});
|
|
73
|
+
} catch (err) {
|
|
74
|
+
generatedVariants.push({
|
|
75
|
+
name: profile.name,
|
|
76
|
+
profile: profile.system,
|
|
77
|
+
solution: `Error generating variant: ${err.message}`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Phase 2: Score each variant against criteria
|
|
83
|
+
const scoringPrompt = `You are an impartial judge evaluating ${numVariants} solution variants against these criteria: ${criteria.join(", ")}.
|
|
84
|
+
|
|
85
|
+
For each variant, assign a score from 1-10 for each criterion. Then provide an overall ranking.
|
|
86
|
+
|
|
87
|
+
${generatedVariants
|
|
88
|
+
.map((v, i) => `### Variant ${i + 1}: ${v.name}\n${v.solution}`)
|
|
89
|
+
.join("\n\n---\n\n")}
|
|
90
|
+
|
|
91
|
+
Respond in this exact format for each variant:
|
|
92
|
+
SCORES:
|
|
93
|
+
${generatedVariants.map((v, i) => `Variant ${i + 1} (${v.name}): ${criteria.map((c) => `${c}=X`).join(", ")}`).join("\n")}
|
|
94
|
+
|
|
95
|
+
RANKING: (best to worst, comma-separated variant names)
|
|
96
|
+
WINNER: (name of the best variant)
|
|
97
|
+
REASON: (1-2 sentence justification)`;
|
|
98
|
+
|
|
99
|
+
let scores = [];
|
|
100
|
+
let ranking = [];
|
|
101
|
+
let winner = "";
|
|
102
|
+
let reason = "";
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const judgement = await chat(
|
|
106
|
+
[
|
|
107
|
+
{
|
|
108
|
+
role: "system",
|
|
109
|
+
content:
|
|
110
|
+
"You are an impartial technical evaluator. Score solutions objectively based on the given criteria.",
|
|
111
|
+
},
|
|
112
|
+
{ role: "user", content: scoringPrompt },
|
|
113
|
+
],
|
|
114
|
+
{ maxTokens: 1500 },
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Parse scores
|
|
118
|
+
scores = parseScores(judgement, generatedVariants, criteria);
|
|
119
|
+
ranking = parseRanking(judgement, generatedVariants);
|
|
120
|
+
winner = parseWinner(judgement) || generatedVariants[0]?.name || "unknown";
|
|
121
|
+
reason = parseReason(judgement) || "See detailed scores above.";
|
|
122
|
+
} catch (err) {
|
|
123
|
+
reason = `Scoring error: ${err.message}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
prompt,
|
|
128
|
+
criteria,
|
|
129
|
+
variants: generatedVariants.map((v, i) => ({
|
|
130
|
+
...v,
|
|
131
|
+
scores: scores[i] || {},
|
|
132
|
+
totalScore: scores[i]
|
|
133
|
+
? Object.values(scores[i]).reduce((a, b) => a + b, 0)
|
|
134
|
+
: 0,
|
|
135
|
+
})),
|
|
136
|
+
ranking,
|
|
137
|
+
winner,
|
|
138
|
+
reason,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseScores(text, variants, criteria) {
|
|
143
|
+
const scores = [];
|
|
144
|
+
for (let i = 0; i < variants.length; i++) {
|
|
145
|
+
const variantScores = {};
|
|
146
|
+
for (const c of criteria) {
|
|
147
|
+
const pattern = new RegExp(
|
|
148
|
+
`variant\\s*${i + 1}[^\\n]*${c}\\s*=\\s*(\\d+)`,
|
|
149
|
+
"i",
|
|
150
|
+
);
|
|
151
|
+
const match = text.match(pattern);
|
|
152
|
+
variantScores[c] = match ? parseInt(match[1], 10) : 5;
|
|
153
|
+
}
|
|
154
|
+
scores.push(variantScores);
|
|
155
|
+
}
|
|
156
|
+
return scores;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseRanking(text, variants) {
|
|
160
|
+
const match = text.match(/RANKING:\s*(.+)/i);
|
|
161
|
+
if (match) {
|
|
162
|
+
return match[1]
|
|
163
|
+
.split(",")
|
|
164
|
+
.map((s) => s.trim())
|
|
165
|
+
.filter(Boolean);
|
|
166
|
+
}
|
|
167
|
+
return variants.map((v) => v.name);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseWinner(text) {
|
|
171
|
+
const match = text.match(/WINNER:\s*(.+)/i);
|
|
172
|
+
return match ? match[1].trim() : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseReason(text) {
|
|
176
|
+
const match = text.match(/REASON:\s*(.+)/i);
|
|
177
|
+
return match ? match[1].trim() : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { DEFAULT_CRITERIA, VARIANT_PROFILES };
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Knowledge Graph for CLI
|
|
3
|
+
*
|
|
4
|
+
* Builds a lightweight entity-relationship graph from source code:
|
|
5
|
+
* - Files, classes, functions, imports, exports
|
|
6
|
+
* - Dependency relationships
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
|
|
12
|
+
const CODE_EXTENSIONS = new Set([
|
|
13
|
+
".js",
|
|
14
|
+
".ts",
|
|
15
|
+
".jsx",
|
|
16
|
+
".tsx",
|
|
17
|
+
".vue",
|
|
18
|
+
".py",
|
|
19
|
+
".java",
|
|
20
|
+
".go",
|
|
21
|
+
".rs",
|
|
22
|
+
".rb",
|
|
23
|
+
".kt",
|
|
24
|
+
".swift",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build a knowledge graph from a file or directory
|
|
29
|
+
*
|
|
30
|
+
* @param {object} params
|
|
31
|
+
* @param {string} params.targetPath - File or directory to analyze
|
|
32
|
+
* @param {number} [params.maxFiles=50] - Max files to process
|
|
33
|
+
* @returns {Promise<object>} Knowledge graph
|
|
34
|
+
*/
|
|
35
|
+
export async function buildKnowledgeGraph({ targetPath, maxFiles = 50 }) {
|
|
36
|
+
const entities = [];
|
|
37
|
+
const relationships = [];
|
|
38
|
+
|
|
39
|
+
const stat = fs.statSync(targetPath);
|
|
40
|
+
const files = stat.isDirectory()
|
|
41
|
+
? collectFiles(targetPath, maxFiles)
|
|
42
|
+
: [targetPath];
|
|
43
|
+
|
|
44
|
+
for (const filePath of files) {
|
|
45
|
+
const ext = path.extname(filePath);
|
|
46
|
+
if (!CODE_EXTENSIONS.has(ext)) continue;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
50
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
51
|
+
|
|
52
|
+
entities.push({
|
|
53
|
+
type: "file",
|
|
54
|
+
name: relativePath,
|
|
55
|
+
language: ext.slice(1),
|
|
56
|
+
lines: content.split("\n").length,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Extract imports
|
|
60
|
+
const imports = extractImports(content, ext);
|
|
61
|
+
for (const imp of imports) {
|
|
62
|
+
relationships.push({
|
|
63
|
+
from: relativePath,
|
|
64
|
+
to: imp,
|
|
65
|
+
type: "imports",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Extract exports / public APIs
|
|
70
|
+
const exports = extractExports(content, ext);
|
|
71
|
+
for (const exp of exports) {
|
|
72
|
+
entities.push({
|
|
73
|
+
type: "export",
|
|
74
|
+
name: exp.name,
|
|
75
|
+
kind: exp.kind,
|
|
76
|
+
file: relativePath,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Extract classes/functions
|
|
81
|
+
const definitions = extractDefinitions(content, ext);
|
|
82
|
+
for (const def of definitions) {
|
|
83
|
+
entities.push({
|
|
84
|
+
type: def.kind,
|
|
85
|
+
name: def.name,
|
|
86
|
+
file: relativePath,
|
|
87
|
+
line: def.line,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Skip unreadable files
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build summary
|
|
96
|
+
const fileCount = entities.filter((e) => e.type === "file").length;
|
|
97
|
+
const exportCount = entities.filter((e) => e.type === "export").length;
|
|
98
|
+
const defCount = entities.filter(
|
|
99
|
+
(e) => e.type === "class" || e.type === "function",
|
|
100
|
+
).length;
|
|
101
|
+
|
|
102
|
+
const summary = [
|
|
103
|
+
`Code Knowledge Graph for: ${targetPath}`,
|
|
104
|
+
` Files analyzed: ${fileCount}`,
|
|
105
|
+
` Entities: ${entities.length} (${defCount} definitions, ${exportCount} exports)`,
|
|
106
|
+
` Relationships: ${relationships.length}`,
|
|
107
|
+
"",
|
|
108
|
+
"Top imports:",
|
|
109
|
+
...getTopImports(relationships).map((r) => ` ${r.to} (${r.count} refs)`),
|
|
110
|
+
].join("\n");
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
entities,
|
|
114
|
+
relationships,
|
|
115
|
+
stats: {
|
|
116
|
+
fileCount,
|
|
117
|
+
exportCount,
|
|
118
|
+
defCount,
|
|
119
|
+
relationshipCount: relationships.length,
|
|
120
|
+
},
|
|
121
|
+
summary,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function collectFiles(dir, maxFiles) {
|
|
126
|
+
const files = [];
|
|
127
|
+
const queue = [dir];
|
|
128
|
+
|
|
129
|
+
while (queue.length > 0 && files.length < maxFiles) {
|
|
130
|
+
const current = queue.shift();
|
|
131
|
+
try {
|
|
132
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
135
|
+
continue;
|
|
136
|
+
const fullPath = path.join(current, entry.name);
|
|
137
|
+
if (entry.isDirectory()) {
|
|
138
|
+
queue.push(fullPath);
|
|
139
|
+
} else if (CODE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
140
|
+
files.push(fullPath);
|
|
141
|
+
if (files.length >= maxFiles) break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// Skip unreadable dirs
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return files;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function extractImports(content, ext) {
|
|
153
|
+
const imports = [];
|
|
154
|
+
|
|
155
|
+
// ES6 imports
|
|
156
|
+
const esImportRe = /import\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
157
|
+
let match;
|
|
158
|
+
while ((match = esImportRe.exec(content))) {
|
|
159
|
+
imports.push(match[1]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// CommonJS require
|
|
163
|
+
const cjsRe = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
164
|
+
while ((match = cjsRe.exec(content))) {
|
|
165
|
+
imports.push(match[1]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Python imports
|
|
169
|
+
if (ext === ".py") {
|
|
170
|
+
const pyRe = /(?:from|import)\s+([\w.]+)/g;
|
|
171
|
+
while ((match = pyRe.exec(content))) {
|
|
172
|
+
imports.push(match[1]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return [...new Set(imports)];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function extractExports(content, ext) {
|
|
180
|
+
const exports = [];
|
|
181
|
+
|
|
182
|
+
// ES6 exports
|
|
183
|
+
const esExportRe =
|
|
184
|
+
/export\s+(?:default\s+)?(?:(?:async\s+)?function|class|const|let|var)\s+(\w+)/g;
|
|
185
|
+
let match;
|
|
186
|
+
while ((match = esExportRe.exec(content))) {
|
|
187
|
+
exports.push({ name: match[1], kind: "named" });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// module.exports
|
|
191
|
+
if (/module\.exports\s*=/.test(content)) {
|
|
192
|
+
exports.push({ name: "default", kind: "cjs" });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return exports;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function extractDefinitions(content, ext) {
|
|
199
|
+
const defs = [];
|
|
200
|
+
const lines = content.split("\n");
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < lines.length; i++) {
|
|
203
|
+
const line = lines[i];
|
|
204
|
+
|
|
205
|
+
// Class definitions
|
|
206
|
+
const classMatch = line.match(/(?:export\s+)?class\s+(\w+)/);
|
|
207
|
+
if (classMatch) {
|
|
208
|
+
defs.push({ kind: "class", name: classMatch[1], line: i + 1 });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Function definitions
|
|
212
|
+
const funcMatch = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)/);
|
|
213
|
+
if (funcMatch) {
|
|
214
|
+
defs.push({ kind: "function", name: funcMatch[1], line: i + 1 });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return defs;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getTopImports(relationships) {
|
|
222
|
+
const counts = {};
|
|
223
|
+
for (const r of relationships) {
|
|
224
|
+
if (r.type === "imports") {
|
|
225
|
+
counts[r.to] = (counts[r.to] || 0) + 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return Object.entries(counts)
|
|
229
|
+
.sort((a, b) => b[1] - a[1])
|
|
230
|
+
.slice(0, 10)
|
|
231
|
+
.map(([to, count]) => ({ to, count }));
|
|
232
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-perspective debate review for CLI
|
|
3
|
+
*
|
|
4
|
+
* Spawns multiple reviewer agents (performance, security, maintainability, etc.)
|
|
5
|
+
* Each reviews independently via LLM, then a moderator synthesizes a verdict.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createChatFn } from "../cowork-adapter.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PERSPECTIVES = ["performance", "security", "maintainability"];
|
|
11
|
+
|
|
12
|
+
const PERSPECTIVE_PROMPTS = {
|
|
13
|
+
performance: {
|
|
14
|
+
role: "Performance Reviewer",
|
|
15
|
+
system:
|
|
16
|
+
"You are a performance-focused code reviewer. Analyze code for time complexity, memory usage, unnecessary allocations, blocking operations, N+1 queries, and scalability issues. Be specific and cite line numbers when possible.",
|
|
17
|
+
},
|
|
18
|
+
security: {
|
|
19
|
+
role: "Security Reviewer",
|
|
20
|
+
system:
|
|
21
|
+
"You are a security-focused code reviewer. Analyze code for injection vulnerabilities, authentication/authorization issues, data exposure, insecure defaults, OWASP top 10, and supply chain risks. Be specific and cite line numbers when possible.",
|
|
22
|
+
},
|
|
23
|
+
maintainability: {
|
|
24
|
+
role: "Maintainability Reviewer",
|
|
25
|
+
system:
|
|
26
|
+
"You are a maintainability-focused code reviewer. Analyze code for readability, naming conventions, coupling, cohesion, DRY violations, missing error handling, test coverage gaps, and documentation needs. Be specific and cite line numbers when possible.",
|
|
27
|
+
},
|
|
28
|
+
correctness: {
|
|
29
|
+
role: "Correctness Reviewer",
|
|
30
|
+
system:
|
|
31
|
+
"You are a correctness-focused code reviewer. Analyze code for logic errors, off-by-one bugs, race conditions, null/undefined handling, edge cases, type mismatches, and incorrect assumptions. Be specific and cite line numbers when possible.",
|
|
32
|
+
},
|
|
33
|
+
architecture: {
|
|
34
|
+
role: "Architecture Reviewer",
|
|
35
|
+
system:
|
|
36
|
+
"You are an architecture-focused code reviewer. Analyze code for separation of concerns, dependency management, design pattern usage, extensibility, and alignment with established project patterns. Be specific.",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run a multi-perspective debate review
|
|
42
|
+
*
|
|
43
|
+
* @param {object} params
|
|
44
|
+
* @param {string} params.target - Description of what's being reviewed
|
|
45
|
+
* @param {string} params.code - The code to review
|
|
46
|
+
* @param {string[]} [params.perspectives] - Perspectives to use
|
|
47
|
+
* @param {object} [params.llmOptions] - LLM provider options
|
|
48
|
+
* @returns {Promise<object>} Review result with verdict and reviews
|
|
49
|
+
*/
|
|
50
|
+
export async function startDebate({
|
|
51
|
+
target,
|
|
52
|
+
code,
|
|
53
|
+
perspectives = DEFAULT_PERSPECTIVES,
|
|
54
|
+
llmOptions = {},
|
|
55
|
+
}) {
|
|
56
|
+
const chat = createChatFn(llmOptions);
|
|
57
|
+
const reviews = [];
|
|
58
|
+
|
|
59
|
+
// Phase 1: Independent reviews from each perspective
|
|
60
|
+
for (const perspective of perspectives) {
|
|
61
|
+
const config = PERSPECTIVE_PROMPTS[perspective] || {
|
|
62
|
+
role: `${perspective} Reviewer`,
|
|
63
|
+
system: `You are a ${perspective}-focused code reviewer. Provide specific, actionable feedback.`,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const messages = [
|
|
67
|
+
{ role: "system", content: config.system },
|
|
68
|
+
{
|
|
69
|
+
role: "user",
|
|
70
|
+
content: `Review the following code/content.\n\nTarget: ${target}\n\n\`\`\`\n${code}\n\`\`\`\n\nProvide your review as a ${config.role}. Format your response as:\n\n## Issues Found\n- List each issue with severity (HIGH/MEDIUM/LOW)\n\n## Recommendations\n- List specific improvements\n\n## Verdict\nAPPROVE, NEEDS_WORK, or REJECT with a brief reason.`,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const response = await chat(messages, { maxTokens: 1500 });
|
|
76
|
+
const verdict = extractVerdict(response);
|
|
77
|
+
reviews.push({
|
|
78
|
+
perspective,
|
|
79
|
+
role: config.role,
|
|
80
|
+
review: response,
|
|
81
|
+
verdict,
|
|
82
|
+
});
|
|
83
|
+
} catch (err) {
|
|
84
|
+
reviews.push({
|
|
85
|
+
perspective,
|
|
86
|
+
role: config.role,
|
|
87
|
+
review: `Error: ${err.message}`,
|
|
88
|
+
verdict: "ERROR",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Phase 2: Moderator synthesizes final verdict
|
|
94
|
+
const moderatorMessages = [
|
|
95
|
+
{
|
|
96
|
+
role: "system",
|
|
97
|
+
content:
|
|
98
|
+
"You are a senior engineering lead moderating a code review. Synthesize the perspectives below into a final verdict. Weight security and correctness issues higher than style concerns.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
role: "user",
|
|
102
|
+
content: `Multiple reviewers analyzed this code. Synthesize their findings into a final verdict.\n\nTarget: ${target}\n\n${reviews
|
|
103
|
+
.map((r) => `### ${r.role} (${r.verdict})\n${r.review}`)
|
|
104
|
+
.join(
|
|
105
|
+
"\n\n---\n\n",
|
|
106
|
+
)}\n\nProvide:\n1. Final Verdict: APPROVE / NEEDS_WORK / REJECT\n2. Consensus Score: 0-100 (how much the reviewers agree)\n3. Summary of key findings across all perspectives\n4. Priority action items (if any)`,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
let finalVerdict = "NEEDS_WORK";
|
|
111
|
+
let consensusScore = 50;
|
|
112
|
+
let summary = "";
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
summary = await chat(moderatorMessages, { maxTokens: 1500 });
|
|
116
|
+
finalVerdict = extractVerdict(summary);
|
|
117
|
+
consensusScore = extractConsensusScore(summary);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
summary = `Moderator error: ${err.message}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
target,
|
|
124
|
+
perspectives,
|
|
125
|
+
reviews,
|
|
126
|
+
verdict: finalVerdict,
|
|
127
|
+
consensusScore,
|
|
128
|
+
summary,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractVerdict(text) {
|
|
133
|
+
if (/\bREJECT\b/i.test(text)) return "REJECT";
|
|
134
|
+
if (/\bAPPROVE\b/i.test(text)) return "APPROVE";
|
|
135
|
+
return "NEEDS_WORK";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractConsensusScore(text) {
|
|
139
|
+
const match = text.match(/consensus\s*score[:\s]*(\d+)/i);
|
|
140
|
+
if (match) return parseInt(match[1], 10);
|
|
141
|
+
return 50;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { DEFAULT_PERSPECTIVES, PERSPECTIVE_PROMPTS };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Knowledge Base for CLI
|
|
3
|
+
*
|
|
4
|
+
* Extracts architectural decisions from code and documentation using LLM analysis.
|
|
5
|
+
* Outputs structured ADR-like records.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { createChatFn } from "../cowork-adapter.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract architectural decisions from a file or directory
|
|
14
|
+
*
|
|
15
|
+
* @param {object} params
|
|
16
|
+
* @param {string} params.targetPath - File or directory to analyze
|
|
17
|
+
* @param {object} [params.llmOptions] - LLM provider options
|
|
18
|
+
* @returns {Promise<object>} Extracted decisions
|
|
19
|
+
*/
|
|
20
|
+
export async function extractDecisions({ targetPath, llmOptions = {} }) {
|
|
21
|
+
const chat = createChatFn(llmOptions);
|
|
22
|
+
const stat = fs.statSync(targetPath);
|
|
23
|
+
|
|
24
|
+
let content;
|
|
25
|
+
if (stat.isDirectory()) {
|
|
26
|
+
// Read key files that typically contain decisions
|
|
27
|
+
const candidates = [
|
|
28
|
+
"CLAUDE.md",
|
|
29
|
+
"CLAUDE-decisions.md",
|
|
30
|
+
"README.md",
|
|
31
|
+
"ARCHITECTURE.md",
|
|
32
|
+
"ADR",
|
|
33
|
+
"docs/adr",
|
|
34
|
+
"docs/decisions",
|
|
35
|
+
"package.json",
|
|
36
|
+
];
|
|
37
|
+
const parts = [];
|
|
38
|
+
for (const candidate of candidates) {
|
|
39
|
+
const fullPath = path.join(targetPath, candidate);
|
|
40
|
+
if (fs.existsSync(fullPath)) {
|
|
41
|
+
const stat2 = fs.statSync(fullPath);
|
|
42
|
+
if (stat2.isFile()) {
|
|
43
|
+
try {
|
|
44
|
+
const fileContent = fs.readFileSync(fullPath, "utf-8");
|
|
45
|
+
parts.push(
|
|
46
|
+
`--- ${candidate} ---\n${fileContent.substring(0, 5000)}`,
|
|
47
|
+
);
|
|
48
|
+
} catch {
|
|
49
|
+
// Skip
|
|
50
|
+
}
|
|
51
|
+
} else if (stat2.isDirectory()) {
|
|
52
|
+
// Read first few files in ADR directory
|
|
53
|
+
try {
|
|
54
|
+
const files = fs.readdirSync(fullPath).slice(0, 10);
|
|
55
|
+
for (const f of files) {
|
|
56
|
+
const fp = path.join(fullPath, f);
|
|
57
|
+
if (fs.statSync(fp).isFile()) {
|
|
58
|
+
const fc = fs.readFileSync(fp, "utf-8");
|
|
59
|
+
parts.push(
|
|
60
|
+
`--- ${candidate}/${f} ---\n${fc.substring(0, 3000)}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Skip
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
content = parts.join("\n\n");
|
|
71
|
+
} else {
|
|
72
|
+
content = fs.readFileSync(targetPath, "utf-8");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!content || content.trim().length === 0) {
|
|
76
|
+
return {
|
|
77
|
+
decisions: [],
|
|
78
|
+
summary: "No relevant content found for decision extraction.",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Truncate if too long
|
|
83
|
+
if (content.length > 20000) {
|
|
84
|
+
content = content.substring(0, 20000) + "\n... (truncated)";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const messages = [
|
|
88
|
+
{
|
|
89
|
+
role: "system",
|
|
90
|
+
content: `You are an experienced software architect. Extract architectural decisions from the provided content. For each decision, identify:
|
|
91
|
+
1. Title — short name for the decision
|
|
92
|
+
2. Status — (accepted, proposed, deprecated, superseded)
|
|
93
|
+
3. Context — why was this decision needed?
|
|
94
|
+
4. Decision — what was decided?
|
|
95
|
+
5. Consequences — what are the trade-offs?
|
|
96
|
+
|
|
97
|
+
Format each decision as:
|
|
98
|
+
### Decision: <title>
|
|
99
|
+
- **Status**: <status>
|
|
100
|
+
- **Context**: <context>
|
|
101
|
+
- **Decision**: <what was decided>
|
|
102
|
+
- **Consequences**: <trade-offs>
|
|
103
|
+
|
|
104
|
+
List all decisions you can find. If no explicit decisions are documented, infer them from technology choices, patterns, and configuration.`,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
role: "user",
|
|
108
|
+
content: `Extract architectural decisions from this content:\n\n${content}`,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const response = await chat(messages, { maxTokens: 2000 });
|
|
114
|
+
const decisions = parseDecisions(response);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
decisions,
|
|
118
|
+
raw: response,
|
|
119
|
+
summary: `Found ${decisions.length} architectural decisions in ${targetPath}\n\n${decisions.map((d) => ` - ${d.title} (${d.status})`).join("\n")}`,
|
|
120
|
+
};
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return {
|
|
123
|
+
decisions: [],
|
|
124
|
+
summary: `Decision extraction failed: ${err.message}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseDecisions(text) {
|
|
130
|
+
const decisions = [];
|
|
131
|
+
const sections = text.split(/###\s*Decision:\s*/i);
|
|
132
|
+
|
|
133
|
+
for (let i = 1; i < sections.length; i++) {
|
|
134
|
+
const section = sections[i];
|
|
135
|
+
const lines = section.split("\n");
|
|
136
|
+
const title = lines[0].trim();
|
|
137
|
+
|
|
138
|
+
const statusMatch = section.match(/\*\*Status\*\*:\s*(.+)/i);
|
|
139
|
+
const contextMatch = section.match(/\*\*Context\*\*:\s*(.+)/i);
|
|
140
|
+
const decisionMatch = section.match(/\*\*Decision\*\*:\s*(.+)/i);
|
|
141
|
+
const consequencesMatch = section.match(/\*\*Consequences?\*\*:\s*(.+)/i);
|
|
142
|
+
|
|
143
|
+
decisions.push({
|
|
144
|
+
title,
|
|
145
|
+
status: statusMatch ? statusMatch[1].trim() : "accepted",
|
|
146
|
+
context: contextMatch ? contextMatch[1].trim() : "",
|
|
147
|
+
decision: decisionMatch ? decisionMatch[1].trim() : "",
|
|
148
|
+
consequences: consequencesMatch ? consequencesMatch[1].trim() : "",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return decisions;
|
|
153
|
+
}
|