scai 0.1.177 → 0.1.178

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.
@@ -61,6 +61,13 @@ export class MainAgent {
61
61
  */
62
62
  constructor(context, ui) {
63
63
  this.runCount = 0;
64
+ this.verboseAgentLogs = process.env.SCAI_VERBOSE_AGENT_LOGS === "1";
65
+ this.hasPrintedHeader = false;
66
+ this.activity = {
67
+ searches: [],
68
+ lists: [],
69
+ reads: [],
70
+ };
64
71
  this.context = context;
65
72
  this.query = context.initContext?.userQuery ?? "";
66
73
  this.ui = ui;
@@ -69,16 +76,23 @@ export class MainAgent {
69
76
  async run() {
70
77
  try {
71
78
  this.runCount = 0;
79
+ this.printHeader("Boot & Scope");
72
80
  await this.runBoot();
73
81
  await this.runScope();
82
+ this.printHeader("Search");
74
83
  await this.runSearch();
84
+ this.printHeader("Verify");
75
85
  await this.runVerify();
86
+ this.printHeader("Research");
76
87
  await this.runResearch();
77
88
  const canProceedToExecution = this.isResearchGateSatisfied();
78
89
  if (canProceedToExecution) {
90
+ this.printHeader("Plan");
79
91
  await this.runPlan();
92
+ this.printHeader("Execute");
80
93
  await this.runWorkLoop();
81
94
  }
95
+ this.printHeader("Finalize");
82
96
  await this.runFinalize();
83
97
  }
84
98
  finally {
@@ -101,7 +115,7 @@ export class MainAgent {
101
115
  taskSteps: [],
102
116
  });
103
117
  this.context.task.id = this.taskId;
104
- this.logLine("TASK", "Boot complete", undefined, `taskId=${this.taskId}`, { highlight: true });
118
+ this.userOutput(`Boot complete (taskId=${this.taskId}).`);
105
119
  }
106
120
  /* ───────────── scope ───────────── */
107
121
  async runScope() {
@@ -109,10 +123,11 @@ export class MainAgent {
109
123
  await resolveExecutionModeStep.run(this.context);
110
124
  await routingDecisionStep.run(this.context);
111
125
  const routing = this.context.analysis?.routingDecision;
126
+ const scope = this.context.analysis?.scopeType ?? "unknown";
112
127
  if (routing) {
113
- this.logLine("TASK", "Routing decision", undefined, `${routing.decision} | search=${routing.allowSearch} | research=${routing.allowResearch} | transform=${routing.allowTransform} | scopeLocked=${routing.scopeLocked}`);
128
+ this.userOutput(`Routing: ${routing.decision} (scope=${scope}, search=${routing.allowSearch}, research=${routing.allowResearch}, transform=${routing.allowTransform}, scopeLocked=${routing.scopeLocked}).`);
114
129
  }
115
- this.logLine("TASK", "Scope classification complete");
130
+ this.userOutput("Scope classification complete.");
116
131
  }
117
132
  /* ───────────── search ───────────── */
118
133
  /**
@@ -121,6 +136,7 @@ export class MainAgent {
121
136
  */
122
137
  async runSearch() {
123
138
  const { rawUserQuery, retrievalQuery } = this.resolveInitialRetrievalQueries();
139
+ this.userOutput("I’ll start by identifying likely files from semantic search before analyzing content.");
124
140
  const t = this.startTimer();
125
141
  try {
126
142
  const results = await this.fetchInitialRetrievalResults(retrievalQuery);
@@ -129,7 +145,12 @@ export class MainAgent {
129
145
  const mergedRelatedCount = this.mergeSeededInitialContext(rawUserQuery, seededContext);
130
146
  const prefilter = this.applyDeterministicPreGroundingPrefilter(retrievalQuery);
131
147
  const repoDefaults = this.injectWellKnownRepoFiles(prefilter.after);
148
+ this.recordSearch(retrievalQuery, path.basename(process.cwd()));
149
+ this.recordListEntries(Array.from(new Set(results.map(result => path.dirname(this.formatPathForLog(result.path)))))
150
+ .slice(0, 2)
151
+ .map(dir => dir === "." ? process.cwd() : dir));
132
152
  this.logLine("ANALYSIS", "initialRetrieval", t(), `${results.length} result(s), ${mergedRelatedCount} candidate file(s), prefilter ${prefilter.before} -> ${prefilter.after}, defaults +${repoDefaults.added} (${repoDefaults.reason})`);
153
+ this.flushActivity();
133
154
  }
134
155
  catch (err) {
135
156
  this.logLine("ANALYSIS", "initialRetrieval", t(), `failed: ${String(err)}`);
@@ -151,24 +172,52 @@ export class MainAgent {
151
172
  this.pruneMissingVerifyPaths();
152
173
  this.logLine("ANALYSIS", "groundingWave", undefined, `wave ${groundingWave}/${maxGroundingWaves}`);
153
174
  const beforeFocus = this.captureVerifyFocusSnapshot();
175
+ const beforeWorking = new Set((this.context.workingFiles ?? []).map(file => file.path));
176
+ const beforeSelected = new Set(this.context.analysis?.focus?.selectedFiles ?? []);
177
+ const beforeCandidate = new Set(this.context.analysis?.focus?.candidateFiles ?? []);
178
+ const structuralBefore = new Set(Object.entries(this.context.analysis?.fileAnalysis ?? {})
179
+ .filter(([_, analysis]) => !!analysis?.structural)
180
+ .map(([filePath]) => filePath));
154
181
  // ---------------- EVIDENCE PIPELINE ----------------
155
182
  // -------- STRUCTURAL PRELOAD --------
156
183
  const t0 = this.startTimer();
157
184
  await structuralPreloadStep.run({ query: this.query, context: this.context });
185
+ const structuralAfter = Object.entries(this.context.analysis?.fileAnalysis ?? {})
186
+ .filter(([_, analysis]) => !!analysis?.structural)
187
+ .map(([filePath]) => filePath);
188
+ const newStructural = structuralAfter.filter(filePath => !structuralBefore.has(filePath));
158
189
  this.logLine("ANALYSIS", "structuralPreload", t0());
159
190
  const t1 = this.startTimer();
160
191
  await evidenceVerifierStep.run({ query: this.query, context: this.context });
192
+ const verifyEntries = Object.entries(this.context.analysis?.verify?.byFile ?? {});
161
193
  this.logLine("ANALYSIS", "collectAnalysisEvidence", t1());
162
194
  const t2 = this.startTimer();
163
195
  await fileCheckStep(this.context);
196
+ const selectedAfterCheck = this.context.analysis?.focus?.selectedFiles ?? [];
197
+ const candidateAfterCheck = this.context.analysis?.focus?.candidateFiles ?? [];
198
+ const addedSelected = selectedAfterCheck.filter(filePath => !beforeSelected.has(filePath));
199
+ const addedCandidate = candidateAfterCheck.filter(filePath => !beforeCandidate.has(filePath));
164
200
  this.logLine("ANALYSIS", "fileCheckStep", t2());
165
201
  const t3 = this.startTimer();
166
202
  await selectRelevantSourcesStep.run({ query: this.query, context: this.context });
203
+ const workingAfter = this.context.workingFiles ?? [];
204
+ const newWorking = workingAfter
205
+ .map(file => file.path)
206
+ .filter(filePath => !beforeWorking.has(filePath));
167
207
  this.logLine("ANALYSIS", "selectRelevantSources", t3());
168
208
  // ---------------- READINESS GATE ----------------
169
209
  const t4 = this.startTimer();
170
210
  await readinessGateStep.run(this.context);
171
- this.logLine("ANALYSIS", "readinessGate", t4());
211
+ const readiness = this.context.analysis?.readiness;
212
+ const selectedCount = this.context.analysis?.focus?.selectedFiles?.length ?? 0;
213
+ this.logLine("ANALYSIS", "readinessGate", t4(), `decision=${readiness?.decision ?? "unknown"}, confidence=${(readiness?.confidence ?? 0).toFixed(2)}, selected=${selectedCount}`);
214
+ this.userOutput("I mapped the verify wave. Next I’ll analyze selected files and refine scope if needed.");
215
+ this.recordReadEntries(newStructural);
216
+ this.recordReadEntries(verifyEntries.map(([filePath]) => filePath));
217
+ this.recordReadEntries(addedSelected);
218
+ this.recordListEntries(addedCandidate);
219
+ this.recordReadEntries(newWorking);
220
+ this.flushActivity();
172
221
  ready = this.context.analysis?.readiness?.decision === "ready";
173
222
  if (ready) {
174
223
  break;
@@ -389,6 +438,8 @@ export class MainAgent {
389
438
  seeded,
390
439
  });
391
440
  this.logLine("PLAN", "taskStepSeed", undefined, `${seededCount} execution step(s) planned`);
441
+ this.recordReadEntries(seeded.map(step => step.filePath));
442
+ this.flushActivity();
392
443
  }
393
444
  /**
394
445
  * Sets minimum verify confidence before a file can be plan-seeded from verify-only signal.
@@ -973,7 +1024,8 @@ export class MainAgent {
973
1024
  return { query: input.query, content: input.content, data: { skipped: true } };
974
1025
  }
975
1026
  try {
976
- this.ui.update(`Running step: ${step.action}`);
1027
+ const stepTarget = step.targetFile ? this.formatPathForLog(step.targetFile) : undefined;
1028
+ this.ui.update(stepTarget ? `Running ${step.action} on ${stepTarget}` : `Running ${step.action}`);
977
1029
  const output = await mod.run({ query: step.description ?? input.query, content: input.data ?? input.content, context: this.context });
978
1030
  const errors = Array.isArray(output.data?.errors)
979
1031
  ? output.data.errors.filter((e) => typeof e === "string" && e.trim().length > 0)
@@ -1182,22 +1234,31 @@ export class MainAgent {
1182
1234
  let removedSelected = 0;
1183
1235
  let removedCandidate = 0;
1184
1236
  if (init?.relatedFiles?.length) {
1185
- const before = init.relatedFiles.length;
1186
- init.relatedFiles = init.relatedFiles.filter(existsOrResearch);
1187
- removedRelated = before - init.relatedFiles.length;
1237
+ const existing = init.relatedFiles;
1238
+ init.relatedFiles = existing.filter(filePath => {
1239
+ const keep = existsOrResearch(filePath);
1240
+ return keep;
1241
+ });
1242
+ removedRelated = existing.length - init.relatedFiles.length;
1188
1243
  if (removedRelated > 0 && init.relatedFileScores) {
1189
1244
  init.relatedFileScores = Object.fromEntries(Object.entries(init.relatedFileScores).filter(([filePath]) => init.relatedFiles?.includes(filePath)));
1190
1245
  }
1191
1246
  }
1192
1247
  if (focus?.selectedFiles?.length) {
1193
- const before = focus.selectedFiles.length;
1194
- focus.selectedFiles = focus.selectedFiles.filter(existsOrResearch);
1195
- removedSelected = before - focus.selectedFiles.length;
1248
+ const existing = focus.selectedFiles;
1249
+ focus.selectedFiles = existing.filter(filePath => {
1250
+ const keep = existsOrResearch(filePath);
1251
+ return keep;
1252
+ });
1253
+ removedSelected = existing.length - focus.selectedFiles.length;
1196
1254
  }
1197
1255
  if (focus?.candidateFiles?.length) {
1198
- const before = focus.candidateFiles.length;
1199
- focus.candidateFiles = focus.candidateFiles.filter(existsOrResearch);
1200
- removedCandidate = before - focus.candidateFiles.length;
1256
+ const existing = focus.candidateFiles;
1257
+ focus.candidateFiles = existing.filter(filePath => {
1258
+ const keep = existsOrResearch(filePath);
1259
+ return keep;
1260
+ });
1261
+ removedCandidate = existing.length - focus.candidateFiles.length;
1201
1262
  }
1202
1263
  if (removedRelated + removedSelected + removedCandidate > 0) {
1203
1264
  this.logLine("ANALYSIS", "verifyPruneMissing", undefined, `removed related=${removedRelated}, selected=${removedSelected}, candidate=${removedCandidate}`);
@@ -1326,7 +1387,7 @@ export class MainAgent {
1326
1387
  this.logLine("TASK", "Route updated from step reasoning", undefined, "expand-scope requested; re-enabled search expansion");
1327
1388
  }
1328
1389
  startTaskStep(taskStep, stepCount) {
1329
- this.ensureWorkingFilesLoaded([taskStep.filePath], "Current task step");
1390
+ const hydration = this.ensureWorkingFilesLoaded([taskStep.filePath], "Current task step");
1330
1391
  const db = getDbForRepo();
1331
1392
  this.ensureTaskIdentityForPersistence(db);
1332
1393
  this.context.task.currentStep = taskStep;
@@ -1335,7 +1396,10 @@ export class MainAgent {
1335
1396
  taskStep.status = "pending";
1336
1397
  persistTaskStepInsert(taskStep, db);
1337
1398
  const displayPath = this.formatTaskStepDisplayPath(taskStep.filePath);
1338
- this.logLine("NEW STEP", `Processing taskStep ${stepCount}`, undefined, displayPath, { highlight: true });
1399
+ this.printHeader(`Step ${stepCount}`);
1400
+ this.userOutput(`Now processing ${this.formatPathForLog(displayPath)}.`);
1401
+ this.recordReadEntries(hydration.loadedFromDisk);
1402
+ this.flushActivity();
1339
1403
  taskStep.startTime = Date.now();
1340
1404
  persistTaskStepStart(taskStep, db);
1341
1405
  }
@@ -1386,6 +1450,31 @@ export class MainAgent {
1386
1450
  this.ui.pause(() => { process.stdout.write('\r\x1b[K'); fn(); });
1387
1451
  }
1388
1452
  logLine(phase, step, ms, desc, options) {
1453
+ if (!this.verboseAgentLogs) {
1454
+ // Keep only key transitions when verbose logging is disabled.
1455
+ if (phase === "STEP-DONE" && desc) {
1456
+ this.userOutput(`Completed ${this.formatPathForLog(desc)}.`);
1457
+ }
1458
+ else if (phase === "TASK" && step.includes("Execution paused")) {
1459
+ this.userOutput("Execution paused; waiting for user clarification.");
1460
+ }
1461
+ else if (phase === "TASK" && step.includes("All selected files processed")) {
1462
+ this.userOutput("All selected files processed.");
1463
+ }
1464
+ else if (phase === "TASK" && step.includes("No eligible taskStep found")) {
1465
+ this.userOutput("No eligible task step found.");
1466
+ }
1467
+ else if (phase === "TASK" && step.includes("Finalize complete")) {
1468
+ this.userOutput("Finalize complete.");
1469
+ }
1470
+ else if (phase === "RESEARCH" && step === "taskStepSeed" && desc?.includes("skipped")) {
1471
+ this.userOutput("Research skipped for this route.");
1472
+ }
1473
+ else if (phase === "RESEARCH" && step === "taskStepSeed" && desc) {
1474
+ this.userOutput(desc);
1475
+ }
1476
+ return;
1477
+ }
1389
1478
  this.withSpinnerPaused(() => {
1390
1479
  const suffix = desc ? ` — ${desc}` : "";
1391
1480
  const timing = typeof ms === "number" ? ` (${ms}ms)` : "";
@@ -1400,12 +1489,21 @@ export class MainAgent {
1400
1489
  // Print a blank line **before** NEW STEP only
1401
1490
  if (phase === "NEW STEP")
1402
1491
  console.log();
1492
+ process.stdout.write("\r\x1b[K");
1403
1493
  console.log(line);
1404
1494
  });
1405
1495
  }
1406
1496
  userOutput(message) {
1407
1497
  this.withSpinnerPaused(() => {
1408
- console.log(`[USER OUTPUT] ${message}`);
1498
+ console.log(message);
1499
+ });
1500
+ }
1501
+ printHeader(title) {
1502
+ this.withSpinnerPaused(() => {
1503
+ if (this.hasPrintedHeader)
1504
+ console.log("");
1505
+ console.log(chalk.bold(`== ${title} ==`));
1506
+ this.hasPrintedHeader = true;
1409
1507
  });
1410
1508
  }
1411
1509
  /**
@@ -1416,18 +1514,25 @@ export class MainAgent {
1416
1514
  var _a;
1417
1515
  (_a = this.context).workingFiles || (_a.workingFiles = []);
1418
1516
  const indexByPath = new Map(this.context.workingFiles.map(file => [file.path, file]));
1517
+ const loadedFromDisk = [];
1518
+ const alreadyLoaded = [];
1519
+ const skippedMissing = [];
1419
1520
  for (const filePath of paths) {
1420
1521
  if (!filePath || filePath.startsWith("__research__/"))
1421
1522
  continue;
1422
- if (!fs.existsSync(filePath))
1523
+ if (!fs.existsSync(filePath)) {
1524
+ skippedMissing.push(filePath);
1423
1525
  continue;
1526
+ }
1424
1527
  const existing = indexByPath.get(filePath);
1425
1528
  if (existing && typeof existing.code === "string" && existing.code.length > 0) {
1529
+ alreadyLoaded.push(filePath);
1426
1530
  continue;
1427
1531
  }
1428
1532
  let code;
1429
1533
  try {
1430
1534
  code = fs.readFileSync(filePath, "utf-8");
1535
+ loadedFromDisk.push(filePath);
1431
1536
  }
1432
1537
  catch {
1433
1538
  code = undefined;
@@ -1442,6 +1547,77 @@ export class MainAgent {
1442
1547
  indexByPath.set(filePath, capsule);
1443
1548
  }
1444
1549
  }
1550
+ return { loadedFromDisk, alreadyLoaded, skippedMissing };
1551
+ }
1552
+ formatPathForLog(filePath) {
1553
+ if (!filePath)
1554
+ return "(none)";
1555
+ if (filePath.startsWith("__research__/")) {
1556
+ return filePath.replace("__research__/", "research/");
1557
+ }
1558
+ const normalized = path.normalize(filePath);
1559
+ const relative = path.relative(process.cwd(), normalized);
1560
+ if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
1561
+ return relative || path.basename(normalized);
1562
+ }
1563
+ return normalized;
1564
+ }
1565
+ describeFileList(paths, maxItems = 4) {
1566
+ const unique = Array.from(new Set(paths.filter(Boolean)));
1567
+ if (unique.length === 0)
1568
+ return "none";
1569
+ const shown = unique.slice(0, maxItems).map(filePath => this.formatPathForLog(filePath));
1570
+ const extra = unique.length - shown.length;
1571
+ if (extra > 0)
1572
+ shown.push(`+${extra} more`);
1573
+ return shown.join(", ");
1574
+ }
1575
+ recordSearch(query, scope) {
1576
+ const normalizedScope = scope || "repo";
1577
+ this.activity.searches.push(`Searched for ${query} in ${normalizedScope}`);
1578
+ }
1579
+ recordListEntries(entries) {
1580
+ for (const entry of entries) {
1581
+ const normalized = entry.replace(/\\/g, "/");
1582
+ const asDir = normalized.endsWith("/") || path.extname(normalized) === ""
1583
+ ? normalized
1584
+ : path.dirname(normalized);
1585
+ const display = asDir === "." ? "repo root" : this.formatPathForLog(asDir);
1586
+ this.activity.lists.push(`Listed files in ${display}`);
1587
+ }
1588
+ }
1589
+ recordReadEntries(paths) {
1590
+ for (const filePath of paths) {
1591
+ if (!filePath || filePath.startsWith("__research__/"))
1592
+ continue;
1593
+ this.activity.reads.push(`Read ${this.formatPathForLog(filePath)}`);
1594
+ }
1595
+ }
1596
+ flushActivity() {
1597
+ const searches = Array.from(new Set(this.activity.searches));
1598
+ const lists = Array.from(new Set(this.activity.lists));
1599
+ const reads = Array.from(new Set(this.activity.reads));
1600
+ if (searches.length === 0 && lists.length === 0 && reads.length === 0) {
1601
+ return;
1602
+ }
1603
+ const parts = [];
1604
+ if (searches.length > 0) {
1605
+ parts.push(`${searches.length} search${searches.length === 1 ? "" : "es"}`);
1606
+ }
1607
+ if (lists.length > 0) {
1608
+ parts.push(`${lists.length} director${lists.length === 1 ? "y" : "ies"}`);
1609
+ }
1610
+ if (reads.length > 0) {
1611
+ parts.push(`${reads.length} file${reads.length === 1 ? "" : "s"}`);
1612
+ }
1613
+ this.userOutput(`Explored ${parts.join(", ")}`);
1614
+ for (const line of searches.slice(0, 2))
1615
+ this.userOutput(line);
1616
+ for (const line of lists.slice(0, 3))
1617
+ this.userOutput(line);
1618
+ for (const line of reads.slice(0, 6))
1619
+ this.userOutput(line);
1620
+ this.activity = { searches: [], lists: [], reads: [] };
1445
1621
  }
1446
1622
  /**
1447
1623
  * Ensures the current task id exists in the active DB before step persistence.
@@ -103,10 +103,14 @@ Return a valid JSON object in this format:
103
103
  });
104
104
  context.analysis.planSuggestion = { plan: { steps: finalSteps } };
105
105
  logInputOutput("analysisPlanGen", "output", finalSteps);
106
- console.log("🧠 [analysisPlanGenStep] EXIT (success)");
106
+ if (process.env.SCAI_VERBOSE_AGENT_LOGS === "1") {
107
+ console.log("🧠 [analysisPlanGenStep] EXIT (success)");
108
+ }
107
109
  }
108
110
  catch (err) {
109
- console.warn("⚠️ [analysisPlanGenStep] FAILED to generate analysis plan:", err);
111
+ if (process.env.SCAI_VERBOSE_AGENT_LOGS === "1") {
112
+ console.warn("⚠️ [analysisPlanGenStep] FAILED to generate analysis plan:", err);
113
+ }
110
114
  context.analysis.planSuggestion = { plan: { steps: [] } };
111
115
  logInputOutput("analysisPlanGen", "output", { steps: [] });
112
116
  }
@@ -176,8 +176,8 @@ function pickRandom(items) {
176
176
  return items[Math.floor(Math.random() * items.length)];
177
177
  }
178
178
  function logHighContrastQuery(prefix, query) {
179
- // Bright white + bold for dark terminal readability.
180
- console.log(chalk.bold.white(`${prefix} ${query}`));
179
+ // Neutral bold so it is readable on both light and dark terminal themes.
180
+ console.log(chalk.bold(`${prefix} ${query}`));
181
181
  }
182
182
  function countOccurrences(haystack, needle) {
183
183
  if (!needle)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.177",
3
+ "version": "0.1.178",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"