scai 0.1.158 → 0.1.160

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.
@@ -1,12 +1,9 @@
1
1
  import { builtInModules } from "../pipeline/registry/moduleRegistry.js";
2
2
  import { logInputOutput } from "../utils/promptLogHelper.js";
3
- import { infoPlanGen } from "./infoPlanGenStep.js";
3
+ import { infoPlanGenStep } from "./infoPlanGenStep.js";
4
4
  import { understandIntentStep } from "./understandIntentStep.js";
5
5
  import { contextReviewStep } from "./contextReviewStep.js";
6
- import { planTargetFilesStep } from "./planTargetFilesStep.js";
7
- import { selectRelevantSourcesStep } from "./selectRelevantSourcesStep.js";
8
6
  import { transformPlanGenStep } from "./transformPlanGenStep.js";
9
- import { finalPlanGenStep } from "./finalPlanGenStep.js";
10
7
  import { getDbForRepo } from "../db/client.js";
11
8
  import { writeFileStep } from "./writeFileStep.js";
12
9
  import { resolveExecutionModeStep } from "./resolveExecutionModeStep.js";
@@ -14,8 +11,14 @@ import { fileCheckStep } from "./fileCheckStep.js";
14
11
  import { analysisPlanGenStep } from "./analysisPlanGenStep.js";
15
12
  import { readinessGateStep } from "./readinessGateStep.js";
16
13
  import { scopeClassificationStep } from "./scopeClassificationStep.js";
17
- import { routingDecisionStep } from "./routingDecisionStep.js";
18
14
  import { evidenceVerifierStep } from "./evidenceVerifierStep.js";
15
+ import { validateChangesStep } from './validateChangesStep.js';
16
+ import { reasonNextIterationStep } from './reasonNextIterationStep.js';
17
+ import { collaboratorStep } from './collaboratorStep.js';
18
+ import { integrateFeedbackStep } from './integrateFeedbackStep.js';
19
+ import { selectRelevantSourcesStep } from "./selectRelevantSourcesStep.js";
20
+ import { iterationFileSelector } from "./iterationFileSelector.js";
21
+ import { finalAnswerModule } from "../pipeline/modules/finalAnswerModule.js";
19
22
  /* ───────────────────────── registry ───────────────────────── */
20
23
  /**
21
24
  * Registry mapping action names to their corresponding Module implementations.
@@ -39,7 +42,7 @@ function resolveModuleForAction(action) {
39
42
  * The agent follows a multi-phase execution model:
40
43
  * 1. Boot: Determine intent and execution mode
41
44
  * 2. Precheck: File existence validation
42
- * 3. Scope & Routing: Determine where to act and what actions are allowed
45
+ * 3. Scope: Determine where to act and what actions are allowed
43
46
  * 4. Grounding & Readiness Loop: Acquire evidence and verify readiness
44
47
  * 5. Analysis: Perform in-depth analysis
45
48
  * 6. Transform: Generate and execute transformations
@@ -59,53 +62,175 @@ export class MainAgent {
59
62
  this.query = context.initContext?.userQuery ?? "";
60
63
  this.ui = ui;
61
64
  }
62
- /* ───────────── timers ───────────── */
63
- /**
64
- * Creates a timer function that measures execution time.
65
- *
66
- * @returns A function that returns the elapsed time in milliseconds.
67
- */
68
- startTimer() {
69
- const start = Date.now();
70
- return () => Date.now() - start;
71
- }
72
- /* ───────────── spinner helpers ───────────── */
73
- /**
74
- * Executes a function while pausing the UI spinner.
75
- *
76
- * @param fn - The function to execute while spinner is paused.
77
- */
78
- withSpinnerPaused(fn) {
79
- this.ui.pause(() => {
80
- // Ensure spinner line is fully gone
81
- process.stdout.write('\r\x1b[K');
82
- fn();
83
- });
65
+ /* ───────────── main run ───────────── */
66
+ async run() {
67
+ this.runCount = 0;
68
+ /* ===== BOOT ===== */
69
+ await understandIntentStep.run({ context: this.context });
70
+ await resolveExecutionModeStep.run(this.context);
71
+ this.taskId = bootTaskForRepo(this.context, getDbForRepo(), this.logLine.bind(this));
72
+ /* ===== PRECHECK ===== */
73
+ await fileCheckStep(this.context);
74
+ /* ===== SCOPE ===== */
75
+ await scopeClassificationStep.run(this.context);
76
+ /* ===== GROUNDING / INFO ACQUISITION ===== */
77
+ await this.runGrounding();
78
+ /* ===== WORK LOOP ===== */
79
+ const MAX_ITERATIONS = 5;
80
+ let loopCount = 0;
81
+ // Helper: determine next iteration action
82
+ const getNextIterationAction = () => {
83
+ const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
84
+ if (!nextAction)
85
+ return "complete"; // default safe exit
86
+ return nextAction;
87
+ };
88
+ while (loopCount < MAX_ITERATIONS) {
89
+ this.runCount++;
90
+ loopCount++;
91
+ await this.runWorkIteration();
92
+ const nextAction = getNextIterationAction();
93
+ this.logLine("LOOP", "nextAction", undefined, nextAction);
94
+ if (nextAction === "complete")
95
+ break;
96
+ if (nextAction === "request-feedback")
97
+ return;
98
+ }
99
+ /* ===== FINALIZE ===== */
100
+ // Directly generate the final answer for the user
101
+ await finalAnswerModule.run({ query: this.query, context: this.context });
102
+ // Persist task as usual
103
+ persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
84
104
  }
85
- /**
86
- * Logs a line with timing information.
87
- *
88
- * @param phase - The phase of execution.
89
- * @param step - The step being executed.
90
- * @param ms - The execution time in milliseconds.
91
- * @param desc - Optional description of the step.
92
- */
93
- logLine(phase, step, ms, desc) {
94
- this.withSpinnerPaused(() => {
95
- const suffix = desc ? ` — ${desc}` : "";
96
- const timing = typeof ms === "number" ? ` (${ms}ms)` : "";
97
- console.log(`[AGENT] ${phase} :: ${step}${suffix}${timing}`);
98
- });
105
+ /* ───────────── grounding / info acquisition loop ───────────── */
106
+ async runGrounding() {
107
+ let ready = false;
108
+ while (!ready && this.runCount < this.maxRuns) {
109
+ // ---------------- INFORMATION ACQUISITION ----------------
110
+ if (this.canExecutePhase("planning") &&
111
+ this.canExecuteScope("planning")) {
112
+ const t = this.startTimer();
113
+ // Generate info plan step(s) in context
114
+ await infoPlanGenStep.run(this.context);
115
+ const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
116
+ for (const step of infoPlan.steps) {
117
+ const stepIO = { query: this.query };
118
+ await this.executeStep(step, stepIO);
119
+ // Re-check any new files discovered
120
+ const t2 = this.startTimer();
121
+ await fileCheckStep(this.context);
122
+ this.logLine("PRECHECK", "postFileSearch", t2());
123
+ }
124
+ this.logLine("PLAN", "infoPlanGen", t());
125
+ }
126
+ // ---------------- DETERMINISTIC EVIDENCE VERIFICATION ----------------
127
+ const t1 = this.startTimer();
128
+ await evidenceVerifierStep.run({ query: this.query, context: this.context });
129
+ this.logLine("ANALYSIS", "collectAnalysisEvidence", t1(), undefined);
130
+ // Select grounded candidate files
131
+ const t2 = this.startTimer();
132
+ await selectRelevantSourcesStep.run({ query: this.query, context: this.context });
133
+ this.logLine("ANALYSIS", "selectRelevantSources", t2(), undefined);
134
+ // ---------------- READINESS GATE ----------------
135
+ const t3 = this.startTimer();
136
+ await readinessGateStep.run(this.context);
137
+ this.logLine("HASINFO", "readinessGate", t3(), undefined);
138
+ ready = this.context.analysis?.readiness?.decision === "ready";
139
+ if (!ready) {
140
+ this.runCount++;
141
+ // Previously resetInitContextForLoop removed; optionally clear temporary data
142
+ if (this.context.analysis) {
143
+ this.context.analysis.planSuggestion = undefined;
144
+ }
145
+ this.logLine("HASINFO", "readinessGate", undefined, "Not ready, looping back to information acquisition");
146
+ }
147
+ }
99
148
  }
100
- /**
101
- * Logs a message to the user output.
102
- *
103
- * @param message - The message to log.
104
- */
105
- userOutput(message) {
106
- this.withSpinnerPaused(() => {
107
- console.log(`[USER OUTPUT] ${message}`);
108
- });
149
+ /* ───────────── work iteration ───────────── */
150
+ async runWorkIteration() {
151
+ // ---------------- FILE SELECTION ----------------
152
+ const tSelect = this.startTimer();
153
+ await iterationFileSelector.run(this.context); // <-- sets context.analysis.currentTargetFile
154
+ this.logLine("LOOP", "iterationFileSelector", tSelect());
155
+ // ---------------- ANALYSIS ----------------
156
+ if (this.canExecutePhase("analysis") && this.canExecuteScope("analysis")) {
157
+ const tAnalysis = this.startTimer();
158
+ await analysisPlanGenStep.run(this.context);
159
+ this.logLine("PLAN", "analysisPlanGen", tAnalysis());
160
+ /* // 🔎 DEBUG BREAKPOINT — inspect context BEFORE analysis planning
161
+ debugContext(this.context, {
162
+ step: "pre-analysisPlanGen",
163
+ note: "After iterationFileSelector, before analysisPlanGen",
164
+ exit: true, // set to false if you want execution to continue
165
+ depth: 6
166
+ });
167
+ */
168
+ const analysisPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
169
+ for (const step of analysisPlan.steps) {
170
+ const tStep = this.startTimer();
171
+ await this.executeStep(step, { query: this.query });
172
+ this.logLine("STEP", step.action || "unnamedStep", tStep());
173
+ }
174
+ if (this.context.analysis) {
175
+ this.context.analysis.planSuggestion = undefined;
176
+ }
177
+ }
178
+ // ---------------- TRANSFORM ----------------
179
+ if (this.canExecutePhase("transform") && this.canExecuteScope("transform")) {
180
+ const tTransform = this.startTimer();
181
+ await transformPlanGenStep.run(this.context);
182
+ this.logLine("PLAN", "transformPlanGen", tTransform());
183
+ const transformPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
184
+ let stepCounter = 1; // 🔹 initialize step counter
185
+ for (const step of transformPlan.steps) {
186
+ const tStep = this.startTimer();
187
+ await this.executeStep(step, { query: this.query });
188
+ // 🔹 include step number in log
189
+ this.logLine("STEP", `#${stepCounter} - ${step.action || "unnamedStep"}`, tStep());
190
+ stepCounter++; // 🔹 increment counter
191
+ }
192
+ if (this.context.analysis) {
193
+ this.context.analysis.planSuggestion = undefined;
194
+ }
195
+ // ---------------- WRITE ----------------
196
+ if (this.canExecutePhase("write") && this.canExecuteScope("write")) {
197
+ const tWrite = this.startTimer();
198
+ await writeFileStep.run({ query: this.query, context: this.context });
199
+ this.logLine("WRITE", "writeFileStep", tWrite());
200
+ }
201
+ // ---------------- VALIDATION ----------------
202
+ const tValidate = this.startTimer();
203
+ await validateChangesStep.run(this.context);
204
+ this.logLine("VALIDATION", "validateChangesStep", tValidate());
205
+ }
206
+ // ---------------- REASONING ----------------
207
+ const tReason = this.startTimer();
208
+ await reasonNextIterationStep.run(this.context);
209
+ this.logLine("REASONING", "reasonNextIterationStep", tReason());
210
+ // ---------------- COLLABORATOR FEEDBACK ----------------
211
+ const tCollab = this.startTimer();
212
+ await collaboratorStep.run(this.context);
213
+ this.logLine("FEEDBACK", "collaboratorStep", tCollab());
214
+ // ---------------- INTEGRATE FEEDBACK ----------------
215
+ const tIntegrate = this.startTimer();
216
+ await integrateFeedbackStep.run(this.context);
217
+ this.logLine("FEEDBACK", "integrateFeedbackStep", tIntegrate());
218
+ // ---------------- REVIEW / RECOVERY ----------------
219
+ const tReview = this.startTimer();
220
+ const review = await contextReviewStep(this.context);
221
+ this.logLine("REVIEW", "contextReviewStep", tReview());
222
+ if (review.decision === "gatherData" && this.runCount < this.maxRuns) {
223
+ this.runCount++;
224
+ if (this.context.analysis) {
225
+ this.context.analysis.readiness = {
226
+ decision: 'not-ready',
227
+ confidence: 0,
228
+ rationale: 'Additional data gathering required.'
229
+ };
230
+ this.context.analysis.planSuggestion = undefined;
231
+ }
232
+ this.logLine("REVIEW", "contextReviewStep", undefined, "Not ready, looping back for additional data gathering");
233
+ }
109
234
  }
110
235
  /* ───────────── step executor ───────────── */
111
236
  /**
@@ -118,7 +243,9 @@ export class MainAgent {
118
243
  */
119
244
  async executeStep(step, input) {
120
245
  const stop = this.startTimer();
246
+ // Set current step in context
121
247
  this.context.currentStep = step;
248
+ // Resolve module for this action
122
249
  const mod = resolveModuleForAction(step.action);
123
250
  if (!mod) {
124
251
  this.logLine("EXECUTE", step.action, stop(), "skipped (missing module)");
@@ -126,15 +253,15 @@ export class MainAgent {
126
253
  }
127
254
  try {
128
255
  this.ui.update(`Running step: ${step.action}`);
129
- const output = await mod.run({
256
+ // Execute the module
257
+ await mod.run({
130
258
  query: step.description ?? input.query,
131
259
  content: input.data ?? input.content,
132
260
  context: this.context
133
261
  });
134
- if (!output)
135
- throw new Error(`Module "${mod.name}" returned empty output`);
136
262
  this.logLine("EXECUTE", step.action, stop());
137
- return { query: step.description ?? input.query, data: output.data };
263
+ // Return ModuleIO (can remain minimal since output is untyped)
264
+ return { query: step.description ?? input.query, data: {} };
138
265
  }
139
266
  catch (err) {
140
267
  this.logLine("EXECUTE", step.action, stop(), "failed");
@@ -205,211 +332,56 @@ export class MainAgent {
205
332
  this.logLine("EXEC", "canExecuteScope", undefined, `phase=${phase}, scope=${scope}, allowed=${allowed}`);
206
333
  return allowed;
207
334
  }
208
- /* ───────────── main run ───────────── */
335
+ /* ----------------------------------- */
336
+ /* ------------- helpers ------------- */
337
+ /* ----------------------------------- */
338
+ /* ───────────── timers ───────────── */
209
339
  /**
210
- * Executes the main agent run sequence.
340
+ * Creates a timer function that measures execution time.
211
341
  *
212
- * @returns A promise resolving to the final ModuleIO output or void.
342
+ * @returns A function that returns the elapsed time in milliseconds.
213
343
  */
214
- async run() {
215
- this.runCount++;
216
- const stopRun = this.startTimer();
217
- this.logLine("RUN", `start #${this.runCount}`);
218
- logInputOutput("GlobalContext (structured)", "input", this.context);
219
- /* ================= BOOT ================= */
220
- // AXIS 1: Capability — determine WHAT actions are allowed
221
- // executionMode: "explain" | "analyze" | "transform"
222
- // Controls: files may be written, code analyzed, or run text-only
223
- // This step does NOT determine WHERE to act or whether enough evidence exists
224
- {
225
- const t1 = this.startTimer();
226
- await understandIntentStep.run({ context: this.context }); // Classify user intent
227
- this.logLine("BOOT", "understandIntent", t1());
228
- const t2 = this.startTimer();
229
- await resolveExecutionModeStep.run(this.context); // Set executionMode
230
- this.logLine("BOOT", "resolveExecutionMode", t2(), `mode = ${this.context.executionControl?.mode}`);
231
- const db = getDbForRepo();
232
- this.taskId = bootTaskForRepo(this.context, db, this.logLine.bind(this)); // Persist task
233
- }
234
- /* ================= PRECHECK (INITIAL) ================= */
235
- // Quick file existence check (pre-grounding)
236
- // Does NOT infer relevance or scope beyond directly referenced files
237
- {
238
- const t = this.startTimer();
239
- await fileCheckStep(this.context);
240
- this.logLine("PRECHECK", "preFileSearch", t());
241
- }
242
- /* ───────────────────────── SCOPE & ROUTING GATE ───────────────────────── */
243
- // AXIS 2: Scope — determine WHERE the agent may act
244
- // scopeType: "none" | "single-file" | "multi-file" | "repo-wide"
245
- // Responsible for deciding which files, repos, or modules the agent should consider
246
- // Does NOT validate anchors (Axis 3) or decide allowed actions (Axis 1)
247
- // ----------------- SCOPE CLASSIFICATION -----------------
248
- // Responsibility:
249
- // - Determine problem coverage: none / single-file / multi-file / repo-wide
250
- // - Purely repo/file coverage decision
251
- // - Output: context.analysis.scopeType
252
- // Does NOT decide allowed actions, validate evidence, or affect executionMode
253
- {
254
- const t1 = this.startTimer();
255
- await scopeClassificationStep.run(this.context);
256
- this.logLine("SCOPE", "scopeClassification", t1());
257
- }
258
- // ----------------- ROUTING DECISION -----------------
259
- // Responsibility:
260
- // - Determine allowed actions within scope
261
- // * Can search be performed?
262
- // * Are early exits (explain) allowed?
263
- // - Output: context.analysis.routingDecision
264
- // Does NOT check anchors, determine readiness, or modify scope
265
- {
266
- const t2 = this.startTimer();
267
- await routingDecisionStep.run(this.context);
268
- this.logLine("SCOPE", "routingDecision", t2());
269
- }
270
- /* ================= GROUNDING & READINESS LOOP ================= */
271
- // AXIS 3: Certainty — do we have enough evidence to safely proceed?
272
- // confidence: "high" | "medium" | "low"
273
- // Controls whether we loop back to acquire more info
274
- // Does NOT change executionMode (Axis 1) or scopeType (Axis 2)
275
- let ready = false;
276
- while (!ready && this.runCount < this.maxRuns) {
277
- const routing = this.context.analysis?.routingDecision;
278
- // ---------------- INFORMATION ACQUISITION ----------------
279
- // Plan & execute info steps; may discover new files
280
- // Does NOT guarantee readiness — anchors still need verification
281
- if (this.canExecutePhase("planning") &&
282
- this.canExecuteScope("planning") &&
283
- routing?.allowInfoSteps !== false) {
284
- const t = this.startTimer();
285
- await infoPlanGen.run(this.context); // Generate info-gathering plan
286
- this.logLine("PLAN", "infoPlanGen", t());
287
- let stepIO = { query: this.query };
288
- const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
289
- for (const step of infoPlan.steps.filter(s => s.groups?.includes("info"))) {
290
- // Guard: skip fileSearch if routingDecision forbids repo search
291
- if (step.action === "fileSearch" && !routing?.allowSearch) {
292
- this.logLine("PLAN", "infoStepSkipped", undefined, `Step "${step.description}" skipped: file search not allowed by routingDecision`);
293
- continue;
294
- }
295
- stepIO = await this.executeStep(step, stepIO);
296
- }
297
- // Re-run precheck for newly discovered files
298
- if (infoPlan.steps.length > 0) {
299
- const t2 = this.startTimer();
300
- await fileCheckStep(this.context);
301
- this.logLine("PRECHECK", "postFileSearch", t2());
302
- }
303
- }
304
- // ---------------- DETERMINISTIC EVIDENCE VERIFICATION ----------------
305
- // Verify anchors exist (filenames, paths, literals)
306
- // Does NOT execute full analysis or transformations
307
- const t1 = this.startTimer();
308
- await evidenceVerifierStep.run({ query: this.query, context: this.context });
309
- this.logLine("ANALYSIS", "collectAnalysisEvidence", t1(), undefined);
310
- // Select grounded candidate files
311
- // Does NOT update executionMode or scope classification
312
- const t2 = this.startTimer();
313
- await selectRelevantSourcesStep.run({ query: this.query, context: this.context });
314
- this.logLine("ANALYSIS", "selectRelevantSources", t2(), undefined);
315
- // ---------------- READINESS GATE ----------------
316
- // Decide if we have enough info to proceed
317
- // Does NOT modify info plan or transform steps
318
- const t3 = this.startTimer();
319
- await readinessGateStep.run(this.context);
320
- this.logLine("HASINFO", "readinessGate", t3(), undefined);
321
- ready = this.context.analysis?.readiness?.decision === "ready";
322
- if (!ready) {
323
- this.runCount++;
324
- this.resetInitContextForLoop();
325
- this.logLine("HASINFO", "readinessGate", undefined, "Not ready, looping back to information acquisition");
326
- }
327
- }
328
- /* ================= ANALYSIS (DEEP) ================= */
329
- if (this.canExecutePhase("analysis") &&
330
- this.canExecuteScope("analysis")) {
331
- {
332
- const t = this.startTimer();
333
- await analysisPlanGenStep.run(this.context);
334
- this.logLine("PLAN", "analysisPlanGen", t());
335
- }
336
- let stepIO = { query: this.query };
337
- const analysisPlan = this.context.analysis?.planSuggestion?.plan?.steps ?? [];
338
- for (const step of analysisPlan.filter(s => s.groups?.includes("analysis"))) {
339
- stepIO = await this.executeStep(step, stepIO);
340
- }
341
- {
342
- const t = this.startTimer();
343
- await planTargetFilesStep.run({ query: this.query, context: this.context });
344
- this.logLine("ANALYSIS", "planTargetFiles", t());
345
- }
346
- const review = await contextReviewStep(this.context);
347
- if (review.decision === "has" && this.runCount < this.maxRuns) {
348
- this.runCount++;
349
- this.resetInitContextForLoop();
350
- return this.run();
351
- }
352
- }
353
- /* ================= EXPLAIN (EVIDENCE EXIT) ================= */
354
- // Early exit if mode=explain and routing allows it
355
- // Does NOT perform analysis, transform, or write steps
356
- if (this.canExecutePhase("explain") &&
357
- this.canExecuteScope("explain") &&
358
- this.context.analysis?.routingDecision?.decision === "has-info") {
359
- const explainMod = resolveModuleForAction("explain");
360
- if (!explainMod)
361
- throw new Error("Explain module not found");
362
- return await explainMod.run({
363
- query: this.query,
364
- context: this.context
365
- });
366
- }
367
- // Remaining phases (ANALYSIS, TRANSFORM, FINALIZE, PERSIST) execute after loop exits
368
- /* ================= TRANSFORM ================= */
369
- if (this.canExecutePhase("transform") &&
370
- this.canExecuteScope("transform")) {
371
- {
372
- const t = this.startTimer();
373
- await transformPlanGenStep.run(this.context);
374
- this.logLine("PLAN", "transformPlanGen", t());
375
- }
376
- let stepIO = { query: this.query };
377
- const transformSteps = this.context.analysis?.planSuggestion?.plan?.steps
378
- ?.filter(s => s.groups?.includes("transform")) ?? [];
379
- for (const step of transformSteps) {
380
- stepIO = await this.executeStep(step, stepIO);
381
- }
382
- if (this.canExecutePhase("write") &&
383
- this.canExecuteScope("write")) {
384
- const t = this.startTimer();
385
- await writeFileStep.run({ query: this.query, context: this.context });
386
- this.logLine("EXECUTE", "writeFile", t());
387
- }
388
- }
389
- /* ================= FINALIZE ================= */
390
- {
391
- const t = this.startTimer();
392
- await finalPlanGenStep.run(this.context);
393
- this.logLine("PLAN", "finalPlanGen", t());
394
- let stepIO = { query: this.query };
395
- const finalPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
396
- for (const step of finalPlan.steps.filter(s => s.groups?.includes("finalize"))) {
397
- stepIO = await this.executeStep(step, stepIO);
398
- }
399
- }
400
- const db = getDbForRepo();
401
- persistTaskData(this.context, this.taskId, db, this.logLine.bind(this));
402
- this.logLine("RUN", "complete", stopRun());
403
- return { query: this.query, data: {} };
344
+ startTimer() {
345
+ const start = Date.now();
346
+ return () => Date.now() - start;
404
347
  }
348
+ /* ───────────── spinner helpers ───────────── */
405
349
  /**
406
- * Resets the initial context for the next loop iteration.
350
+ * Executes a function while pausing the UI spinner.
407
351
  *
408
- * This is used to clear related files from the context before looping back.
352
+ * @param fn - The function to execute while spinner is paused.
409
353
  */
410
- resetInitContextForLoop() {
411
- if (this.context.initContext)
412
- this.context.initContext.relatedFiles = [];
354
+ withSpinnerPaused(fn) {
355
+ this.ui.pause(() => {
356
+ // Ensure spinner line is fully gone
357
+ process.stdout.write('\r\x1b[K');
358
+ fn();
359
+ });
360
+ }
361
+ /**
362
+ * Logs a line with timing information.
363
+ *
364
+ * @param phase - The phase of execution.
365
+ * @param step - The step being executed.
366
+ * @param ms - The execution time in milliseconds.
367
+ * @param desc - Optional description of the step.
368
+ */
369
+ logLine(phase, step, ms, desc) {
370
+ this.withSpinnerPaused(() => {
371
+ const suffix = desc ? ` — ${desc}` : "";
372
+ const timing = typeof ms === "number" ? ` (${ms}ms)` : "";
373
+ console.log(`[AGENT] ${phase} :: ${step}${suffix}${timing}`);
374
+ });
375
+ }
376
+ /**
377
+ * Logs a message to the user output.
378
+ *
379
+ * @param message - The message to log.
380
+ */
381
+ userOutput(message) {
382
+ this.withSpinnerPaused(() => {
383
+ console.log(`[USER OUTPUT] ${message}`);
384
+ });
413
385
  }
414
386
  }
415
387
  /* ───────────── FOLDER CAPSULES SUMMARY HELPER ───────────── */
@@ -440,7 +412,7 @@ export function logFolderCapsulesSummary(context) {
440
412
  body += summaryText;
441
413
  if (key) {
442
414
  const keyHint = `Key: ${key.path.split("/").pop()} — ${truncate(key.reason)}`;
443
- body += summaryText ? ` [${keyHint}]` : keyHint;
415
+ body += summaryText ? `[${keyHint}]` : keyHint;
444
416
  }
445
417
  }
446
418
  return body ? `${header}\n${body}` : header;
@@ -480,11 +452,6 @@ export function persistTaskData(context, taskId, db, logLine) {
480
452
  if (context.analysis?.focus?.candidateFiles?.length) {
481
453
  fieldsToUpdate.missing_files_json = JSON.stringify(context.analysis.focus.candidateFiles);
482
454
  }
483
- // ❌ focus_rationale intentionally removed
484
- // Persist routing decision (meta / audit)
485
- if (context.analysis?.routingDecision) {
486
- fieldsToUpdate.routing_decision_json = JSON.stringify(context.analysis.routingDecision);
487
- }
488
455
  // ✅ Persist final answer → tasks.summary
489
456
  if (context.analysis?.finalAnswer) {
490
457
  fieldsToUpdate.summary = context.analysis.finalAnswer;