scai 0.1.149 → 0.1.152
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/dist/agents/MainAgent.js +63 -73
- package/dist/agents/collectAnalysisEvidenceStep.js +189 -0
- package/dist/agents/readinessGateStep.js +34 -29
- package/dist/agents/resolveExecutionModeStep.js +9 -0
- package/dist/agents/selectRelevantSourcesStep.js +84 -58
- package/dist/agents/understandIntentStep.js +23 -4
- package/dist/commands/factory.js +0 -27
- package/dist/config.js +20 -10
- package/dist/index.js +2 -2
- package/dist/lib/generate.js +6 -3
- package/dist/pipeline/modules/codeTransformModule.js +48 -155
- package/dist/pipeline/modules/semanticAnalysisModule.js +40 -34
- package/package.json +1 -1
package/dist/agents/MainAgent.js
CHANGED
|
@@ -12,8 +12,8 @@ import { writeFileStep } from "./writeFileStep.js";
|
|
|
12
12
|
import { resolveExecutionModeStep } from "./resolveExecutionModeStep.js";
|
|
13
13
|
import { fileCheckStep } from "./fileCheckStep.js";
|
|
14
14
|
import { analysisPlanGenStep } from "./analysisPlanGenStep.js";
|
|
15
|
-
import chalk from "chalk";
|
|
16
15
|
import { readinessGateStep } from "./readinessGateStep.js";
|
|
16
|
+
import { collectAnalysisEvidenceStep } from "./collectAnalysisEvidenceStep.js";
|
|
17
17
|
/* ───────────────────────── registry ───────────────────────── */
|
|
18
18
|
const MODULE_REGISTRY = Object.fromEntries(Object.entries(builtInModules).map(([name, mod]) => [name, mod]));
|
|
19
19
|
function resolveModuleForAction(action) {
|
|
@@ -114,122 +114,112 @@ export class MainAgent {
|
|
|
114
114
|
const db = getDbForRepo();
|
|
115
115
|
this.taskId = bootTaskForRepo(this.context, db, this.logLine.bind(this));
|
|
116
116
|
}
|
|
117
|
-
|
|
118
|
-
let t = this.startTimer();
|
|
119
|
-
await fileCheckStep(this.context);
|
|
120
|
-
this.logLine("PRECHECK", "preFileSearch", t());
|
|
121
|
-
// -------------------- EXPLAIN MODE HANDLING --------------------
|
|
122
|
-
if (this.canExecutePhase("explain")) {
|
|
123
|
-
const explainMod = resolveModuleForAction("explain");
|
|
124
|
-
if (!explainMod)
|
|
125
|
-
throw new Error("Explain module not found");
|
|
126
|
-
const explainOutput = await explainMod.run({
|
|
127
|
-
query: this.query,
|
|
128
|
-
context: this.context
|
|
129
|
-
});
|
|
130
|
-
this.logLine("MODE", "explain", undefined, "returning AI-generated explanation after preFileSearchCheck");
|
|
131
|
-
return explainOutput;
|
|
132
|
-
}
|
|
133
|
-
// -------------------- summarize folder capsules --------------------
|
|
134
|
-
logFolderCapsulesSummary(this.context);
|
|
135
|
-
this.logLine("BOOT", "folderCapsulesSummary", t(), `📂 ${this.context.initContext?.folderCapsules?.length} folders summarized`);
|
|
117
|
+
/* ================= PRECHECK (INITIAL) ================= */
|
|
136
118
|
{
|
|
137
|
-
|
|
138
|
-
await
|
|
139
|
-
this.logLine("
|
|
119
|
+
const t = this.startTimer();
|
|
120
|
+
await fileCheckStep(this.context);
|
|
121
|
+
this.logLine("PRECHECK", "preFileSearch", t());
|
|
140
122
|
}
|
|
141
|
-
/* ================= INFORMATION ACQUISITION
|
|
123
|
+
/* ================= INFORMATION ACQUISITION ================= */
|
|
142
124
|
if (this.canExecutePhase("planning")) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
125
|
+
{
|
|
126
|
+
const t = this.startTimer();
|
|
127
|
+
await infoPlanGen.run(this.context);
|
|
128
|
+
this.logLine("PLAN", "infoPlanGen", t());
|
|
129
|
+
}
|
|
147
130
|
let stepIO = { query: this.query };
|
|
131
|
+
const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
|
|
148
132
|
for (const step of infoPlan.steps.filter(s => s.groups?.includes("info"))) {
|
|
149
133
|
stepIO = await this.executeStep(step, stepIO);
|
|
150
134
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
t = this.startTimer();
|
|
135
|
+
if (infoPlan.steps.length > 0) {
|
|
136
|
+
const t = this.startTimer();
|
|
154
137
|
await fileCheckStep(this.context);
|
|
155
138
|
this.logLine("PRECHECK", "postFileSearch", t());
|
|
156
139
|
}
|
|
157
|
-
else {
|
|
158
|
-
this.logLine("PRECHECK", "postFileSearch", undefined, chalk.gray("skipped (no new info search steps)"));
|
|
159
|
-
}
|
|
160
140
|
}
|
|
161
|
-
/* =================
|
|
141
|
+
/* ================= GROUNDING & READINESS GATE ================= */
|
|
162
142
|
{
|
|
163
|
-
|
|
143
|
+
const t1 = this.startTimer();
|
|
144
|
+
await collectAnalysisEvidenceStep.run({ query: this.query, context: this.context });
|
|
145
|
+
this.logLine("ANALYSIS", "collectAnalysisEvidence", t1());
|
|
146
|
+
const t2 = this.startTimer();
|
|
164
147
|
await selectRelevantSourcesStep.run({ query: this.query, context: this.context });
|
|
165
|
-
this.logLine("ANALYSIS", "selectRelevantSources",
|
|
148
|
+
this.logLine("ANALYSIS", "selectRelevantSources", t2());
|
|
149
|
+
const t3 = this.startTimer();
|
|
150
|
+
await readinessGateStep.run(this.context);
|
|
151
|
+
this.logLine("HASINFO", "readinessGate", t3());
|
|
152
|
+
}
|
|
153
|
+
/* ================= EXPLAIN (EVIDENCE EXIT) ================= */
|
|
154
|
+
if (this.canExecutePhase("explain") &&
|
|
155
|
+
this.context.analysis?.routingDecision?.decision === "has-info") {
|
|
156
|
+
const explainMod = resolveModuleForAction("explain");
|
|
157
|
+
if (!explainMod)
|
|
158
|
+
throw new Error("Explain module not found");
|
|
159
|
+
return await explainMod.run({
|
|
160
|
+
query: this.query,
|
|
161
|
+
context: this.context
|
|
162
|
+
});
|
|
166
163
|
}
|
|
164
|
+
/* ================= ANALYSIS (DEEP) ================= */
|
|
167
165
|
if (this.canExecutePhase("analysis")) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
166
|
+
{
|
|
167
|
+
const t = this.startTimer();
|
|
168
|
+
await analysisPlanGenStep.run(this.context);
|
|
169
|
+
this.logLine("PLAN", "analysisPlanGen", t());
|
|
170
|
+
}
|
|
172
171
|
let stepIO = { query: this.query };
|
|
172
|
+
const analysisPlan = this.context.analysis?.planSuggestion?.plan?.steps ?? [];
|
|
173
173
|
for (const step of analysisPlan.filter(s => s.groups?.includes("analysis"))) {
|
|
174
174
|
stepIO = await this.executeStep(step, stepIO);
|
|
175
175
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this.logLine("ANALYSIS", "planTargetFiles", t());
|
|
181
|
-
}
|
|
182
|
-
const review = await contextReviewStep(this.context);
|
|
183
|
-
if (review.decision === "has") {
|
|
184
|
-
if (this.runCount >= this.maxRuns) {
|
|
185
|
-
this.logLine("ROUTING", "gatherData", undefined, chalk.yellow("max runs reached, proceeding anyway"));
|
|
176
|
+
{
|
|
177
|
+
const t = this.startTimer();
|
|
178
|
+
await planTargetFilesStep.run({ query: this.query, context: this.context });
|
|
179
|
+
this.logLine("ANALYSIS", "planTargetFiles", t());
|
|
186
180
|
}
|
|
187
|
-
|
|
188
|
-
|
|
181
|
+
const review = await contextReviewStep(this.context);
|
|
182
|
+
if (review.decision === "has" && this.runCount < this.maxRuns) {
|
|
189
183
|
this.runCount++;
|
|
190
184
|
this.resetInitContextForLoop();
|
|
191
185
|
return this.run();
|
|
192
186
|
}
|
|
193
187
|
}
|
|
194
|
-
/* ================= TRANSFORM
|
|
188
|
+
/* ================= TRANSFORM ================= */
|
|
195
189
|
if (this.canExecutePhase("transform")) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
190
|
+
{
|
|
191
|
+
const t = this.startTimer();
|
|
192
|
+
await transformPlanGenStep.run(this.context);
|
|
193
|
+
this.logLine("PLAN", "transformPlanGen", t());
|
|
194
|
+
}
|
|
201
195
|
let stepIO = { query: this.query };
|
|
196
|
+
const transformSteps = this.context.analysis?.planSuggestion?.plan?.steps
|
|
197
|
+
?.filter(s => s.groups?.includes("transform")) ?? [];
|
|
202
198
|
for (const step of transformSteps) {
|
|
203
199
|
stepIO = await this.executeStep(step, stepIO);
|
|
204
200
|
}
|
|
205
201
|
if (this.canExecutePhase("write")) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
context: this.context,
|
|
210
|
-
data: stepIO.data
|
|
211
|
-
});
|
|
212
|
-
this.logLine("EXECUTE", "writeFile", tWrite());
|
|
202
|
+
const t = this.startTimer();
|
|
203
|
+
await writeFileStep.run({ query: this.query, context: this.context });
|
|
204
|
+
this.logLine("EXECUTE", "writeFile", t());
|
|
213
205
|
}
|
|
214
206
|
}
|
|
215
|
-
/* ================= FINALIZE
|
|
207
|
+
/* ================= FINALIZE ================= */
|
|
216
208
|
{
|
|
217
|
-
t = this.startTimer();
|
|
209
|
+
const t = this.startTimer();
|
|
218
210
|
await finalPlanGenStep.run(this.context);
|
|
219
211
|
this.logLine("PLAN", "finalPlanGen", t());
|
|
220
|
-
const finalPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
|
|
221
212
|
let stepIO = { query: this.query };
|
|
213
|
+
const finalPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
|
|
222
214
|
for (const step of finalPlan.steps.filter(s => s.groups?.includes("finalize"))) {
|
|
223
215
|
stepIO = await this.executeStep(step, stepIO);
|
|
224
216
|
}
|
|
225
217
|
}
|
|
226
|
-
this.userOutput("All input/output logs can be found at ~/.scai/input_output.log");
|
|
227
|
-
this.logLine("RUN", "complete", stopRun());
|
|
228
|
-
// -------------------- PERSIST TASK DATA --------------------
|
|
229
218
|
const db = getDbForRepo();
|
|
230
219
|
persistTaskData(this.context, this.taskId, db, this.logLine.bind(this));
|
|
220
|
+
this.logLine("RUN", "complete", stopRun());
|
|
231
221
|
return { query: this.query, data: {} };
|
|
232
|
-
}
|
|
222
|
+
}
|
|
233
223
|
resetInitContextForLoop() {
|
|
234
224
|
if (this.context.initContext)
|
|
235
225
|
this.context.initContext.relatedFiles = [];
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// File: src/modules/collectAnalysisEvidenceStep.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { generate } from "../lib/generate.js";
|
|
4
|
+
import { cleanupModule } from "../pipeline/modules/cleanupModule.js";
|
|
5
|
+
import { logInputOutput } from "../utils/promptLogHelper.js";
|
|
6
|
+
export const collectAnalysisEvidenceStep = {
|
|
7
|
+
name: "collectAnalysisEvidence",
|
|
8
|
+
description: "Evidence-first, deterministic analysis over candidate source files. Builds fileAnalysis only.",
|
|
9
|
+
groups: ["analysis"],
|
|
10
|
+
run: async (input) => {
|
|
11
|
+
var _a;
|
|
12
|
+
const query = input.query ?? "";
|
|
13
|
+
const context = input.context;
|
|
14
|
+
if (!context?.analysis) {
|
|
15
|
+
throw new Error("[collectAnalysisEvidence] context.analysis is required.");
|
|
16
|
+
}
|
|
17
|
+
context.analysis.fileAnalysis = context.analysis.fileAnalysis ?? {};
|
|
18
|
+
const intentCategory = context.analysis.intent?.intentCategory ?? "other";
|
|
19
|
+
// ───────────── Guard ─────────────
|
|
20
|
+
if (!["refactorTask", "codingTask", "writing"].includes(intentCategory)) {
|
|
21
|
+
console.log(`[collectAnalysisEvidence] Skipping file scan because intentCategory=${intentCategory}`);
|
|
22
|
+
return {
|
|
23
|
+
query,
|
|
24
|
+
data: {
|
|
25
|
+
fileAnalysis: context.analysis.fileAnalysis
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// --------------------------------------------------
|
|
30
|
+
// Collect candidate paths (old selectRelevantSources behavior)
|
|
31
|
+
// --------------------------------------------------
|
|
32
|
+
const candidatePaths = [
|
|
33
|
+
...(context.analysis.focus?.relevantFiles ?? []),
|
|
34
|
+
...(context.initContext?.relatedFiles ?? []),
|
|
35
|
+
...(context.workingFiles?.map(f => f.path) ?? []),
|
|
36
|
+
];
|
|
37
|
+
const uniquePaths = Array.from(new Set(candidatePaths));
|
|
38
|
+
if (!uniquePaths.length) {
|
|
39
|
+
console.warn("⚠️ No candidate paths available for evidence collection.");
|
|
40
|
+
return { query, data: {} };
|
|
41
|
+
}
|
|
42
|
+
const filesWithEvidence = [];
|
|
43
|
+
// --------------------------------------------------
|
|
44
|
+
// Deterministic evidence collection
|
|
45
|
+
// --------------------------------------------------
|
|
46
|
+
for (const path of uniquePaths) {
|
|
47
|
+
let code;
|
|
48
|
+
try {
|
|
49
|
+
code = fs.readFileSync(path, "utf-8");
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
console.warn(`⚠️ Failed to read file ${path}:`, err.message);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const lines = code.split("\n");
|
|
56
|
+
const evidenceItems = [];
|
|
57
|
+
const sentenceMatches = [];
|
|
58
|
+
// Heuristic: quoted strings in query
|
|
59
|
+
const sentenceRegex = /['"`](.+?)['"`]/g;
|
|
60
|
+
const targetSentences = [];
|
|
61
|
+
let match;
|
|
62
|
+
while ((match = sentenceRegex.exec(query)) !== null) {
|
|
63
|
+
targetSentences.push(match[1]);
|
|
64
|
+
}
|
|
65
|
+
// Line-by-line scan
|
|
66
|
+
lines.forEach((line, index) => {
|
|
67
|
+
for (const target of targetSentences) {
|
|
68
|
+
if (line.includes(target)) {
|
|
69
|
+
evidenceItems.push({
|
|
70
|
+
claim: `Line contains the target sentence: "${target}"`,
|
|
71
|
+
excerpt: line,
|
|
72
|
+
span: { startLine: index + 1, endLine: index + 1 },
|
|
73
|
+
confidence: 1,
|
|
74
|
+
});
|
|
75
|
+
sentenceMatches.push(line);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
const isRelevant = evidenceItems.length > 0;
|
|
80
|
+
const shouldModify = determineShouldModify(intentCategory, isRelevant);
|
|
81
|
+
const scope = determineScope(evidenceItems.length, intentCategory, isRelevant);
|
|
82
|
+
if (isRelevant) {
|
|
83
|
+
filesWithEvidence.push(path);
|
|
84
|
+
}
|
|
85
|
+
const fileAnalysis = {
|
|
86
|
+
intent: isRelevant ? "relevant" : "irrelevant",
|
|
87
|
+
relevance: isRelevant
|
|
88
|
+
? `${evidenceItems.length} candidate item(s) relevant to the query.`
|
|
89
|
+
: "No matching lines found in the file.",
|
|
90
|
+
role: "primary",
|
|
91
|
+
action: {
|
|
92
|
+
isRelevant,
|
|
93
|
+
shouldModify
|
|
94
|
+
},
|
|
95
|
+
proposedChanges: {
|
|
96
|
+
summary: isRelevant
|
|
97
|
+
? "File contains elements that should be modified according to the query."
|
|
98
|
+
: "No changes required.",
|
|
99
|
+
scope,
|
|
100
|
+
targets: sentenceMatches,
|
|
101
|
+
rationale: isRelevant
|
|
102
|
+
? "Detected sentence(s) in file matching the query."
|
|
103
|
+
: undefined,
|
|
104
|
+
},
|
|
105
|
+
risks: [],
|
|
106
|
+
evidence: evidenceItems,
|
|
107
|
+
};
|
|
108
|
+
// Optional LLM rationale (non-authoritative)
|
|
109
|
+
if (isRelevant) {
|
|
110
|
+
try {
|
|
111
|
+
const prompt = `
|
|
112
|
+
Provide a concise rationale (1–2 sentences) explaining why the identified lines in this file are relevant to the user's query.
|
|
113
|
+
|
|
114
|
+
Query:
|
|
115
|
+
"${query}"
|
|
116
|
+
|
|
117
|
+
File:
|
|
118
|
+
${path}
|
|
119
|
+
|
|
120
|
+
Lines:
|
|
121
|
+
${sentenceMatches.join("\n")}
|
|
122
|
+
`.trim();
|
|
123
|
+
const response = await generate({ content: prompt, query });
|
|
124
|
+
const cleaned = await cleanupModule.run({ query, content: response.data });
|
|
125
|
+
if (typeof cleaned.data === "string") {
|
|
126
|
+
fileAnalysis.proposedChanges.rationale = cleaned.data;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
console.warn(`[collectAnalysisEvidence] Rationale enrichment failed for ${path}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Persist analysis only
|
|
134
|
+
context.analysis.fileAnalysis[path] = {
|
|
135
|
+
...context.analysis.fileAnalysis[path],
|
|
136
|
+
...fileAnalysis,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// ───────────── Conditional relevance update ─────────────
|
|
140
|
+
// Only update focus.relevantFiles if at least one file has evidence
|
|
141
|
+
if (filesWithEvidence.length > 0) {
|
|
142
|
+
(_a = context.analysis).focus ?? (_a.focus = { relevantFiles: [] });
|
|
143
|
+
context.analysis.focus.relevantFiles = filesWithEvidence;
|
|
144
|
+
// Mark others as irrelevant
|
|
145
|
+
for (const path of uniquePaths) {
|
|
146
|
+
if (!filesWithEvidence.includes(path)) {
|
|
147
|
+
context.analysis.fileAnalysis[path] = {
|
|
148
|
+
...context.analysis.fileAnalysis[path],
|
|
149
|
+
intent: "irrelevant",
|
|
150
|
+
action: { ...context.analysis.fileAnalysis[path]?.action, isRelevant: false }
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const output = {
|
|
156
|
+
query,
|
|
157
|
+
data: {
|
|
158
|
+
fileAnalysis: context.analysis.fileAnalysis,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
logInputOutput("collectAnalysisEvidence", "output", output.data);
|
|
162
|
+
return output;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
// ───────────── helpers ─────────────
|
|
166
|
+
function determineShouldModify(intentCategory, isRelevant) {
|
|
167
|
+
if (!isRelevant)
|
|
168
|
+
return false;
|
|
169
|
+
return ["codingTask", "refactorTask", "writing"].includes(intentCategory);
|
|
170
|
+
}
|
|
171
|
+
function determineScope(evidenceCount, intentCategory, isRelevant) {
|
|
172
|
+
if (!isRelevant)
|
|
173
|
+
return "none";
|
|
174
|
+
let scope = "minor";
|
|
175
|
+
if (evidenceCount <= 2)
|
|
176
|
+
scope = "minor";
|
|
177
|
+
else if (evidenceCount <= 5)
|
|
178
|
+
scope = "moderate";
|
|
179
|
+
else
|
|
180
|
+
scope = "major";
|
|
181
|
+
// Intent bump
|
|
182
|
+
if (["refactorTask", "codingTask"].includes(intentCategory)) {
|
|
183
|
+
if (scope === "minor" && evidenceCount > 1)
|
|
184
|
+
scope = "moderate";
|
|
185
|
+
if (scope === "moderate" && evidenceCount > 4)
|
|
186
|
+
scope = "major";
|
|
187
|
+
}
|
|
188
|
+
return scope;
|
|
189
|
+
}
|
|
@@ -4,7 +4,7 @@ function logRoutingDecision(context) {
|
|
|
4
4
|
logInputOutput('readinessGateStep', 'output', {
|
|
5
5
|
routingDecision: context.analysis?.routingDecision,
|
|
6
6
|
focus: context.analysis?.focus,
|
|
7
|
-
|
|
7
|
+
fileAnalysis: context.analysis?.fileAnalysis,
|
|
8
8
|
});
|
|
9
9
|
}
|
|
10
10
|
export const readinessGateStep = {
|
|
@@ -15,51 +15,56 @@ export const readinessGateStep = {
|
|
|
15
15
|
var _a, _b;
|
|
16
16
|
context.analysis || (context.analysis = {});
|
|
17
17
|
(_a = context.analysis).focus || (_a.focus = { relevantFiles: [], missingFiles: [], rationale: '' });
|
|
18
|
-
(_b = context.analysis).
|
|
18
|
+
(_b = context.analysis).fileAnalysis || (_b.fileAnalysis = {});
|
|
19
19
|
const focus = context.analysis.focus;
|
|
20
|
+
const fileAnalysis = context.analysis.fileAnalysis;
|
|
21
|
+
// ================= CHECK EVIDENCE =================
|
|
22
|
+
const relevantFiles = Object.entries(fileAnalysis)
|
|
23
|
+
.filter(([_, fa]) => fa.intent === 'relevant')
|
|
24
|
+
.map(([path, _]) => path);
|
|
25
|
+
const irrelevantFiles = Object.entries(fileAnalysis)
|
|
26
|
+
.filter(([_, fa]) => fa.intent === 'irrelevant')
|
|
27
|
+
.map(([path, _]) => path);
|
|
20
28
|
const missingFiles = focus.missingFiles ?? [];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// If fileCheckStep says nothing is missing, we already have enough info
|
|
24
|
-
if (missingFiles.length === 0) {
|
|
29
|
+
// Fast path: no relevant files, evidence indicates info is missing
|
|
30
|
+
if (relevantFiles.length === 0 || missingFiles.length > 0) {
|
|
25
31
|
context.analysis.routingDecision = {
|
|
26
|
-
decision: '
|
|
27
|
-
rationale:
|
|
28
|
-
confidence: 0.
|
|
32
|
+
decision: 'needs-info',
|
|
33
|
+
rationale: `No relevant evidence found or missing files: ${missingFiles.join(', ')}`,
|
|
34
|
+
confidence: 0.9,
|
|
29
35
|
};
|
|
30
36
|
logRoutingDecision(context);
|
|
31
37
|
return;
|
|
32
38
|
}
|
|
33
|
-
//
|
|
34
|
-
//
|
|
39
|
+
// Evidence-based classification
|
|
40
|
+
// Build simple summary for model
|
|
41
|
+
const evidenceSummary = relevantFiles
|
|
42
|
+
.map(path => {
|
|
43
|
+
const fa = fileAnalysis[path];
|
|
44
|
+
const claims = fa.evidence?.map(e => `- ${e.claim}`).join('\n') ?? '';
|
|
45
|
+
return `File: ${path}\nRole: ${fa.role}\nRelevance: ${fa.relevance}\nClaims:\n${claims}`;
|
|
46
|
+
})
|
|
47
|
+
.join('\n\n');
|
|
35
48
|
const prompt = `
|
|
36
|
-
You are a routing classifier
|
|
49
|
+
You are a routing classifier. Your task is to determine if the agent has sufficient evidence to answer the user's query.
|
|
37
50
|
|
|
38
51
|
User query:
|
|
39
52
|
${context.initContext?.userQuery}
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
${
|
|
43
|
-
|
|
44
|
-
Repo section:
|
|
45
|
-
${context.initContext?.repoTree}
|
|
46
|
-
|
|
47
|
-
${context.analysis?.folderCapsulesHuman}
|
|
48
|
-
|
|
49
|
-
Missing files (as determined earlier):
|
|
50
|
-
${JSON.stringify(missingFiles, null, 2)}
|
|
54
|
+
Evidence from analyzed files:
|
|
55
|
+
${evidenceSummary}
|
|
51
56
|
|
|
52
57
|
Decide ONE of the following:
|
|
53
58
|
|
|
54
59
|
HAS-INFO:
|
|
60
|
+
- Sufficient evidence exists in the analyzed files.
|
|
55
61
|
- All required information is present to answer the user's query.
|
|
56
|
-
- No further information acquisition is required.
|
|
57
62
|
|
|
58
63
|
NEEDS-INFO:
|
|
59
|
-
-
|
|
60
|
-
-
|
|
64
|
+
- Insufficient evidence or missing critical files.
|
|
65
|
+
- Additional information acquisition must run.
|
|
61
66
|
|
|
62
|
-
Return ONLY the label
|
|
67
|
+
Return ONLY the label (HAS-INFO or NEEDS-INFO) and optionally a short rationale.
|
|
63
68
|
`.trim();
|
|
64
69
|
try {
|
|
65
70
|
const genInput = {
|
|
@@ -72,15 +77,15 @@ Return ONLY the label, optionally with a short rationale.
|
|
|
72
77
|
const extractedText = raw.replace(/^(HAS-INFO|NEEDS-INFO):?\s*/i, '').trim();
|
|
73
78
|
const modelDecision = raw.toUpperCase().startsWith('HAS-INFO') ? 'has-info' : 'needs-info';
|
|
74
79
|
const combinedRationale = [
|
|
75
|
-
|
|
80
|
+
`Evidence-based routing: ${relevantFiles.length} relevant files, ${irrelevantFiles.length} irrelevant files.`,
|
|
76
81
|
extractedText ? `Model: ${extractedText}` : null,
|
|
77
82
|
]
|
|
78
83
|
.filter(Boolean)
|
|
79
|
-
.join('\n')
|
|
84
|
+
.join('\n');
|
|
80
85
|
context.analysis.routingDecision = {
|
|
81
86
|
decision: modelDecision,
|
|
82
87
|
rationale: combinedRationale,
|
|
83
|
-
confidence: modelDecision === 'has-info' ? 0.
|
|
88
|
+
confidence: modelDecision === 'has-info' ? 0.85 : 0.9,
|
|
84
89
|
};
|
|
85
90
|
logRoutingDecision(context);
|
|
86
91
|
}
|
|
@@ -29,6 +29,15 @@ export const resolveExecutionModeStep = {
|
|
|
29
29
|
mode = "explain";
|
|
30
30
|
rationale = "User intent requests text explanation only.";
|
|
31
31
|
break;
|
|
32
|
+
case "docsAndComments":
|
|
33
|
+
case "docsandcomments":
|
|
34
|
+
case "docsAndComment":
|
|
35
|
+
case "docs":
|
|
36
|
+
case "comments":
|
|
37
|
+
case "comment":
|
|
38
|
+
mode = "transform";
|
|
39
|
+
rationale = "User intent requests adding documentation/comments to files.";
|
|
40
|
+
break;
|
|
32
41
|
default:
|
|
33
42
|
mode = "explain";
|
|
34
43
|
rationale = "Defaulted to explanation due to unclear intent.";
|
|
@@ -1,95 +1,121 @@
|
|
|
1
1
|
// File: src/modules/selectRelevantSourcesStep.ts
|
|
2
2
|
import fs from "fs";
|
|
3
|
+
import chalk from "chalk";
|
|
3
4
|
import { generate } from "../lib/generate.js";
|
|
4
5
|
import { cleanupModule } from "../pipeline/modules/cleanupModule.js";
|
|
5
6
|
import { logInputOutput } from "../utils/promptLogHelper.js";
|
|
6
7
|
export const selectRelevantSourcesStep = {
|
|
7
8
|
name: "selectRelevantSources",
|
|
8
|
-
description: "Selects
|
|
9
|
+
description: "Selects relevant files from analysis.fileAnalysis and populates focus.relevantFiles; also optionally adds to workingFiles with optional LLM fallback.",
|
|
9
10
|
groups: ["analysis"],
|
|
10
11
|
run: async (input) => {
|
|
11
12
|
const query = input.query ?? "";
|
|
12
13
|
const context = input.context;
|
|
13
|
-
if (!context) {
|
|
14
|
-
throw new Error("[selectRelevantSources]
|
|
14
|
+
if (!context?.analysis) {
|
|
15
|
+
throw new Error("[selectRelevantSources] context.analysis is required.");
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
17
|
+
const fileAnalysis = context.analysis.fileAnalysis ?? {};
|
|
18
|
+
// --------------------------------------------------
|
|
19
|
+
// 1. Evidence-first promotion (authoritative)
|
|
20
|
+
// --------------------------------------------------
|
|
21
|
+
const evidenceSelected = [];
|
|
22
|
+
for (const [path, analysis] of Object.entries(fileAnalysis)) {
|
|
23
|
+
if (!analysis.action?.isRelevant)
|
|
24
|
+
continue;
|
|
25
|
+
let code;
|
|
26
|
+
try {
|
|
27
|
+
code = fs.readFileSync(path, "utf-8");
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
console.warn(`⚠️ Failed to read file ${path}:`, err.message);
|
|
31
|
+
}
|
|
32
|
+
evidenceSelected.push({ path, code });
|
|
33
|
+
}
|
|
34
|
+
// --------------------------------------------------
|
|
35
|
+
// 2. LLM fallback (only for unanalyzed paths)
|
|
36
|
+
// --------------------------------------------------
|
|
19
37
|
const candidatePaths = [
|
|
20
|
-
...(context.analysis
|
|
38
|
+
...(context.analysis.focus?.relevantFiles ?? []),
|
|
21
39
|
...(context.initContext?.relatedFiles ?? []),
|
|
22
|
-
...(context.workingFiles?.map(f => f.path) ?? [])
|
|
23
40
|
];
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
You are given:
|
|
30
|
-
|
|
41
|
+
const analyzedPaths = new Set(Object.keys(fileAnalysis));
|
|
42
|
+
const llmCandidates = Array.from(new Set(candidatePaths)).filter(p => !analyzedPaths.has(p));
|
|
43
|
+
let llmSelected = [];
|
|
44
|
+
if (llmCandidates.length > 0) {
|
|
45
|
+
const prompt = `
|
|
31
46
|
Query:
|
|
32
47
|
"${query}"
|
|
33
48
|
|
|
34
49
|
Candidate file paths:
|
|
35
|
-
${JSON.stringify(
|
|
50
|
+
${JSON.stringify(llmCandidates, null, 2)}
|
|
36
51
|
|
|
37
52
|
Task:
|
|
38
|
-
-
|
|
53
|
+
- Select ONLY files that are directly relevant to answering the query.
|
|
39
54
|
- Return ONLY a JSON array of objects.
|
|
40
|
-
- Each object must
|
|
55
|
+
- Each object must include "path".
|
|
41
56
|
- Optional fields: "summary".
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
console.error("❌ [selectRelevantSources] Model selection failed:", err);
|
|
70
|
-
topFiles = [];
|
|
57
|
+
- No explanations.
|
|
58
|
+
`.trim();
|
|
59
|
+
try {
|
|
60
|
+
const response = await generate({ content: prompt, query: "" });
|
|
61
|
+
const cleaned = await cleanupModule.run({
|
|
62
|
+
query,
|
|
63
|
+
content: response.data,
|
|
64
|
+
});
|
|
65
|
+
const rawFiles = Array.isArray(cleaned.data) ? cleaned.data : [];
|
|
66
|
+
llmSelected = rawFiles
|
|
67
|
+
.map((f) => {
|
|
68
|
+
if (typeof f?.path !== "string")
|
|
69
|
+
return null;
|
|
70
|
+
let code;
|
|
71
|
+
try {
|
|
72
|
+
code = fs.readFileSync(f.path, "utf-8");
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.warn(`⚠️ Failed to read file ${f.path}:`, err.message);
|
|
76
|
+
}
|
|
77
|
+
return { path: f.path, code };
|
|
78
|
+
})
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
console.error(chalk.red("❌ [selectRelevantSources] LLM fallback failed:"), err);
|
|
83
|
+
}
|
|
71
84
|
}
|
|
72
|
-
//
|
|
73
|
-
// Persist
|
|
74
|
-
//
|
|
85
|
+
// --------------------------------------------------
|
|
86
|
+
// 3. Persist workingFiles (append + dedupe)
|
|
87
|
+
// --------------------------------------------------
|
|
75
88
|
context.workingFiles = [
|
|
76
|
-
...(context.workingFiles ?? []),
|
|
77
|
-
...
|
|
89
|
+
...(context.workingFiles ?? []),
|
|
90
|
+
...evidenceSelected,
|
|
91
|
+
...llmSelected,
|
|
78
92
|
];
|
|
79
|
-
|
|
80
|
-
const seenPaths = new Set();
|
|
93
|
+
const seen = new Set();
|
|
81
94
|
context.workingFiles = context.workingFiles.filter(f => {
|
|
82
95
|
if (!f.path)
|
|
83
|
-
return false; // safety check
|
|
84
|
-
if (seenPaths.has(f.path))
|
|
85
96
|
return false;
|
|
86
|
-
|
|
97
|
+
if (seen.has(f.path))
|
|
98
|
+
return false;
|
|
99
|
+
seen.add(f.path);
|
|
87
100
|
return true;
|
|
88
101
|
});
|
|
102
|
+
// --------------------------------------------------
|
|
103
|
+
// 4. Update focus.relevantFiles (canonical source for planning)
|
|
104
|
+
// --------------------------------------------------
|
|
105
|
+
context.analysis.focus = {
|
|
106
|
+
relevantFiles: Array.from(new Set([
|
|
107
|
+
...evidenceSelected.map(f => f.path),
|
|
108
|
+
...llmSelected.map(f => f.path),
|
|
109
|
+
])),
|
|
110
|
+
missingFiles: context.analysis.focus?.missingFiles ?? [],
|
|
111
|
+
discardedFiles: context.analysis.focus?.discardedFiles ?? [],
|
|
112
|
+
rationale: context.analysis.focus?.rationale ?? "",
|
|
113
|
+
};
|
|
89
114
|
const output = {
|
|
90
115
|
query,
|
|
91
116
|
data: {
|
|
92
|
-
workingFiles:
|
|
117
|
+
workingFiles: context.workingFiles.map(f => ({ path: f.path })),
|
|
118
|
+
relevantFiles: context.analysis.focus?.relevantFiles,
|
|
93
119
|
},
|
|
94
120
|
};
|
|
95
121
|
logInputOutput("selectRelevantSources", "output", output.data);
|
|
@@ -18,7 +18,7 @@ ${context.initContext?.userQuery}
|
|
|
18
18
|
Return a STRICT JSON object with the following fields:
|
|
19
19
|
{
|
|
20
20
|
"intent": "short sentence summarizing the user's intent",
|
|
21
|
-
"intentCategory": "one of: question, request, codingTask, refactorTask, explanation, debugging, planning, writing,
|
|
21
|
+
"intentCategory": "one of: question, request, codingTask, refactorTask, explanation, debugging, planning, writing, docsAndComments",
|
|
22
22
|
"normalizedQuery": "a cleaned and direct restatement of the user query",
|
|
23
23
|
"confidence": 0-1 // float
|
|
24
24
|
}
|
|
@@ -42,17 +42,36 @@ Do not include commentary. Emit ONLY valid JSON.
|
|
|
42
42
|
catch {
|
|
43
43
|
parsed = {
|
|
44
44
|
intent: "unknown",
|
|
45
|
-
intentCategory: "
|
|
45
|
+
intentCategory: "",
|
|
46
46
|
normalizedQuery: context.initContext?.userQuery,
|
|
47
47
|
confidence: 0.3
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
+
// ---------------------
|
|
51
|
+
// Fallback for invalid / "other" categories
|
|
52
|
+
// ---------------------
|
|
53
|
+
let category = parsed.intentCategory?.trim();
|
|
54
|
+
if (!category || category.toLowerCase() === "other") {
|
|
55
|
+
const q = parsed.normalizedQuery?.toLowerCase() ?? "";
|
|
56
|
+
if (q.includes("move") || q.includes("append") || q.includes("remove") || q.includes("insert")) {
|
|
57
|
+
category = "refactorTask";
|
|
58
|
+
}
|
|
59
|
+
else if (q.includes("comment") || q.includes("documentation") || q.includes("jsdoc") || q.includes("docstring")) {
|
|
60
|
+
category = "docsAndComments";
|
|
61
|
+
}
|
|
62
|
+
else if (q.includes("sentence") || q.includes("file")) {
|
|
63
|
+
category = "explanation";
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
category = "request"; // safe default
|
|
67
|
+
}
|
|
68
|
+
}
|
|
50
69
|
// Ensure the analysis object exists
|
|
51
70
|
context.analysis ?? (context.analysis = {});
|
|
52
71
|
// Store intent inside a dedicated object
|
|
53
72
|
context.analysis.intent = {
|
|
54
73
|
intent: parsed.intent,
|
|
55
|
-
intentCategory:
|
|
74
|
+
intentCategory: category,
|
|
56
75
|
normalizedQuery: parsed.normalizedQuery,
|
|
57
76
|
confidence: parsed.confidence
|
|
58
77
|
};
|
|
@@ -63,7 +82,7 @@ Do not include commentary. Emit ONLY valid JSON.
|
|
|
63
82
|
context.analysis ?? (context.analysis = {});
|
|
64
83
|
context.analysis.intent = {
|
|
65
84
|
intent: "unknown",
|
|
66
|
-
intentCategory: "
|
|
85
|
+
intentCategory: "request",
|
|
67
86
|
normalizedQuery: context.initContext?.userQuery ?? '',
|
|
68
87
|
confidence: 0.0
|
|
69
88
|
};
|
package/dist/commands/factory.js
CHANGED
|
@@ -9,8 +9,6 @@ import { reviewPullRequestCmd } from './ReviewCmd.js';
|
|
|
9
9
|
import { checkGit } from './GitCmd.js';
|
|
10
10
|
import { promptForToken } from '../github/token.js';
|
|
11
11
|
import { validateGitHubTokenAgainstRepo } from '../github/githubAuthCheck.js';
|
|
12
|
-
import { runWorkflowCommand } from './WorkflowCmd.js';
|
|
13
|
-
import { handleStandaloneChangelogUpdate } from './ChangeLogUpdateCmd.js';
|
|
14
12
|
import { runInteractiveSwitch, runSwitchCommand, statusIndex } from './SwitchCmd.js';
|
|
15
13
|
import { runInteractiveDelete } from './DeleteIndex.js';
|
|
16
14
|
import { startDaemon, stopDaemon, restartDaemon, statusDaemon, unlockConfig, showLogs } from './DaemonCmd.js';
|
|
@@ -112,31 +110,6 @@ export function createProgram() {
|
|
|
112
110
|
console.log('🔑 GitHub token set.');
|
|
113
111
|
});
|
|
114
112
|
});
|
|
115
|
-
// ---------------- Workflow ----------------
|
|
116
|
-
const workflow = cmd.command('workflow').description('Run or manage module pipelines');
|
|
117
|
-
workflow
|
|
118
|
-
.command('run <goals...>')
|
|
119
|
-
.description('Run one or more modules as a pipeline')
|
|
120
|
-
.option('-f, --file <filepath>', 'File to process (omit to read from stdin)')
|
|
121
|
-
.action(async (goals, options) => {
|
|
122
|
-
await withContext(async () => {
|
|
123
|
-
await runWorkflowCommand(goals, options);
|
|
124
|
-
});
|
|
125
|
-
})
|
|
126
|
-
.on('--help', () => {
|
|
127
|
-
console.log('\nExamples:');
|
|
128
|
-
console.log(' $ scai workflow run comments tests -f path/to/myfile.ts');
|
|
129
|
-
console.log(' $ cat file.ts | scai workflow run comments tests > file.test.ts\n');
|
|
130
|
-
console.log('Available modules: summary, comments, tests');
|
|
131
|
-
});
|
|
132
|
-
// ---------------- Gen / Changelog ----------------
|
|
133
|
-
const gen = cmd.command('gen').description('Generate code-related output');
|
|
134
|
-
gen
|
|
135
|
-
.command('changelog')
|
|
136
|
-
.description('Update or create CHANGELOG.md')
|
|
137
|
-
.action(async () => {
|
|
138
|
-
await withContext(async () => { await handleStandaloneChangelogUpdate(); });
|
|
139
|
-
});
|
|
140
113
|
// ---------------- Config ----------------
|
|
141
114
|
const config = cmd.command('config').description('Manage SCAI configuration');
|
|
142
115
|
config
|
package/dist/config.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getHashedRepoKey } from './utils/repoKey.js';
|
|
|
8
8
|
const defaultConfig = {
|
|
9
9
|
model: 'qwen3-coder:30b',
|
|
10
10
|
contextLength: 32768,
|
|
11
|
+
maxOutputTokens: 2048,
|
|
11
12
|
language: 'ts',
|
|
12
13
|
indexDir: '',
|
|
13
14
|
githubToken: '',
|
|
@@ -26,7 +27,15 @@ function ensureConfigDir() {
|
|
|
26
27
|
export function readConfig() {
|
|
27
28
|
try {
|
|
28
29
|
const content = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
29
|
-
|
|
30
|
+
const parsed = JSON.parse(content);
|
|
31
|
+
// ⛔ Never allow static config.json to control token limits
|
|
32
|
+
const { contextLength: _ignoredContextLength, maxOutputTokens: _ignoredMaxOutputTokens, ...rest } = parsed;
|
|
33
|
+
return {
|
|
34
|
+
...defaultConfig,
|
|
35
|
+
...rest,
|
|
36
|
+
contextLength: defaultConfig.contextLength,
|
|
37
|
+
maxOutputTokens: defaultConfig.maxOutputTokens,
|
|
38
|
+
};
|
|
30
39
|
}
|
|
31
40
|
catch {
|
|
32
41
|
return defaultConfig;
|
|
@@ -220,22 +229,23 @@ export const Config = {
|
|
|
220
229
|
const cfg = readConfig();
|
|
221
230
|
const active = cfg.activeRepo;
|
|
222
231
|
console.log(`🔧 Current configuration:`);
|
|
223
|
-
console.log(` Active index dir: ${active || 'Not Set'}`);
|
|
232
|
+
console.log(` Active index dir : ${active || 'Not Set'}`);
|
|
224
233
|
const repoCfg = active ? cfg.repos[active] : {};
|
|
225
|
-
console.log(` Model
|
|
226
|
-
console.log(` Language
|
|
227
|
-
console.log(`
|
|
234
|
+
console.log(` Model : ${repoCfg?.model || cfg.model}`);
|
|
235
|
+
console.log(` Language : ${repoCfg?.language || cfg.language}`);
|
|
236
|
+
console.log(` Context length : ${cfg.contextLength} tokens`);
|
|
237
|
+
console.log(` Max output tokens : ${cfg.maxOutputTokens} tokens`);
|
|
238
|
+
console.log(` GitHub Token : ${cfg.githubToken ? '*****' : 'Not Set'}`);
|
|
228
239
|
const daemon = this.getDaemonConfig();
|
|
229
|
-
console.log(` Daemon sleepMs
|
|
230
|
-
console.log(` Daemon idleMs
|
|
240
|
+
console.log(` Daemon sleepMs : ${daemon.sleepMs}ms`);
|
|
241
|
+
console.log(` Daemon idleMs : ${daemon.idleSleepMs}ms`);
|
|
231
242
|
// ✅ Show lock status
|
|
232
243
|
let lockStatus = 'Unlocked';
|
|
233
|
-
let lockedRepo = null;
|
|
234
244
|
if (fs.existsSync(CONFIG_LOCK_PATH)) {
|
|
235
|
-
lockedRepo = fs.readFileSync(CONFIG_LOCK_PATH, 'utf8');
|
|
245
|
+
const lockedRepo = fs.readFileSync(CONFIG_LOCK_PATH, 'utf8');
|
|
236
246
|
lockStatus = `Locked to repo '${lockedRepo}'`;
|
|
237
247
|
}
|
|
238
|
-
console.log(` Daemon Lock
|
|
248
|
+
console.log(` Daemon Lock : ${lockStatus}`);
|
|
239
249
|
},
|
|
240
250
|
getRaw() {
|
|
241
251
|
return readConfig();
|
package/dist/index.js
CHANGED
|
@@ -117,6 +117,8 @@ function editInEditorAsync(initialContent = '') {
|
|
|
117
117
|
// run terminal commands with '!', or edit input in an external editor with '/edit'.
|
|
118
118
|
// It uses withContext to ensure proper execution context for each command or query.
|
|
119
119
|
async function startShell() {
|
|
120
|
+
// Clear screen first
|
|
121
|
+
process.stdout.write('\x1Bc');
|
|
120
122
|
console.log(chalk.yellow("Welcome to SCAI shell!") + "\n" +
|
|
121
123
|
chalk.blueBright(`- Type your query directly for short commands or questions.
|
|
122
124
|
- Use !command to run terminal commands.
|
|
@@ -159,9 +161,7 @@ async function startShell() {
|
|
|
159
161
|
const content = await editInEditorAsync();
|
|
160
162
|
const trimmedContent = content.trim();
|
|
161
163
|
if (trimmedContent) {
|
|
162
|
-
// Print the editor content in orange
|
|
163
164
|
console.log(chalk.hex('#FFA500')('\n[Editor input]:\n' + trimmedContent + '\n'));
|
|
164
|
-
// Execute the query
|
|
165
165
|
await withContext(() => runAskCommand(trimmedContent));
|
|
166
166
|
}
|
|
167
167
|
continue;
|
package/dist/lib/generate.js
CHANGED
|
@@ -9,17 +9,17 @@ import { Config, readConfig } from '../config.js';
|
|
|
9
9
|
*/
|
|
10
10
|
export async function generate(input) {
|
|
11
11
|
const model = Config.getModel();
|
|
12
|
-
const { contextLength } = readConfig();
|
|
12
|
+
const { contextLength, maxOutputTokens } = readConfig();
|
|
13
13
|
// Safely build prompt
|
|
14
14
|
const queryPart = input.query ? `User query:\n${input.query}\n\n` : '';
|
|
15
15
|
const contentPart = input.content && typeof input.content !== 'string'
|
|
16
16
|
? JSON.stringify(input.content, null, 2)
|
|
17
17
|
: input.content || '';
|
|
18
18
|
const prompt = `${queryPart}${contentPart}`.trim();
|
|
19
|
-
const data = await doGenerate(prompt, model, contextLength);
|
|
19
|
+
const data = await doGenerate(prompt, model, contextLength, maxOutputTokens);
|
|
20
20
|
return { query: input.query, data };
|
|
21
21
|
}
|
|
22
|
-
async function doGenerate(prompt, model, contextLength) {
|
|
22
|
+
async function doGenerate(prompt, model, contextLength, maxOutputTokens) {
|
|
23
23
|
const res = await fetch('http://localhost:11434/api/generate', {
|
|
24
24
|
method: 'POST',
|
|
25
25
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -28,7 +28,10 @@ async function doGenerate(prompt, model, contextLength) {
|
|
|
28
28
|
prompt,
|
|
29
29
|
stream: false,
|
|
30
30
|
options: {
|
|
31
|
+
// context window
|
|
31
32
|
num_ctx: contextLength,
|
|
33
|
+
// limit output to avoid exceeding context
|
|
34
|
+
max_output_tokens: maxOutputTokens,
|
|
32
35
|
},
|
|
33
36
|
}),
|
|
34
37
|
});
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { generate } from "../../lib/generate.js";
|
|
2
2
|
import { cleanupModule } from "./cleanupModule.js";
|
|
3
3
|
import { logInputOutput } from "../../utils/promptLogHelper.js";
|
|
4
|
-
import { splitCodeIntoChunks, countTokens } from "../../utils/splitCodeIntoChunk.js";
|
|
5
4
|
import chalk from "chalk";
|
|
6
|
-
// ───────────── Token limits ─────────────
|
|
7
|
-
const SINGLE_SHOT_TOKEN_LIMIT = 2500; // keep large enough for small files
|
|
8
|
-
const CHUNK_TOKEN_LIMIT = 2200; // chunked transformation limit
|
|
9
5
|
function stripCodeFences(text) {
|
|
10
6
|
const lines = text.split("\n");
|
|
11
7
|
while (lines.length && /^```/.test(lines[0].trim()))
|
|
@@ -14,24 +10,12 @@ function stripCodeFences(text) {
|
|
|
14
10
|
lines.pop();
|
|
15
11
|
return lines.join("\n");
|
|
16
12
|
}
|
|
17
|
-
function isSuspiciousChunkOutput(text, originalChunk) {
|
|
18
|
-
const trimmed = text.trim();
|
|
19
|
-
if (!trimmed)
|
|
20
|
-
return true;
|
|
21
|
-
if (/here is|transformed|updated code|explanation/i.test(trimmed))
|
|
22
|
-
return true;
|
|
23
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}"))
|
|
24
|
-
return true;
|
|
25
|
-
if (trimmed.length < originalChunk.trim().length * 0.3)
|
|
26
|
-
return true;
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
13
|
export const codeTransformModule = {
|
|
30
14
|
name: "codeTransform",
|
|
31
15
|
description: "Transforms a single file specified in the current plan step based on user instruction.",
|
|
32
16
|
groups: ["transform"],
|
|
33
17
|
run: async (input) => {
|
|
34
|
-
var _a, _b
|
|
18
|
+
var _a, _b;
|
|
35
19
|
const query = typeof input.query === "string" ? input.query : String(input.query ?? "");
|
|
36
20
|
const context = input.context;
|
|
37
21
|
if (!context) {
|
|
@@ -78,17 +62,13 @@ ${proposed.rationale ? `- Rationale: ${proposed.rationale}` : ""}
|
|
|
78
62
|
: "";
|
|
79
63
|
const outputs = [];
|
|
80
64
|
const perFileErrors = [];
|
|
81
|
-
const tokenCount = countTokens(file.code);
|
|
82
65
|
context.execution || (context.execution = {});
|
|
83
66
|
(_a = context.execution).codeTransformArtifacts || (_a.codeTransformArtifacts = { files: [] });
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
message: "Starting small file transformation",
|
|
90
|
-
});
|
|
91
|
-
const prompt = `
|
|
67
|
+
logInputOutput("codeTransform", "output", {
|
|
68
|
+
file: file.path,
|
|
69
|
+
message: "Starting single-shot file transformation",
|
|
70
|
+
});
|
|
71
|
+
const prompt = `
|
|
92
72
|
You are a precise code transformation assistant.
|
|
93
73
|
|
|
94
74
|
User instruction (normalized):
|
|
@@ -117,139 +97,52 @@ JSON schema:
|
|
|
117
97
|
"errors": []
|
|
118
98
|
}
|
|
119
99
|
`.trim();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
logInputOutput("codeTransform", "output", {
|
|
124
|
-
file: file.path,
|
|
125
|
-
message: "Received LLM response",
|
|
126
|
-
responseSnippet: String(llmResponse.data ?? "").slice(0, 200),
|
|
127
|
-
});
|
|
128
|
-
const cleaned = await cleanupModule.run({ query, content: llmResponse.data });
|
|
129
|
-
let finalContent;
|
|
130
|
-
let notes = undefined;
|
|
131
|
-
if (typeof cleaned.data === 'string') {
|
|
132
|
-
// Raw code returned
|
|
133
|
-
finalContent = cleaned.data;
|
|
134
|
-
console.debug(chalk.yellow(` - [cleanupModule] Using raw code output for ${file.path}`));
|
|
135
|
-
}
|
|
136
|
-
else if (cleaned.data &&
|
|
137
|
-
typeof cleaned.data === 'object' &&
|
|
138
|
-
Array.isArray(cleaned.data.files)) {
|
|
139
|
-
// Structured JSON returned
|
|
140
|
-
const structured = cleaned.data;
|
|
141
|
-
const out = structured.files.find(f => f.filePath === file.path);
|
|
142
|
-
finalContent = out?.content ?? file.code;
|
|
143
|
-
notes = out?.notes;
|
|
144
|
-
perFileErrors.push(...(structured.errors ?? []));
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
// Fallback
|
|
148
|
-
finalContent = file.code;
|
|
149
|
-
}
|
|
150
|
-
outputs.push({ filePath: file.path, content: finalContent, notes });
|
|
151
|
-
// ensure artifacts are updated
|
|
152
|
-
context.execution.codeTransformArtifacts.files =
|
|
153
|
-
context.execution.codeTransformArtifacts.files.filter(f => f.filePath !== file.path);
|
|
154
|
-
context.execution.codeTransformArtifacts.files.push(outputs[0]);
|
|
155
|
-
context.plan || (context.plan = {});
|
|
156
|
-
(_b = context.plan).touchedFiles || (_b.touchedFiles = []);
|
|
157
|
-
if (!context.plan.touchedFiles.includes(file.path))
|
|
158
|
-
context.plan.touchedFiles.push(file.path);
|
|
159
|
-
const output = { query, data: { files: outputs, errors: perFileErrors } };
|
|
160
|
-
logInputOutput("codeTransform", "output", context.execution.codeTransformArtifacts.files);
|
|
161
|
-
return output;
|
|
162
|
-
}
|
|
163
|
-
catch (err) {
|
|
164
|
-
const finalContent = file.code;
|
|
165
|
-
outputs.push({ filePath: file.path, content: finalContent });
|
|
166
|
-
perFileErrors.push(`LLM call or cleanup failed: ${err.message}`);
|
|
167
|
-
context.execution.codeTransformArtifacts.files.push(outputs[0]);
|
|
168
|
-
const output = { query, data: { files: outputs, errors: perFileErrors } };
|
|
169
|
-
return output;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
// ───────────── LARGE FILE (chunked) ─────────────
|
|
173
|
-
const chunks = splitCodeIntoChunks(file.code, CHUNK_TOKEN_LIMIT);
|
|
174
|
-
console.warn(chalk.yellow([
|
|
175
|
-
"",
|
|
176
|
-
"⚠️ Large file detected — falling back to chunked transformation",
|
|
177
|
-
` File: ${file.path}`,
|
|
178
|
-
` Estimated tokens: ${tokenCount}`,
|
|
179
|
-
` Chunks: ${chunks.length}`,
|
|
180
|
-
" Note: Chunked refactors may be less accurate or inconsistent.",
|
|
181
|
-
" If results look wrong, try reducing scope or refactoring manually.",
|
|
182
|
-
"",
|
|
183
|
-
].join("\n")));
|
|
184
|
-
const transformedChunks = [];
|
|
185
|
-
logInputOutput("codeTransform", "output", { file: file.path, chunkCount: chunks.length, message: "Starting chunked transformation" });
|
|
186
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
187
|
-
const chunk = chunks[i];
|
|
188
|
-
process.stdout.write("\r\x1b[K");
|
|
189
|
-
console.log(` - Processing chunk ${i + 1} of ${chunks.length} for ${file.path}...`);
|
|
100
|
+
try {
|
|
101
|
+
logInputOutput("codeTransform", "output", { file: file.path, message: "Sending prompt to LLM" });
|
|
102
|
+
const llmResponse = await generate({ content: prompt, query });
|
|
190
103
|
logInputOutput("codeTransform", "output", {
|
|
191
104
|
file: file.path,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
message: "Processing chunk",
|
|
105
|
+
message: "Received LLM response",
|
|
106
|
+
responseSnippet: String(llmResponse.data ?? "").slice(0, 200),
|
|
195
107
|
});
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
Transformation rules:
|
|
212
|
-
- Apply the instruction ONLY if it is clearly relevant to this chunk.
|
|
213
|
-
- If no change is needed, return the chunk UNCHANGED.
|
|
214
|
-
- You MUST return the FULL chunk content.
|
|
215
|
-
- Do NOT return diffs, explanations, or partial snippets.
|
|
216
|
-
|
|
217
|
-
FILE: ${file.path}
|
|
218
|
-
CHUNK ${i + 1} / ${chunks.length}
|
|
219
|
-
---
|
|
220
|
-
${chunk}
|
|
221
|
-
`.trim();
|
|
222
|
-
try {
|
|
223
|
-
const llmResponse = await generate({ content: prompt, query });
|
|
224
|
-
const raw = String(llmResponse.data ?? "");
|
|
225
|
-
const stripped = stripCodeFences(raw);
|
|
226
|
-
if (isSuspiciousChunkOutput(stripped, chunk)) {
|
|
227
|
-
transformedChunks.push(chunk);
|
|
228
|
-
perFileErrors.push(`Chunk ${i + 1} suspicious; original preserved.`);
|
|
229
|
-
logInputOutput("codeTransform", "output", { file: file.path, chunkIndex: i + 1, message: "Suspicious output, original chunk preserved" });
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
transformedChunks.push(stripped);
|
|
233
|
-
logInputOutput("codeTransform", "output", { file: file.path, chunkIndex: i + 1, message: "Chunk transformed successfully" });
|
|
234
|
-
}
|
|
108
|
+
const cleaned = await cleanupModule.run({ query, content: llmResponse.data });
|
|
109
|
+
let finalContent;
|
|
110
|
+
let notes = undefined;
|
|
111
|
+
if (typeof cleaned.data === 'string') {
|
|
112
|
+
finalContent = cleaned.data;
|
|
113
|
+
console.debug(chalk.yellow(` - [cleanupModule] Using raw code output for ${file.path}`));
|
|
114
|
+
}
|
|
115
|
+
else if (cleaned.data &&
|
|
116
|
+
typeof cleaned.data === 'object' &&
|
|
117
|
+
Array.isArray(cleaned.data.files)) {
|
|
118
|
+
const structured = cleaned.data;
|
|
119
|
+
const out = structured.files.find(f => f.filePath === file.path);
|
|
120
|
+
finalContent = out?.content ?? file.code;
|
|
121
|
+
notes = out?.notes;
|
|
122
|
+
perFileErrors.push(...(structured.errors ?? []));
|
|
235
123
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
perFileErrors.push(`Chunk ${i + 1} failed; original preserved. Error: ${err.message}`);
|
|
239
|
-
logInputOutput("codeTransform", "output", { file: file.path, chunkIndex: i + 1, error: err.message, message: "Chunk transformation failed, original preserved" });
|
|
124
|
+
else {
|
|
125
|
+
finalContent = file.code;
|
|
240
126
|
}
|
|
127
|
+
outputs.push({ filePath: file.path, content: finalContent, notes });
|
|
128
|
+
context.execution.codeTransformArtifacts.files =
|
|
129
|
+
context.execution.codeTransformArtifacts.files.filter(f => f.filePath !== file.path);
|
|
130
|
+
context.execution.codeTransformArtifacts.files.push(outputs[0]);
|
|
131
|
+
context.plan || (context.plan = {});
|
|
132
|
+
(_b = context.plan).touchedFiles || (_b.touchedFiles = []);
|
|
133
|
+
if (!context.plan.touchedFiles.includes(file.path))
|
|
134
|
+
context.plan.touchedFiles.push(file.path);
|
|
135
|
+
const output = { query, data: { files: outputs, errors: perFileErrors } };
|
|
136
|
+
logInputOutput("codeTransform", "output", context.execution.codeTransformArtifacts.files);
|
|
137
|
+
return output;
|
|
241
138
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const output = { query, data: { files: outputs, errors: perFileErrors } };
|
|
252
|
-
logInputOutput("codeTransform", "output", context.execution.codeTransformArtifacts.files);
|
|
253
|
-
return output;
|
|
254
|
-
},
|
|
139
|
+
catch (err) {
|
|
140
|
+
const finalContent = file.code;
|
|
141
|
+
outputs.push({ filePath: file.path, content: finalContent });
|
|
142
|
+
perFileErrors.push(`LLM call or cleanup failed: ${err.message}`);
|
|
143
|
+
context.execution.codeTransformArtifacts.files.push(outputs[0]);
|
|
144
|
+
const output = { query, data: { files: outputs, errors: perFileErrors } };
|
|
145
|
+
return output;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
255
148
|
};
|
|
@@ -3,12 +3,6 @@ import { logInputOutput } from "../../utils/promptLogHelper.js";
|
|
|
3
3
|
import { cleanupModule } from "./cleanupModule.js";
|
|
4
4
|
import { generate } from "../../lib/generate.js";
|
|
5
5
|
const MAX_CODE_CHARS = 6000; // conservative prompt-safe limit
|
|
6
|
-
/**
|
|
7
|
-
* Semantic analysis module:
|
|
8
|
-
* - Performs file-level semantic analysis based on provided steps
|
|
9
|
-
* - Stores results in context.analysis.fileAnalysis
|
|
10
|
-
* - Optionally produces cross-file combined analysis
|
|
11
|
-
*/
|
|
12
6
|
export const semanticAnalysisModule = {
|
|
13
7
|
name: "semanticAnalysis",
|
|
14
8
|
description: "Performs semantic analysis for each file provided by the plan steps, storing file-level and combined insights.",
|
|
@@ -27,7 +21,8 @@ export const semanticAnalysisModule = {
|
|
|
27
21
|
context.analysis || (context.analysis = {});
|
|
28
22
|
(_a = context.analysis).fileAnalysis || (_a.fileAnalysis = {});
|
|
29
23
|
(_b = context.analysis).combinedAnalysis || (_b.combinedAnalysis = {});
|
|
30
|
-
|
|
24
|
+
const intentCategory = context.analysis.intent?.intentCategory ?? "other";
|
|
25
|
+
// Determine which files to analyze based on plan steps
|
|
31
26
|
const planSteps = context.analysis.planSuggestion?.plan?.steps ?? [];
|
|
32
27
|
const filesToAnalyze = planSteps
|
|
33
28
|
.filter(step => step.action === "semanticAnalysis" && step.targetFile)
|
|
@@ -39,28 +34,41 @@ export const semanticAnalysisModule = {
|
|
|
39
34
|
// ----------------------------
|
|
40
35
|
for (const file of filesToAnalyze) {
|
|
41
36
|
const filePath = file.path;
|
|
42
|
-
|
|
43
|
-
if
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
const prevAnalysis = context.analysis.fileAnalysis[filePath];
|
|
38
|
+
// Use evidence relevance if available
|
|
39
|
+
const isRelevant = prevAnalysis?.action?.isRelevant ?? false;
|
|
40
|
+
if (!isRelevant)
|
|
41
|
+
continue; // skip irrelevant files
|
|
42
|
+
const shouldModify = prevAnalysis?.action?.shouldModify ?? determineShouldModify(intentCategory, isRelevant);
|
|
43
|
+
// Analyze the file (enriching previous evidence)
|
|
44
|
+
const semanticAnalysis = await analyzeFile(file, input.query, context, isRelevant, shouldModify);
|
|
45
|
+
context.analysis.fileAnalysis[filePath] = {
|
|
46
|
+
...prevAnalysis, // keep evidence and prior info
|
|
47
|
+
...semanticAnalysis, // add semantic insights
|
|
48
|
+
action: { isRelevant, shouldModify },
|
|
49
|
+
};
|
|
47
50
|
logInputOutput("semanticAnalysisStep - per-file", "output", {
|
|
48
51
|
file: filePath,
|
|
49
|
-
analysis: fileAnalysis,
|
|
52
|
+
analysis: context.analysis.fileAnalysis[filePath],
|
|
50
53
|
});
|
|
51
54
|
}
|
|
52
55
|
// ----------------------------
|
|
53
56
|
// 2️⃣ Cross-file combined analysis (optional)
|
|
54
|
-
// Only
|
|
57
|
+
// Only consider relevant files
|
|
55
58
|
// ----------------------------
|
|
56
|
-
const
|
|
59
|
+
const relevantFiles = Object.entries(context.analysis.fileAnalysis)
|
|
60
|
+
.filter(([_, analysis]) => analysis.action?.isRelevant)
|
|
61
|
+
.map(([path]) => path);
|
|
57
62
|
let combinedAnalysis = {
|
|
58
63
|
sharedPatterns: [],
|
|
59
64
|
architectureSummary: "[skipped]",
|
|
60
65
|
hotspots: [],
|
|
61
66
|
};
|
|
62
|
-
if (
|
|
63
|
-
|
|
67
|
+
if (relevantFiles.length > 2) {
|
|
68
|
+
const filteredAnalysis = {};
|
|
69
|
+
for (const path of relevantFiles)
|
|
70
|
+
filteredAnalysis[path] = context.analysis.fileAnalysis[path];
|
|
71
|
+
combinedAnalysis = await analyzeCombined(filteredAnalysis, input.query);
|
|
64
72
|
logInputOutput("semanticAnalysisStep - combined", "output", combinedAnalysis);
|
|
65
73
|
}
|
|
66
74
|
context.analysis.combinedAnalysis = combinedAnalysis;
|
|
@@ -74,24 +82,16 @@ export const semanticAnalysisModule = {
|
|
|
74
82
|
/* -------------------------
|
|
75
83
|
Analyze single file
|
|
76
84
|
---------------------------- */
|
|
77
|
-
async function analyzeFile(file, query, context) {
|
|
85
|
+
async function analyzeFile(file, query, context, isRelevant = false, shouldModify = false) {
|
|
78
86
|
const slicedCode = sliceCodeForAnalysis(file.code);
|
|
79
|
-
// Use rationale / understanding from previous steps
|
|
80
87
|
const focus = context?.analysis?.focus;
|
|
81
88
|
const understanding = context?.analysis?.understanding;
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
? `Assumptions: ${understanding.assumptions.join("; ")}`
|
|
85
|
-
: ""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
: "";
|
|
89
|
-
const risksSnippet = understanding?.risks
|
|
90
|
-
? `Known risks: ${understanding.risks.join("; ")}`
|
|
91
|
-
: "";
|
|
92
|
-
const contextSnippet = [rationaleSnippet, assumptionsSnippet, constraintsSnippet, risksSnippet]
|
|
93
|
-
.filter(Boolean)
|
|
94
|
-
.join("\n");
|
|
89
|
+
const contextSnippet = [
|
|
90
|
+
focus?.rationale ? `Rationale: ${focus.rationale}` : "",
|
|
91
|
+
understanding?.assumptions ? `Assumptions: ${understanding.assumptions.join("; ")}` : "",
|
|
92
|
+
understanding?.constraints ? `Constraints: ${understanding.constraints.join("; ")}` : "",
|
|
93
|
+
understanding?.risks ? `Known risks: ${understanding.risks.join("; ")}` : "",
|
|
94
|
+
].filter(Boolean).join("\n");
|
|
95
95
|
const prompt = `
|
|
96
96
|
You are analyzing a single file in the context of a user query.
|
|
97
97
|
|
|
@@ -138,12 +138,10 @@ Return STRICT JSON:
|
|
|
138
138
|
data = JSON.parse(String(cleaned.content ?? "{}"));
|
|
139
139
|
}
|
|
140
140
|
catch {
|
|
141
|
-
console.warn(`[semanticAnalysisStep] Non-JSON output for ${file.path}, defaulting to irrelevant`);
|
|
142
141
|
data = {};
|
|
143
142
|
}
|
|
144
143
|
}
|
|
145
144
|
const intent = data.intent === "relevant" || data.intent === "irrelevant" ? data.intent : "irrelevant";
|
|
146
|
-
const shouldModify = intent === "relevant" && data.action?.shouldModify === true;
|
|
147
145
|
return {
|
|
148
146
|
intent,
|
|
149
147
|
relevance: typeof data.relevance === "string" && data.relevance.trim()
|
|
@@ -232,3 +230,11 @@ Return STRICT JSON:
|
|
|
232
230
|
};
|
|
233
231
|
}
|
|
234
232
|
}
|
|
233
|
+
/* -------------------------
|
|
234
|
+
Helpers
|
|
235
|
+
---------------------------- */
|
|
236
|
+
function determineShouldModify(intentCategory, isRelevant) {
|
|
237
|
+
if (!isRelevant)
|
|
238
|
+
return false;
|
|
239
|
+
return ["codingTask", "refactorTask", "writing"].includes(intentCategory);
|
|
240
|
+
}
|