scai 0.1.161 → 0.1.163

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.
@@ -155,9 +155,6 @@ export class MainAgent {
155
155
  async runWorkLoop() {
156
156
  const MAX_TASK_STEPS = 5;
157
157
  let stepCount = 0;
158
- // ---------------------------
159
- // 1️⃣ Task-level reasoning: decide remaining files / next action
160
- // ---------------------------
161
158
  while (stepCount < MAX_TASK_STEPS) {
162
159
  await reasonNextTaskStep.run(this.context);
163
160
  const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
@@ -165,32 +162,43 @@ export class MainAgent {
165
162
  this.logLine("TASK", "All selected files processed — task complete");
166
163
  return;
167
164
  }
168
- // ---------------------------
169
- // 2️⃣ Select next file to process
170
- // ---------------------------
171
165
  const taskStep = await iterationFileSelector.run(this.context);
172
166
  if (!taskStep) {
173
167
  this.logLine("TASK", "No eligible taskStep found — task complete");
174
168
  return;
175
169
  }
176
- // Only assign if taskStep is not null and context.task is defined
177
- if (taskStep && this.context.task) {
170
+ // Set current step in context
171
+ if (this.context.task) {
178
172
  this.context.task.currentStep = taskStep;
179
173
  }
180
174
  stepCount++;
175
+ taskStep.taskId = this.taskId;
176
+ taskStep.stepIndex = stepCount;
177
+ taskStep.status = "pending";
178
+ // ⬇️ Persist newly created step
179
+ persistTaskStepInsert(taskStep, getDbForRepo());
181
180
  logInputOutput("Step to process", "input", taskStep);
182
181
  this.logLine("TASK", `Processing taskStep ${stepCount}/${MAX_TASK_STEPS}`, undefined, taskStep.filePath);
183
182
  // ---------------------------
184
- // 3️⃣ Step-level iterations: reason only about this file
183
+ // Start the step
184
+ // ---------------------------
185
+ taskStep.startTime = Date.now();
186
+ persistTaskStepStart(taskStep, getDbForRepo());
185
187
  // ---------------------------
186
- await this.runStepIterations(taskStep);
187
- // Mark step complete if iterationReasoning says complete for this file
188
- const stepAction = this.context.analysis?.iterationReasoning?.nextAction;
188
+ // Step-level iterations: reason only about this file
189
+ // ---------------------------
190
+ const stepAction = await this.runStepIterations(taskStep);
189
191
  if (stepAction === "complete") {
190
192
  taskStep.status = "completed";
193
+ taskStep.endTime = Date.now();
194
+ persistTaskStepCompletion(taskStep, getDbForRepo());
195
+ }
196
+ else {
197
+ // optionally mark as pending or redo-step
198
+ taskStep.status = "pending";
199
+ persistTaskStepCompletion(taskStep, getDbForRepo());
191
200
  }
192
201
  }
193
- // Safety exit
194
202
  this.logLine("TASK", "Max task step limit reached — stopping work loop");
195
203
  }
196
204
  /* ───────────── step iterations ───────────── */
@@ -208,8 +216,7 @@ export class MainAgent {
208
216
  }
209
217
  return nextAction;
210
218
  };
211
- // Here we loop through iterations for a single task step (file),
212
- // allowing the agent to reason about completeness, redo work, or request feedback.
219
+ // Loop through iterations for a single task step (file)
213
220
  while (loopCount < MAX_ITERATIONS) {
214
221
  this.runCount++;
215
222
  loopCount++;
@@ -221,11 +228,12 @@ export class MainAgent {
221
228
  const nextAction = getNextIterationAction();
222
229
  this.logLine("STEP-LOOP", "nextAction", undefined, nextAction);
223
230
  if (nextAction === "complete")
224
- return;
231
+ return "complete";
225
232
  if (nextAction === "request-feedback")
226
- return;
233
+ return "request-feedback";
227
234
  // else: continue
228
235
  }
236
+ return "continue";
229
237
  }
230
238
  /* ───────────── work iteration ───────────── */
231
239
  async runWorkIteration(taskStep) {
@@ -312,7 +320,6 @@ export class MainAgent {
312
320
  content: input.data ?? input.content,
313
321
  context: this.context
314
322
  });
315
- this.logLine("EXECUTE", step.action, stop());
316
323
  // Return ModuleIO (can remain minimal since output is untyped)
317
324
  return { query: step.description ?? input.query, data: {} };
318
325
  }
@@ -344,7 +351,6 @@ export class MainAgent {
344
351
  allowed = constraints?.allowFileWrites ?? false;
345
352
  break;
346
353
  }
347
- this.logLine("EXEC", "canExecutePhase", undefined, `phase=${phase}, docsOnly=${docsOnly}, allowed=${allowed}`);
348
354
  return allowed;
349
355
  }
350
356
  /* ───────────── scope gates ───────────── */
@@ -368,7 +374,6 @@ export class MainAgent {
368
374
  allowed = true;
369
375
  break;
370
376
  }
371
- this.logLine("EXEC", "canExecuteScope", undefined, `phase=${phase}, scope=${scope}, allowed=${allowed}`);
372
377
  return allowed;
373
378
  }
374
379
  /* ----------------------------------- */
@@ -544,3 +549,58 @@ export function bootTaskForRepo(context, db, logLine) {
544
549
  logLine("TASK", `created task id = ${taskId}`);
545
550
  return taskId;
546
551
  }
552
+ export function persistTaskStepInsert(taskStep, db) {
553
+ if (taskStep.id)
554
+ return;
555
+ const nowIso = new Date().toISOString();
556
+ const result = db.prepare(`
557
+ INSERT INTO task_steps (
558
+ task_id,
559
+ file_path,
560
+ action,
561
+ status,
562
+ step_index,
563
+ start_time,
564
+ result_json,
565
+ notes,
566
+ created_at,
567
+ updated_at
568
+ )
569
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
570
+ `).run(taskStep.taskId, taskStep.filePath, taskStep.action ?? null, taskStep.status, taskStep.stepIndex ?? null, taskStep.startTime ?? null, taskStep.result != null ? JSON.stringify(taskStep.result) : null, taskStep.notes ?? null, nowIso, nowIso);
571
+ taskStep.id = result.lastInsertRowid;
572
+ }
573
+ export function persistTaskStepStart(taskStep, db) {
574
+ if (!taskStep.id)
575
+ return;
576
+ if (!taskStep.startTime) {
577
+ taskStep.startTime = Date.now();
578
+ }
579
+ db.prepare(`
580
+ UPDATE task_steps
581
+ SET
582
+ start_time = ?,
583
+ action = ?,
584
+ notes = ?,
585
+ updated_at = ?
586
+ WHERE id = ?
587
+ `).run(taskStep.startTime, taskStep.action ?? null, taskStep.notes ?? null, new Date().toISOString(), taskStep.id);
588
+ }
589
+ export function persistTaskStepCompletion(taskStep, db) {
590
+ if (!taskStep.id)
591
+ return;
592
+ if (!taskStep.endTime) {
593
+ taskStep.endTime = Date.now();
594
+ }
595
+ db.prepare(`
596
+ UPDATE task_steps
597
+ SET
598
+ status = ?,
599
+ end_time = ?,
600
+ action = ?,
601
+ result_json = ?,
602
+ notes = ?,
603
+ updated_at = ?
604
+ WHERE id = ?
605
+ `).run(taskStep.status, taskStep.endTime, taskStep.action ?? null, taskStep.result != null ? JSON.stringify(taskStep.result) : null, taskStep.notes ?? null, new Date().toISOString(), taskStep.id);
606
+ }
@@ -24,8 +24,9 @@ export const selectRelevantSourcesStep = {
24
24
  const discarded = [];
25
25
  const rationaleParts = [];
26
26
  // ---------------------------
27
- // 1️⃣ Classify files by evidence
27
+ // 1️⃣ Classify files by relevance, evidence, and mode
28
28
  // ---------------------------
29
+ const mode = context.executionControl?.mode ?? "transform";
29
30
  for (const [path, analysis] of Object.entries(fileAnalysis)) {
30
31
  const isRelevant = analysis.action?.isRelevant === true;
31
32
  if (!isRelevant) {
@@ -41,11 +42,17 @@ export const selectRelevantSourcesStep = {
41
42
  catch {
42
43
  code = undefined;
43
44
  }
44
- if (hasEvidence && canModify) {
45
+ // 🔑 Mode-aware selection rule
46
+ const isSelected = mode === "transform"
47
+ ? hasEvidence && canModify
48
+ : true; // analyze: relevance is sufficient
49
+ if (isSelected) {
45
50
  selected.push({
46
51
  path,
47
52
  code,
48
- selectionReason: "Selected: concrete evidence found and modification permitted.",
53
+ selectionReason: mode === "transform"
54
+ ? "Selected: concrete evidence found and modification permitted."
55
+ : "Selected: relevant focus file for semantic analysis.",
49
56
  });
50
57
  }
51
58
  else {
@@ -55,11 +55,9 @@ export function showTaskDetails(taskId) {
55
55
  const isArray = Array.isArray(value);
56
56
  const isObject = typeof value === "object" && !isArray;
57
57
  if (typeof value === "string" && value.length < 80) {
58
- // Short string: inline
59
58
  console.log(chalk.yellow(`${label}:`), value);
60
59
  }
61
60
  else if (isArray) {
62
- // Arrays: print first 5 items each on new line, then show count if truncated
63
61
  console.log(chalk.yellow(`${label}:`));
64
62
  const items = value;
65
63
  const displayCount = Math.min(5, items.length);
@@ -71,24 +69,20 @@ export function showTaskDetails(taskId) {
71
69
  }
72
70
  }
73
71
  else if (isObject) {
74
- // Objects: pretty print JSON
75
72
  console.log(chalk.yellow(`${label}:`));
76
73
  console.log(indent + JSON.stringify(value, null, 2).replace(/\n/g, "\n" + indent));
77
74
  }
78
75
  else {
79
- // Long string: print on next line with indentation
80
76
  console.log(chalk.yellow(`${label}:`));
81
77
  console.log(indent + String(value));
82
78
  }
83
79
  };
84
80
  // Lifecycle
85
- // Lifecycle
86
81
  print("Initial query", task.initial_query);
87
- // Final answer (canonical)
88
82
  if (task.summary) {
89
83
  console.log(chalk.green.bold("\nFinal answer:\n"));
90
84
  console.log(chalk.white(task.summary));
91
- console.log(); // spacing
85
+ console.log();
92
86
  }
93
87
  // Intent
94
88
  print("Agreed intent", task.agreed_intent);
@@ -124,6 +118,28 @@ export function showTaskDetails(taskId) {
124
118
  // Bookkeeping
125
119
  print("Created", task.created_at);
126
120
  print("Updated", task.updated_at);
121
+ // ----------------- TASK STEPS -----------------
122
+ const steps = db.prepare(`
123
+ SELECT id, file_path, action, status, step_index, start_time, end_time, created_at, updated_at
124
+ FROM task_steps
125
+ WHERE task_id = ?
126
+ ORDER BY step_index ASC
127
+ `).all(taskId);
128
+ if (steps.length === 0) {
129
+ console.log(chalk.dim("\nNo task steps recorded."));
130
+ }
131
+ else {
132
+ console.log(chalk.bold("\nTask Steps:\n"));
133
+ steps.forEach(s => {
134
+ console.log(`${chalk.cyan(`[${s.id}]`)} ` +
135
+ `${chalk.yellow((s.status || "").padEnd(9))} ` +
136
+ `${s.file_path}` +
137
+ (s.action ? ` → ${s.action}` : "") +
138
+ ` (index=${s.step_index ?? "-"})` +
139
+ (s.start_time ? ` started=${s.start_time}` : "") +
140
+ (s.end_time ? ` ended=${s.end_time}` : ""));
141
+ });
142
+ }
127
143
  }
128
144
  /** Helper: truncate long text to a single line */
129
145
  function oneLineTruncate(input, max = 80) {
package/dist/db/schema.js CHANGED
@@ -52,6 +52,7 @@ export function initSchema() {
52
52
  project_id INTEGER NOT NULL,
53
53
 
54
54
  -- lifecycle
55
+ -- active | completed | paused | needs-feedback
55
56
  status TEXT NOT NULL DEFAULT 'active',
56
57
 
57
58
  -- user-facing / context
@@ -71,6 +72,9 @@ export function initSchema() {
71
72
  -- routing decision
72
73
  routing_decision_json TEXT,
73
74
 
75
+ -- 🔴 task-level feedback / questions (append-only)
76
+ task_questions_json TEXT,
77
+
74
78
  -- bookkeeping
75
79
  created_at TEXT NOT NULL,
76
80
  updated_at TEXT NOT NULL,
@@ -85,6 +89,47 @@ export function initSchema() {
85
89
 
86
90
  CREATE INDEX IF NOT EXISTS idx_tasks_status
87
91
  ON tasks(status);
92
+ `);
93
+ // --- Task steps ---
94
+ db.exec(`
95
+ CREATE TABLE IF NOT EXISTS task_steps (
96
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
97
+
98
+ task_id INTEGER NOT NULL,
99
+
100
+ file_path TEXT NOT NULL,
101
+ action TEXT,
102
+
103
+ -- pending | completed | skipped | needs-feedback
104
+ status TEXT NOT NULL DEFAULT 'pending',
105
+
106
+ start_time INTEGER,
107
+ end_time INTEGER,
108
+
109
+ result_json TEXT,
110
+ notes TEXT,
111
+
112
+ step_index INTEGER,
113
+
114
+ -- 🔴 step-level feedback / questions (append-only)
115
+ step_questions_json TEXT,
116
+
117
+ created_at TEXT NOT NULL,
118
+ updated_at TEXT NOT NULL,
119
+
120
+ FOREIGN KEY (task_id)
121
+ REFERENCES tasks(id)
122
+ ON DELETE CASCADE
123
+ );
124
+
125
+ CREATE INDEX IF NOT EXISTS idx_task_steps_task
126
+ ON task_steps(task_id);
127
+
128
+ CREATE INDEX IF NOT EXISTS idx_task_steps_status
129
+ ON task_steps(status);
130
+
131
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_task_steps_task_file
132
+ ON task_steps(task_id, file_path);
88
133
  `);
89
134
  // --- Core tables ---
90
135
  db.exec(`
@@ -90,6 +90,46 @@ else {
90
90
  sampleTasks.forEach(t => console.log(` [${t.id}] "${t.initial_query}" (status=${t.status}, created=${t.created_at})`));
91
91
  }
92
92
  }
93
+ /* ───────────────────────── task_steps sanity ───────────────────────── */
94
+ header("🦾 task_steps");
95
+ const taskStepsExists = db
96
+ .prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name='task_steps'`)
97
+ .get();
98
+ if (!taskStepsExists) {
99
+ console.log("❌ task_steps table missing — you need to run initSchema with task_steps");
100
+ }
101
+ else {
102
+ const totalTaskSteps = tableCount("task_steps");
103
+ const pendingSteps = nonEmptyCount("task_steps", "status"); // all steps have status
104
+ console.log(`📊 total task_steps: ${totalTaskSteps}`);
105
+ console.log(`📝 task_steps with status: ${pendingSteps}`);
106
+ const sampleSteps = db.prepare(`
107
+ SELECT id, task_id, file_path, status, step_index, created_at
108
+ FROM task_steps
109
+ ORDER BY created_at DESC
110
+ LIMIT 3
111
+ `).all();
112
+ if (sampleSteps.length === 0) {
113
+ console.log("⚠️ no task_steps yet");
114
+ }
115
+ else {
116
+ console.log("✅ sample task_steps:");
117
+ sampleSteps.forEach(s => console.log(` [${s.id}] task ${s.task_id} - file "${s.file_path}" (status=${s.status}, index=${s.step_index}, created=${s.created_at})`));
118
+ }
119
+ // Check for steps referencing missing tasks
120
+ const danglingSteps = db.prepare(`
121
+ SELECT ts.id, ts.task_id
122
+ FROM task_steps ts
123
+ LEFT JOIN tasks t ON ts.task_id = t.id
124
+ WHERE t.id IS NULL
125
+ `).all();
126
+ if (danglingSteps.length > 0) {
127
+ console.log(`❌ ${danglingSteps.length} task_steps reference missing tasks`);
128
+ }
129
+ else {
130
+ console.log("✅ all task_steps reference valid tasks");
131
+ }
132
+ }
93
133
  /* ───────────────────────── files ───────────────────────── */
94
134
  header("🗂 files");
95
135
  const totalFiles = tableCount("files");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.161",
3
+ "version": "0.1.163",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"