@lisa.ai/agent 2.1.3 → 2.2.0
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.
- package/dist/commands/coverage.js +99 -70
- package/dist/commands/heal.js +23 -5
- package/dist/index.js +58 -26
- package/dist/services/config.service.js +52 -5
- package/dist/services/discovery.service.js +48 -0
- package/dist/services/git.service.js +15 -3
- package/dist/services/llm.service.js +25 -8
- package/dist/services/telemetry.service.js +52 -13
- package/dist/utils/parser.js +8 -0
- package/package.json +1 -1
|
@@ -45,16 +45,21 @@ const discovery_service_1 = require("../services/discovery.service");
|
|
|
45
45
|
const installer_service_1 = require("../services/installer.service");
|
|
46
46
|
const generator_service_1 = require("../services/generator.service");
|
|
47
47
|
async function coverageCommand(command, modelProvider, attempt = 1, maxRetries = 3, projectId = 'local', apiKey) {
|
|
48
|
-
//
|
|
48
|
+
// Bug 8 fix: Always run scanRepository() so we have a framework fingerprint regardless of
|
|
49
|
+
// whether the user passed --command explicitly. scanRepository() is cheap (reads package.json)
|
|
50
|
+
// and the framework string is critical for generating syntactically correct tests (Bug 3).
|
|
51
|
+
const fingerprint = discovery_service_1.AutoDiscoveryService.scanRepository();
|
|
52
|
+
const detectedFramework = fingerprint.testingFramework !== 'none' ? fingerprint.testingFramework : undefined;
|
|
53
|
+
// [Lisa.ai V2 Zero-Config Engine]
|
|
49
54
|
if (!command) {
|
|
50
55
|
console.log(`\n[Lisa.ai Auto-Discovery] No explicit --command provided. Initiating Autonomous Framework Fingerprinting...`);
|
|
51
|
-
let
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
await generator_service_1.AutoGeneratorService.provisionConfigurationFiles(
|
|
56
|
+
let mutableFingerprint = fingerprint;
|
|
57
|
+
if (mutableFingerprint.testingFramework === 'none') {
|
|
58
|
+
mutableFingerprint = await installer_service_1.AutoInstallerService.installMissingFramework(mutableFingerprint);
|
|
59
|
+
await generator_service_1.AutoGeneratorService.provisionConfigurationFiles(mutableFingerprint, modelProvider, apiKey);
|
|
55
60
|
}
|
|
56
|
-
if (
|
|
57
|
-
command =
|
|
61
|
+
if (mutableFingerprint.suggestedTestCommand) {
|
|
62
|
+
command = mutableFingerprint.suggestedTestCommand;
|
|
58
63
|
console.log(`[Lisa.ai Auto-Discovery] Bootstrapping execution with natively discovered command: ${command}`);
|
|
59
64
|
}
|
|
60
65
|
else {
|
|
@@ -63,7 +68,6 @@ async function coverageCommand(command, modelProvider, attempt = 1, maxRetries =
|
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
console.log(`\n[Lisa.ai Coverage] ${command} (Attempt ${attempt}/${maxRetries}) Using Model: ${modelProvider}`);
|
|
66
|
-
let executionPassed = true;
|
|
67
71
|
await (0, telemetry_service_1.reportTelemetry)({
|
|
68
72
|
projectId,
|
|
69
73
|
type: 'coverage',
|
|
@@ -74,7 +78,7 @@ async function coverageCommand(command, modelProvider, attempt = 1, maxRetries =
|
|
|
74
78
|
});
|
|
75
79
|
const runSilentlyAndIntercept = (cmd, printProgress = false) => {
|
|
76
80
|
return new Promise((resolve, reject) => {
|
|
77
|
-
// Node 21+ Deprecation Fix: When shell is true, pass the entire raw command string
|
|
81
|
+
// Node 21+ Deprecation Fix: When shell is true, pass the entire raw command string
|
|
78
82
|
// rather than explicitly escaping array arguments to bypass DEP0190.
|
|
79
83
|
const child = (0, child_process_1.spawn)(cmd, { shell: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
80
84
|
let stdoutData = '';
|
|
@@ -125,72 +129,97 @@ async function coverageCommand(command, modelProvider, attempt = 1, maxRetries =
|
|
|
125
129
|
await coverageCommand(command, modelProvider, attempt + 1, maxRetries, projectId, apiKey);
|
|
126
130
|
return;
|
|
127
131
|
}
|
|
128
|
-
// 2.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
// 2. Tests passed — evaluate coverage.
|
|
133
|
+
// Bug 5 fix: removed dead `executionPassed` variable. It was set to `true` on line 42
|
|
134
|
+
// and never changed, making `if (executionPassed)` an always-true guard that added
|
|
135
|
+
// misleading nesting. The try block above returns/recursed-and-returned on failure,
|
|
136
|
+
// so reaching here already implies execution passed.
|
|
137
|
+
try {
|
|
138
|
+
// Find JSON summary expecting standard output paths
|
|
139
|
+
const coveragePath = path.resolve(process.cwd(), 'coverage/coverage-summary.json');
|
|
140
|
+
let uncoveredFiles = [];
|
|
141
|
+
if (!fs.existsSync(coveragePath)) {
|
|
142
|
+
console.log(`\n[Lisa.ai Coverage] No coverage-summary.json found. Initiating Cold-Start Project Crawler...`);
|
|
143
|
+
// [Lisa.ai Phase 8] Cold-Start Discovery
|
|
144
|
+
uncoveredFiles = discovery_service_1.AutoDiscoveryService.findUntestedFiles(process.cwd());
|
|
145
|
+
if (uncoveredFiles.length === 0) {
|
|
146
|
+
console.log(`✅ [Lisa.ai Coverage] Zero-Test scan complete. No untested source files discovered.`);
|
|
135
147
|
return;
|
|
136
148
|
}
|
|
149
|
+
console.log(`[Lisa.ai Coverage] Discovered ${uncoveredFiles.length} untested file(s). Seeding first test suite...`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
137
152
|
console.log(`[Lisa.ai Coverage] Evaluating summary...`);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
let testCode = '';
|
|
170
|
-
const fileContent = fs.readFileSync(absoluteTarget, 'utf-8');
|
|
171
|
-
if (existingSpecContent) {
|
|
172
|
-
console.log(`[Lisa.ai Coverage] Existing test suite discovered for ${targetFilePath}. Requesting single-pass logic coverage append...`);
|
|
173
|
-
testCode = await (0, llm_service_1.updateTestForFile)(targetFilePath, fileContent, path.relative(process.cwd(), targetSpecPath), existingSpecContent, modelProvider, apiKey);
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
console.log(`[Lisa.ai Coverage] Requesting newly generated test suite for ${targetFilePath}...`);
|
|
177
|
-
testCode = await (0, llm_service_1.generateTestForFile)(targetFilePath, fileContent, modelProvider, apiKey);
|
|
153
|
+
uncoveredFiles = (0, coverage_parser_1.parseCoverageSummary)('coverage/coverage-summary.json');
|
|
154
|
+
}
|
|
155
|
+
if (uncoveredFiles.length === 0) {
|
|
156
|
+
console.log(`✅ [Lisa.ai Coverage] 100% Logic Coverage Verified! No un-covered files remaining.`);
|
|
157
|
+
return; // We exit the loop
|
|
158
|
+
}
|
|
159
|
+
// [Lisa.ai V2 Infinite Loop]
|
|
160
|
+
// We consciously remove the global retry cutoff. Lisa natively runs infinitely in the background
|
|
161
|
+
// resolving testing issues continuously until the pipeline hits 100% green code organically.
|
|
162
|
+
console.log(`[Lisa.ai Coverage] Found ${uncoveredFiles.length} file(s) below 100% threshold:`, uncoveredFiles);
|
|
163
|
+
// 3. Address the first uncovered file
|
|
164
|
+
const targetFilePath = uncoveredFiles[0];
|
|
165
|
+
const absoluteTarget = path.resolve(process.cwd(), targetFilePath);
|
|
166
|
+
const parsedPath = path.parse(absoluteTarget);
|
|
167
|
+
// Dynamically detect existing Testing specs irrespective of framework
|
|
168
|
+
const possibleSpecs = [
|
|
169
|
+
path.join(parsedPath.dir, `${parsedPath.name}.spec${parsedPath.ext}`),
|
|
170
|
+
path.join(parsedPath.dir, `${parsedPath.name}.test${parsedPath.ext}`),
|
|
171
|
+
path.join(parsedPath.dir, `${parsedPath.name}.spec.js`),
|
|
172
|
+
path.join(parsedPath.dir, `${parsedPath.name}.test.js`),
|
|
173
|
+
path.join(parsedPath.dir, `${parsedPath.name}.spec.ts`),
|
|
174
|
+
path.join(parsedPath.dir, `${parsedPath.name}.test.ts`)
|
|
175
|
+
];
|
|
176
|
+
let existingSpecContent = null;
|
|
177
|
+
let targetSpecPath = path.join(parsedPath.dir, `${parsedPath.name}.spec${parsedPath.ext}`); // Default
|
|
178
|
+
for (const p of possibleSpecs) {
|
|
179
|
+
if (fs.existsSync(p)) {
|
|
180
|
+
targetSpecPath = p;
|
|
181
|
+
existingSpecContent = fs.readFileSync(p, 'utf-8');
|
|
182
|
+
break;
|
|
178
183
|
}
|
|
179
|
-
fs.writeFileSync(targetSpecPath, testCode, 'utf-8');
|
|
180
|
-
console.log(`[Lisa.ai Coverage] Wrote Spec File to ${targetSpecPath}`);
|
|
181
|
-
await (0, telemetry_service_1.reportTelemetry)({
|
|
182
|
-
projectId,
|
|
183
|
-
type: 'coverage',
|
|
184
|
-
filePath: targetFilePath,
|
|
185
|
-
modelUsed: modelProvider,
|
|
186
|
-
status: 'success',
|
|
187
|
-
details: `### Logic Coverage Action Detected\n**Auto-Generated Specification Append (${modelProvider}):**\n\`\`\`typescript\n${testCode}\n\`\`\``
|
|
188
|
-
});
|
|
189
|
-
// 4. Recursive iteration to verify newly written test and hunt for next gap
|
|
190
|
-
await coverageCommand(command, modelProvider, attempt + 1, maxRetries, projectId, apiKey);
|
|
191
184
|
}
|
|
192
|
-
|
|
193
|
-
|
|
185
|
+
let testCode = '';
|
|
186
|
+
const fileContent = fs.readFileSync(absoluteTarget, 'utf-8');
|
|
187
|
+
if (existingSpecContent) {
|
|
188
|
+
console.log(`[Lisa.ai Coverage] Existing test suite discovered for ${targetFilePath}. Requesting single-pass logic coverage append...`);
|
|
189
|
+
// Bug 8 fix: pass detectedFramework so the LLM generates correct syntax for the project's framework.
|
|
190
|
+
testCode = await (0, llm_service_1.updateTestForFile)(targetFilePath, fileContent, path.relative(process.cwd(), targetSpecPath), existingSpecContent, modelProvider, apiKey, detectedFramework);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
console.log(`[Lisa.ai Coverage] Requesting newly generated test suite for ${targetFilePath}...`);
|
|
194
|
+
// Bug 8 fix: pass detectedFramework so the LLM generates correct syntax for the project's framework.
|
|
195
|
+
testCode = await (0, llm_service_1.generateTestForFile)(targetFilePath, fileContent, modelProvider, apiKey, detectedFramework);
|
|
194
196
|
}
|
|
197
|
+
fs.writeFileSync(targetSpecPath, testCode, 'utf-8');
|
|
198
|
+
console.log(`[Lisa.ai Coverage] Wrote Spec File to ${targetSpecPath}`);
|
|
199
|
+
await (0, telemetry_service_1.reportTelemetry)({
|
|
200
|
+
projectId,
|
|
201
|
+
type: 'coverage',
|
|
202
|
+
filePath: targetFilePath,
|
|
203
|
+
modelUsed: modelProvider,
|
|
204
|
+
status: 'success',
|
|
205
|
+
details: `### Logic Coverage Action Detected\n**Auto-Generated Specification Append (${modelProvider}):**\n\`\`\`typescript\n${testCode}\n\`\`\``
|
|
206
|
+
});
|
|
207
|
+
// 4. Recursive iteration to verify newly written test and hunt for next gap
|
|
208
|
+
await coverageCommand(command, modelProvider, attempt + 1, maxRetries, projectId, apiKey);
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
console.error(`[Lisa.ai Coverage loop failure]:`, e.message);
|
|
212
|
+
// Telemetry Bug 2 fix: send an error event so the control plane dashboard doesn't
|
|
213
|
+
// show a run that started (status: 'running') but never resolved. Previously any
|
|
214
|
+
// exception thrown inside the coverage loop — LLM timeout, unreadable file, bad JSON —
|
|
215
|
+
// would be caught here and silently disappear from the dashboard.
|
|
216
|
+
await (0, telemetry_service_1.reportTelemetry)({
|
|
217
|
+
projectId,
|
|
218
|
+
type: 'coverage',
|
|
219
|
+
filePath: 'coverage-loop',
|
|
220
|
+
modelUsed: modelProvider,
|
|
221
|
+
status: 'error',
|
|
222
|
+
details: `Coverage loop encountered an unrecoverable error: ${e.message}`
|
|
223
|
+
});
|
|
195
224
|
}
|
|
196
225
|
}
|
package/dist/commands/heal.js
CHANGED
|
@@ -44,6 +44,10 @@ const telemetry_service_1 = require("../services/telemetry.service");
|
|
|
44
44
|
const discovery_service_1 = require("../services/discovery.service");
|
|
45
45
|
const installer_service_1 = require("../services/installer.service");
|
|
46
46
|
const generator_service_1 = require("../services/generator.service");
|
|
47
|
+
// Bug 6 fix: threshold for the failTracker — how many times a file is allowed to exhaust
|
|
48
|
+
// all local retries across the entire global run before being permanently abandoned.
|
|
49
|
+
// This is the "3-strike skip" described in the context architecture doc.
|
|
50
|
+
const GLOBAL_FAIL_THRESHOLD = 3;
|
|
47
51
|
function fetchSiblingContext(specPath) {
|
|
48
52
|
// Drop the .spec or .test extensions to find the actual Component / Service logic
|
|
49
53
|
const baseSiblingPath = specPath.replace(/\.(spec|test)\.(ts|js|jsx|tsx)$/, '.$2');
|
|
@@ -128,7 +132,7 @@ async function healCommand(command, modelProvider, attempt = 1, healedFilePath =
|
|
|
128
132
|
console.log(`\n[Lisa.ai Executing] ${command} (Global Engine) Using Model: ${modelProvider}`);
|
|
129
133
|
const runSilentlyAndIntercept = (cmd, printProgress = false) => {
|
|
130
134
|
return new Promise((resolve, reject) => {
|
|
131
|
-
// Node 21+ Deprecation Fix: When shell is true, pass the entire raw command string
|
|
135
|
+
// Node 21+ Deprecation Fix: When shell is true, pass the entire raw command string
|
|
132
136
|
// rather than explicitly escaping array arguments to bypass DEP0190.
|
|
133
137
|
const child = (0, child_process_1.spawn)(cmd, { shell: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
134
138
|
let stdoutData = '';
|
|
@@ -137,7 +141,7 @@ async function healCommand(command, modelProvider, attempt = 1, healedFilePath =
|
|
|
137
141
|
const text = data.toString();
|
|
138
142
|
stdoutData += text;
|
|
139
143
|
// [Lisa.ai Stream Interceptor]
|
|
140
|
-
// The user explicitly requested to see a single clean loading line instead of
|
|
144
|
+
// The user explicitly requested to see a single clean loading line instead of
|
|
141
145
|
// thousands of milliseconds of Redundant 'Executed' loops.
|
|
142
146
|
// Output is fully swallowed, and instead we log a spinner on boot.
|
|
143
147
|
});
|
|
@@ -155,8 +159,7 @@ async function healCommand(command, modelProvider, attempt = 1, healedFilePath =
|
|
|
155
159
|
});
|
|
156
160
|
};
|
|
157
161
|
try {
|
|
158
|
-
|
|
159
|
-
console.log(`⏳ [Lisa.ai Testing] Booting Headless Test Suite in background...`);
|
|
162
|
+
console.log(`⏳ [Lisa.ai Testing] Booting Headless Test Suite in background...`);
|
|
160
163
|
await runSilentlyAndIntercept(command, false); // False = output is suppressed natively
|
|
161
164
|
console.log(`\n✅ [Lisa.ai Success] Global Command executed successfully.`);
|
|
162
165
|
if (healedFilePath) {
|
|
@@ -179,7 +182,18 @@ async function healCommand(command, modelProvider, attempt = 1, healedFilePath =
|
|
|
179
182
|
console.error(`\n🚨 [Lisa.ai Error] Extracted file path does not exist: ${absolutePath}`);
|
|
180
183
|
process.exit(1);
|
|
181
184
|
}
|
|
182
|
-
|
|
185
|
+
// Bug 6 fix: implement the "3-strike global skip" described in the architecture doc.
|
|
186
|
+
// If this file has already exhausted all local retries GLOBAL_FAIL_THRESHOLD times
|
|
187
|
+
// across the entire run, it is a persistently broken file. Skip it immediately
|
|
188
|
+
// instead of wasting more LLM calls on it.
|
|
189
|
+
const globalFailCount = failTracker[filePath] || 0;
|
|
190
|
+
if (globalFailCount >= GLOBAL_FAIL_THRESHOLD) {
|
|
191
|
+
console.warn(`\n[Lisa.ai Auto-Heal] ⚠️ ${filePath} has failed globally ${globalFailCount} time(s) — exceeds threshold of ${GLOBAL_FAIL_THRESHOLD}. Permanently skipping.`);
|
|
192
|
+
skipLedger.push(filePath);
|
|
193
|
+
await healCommand(command, modelProvider, 1, null, maxRetries, projectId, undefined, apiKey, skipLedger, 0, failTracker);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
console.log(`[Lisa.ai Auto-Heal] Identified bleeding file: ${filePath} (global fail count: ${globalFailCount}/${GLOBAL_FAIL_THRESHOLD})`);
|
|
183
197
|
// [Lisa.ai V2 Micro-Targeted Healing Loop]
|
|
184
198
|
// Instead of restarting the global CI engine (e.g. `npm run test`) and waiting for all 400 specs
|
|
185
199
|
// to run just to fetch the error for this one file, we trap it locally with its native CLI frame isolation arguments!
|
|
@@ -242,6 +256,10 @@ async function healCommand(command, modelProvider, attempt = 1, healedFilePath =
|
|
|
242
256
|
status: 'error',
|
|
243
257
|
details: `Agent exhausted all ${maxRetries} attempts without success.\n\n` + generatedDetails
|
|
244
258
|
});
|
|
259
|
+
// Bug 6 fix: increment the global fail counter for this file so the 3-strike
|
|
260
|
+
// check at the top of the next iteration can catch it and skip it early.
|
|
261
|
+
failTracker[filePath] = (failTracker[filePath] || 0) + 1;
|
|
262
|
+
console.log(`[Lisa.ai Auto-Heal] Global fail counter for ${filePath}: ${failTracker[filePath]}/${GLOBAL_FAIL_THRESHOLD}`);
|
|
245
263
|
skipLedger.push(filePath);
|
|
246
264
|
// Advanced Path Neutralization for generic JS tests across the V2 Ecosystem Architecture
|
|
247
265
|
if (filePath.match(/\.(spec|test)\.(ts|js|tsx|jsx|vue)$/)) {
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,8 @@ const commander_1 = require("commander");
|
|
|
5
5
|
const config_service_1 = require("./services/config.service");
|
|
6
6
|
const heal_1 = require("./commands/heal");
|
|
7
7
|
const coverage_1 = require("./commands/coverage");
|
|
8
|
+
const pkg = require('../package.json');
|
|
8
9
|
function printBanner() {
|
|
9
|
-
const pkg = require('../package.json');
|
|
10
10
|
console.log(`\n======================================================`);
|
|
11
11
|
console.log(`✨ Lisa.ai Agent v${pkg.version} Running... `);
|
|
12
12
|
console.log(` Where pure magic happens! 🪄`);
|
|
@@ -16,7 +16,9 @@ const program = new commander_1.Command();
|
|
|
16
16
|
program
|
|
17
17
|
.name('lisa-agent')
|
|
18
18
|
.description('Lisa.ai - Autonomous CI/CD Platform Worker Agent')
|
|
19
|
-
|
|
19
|
+
// CP Bug 2 fix: was hardcoded as '1.0.0' while package.json (and the banner) show 2.1.x.
|
|
20
|
+
// `lisa-agent --version` now reports the real package version.
|
|
21
|
+
.version(pkg.version);
|
|
20
22
|
program
|
|
21
23
|
.command('heal')
|
|
22
24
|
.description('Run a command and autonomously heal errors')
|
|
@@ -29,21 +31,34 @@ program
|
|
|
29
31
|
let model = options.model;
|
|
30
32
|
let apiKey = undefined;
|
|
31
33
|
if (options.projectId) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// CP Bug 4 fix: fetchRemoteConfig now returns a discriminated union so we can
|
|
35
|
+
// tell the difference between "project not found" (→ hard exit, user must create
|
|
36
|
+
// the project in the Dashboard) and "control plane unreachable" (→ warn and fall
|
|
37
|
+
// back to local CLI defaults so the agent can still do its job offline).
|
|
38
|
+
const result = await (0, config_service_1.fetchRemoteConfig)(options.projectId);
|
|
39
|
+
if (!result.ok) {
|
|
40
|
+
if (result.reason === 'not_found') {
|
|
41
|
+
console.error(`\n🚨 [Lisa.ai Agent Error] Project '${options.projectId}' does not exist on the Control Plane. Please create it in the Dashboard first or run locally without a project ID.`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// 'unreachable' — control plane is temporarily down; continue with local defaults
|
|
46
|
+
console.warn(`\n⚠️ [Lisa.ai Warning] Control Plane is unreachable. Continuing with local CLI defaults (model=${model}, maxRetries=${maxRetries}).`);
|
|
47
|
+
}
|
|
36
48
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
console.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
else {
|
|
50
|
+
const config = result.config;
|
|
51
|
+
console.log(`[Lisa.ai Agent] Dynamic Config Loaded: Provider=${config.modelProvider}, MaxRetries=${config.maxRetries}`);
|
|
52
|
+
if (config.maxRetries < 5) {
|
|
53
|
+
console.warn(`\n⚠️ [Lisa.ai Warning] Your Dashboard Analytics Config has maxRetries set to ${config.maxRetries}. Consider increasing this to 5+ in the WEB UI for complex Angular Healing!`);
|
|
54
|
+
}
|
|
55
|
+
model = config.modelProvider;
|
|
56
|
+
maxRetries = config.maxRetries;
|
|
57
|
+
apiKey = config.apiKey;
|
|
58
|
+
if (config.autoHealEnabled === false) {
|
|
59
|
+
console.log(`[Lisa.ai Agent] Auto-heal is disabled by Control Plane.`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
47
62
|
}
|
|
48
63
|
}
|
|
49
64
|
await (0, heal_1.healCommand)(options.command, model, 1, null, maxRetries, options.projectId || 'local', undefined, apiKey);
|
|
@@ -60,18 +75,35 @@ program
|
|
|
60
75
|
let model = options.model;
|
|
61
76
|
let apiKey = undefined;
|
|
62
77
|
if (options.projectId) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
// CP Bug 4 fix: same discriminated-union handling as the heal command above.
|
|
79
|
+
const result = await (0, config_service_1.fetchRemoteConfig)(options.projectId);
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
if (result.reason === 'not_found') {
|
|
82
|
+
console.error(`\n🚨 [Lisa.ai Agent Error] Project '${options.projectId}' does not exist on the Control Plane. Please create it in the Dashboard first or run locally without a project ID.`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.warn(`\n⚠️ [Lisa.ai Warning] Control Plane is unreachable. Continuing with local CLI defaults (model=${model}, maxRetries=${maxRetries}).`);
|
|
87
|
+
}
|
|
67
88
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
console.
|
|
89
|
+
else {
|
|
90
|
+
const config = result.config;
|
|
91
|
+
console.log(`[Lisa.ai Agent] Dynamic Config Loaded: Provider=${config.modelProvider}, MaxRetries=${config.maxRetries}`);
|
|
92
|
+
if (config.maxRetries < 5) {
|
|
93
|
+
console.warn(`\n⚠️ [Lisa.ai Warning] Your Dashboard Analytics Config has maxRetries set to ${config.maxRetries}. Consider increasing this to 5+ in the WEB UI for complex Angular Healing!`);
|
|
94
|
+
}
|
|
95
|
+
model = config.modelProvider;
|
|
96
|
+
maxRetries = config.maxRetries;
|
|
97
|
+
apiKey = config.apiKey;
|
|
98
|
+
// CP Bug 1 fix: the coverage command was missing the autoHealEnabled check.
|
|
99
|
+
// Without this, a user who disables auto-heal on the control plane dashboard
|
|
100
|
+
// would still have their tests auto-healed internally via coverageCommand's
|
|
101
|
+
// fallback to healCommand when tests fail to compile.
|
|
102
|
+
if (config.autoHealEnabled === false) {
|
|
103
|
+
console.log(`[Lisa.ai Agent] Auto-heal is disabled by Control Plane.`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
71
106
|
}
|
|
72
|
-
model = config.modelProvider;
|
|
73
|
-
maxRetries = config.maxRetries;
|
|
74
|
-
apiKey = config.apiKey;
|
|
75
107
|
}
|
|
76
108
|
await (0, coverage_1.coverageCommand)(options.command, model, 1, maxRetries, options.projectId || 'local', apiKey);
|
|
77
109
|
});
|
|
@@ -1,22 +1,69 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.fetchRemoteConfig = fetchRemoteConfig;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const dotenv = __importStar(require("dotenv"));
|
|
39
|
+
// CP Bug 3 fix: load .env explicitly so LISA_CONTROL_PLANE_URL is always available
|
|
40
|
+
// regardless of module import order. Previously this relied on llm.service.ts being
|
|
41
|
+
// imported first (a fragile side-effect dependency).
|
|
42
|
+
dotenv.config({ path: path.resolve(__dirname, '../../.env'), quiet: true });
|
|
4
43
|
async function fetchRemoteConfig(projectId) {
|
|
5
44
|
const MASTER_CONTROL_URL = process.env.LISA_CONTROL_PLANE_URL || 'http://localhost:3000';
|
|
6
45
|
try {
|
|
7
46
|
const url = `${MASTER_CONTROL_URL}/api/config/${projectId}`;
|
|
8
47
|
console.log(`[Lisa.ai Agent] Fetching dynamic configuration from ${url}...`);
|
|
9
|
-
// Using native Node Fetch API available in Node 18+
|
|
10
48
|
const response = await fetch(url);
|
|
11
49
|
if (!response.ok) {
|
|
12
|
-
|
|
13
|
-
|
|
50
|
+
// 404 means the project was never created on the control plane.
|
|
51
|
+
// Other 4xx/5xx errors are treated as unreachable so the agent can fallback.
|
|
52
|
+
const reason = response.status === 404 ? 'not_found' : 'unreachable';
|
|
53
|
+
if (reason === 'not_found') {
|
|
54
|
+
console.warn(`[Lisa.ai Agent Warning] Control Plane returned 404. Project '${projectId}' not found.`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.warn(`[Lisa.ai Agent Warning] Control Plane returned ${response.status}. Falling back to local defaults.`);
|
|
58
|
+
}
|
|
59
|
+
return { ok: false, reason };
|
|
14
60
|
}
|
|
15
61
|
const config = await response.json();
|
|
16
|
-
return config;
|
|
62
|
+
return { ok: true, config };
|
|
17
63
|
}
|
|
18
64
|
catch (error) {
|
|
65
|
+
// Network-level failure — the control plane is unreachable, not "project not found".
|
|
19
66
|
console.warn(`[Lisa.ai Agent Warning] Failed to reach Control Plane (${MASTER_CONTROL_URL}). Using local fallback configuration.`);
|
|
20
|
-
return
|
|
67
|
+
return { ok: false, reason: 'unreachable' };
|
|
21
68
|
}
|
|
22
69
|
}
|
|
@@ -89,5 +89,53 @@ class AutoDiscoveryService {
|
|
|
89
89
|
}
|
|
90
90
|
return result;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Recursively crawls the project to find source files that lack any corresponding test/spec files.
|
|
94
|
+
*/
|
|
95
|
+
static findUntestedFiles(dir, skipList = []) {
|
|
96
|
+
const untested = [];
|
|
97
|
+
if (!fs.existsSync(dir))
|
|
98
|
+
return untested;
|
|
99
|
+
const files = fs.readdirSync(dir);
|
|
100
|
+
const ignoreDirs = ['node_modules', 'dist', 'build', '.git', '.angular', 'coverage', 'public', 'assets'];
|
|
101
|
+
const ignoreFiles = ['main.ts', 'index.ts', 'app.config.ts', 'app.routes.ts', 'styles.css', 'styles.scss', 'tailwind.config.js', 'eslint.config.js'];
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const fullPath = path.join(dir, file);
|
|
104
|
+
if (ignoreDirs.includes(file))
|
|
105
|
+
continue;
|
|
106
|
+
let stat;
|
|
107
|
+
try {
|
|
108
|
+
stat = fs.statSync(fullPath);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (stat.isDirectory()) {
|
|
114
|
+
untested.push(...this.findUntestedFiles(fullPath, skipList));
|
|
115
|
+
}
|
|
116
|
+
else if (file.match(/\.(ts|tsx|js|jsx|vue)$/) && !file.includes('.spec.') && !file.includes('.test.')) {
|
|
117
|
+
if (ignoreFiles.includes(file))
|
|
118
|
+
continue;
|
|
119
|
+
const ext = path.extname(file);
|
|
120
|
+
const base = file.slice(0, -ext.length);
|
|
121
|
+
const possibleSpecs = [
|
|
122
|
+
path.join(dir, `${base}.spec${ext}`),
|
|
123
|
+
path.join(dir, `${base}.test${ext}`),
|
|
124
|
+
path.join(dir, `${base}.spec.js`),
|
|
125
|
+
path.join(dir, `${base}.test.js`),
|
|
126
|
+
path.join(dir, `${base}.spec.ts`),
|
|
127
|
+
path.join(dir, `${base}.test.ts`)
|
|
128
|
+
];
|
|
129
|
+
const hasTest = possibleSpecs.some(p => fs.existsSync(p));
|
|
130
|
+
if (!hasTest) {
|
|
131
|
+
const relativePath = path.relative(process.cwd(), fullPath);
|
|
132
|
+
if (!skipList.includes(relativePath)) {
|
|
133
|
+
untested.push(relativePath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return untested;
|
|
139
|
+
}
|
|
92
140
|
}
|
|
93
141
|
exports.AutoDiscoveryService = AutoDiscoveryService;
|
|
@@ -13,6 +13,16 @@ async function createPullRequestForHeal(filePath) {
|
|
|
13
13
|
const timestamp = new Date().getTime();
|
|
14
14
|
const branchName = `lisa-fix/build-error-${timestamp}`;
|
|
15
15
|
const commitMsg = `fix: automated auto-heal by Lisa.ai for ${filePath}`;
|
|
16
|
+
// Bug 7 fix: capture the current branch BEFORE making any changes so we can reliably
|
|
17
|
+
// restore it in the finally block. Hardcoding 'main' would leave the repo stuck on a
|
|
18
|
+
// lisa-fix/ branch for any project that uses 'master' or a custom default branch.
|
|
19
|
+
let originalBranch = 'main'; // safe fallback
|
|
20
|
+
try {
|
|
21
|
+
originalBranch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim();
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
// If we can't determine the branch (e.g. detached HEAD), keep the fallback
|
|
25
|
+
}
|
|
16
26
|
try {
|
|
17
27
|
// 1. Git Config Setup
|
|
18
28
|
await git.addConfig('user.name', 'Lisa.ai');
|
|
@@ -43,7 +53,9 @@ This PR was automatically generated by Lisa.ai to resolve a failing compilation/
|
|
|
43
53
|
|
|
44
54
|
Please review the changes.`,
|
|
45
55
|
head: branchName,
|
|
46
|
-
|
|
56
|
+
// Bug 7 fix: use the branch that was active before we created the heal branch
|
|
57
|
+
// rather than the hardcoded 'main' string.
|
|
58
|
+
base: originalBranch,
|
|
47
59
|
});
|
|
48
60
|
console.log(`✅ [Lisa.ai PR Engine] Pull Request created successfully: ${pr.data.html_url}`);
|
|
49
61
|
}
|
|
@@ -55,9 +67,9 @@ Please review the changes.`,
|
|
|
55
67
|
console.error(`\n🚨 [Lisa.ai PR Engine Error] Failed to create Pull Request:`, error.message);
|
|
56
68
|
}
|
|
57
69
|
finally {
|
|
58
|
-
//
|
|
70
|
+
// Bug 7 fix: restore to the originally detected branch, not a hardcoded 'main'.
|
|
59
71
|
try {
|
|
60
|
-
await git.checkout(
|
|
72
|
+
await git.checkout(originalBranch);
|
|
61
73
|
}
|
|
62
74
|
catch (e) {
|
|
63
75
|
// Ignore checkout cleanup error
|
|
@@ -43,19 +43,22 @@ const anthropic_1 = require("@ai-sdk/anthropic");
|
|
|
43
43
|
const google_1 = require("@ai-sdk/google");
|
|
44
44
|
const path = __importStar(require("path"));
|
|
45
45
|
const dotenv = __importStar(require("dotenv"));
|
|
46
|
-
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|
46
|
+
dotenv.config({ path: path.resolve(__dirname, '../../.env'), quiet: true });
|
|
47
47
|
function getProvider(provider, apiKey) {
|
|
48
48
|
if (provider === 'claude') {
|
|
49
49
|
const key = apiKey || process.env.ANTHROPIC_API_KEY;
|
|
50
50
|
if (!key)
|
|
51
51
|
throw new Error('No Anthropic API key provided by local ENV or Control Plane');
|
|
52
|
-
|
|
52
|
+
// Bug 2 fix: claude-3-haiku is too weak for complex DI/module fixes.
|
|
53
|
+
// claude-3-5-sonnet is significantly better at understanding Angular/React test scaffolding.
|
|
54
|
+
return (0, anthropic_1.createAnthropic)({ apiKey: key })('claude-3-5-sonnet-20241022');
|
|
53
55
|
}
|
|
54
56
|
if (provider === 'openai') {
|
|
55
57
|
const key = apiKey || process.env.OPENAI_API_KEY;
|
|
56
58
|
if (!key)
|
|
57
59
|
throw new Error('No OpenAI API key provided by local ENV or Control Plane');
|
|
58
|
-
|
|
60
|
+
// Bug 2 fix: gpt-3.5-turbo is deprecated and produces low-quality code fixes.
|
|
61
|
+
return (0, openai_1.createOpenAI)({ apiKey: key })('gpt-4o-mini');
|
|
59
62
|
}
|
|
60
63
|
const key = apiKey || process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
|
61
64
|
if (!key)
|
|
@@ -105,19 +108,28 @@ async function askLisaForFix(filePath, fileContent, errorLog, modelProvider, api
|
|
|
105
108
|
model,
|
|
106
109
|
prompt,
|
|
107
110
|
});
|
|
108
|
-
|
|
111
|
+
// Bug 1 fix: handle javascript/js code blocks in addition to typescript/ts.
|
|
112
|
+
// Without this, JS file fixes from the LLM using ```javascript blocks are not extracted —
|
|
113
|
+
// the raw markdown (including ``` markers) gets written to disk, permanently corrupting the file.
|
|
114
|
+
const match = text.match(/```(?:typescript|ts|javascript|js)?\n([\s\S]*?)```/);
|
|
109
115
|
return match ? match[1].trim() : text.trim();
|
|
110
116
|
}
|
|
111
|
-
async function generateTestForFile(sourceFilePath, sourceFileContent, modelProvider, apiKey) {
|
|
117
|
+
async function generateTestForFile(sourceFilePath, sourceFileContent, modelProvider, apiKey, framework) {
|
|
112
118
|
console.log(`[Lisa.ai Coverage] Requesting test generation from ${modelProvider} for ${sourceFilePath}...`);
|
|
113
119
|
const model = getProvider(modelProvider, apiKey);
|
|
120
|
+
// Bug 3 fix: include the detected testing framework in the prompt so the LLM doesn't
|
|
121
|
+
// randomly pick Jest syntax for a Karma project (or vice-versa), causing immediate
|
|
122
|
+
// compile failures that trigger an unnecessary heal loop.
|
|
123
|
+
const frameworkInstruction = framework
|
|
124
|
+
? `3. You MUST use the '${framework}' testing framework exclusively. All imports, describe/it/test blocks, and mock utilities must follow '${framework}' conventions.\n`
|
|
125
|
+
: "3. Include all necessary imports assuming a standard testing framework (Jest/Karma/Vitest) is available.\n";
|
|
114
126
|
const prompt = "You are Lisa.ai, an autonomous CI/CD expert platform.\n" +
|
|
115
127
|
"A source file lacks 100% test coverage. Your task is to generate a comprehensive testing suite covering all branches, lines, and functions.\n\n" +
|
|
116
128
|
"--- Target File Content (" + sourceFilePath + ") ---\n" + sourceFileContent + "\n\n" +
|
|
117
129
|
"--- Constraints ---\n" +
|
|
118
130
|
"1. Return the generated test code wrapped in a markdown code block (```typescript ... ```).\n" +
|
|
119
131
|
"2. Do not include any explanation or intro text.\n" +
|
|
120
|
-
|
|
132
|
+
frameworkInstruction +
|
|
121
133
|
"4. Aim for 100% logic coverage.";
|
|
122
134
|
const { text } = await (0, ai_1.generateText)({
|
|
123
135
|
model,
|
|
@@ -126,9 +138,14 @@ async function generateTestForFile(sourceFilePath, sourceFileContent, modelProvi
|
|
|
126
138
|
const match = text.match(/```(?:typescript|ts|javascript|js)?\n([\s\S]*?)```/);
|
|
127
139
|
return match ? match[1].trim() : text.trim();
|
|
128
140
|
}
|
|
129
|
-
async function updateTestForFile(sourceFilePath, sourceFileContent, testFilePath, existingTestContent, modelProvider, apiKey) {
|
|
141
|
+
async function updateTestForFile(sourceFilePath, sourceFileContent, testFilePath, existingTestContent, modelProvider, apiKey, framework) {
|
|
130
142
|
console.log(`[Lisa.ai Coverage] Requesting test update from ${modelProvider} for ${sourceFilePath}...`);
|
|
131
143
|
const model = getProvider(modelProvider, apiKey);
|
|
144
|
+
// Bug 3 fix: same as generateTestForFile — specify the framework so updates
|
|
145
|
+
// don't introduce incompatible syntax into an existing test suite.
|
|
146
|
+
const frameworkInstruction = framework
|
|
147
|
+
? `3. You MUST use the '${framework}' testing framework exclusively. All new tests must follow '${framework}' conventions and integrate cleanly with the existing suite.\n`
|
|
148
|
+
: "3. Append missing tests to the existing suite. Do not delete existing passing tests unless they are fundamentally broken.\n";
|
|
132
149
|
const prompt = "You are Lisa.ai, an autonomous CI/CD expert platform.\n" +
|
|
133
150
|
"A source file lacks 100% test coverage. You must update its existing test suite to achieve full coverage.\n\n" +
|
|
134
151
|
"--- Target File Content (" + sourceFilePath + ") ---\n" + sourceFileContent + "\n\n" +
|
|
@@ -136,7 +153,7 @@ async function updateTestForFile(sourceFilePath, sourceFileContent, testFilePath
|
|
|
136
153
|
"--- Constraints ---\n" +
|
|
137
154
|
"1. Return the updated complete test code wrapped in a markdown code block (```typescript ... ```).\n" +
|
|
138
155
|
"2. Do not include any explanation or intro text.\n" +
|
|
139
|
-
|
|
156
|
+
frameworkInstruction +
|
|
140
157
|
"4. Aim for 100% logic coverage across branches, lines, and functions.";
|
|
141
158
|
const { text } = await (0, ai_1.generateText)({
|
|
142
159
|
model,
|
|
@@ -1,21 +1,60 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.reportTelemetry = reportTelemetry;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const dotenv = __importStar(require("dotenv"));
|
|
39
|
+
// CP Bug 3 fix: load .env explicitly so LISA_CONTROL_PLANE_URL is always available.
|
|
40
|
+
dotenv.config({ path: path.resolve(__dirname, '../../.env'), quiet: true });
|
|
4
41
|
async function reportTelemetry(event) {
|
|
5
42
|
const MASTER_CONTROL_URL = process.env.LISA_CONTROL_PLANE_URL || 'http://localhost:3000';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
43
|
+
const url = `${MASTER_CONTROL_URL}/api/telemetry`;
|
|
44
|
+
// Telemetry Bug 1 fix: true fire-and-forget — attach a .catch() handler and return immediately.
|
|
45
|
+
// The previous `await fetch(...)` contradicted the "don't block" comment: with 3 telemetry
|
|
46
|
+
// calls per file and 50+ files in a large project, the heal loop was silently waiting for
|
|
47
|
+
// 150+ network round-trips before doing any actual work.
|
|
48
|
+
// Keeping the function signature as async+Promise<void> means existing `await reportTelemetry()`
|
|
49
|
+
// callsites continue to compile without change — they just resolve instantly now.
|
|
50
|
+
fetch(url, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json'
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify(event)
|
|
56
|
+
}).catch((error) => {
|
|
18
57
|
// Silently fail if control plane is down so agent can still work detached
|
|
19
58
|
console.debug(`[Lisa.ai Agent Debug] Failed to report telemetry: ${error.message}`);
|
|
20
|
-
}
|
|
59
|
+
});
|
|
21
60
|
}
|
package/dist/utils/parser.js
CHANGED
|
@@ -49,6 +49,11 @@ function extractFilePath(errorLog, skipFiles = [], searchDir = process.cwd()) {
|
|
|
49
49
|
while ((match = exactErrorRegex.exec(cleanLog)) !== null) {
|
|
50
50
|
const foundPath = match[1];
|
|
51
51
|
if (foundPath) {
|
|
52
|
+
// Bug 4 fix: skip paths that live inside node_modules or build output directories.
|
|
53
|
+
// Without this check a non-scoped package path (e.g. node_modules/jest-config/build/index.js)
|
|
54
|
+
// satisfies the regex AND exists on disk, so Lisa would attempt to "heal" a library file.
|
|
55
|
+
if (/[/\\](node_modules|dist|build)[/\\]/.test(foundPath))
|
|
56
|
+
continue;
|
|
52
57
|
const absoluteFoundPath = path.isAbsolute(foundPath) ? foundPath : path.resolve(searchDir, foundPath);
|
|
53
58
|
if (!normalizedSkips.includes(absoluteFoundPath) && fs.existsSync(absoluteFoundPath)) {
|
|
54
59
|
return foundPath;
|
|
@@ -72,6 +77,9 @@ function extractFilePath(errorLog, skipFiles = [], searchDir = process.cwd()) {
|
|
|
72
77
|
while ((fallbackMatch = fallbackRegex.exec(cleanLog)) !== null) {
|
|
73
78
|
const foundPath = fallbackMatch[1];
|
|
74
79
|
if (foundPath) {
|
|
80
|
+
// Bug 4 fix: same node_modules/dist guard as pass 1.
|
|
81
|
+
if (/[/\\](node_modules|dist|build)[/\\]/.test(foundPath))
|
|
82
|
+
continue;
|
|
75
83
|
const absoluteFoundPath = path.isAbsolute(foundPath) ? foundPath : path.resolve(searchDir, foundPath);
|
|
76
84
|
if (!normalizedSkips.includes(absoluteFoundPath) && fs.existsSync(absoluteFoundPath)) {
|
|
77
85
|
return foundPath;
|