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.
@@ -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
- // -------------------- INITIAL PRE-FILE CHECK --------------------
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
- let t = this.startTimer();
138
- await readinessGateStep.run(this.context);
139
- this.logLine("HASINFO", "routing", t());
119
+ const t = this.startTimer();
120
+ await fileCheckStep(this.context);
121
+ this.logLine("PRECHECK", "preFileSearch", t());
140
122
  }
141
- /* ================= INFORMATION ACQUISITION PHASE ================= */
123
+ /* ================= INFORMATION ACQUISITION ================= */
142
124
  if (this.canExecutePhase("planning")) {
143
- t = this.startTimer();
144
- await infoPlanGen.run(this.context);
145
- this.logLine("PLAN", "infoPlanGen", t());
146
- const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
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
- const newFilesFound = infoPlan.steps.length > 0;
152
- if (newFilesFound) {
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
- /* ================= ANALYSIS PHASE ================= */
141
+ /* ================= GROUNDING & READINESS GATE ================= */
162
142
  {
163
- t = this.startTimer();
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", t());
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
- t = this.startTimer();
169
- await analysisPlanGenStep.run(this.context);
170
- this.logLine("PLAN", "analysisPlanGen", t());
171
- const analysisPlan = this.context.analysis?.planSuggestion?.plan?.steps ?? [];
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
- t = this.startTimer();
179
- await planTargetFilesStep.run({ query: this.query, context: this.context });
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
- else {
188
- this.logLine("ROUTING", "gatherData", undefined, chalk.yellow(review.reason));
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 PHASE ================= */
188
+ /* ================= TRANSFORM ================= */
195
189
  if (this.canExecutePhase("transform")) {
196
- t = this.startTimer();
197
- await transformPlanGenStep.run(this.context);
198
- this.logLine("PLAN", "transformPlanGen", t());
199
- const transformSteps = (this.context.analysis?.planSuggestion?.plan?.steps ?? [])
200
- .filter(s => s.groups?.includes("transform"));
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 tWrite = this.startTimer();
207
- stepIO = await writeFileStep.run({
208
- query: this.query,
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 PHASE ================= */
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
- } // <-- close run() properly
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
- understanding: context.analysis?.understanding,
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).understanding || (_b.understanding = {});
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
- const availableFiles = focus.relevantFiles ?? [];
22
- // ================= FAST PATH =================
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: 'has-info',
27
- rationale: focus.rationale || 'All required information is already available.',
28
- confidence: 0.85,
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
- // ================= MODEL CLASSIFICATION =================
34
- // Only classify readiness; do NOT infer files or plans
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, not a problem solver.
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
- Available files:
42
- ${JSON.stringify(availableFiles, null, 2)}
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
- - Some files or context are missing, or additional analysis is required.
60
- - Information acquisition must run before execution.
64
+ - Insufficient evidence or missing critical files.
65
+ - Additional information acquisition must run.
61
66
 
62
- Return ONLY the label, optionally with a short rationale.
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
- focus.rationale?.trim(),
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') || `Missing files detected: ${missingFiles.join(', ')}`;
84
+ .join('\n');
80
85
  context.analysis.routingDecision = {
81
86
  decision: modelDecision,
82
87
  rationale: combinedRationale,
83
- confidence: modelDecision === 'has-info' ? 0.8 : 0.85,
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 the most relevant files for the query from relatedFiles + workingFiles, loads their code, and updates context.workingFiles.",
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] StructuredContext is required.");
14
+ if (!context?.analysis) {
15
+ throw new Error("[selectRelevantSources] context.analysis is required.");
15
16
  }
16
- // Merge candidate paths (relatedFiles + existing workingFiles)
17
- // At this point in-depth context build or searchfiles may have placed files in
18
- // working files.
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?.focus?.relevantFiles ?? []),
38
+ ...(context.analysis.focus?.relevantFiles ?? []),
21
39
  ...(context.initContext?.relatedFiles ?? []),
22
- ...(context.workingFiles?.map(f => f.path) ?? [])
23
40
  ];
24
- const uniquePaths = Array.from(new Set(candidatePaths));
25
- // -----------------------------
26
- // Prompt the LLM to rank files
27
- // -----------------------------
28
- const prompt = `
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(uniquePaths, null, 2)}
50
+ ${JSON.stringify(llmCandidates, null, 2)}
36
51
 
37
52
  Task:
38
- - Identify ONLY the files you find to be directly relevant to the query.
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 have at least a "path" field.
55
+ - Each object must include "path".
41
56
  - Optional fields: "summary".
42
- - Do NOT include explanations.
43
- `.trim();
44
- let topFiles = [];
45
- try {
46
- const response = await generate({ content: prompt, query: "" });
47
- const cleaned = await cleanupModule.run({
48
- query,
49
- content: response.data,
50
- });
51
- const rawFiles = Array.isArray(cleaned.data) ? cleaned.data : [];
52
- // Load code for selected files
53
- topFiles = rawFiles
54
- .map((f) => {
55
- if (typeof f?.path !== "string")
56
- return null;
57
- let code;
58
- try {
59
- code = fs.readFileSync(f.path, "utf-8");
60
- }
61
- catch (err) {
62
- console.warn(`⚠️ Failed to read file ${f.path}:`, err.message);
63
- }
64
- return { path: f.path, code };
65
- })
66
- .filter(Boolean);
67
- }
68
- catch (err) {
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 authoritative context (append, do not overwrite)
74
- // -----------------------------
85
+ // --------------------------------------------------
86
+ // 3. Persist workingFiles (append + dedupe)
87
+ // --------------------------------------------------
75
88
  context.workingFiles = [
76
- ...(context.workingFiles ?? []), // existing working files
77
- ...topFiles, // new top files
89
+ ...(context.workingFiles ?? []),
90
+ ...evidenceSelected,
91
+ ...llmSelected,
78
92
  ];
79
- // Deduplicate by path
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
- seenPaths.add(f.path);
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: topFiles.map(f => ({ path: f.path })),
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, other",
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: "other",
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: parsed.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: "other",
85
+ intentCategory: "request",
67
86
  normalizedQuery: context.initContext?.userQuery ?? '',
68
87
  confidence: 0.0
69
88
  };
@@ -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
- return { ...defaultConfig, ...JSON.parse(content) };
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 : ${repoCfg?.model || cfg.model}`);
226
- console.log(` Language : ${repoCfg?.language || cfg.language}`);
227
- console.log(` GitHub Token : ${cfg.githubToken ? '*****' : 'Not Set'}`);
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 : ${daemon.sleepMs}ms`);
230
- console.log(` Daemon idleMs : ${daemon.idleSleepMs}ms`);
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 : ${lockStatus}`);
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;
@@ -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, _c;
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
- // ───────────── SMALL FILE ─────────────
85
- if (tokenCount <= SINGLE_SHOT_TOKEN_LIMIT) {
86
- logInputOutput("codeTransform", "output", {
87
- file: file.path,
88
- tokenCount,
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
- try {
121
- logInputOutput("codeTransform", "output", { file: file.path, message: "Sending prompt to LLM" });
122
- const llmResponse = await generate({ content: prompt, query });
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
- chunkIndex: i + 1,
193
- chunkLength: chunk.length,
194
- message: "Processing chunk",
105
+ message: "Received LLM response",
106
+ responseSnippet: String(llmResponse.data ?? "").slice(0, 200),
195
107
  });
196
- const prompt = `
197
- You are a precise and conservative code transformation assistant.
198
-
199
- User instruction (normalized):
200
- ${normalizedQuery}
201
-
202
- IMPORTANT SAFETY RULES:
203
- - If this chunk contains an UNFINISHED or UNCLOSED construct
204
- (for example: an opening "{", "(", "[", "function", "class", or similar
205
- that is not clearly closed within this chunk),
206
- then you MUST return the chunk UNCHANGED.
207
- - Do NOT invent missing braces, code, or structure.
208
- - Do NOT attempt to "fix" incomplete syntax.
209
- - When in doubt, return the chunk EXACTLY as given.
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
- catch (err) {
237
- transformedChunks.push(chunk);
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
- const finalContent = transformedChunks.join("\n");
243
- outputs.push({ filePath: file.path, content: finalContent });
244
- context.execution.codeTransformArtifacts.files =
245
- context.execution.codeTransformArtifacts.files.filter(f => f.filePath !== file.path);
246
- context.execution.codeTransformArtifacts.files.push(outputs[0]);
247
- context.plan || (context.plan = {});
248
- (_c = context.plan).touchedFiles || (_c.touchedFiles = []);
249
- if (!context.plan.touchedFiles.includes(file.path))
250
- context.plan.touchedFiles.push(file.path);
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
- // Use the planSuggestion.steps to determine which files to analyze
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
- // Skip if already analyzed
43
- if (context.analysis.fileAnalysis[filePath])
44
- continue;
45
- const fileAnalysis = await analyzeFile(file, input.query, context);
46
- context.analysis.fileAnalysis[filePath] = fileAnalysis;
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 combine if > 2 files analyzed
57
+ // Only consider relevant files
55
58
  // ----------------------------
56
- const analyzedFiles = Object.keys(context.analysis.fileAnalysis);
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 (analyzedFiles.length > 2) {
63
- combinedAnalysis = await analyzeCombined(context.analysis.fileAnalysis, input.query);
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 rationaleSnippet = focus?.rationale ? `Rationale: ${focus.rationale}` : "";
83
- const assumptionsSnippet = understanding?.assumptions
84
- ? `Assumptions: ${understanding.assumptions.join("; ")}`
85
- : "";
86
- const constraintsSnippet = understanding?.constraints
87
- ? `Constraints: ${understanding.constraints.join("; ")}`
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.149",
3
+ "version": "0.1.152",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"