scai 0.1.166 → 0.1.167

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.
@@ -19,18 +19,9 @@ import { selectRelevantSourcesStep } from "./selectRelevantSourcesStep.js";
19
19
  import { iterationFileSelector } from "./iterationFileSelector.js";
20
20
  import { finalAnswerModule } from "../pipeline/modules/finalAnswerModule.js";
21
21
  import { reasonNextStep } from "./reasonNextStep.js";
22
+ import chalk from "chalk";
22
23
  /* ───────────────────────── registry ───────────────────────── */
23
- /**
24
- * Registry mapping action names to their corresponding Module implementations.
25
- * This registry is populated with built-in modules from the module registry.
26
- */
27
24
  const MODULE_REGISTRY = Object.fromEntries(Object.entries(builtInModules).map(([name, mod]) => [name, mod]));
28
- /**
29
- * Resolves a module by its action name.
30
- *
31
- * @param action - The action name to resolve.
32
- * @returns The Module implementation if found, otherwise undefined.
33
- */
34
25
  function resolveModuleForAction(action) {
35
26
  return MODULE_REGISTRY[action];
36
27
  }
@@ -66,7 +57,6 @@ export class MainAgent {
66
57
  async run() {
67
58
  this.runCount = 0;
68
59
  await this.runBoot();
69
- await this.runPrecheck();
70
60
  await this.runScope();
71
61
  await this.runGrounding();
72
62
  await this.runWorkLoop();
@@ -78,27 +68,23 @@ export class MainAgent {
78
68
  await understandIntentStep.run({ context: this.context });
79
69
  await resolveExecutionModeStep.run(this.context);
80
70
  // Boot the task and get the real DB taskId
81
- this.taskId = bootTaskForRepo(this.context, getDbForRepo(), this.logLine.bind(this));
82
- // Ensure context.task exists and has all required fields
71
+ this.taskId = bootTaskForRepo(this.context, getDbForRepo(), (phase, step, ms, desc) => this.logLine(phase, step, ms, desc, { highlight: true }));
83
72
  (_a = this.context).task || (_a.task = {
84
73
  id: this.taskId,
85
- projectId: 0, // optionally set to a real projectId if available
74
+ projectId: 0,
86
75
  status: "active",
87
76
  initialQuery: this.context.initContext?.userQuery ?? "",
88
77
  createdAt: new Date().toISOString(),
89
78
  updatedAt: new Date().toISOString(),
90
79
  taskSteps: [],
91
80
  });
92
- // If task existed but id wasn’t set, set it now
93
81
  this.context.task.id = this.taskId;
94
- }
95
- /* ───────────── precheck ───────────── */
96
- async runPrecheck() {
97
- await fileCheckStep(this.context);
82
+ this.logLine("TASK", "Boot complete", undefined, `taskId=${this.taskId}`, { highlight: true });
98
83
  }
99
84
  /* ───────────── scope ───────────── */
100
85
  async runScope() {
101
86
  await scopeClassificationStep.run(this.context);
87
+ this.logLine("TASK", "Scope classification complete");
102
88
  }
103
89
  /* ───────────── grounding / info acquisition loop ───────────── */
104
90
  async runGrounding() {
@@ -108,48 +94,44 @@ export class MainAgent {
108
94
  if (this.canExecutePhase("planning") &&
109
95
  this.canExecuteScope("planning")) {
110
96
  const t = this.startTimer();
111
- // Generate info plan step(s) in context
112
97
  await infoPlanGenStep.run(this.context);
113
98
  const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
114
99
  for (const step of infoPlan.steps) {
115
100
  const stepIO = { query: this.query };
116
101
  await this.executeStep(step, stepIO);
117
- // Re-check any new files discovered
118
- const t2 = this.startTimer();
119
- await fileCheckStep(this.context);
120
- this.logLine("PRECHECK", "postFileSearch", t2());
121
102
  }
122
- this.logLine("PLAN", "infoPlanGen", t());
103
+ this.logLine("PLAN", "infoPlanGen", t(), undefined, { highlight: false });
123
104
  }
124
105
  // ---------------- DETERMINISTIC EVIDENCE VERIFICATION ----------------
125
106
  const t1 = this.startTimer();
126
107
  await evidenceVerifierStep.run({ query: this.query, context: this.context });
127
- this.logLine("ANALYSIS", "collectAnalysisEvidence", t1(), undefined);
128
- // Select grounded candidate files
108
+ this.logLine("ANALYSIS", "collectAnalysisEvidence", t1());
109
+ /* ───────────── precheck ───────────── */
129
110
  const t2 = this.startTimer();
111
+ await fileCheckStep(this.context);
112
+ this.logLine("ANALYSIS", "fileCheckStep", t2());
113
+ // Select grounded candidate files
114
+ const t3 = this.startTimer();
130
115
  await selectRelevantSourcesStep.run({ query: this.query, context: this.context });
131
- this.logLine("ANALYSIS", "selectRelevantSources", t2(), undefined);
116
+ this.logLine("ANALYSIS", "selectRelevantSources", t3());
132
117
  // ---------------- READINESS GATE ----------------
133
- const t3 = this.startTimer();
118
+ const t4 = this.startTimer();
134
119
  await readinessGateStep.run(this.context);
135
- this.logLine("HASINFO", "readinessGate", t3(), undefined);
120
+ this.logLine("HASINFO", "readinessGate", t4());
136
121
  ready = this.context.analysis?.readiness?.decision === "ready";
137
122
  if (!ready) {
138
123
  this.runCount++;
139
- // Previously resetInitContextForLoop removed; optionally clear temporary data
140
- if (this.context.analysis) {
124
+ if (this.context.analysis)
141
125
  this.context.analysis.planSuggestion = undefined;
142
- }
143
- this.logLine("HASINFO", "readinessGate", undefined, "Not ready, looping back to information acquisition");
126
+ this.logLine("HASINFO", "Not ready — looping back to information acquisition", undefined, undefined, { highlight: false });
144
127
  }
145
128
  }
146
129
  }
147
130
  /* ───────────── finalize ───────────── */
148
131
  async runFinalize() {
149
- // generate final answer
150
132
  await finalAnswerModule.run({ query: this.query, context: this.context });
151
- // persist task and steps
152
133
  persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
134
+ this.logLine("TASK", "Finalize complete", undefined, undefined, { highlight: false });
153
135
  }
154
136
  /* ───────────── work loop ───────────── */
155
137
  async runWorkLoop() {
@@ -159,29 +141,21 @@ export class MainAgent {
159
141
  await reasonNextTaskStep.run(this.context);
160
142
  const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
161
143
  if (nextAction === "complete") {
162
- this.logLine("TASK", "All selected files processed — task complete");
144
+ this.logLine("TASK", "All selected files processed — task complete", undefined, undefined, { highlight: false });
163
145
  return;
164
146
  }
165
147
  const taskStep = await iterationFileSelector.run(this.context);
166
148
  if (!taskStep) {
167
- this.logLine("TASK", "No eligible taskStep found — task complete");
149
+ this.logLine("TASK", "No eligible taskStep found — task complete", undefined, undefined, { highlight: false });
168
150
  return;
169
151
  }
170
- // Set current step in context
171
- if (this.context.task) {
172
- this.context.task.currentStep = taskStep;
173
- }
152
+ this.context.task.currentStep = taskStep;
174
153
  stepCount++;
175
154
  taskStep.taskId = this.taskId;
176
155
  taskStep.stepIndex = stepCount;
177
156
  taskStep.status = "pending";
178
- // ⬇️ Persist newly created step
179
157
  persistTaskStepInsert(taskStep, getDbForRepo());
180
- logInputOutput("Step to process", "input", taskStep);
181
- this.logLine("TASK", `Processing taskStep ${stepCount}/${MAX_TASK_STEPS}`, undefined, taskStep.filePath);
182
- // ---------------------------
183
- // Start the step
184
- // ---------------------------
158
+ this.logLine("NEW STEP", `Processing taskStep ${stepCount}/${MAX_TASK_STEPS}`, undefined, taskStep.filePath, { highlight: true });
185
159
  taskStep.startTime = Date.now();
186
160
  persistTaskStepStart(taskStep, getDbForRepo());
187
161
  // ---------------------------
@@ -192,14 +166,15 @@ export class MainAgent {
192
166
  taskStep.status = "completed";
193
167
  taskStep.endTime = Date.now();
194
168
  persistTaskStepCompletion(taskStep, getDbForRepo());
169
+ this.logLine("STEP-DONE", `Completed taskStep ${stepCount}`, undefined, taskStep.filePath, { highlight: false });
195
170
  }
196
171
  else {
197
- // optionally mark as pending or redo-step
198
172
  taskStep.status = "pending";
199
173
  persistTaskStepCompletion(taskStep, getDbForRepo());
174
+ this.logLine("STEP", `Pending taskStep ${stepCount}`, undefined, taskStep.filePath);
200
175
  }
201
176
  }
202
- this.logLine("TASK", "Max task step limit reached — stopping work loop");
177
+ this.logLine("TASK", "Max task step limit reached — stopping work loop", undefined, undefined, { highlight: false });
203
178
  }
204
179
  /* ───────────── step iterations ───────────── */
205
180
  async runStepIterations(taskStep) {
@@ -207,31 +182,22 @@ export class MainAgent {
207
182
  let loopCount = 0;
208
183
  const getNextIterationAction = () => {
209
184
  const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
210
- if (nextAction !== "continue" &&
211
- nextAction !== "redo-step" &&
212
- nextAction !== "expand-scope" &&
213
- nextAction !== "request-feedback" &&
214
- nextAction !== "complete") {
215
- return "complete"; // fallback
216
- }
185
+ if (!["continue", "redo-step", "expand-scope", "request-feedback", "complete"].includes(nextAction ?? ""))
186
+ return "complete";
217
187
  return nextAction;
218
188
  };
219
- // Loop through iterations for a single task step (file)
220
189
  while (loopCount < MAX_ITERATIONS) {
221
190
  this.runCount++;
222
191
  loopCount++;
223
- // 🔴 HARD RESET — no carryover allowed
224
- if (this.context.analysis?.iterationReasoning) {
192
+ if (this.context.analysis?.iterationReasoning)
225
193
  this.context.analysis.iterationReasoning.nextAction = undefined;
226
- }
227
194
  await this.runWorkIteration(taskStep);
228
195
  const nextAction = getNextIterationAction();
229
- this.logLine("STEP-LOOP", "nextAction", undefined, nextAction);
196
+ this.logLine("STEP-LOOP", `nextAction = ${nextAction}`);
230
197
  if (nextAction === "complete")
231
198
  return "complete";
232
199
  if (nextAction === "request-feedback")
233
200
  return "request-feedback";
234
- // else: continue
235
201
  }
236
202
  return "continue";
237
203
  }
@@ -239,56 +205,47 @@ export class MainAgent {
239
205
  async runWorkIteration(taskStep) {
240
206
  if (!this.context.analysis)
241
207
  this.context.analysis = {};
242
- // ---------------- ANALYSIS ----------------
243
208
  if (this.canExecutePhase("analysis") && this.canExecuteScope("analysis")) {
244
209
  const tAnalysis = this.startTimer();
245
210
  await analysisPlanGenStep.run(this.context);
246
- this.logLine("PLAN", "analysisPlanGen", tAnalysis());
211
+ this.logLine("PLAN", "analysisPlanGen", tAnalysis(), undefined, { highlight: false });
247
212
  const analysisPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
248
213
  for (const step of analysisPlan.steps) {
249
214
  const tStep = this.startTimer();
250
215
  await this.executeStep(step, { query: this.query });
251
- this.logLine("STEP", step.action || "unnamedStep", tStep());
216
+ this.logLine("PLANNING-STEP", step.action || "unnamedStep", tStep());
252
217
  }
253
- if (this.context.analysis) {
218
+ if (this.context.analysis)
254
219
  this.context.analysis.planSuggestion = undefined;
255
- }
256
220
  }
257
- // ---------------- TRANSFORM ----------------
258
221
  if (this.canExecutePhase("transform") && this.canExecuteScope("transform")) {
259
222
  const tTransform = this.startTimer();
260
223
  await transformPlanGenStep.run(this.context);
261
- this.logLine("PLAN", "transformPlanGen", tTransform());
224
+ this.logLine("PLAN", "transformPlanGen", tTransform(), undefined, { highlight: false });
262
225
  const transformPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
263
226
  const firstStep = transformPlan.steps[0];
264
227
  if (firstStep) {
265
228
  const tStep = this.startTimer();
266
229
  await this.executeStep(firstStep, { query: this.query });
267
- this.logLine("STEP", `#1 (only) - ${firstStep.action || "unnamedStep"}`, tStep());
230
+ this.logLine("PLANNING-STEP", `#1 (only) - ${firstStep.action || "unnamedStep"}`, tStep());
268
231
  }
269
- if (this.context.analysis) {
232
+ if (this.context.analysis)
270
233
  this.context.analysis.planSuggestion = undefined;
271
- }
272
- // ---------------- WRITE ----------------
273
234
  if (this.canExecutePhase("write") && this.canExecuteScope("write")) {
274
235
  const tWrite = this.startTimer();
275
236
  await writeFileStep.run({ query: this.query, context: this.context });
276
237
  this.logLine("WRITE", "writeFileStep", tWrite());
277
238
  }
278
- // ---------------- VALIDATION ----------------
279
239
  const tValidate = this.startTimer();
280
240
  await validateChangesStep.run(this.context);
281
241
  this.logLine("VALIDATION", "validateChangesStep", tValidate());
282
242
  }
283
- // ---------------- REASONING ----------------
284
243
  const tReason = this.startTimer();
285
244
  await reasonNextStep.run(this.context, taskStep);
286
245
  this.logLine("REASONING", "reasonNextStep", tReason());
287
- // ---------------- COLLABORATOR FEEDBACK ----------------
288
246
  const tCollab = this.startTimer();
289
247
  await collaboratorStep.run(this.context);
290
248
  this.logLine("FEEDBACK", "collaboratorStep", tCollab());
291
- // ---------------- INTEGRATE FEEDBACK ----------------
292
249
  const tIntegrate = this.startTimer();
293
250
  await integrateFeedbackStep.run(this.context);
294
251
  this.logLine("FEEDBACK", "integrateFeedbackStep", tIntegrate());
@@ -304,9 +261,7 @@ export class MainAgent {
304
261
  */
305
262
  async executeStep(step, input) {
306
263
  const stop = this.startTimer();
307
- // Set current step in context
308
264
  this.context.currentStep = step;
309
- // Resolve module for this action
310
265
  const mod = resolveModuleForAction(step.action);
311
266
  if (!mod) {
312
267
  this.logLine("EXECUTE", step.action, stop(), "skipped (missing module)");
@@ -314,13 +269,7 @@ export class MainAgent {
314
269
  }
315
270
  try {
316
271
  this.ui.update(`Running step: ${step.action}`);
317
- // Execute the module
318
- await mod.run({
319
- query: step.description ?? input.query,
320
- content: input.data ?? input.content,
321
- context: this.context
322
- });
323
- // Return ModuleIO (can remain minimal since output is untyped)
272
+ await mod.run({ query: step.description ?? input.query, content: input.data ?? input.content, context: this.context });
324
273
  return { query: step.description ?? input.query, data: {} };
325
274
  }
326
275
  catch (err) {
@@ -342,12 +291,10 @@ export class MainAgent {
342
291
  switch (phase) {
343
292
  case "analysis":
344
293
  case "planning":
345
- // Read-only phases are always allowed
346
294
  allowed = !docsOnly;
347
295
  break;
348
296
  case "transform":
349
297
  case "write":
350
- // Side-effect phases require explicit permission
351
298
  allowed = constraints?.allowFileWrites ?? false;
352
299
  break;
353
300
  }
@@ -365,14 +312,10 @@ export class MainAgent {
365
312
  let allowed = false;
366
313
  switch (scope) {
367
314
  case "none":
368
- // Non-repo questions: analysis/planning only
369
315
  allowed = phase === "analysis" || phase === "planning";
370
316
  break;
371
- case "single-file":
372
- case "multi-file":
373
- case "repo-wide":
317
+ default:
374
318
  allowed = true;
375
- break;
376
319
  }
377
320
  return allowed;
378
321
  }
@@ -396,38 +339,33 @@ export class MainAgent {
396
339
  * @param fn - The function to execute while spinner is paused.
397
340
  */
398
341
  withSpinnerPaused(fn) {
399
- this.ui.pause(() => {
400
- // Ensure spinner line is fully gone
401
- process.stdout.write('\r\x1b[K');
402
- fn();
403
- });
342
+ this.ui.pause(() => { process.stdout.write('\r\x1b[K'); fn(); });
404
343
  }
405
- /**
406
- * Logs a line with timing information.
407
- *
408
- * @param phase - The phase of execution.
409
- * @param step - The step being executed.
410
- * @param ms - The execution time in milliseconds.
411
- * @param desc - Optional description of the step.
412
- */
413
- logLine(phase, step, ms, desc) {
344
+ logLine(phase, step, ms, desc, options) {
414
345
  this.withSpinnerPaused(() => {
415
346
  const suffix = desc ? ` — ${desc}` : "";
416
347
  const timing = typeof ms === "number" ? ` (${ms}ms)` : "";
417
- console.log(`[AGENT] ${phase} :: ${step}${suffix}${timing}`);
348
+ let line = `[AGENT] ${phase} :: ${step}${suffix}${timing}`;
349
+ // Coloring rules
350
+ if (phase === "NEW STEP" || phase === "STEP") {
351
+ line = chalk.green(line);
352
+ }
353
+ else if (options?.highlight) {
354
+ line = chalk.green(line);
355
+ }
356
+ // Print a blank line **before** NEW STEP only
357
+ if (phase === "NEW STEP")
358
+ console.log();
359
+ console.log(line);
418
360
  });
419
361
  }
420
- /**
421
- * Logs a message to the user output.
422
- *
423
- * @param message - The message to log.
424
- */
425
362
  userOutput(message) {
426
363
  this.withSpinnerPaused(() => {
427
364
  console.log(`[USER OUTPUT] ${message}`);
428
365
  });
429
366
  }
430
367
  }
368
+ // All helper functions (persistTaskData, bootTaskForRepo, persistTaskStep*) remain unchanged
431
369
  /* ───────────── FOLDER CAPSULES SUMMARY HELPER ───────────── */
432
370
  /**
433
371
  * Generates a human-readable summary of folder capsules and logs it.
@@ -3,15 +3,17 @@ import fs from "fs";
3
3
  import { logInputOutput } from "../utils/promptLogHelper.js";
4
4
  /**
5
5
  * Deterministic evidence verification:
6
- * - Scans candidate files line-by-line for exact or heuristic matches to the query.
7
- * - Marks relevance passively; only enables modification when evidence is found.
6
+ * - Scans candidate files line-by-line for meaningful matches to the query.
7
+ * - Filters stopwords and short tokens.
8
+ * - Deduplicates symbol evidence per file.
9
+ * - Removes low-signal keyword clustering.
8
10
  */
9
11
  export const evidenceVerifierStep = {
10
12
  name: "evidenceVerifier",
11
13
  description: "Deterministic evidence-first scan over candidate files to populate fileAnalysis, with filename dominance.",
12
14
  groups: ["analysis"],
13
15
  run: async (input) => {
14
- var _a, _b, _c;
16
+ var _a, _b;
15
17
  const query = input.query ?? "";
16
18
  const context = input.context;
17
19
  if (!context?.analysis) {
@@ -19,24 +21,32 @@ export const evidenceVerifierStep = {
19
21
  }
20
22
  (_a = context.analysis).fileAnalysis ?? (_a.fileAnalysis = {});
21
23
  const candidatePaths = [
22
- ...(context.analysis.focus?.selectedFiles ?? []),
23
- ...(context.analysis.focus?.candidateFiles ?? []),
24
+ ...(context.initContext?.relatedFiles ?? []),
24
25
  ];
25
26
  const uniquePaths = Array.from(new Set(candidatePaths));
26
27
  if (!uniquePaths.length) {
27
28
  console.warn("[evidenceVerifier] No candidate files to scan.");
28
29
  return { query, data: {} };
29
30
  }
30
- const filesWithEvidence = [];
31
+ // ----------------- Stopwords -----------------
32
+ const STOPWORDS = new Set([
33
+ "the", "and", "for", "with", "from", "that",
34
+ "this", "are", "was", "were", "has", "have",
35
+ "had", "not", "but", "can", "could", "should",
36
+ "would", "into", "onto", "about", "above",
37
+ "below", "under", "over", "then", "else",
38
+ "when", "where", "what", "which", "while",
39
+ "return", "const", "let", "var", "true", "false",
40
+ "null", "undefined", "new", "set", "get",
41
+ "in", "to", "of", "on", "at", "by"
42
+ ]);
31
43
  // ----------------- Parse query for targets -----------------
32
44
  const sentenceTargets = [];
33
- // 1️⃣ Capture text inside quotes (existing behavior)
34
45
  const quoteRegex = /['"`](.+?)['"`]/g;
35
46
  let match;
36
47
  while ((match = quoteRegex.exec(query)) !== null) {
37
48
  sentenceTargets.push(match[1]);
38
49
  }
39
- // 2️⃣ Capture heuristic unquoted sentences if none found inside quotes
40
50
  if (!sentenceTargets.length) {
41
51
  const heuristicSentences = query
42
52
  .split(/[\.\n]/)
@@ -51,15 +61,43 @@ export const evidenceVerifierStep = {
51
61
  const filenameTargets = query
52
62
  .split(/\s+/)
53
63
  .map(word => word.replace(/['",]/g, ''))
54
- .filter(w => w.match(/\.(ts|js|tsx|md)$/) || w.length > 5);
64
+ .filter(w => w.match(/\.(ts|js|tsx|md)$/));
55
65
  const baseNameTargets = filenameTargets.map(t => t.replace(/\.(ts|js|tsx|md)$/, ''));
66
+ // ---- Symbol extraction (filtered + deduplicated) ----
56
67
  const symbolTargets = [];
57
- const symbolRegex = /\b([a-zA-Z_]\w+)(?:\(\))?\b/g;
68
+ const symbolRegex = /\b([a-zA-Z_]\w{2,})(?:\(\))?\b/g;
58
69
  let symMatch;
59
70
  while ((symMatch = symbolRegex.exec(query)) !== null) {
60
- symbolTargets.push(symMatch[1]);
71
+ const token = symMatch[1];
72
+ if (token.length >= 3 &&
73
+ !STOPWORDS.has(token.toLowerCase()) &&
74
+ !sentenceTargets.includes(token)) {
75
+ symbolTargets.push(token);
76
+ }
77
+ }
78
+ const uniqueSymbolTargets = Array.from(new Set(symbolTargets));
79
+ // ----------------- Token strength tiering -----------------
80
+ const WEAK_TOKENS = new Set([
81
+ "file",
82
+ "line",
83
+ "move",
84
+ "update",
85
+ "change",
86
+ "modify",
87
+ "readme"
88
+ ]);
89
+ function computeSymbolConfidence(sym) {
90
+ const lower = sym.toLowerCase();
91
+ // If symbol appears inside quoted sentence → very strong
92
+ const fromQuoted = sentenceTargets.some(s => s.toLowerCase().includes(lower));
93
+ if (fromQuoted)
94
+ return 0.95;
95
+ // Weak structural tokens → low weight
96
+ if (WEAK_TOKENS.has(lower))
97
+ return 0.5;
98
+ // Default meaningful symbol
99
+ return 0.85;
61
100
  }
62
- const mode = context.executionControl?.mode ?? "transform";
63
101
  // ----------------- Process each file -----------------
64
102
  for (const path of uniquePaths) {
65
103
  let code = null;
@@ -72,60 +110,98 @@ export const evidenceVerifierStep = {
72
110
  const lines = code?.split("\n") ?? [];
73
111
  const evidenceItems = [];
74
112
  const matchedLines = [];
75
- // Sentence & Symbol evidence
113
+ const addedSymbols = new Set();
114
+ // Sentence matches
76
115
  lines.forEach((line, idx) => {
77
116
  sentenceTargets.forEach(target => {
78
117
  if (line.includes(target)) {
118
+ const snippet = lines
119
+ .slice(Math.max(0, idx - 1), Math.min(lines.length, idx + 2))
120
+ .join("\n");
79
121
  evidenceItems.push({
80
- claim: `Line contains the target sentence: "${target}"`,
81
- excerpt: line,
122
+ claim: `Sentence match: "${target}"`,
123
+ type: "sentence",
124
+ excerpt: snippet,
82
125
  span: { startLine: idx + 1, endLine: idx + 1 },
83
126
  confidence: 1,
84
127
  });
85
128
  matchedLines.push(line);
86
129
  }
87
130
  });
88
- symbolTargets.forEach(sym => {
89
- if (line.includes(`function ${sym}`) || line.includes(`class ${sym}`)) {
90
- evidenceItems.push({
91
- claim: `Line contains target symbol: "${sym}"`,
92
- excerpt: line,
93
- span: { startLine: idx + 1, endLine: idx + 1 },
94
- confidence: 0.9,
95
- });
96
- matchedLines.push(line);
131
+ });
132
+ // Symbol matches (deduplicated per symbol)
133
+ uniqueSymbolTargets.forEach(sym => {
134
+ for (let idx = 0; idx < lines.length; idx++) {
135
+ const line = lines[idx];
136
+ if (line.includes(`function ${sym}`) ||
137
+ line.includes(`class ${sym}`) ||
138
+ line.includes(sym)) {
139
+ if (!addedSymbols.has(sym)) {
140
+ addedSymbols.add(sym);
141
+ const snippet = lines
142
+ .slice(Math.max(0, idx - 1), Math.min(lines.length, idx + 2))
143
+ .join("\n");
144
+ evidenceItems.push({
145
+ claim: `Symbol reference found: "${sym}"`,
146
+ type: "symbol",
147
+ excerpt: snippet,
148
+ span: { startLine: idx + 1, endLine: idx + 1 },
149
+ confidence: computeSymbolConfidence(sym),
150
+ });
151
+ matchedLines.push(line);
152
+ }
153
+ break; // stop after first meaningful match
97
154
  }
98
- });
155
+ }
99
156
  });
100
157
  // Filename-level evidence
101
158
  const fullFileName = path.split("/").pop() ?? "";
102
159
  const baseFileName = fullFileName.replace(/\.(ts|js|tsx|md)$/, "");
103
- if (filenameTargets.includes(fullFileName) || baseNameTargets.includes(baseFileName)) {
160
+ if (filenameTargets.includes(fullFileName) ||
161
+ baseNameTargets.includes(baseFileName)) {
104
162
  evidenceItems.push({
105
163
  claim: `Filename matches query target: "${fullFileName}"`,
164
+ type: "filename",
106
165
  excerpt: `Path: ${path}`,
107
166
  span: { startLine: 0, endLine: 0 },
108
167
  confidence: 1,
109
168
  });
110
169
  }
170
+ // ----------------- Compute file-level confidence -----------------
171
+ let fileScore = 0;
172
+ for (const ev of evidenceItems) {
173
+ if (ev.type === "sentence")
174
+ fileScore += 1.0;
175
+ else if (ev.type === "filename")
176
+ fileScore += 1.0;
177
+ else if (ev.type === "symbol")
178
+ fileScore += ev.confidence ?? 0.8;
179
+ }
180
+ // Normalize to 0–1 range (soft cap)
181
+ const fileConfidence = fileScore === 0
182
+ ? 0
183
+ : Math.min(1, fileScore / 3); // 3 strong signals = max confidence
111
184
  const isFocusFile = context.analysis.focus?.selectedFiles?.includes(path) ?? false;
112
185
  const hasEvidence = evidenceItems.length > 0;
113
186
  // ----------------- Merge into fileAnalysis -----------------
114
187
  if (isFocusFile || hasEvidence) {
188
+ const confidenceLabel = fileConfidence.toFixed(2);
115
189
  context.analysis.fileAnalysis[path] = {
116
190
  ...context.analysis.fileAnalysis[path],
117
191
  intent: "relevant",
118
- relevanceExplanation: `${evidenceItems.length} evidence item(s) match the query${isFocusFile ? " (focus file already selected)" : ""}`,
192
+ relevanceExplanation: `[confidence:${confidenceLabel}] ${evidenceItems.length} evidence item(s) match the query${isFocusFile ? " (focus file already selected)" : ""}`,
119
193
  role: "primary",
120
194
  action: {
121
195
  isRelevant: true,
122
- shouldModify: hasEvidence
196
+ shouldModify: hasEvidence,
123
197
  },
124
198
  proposedChanges: hasEvidence
125
199
  ? {
126
200
  summary: "Evidence found in file",
127
201
  scope: "minor",
128
- targets: matchedLines.length ? matchedLines : undefined,
202
+ targets: matchedLines.length
203
+ ? Array.from(new Set(matchedLines))
204
+ : undefined,
129
205
  }
130
206
  : {
131
207
  summary: "No evidence found",
@@ -137,9 +213,6 @@ export const evidenceVerifierStep = {
137
213
  : ["No concrete evidence found; modification not permitted"],
138
214
  evidence: evidenceItems,
139
215
  };
140
- if (hasEvidence) {
141
- filesWithEvidence.push(path);
142
- }
143
216
  }
144
217
  else {
145
218
  (_b = context.analysis.fileAnalysis)[path] || (_b[path] = {
@@ -152,12 +225,6 @@ export const evidenceVerifierStep = {
152
225
  });
153
226
  }
154
227
  }
155
- // ----------------- Update focus after processing -----------------
156
- (_c = context.analysis).focus ?? (_c.focus = { selectedFiles: [] });
157
- context.analysis.focus.selectedFiles = Array.from(new Set([
158
- ...(context.analysis.focus.selectedFiles ?? []),
159
- ...filesWithEvidence,
160
- ]));
161
228
  const output = {
162
229
  query,
163
230
  data: { fileAnalysis: context.analysis.fileAnalysis },
@@ -10,7 +10,9 @@ export async function fileCheckStep(context) {
10
10
  const intent = context.analysis.intent;
11
11
  const planSuggestion = context.analysis.planSuggestion?.text ?? "";
12
12
  // Step 1: gather known files from initContext only
13
- const knownFiles = new Set(context.initContext?.relatedFiles ?? []);
13
+ const knownFiles = new Set([
14
+ ...(context.initContext?.relatedFiles ?? []),
15
+ ]);
14
16
  // Step 2: extract file names from normalizedQuery or planSuggestion
15
17
  const extractedFiles = extractFilesFromAnalysis(context.analysis);
16
18
  // Step 3: populate focus with safe defaults
@@ -31,9 +33,30 @@ export async function fileCheckStep(context) {
31
33
  context.analysis.focus.candidateFiles = candidateFiles;
32
34
  context.analysis.focus.rationale =
33
35
  `Pre-check: ${selectedFiles.length} files already available, ${candidateFiles.length} unresolved candidate(s).`;
34
- // Optional: LLM call for semantic understanding (assumptions, constraints, risks)
36
+ // ----------------- Deterministic Evidence Extraction -----------------
37
+ const evidenceByFile = Object.entries(context.analysis.fileAnalysis ?? {}).reduce((acc, [filePath, analysis]) => {
38
+ if (!analysis?.evidence?.length)
39
+ return acc;
40
+ const evidenceItems = analysis.evidence.map(ev => ({
41
+ type: ev.type,
42
+ claim: ev.claim,
43
+ excerpt: ev.excerpt,
44
+ span: ev.span,
45
+ confidence: ev.confidence
46
+ }));
47
+ // Deterministic file-level confidence aggregation
48
+ const fileConfidence = evidenceItems.reduce((sum, e) => sum + (e.confidence ?? 0), 0) /
49
+ evidenceItems.length;
50
+ acc[filePath] = {
51
+ fileConfidence: Number(fileConfidence.toFixed(3)),
52
+ evidenceCount: evidenceItems.length,
53
+ evidence: evidenceItems
54
+ };
55
+ return acc;
56
+ }, {});
57
+ // ----------------- Prompt -----------------
35
58
  const prompt = `
36
- You are an AI meta-agent assisting with context verification. The user intent and plan suggestion are below.
59
+ You are an AI meta-agent assisting with context verification.
37
60
 
38
61
  User intent:
39
62
  ${JSON.stringify(intent ?? {}, null, 2)}
@@ -44,11 +67,23 @@ ${planSuggestion}
44
67
  Known files in context:
45
68
  ${JSON.stringify([...knownFiles], null, 2)}
46
69
 
70
+ Deterministic evidence per file:
71
+ ${JSON.stringify(evidenceByFile, null, 2)}
72
+
73
+ Evidence rules:
74
+ - Evidence includes a type ("filename", "symbol", "sentence", "keyword-cluster").
75
+ - Each evidence item includes a confidence score between 0 and 1.
76
+ - Each file includes an aggregated fileConfidence (average of its evidence confidence).
77
+ - Higher confidence and stronger evidence types (e.g. filename, symbol definition) indicate stronger relevance.
78
+ - Evidence alone is NOT sufficient — it must align with user intent.
79
+
47
80
  Task:
48
- 1. Confirm which files are already available to satisfy the intent.
49
- 2. List unresolved candidate files if any.
50
- 3. Suggest any assumptions, constraints, or risks that may affect execution.
51
- 4. Return STRICT JSON with shape:
81
+ 1. Evaluate file relevance using BOTH fileConfidence and semantic alignment with intent.
82
+ 2. Prefer files with higher fileConfidence when multiple candidates exist.
83
+ 3. Only promote files to selectedFiles if they clearly satisfy the user intent.
84
+ 4. List unresolved candidate files if any.
85
+ 5. Suggest assumptions, constraints, or risks that may affect execution.
86
+ 6. Return STRICT JSON with shape:
52
87
 
53
88
  {
54
89
  "selectedFiles": string[],
@@ -70,11 +70,46 @@ export const reasonNextTaskStep = {
70
70
  // ---------------------------
71
71
  // 5️⃣ Task-level reasoning (cross-file)
72
72
  // ---------------------------
73
+ // Instead of deriving remainingFiles from artifacts, use TaskStep.status
74
+ const pendingFiles = intendedFiles.filter(path => {
75
+ const step = taskSteps.find(s => s.filePath === path);
76
+ return !step || step.status !== "completed";
77
+ });
78
+ // ---------------------------
79
+ // 🛑 Deterministic Completion Gate
80
+ // ---------------------------
81
+ const isDeterministicallyComplete = redoFiles.length === 0 &&
82
+ pendingFiles.length === 0;
83
+ if (isDeterministicallyComplete) {
84
+ // ✅ mark task as completed
85
+ context.task.status = "completed";
86
+ context.task.currentStep = undefined;
87
+ context.analysis.iterationReasoning = {
88
+ ...iteration,
89
+ nextAction: "complete",
90
+ rationale: "All selected files have completed TaskSteps and passed validation.",
91
+ confidence: 1.0,
92
+ redoFiles: [],
93
+ nextTargets: { files: [] },
94
+ summary: "Task step decision: complete",
95
+ };
96
+ logInputOutput("reasonNextTaskStep", "output", {
97
+ intendedFiles,
98
+ pendingFiles,
99
+ redoFiles,
100
+ currentStep: context.task.currentStep,
101
+ iterationReasoning: context.analysis.iterationReasoning,
102
+ });
103
+ return; // 🔴 HARD EXIT — skip Step 6 and risk reasoning entirely
104
+ }
105
+ // ---------------------------
106
+ // Task-level reasoning (cross-file)
107
+ // ---------------------------
73
108
  const remainingFiles = intendedFiles.filter(path => !transformedFiles.includes(path) &&
74
109
  !analyzedFiles.includes(path) &&
75
110
  !redoFiles.includes(path));
76
111
  // ---------------------------
77
- // 6️⃣ Decide next step
112
+ // Decide next step
78
113
  // ---------------------------
79
114
  let nextAction = "continue";
80
115
  let rationale = "";
@@ -3,38 +3,29 @@ import fs from "fs";
3
3
  import { logInputOutput } from "../utils/promptLogHelper.js";
4
4
  /**
5
5
  * Purpose:
6
- * Selects relevant files based on evidence strength.
7
- * - Files with concrete evidence (shouldModify = true) are selected.
8
- * - Relevant files without evidence are treated as candidates.
9
- * - Rewrites focus rationale based on observed evidence.
6
+ * Mirrors previously selected files into workingFiles.
7
+ * - Does NOT promote files based on evidence.
8
+ * - Candidate files are left untouched.
9
+ * - Rationale is preserved from preFileSearchCheckStep.
10
10
  */
11
11
  export const selectRelevantSourcesStep = {
12
12
  name: "selectRelevantSources",
13
- description: "Selects relevant files from analysis.fileAnalysis using evidence and modification readiness.",
13
+ description: "Mirrors focus.selectedFiles into workingFiles without reasoning over evidence.",
14
14
  groups: ["analysis"],
15
15
  run: async (input) => {
16
16
  const query = input.query ?? "";
17
17
  const context = input.context;
18
- if (!context?.analysis) {
19
- throw new Error("[selectRelevantSources] context.analysis is required.");
18
+ if (!context?.analysis?.focus) {
19
+ throw new Error("[selectRelevantSources] context.analysis.focus is required.");
20
20
  }
21
- const fileAnalysis = context.analysis.fileAnalysis ?? {};
22
- const selected = [];
23
- const candidates = [];
24
- const discarded = [];
25
- const rationaleParts = [];
21
+ const selectedFiles = context.analysis.focus.selectedFiles ?? [];
22
+ const candidateFiles = context.analysis.focus.candidateFiles ?? [];
23
+ const discardedFiles = []; // no reasoning done here
26
24
  // ---------------------------
27
- // 1️⃣ Classify files by relevance, evidence, and mode
25
+ // 1️⃣ Populate workingFiles from selectedFiles only
28
26
  // ---------------------------
29
- const mode = context.executionControl?.mode ?? "transform";
30
- for (const [path, analysis] of Object.entries(fileAnalysis)) {
31
- const isRelevant = analysis.action?.isRelevant === true;
32
- if (!isRelevant) {
33
- discarded.push(path);
34
- continue;
35
- }
36
- const hasEvidence = (analysis.evidence?.length ?? 0) > 0;
37
- const canModify = analysis.action?.shouldModify === true;
27
+ const workingFiles = [];
28
+ for (const path of selectedFiles) {
38
29
  let code;
39
30
  try {
40
31
  code = fs.readFileSync(path, "utf-8");
@@ -42,72 +33,40 @@ export const selectRelevantSourcesStep = {
42
33
  catch {
43
34
  code = undefined;
44
35
  }
45
- // 🔑 Mode-aware selection rule
46
- const isSelected = mode === "transform"
47
- ? hasEvidence && canModify
48
- : true; // analyze: relevance is sufficient
49
- if (isSelected) {
50
- selected.push({
51
- path,
52
- code,
53
- selectionReason: mode === "transform"
54
- ? "Selected: concrete evidence found and modification permitted."
55
- : "Selected: relevant focus file for semantic analysis.",
56
- });
57
- }
58
- else {
59
- candidates.push({
60
- path,
61
- code,
62
- selectionReason: hasEvidence
63
- ? "Candidate: evidence found but modification not permitted."
64
- : "Candidate: relevant but no concrete evidence found.",
65
- });
66
- }
67
- }
68
- // ---------------------------
69
- // 2️⃣ Rewrite selection rationale
70
- // ---------------------------
71
- if (selected.length > 0) {
72
- rationaleParts.push(`Selected ${selected.length} file(s) with concrete evidence permitting modification.`);
73
- }
74
- if (candidates.length > 0) {
75
- rationaleParts.push(`Identified ${candidates.length} additional relevant file(s) lacking sufficient evidence for modification.`);
76
- }
77
- if (selected.length === 0 && candidates.length > 0) {
78
- rationaleParts.push("No files selected for modification; evidence insufficient or ambiguous.");
36
+ workingFiles.push({
37
+ path,
38
+ code,
39
+ selectionReason: "Selected by preFileSearchCheckStep",
40
+ });
79
41
  }
80
- const rationale = rationaleParts.join(" ");
81
- // 3️⃣ Persist workingFiles (selected only)
42
+ // Deduplicate if anything preexists
43
+ const seen = new Set();
82
44
  context.workingFiles = [
83
45
  ...(context.workingFiles ?? []),
84
- ...selected,
46
+ ...workingFiles.filter(f => {
47
+ if (!f.path || seen.has(f.path))
48
+ return false;
49
+ seen.add(f.path);
50
+ return true;
51
+ }),
85
52
  ];
86
- const seen = new Set();
87
- context.workingFiles = context.workingFiles.filter(f => {
88
- if (!f.path)
89
- return false;
90
- if (seen.has(f.path))
91
- return false;
92
- seen.add(f.path);
93
- return true;
94
- });
95
53
  // ---------------------------
96
- // 4️⃣ Update canonical focus
54
+ // 2️⃣ Preserve focus but update rationale
97
55
  // ---------------------------
98
56
  context.analysis.focus = {
99
- selectedFiles: selected.map((f) => f.path),
100
- candidateFiles: candidates.map((f) => f.path),
101
- discardedFiles: discarded,
102
- rationale,
57
+ selectedFiles,
58
+ candidateFiles,
59
+ discardedFiles,
60
+ rationale: (context.analysis.focus.rationale ?? "") +
61
+ "\n[selectRelevantSources] Working files mirrored mechanically from previous selections.",
103
62
  };
104
63
  const output = {
105
64
  query,
106
65
  data: {
107
- workingFiles: context.workingFiles.map((f) => ({ path: f.path })),
108
- selectedFiles: context.analysis.focus.selectedFiles,
109
- candidateFiles: context.analysis.focus.candidateFiles,
110
- rationale,
66
+ workingFiles: context.workingFiles.map(f => ({ path: f.path })),
67
+ selectedFiles,
68
+ candidateFiles,
69
+ rationale: context.analysis.focus.rationale,
111
70
  },
112
71
  };
113
72
  logInputOutput("selectRelevantSources", "output", output.data);
@@ -75,7 +75,7 @@ export const writeFileStep = {
75
75
  }
76
76
  await fs.mkdir(path.dirname(filePath), { recursive: true });
77
77
  await fs.writeFile(filePath, artifact.content, "utf-8");
78
- console.log(chalk.green(`${MODULE_STEP_INDENTATION} Wrote file: ${filePath}`));
78
+ console.log(chalk.blue(`${MODULE_STEP_INDENTATION} Wrote file: ${filePath}`));
79
79
  writtenFiles.push(filePath);
80
80
  artifact.written = true;
81
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.166",
3
+ "version": "0.1.167",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"