scai 0.1.152 → 0.1.154

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.
@@ -13,7 +13,9 @@ import { resolveExecutionModeStep } from "./resolveExecutionModeStep.js";
13
13
  import { fileCheckStep } from "./fileCheckStep.js";
14
14
  import { analysisPlanGenStep } from "./analysisPlanGenStep.js";
15
15
  import { readinessGateStep } from "./readinessGateStep.js";
16
- import { collectAnalysisEvidenceStep } from "./collectAnalysisEvidenceStep.js";
16
+ import { scopeClassificationStep } from "./scopeClassificationStep.js";
17
+ import { routingDecisionStep } from "./routingDecisionStep.js";
18
+ import { evidenceVerifierStep } from "./evidenceVerifierStep.js";
17
19
  /* ───────────────────────── registry ───────────────────────── */
18
20
  const MODULE_REGISTRY = Object.fromEntries(Object.entries(builtInModules).map(([name, mod]) => [name, mod]));
19
21
  function resolveModuleForAction(action) {
@@ -82,19 +84,29 @@ export class MainAgent {
82
84
  /* ───────────── execution gates ───────────── */
83
85
  canExecutePhase(phase) {
84
86
  const mode = this.context.executionControl?.mode ?? "transform";
87
+ const constraints = this.context.executionControl?.constraints;
88
+ let allowed;
89
+ // If docsOnly is true, prevent any non-doc actions
90
+ const docsOnly = constraints?.docsOnly ?? false;
85
91
  switch (phase) {
86
92
  case "analysis":
87
- return mode !== "explain"; // only analyze if not explain
93
+ allowed = !docsOnly && (constraints?.allowAnalysis ?? (mode !== "explain"));
94
+ break;
88
95
  case "planning":
89
- return mode === "analyze" || mode === "transform";
96
+ allowed = !docsOnly && (constraints?.allowPlanning ?? (mode === "analyze" || mode === "transform"));
97
+ break;
90
98
  case "transform":
91
99
  case "write":
92
- return mode === "transform"; // write only in transform
100
+ allowed = constraints?.allowFileWrites ?? (mode === "transform");
101
+ break;
93
102
  case "explain":
94
- return mode === "explain"; // only explain mode runs explain
103
+ allowed = mode === "explain"; // explain is purely mode-based
104
+ break;
95
105
  default:
96
- return false;
106
+ allowed = false;
97
107
  }
108
+ this.logLine("EXEC", "canExecutePhase", undefined, `phase=${phase}, mode=${mode}, docsOnly=${docsOnly}, allowed=${allowed}`);
109
+ return allowed;
98
110
  }
99
111
  /* ───────────── main run ───────────── */
100
112
  async run() {
@@ -103,54 +115,115 @@ export class MainAgent {
103
115
  this.logLine("RUN", `start #${this.runCount}`);
104
116
  logInputOutput("GlobalContext (structured)", "input", this.context);
105
117
  /* ================= BOOT ================= */
118
+ // AXIS 1: Capability — determine WHAT actions are allowed
119
+ // executionMode: "explain" | "analyze" | "transform"
120
+ // Controls: files may be written, code analyzed, or run text-only
121
+ // This step does NOT determine WHERE to act or whether enough evidence exists
106
122
  {
107
123
  const t1 = this.startTimer();
108
- await understandIntentStep.run({ context: this.context });
124
+ await understandIntentStep.run({ context: this.context }); // Classify user intent
109
125
  this.logLine("BOOT", "understandIntent", t1());
110
126
  const t2 = this.startTimer();
111
- await resolveExecutionModeStep.run(this.context);
127
+ await resolveExecutionModeStep.run(this.context); // Set executionMode
112
128
  this.logLine("BOOT", "resolveExecutionMode", t2(), `mode = ${this.context.executionControl?.mode}`);
113
- ensureExecutionControl(this.context);
114
129
  const db = getDbForRepo();
115
- this.taskId = bootTaskForRepo(this.context, db, this.logLine.bind(this));
130
+ this.taskId = bootTaskForRepo(this.context, db, this.logLine.bind(this)); // Persist task
116
131
  }
117
132
  /* ================= PRECHECK (INITIAL) ================= */
133
+ // Quick file existence check (pre-grounding)
134
+ // Does NOT infer relevance or scope beyond directly referenced files
118
135
  {
119
136
  const t = this.startTimer();
120
137
  await fileCheckStep(this.context);
121
138
  this.logLine("PRECHECK", "preFileSearch", t());
122
139
  }
123
- /* ================= INFORMATION ACQUISITION ================= */
124
- if (this.canExecutePhase("planning")) {
125
- {
140
+ /* ───────────────────────── SCOPE & ROUTING GATE ───────────────────────── */
141
+ // AXIS 2: Scope — determine WHERE the agent may act
142
+ // scopeType: "none" | "single-file" | "multi-file" | "repo-wide"
143
+ // Responsible for deciding which files, repos, or modules the agent should consider
144
+ // Does NOT validate anchors (Axis 3) or decide allowed actions (Axis 1)
145
+ // ----------------- SCOPE CLASSIFICATION -----------------
146
+ // Responsibility:
147
+ // - Determine problem coverage: none / single-file / multi-file / repo-wide
148
+ // - Purely repo/file coverage decision
149
+ // - Output: context.analysis.scopeType
150
+ // Does NOT decide allowed actions, validate evidence, or affect executionMode
151
+ {
152
+ const t1 = this.startTimer();
153
+ await scopeClassificationStep.run(this.context);
154
+ this.logLine("SCOPE", "scopeClassification", t1());
155
+ }
156
+ // ----------------- ROUTING DECISION -----------------
157
+ // Responsibility:
158
+ // - Determine allowed actions within scope
159
+ // * Can search be performed?
160
+ // * Are early exits (explain) allowed?
161
+ // - Output: context.analysis.routingDecision
162
+ // Does NOT check anchors, determine readiness, or modify scope
163
+ {
164
+ const t2 = this.startTimer();
165
+ await routingDecisionStep.run(this.context);
166
+ this.logLine("SCOPE", "routingDecision", t2());
167
+ }
168
+ /* ================= GROUNDING & READINESS LOOP ================= */
169
+ // AXIS 3: Certainty — do we have enough evidence to safely proceed?
170
+ // confidence: "high" | "medium" | "low"
171
+ // Controls whether we loop back to acquire more info
172
+ // Does NOT change executionMode (Axis 1) or scopeType (Axis 2)
173
+ let ready = false;
174
+ while (!ready && this.runCount < this.maxRuns) {
175
+ const routing = this.context.analysis?.routingDecision;
176
+ // ---------------- INFORMATION ACQUISITION ----------------
177
+ // Plan & execute info steps; may discover new files
178
+ // Does NOT guarantee readiness — anchors still need verification
179
+ if (this.canExecutePhase("planning") && routing?.allowInfoSteps !== false) {
126
180
  const t = this.startTimer();
127
- await infoPlanGen.run(this.context);
181
+ await infoPlanGen.run(this.context); // Generate info-gathering plan
128
182
  this.logLine("PLAN", "infoPlanGen", t());
183
+ let stepIO = { query: this.query };
184
+ const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
185
+ for (const step of infoPlan.steps.filter(s => s.groups?.includes("info"))) {
186
+ // Guard: skip fileSearch if routingDecision forbids repo search
187
+ if (step.action === "fileSearch" && !routing?.allowSearch) {
188
+ this.logLine("PLAN", "infoStepSkipped", undefined, `Step "${step.description}" skipped: file search not allowed by routingDecision`);
189
+ continue;
190
+ }
191
+ stepIO = await this.executeStep(step, stepIO);
192
+ }
193
+ // Re-run precheck for newly discovered files
194
+ if (infoPlan.steps.length > 0) {
195
+ const t2 = this.startTimer();
196
+ await fileCheckStep(this.context);
197
+ this.logLine("PRECHECK", "postFileSearch", t2());
198
+ }
129
199
  }
130
- let stepIO = { query: this.query };
131
- const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
132
- for (const step of infoPlan.steps.filter(s => s.groups?.includes("info"))) {
133
- stepIO = await this.executeStep(step, stepIO);
134
- }
135
- if (infoPlan.steps.length > 0) {
136
- const t = this.startTimer();
137
- await fileCheckStep(this.context);
138
- this.logLine("PRECHECK", "postFileSearch", t());
139
- }
140
- }
141
- /* ================= GROUNDING & READINESS GATE ================= */
142
- {
200
+ // ---------------- DETERMINISTIC EVIDENCE VERIFICATION ----------------
201
+ // Verify anchors exist (filenames, paths, literals)
202
+ // Does NOT execute full analysis or transformations
143
203
  const t1 = this.startTimer();
144
- await collectAnalysisEvidenceStep.run({ query: this.query, context: this.context });
145
- this.logLine("ANALYSIS", "collectAnalysisEvidence", t1());
204
+ await evidenceVerifierStep.run({ query: this.query, context: this.context });
205
+ this.logLine("ANALYSIS", "collectAnalysisEvidence", t1(), undefined);
206
+ // Select grounded candidate files
207
+ // Does NOT update executionMode or scope classification
146
208
  const t2 = this.startTimer();
147
209
  await selectRelevantSourcesStep.run({ query: this.query, context: this.context });
148
- this.logLine("ANALYSIS", "selectRelevantSources", t2());
210
+ this.logLine("ANALYSIS", "selectRelevantSources", t2(), undefined);
211
+ // ---------------- READINESS GATE ----------------
212
+ // Decide if we have enough info to proceed
213
+ // Does NOT modify info plan or transform steps
149
214
  const t3 = this.startTimer();
150
215
  await readinessGateStep.run(this.context);
151
- this.logLine("HASINFO", "readinessGate", t3());
216
+ this.logLine("HASINFO", "readinessGate", t3(), undefined);
217
+ ready = this.context.analysis?.readiness?.decision === "ready";
218
+ if (!ready) {
219
+ this.runCount++;
220
+ this.resetInitContextForLoop();
221
+ this.logLine("HASINFO", "readinessGate", undefined, "Not ready, looping back to information acquisition");
222
+ }
152
223
  }
153
224
  /* ================= EXPLAIN (EVIDENCE EXIT) ================= */
225
+ // Early exit if mode=explain and routing allows it
226
+ // Does NOT perform analysis, transform, or write steps
154
227
  if (this.canExecutePhase("explain") &&
155
228
  this.context.analysis?.routingDecision?.decision === "has-info") {
156
229
  const explainMod = resolveModuleForAction("explain");
@@ -161,6 +234,7 @@ export class MainAgent {
161
234
  context: this.context
162
235
  });
163
236
  }
237
+ // Remaining phases (ANALYSIS, TRANSFORM, FINALIZE, PERSIST) execute after loop exits
164
238
  /* ================= ANALYSIS (DEEP) ================= */
165
239
  if (this.canExecutePhase("analysis")) {
166
240
  {
@@ -300,52 +374,6 @@ export function persistTaskData(context, taskId, db, logLine) {
300
374
  db.prepare(`UPDATE tasks SET ${setClause}, updated_at = ? WHERE id = ?`).run(...Object.values(fieldsToUpdate), now, taskId);
301
375
  logLine("TASK", "persistTaskData", undefined, `task ${taskId} updated with run data`);
302
376
  }
303
- /**
304
- * Ensure executionControl exists and constraints are correctly set
305
- */
306
- export function ensureExecutionControl(context) {
307
- var _a;
308
- context.executionControl ?? (context.executionControl = {
309
- mode: "transform",
310
- rationale: "Default execution mode",
311
- confidence: 1,
312
- constraints: {
313
- allowAnalysis: true,
314
- allowPlanning: true,
315
- allowFileWrites: false
316
- }
317
- });
318
- const mode = context.executionControl.mode;
319
- switch (mode) {
320
- case "explain":
321
- context.executionControl.constraints = {
322
- allowAnalysis: false,
323
- allowPlanning: false,
324
- allowFileWrites: false
325
- };
326
- break;
327
- case "analyze":
328
- context.executionControl.constraints = {
329
- allowAnalysis: true,
330
- allowPlanning: true,
331
- allowFileWrites: false
332
- };
333
- break;
334
- case "transform":
335
- context.executionControl.constraints = {
336
- allowAnalysis: true,
337
- allowPlanning: true,
338
- allowFileWrites: true
339
- };
340
- break;
341
- default:
342
- (_a = context.executionControl).constraints ?? (_a.constraints = {
343
- allowAnalysis: true,
344
- allowPlanning: true,
345
- allowFileWrites: false
346
- });
347
- }
348
- }
349
377
  /**
350
378
  * Ensure global_state, project, and task exist for this repo.
351
379
  * Returns the created taskId
@@ -0,0 +1,141 @@
1
+ // File: src/modules/evidenceVerifierStep.ts
2
+ import fs from "fs";
3
+ import { logInputOutput } from "../utils/promptLogHelper.js";
4
+ /**
5
+ * Purpose:
6
+ * Deterministic evidence verification.
7
+ * - Scans candidate files line-by-line for exact or heuristic matches to the query.
8
+ * - Checks for filename matches to the query (dominant signal).
9
+ * - Optionally checks for top-level function/class names in query.
10
+ * - Populates context.analysis.fileAnalysis.
11
+ * - Does NOT make execution decisions, update scope, or plan transformations.
12
+ * - Strictly Axis 3: Certainty.
13
+ */
14
+ export const evidenceVerifierStep = {
15
+ name: "evidenceVerifier",
16
+ description: "Deterministic evidence-first scan over candidate files to populate fileAnalysis, with filename dominance.",
17
+ groups: ["analysis"],
18
+ run: async (input) => {
19
+ var _a, _b;
20
+ const query = input.query ?? "";
21
+ const context = input.context;
22
+ if (!context?.analysis) {
23
+ throw new Error("[evidenceVerifier] context.analysis is required.");
24
+ }
25
+ (_a = context.analysis).fileAnalysis ?? (_a.fileAnalysis = {});
26
+ const candidatePaths = [
27
+ ...(context.analysis.focus?.relevantFiles ?? []),
28
+ ...(context.workingFiles?.map(f => f.path) ?? []),
29
+ ];
30
+ const uniquePaths = Array.from(new Set(candidatePaths));
31
+ if (!uniquePaths.length) {
32
+ console.warn("[evidenceVerifier] No candidate files to scan.");
33
+ return { query, data: {} };
34
+ }
35
+ const filesWithEvidence = [];
36
+ // ----------------- Parse query for targets -----------------
37
+ const sentenceRegex = /['"`](.+?)['"`]/g;
38
+ const sentenceTargets = [];
39
+ let match;
40
+ while ((match = sentenceRegex.exec(query)) !== null) {
41
+ sentenceTargets.push(match[1]);
42
+ }
43
+ const filenameTargets = query.split(/\s+/).filter(w => w.match(/\.(ts|js|tsx)$/));
44
+ const symbolTargets = [];
45
+ const symbolRegex = /\b([A-Z]\w+|[a-z]\w+)\(\)/g;
46
+ let symMatch;
47
+ while ((symMatch = symbolRegex.exec(query)) !== null) {
48
+ symbolTargets.push(symMatch[1]);
49
+ }
50
+ const mode = context.executionControl?.mode ?? "transform";
51
+ const allowAnalysis = context.executionControl?.constraints?.allowAnalysis ?? (mode !== "explain");
52
+ const allowWrites = context.executionControl?.constraints?.allowFileWrites ?? (mode === "transform");
53
+ // ----------------- Process each file -----------------
54
+ for (const path of uniquePaths) {
55
+ let code = null;
56
+ try {
57
+ code = fs.readFileSync(path, "utf-8");
58
+ }
59
+ catch (err) {
60
+ console.warn(`[evidenceVerifier] Failed to read ${path}: ${err.message}`);
61
+ }
62
+ const lines = code?.split("\n") ?? [];
63
+ const evidenceItems = [];
64
+ const matchedLines = [];
65
+ // Sentence & Symbol evidence
66
+ lines.forEach((line, idx) => {
67
+ sentenceTargets.forEach(target => {
68
+ if (line.includes(target)) {
69
+ evidenceItems.push({
70
+ claim: `Line contains the target sentence: "${target}"`,
71
+ excerpt: line,
72
+ span: { startLine: idx + 1, endLine: idx + 1 },
73
+ confidence: 1,
74
+ });
75
+ matchedLines.push(line);
76
+ }
77
+ });
78
+ symbolTargets.forEach(sym => {
79
+ if (line.includes(`function ${sym}`) || line.includes(`class ${sym}`)) {
80
+ evidenceItems.push({
81
+ claim: `Line contains target symbol: "${sym}"`,
82
+ excerpt: line,
83
+ span: { startLine: idx + 1, endLine: idx + 1 },
84
+ confidence: 0.9,
85
+ });
86
+ matchedLines.push(line);
87
+ }
88
+ });
89
+ });
90
+ // Filename-level evidence
91
+ const fileName = path.split("/").pop() ?? "";
92
+ if (filenameTargets.includes(fileName)) {
93
+ evidenceItems.push({
94
+ claim: `Filename matches query target: "${fileName}"`,
95
+ excerpt: "",
96
+ span: { startLine: 0, endLine: 0 },
97
+ confidence: 1,
98
+ });
99
+ }
100
+ // ----------------- Force focus files as relevant -----------------
101
+ const isFocusFile = context.analysis.focus?.relevantFiles?.includes(path) ?? false;
102
+ const isRelevant = isFocusFile || evidenceItems.length > 0;
103
+ context.analysis.fileAnalysis[path] = {
104
+ ...context.analysis.fileAnalysis[path],
105
+ intent: isRelevant ? "relevant" : "irrelevant",
106
+ relevance: isRelevant
107
+ ? `${evidenceItems.length} evidence item(s) match the query${isFocusFile ? " (focus file forced relevant)" : ""}`
108
+ : "No matching evidence found",
109
+ role: "primary",
110
+ action: { isRelevant, shouldModify: isRelevant && allowAnalysis && allowWrites },
111
+ proposedChanges: {
112
+ summary: isRelevant ? "Evidence found in file" : "No evidence",
113
+ scope: isRelevant ? "minor" : "none",
114
+ targets: matchedLines,
115
+ rationale: isFocusFile ? "Focus file forced relevant even without explicit evidence" : undefined,
116
+ },
117
+ risks: [],
118
+ evidence: evidenceItems,
119
+ };
120
+ if (isRelevant)
121
+ filesWithEvidence.push(path);
122
+ }
123
+ // ----------------- Update focus after processing -----------------
124
+ if (filesWithEvidence.length > 0) {
125
+ (_b = context.analysis).focus ?? (_b.focus = { relevantFiles: [] });
126
+ context.analysis.focus.relevantFiles = filesWithEvidence;
127
+ for (const path of uniquePaths) {
128
+ if (!filesWithEvidence.includes(path)) {
129
+ context.analysis.fileAnalysis[path] = {
130
+ ...context.analysis.fileAnalysis[path],
131
+ intent: "irrelevant",
132
+ action: { ...context.analysis.fileAnalysis[path]?.action, isRelevant: false },
133
+ };
134
+ }
135
+ }
136
+ }
137
+ const output = { query, data: { fileAnalysis: context.analysis.fileAnalysis } };
138
+ logInputOutput("evidenceVerifier", "output", output.data);
139
+ return output;
140
+ },
141
+ };
@@ -27,9 +27,12 @@ export const infoPlanGen = {
27
27
  const missingFiles = Array.isArray(context.analysis.focus?.missingFiles)
28
28
  ? context.analysis.focus.missingFiles
29
29
  : [];
30
- if (missingFiles.length === 0) {
30
+ const analysis = context.analysis;
31
+ const needsDiscovery = analysis?.routingDecision?.decision === "needs-info" ||
32
+ analysis?.scopeType === "repo-wide";
33
+ if (!needsDiscovery && missingFiles.length === 0) {
31
34
  context.analysis.planSuggestion = { plan: { steps: [] } };
32
- logInputOutput('infoPlanGen', 'output', { steps: [] });
35
+ logInputOutput("infoPlanGen", "output", { steps: [] });
33
36
  return;
34
37
  }
35
38
  // --------------------------------------------------
@@ -1,52 +1,45 @@
1
+ // File: src/modules/readinessGateStep.ts
1
2
  import { generate } from '../lib/generate.js';
2
3
  import { logInputOutput } from '../utils/promptLogHelper.js';
3
- function logRoutingDecision(context) {
4
- logInputOutput('readinessGateStep', 'output', {
5
- routingDecision: context.analysis?.routingDecision,
6
- focus: context.analysis?.focus,
7
- fileAnalysis: context.analysis?.fileAnalysis,
8
- });
9
- }
10
4
  export const readinessGateStep = {
11
5
  name: 'readinessGate',
6
+ description: 'Determines if there is sufficient evidence to safely proceed with analysis or transformation.',
7
+ groups: ['analysis'],
12
8
  requires: ['userQuery', 'intent'],
13
- produces: ['analysis.routingDecision'],
9
+ produces: ['analysis.readiness'],
14
10
  async run(context) {
15
- var _a, _b;
11
+ var _a, _b, _c, _d, _e;
16
12
  context.analysis || (context.analysis = {});
17
13
  (_a = context.analysis).focus || (_a.focus = { relevantFiles: [], missingFiles: [], rationale: '' });
18
14
  (_b = context.analysis).fileAnalysis || (_b.fileAnalysis = {});
15
+ (_c = context.analysis).readiness || (_c.readiness = { decision: 'not-ready', confidence: 0, rationale: '' });
19
16
  const focus = context.analysis.focus;
20
17
  const fileAnalysis = context.analysis.fileAnalysis;
21
- // ================= CHECK EVIDENCE =================
18
+ // ---------------- EVIDENCE COLLECTION ----------------
22
19
  const relevantFiles = Object.entries(fileAnalysis)
23
20
  .filter(([_, fa]) => fa.intent === 'relevant')
24
- .map(([path, _]) => path);
25
- const irrelevantFiles = Object.entries(fileAnalysis)
26
- .filter(([_, fa]) => fa.intent === 'irrelevant')
27
- .map(([path, _]) => path);
21
+ .map(([path]) => path);
28
22
  const missingFiles = focus.missingFiles ?? [];
29
- // Fast path: no relevant files, evidence indicates info is missing
23
+ // Deterministic short-circuit: nothing relevant or missing files
30
24
  if (relevantFiles.length === 0 || missingFiles.length > 0) {
31
- context.analysis.routingDecision = {
32
- decision: 'needs-info',
33
- rationale: `No relevant evidence found or missing files: ${missingFiles.join(', ')}`,
34
- confidence: 0.9,
25
+ context.analysis.readiness = {
26
+ decision: 'not-ready',
27
+ confidence: 0.95,
28
+ rationale: `No relevant files or missing files detected: ${missingFiles.join(', ')}`,
35
29
  };
36
- logRoutingDecision(context);
30
+ logOutput(context);
37
31
  return;
38
32
  }
39
- // Evidence-based classification
40
- // Build simple summary for model
33
+ // ---------------- SUMMARIZE EVIDENCE FOR LLM ----------------
41
34
  const evidenceSummary = relevantFiles
42
35
  .map(path => {
43
36
  const fa = fileAnalysis[path];
44
37
  const claims = fa.evidence?.map(e => `- ${e.claim}`).join('\n') ?? '';
45
- return `File: ${path}\nRole: ${fa.role}\nRelevance: ${fa.relevance}\nClaims:\n${claims}`;
38
+ return `File: ${path}\nRole: ${fa.role}\nClaims:\n${claims}`;
46
39
  })
47
40
  .join('\n\n');
48
41
  const prompt = `
49
- You are a routing classifier. Your task is to determine if the agent has sufficient evidence to answer the user's query.
42
+ You are a focused evidence assessor. Do NOT invent new facts. Only reason about the evidence given.
50
43
 
51
44
  User query:
52
45
  ${context.initContext?.userQuery}
@@ -54,48 +47,72 @@ ${context.initContext?.userQuery}
54
47
  Evidence from analyzed files:
55
48
  ${evidenceSummary}
56
49
 
57
- Decide ONE of the following:
58
-
59
- HAS-INFO:
60
- - Sufficient evidence exists in the analyzed files.
61
- - All required information is present to answer the user's query.
62
-
63
- NEEDS-INFO:
64
- - Insufficient evidence or missing critical files.
65
- - Additional information acquisition must run.
50
+ Instruction:
51
+ - Decide whether there is sufficient evidence to proceed safely with analysis or transformation.
52
+ - Return a JSON object EXACTLY in this format:
66
53
 
67
- Return ONLY the label (HAS-INFO or NEEDS-INFO) and optionally a short rationale.
54
+ {
55
+ "decision": "ready" | "not-ready",
56
+ "confidence": 0.0-1.0,
57
+ "rationale": "Concise explanation based only on the evidence provided."
58
+ }
68
59
  `.trim();
69
60
  try {
70
- const genInput = {
71
- query: context.initContext?.userQuery ?? '',
72
- content: prompt,
73
- };
61
+ const genInput = { query: context.initContext?.userQuery ?? '', content: prompt };
74
62
  const genOutput = await generate(genInput);
75
63
  const raw = String(genOutput.data ?? '').trim();
76
64
  logInputOutput('readinessGateStep raw', 'output', raw);
77
- const extractedText = raw.replace(/^(HAS-INFO|NEEDS-INFO):?\s*/i, '').trim();
78
- const modelDecision = raw.toUpperCase().startsWith('HAS-INFO') ? 'has-info' : 'needs-info';
65
+ // Attempt to parse JSON from LLM
66
+ let modelDecision;
67
+ try {
68
+ modelDecision = JSON.parse(raw);
69
+ }
70
+ catch {
71
+ modelDecision = {
72
+ decision: 'not-ready',
73
+ confidence: 0.5,
74
+ rationale: 'Failed to parse model output; assuming not-ready.',
75
+ };
76
+ }
77
+ // Merge deterministic info with LLM summary
79
78
  const combinedRationale = [
80
- `Evidence-based routing: ${relevantFiles.length} relevant files, ${irrelevantFiles.length} irrelevant files.`,
81
- extractedText ? `Model: ${extractedText}` : null,
79
+ `Evidence-based: ${relevantFiles.length} relevant file(s).`,
80
+ modelDecision.rationale,
82
81
  ]
83
82
  .filter(Boolean)
84
- .join('\n');
85
- context.analysis.routingDecision = {
86
- decision: modelDecision,
83
+ .join(' ');
84
+ context.analysis.readiness = {
85
+ decision: modelDecision.decision,
86
+ confidence: modelDecision.confidence,
87
87
  rationale: combinedRationale,
88
- confidence: modelDecision === 'has-info' ? 0.85 : 0.9,
89
88
  };
90
- logRoutingDecision(context);
89
+ // ---------------- DOCS-ONLY FILES ----------------
90
+ // If executionControl.docsOnly is true, mark relevant files as modifiable
91
+ const docsOnly = context.executionControl?.constraints?.docsOnly ?? false;
92
+ if (docsOnly && context.analysis.readiness.decision === 'ready') {
93
+ for (const path of relevantFiles) {
94
+ (_d = context.analysis.fileAnalysis)[path] || (_d[path] = { action: {}, intent: 'relevant' });
95
+ (_e = context.analysis.fileAnalysis[path]).action || (_e.action = {});
96
+ context.analysis.fileAnalysis[path].action.shouldModify = true;
97
+ }
98
+ }
99
+ logOutput(context);
91
100
  }
92
101
  catch (err) {
93
102
  console.warn('⚠️ readinessGateStep failed:', err);
94
- context.analysis.routingDecision = {
95
- decision: 'needs-info',
96
- rationale: 'readinessGateStep encountered an internal error.',
103
+ context.analysis.readiness = {
104
+ decision: 'not-ready',
97
105
  confidence: 0.2,
106
+ rationale: 'Internal error while determining readiness.',
98
107
  };
99
108
  }
100
109
  },
101
110
  };
111
+ // ---------------- HELPER ----------------
112
+ function logOutput(context) {
113
+ logInputOutput('readinessGateStep', 'output', {
114
+ readiness: context.analysis?.readiness,
115
+ focus: context.analysis?.focus,
116
+ fileAnalysis: context.analysis?.fileAnalysis,
117
+ });
118
+ }