@triedotdev/mcp 1.0.109 → 1.0.111
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/{chunk-OTTR5JX4.js → chunk-6QR6QZIX.js} +70 -1
- package/dist/chunk-6QR6QZIX.js.map +1 -0
- package/dist/{chunk-G6EC4JNL.js → chunk-HGEKZ2VS.js} +2919 -355
- package/dist/chunk-HGEKZ2VS.js.map +1 -0
- package/dist/chunk-QYOACM2C.js +1923 -0
- package/dist/chunk-QYOACM2C.js.map +1 -0
- package/dist/{chunk-SUHYYM2J.js → chunk-SDS3UVFY.js} +2 -2
- package/dist/chunk-TKMV7JKN.js +1562 -0
- package/dist/chunk-TKMV7JKN.js.map +1 -0
- package/dist/cli/main.js +320 -57
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/yolo-daemon.js +4 -4
- package/dist/{guardian-agent-4UJN5QGX.js → guardian-agent-XEYNG7RH.js} +3 -3
- package/dist/index.js +357 -2593
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-FLBK5ILJ.js +0 -3729
- package/dist/chunk-FLBK5ILJ.js.map +0 -1
- package/dist/chunk-G6EC4JNL.js.map +0 -1
- package/dist/chunk-HIKONDDO.js +0 -26
- package/dist/chunk-HIKONDDO.js.map +0 -1
- package/dist/chunk-OTTR5JX4.js.map +0 -1
- /package/dist/{chunk-SUHYYM2J.js.map → chunk-SDS3UVFY.js.map} +0 -0
- /package/dist/{guardian-agent-4UJN5QGX.js.map → guardian-agent-XEYNG7RH.js.map} +0 -0
package/dist/chunk-FLBK5ILJ.js
DELETED
|
@@ -1,3729 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ContextGraph
|
|
3
|
-
} from "./chunk-OTTR5JX4.js";
|
|
4
|
-
import {
|
|
5
|
-
scanForVibeCodeIssues
|
|
6
|
-
} from "./chunk-IXO4G4D3.js";
|
|
7
|
-
import {
|
|
8
|
-
storeIssues
|
|
9
|
-
} from "./chunk-6JPPYG7F.js";
|
|
10
|
-
import {
|
|
11
|
-
getTrieDirectory,
|
|
12
|
-
getWorkingDirectory
|
|
13
|
-
} from "./chunk-R4AAPFXC.js";
|
|
14
|
-
import {
|
|
15
|
-
scanForVulnerabilities
|
|
16
|
-
} from "./chunk-F4NJ4CBP.js";
|
|
17
|
-
import {
|
|
18
|
-
Trie
|
|
19
|
-
} from "./chunk-6NLHFIYA.js";
|
|
20
|
-
import {
|
|
21
|
-
isInteractiveMode
|
|
22
|
-
} from "./chunk-APMV77PU.js";
|
|
23
|
-
import {
|
|
24
|
-
__require
|
|
25
|
-
} from "./chunk-DGUM43GV.js";
|
|
26
|
-
|
|
27
|
-
// src/utils/project-info.ts
|
|
28
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
29
|
-
import { existsSync } from "fs";
|
|
30
|
-
import { join } from "path";
|
|
31
|
-
var PROJECT_MD_PATH = "PROJECT.md";
|
|
32
|
-
function getProjectTemplate() {
|
|
33
|
-
return `# Project Information
|
|
34
|
-
|
|
35
|
-
> This file stores important project context for AI assistants.
|
|
36
|
-
> Edit freely - this file is yours, not auto-generated.
|
|
37
|
-
> Available via MCP resource: \`trie://project\`
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## Project Overview
|
|
42
|
-
|
|
43
|
-
<!-- Describe your project's purpose and goals -->
|
|
44
|
-
|
|
45
|
-
[Add project description here]
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
## Technology Stack
|
|
50
|
-
|
|
51
|
-
<!-- List frameworks, languages, databases, cloud services, etc. -->
|
|
52
|
-
|
|
53
|
-
- **Language:**
|
|
54
|
-
- **Framework:**
|
|
55
|
-
- **Database:**
|
|
56
|
-
- **Hosting:**
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## Architecture
|
|
61
|
-
|
|
62
|
-
<!-- Key patterns, architectural decisions, and system design -->
|
|
63
|
-
|
|
64
|
-
[Describe your architecture here]
|
|
65
|
-
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
## Coding Conventions
|
|
69
|
-
|
|
70
|
-
<!-- Style guidelines, naming conventions, patterns to follow -->
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
## Environment
|
|
79
|
-
|
|
80
|
-
<!-- URLs, API endpoints, deployment info -->
|
|
81
|
-
|
|
82
|
-
| Environment | URL | Notes |
|
|
83
|
-
|-------------|-----|-------|
|
|
84
|
-
| Development | | |
|
|
85
|
-
| Staging | | |
|
|
86
|
-
| Production | | |
|
|
87
|
-
|
|
88
|
-
---
|
|
89
|
-
|
|
90
|
-
## Team
|
|
91
|
-
|
|
92
|
-
<!-- Ownership, contacts, responsibilities -->
|
|
93
|
-
|
|
94
|
-
- **Owner:**
|
|
95
|
-
- **Team:**
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## Compliance
|
|
100
|
-
|
|
101
|
-
<!-- HIPAA, SOC2, GDPR, PCI-DSS requirements if applicable -->
|
|
102
|
-
|
|
103
|
-
- [ ] GDPR
|
|
104
|
-
- [ ] SOC2
|
|
105
|
-
- [ ] HIPAA
|
|
106
|
-
- [ ] PCI-DSS
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## AI Instructions
|
|
111
|
-
|
|
112
|
-
<!-- Special instructions for AI assistants working on this project -->
|
|
113
|
-
|
|
114
|
-
When working on this project, AI assistants should:
|
|
115
|
-
|
|
116
|
-
1.
|
|
117
|
-
2.
|
|
118
|
-
3.
|
|
119
|
-
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
*This file is read by Trie agents and exposed via \`trie://project\` MCP resource.*
|
|
123
|
-
*Edit this file to provide context to Claude Code, Cursor, GitHub Actions, and other AI tools.*
|
|
124
|
-
`;
|
|
125
|
-
}
|
|
126
|
-
function projectInfoExists(workDir) {
|
|
127
|
-
const dir = workDir || getWorkingDirectory(void 0, true);
|
|
128
|
-
const projectPath = join(getTrieDirectory(dir), PROJECT_MD_PATH);
|
|
129
|
-
return existsSync(projectPath);
|
|
130
|
-
}
|
|
131
|
-
async function loadProjectInfo(workDir) {
|
|
132
|
-
const dir = workDir || getWorkingDirectory(void 0, true);
|
|
133
|
-
const projectPath = join(getTrieDirectory(dir), PROJECT_MD_PATH);
|
|
134
|
-
try {
|
|
135
|
-
if (!existsSync(projectPath)) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
return await readFile(projectPath, "utf-8");
|
|
139
|
-
} catch {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
async function saveProjectInfo(content, workDir) {
|
|
144
|
-
const dir = workDir || getWorkingDirectory(void 0, true);
|
|
145
|
-
const trieDir = getTrieDirectory(dir);
|
|
146
|
-
const projectPath = join(trieDir, PROJECT_MD_PATH);
|
|
147
|
-
await mkdir(trieDir, { recursive: true });
|
|
148
|
-
await writeFile(projectPath, content, "utf-8");
|
|
149
|
-
}
|
|
150
|
-
async function initProjectInfo(workDir) {
|
|
151
|
-
const dir = workDir || getWorkingDirectory(void 0, true);
|
|
152
|
-
const projectPath = join(getTrieDirectory(dir), PROJECT_MD_PATH);
|
|
153
|
-
if (existsSync(projectPath)) {
|
|
154
|
-
return { created: false, path: projectPath };
|
|
155
|
-
}
|
|
156
|
-
await saveProjectInfo(getProjectTemplate(), dir);
|
|
157
|
-
return { created: true, path: projectPath };
|
|
158
|
-
}
|
|
159
|
-
async function getProjectSection(sectionName, workDir) {
|
|
160
|
-
const content = await loadProjectInfo(workDir);
|
|
161
|
-
if (!content) return null;
|
|
162
|
-
const sectionRegex = new RegExp(
|
|
163
|
-
`## ${escapeRegex(sectionName)}\\s*\\n([\\s\\S]*?)(?=\\n## |\\n---\\s*$|$)`,
|
|
164
|
-
"i"
|
|
165
|
-
);
|
|
166
|
-
const match = content.match(sectionRegex);
|
|
167
|
-
if (match?.[1]) {
|
|
168
|
-
return match[1].trim();
|
|
169
|
-
}
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
async function updateProjectSection(sectionName, newContent, workDir) {
|
|
173
|
-
let content = await loadProjectInfo(workDir);
|
|
174
|
-
if (!content) {
|
|
175
|
-
await initProjectInfo(workDir);
|
|
176
|
-
content = await loadProjectInfo(workDir);
|
|
177
|
-
if (!content) return false;
|
|
178
|
-
}
|
|
179
|
-
const sectionRegex = new RegExp(
|
|
180
|
-
`(## ${escapeRegex(sectionName)}\\s*\\n)([\\s\\S]*?)((?=\\n## )|(?=\\n---\\s*$)|$)`,
|
|
181
|
-
"i"
|
|
182
|
-
);
|
|
183
|
-
if (content.match(sectionRegex)) {
|
|
184
|
-
const updatedContent = content.replace(sectionRegex, `$1
|
|
185
|
-
${newContent}
|
|
186
|
-
|
|
187
|
-
$3`);
|
|
188
|
-
await saveProjectInfo(updatedContent, workDir);
|
|
189
|
-
return true;
|
|
190
|
-
}
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
async function appendToSection(sectionName, contentToAdd, workDir) {
|
|
194
|
-
const currentContent = await getProjectSection(sectionName, workDir);
|
|
195
|
-
if (currentContent === null) return false;
|
|
196
|
-
const newContent = currentContent + "\n" + contentToAdd;
|
|
197
|
-
return updateProjectSection(sectionName, newContent, workDir);
|
|
198
|
-
}
|
|
199
|
-
async function getProjectSections(workDir) {
|
|
200
|
-
const content = await loadProjectInfo(workDir);
|
|
201
|
-
if (!content) return [];
|
|
202
|
-
const sectionRegex = /^## (.+)$/gm;
|
|
203
|
-
const sections = [];
|
|
204
|
-
let match;
|
|
205
|
-
while ((match = sectionRegex.exec(content)) !== null) {
|
|
206
|
-
if (match[1]) {
|
|
207
|
-
sections.push(match[1].trim());
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return sections;
|
|
211
|
-
}
|
|
212
|
-
async function getProjectInfoStructured(workDir) {
|
|
213
|
-
const dir = workDir || getWorkingDirectory(void 0, true);
|
|
214
|
-
const projectPath = join(getTrieDirectory(dir), PROJECT_MD_PATH);
|
|
215
|
-
const content = await loadProjectInfo(dir);
|
|
216
|
-
if (!content) {
|
|
217
|
-
return {
|
|
218
|
-
exists: false,
|
|
219
|
-
path: projectPath,
|
|
220
|
-
sections: {},
|
|
221
|
-
raw: null
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
const sectionNames = await getProjectSections(dir);
|
|
225
|
-
const sections = {};
|
|
226
|
-
for (const name of sectionNames) {
|
|
227
|
-
const sectionContent = await getProjectSection(name, dir);
|
|
228
|
-
if (sectionContent) {
|
|
229
|
-
sections[name] = sectionContent;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return {
|
|
233
|
-
exists: true,
|
|
234
|
-
path: projectPath,
|
|
235
|
-
sections,
|
|
236
|
-
raw: content
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
function escapeRegex(str) {
|
|
240
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// src/cli/checkpoint.ts
|
|
244
|
-
import { existsSync as existsSync2 } from "fs";
|
|
245
|
-
import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile2 } from "fs/promises";
|
|
246
|
-
import { join as join2 } from "path";
|
|
247
|
-
async function saveCheckpoint(options) {
|
|
248
|
-
const workDir = options.workDir || getWorkingDirectory(void 0, true);
|
|
249
|
-
const trieDir = getTrieDirectory(workDir);
|
|
250
|
-
const checkpointPath = join2(trieDir, "checkpoints.json");
|
|
251
|
-
await mkdir2(trieDir, { recursive: true });
|
|
252
|
-
let log = { checkpoints: [] };
|
|
253
|
-
try {
|
|
254
|
-
if (existsSync2(checkpointPath)) {
|
|
255
|
-
log = JSON.parse(await readFile2(checkpointPath, "utf-8"));
|
|
256
|
-
}
|
|
257
|
-
} catch {
|
|
258
|
-
log = { checkpoints: [] };
|
|
259
|
-
}
|
|
260
|
-
const checkpoint = {
|
|
261
|
-
id: `cp-${Date.now().toString(36)}`,
|
|
262
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
263
|
-
files: options.files || [],
|
|
264
|
-
createdBy: options.createdBy || "cli"
|
|
265
|
-
};
|
|
266
|
-
if (options.message) checkpoint.message = options.message;
|
|
267
|
-
if (options.notes) checkpoint.notes = options.notes;
|
|
268
|
-
log.checkpoints.push(checkpoint);
|
|
269
|
-
log.lastCheckpoint = checkpoint.id;
|
|
270
|
-
if (log.checkpoints.length > 50) {
|
|
271
|
-
log.checkpoints = log.checkpoints.slice(-50);
|
|
272
|
-
}
|
|
273
|
-
await writeFile2(checkpointPath, JSON.stringify(log, null, 2));
|
|
274
|
-
await updateAgentsMdWithCheckpoint(checkpoint, workDir);
|
|
275
|
-
return checkpoint;
|
|
276
|
-
}
|
|
277
|
-
async function listCheckpoints(workDir) {
|
|
278
|
-
const dir = workDir || getWorkingDirectory(void 0, true);
|
|
279
|
-
const checkpointPath = join2(getTrieDirectory(dir), "checkpoints.json");
|
|
280
|
-
try {
|
|
281
|
-
if (existsSync2(checkpointPath)) {
|
|
282
|
-
const log = JSON.parse(await readFile2(checkpointPath, "utf-8"));
|
|
283
|
-
return log.checkpoints;
|
|
284
|
-
}
|
|
285
|
-
} catch {
|
|
286
|
-
}
|
|
287
|
-
return [];
|
|
288
|
-
}
|
|
289
|
-
async function getLastCheckpoint(workDir) {
|
|
290
|
-
const checkpoints = await listCheckpoints(workDir);
|
|
291
|
-
const last = checkpoints[checkpoints.length - 1];
|
|
292
|
-
return last ?? null;
|
|
293
|
-
}
|
|
294
|
-
async function updateAgentsMdWithCheckpoint(checkpoint, workDir) {
|
|
295
|
-
const agentsPath = join2(getTrieDirectory(workDir), "AGENTS.md");
|
|
296
|
-
let content = "";
|
|
297
|
-
try {
|
|
298
|
-
if (existsSync2(agentsPath)) {
|
|
299
|
-
content = await readFile2(agentsPath, "utf-8");
|
|
300
|
-
}
|
|
301
|
-
} catch {
|
|
302
|
-
content = "";
|
|
303
|
-
}
|
|
304
|
-
const checkpointSection = `
|
|
305
|
-
## Last Checkpoint
|
|
306
|
-
|
|
307
|
-
- **ID:** ${checkpoint.id}
|
|
308
|
-
- **Time:** ${checkpoint.timestamp}
|
|
309
|
-
${checkpoint.message ? `- **Message:** ${checkpoint.message}` : ""}
|
|
310
|
-
${checkpoint.files.length > 0 ? `- **Files:** ${checkpoint.files.length} files` : ""}
|
|
311
|
-
${checkpoint.notes ? `- **Notes:** ${checkpoint.notes}` : ""}
|
|
312
|
-
`;
|
|
313
|
-
const checkpointRegex = /## Last Checkpoint[\s\S]*?(?=\n## |\n---|\Z)/;
|
|
314
|
-
if (checkpointRegex.test(content)) {
|
|
315
|
-
content = content.replace(checkpointRegex, checkpointSection.trim());
|
|
316
|
-
} else {
|
|
317
|
-
content = content.trim() + "\n\n" + checkpointSection.trim() + "\n";
|
|
318
|
-
}
|
|
319
|
-
await writeFile2(agentsPath, content);
|
|
320
|
-
}
|
|
321
|
-
async function handleCheckpointCommand(args) {
|
|
322
|
-
const subcommand = args[0] || "save";
|
|
323
|
-
switch (subcommand) {
|
|
324
|
-
case "save": {
|
|
325
|
-
let message;
|
|
326
|
-
let notes;
|
|
327
|
-
const files = [];
|
|
328
|
-
for (let i = 1; i < args.length; i++) {
|
|
329
|
-
const arg = args[i];
|
|
330
|
-
if (arg === "-m" || arg === "--message") {
|
|
331
|
-
message = args[++i] ?? "";
|
|
332
|
-
} else if (arg === "-n" || arg === "--notes") {
|
|
333
|
-
notes = args[++i] ?? "";
|
|
334
|
-
} else if (arg === "-f" || arg === "--file") {
|
|
335
|
-
const file = args[++i];
|
|
336
|
-
if (file) files.push(file);
|
|
337
|
-
} else if (arg && !arg.startsWith("-")) {
|
|
338
|
-
message = arg;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
const opts = { files };
|
|
342
|
-
if (message) opts.message = message;
|
|
343
|
-
if (notes) opts.notes = notes;
|
|
344
|
-
const checkpoint = await saveCheckpoint(opts);
|
|
345
|
-
console.log("\n Checkpoint saved\n");
|
|
346
|
-
console.log(` ID: ${checkpoint.id}`);
|
|
347
|
-
console.log(` Time: ${checkpoint.timestamp}`);
|
|
348
|
-
if (checkpoint.message) {
|
|
349
|
-
console.log(` Message: ${checkpoint.message}`);
|
|
350
|
-
}
|
|
351
|
-
if (checkpoint.files.length > 0) {
|
|
352
|
-
console.log(` Files: ${checkpoint.files.join(", ")}`);
|
|
353
|
-
}
|
|
354
|
-
console.log("\n Context saved to .trie/\n");
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
357
|
-
case "list": {
|
|
358
|
-
const checkpoints = await listCheckpoints();
|
|
359
|
-
if (checkpoints.length === 0) {
|
|
360
|
-
console.log("\n No checkpoints yet. Run `trie checkpoint` to save one.\n");
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
console.log("\n Checkpoints:\n");
|
|
364
|
-
for (const cp of checkpoints.slice(-10).reverse()) {
|
|
365
|
-
const date = new Date(cp.timestamp).toLocaleString();
|
|
366
|
-
console.log(` ${cp.id} ${date} ${cp.message || "(no message)"}`);
|
|
367
|
-
}
|
|
368
|
-
console.log("");
|
|
369
|
-
break;
|
|
370
|
-
}
|
|
371
|
-
case "last": {
|
|
372
|
-
const checkpoint = await getLastCheckpoint();
|
|
373
|
-
if (!checkpoint) {
|
|
374
|
-
console.log("\n No checkpoints yet. Run `trie checkpoint` to save one.\n");
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
console.log("\n Last Checkpoint:\n");
|
|
378
|
-
console.log(` ID: ${checkpoint.id}`);
|
|
379
|
-
console.log(` Time: ${new Date(checkpoint.timestamp).toLocaleString()}`);
|
|
380
|
-
if (checkpoint.message) {
|
|
381
|
-
console.log(` Message: ${checkpoint.message}`);
|
|
382
|
-
}
|
|
383
|
-
if (checkpoint.notes) {
|
|
384
|
-
console.log(` Notes: ${checkpoint.notes}`);
|
|
385
|
-
}
|
|
386
|
-
if (checkpoint.files.length > 0) {
|
|
387
|
-
console.log(` Files: ${checkpoint.files.join(", ")}`);
|
|
388
|
-
}
|
|
389
|
-
console.log("");
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
default:
|
|
393
|
-
console.log(`
|
|
394
|
-
Usage: trie checkpoint [command] [options]
|
|
395
|
-
|
|
396
|
-
Commands:
|
|
397
|
-
save [message] Save a checkpoint (default)
|
|
398
|
-
list List recent checkpoints
|
|
399
|
-
last Show the last checkpoint
|
|
400
|
-
|
|
401
|
-
Options:
|
|
402
|
-
-m, --message Checkpoint message
|
|
403
|
-
-n, --notes Additional notes
|
|
404
|
-
-f, --file File to include (can be repeated)
|
|
405
|
-
|
|
406
|
-
Examples:
|
|
407
|
-
trie checkpoint "finished auth flow"
|
|
408
|
-
trie checkpoint save -m "WIP: payment integration" -n "needs testing"
|
|
409
|
-
trie checkpoint list
|
|
410
|
-
`);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// src/utils/context-state.ts
|
|
415
|
-
import { readFile as readFile3, mkdir as mkdir3 } from "fs/promises";
|
|
416
|
-
import { existsSync as existsSync3 } from "fs";
|
|
417
|
-
import { join as join3, basename } from "path";
|
|
418
|
-
var STATE_JSON_PATH = "state.json";
|
|
419
|
-
async function loadContextState() {
|
|
420
|
-
const workDir = getWorkingDirectory(void 0, true);
|
|
421
|
-
const statePath = join3(getTrieDirectory(workDir), STATE_JSON_PATH);
|
|
422
|
-
const defaults = getDefaultState();
|
|
423
|
-
try {
|
|
424
|
-
if (existsSync3(statePath)) {
|
|
425
|
-
const content = await readFile3(statePath, "utf-8");
|
|
426
|
-
const loaded = JSON.parse(content);
|
|
427
|
-
return {
|
|
428
|
-
...defaults,
|
|
429
|
-
...loaded,
|
|
430
|
-
skills: loaded.skills || defaults.skills
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
} catch {
|
|
434
|
-
}
|
|
435
|
-
return defaults;
|
|
436
|
-
}
|
|
437
|
-
function getDefaultState() {
|
|
438
|
-
return {
|
|
439
|
-
lastScan: null,
|
|
440
|
-
healthScore: 0,
|
|
441
|
-
activePriorities: [
|
|
442
|
-
"Initial setup required - run first scan with `trie scan`",
|
|
443
|
-
"Configure agents in `.trie/config.json`",
|
|
444
|
-
"Set up CI/CD integration"
|
|
445
|
-
],
|
|
446
|
-
contextSignals: {},
|
|
447
|
-
agentStatus: {},
|
|
448
|
-
scanHistory: [],
|
|
449
|
-
customAgents: [],
|
|
450
|
-
skills: {},
|
|
451
|
-
environment: detectEnvironment()
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
function detectEnvironment() {
|
|
455
|
-
if (process.env.GITHUB_ACTIONS) return "github-actions";
|
|
456
|
-
if (process.env.GITLAB_CI) return "gitlab-ci";
|
|
457
|
-
if (process.env.CI) return "ci";
|
|
458
|
-
const parent = process.env._ || "";
|
|
459
|
-
if (parent.includes("cursor")) return "cursor";
|
|
460
|
-
if (parent.includes("claude")) return "claude-code";
|
|
461
|
-
return "cli";
|
|
462
|
-
}
|
|
463
|
-
async function getContextForAI() {
|
|
464
|
-
const state = await loadContextState();
|
|
465
|
-
const workDir = getWorkingDirectory(void 0, true);
|
|
466
|
-
const lines = [];
|
|
467
|
-
if (projectInfoExists(workDir)) {
|
|
468
|
-
const projectInfo = await loadProjectInfo(workDir);
|
|
469
|
-
if (projectInfo) {
|
|
470
|
-
lines.push(projectInfo);
|
|
471
|
-
lines.push("");
|
|
472
|
-
lines.push("---");
|
|
473
|
-
lines.push("");
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
lines.push(
|
|
477
|
-
"## Trie Scan Context",
|
|
478
|
-
"",
|
|
479
|
-
`**Health Score:** ${state.healthScore}%`,
|
|
480
|
-
`**Last Scan:** ${state.lastScan ? new Date(state.lastScan.timestamp).toLocaleString() : "Never"}`,
|
|
481
|
-
"",
|
|
482
|
-
"**Active Priorities:**",
|
|
483
|
-
...state.activePriorities.map((p) => `- ${p}`),
|
|
484
|
-
""
|
|
485
|
-
);
|
|
486
|
-
if (state.lastScan) {
|
|
487
|
-
lines.push(
|
|
488
|
-
"**Recent Issues:**",
|
|
489
|
-
`- Critical: ${state.lastScan.issues.critical}`,
|
|
490
|
-
`- Serious: ${state.lastScan.issues.serious}`,
|
|
491
|
-
`- Moderate: ${state.lastScan.issues.moderate}`,
|
|
492
|
-
`- Low: ${state.lastScan.issues.low}`,
|
|
493
|
-
""
|
|
494
|
-
);
|
|
495
|
-
if (state.lastScan.hotFiles.length > 0) {
|
|
496
|
-
lines.push(
|
|
497
|
-
"**Hot Files (most issues):**",
|
|
498
|
-
...state.lastScan.hotFiles.slice(0, 5).map((f) => `- ${f.file}: ${f.issueCount} issues`),
|
|
499
|
-
""
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
return lines.join("\n");
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// src/config/loader.ts
|
|
507
|
-
import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
|
|
508
|
-
import { existsSync as existsSync5 } from "fs";
|
|
509
|
-
import { join as join5, dirname } from "path";
|
|
510
|
-
|
|
511
|
-
// src/config/validation.ts
|
|
512
|
-
import { z } from "zod";
|
|
513
|
-
import { existsSync as existsSync4, readFileSync } from "fs";
|
|
514
|
-
import { resolve, join as join4 } from "path";
|
|
515
|
-
var API_KEY_PATTERNS = {
|
|
516
|
-
anthropic: /^sk-ant-api\d{2}-[\w-]{95}$/,
|
|
517
|
-
openai: /^sk-[\w]{48}$/,
|
|
518
|
-
github: /^ghp_[\w]{36}$/,
|
|
519
|
-
vercel: /^[\w]{24}$/,
|
|
520
|
-
linear: /^lin_api_[\w]{40,60}$/
|
|
521
|
-
};
|
|
522
|
-
var ApiKeysSchema = z.object({
|
|
523
|
-
anthropic: z.string().regex(API_KEY_PATTERNS.anthropic, "Invalid Anthropic API key format").optional(),
|
|
524
|
-
openai: z.string().regex(API_KEY_PATTERNS.openai, "Invalid OpenAI API key format").optional(),
|
|
525
|
-
github: z.string().regex(API_KEY_PATTERNS.github, "Invalid GitHub token format").optional(),
|
|
526
|
-
vercel: z.string().regex(API_KEY_PATTERNS.vercel, "Invalid Vercel token format").optional(),
|
|
527
|
-
linear: z.string().optional()
|
|
528
|
-
// Linear keys can vary, so we'll be flexible but allow storage
|
|
529
|
-
});
|
|
530
|
-
var AgentConfigSchema = z.object({
|
|
531
|
-
enabled: z.array(z.string()).optional().default([]),
|
|
532
|
-
disabled: z.array(z.string()).optional().default([]),
|
|
533
|
-
parallel: z.boolean().optional().default(true),
|
|
534
|
-
maxConcurrency: z.number().int().min(1).max(20).optional().default(4),
|
|
535
|
-
timeout: z.number().int().min(1e3).max(3e5).optional().default(12e4),
|
|
536
|
-
// 2 minutes
|
|
537
|
-
cache: z.boolean().optional().default(true)
|
|
538
|
-
});
|
|
539
|
-
var ComplianceSchema = z.object({
|
|
540
|
-
standards: z.array(z.enum(["SOC2", "GDPR", "HIPAA", "CCPA", "PCI-DSS"])).optional().default(["SOC2"]),
|
|
541
|
-
enforceCompliance: z.boolean().optional().default(false),
|
|
542
|
-
reportFormat: z.enum(["json", "sarif", "csv", "html"]).optional().default("json")
|
|
543
|
-
});
|
|
544
|
-
var OutputSchema = z.object({
|
|
545
|
-
format: z.enum(["console", "json", "sarif", "junit"]).optional().default("console"),
|
|
546
|
-
level: z.enum(["critical", "serious", "moderate", "low", "all"]).optional().default("all"),
|
|
547
|
-
interactive: z.boolean().optional().default(false),
|
|
548
|
-
streaming: z.boolean().optional().default(true),
|
|
549
|
-
colors: z.boolean().optional().default(true)
|
|
550
|
-
});
|
|
551
|
-
var PathsSchema = z.object({
|
|
552
|
-
include: z.array(z.string()).optional().default([]),
|
|
553
|
-
exclude: z.array(z.string()).optional().default(["node_modules", "dist", "build", ".git"]),
|
|
554
|
-
configDir: z.string().optional().default(".trie"),
|
|
555
|
-
outputDir: z.string().optional().default("trie-reports")
|
|
556
|
-
});
|
|
557
|
-
var IntegrationsSchema = z.object({
|
|
558
|
-
github: z.object({
|
|
559
|
-
enabled: z.boolean().optional().default(false),
|
|
560
|
-
token: z.string().optional(),
|
|
561
|
-
webhook: z.string().url().optional()
|
|
562
|
-
}).optional(),
|
|
563
|
-
slack: z.object({
|
|
564
|
-
enabled: z.boolean().optional().default(false),
|
|
565
|
-
webhook: z.string().url().optional(),
|
|
566
|
-
channel: z.string().optional()
|
|
567
|
-
}).optional(),
|
|
568
|
-
jira: z.object({
|
|
569
|
-
enabled: z.boolean().optional().default(false),
|
|
570
|
-
url: z.string().url().optional(),
|
|
571
|
-
token: z.string().optional(),
|
|
572
|
-
project: z.string().optional()
|
|
573
|
-
}).optional()
|
|
574
|
-
});
|
|
575
|
-
var UserSchema = z.object({
|
|
576
|
-
name: z.string().min(1).optional(),
|
|
577
|
-
email: z.string().email().optional(),
|
|
578
|
-
role: z.enum([
|
|
579
|
-
"developer",
|
|
580
|
-
"designer",
|
|
581
|
-
"qa",
|
|
582
|
-
"devops",
|
|
583
|
-
"security",
|
|
584
|
-
"architect",
|
|
585
|
-
"manager",
|
|
586
|
-
"contributor"
|
|
587
|
-
]).optional().default("developer"),
|
|
588
|
-
github: z.string().optional(),
|
|
589
|
-
// GitHub username
|
|
590
|
-
url: z.string().url().optional()
|
|
591
|
-
// Personal/portfolio URL
|
|
592
|
-
});
|
|
593
|
-
var TrieConfigSchema = z.object({
|
|
594
|
-
version: z.string().optional().default("1.0.0"),
|
|
595
|
-
apiKeys: ApiKeysSchema.optional(),
|
|
596
|
-
agents: AgentConfigSchema.optional(),
|
|
597
|
-
compliance: ComplianceSchema.optional(),
|
|
598
|
-
output: OutputSchema.optional(),
|
|
599
|
-
paths: PathsSchema.optional(),
|
|
600
|
-
integrations: IntegrationsSchema.optional(),
|
|
601
|
-
user: UserSchema.optional()
|
|
602
|
-
// User identity for attribution
|
|
603
|
-
});
|
|
604
|
-
var ConfigValidator = class {
|
|
605
|
-
/**
|
|
606
|
-
* Validate configuration object
|
|
607
|
-
*/
|
|
608
|
-
validateConfig(config) {
|
|
609
|
-
try {
|
|
610
|
-
const validated = TrieConfigSchema.parse(config);
|
|
611
|
-
const businessErrors = this.validateBusinessLogic(validated);
|
|
612
|
-
if (businessErrors.length > 0) {
|
|
613
|
-
return { success: false, errors: businessErrors };
|
|
614
|
-
}
|
|
615
|
-
return { success: true, data: validated };
|
|
616
|
-
} catch (error) {
|
|
617
|
-
if (error instanceof z.ZodError) {
|
|
618
|
-
const errors = error.errors.map(
|
|
619
|
-
(err) => `${err.path.join(".")}: ${err.message}`
|
|
620
|
-
);
|
|
621
|
-
return { success: false, errors };
|
|
622
|
-
}
|
|
623
|
-
return {
|
|
624
|
-
success: false,
|
|
625
|
-
errors: [`Configuration validation failed: ${error instanceof Error ? error.message : "Unknown error"}`]
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Validate environment variables for API keys
|
|
631
|
-
*/
|
|
632
|
-
validateEnvironment() {
|
|
633
|
-
const warnings = [];
|
|
634
|
-
const errors = [];
|
|
635
|
-
const exposedPatterns = [
|
|
636
|
-
"NEXT_PUBLIC_ANTHROPIC",
|
|
637
|
-
"REACT_APP_ANTHROPIC",
|
|
638
|
-
"VITE_ANTHROPIC",
|
|
639
|
-
"PUBLIC_ANTHROPIC"
|
|
640
|
-
];
|
|
641
|
-
for (const pattern of exposedPatterns) {
|
|
642
|
-
const envVars = Object.keys(process.env).filter((key) => key.includes(pattern));
|
|
643
|
-
for (const envVar of envVars) {
|
|
644
|
-
errors.push(`[!] Security risk: API key in client-side environment variable: ${envVar}`);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
let anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
648
|
-
if (!anthropicKey) {
|
|
649
|
-
try {
|
|
650
|
-
const configPath = join4(getTrieDirectory(getWorkingDirectory(void 0, true)), "config.json");
|
|
651
|
-
if (existsSync4(configPath)) {
|
|
652
|
-
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
653
|
-
anthropicKey = config.apiKeys?.anthropic;
|
|
654
|
-
}
|
|
655
|
-
} catch {
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
if (anthropicKey && !API_KEY_PATTERNS.anthropic.test(anthropicKey)) {
|
|
659
|
-
errors.push("ANTHROPIC_API_KEY does not match expected format");
|
|
660
|
-
}
|
|
661
|
-
if (!anthropicKey) {
|
|
662
|
-
warnings.push("ANTHROPIC_API_KEY not set - AI features will be disabled. Set in environment, .trie/config.json, or .env file");
|
|
663
|
-
}
|
|
664
|
-
if (!process.env.GITHUB_TOKEN && process.env.CI) {
|
|
665
|
-
warnings.push("GITHUB_TOKEN not set - GitHub integration disabled");
|
|
666
|
-
}
|
|
667
|
-
return {
|
|
668
|
-
valid: errors.length === 0,
|
|
669
|
-
warnings,
|
|
670
|
-
errors
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
/**
|
|
674
|
-
* Validate file paths in configuration
|
|
675
|
-
*/
|
|
676
|
-
validatePaths(paths) {
|
|
677
|
-
const errors = [];
|
|
678
|
-
if (paths?.include) {
|
|
679
|
-
for (const path8 of paths.include) {
|
|
680
|
-
const resolvedPath = resolve(path8);
|
|
681
|
-
if (!existsSync4(resolvedPath)) {
|
|
682
|
-
errors.push(`Include path does not exist: ${path8}`);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
if (paths?.configDir) {
|
|
687
|
-
const configPath = resolve(paths.configDir);
|
|
688
|
-
if (!existsSync4(configPath)) {
|
|
689
|
-
try {
|
|
690
|
-
__require("fs").mkdirSync(configPath, { recursive: true });
|
|
691
|
-
} catch {
|
|
692
|
-
errors.push(`Cannot create config directory: ${paths.configDir}`);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
return {
|
|
697
|
-
valid: errors.length === 0,
|
|
698
|
-
errors
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Validate integration configurations
|
|
703
|
-
*/
|
|
704
|
-
validateIntegrations(integrations) {
|
|
705
|
-
const errors = [];
|
|
706
|
-
if (integrations?.github?.enabled) {
|
|
707
|
-
if (!integrations.github.token) {
|
|
708
|
-
errors.push("GitHub integration enabled but no token provided");
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
if (integrations?.slack?.enabled) {
|
|
712
|
-
if (!integrations.slack.webhook) {
|
|
713
|
-
errors.push("Slack integration enabled but no webhook URL provided");
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
if (integrations?.jira?.enabled) {
|
|
717
|
-
if (!integrations.jira.url || !integrations.jira.token || !integrations.jira.project) {
|
|
718
|
-
errors.push("JIRA integration enabled but missing required fields (url, token, project)");
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
return {
|
|
722
|
-
valid: errors.length === 0,
|
|
723
|
-
errors
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* Business logic validation
|
|
728
|
-
*/
|
|
729
|
-
validateBusinessLogic(config) {
|
|
730
|
-
const errors = [];
|
|
731
|
-
if (config.agents?.enabled && config.agents?.disabled) {
|
|
732
|
-
const overlap = config.agents.enabled.filter(
|
|
733
|
-
(agent) => config.agents?.disabled?.includes(agent)
|
|
734
|
-
);
|
|
735
|
-
if (overlap.length > 0) {
|
|
736
|
-
errors.push(`Agents cannot be both enabled and disabled: ${overlap.join(", ")}`);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
if (config.agents?.maxConcurrency && config.agents.maxConcurrency > 10) {
|
|
740
|
-
errors.push("maxConcurrency should not exceed 10 for optimal performance");
|
|
741
|
-
}
|
|
742
|
-
if (config.compliance?.standards) {
|
|
743
|
-
const invalidStandards = config.compliance.standards.filter(
|
|
744
|
-
(standard) => !["SOC2", "GDPR", "HIPAA", "CCPA", "PCI-DSS"].includes(standard)
|
|
745
|
-
);
|
|
746
|
-
if (invalidStandards.length > 0) {
|
|
747
|
-
errors.push(`Invalid compliance standards: ${invalidStandards.join(", ")}`);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
if (config.paths) {
|
|
751
|
-
const pathValidation = this.validatePaths(config.paths);
|
|
752
|
-
errors.push(...pathValidation.errors);
|
|
753
|
-
}
|
|
754
|
-
if (config.integrations) {
|
|
755
|
-
const integrationValidation = this.validateIntegrations(config.integrations);
|
|
756
|
-
errors.push(...integrationValidation.errors);
|
|
757
|
-
}
|
|
758
|
-
return errors;
|
|
759
|
-
}
|
|
760
|
-
/**
|
|
761
|
-
* Generate configuration template
|
|
762
|
-
*/
|
|
763
|
-
generateTemplate() {
|
|
764
|
-
return {
|
|
765
|
-
version: "1.0.0",
|
|
766
|
-
agents: {
|
|
767
|
-
enabled: ["security", "bugs", "types"],
|
|
768
|
-
disabled: [],
|
|
769
|
-
parallel: true,
|
|
770
|
-
maxConcurrency: 4,
|
|
771
|
-
timeout: 12e4,
|
|
772
|
-
cache: true
|
|
773
|
-
},
|
|
774
|
-
compliance: {
|
|
775
|
-
standards: ["SOC2"],
|
|
776
|
-
enforceCompliance: false,
|
|
777
|
-
reportFormat: "json"
|
|
778
|
-
},
|
|
779
|
-
output: {
|
|
780
|
-
format: "console",
|
|
781
|
-
level: "all",
|
|
782
|
-
interactive: false,
|
|
783
|
-
streaming: true,
|
|
784
|
-
colors: true
|
|
785
|
-
},
|
|
786
|
-
paths: {
|
|
787
|
-
include: [],
|
|
788
|
-
exclude: ["node_modules", "dist", "build", ".git"],
|
|
789
|
-
configDir: ".trie",
|
|
790
|
-
outputDir: "trie-reports"
|
|
791
|
-
}
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
/**
|
|
795
|
-
* Validate and provide suggestions for improvement
|
|
796
|
-
*/
|
|
797
|
-
analyze(config) {
|
|
798
|
-
const suggestions = [];
|
|
799
|
-
const securityIssues = [];
|
|
800
|
-
const optimizations = [];
|
|
801
|
-
let score = 100;
|
|
802
|
-
let hasApiKey = Boolean(config.apiKeys?.anthropic || process.env.ANTHROPIC_API_KEY);
|
|
803
|
-
if (!hasApiKey) {
|
|
804
|
-
try {
|
|
805
|
-
const workDir = getWorkingDirectory(void 0, true);
|
|
806
|
-
const envFiles = [".env", ".env.local", ".env.production"];
|
|
807
|
-
for (const envFile of envFiles) {
|
|
808
|
-
const envPath = join4(workDir, envFile);
|
|
809
|
-
if (existsSync4(envPath)) {
|
|
810
|
-
const envContent = readFileSync(envPath, "utf-8");
|
|
811
|
-
if (envContent.includes("ANTHROPIC_API_KEY=")) {
|
|
812
|
-
hasApiKey = true;
|
|
813
|
-
break;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
} catch {
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
if (!hasApiKey) {
|
|
821
|
-
suggestions.push("Add ANTHROPIC_API_KEY to enable AI-powered analysis for better issue detection. Set in environment, .trie/config.json, or .env file");
|
|
822
|
-
score -= 10;
|
|
823
|
-
}
|
|
824
|
-
if (config.agents?.parallel === false) {
|
|
825
|
-
optimizations.push("Enable parallel agent execution for 3-5x faster scans");
|
|
826
|
-
score -= 15;
|
|
827
|
-
}
|
|
828
|
-
if (config.agents?.cache === false) {
|
|
829
|
-
optimizations.push("Enable result caching to speed up repeated scans");
|
|
830
|
-
score -= 10;
|
|
831
|
-
}
|
|
832
|
-
if (!config.compliance?.standards || config.compliance.standards.length === 0) {
|
|
833
|
-
suggestions.push("Configure compliance standards (SOC2, GDPR, etc.) for regulatory requirements");
|
|
834
|
-
score -= 5;
|
|
835
|
-
}
|
|
836
|
-
const hasIntegrations = config.integrations && (config.integrations.github?.enabled || config.integrations.slack?.enabled || config.integrations.jira?.enabled);
|
|
837
|
-
if (!hasIntegrations) {
|
|
838
|
-
suggestions.push("Consider enabling GitHub/Slack/JIRA integrations for better team collaboration");
|
|
839
|
-
score -= 5;
|
|
840
|
-
}
|
|
841
|
-
if (config.apiKeys) {
|
|
842
|
-
securityIssues.push("API keys in config file - consider using environment variables instead");
|
|
843
|
-
score -= 20;
|
|
844
|
-
}
|
|
845
|
-
return {
|
|
846
|
-
score: Math.max(0, score),
|
|
847
|
-
suggestions,
|
|
848
|
-
securityIssues,
|
|
849
|
-
optimizations
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
};
|
|
853
|
-
var DEFAULT_CONFIG = {
|
|
854
|
-
version: "1.0.0",
|
|
855
|
-
agents: {
|
|
856
|
-
enabled: [],
|
|
857
|
-
disabled: [],
|
|
858
|
-
parallel: true,
|
|
859
|
-
maxConcurrency: 4,
|
|
860
|
-
timeout: 12e4,
|
|
861
|
-
cache: true
|
|
862
|
-
},
|
|
863
|
-
compliance: {
|
|
864
|
-
standards: ["SOC2"],
|
|
865
|
-
enforceCompliance: false,
|
|
866
|
-
reportFormat: "json"
|
|
867
|
-
},
|
|
868
|
-
output: {
|
|
869
|
-
format: "console",
|
|
870
|
-
level: "all",
|
|
871
|
-
interactive: false,
|
|
872
|
-
streaming: true,
|
|
873
|
-
colors: true
|
|
874
|
-
},
|
|
875
|
-
paths: {
|
|
876
|
-
include: [],
|
|
877
|
-
exclude: ["node_modules", "dist", "build", ".git", ".next", ".nuxt", "coverage"],
|
|
878
|
-
configDir: ".trie",
|
|
879
|
-
outputDir: "trie-reports"
|
|
880
|
-
}
|
|
881
|
-
};
|
|
882
|
-
|
|
883
|
-
// src/config/loader.ts
|
|
884
|
-
async function loadConfig() {
|
|
885
|
-
const validator = new ConfigValidator();
|
|
886
|
-
const configPath = join5(getTrieDirectory(getWorkingDirectory(void 0, true)), "config.json");
|
|
887
|
-
try {
|
|
888
|
-
if (!existsSync5(configPath)) {
|
|
889
|
-
return DEFAULT_CONFIG;
|
|
890
|
-
}
|
|
891
|
-
const configFile = await readFile4(configPath, "utf-8");
|
|
892
|
-
const userConfig = JSON.parse(configFile);
|
|
893
|
-
const merged = mergeConfig(DEFAULT_CONFIG, userConfig);
|
|
894
|
-
const result = validator.validateConfig(merged);
|
|
895
|
-
if (!result.success) {
|
|
896
|
-
if (!isInteractiveMode()) {
|
|
897
|
-
console.error("Configuration validation failed:");
|
|
898
|
-
for (const error of result.errors) {
|
|
899
|
-
console.error(` - ${error}`);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
return DEFAULT_CONFIG;
|
|
903
|
-
}
|
|
904
|
-
if (!isInteractiveMode()) {
|
|
905
|
-
const envValidation = validator.validateEnvironment();
|
|
906
|
-
for (const warning of envValidation.warnings) {
|
|
907
|
-
console.warn(warning);
|
|
908
|
-
}
|
|
909
|
-
for (const error of envValidation.errors) {
|
|
910
|
-
console.error(error);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
return result.data;
|
|
914
|
-
} catch (error) {
|
|
915
|
-
if (!isInteractiveMode()) {
|
|
916
|
-
console.error("Failed to load config, using defaults:", error);
|
|
917
|
-
}
|
|
918
|
-
return DEFAULT_CONFIG;
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
async function saveConfig(config) {
|
|
922
|
-
const configPath = join5(getTrieDirectory(getWorkingDirectory(void 0, true)), "config.json");
|
|
923
|
-
const dir = dirname(configPath);
|
|
924
|
-
if (!existsSync5(dir)) {
|
|
925
|
-
await mkdir4(dir, { recursive: true });
|
|
926
|
-
}
|
|
927
|
-
await writeFile3(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
928
|
-
}
|
|
929
|
-
function mergeConfig(defaults, user) {
|
|
930
|
-
if (typeof user !== "object" || user === null || Array.isArray(user)) {
|
|
931
|
-
return { ...defaults };
|
|
932
|
-
}
|
|
933
|
-
const result = { ...defaults };
|
|
934
|
-
for (const [key, value] of Object.entries(user)) {
|
|
935
|
-
const defaultValue = defaults[key];
|
|
936
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value) && typeof defaultValue === "object" && defaultValue !== null) {
|
|
937
|
-
result[key] = mergeConfig(defaultValue, value);
|
|
938
|
-
} else {
|
|
939
|
-
result[key] = value;
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
return result;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// src/utils/autonomy-config.ts
|
|
946
|
-
import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir5 } from "fs/promises";
|
|
947
|
-
import { existsSync as existsSync6 } from "fs";
|
|
948
|
-
import { join as join6 } from "path";
|
|
949
|
-
|
|
950
|
-
// src/types/autonomy.ts
|
|
951
|
-
var DEFAULT_AUTONOMY_CONFIG = {
|
|
952
|
-
level: "proactive",
|
|
953
|
-
autoCheck: {
|
|
954
|
-
enabled: true,
|
|
955
|
-
threshold: 5,
|
|
956
|
-
onCritical: true,
|
|
957
|
-
cooldownMs: 3e4
|
|
958
|
-
// 30 seconds
|
|
959
|
-
},
|
|
960
|
-
autoFix: {
|
|
961
|
-
enabled: true,
|
|
962
|
-
askFirst: true,
|
|
963
|
-
// Always ask (human-in-the-loop)
|
|
964
|
-
categories: ["trivial", "safe"],
|
|
965
|
-
allowedFixTypes: [
|
|
966
|
-
"remove-console-log",
|
|
967
|
-
"remove-unused-import",
|
|
968
|
-
"add-missing-await",
|
|
969
|
-
"fix-typo"
|
|
970
|
-
]
|
|
971
|
-
},
|
|
972
|
-
pushBlocking: {
|
|
973
|
-
enabled: true,
|
|
974
|
-
allowBypass: true,
|
|
975
|
-
blockOn: ["critical"],
|
|
976
|
-
bypassMethods: ["env", "flag", "confirm"],
|
|
977
|
-
logBypasses: true
|
|
978
|
-
},
|
|
979
|
-
progressiveEscalation: {
|
|
980
|
-
enabled: true,
|
|
981
|
-
thresholds: {
|
|
982
|
-
suggest: 1,
|
|
983
|
-
autoCheck: 3,
|
|
984
|
-
escalate: 5,
|
|
985
|
-
block: 10
|
|
986
|
-
},
|
|
987
|
-
windowMs: 24 * 60 * 60 * 1e3
|
|
988
|
-
// 24 hours
|
|
989
|
-
}
|
|
990
|
-
};
|
|
991
|
-
|
|
992
|
-
// src/utils/autonomy-config.ts
|
|
993
|
-
import { createHash } from "crypto";
|
|
994
|
-
async function loadAutonomyConfig(projectPath) {
|
|
995
|
-
const configPath = join6(getTrieDirectory(projectPath), "config.json");
|
|
996
|
-
try {
|
|
997
|
-
if (!existsSync6(configPath)) {
|
|
998
|
-
return { ...DEFAULT_AUTONOMY_CONFIG };
|
|
999
|
-
}
|
|
1000
|
-
const content = await readFile5(configPath, "utf-8");
|
|
1001
|
-
const config = JSON.parse(content);
|
|
1002
|
-
return mergeWithDefaults(config.autonomy || {});
|
|
1003
|
-
} catch (error) {
|
|
1004
|
-
console.error("Failed to load autonomy config:", error);
|
|
1005
|
-
return { ...DEFAULT_AUTONOMY_CONFIG };
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
function mergeWithDefaults(partial) {
|
|
1009
|
-
return {
|
|
1010
|
-
level: partial.level ?? DEFAULT_AUTONOMY_CONFIG.level,
|
|
1011
|
-
autoCheck: {
|
|
1012
|
-
...DEFAULT_AUTONOMY_CONFIG.autoCheck,
|
|
1013
|
-
...partial.autoCheck || {}
|
|
1014
|
-
},
|
|
1015
|
-
autoFix: {
|
|
1016
|
-
...DEFAULT_AUTONOMY_CONFIG.autoFix,
|
|
1017
|
-
...partial.autoFix || {}
|
|
1018
|
-
},
|
|
1019
|
-
pushBlocking: {
|
|
1020
|
-
...DEFAULT_AUTONOMY_CONFIG.pushBlocking,
|
|
1021
|
-
...partial.pushBlocking || {}
|
|
1022
|
-
},
|
|
1023
|
-
progressiveEscalation: {
|
|
1024
|
-
...DEFAULT_AUTONOMY_CONFIG.progressiveEscalation,
|
|
1025
|
-
...partial.progressiveEscalation || {},
|
|
1026
|
-
thresholds: {
|
|
1027
|
-
...DEFAULT_AUTONOMY_CONFIG.progressiveEscalation.thresholds,
|
|
1028
|
-
...partial.progressiveEscalation?.thresholds || {}
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
};
|
|
1032
|
-
}
|
|
1033
|
-
var occurrenceCache = /* @__PURE__ */ new Map();
|
|
1034
|
-
async function getOccurrences(projectPath) {
|
|
1035
|
-
if (occurrenceCache.has(projectPath)) {
|
|
1036
|
-
return occurrenceCache.get(projectPath);
|
|
1037
|
-
}
|
|
1038
|
-
const occurrencesPath = join6(getTrieDirectory(projectPath), "memory", "occurrences.json");
|
|
1039
|
-
const occurrences = /* @__PURE__ */ new Map();
|
|
1040
|
-
try {
|
|
1041
|
-
if (existsSync6(occurrencesPath)) {
|
|
1042
|
-
const content = await readFile5(occurrencesPath, "utf-8");
|
|
1043
|
-
const data = JSON.parse(content);
|
|
1044
|
-
for (const [hash, occ] of Object.entries(data)) {
|
|
1045
|
-
occurrences.set(hash, occ);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
} catch {
|
|
1049
|
-
}
|
|
1050
|
-
occurrenceCache.set(projectPath, occurrences);
|
|
1051
|
-
return occurrences;
|
|
1052
|
-
}
|
|
1053
|
-
async function saveOccurrences(projectPath) {
|
|
1054
|
-
const occurrences = occurrenceCache.get(projectPath);
|
|
1055
|
-
if (!occurrences) return;
|
|
1056
|
-
const occurrencesPath = join6(getTrieDirectory(projectPath), "memory", "occurrences.json");
|
|
1057
|
-
const memoryDir = join6(getTrieDirectory(projectPath), "memory");
|
|
1058
|
-
try {
|
|
1059
|
-
if (!existsSync6(memoryDir)) {
|
|
1060
|
-
await mkdir5(memoryDir, { recursive: true });
|
|
1061
|
-
}
|
|
1062
|
-
const data = {};
|
|
1063
|
-
for (const [hash, occ] of occurrences.entries()) {
|
|
1064
|
-
data[hash] = occ;
|
|
1065
|
-
}
|
|
1066
|
-
await writeFile4(occurrencesPath, JSON.stringify(data, null, 2));
|
|
1067
|
-
} catch (error) {
|
|
1068
|
-
console.error("Failed to save occurrences:", error);
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
function createIssueHash(file, line, issueType) {
|
|
1072
|
-
const input = `${file}:${line || 0}:${issueType}`;
|
|
1073
|
-
return createHash("md5").update(input).digest("hex").slice(0, 12);
|
|
1074
|
-
}
|
|
1075
|
-
async function trackIssueOccurrence(projectPath, file, line, issueType, config) {
|
|
1076
|
-
const occurrences = await getOccurrences(projectPath);
|
|
1077
|
-
const hash = createIssueHash(file, line, issueType);
|
|
1078
|
-
const now = Date.now();
|
|
1079
|
-
let occurrence = occurrences.get(hash);
|
|
1080
|
-
if (occurrence) {
|
|
1081
|
-
const windowMs = config.progressiveEscalation.windowMs;
|
|
1082
|
-
if (now - occurrence.firstSeen > windowMs) {
|
|
1083
|
-
occurrence = {
|
|
1084
|
-
hash,
|
|
1085
|
-
firstSeen: now,
|
|
1086
|
-
lastSeen: now,
|
|
1087
|
-
count: 1,
|
|
1088
|
-
escalationLevel: "suggest",
|
|
1089
|
-
notified: false,
|
|
1090
|
-
bypassed: false,
|
|
1091
|
-
bypassHistory: occurrence.bypassHistory
|
|
1092
|
-
// Keep bypass history
|
|
1093
|
-
};
|
|
1094
|
-
} else {
|
|
1095
|
-
occurrence.lastSeen = now;
|
|
1096
|
-
occurrence.count++;
|
|
1097
|
-
const thresholds = config.progressiveEscalation.thresholds;
|
|
1098
|
-
if (occurrence.count >= thresholds.block) {
|
|
1099
|
-
occurrence.escalationLevel = "block";
|
|
1100
|
-
} else if (occurrence.count >= thresholds.escalate) {
|
|
1101
|
-
occurrence.escalationLevel = "escalate";
|
|
1102
|
-
} else if (occurrence.count >= thresholds.autoCheck) {
|
|
1103
|
-
occurrence.escalationLevel = "autoCheck";
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
} else {
|
|
1107
|
-
occurrence = {
|
|
1108
|
-
hash,
|
|
1109
|
-
firstSeen: now,
|
|
1110
|
-
lastSeen: now,
|
|
1111
|
-
count: 1,
|
|
1112
|
-
escalationLevel: "suggest",
|
|
1113
|
-
notified: false,
|
|
1114
|
-
bypassed: false,
|
|
1115
|
-
bypassHistory: []
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
occurrences.set(hash, occurrence);
|
|
1119
|
-
await saveOccurrences(projectPath);
|
|
1120
|
-
return occurrence;
|
|
1121
|
-
}
|
|
1122
|
-
async function recordBypass(projectPath, file, line, issueType, method, reason) {
|
|
1123
|
-
const occurrences = await getOccurrences(projectPath);
|
|
1124
|
-
const hash = createIssueHash(file, line, issueType);
|
|
1125
|
-
const occurrence = occurrences.get(hash);
|
|
1126
|
-
if (occurrence) {
|
|
1127
|
-
occurrence.bypassed = true;
|
|
1128
|
-
occurrence.bypassHistory.push({
|
|
1129
|
-
timestamp: Date.now(),
|
|
1130
|
-
method,
|
|
1131
|
-
...reason !== void 0 ? { reason } : {}
|
|
1132
|
-
});
|
|
1133
|
-
await saveOccurrences(projectPath);
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
function shouldAutoFix(fix, config) {
|
|
1137
|
-
if (!config.autoFix.enabled) {
|
|
1138
|
-
return false;
|
|
1139
|
-
}
|
|
1140
|
-
const allowedCategories = config.autoFix.categories;
|
|
1141
|
-
if (!allowedCategories.includes(fix.category) && !allowedCategories.includes("all")) {
|
|
1142
|
-
return false;
|
|
1143
|
-
}
|
|
1144
|
-
if (config.autoFix.allowedFixTypes.length > 0 && !config.autoFix.allowedFixTypes.includes(fix.type)) {
|
|
1145
|
-
return false;
|
|
1146
|
-
}
|
|
1147
|
-
if (fix.confidence < 0.9) {
|
|
1148
|
-
return false;
|
|
1149
|
-
}
|
|
1150
|
-
return true;
|
|
1151
|
-
}
|
|
1152
|
-
function shouldBlockPush(issues, config) {
|
|
1153
|
-
if (!config.pushBlocking.enabled) {
|
|
1154
|
-
return {
|
|
1155
|
-
blocked: false,
|
|
1156
|
-
blockingIssues: [],
|
|
1157
|
-
bypassed: false
|
|
1158
|
-
};
|
|
1159
|
-
}
|
|
1160
|
-
const blockingIssues = issues.filter(
|
|
1161
|
-
(issue) => config.pushBlocking.blockOn.includes(issue.severity)
|
|
1162
|
-
);
|
|
1163
|
-
if (blockingIssues.length === 0) {
|
|
1164
|
-
return {
|
|
1165
|
-
blocked: false,
|
|
1166
|
-
blockingIssues: [],
|
|
1167
|
-
bypassed: false
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
|
-
const envBypass = process.env.TRIE_BYPASS === "1" || process.env.TRIE_BYPASS === "true";
|
|
1171
|
-
if (envBypass && config.pushBlocking.allowBypass) {
|
|
1172
|
-
return {
|
|
1173
|
-
blocked: false,
|
|
1174
|
-
blockingIssues,
|
|
1175
|
-
bypassed: true,
|
|
1176
|
-
bypassMethod: "env"
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
const bypassInstructions = buildBypassInstructions(config);
|
|
1180
|
-
return {
|
|
1181
|
-
blocked: true,
|
|
1182
|
-
reason: `${blockingIssues.length} ${blockingIssues[0]?.severity || "critical"} issue(s) must be fixed before pushing`,
|
|
1183
|
-
blockingIssues,
|
|
1184
|
-
bypassInstructions,
|
|
1185
|
-
bypassed: false
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
function buildBypassInstructions(config) {
|
|
1189
|
-
const instructions = [];
|
|
1190
|
-
if (config.pushBlocking.bypassMethods.includes("env")) {
|
|
1191
|
-
instructions.push("\u2022 Set TRIE_BYPASS=1 to bypass: TRIE_BYPASS=1 git push");
|
|
1192
|
-
}
|
|
1193
|
-
if (config.pushBlocking.bypassMethods.includes("flag")) {
|
|
1194
|
-
instructions.push("\u2022 Use --no-verify flag: git push --no-verify");
|
|
1195
|
-
}
|
|
1196
|
-
if (config.pushBlocking.bypassMethods.includes("confirm")) {
|
|
1197
|
-
instructions.push("\u2022 Run: trie bypass --confirm to bypass this push");
|
|
1198
|
-
}
|
|
1199
|
-
return instructions.join("\n");
|
|
1200
|
-
}
|
|
1201
|
-
var configCache = /* @__PURE__ */ new Map();
|
|
1202
|
-
var CACHE_TTL = 6e4;
|
|
1203
|
-
async function getAutonomyConfig(projectPath) {
|
|
1204
|
-
const cached = configCache.get(projectPath);
|
|
1205
|
-
if (cached && Date.now() - cached.loadedAt < CACHE_TTL) {
|
|
1206
|
-
return cached.config;
|
|
1207
|
-
}
|
|
1208
|
-
const config = await loadAutonomyConfig(projectPath);
|
|
1209
|
-
configCache.set(projectPath, { config, loadedAt: Date.now() });
|
|
1210
|
-
return config;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
// src/skills/audit-logger.ts
|
|
1214
|
-
function formatAuditLog(_entry) {
|
|
1215
|
-
return "Audit logging has been integrated into the decision ledger";
|
|
1216
|
-
}
|
|
1217
|
-
function getAuditStatistics() {
|
|
1218
|
-
return {
|
|
1219
|
-
totalScans: 0,
|
|
1220
|
-
totalIssues: 0,
|
|
1221
|
-
criticalCount: 0,
|
|
1222
|
-
seriousCount: 0,
|
|
1223
|
-
moderateCount: 0,
|
|
1224
|
-
lowCount: 0,
|
|
1225
|
-
totalExecutions: 0,
|
|
1226
|
-
successfulExecutions: 0,
|
|
1227
|
-
failedExecutions: 0,
|
|
1228
|
-
uniqueSkills: 0,
|
|
1229
|
-
totalCommands: 0,
|
|
1230
|
-
blockedCommands: 0,
|
|
1231
|
-
totalNetworkCalls: 0,
|
|
1232
|
-
blockedNetworkCalls: 0
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
|
-
function createAuditEntry(skillName, skillSource, triggeredBy, targetPath) {
|
|
1236
|
-
return {
|
|
1237
|
-
skillName,
|
|
1238
|
-
skillSource,
|
|
1239
|
-
triggeredBy,
|
|
1240
|
-
targetPath,
|
|
1241
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1242
|
-
commands: []
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
function completeAuditEntry(entry, success, error) {
|
|
1246
|
-
const result = {
|
|
1247
|
-
...entry,
|
|
1248
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1249
|
-
success
|
|
1250
|
-
};
|
|
1251
|
-
if (error !== void 0) {
|
|
1252
|
-
result.error = error;
|
|
1253
|
-
}
|
|
1254
|
-
return result;
|
|
1255
|
-
}
|
|
1256
|
-
async function logSkillExecution(_execution) {
|
|
1257
|
-
}
|
|
1258
|
-
async function getRecentAuditLogs(_limit = 10) {
|
|
1259
|
-
return [];
|
|
1260
|
-
}
|
|
1261
|
-
async function getSkillAuditLogs(_skillName) {
|
|
1262
|
-
return [];
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
// src/agent/git.ts
|
|
1266
|
-
import { existsSync as existsSync7 } from "fs";
|
|
1267
|
-
import path from "path";
|
|
1268
|
-
|
|
1269
|
-
// src/utils/command-runner.ts
|
|
1270
|
-
import { exec, execFile, execSync } from "child_process";
|
|
1271
|
-
import { promisify } from "util";
|
|
1272
|
-
var execAsync = promisify(exec);
|
|
1273
|
-
var execFileAsync = promisify(execFile);
|
|
1274
|
-
function redact(text) {
|
|
1275
|
-
return text.replace(/\b(AWS|ANTHROPIC|OPENAI|GITHUB)_[A-Z0-9_]*\s*=\s*([^\s"'`]+)/gi, "$1_<REDACTED>=<REDACTED>").replace(/\bBearer\s+[A-Za-z0-9\-._~+/]+=*\b/g, "Bearer <REDACTED>").replace(/\bghp_[A-Za-z0-9]{20,}\b/g, "ghp_<REDACTED>").replace(/\b(?:xox[baprs]-)[A-Za-z0-9-]{10,}\b/g, "<REDACTED_SLACK_TOKEN>").replace(/\bAKIA[0-9A-Z]{16}\b/g, "AKIA<REDACTED>");
|
|
1276
|
-
}
|
|
1277
|
-
function clampOutput(text, maxChars) {
|
|
1278
|
-
if (text.length <= maxChars) return text;
|
|
1279
|
-
return text.slice(0, maxChars) + `
|
|
1280
|
-
\u2026(truncated ${text.length - maxChars} chars)`;
|
|
1281
|
-
}
|
|
1282
|
-
function buildCommandRecord(command) {
|
|
1283
|
-
return {
|
|
1284
|
-
command,
|
|
1285
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
async function finalizeAndWrite(entry, cmd, outcome, options) {
|
|
1289
|
-
const duration = Date.now() - outcome.startedAt;
|
|
1290
|
-
cmd.duration = duration;
|
|
1291
|
-
if (outcome.exitCode !== void 0) {
|
|
1292
|
-
cmd.exitCode = outcome.exitCode;
|
|
1293
|
-
}
|
|
1294
|
-
const captureOutput = options?.captureOutput ?? false;
|
|
1295
|
-
const redactOutput = options?.redactOutput ?? true;
|
|
1296
|
-
const maxOutputChars = options?.maxOutputChars ?? 2e3;
|
|
1297
|
-
if (captureOutput) {
|
|
1298
|
-
const out = outcome.stdout ?? "";
|
|
1299
|
-
const err = outcome.stderr ?? "";
|
|
1300
|
-
cmd.stdout = redactOutput ? redact(clampOutput(out, maxOutputChars)) : clampOutput(out, maxOutputChars);
|
|
1301
|
-
cmd.stderr = redactOutput ? redact(clampOutput(err, maxOutputChars)) : clampOutput(err, maxOutputChars);
|
|
1302
|
-
}
|
|
1303
|
-
const completed = completeAuditEntry(entry, outcome.success, outcome.error);
|
|
1304
|
-
await logSkillExecution(completed);
|
|
1305
|
-
}
|
|
1306
|
-
function runShellCommandSync(command, audit, options) {
|
|
1307
|
-
const startedAt = Date.now();
|
|
1308
|
-
const entry = createAuditEntry(audit.actor, audit.source ?? "trie", audit.triggeredBy, audit.targetPath);
|
|
1309
|
-
const cmd = buildCommandRecord(command);
|
|
1310
|
-
entry.commands?.push(cmd);
|
|
1311
|
-
try {
|
|
1312
|
-
const stdout = execSync(command, {
|
|
1313
|
-
cwd: options?.cwd,
|
|
1314
|
-
timeout: options?.timeoutMs,
|
|
1315
|
-
maxBuffer: options?.maxBuffer,
|
|
1316
|
-
encoding: "utf-8",
|
|
1317
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1318
|
-
});
|
|
1319
|
-
void finalizeAndWrite(entry, cmd, { success: true, exitCode: 0, stdout, stderr: "", startedAt }, options);
|
|
1320
|
-
return { stdout: stdout ?? "", exitCode: 0 };
|
|
1321
|
-
} catch (e) {
|
|
1322
|
-
const err = e;
|
|
1323
|
-
const stdout = typeof err.stdout === "string" ? err.stdout : "";
|
|
1324
|
-
const stderr = typeof err.stderr === "string" ? err.stderr : "";
|
|
1325
|
-
const exitCode = typeof err.status === "number" ? err.status : 1;
|
|
1326
|
-
void finalizeAndWrite(
|
|
1327
|
-
entry,
|
|
1328
|
-
cmd,
|
|
1329
|
-
{ success: false, exitCode, stdout, stderr, error: err.message, startedAt },
|
|
1330
|
-
{ ...options, captureOutput: options?.captureOutput ?? true }
|
|
1331
|
-
);
|
|
1332
|
-
return { stdout, exitCode };
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
async function runExecFile(file, args, audit, options) {
|
|
1336
|
-
const startedAt = Date.now();
|
|
1337
|
-
const command = [file, ...args].join(" ");
|
|
1338
|
-
const entry = createAuditEntry(audit.actor, audit.source ?? "trie", audit.triggeredBy, audit.targetPath);
|
|
1339
|
-
const cmd = buildCommandRecord(command);
|
|
1340
|
-
entry.commands?.push(cmd);
|
|
1341
|
-
try {
|
|
1342
|
-
const { stdout, stderr } = await execFileAsync(file, args, {
|
|
1343
|
-
cwd: options?.cwd,
|
|
1344
|
-
timeout: options?.timeoutMs,
|
|
1345
|
-
maxBuffer: options?.maxBuffer
|
|
1346
|
-
});
|
|
1347
|
-
await finalizeAndWrite(entry, cmd, { success: true, exitCode: 0, stdout: String(stdout ?? ""), stderr: String(stderr ?? ""), startedAt }, options);
|
|
1348
|
-
return { stdout: String(stdout ?? ""), stderr: String(stderr ?? ""), exitCode: 0 };
|
|
1349
|
-
} catch (e) {
|
|
1350
|
-
const err = e;
|
|
1351
|
-
const stdout = typeof err.stdout === "string" ? err.stdout : "";
|
|
1352
|
-
const stderr = typeof err.stderr === "string" ? err.stderr : "";
|
|
1353
|
-
const exitCode = typeof err.code === "number" ? err.code : 1;
|
|
1354
|
-
await finalizeAndWrite(
|
|
1355
|
-
entry,
|
|
1356
|
-
cmd,
|
|
1357
|
-
{ success: false, exitCode, stdout, stderr, error: err.message, startedAt },
|
|
1358
|
-
{ ...options, captureOutput: options?.captureOutput ?? true }
|
|
1359
|
-
);
|
|
1360
|
-
return { stdout, stderr, exitCode };
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
// src/agent/git.ts
|
|
1365
|
-
async function execGit(args, cwd) {
|
|
1366
|
-
try {
|
|
1367
|
-
const { stdout } = await runExecFile(
|
|
1368
|
-
"git",
|
|
1369
|
-
["-C", cwd, ...args],
|
|
1370
|
-
{ actor: "internal:git", triggeredBy: "manual", targetPath: cwd },
|
|
1371
|
-
{ maxBuffer: 10 * 1024 * 1024, captureOutput: false }
|
|
1372
|
-
);
|
|
1373
|
-
return stdout.trim();
|
|
1374
|
-
} catch (error) {
|
|
1375
|
-
const stderr = error?.stderr?.toString();
|
|
1376
|
-
if (stderr?.includes("not a git repository") || stderr?.includes("does not have any commits")) {
|
|
1377
|
-
return null;
|
|
1378
|
-
}
|
|
1379
|
-
throw error;
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
async function ensureRepo(projectPath) {
|
|
1383
|
-
const result = await execGit(["rev-parse", "--is-inside-work-tree"], projectPath);
|
|
1384
|
-
return result === "true";
|
|
1385
|
-
}
|
|
1386
|
-
function parseNameStatus(output) {
|
|
1387
|
-
return output.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
1388
|
-
const parts = line.split(" ");
|
|
1389
|
-
const status = parts[0] ?? "";
|
|
1390
|
-
const filePath = parts[1] ?? "";
|
|
1391
|
-
const oldPath = parts[2];
|
|
1392
|
-
const change = { status, path: filePath };
|
|
1393
|
-
if (oldPath) change.oldPath = oldPath;
|
|
1394
|
-
return change;
|
|
1395
|
-
}).filter((entry) => entry.path.length > 0);
|
|
1396
|
-
}
|
|
1397
|
-
async function getRecentCommits(projectPath, limit) {
|
|
1398
|
-
const isRepo = await ensureRepo(projectPath);
|
|
1399
|
-
if (!isRepo) return [];
|
|
1400
|
-
const output = await execGit(
|
|
1401
|
-
["log", `-n`, String(limit), "--pretty=format:%H%x09%an%x09%ad%x09%s", "--date=iso"],
|
|
1402
|
-
projectPath
|
|
1403
|
-
);
|
|
1404
|
-
if (!output) return [];
|
|
1405
|
-
return output.split("\n").map((line) => {
|
|
1406
|
-
const [hash, author, date, message] = line.split(" ");
|
|
1407
|
-
return { hash, author, date, message };
|
|
1408
|
-
});
|
|
1409
|
-
}
|
|
1410
|
-
async function getStagedChanges(projectPath) {
|
|
1411
|
-
const isRepo = await ensureRepo(projectPath);
|
|
1412
|
-
if (!isRepo) return [];
|
|
1413
|
-
const output = await execGit(["diff", "--cached", "--name-status"], projectPath);
|
|
1414
|
-
if (!output) return [];
|
|
1415
|
-
return parseNameStatus(output);
|
|
1416
|
-
}
|
|
1417
|
-
async function getUncommittedChanges(projectPath) {
|
|
1418
|
-
const isRepo = await ensureRepo(projectPath);
|
|
1419
|
-
if (!isRepo) return [];
|
|
1420
|
-
const changes = [];
|
|
1421
|
-
const unstaged = await execGit(["diff", "--name-status"], projectPath);
|
|
1422
|
-
if (unstaged) {
|
|
1423
|
-
changes.push(...parseNameStatus(unstaged));
|
|
1424
|
-
}
|
|
1425
|
-
const untracked = await execGit(["ls-files", "--others", "--exclude-standard"], projectPath);
|
|
1426
|
-
if (untracked) {
|
|
1427
|
-
changes.push(
|
|
1428
|
-
...untracked.split("\n").map((p) => p.trim()).filter(Boolean).map((p) => ({ status: "??", path: p }))
|
|
1429
|
-
);
|
|
1430
|
-
}
|
|
1431
|
-
return changes;
|
|
1432
|
-
}
|
|
1433
|
-
async function getDiff(projectPath, commitHash) {
|
|
1434
|
-
const isRepo = await ensureRepo(projectPath);
|
|
1435
|
-
if (!isRepo) return "";
|
|
1436
|
-
const diff = await execGit(["show", commitHash, "--unified=3", "--no-color"], projectPath);
|
|
1437
|
-
return diff ?? "";
|
|
1438
|
-
}
|
|
1439
|
-
async function getWorkingTreeDiff(projectPath, stagedOnly = false) {
|
|
1440
|
-
const isRepo = await ensureRepo(projectPath);
|
|
1441
|
-
if (!isRepo) return "";
|
|
1442
|
-
const args = stagedOnly ? ["diff", "--cached", "--unified=3", "--no-color"] : ["diff", "--unified=3", "--no-color"];
|
|
1443
|
-
const diff = await execGit(args, projectPath);
|
|
1444
|
-
return diff ?? "";
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
// src/agent/perceive.ts
|
|
1448
|
-
import path2 from "path";
|
|
1449
|
-
|
|
1450
|
-
// src/agent/diff-analyzer.ts
|
|
1451
|
-
var RISKY_PATTERNS = [/auth/i, /token/i, /password/i, /secret/i, /validate/i, /sanitize/i];
|
|
1452
|
-
function analyzeDiff(diff) {
|
|
1453
|
-
const files = [];
|
|
1454
|
-
let current = null;
|
|
1455
|
-
const lines = diff.split("\n");
|
|
1456
|
-
for (const line of lines) {
|
|
1457
|
-
if (line.startsWith("+++ b/")) {
|
|
1458
|
-
const filePath = line.replace("+++ b/", "").trim();
|
|
1459
|
-
current = {
|
|
1460
|
-
filePath,
|
|
1461
|
-
added: 0,
|
|
1462
|
-
removed: 0,
|
|
1463
|
-
functionsModified: [],
|
|
1464
|
-
riskyPatterns: []
|
|
1465
|
-
};
|
|
1466
|
-
files.push(current);
|
|
1467
|
-
continue;
|
|
1468
|
-
}
|
|
1469
|
-
if (!current) {
|
|
1470
|
-
continue;
|
|
1471
|
-
}
|
|
1472
|
-
if (line.startsWith("@@")) {
|
|
1473
|
-
const match = line.match(/@@.*?(function\s+([\w$]+)|class\s+([\w$]+)|([\w$]+\s*\())/i);
|
|
1474
|
-
const fnName = match?.[2] || match?.[3] || match?.[4];
|
|
1475
|
-
if (fnName) {
|
|
1476
|
-
current.functionsModified.push(fnName.replace("(", "").trim());
|
|
1477
|
-
}
|
|
1478
|
-
continue;
|
|
1479
|
-
}
|
|
1480
|
-
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
1481
|
-
current.added += 1;
|
|
1482
|
-
markRisk(line, current);
|
|
1483
|
-
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
1484
|
-
current.removed += 1;
|
|
1485
|
-
markRisk(line, current);
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
const totalAdded = files.reduce((acc, f) => acc + f.added, 0);
|
|
1489
|
-
const totalRemoved = files.reduce((acc, f) => acc + f.removed, 0);
|
|
1490
|
-
const riskyFiles = files.filter((f) => f.riskyPatterns.length > 0).map((f) => f.filePath);
|
|
1491
|
-
return {
|
|
1492
|
-
files,
|
|
1493
|
-
totalAdded,
|
|
1494
|
-
totalRemoved,
|
|
1495
|
-
riskyFiles
|
|
1496
|
-
};
|
|
1497
|
-
}
|
|
1498
|
-
function markRisk(line, file) {
|
|
1499
|
-
for (const pattern of RISKY_PATTERNS) {
|
|
1500
|
-
if (pattern.test(line)) {
|
|
1501
|
-
const label = pattern.toString();
|
|
1502
|
-
if (!file.riskyPatterns.includes(label)) {
|
|
1503
|
-
file.riskyPatterns.push(label);
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
// src/agent/perceive.ts
|
|
1510
|
-
async function perceiveCurrentChanges(projectPath, graph) {
|
|
1511
|
-
const ctxGraph = graph ?? new ContextGraph(projectPath);
|
|
1512
|
-
const [staged, unstaged] = await Promise.all([
|
|
1513
|
-
getStagedChanges(projectPath),
|
|
1514
|
-
getUncommittedChanges(projectPath)
|
|
1515
|
-
]);
|
|
1516
|
-
const stagedDiff = await getWorkingTreeDiff(projectPath, true);
|
|
1517
|
-
const unstagedDiff = await getWorkingTreeDiff(projectPath, false);
|
|
1518
|
-
const combinedDiff = [stagedDiff, unstagedDiff].filter(Boolean).join("\n");
|
|
1519
|
-
const diffSummary = analyzeDiff(combinedDiff);
|
|
1520
|
-
const filesTouched = /* @__PURE__ */ new Set();
|
|
1521
|
-
staged.forEach((c) => filesTouched.add(c.path));
|
|
1522
|
-
unstaged.forEach((c) => filesTouched.add(c.path));
|
|
1523
|
-
diffSummary.files.forEach((f) => filesTouched.add(f.filePath));
|
|
1524
|
-
const changeId = await upsertWorkingChange(ctxGraph, Array.from(filesTouched), projectPath);
|
|
1525
|
-
const result = {
|
|
1526
|
-
staged,
|
|
1527
|
-
unstaged,
|
|
1528
|
-
diffSummary
|
|
1529
|
-
};
|
|
1530
|
-
if (changeId) result.changeNodeId = changeId;
|
|
1531
|
-
return result;
|
|
1532
|
-
}
|
|
1533
|
-
async function upsertWorkingChange(graph, files, projectPath) {
|
|
1534
|
-
if (files.length === 0) return void 0;
|
|
1535
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1536
|
-
const change = await graph.addNode("change", {
|
|
1537
|
-
commitHash: null,
|
|
1538
|
-
files,
|
|
1539
|
-
message: "workspace changes",
|
|
1540
|
-
diff: null,
|
|
1541
|
-
author: null,
|
|
1542
|
-
timestamp: now,
|
|
1543
|
-
outcome: "unknown"
|
|
1544
|
-
});
|
|
1545
|
-
for (const filePath of files) {
|
|
1546
|
-
const fileNode = await ensureFileNode(graph, filePath, projectPath);
|
|
1547
|
-
await graph.addEdge(change.id, fileNode.id, "affects");
|
|
1548
|
-
}
|
|
1549
|
-
return change.id;
|
|
1550
|
-
}
|
|
1551
|
-
async function ensureFileNode(graph, filePath, projectPath) {
|
|
1552
|
-
const normalized = path2.resolve(projectPath, filePath);
|
|
1553
|
-
const existing = await graph.getNode("file", normalized);
|
|
1554
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1555
|
-
if (existing) {
|
|
1556
|
-
const data2 = existing.data;
|
|
1557
|
-
await graph.updateNode("file", existing.id, {
|
|
1558
|
-
changeCount: (data2.changeCount ?? 0) + 1,
|
|
1559
|
-
lastChanged: now
|
|
1560
|
-
});
|
|
1561
|
-
return await graph.getNode("file", existing.id);
|
|
1562
|
-
}
|
|
1563
|
-
const data = {
|
|
1564
|
-
path: filePath,
|
|
1565
|
-
extension: path2.extname(filePath),
|
|
1566
|
-
purpose: "",
|
|
1567
|
-
riskLevel: "medium",
|
|
1568
|
-
whyRisky: null,
|
|
1569
|
-
changeCount: 1,
|
|
1570
|
-
lastChanged: now,
|
|
1571
|
-
incidentCount: 0,
|
|
1572
|
-
createdAt: now
|
|
1573
|
-
};
|
|
1574
|
-
return await graph.addNode("file", data);
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
// src/agent/risk-scorer.ts
|
|
1578
|
-
import path3 from "path";
|
|
1579
|
-
var BASE_RISK = {
|
|
1580
|
-
low: 10,
|
|
1581
|
-
medium: 35,
|
|
1582
|
-
high: 65,
|
|
1583
|
-
critical: 85
|
|
1584
|
-
};
|
|
1585
|
-
var SENSITIVE_PATHS = [
|
|
1586
|
-
{ pattern: /auth|login|token|session/i, weight: 20, reason: "touches authentication" },
|
|
1587
|
-
{ pattern: /payment|billing|stripe|paypal|checkout/i, weight: 25, reason: "touches payments" },
|
|
1588
|
-
{ pattern: /secret|credential|env|config\/security/i, weight: 15, reason: "touches secrets/security config" }
|
|
1589
|
-
];
|
|
1590
|
-
function levelFromScore(score) {
|
|
1591
|
-
if (score >= 90) return "critical";
|
|
1592
|
-
if (score >= 65) return "high";
|
|
1593
|
-
if (score >= 40) return "medium";
|
|
1594
|
-
return "low";
|
|
1595
|
-
}
|
|
1596
|
-
async function scoreFile(graph, filePath, matchedPatterns = []) {
|
|
1597
|
-
const reasons = [];
|
|
1598
|
-
const normalized = path3.resolve(graph.projectRoot, filePath);
|
|
1599
|
-
const node = await graph.getNode("file", normalized);
|
|
1600
|
-
const incidents = await graph.getIncidentsForFile(filePath);
|
|
1601
|
-
let score = 10;
|
|
1602
|
-
const data = node?.data;
|
|
1603
|
-
if (data) {
|
|
1604
|
-
score = BASE_RISK[data.riskLevel] ?? score;
|
|
1605
|
-
reasons.push(`baseline ${data.riskLevel}`);
|
|
1606
|
-
if (data.incidentCount > 0) {
|
|
1607
|
-
const incBoost = Math.min(data.incidentCount * 12, 36);
|
|
1608
|
-
score += incBoost;
|
|
1609
|
-
reasons.push(`historical incidents (+${incBoost})`);
|
|
1610
|
-
}
|
|
1611
|
-
if (data.changeCount > 5) {
|
|
1612
|
-
const changeBoost = Math.min((data.changeCount - 5) * 2, 12);
|
|
1613
|
-
score += changeBoost;
|
|
1614
|
-
reasons.push(`frequent changes (+${changeBoost})`);
|
|
1615
|
-
}
|
|
1616
|
-
if (data.lastChanged) {
|
|
1617
|
-
const lastChanged = new Date(data.lastChanged).getTime();
|
|
1618
|
-
const days = (Date.now() - lastChanged) / (1e3 * 60 * 60 * 24);
|
|
1619
|
-
if (days > 60 && data.incidentCount === 0) {
|
|
1620
|
-
score -= 5;
|
|
1621
|
-
reasons.push("stable for 60d (-5)");
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
for (const { pattern, weight, reason } of SENSITIVE_PATHS) {
|
|
1626
|
-
if (pattern.test(filePath)) {
|
|
1627
|
-
score += weight;
|
|
1628
|
-
reasons.push(reason);
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
if (matchedPatterns.length > 0) {
|
|
1632
|
-
const patternBoost = Math.min(
|
|
1633
|
-
matchedPatterns.reduce((acc, p) => acc + (p.data.confidence ?? 50) / 10, 0),
|
|
1634
|
-
20
|
|
1635
|
-
);
|
|
1636
|
-
score += patternBoost;
|
|
1637
|
-
reasons.push(`pattern match (+${Math.round(patternBoost)})`);
|
|
1638
|
-
}
|
|
1639
|
-
if (incidents.length > 0) {
|
|
1640
|
-
const timestamps = incidents.map((i) => new Date(i.data.timestamp).getTime()).sort((a, b) => b - a);
|
|
1641
|
-
const recent = timestamps[0];
|
|
1642
|
-
const daysSince = (Date.now() - recent) / (1e3 * 60 * 60 * 24);
|
|
1643
|
-
if (daysSince > 90) {
|
|
1644
|
-
score -= 5;
|
|
1645
|
-
reasons.push("no incidents in 90d (-5)");
|
|
1646
|
-
} else {
|
|
1647
|
-
score += 8;
|
|
1648
|
-
reasons.push("recent incident (+8)");
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
const level = levelFromScore(score);
|
|
1652
|
-
return {
|
|
1653
|
-
file: filePath,
|
|
1654
|
-
score,
|
|
1655
|
-
level,
|
|
1656
|
-
reasons,
|
|
1657
|
-
incidents,
|
|
1658
|
-
matchedPatterns
|
|
1659
|
-
};
|
|
1660
|
-
}
|
|
1661
|
-
async function scoreChangeSet(graph, files, patternMatches = {}) {
|
|
1662
|
-
const fileResults = [];
|
|
1663
|
-
for (const file of files) {
|
|
1664
|
-
const patterns = patternMatches[file] ?? [];
|
|
1665
|
-
fileResults.push(await scoreFile(graph, file, patterns));
|
|
1666
|
-
}
|
|
1667
|
-
const maxScore = Math.max(...fileResults.map((f) => f.score), 10);
|
|
1668
|
-
const spreadBoost = files.length > 5 ? Math.min((files.length - 5) * 2, 10) : 0;
|
|
1669
|
-
const overallScore = maxScore + spreadBoost;
|
|
1670
|
-
const overall = levelFromScore(overallScore);
|
|
1671
|
-
const shouldEscalate = overall === "critical" || overall === "high";
|
|
1672
|
-
return {
|
|
1673
|
-
files: fileResults,
|
|
1674
|
-
overall,
|
|
1675
|
-
score: overallScore,
|
|
1676
|
-
shouldEscalate
|
|
1677
|
-
};
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
// src/agent/pattern-matcher.ts
|
|
1681
|
-
async function matchPatternsForFiles(graph, files) {
|
|
1682
|
-
const matches = [];
|
|
1683
|
-
const byFile = {};
|
|
1684
|
-
for (const file of files) {
|
|
1685
|
-
const patterns = await graph.getPatternsForFile(file);
|
|
1686
|
-
if (patterns.length === 0) continue;
|
|
1687
|
-
byFile[file] = patterns;
|
|
1688
|
-
for (const pattern of patterns) {
|
|
1689
|
-
matches.push({
|
|
1690
|
-
file,
|
|
1691
|
-
pattern,
|
|
1692
|
-
confidence: pattern.data.confidence,
|
|
1693
|
-
isAntiPattern: pattern.data.isAntiPattern
|
|
1694
|
-
});
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
return { matches, byFile };
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
// src/orchestrator/triager.ts
|
|
1701
|
-
var Triager = class {
|
|
1702
|
-
constructor(_config) {
|
|
1703
|
-
}
|
|
1704
|
-
/**
|
|
1705
|
-
* Triage a change to select appropriate agents
|
|
1706
|
-
* Note: Skills/agents have been removed - Trie is now purely a decision ledger
|
|
1707
|
-
*/
|
|
1708
|
-
async triage(_context, _forceAgents) {
|
|
1709
|
-
return [];
|
|
1710
|
-
}
|
|
1711
|
-
/**
|
|
1712
|
-
* Get all available agent names (deprecated - returns empty array)
|
|
1713
|
-
*/
|
|
1714
|
-
getAvailableAgents() {
|
|
1715
|
-
return [];
|
|
1716
|
-
}
|
|
1717
|
-
};
|
|
1718
|
-
|
|
1719
|
-
// src/utils/parallel-executor.ts
|
|
1720
|
-
import { Worker } from "worker_threads";
|
|
1721
|
-
import { cpus } from "os";
|
|
1722
|
-
import { existsSync as existsSync8 } from "fs";
|
|
1723
|
-
import { fileURLToPath } from "url";
|
|
1724
|
-
var ParallelExecutor = class {
|
|
1725
|
-
maxWorkers;
|
|
1726
|
-
cache;
|
|
1727
|
-
streaming;
|
|
1728
|
-
activeWorkers = /* @__PURE__ */ new Set();
|
|
1729
|
-
cacheEnabled = true;
|
|
1730
|
-
useWorkerThreads = false;
|
|
1731
|
-
workerAvailable = null;
|
|
1732
|
-
warnedWorkerFallback = false;
|
|
1733
|
-
constructor(cacheManager, maxWorkers = Math.max(2, Math.min(cpus().length - 1, 8)), options) {
|
|
1734
|
-
this.maxWorkers = maxWorkers;
|
|
1735
|
-
this.cache = cacheManager;
|
|
1736
|
-
this.cacheEnabled = options?.cacheEnabled ?? true;
|
|
1737
|
-
this.useWorkerThreads = options?.useWorkerThreads ?? false;
|
|
1738
|
-
}
|
|
1739
|
-
/**
|
|
1740
|
-
* Set streaming manager for real-time updates
|
|
1741
|
-
*/
|
|
1742
|
-
setStreaming(streaming) {
|
|
1743
|
-
this.streaming = streaming;
|
|
1744
|
-
}
|
|
1745
|
-
/**
|
|
1746
|
-
* Execute agents in parallel with intelligent scheduling
|
|
1747
|
-
*/
|
|
1748
|
-
async executeAgents(agents, files, context) {
|
|
1749
|
-
if (agents.length === 0) {
|
|
1750
|
-
return /* @__PURE__ */ new Map();
|
|
1751
|
-
}
|
|
1752
|
-
if (this.streaming && this.streaming.getProgress().totalFiles === 0) {
|
|
1753
|
-
this.streaming.startScan(files.length);
|
|
1754
|
-
}
|
|
1755
|
-
const cacheResults = /* @__PURE__ */ new Map();
|
|
1756
|
-
const uncachedTasks = [];
|
|
1757
|
-
for (const agent of agents) {
|
|
1758
|
-
const cached = await this.checkAgentCache(agent, files);
|
|
1759
|
-
if (cached) {
|
|
1760
|
-
cacheResults.set(agent.name, cached);
|
|
1761
|
-
this.streaming?.completeAgent(agent.name, cached.issues);
|
|
1762
|
-
} else {
|
|
1763
|
-
uncachedTasks.push({
|
|
1764
|
-
agent,
|
|
1765
|
-
files,
|
|
1766
|
-
context,
|
|
1767
|
-
priority: agent.priority?.tier || 2,
|
|
1768
|
-
timeoutMs: context?.config?.timeoutMs || 12e4
|
|
1769
|
-
});
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
uncachedTasks.sort((a, b) => a.priority - b.priority);
|
|
1773
|
-
const parallelResults = await this.executeTasksParallel(uncachedTasks);
|
|
1774
|
-
await this.cacheResults(parallelResults);
|
|
1775
|
-
const allResults = /* @__PURE__ */ new Map();
|
|
1776
|
-
for (const [agent, result] of cacheResults) {
|
|
1777
|
-
allResults.set(agent, result);
|
|
1778
|
-
}
|
|
1779
|
-
for (const result of parallelResults) {
|
|
1780
|
-
allResults.set(result.agent, result.result);
|
|
1781
|
-
}
|
|
1782
|
-
const allIssues = Array.from(allResults.values()).flatMap((r) => r.issues);
|
|
1783
|
-
this.streaming?.completeScan(allIssues);
|
|
1784
|
-
return allResults;
|
|
1785
|
-
}
|
|
1786
|
-
/**
|
|
1787
|
-
* Check if agent has cached results for given files
|
|
1788
|
-
*/
|
|
1789
|
-
async checkAgentCache(agent, files) {
|
|
1790
|
-
if (!this.cacheEnabled || !this.cache) {
|
|
1791
|
-
return null;
|
|
1792
|
-
}
|
|
1793
|
-
const cachedIssues = await this.cache.getCachedBatch(files, agent.name);
|
|
1794
|
-
if (cachedIssues.size === files.length) {
|
|
1795
|
-
const allIssues = Array.from(cachedIssues.values()).flat();
|
|
1796
|
-
return {
|
|
1797
|
-
agent: agent.name,
|
|
1798
|
-
issues: allIssues,
|
|
1799
|
-
executionTime: 0,
|
|
1800
|
-
// Cached
|
|
1801
|
-
success: true,
|
|
1802
|
-
metadata: {
|
|
1803
|
-
filesAnalyzed: files.length,
|
|
1804
|
-
linesAnalyzed: 0
|
|
1805
|
-
}
|
|
1806
|
-
};
|
|
1807
|
-
}
|
|
1808
|
-
return null;
|
|
1809
|
-
}
|
|
1810
|
-
/**
|
|
1811
|
-
* Execute tasks in parallel batches
|
|
1812
|
-
*/
|
|
1813
|
-
async executeTasksParallel(tasks) {
|
|
1814
|
-
if (tasks.length === 0) {
|
|
1815
|
-
return [];
|
|
1816
|
-
}
|
|
1817
|
-
const results = [];
|
|
1818
|
-
const batches = this.createBatches(tasks, this.maxWorkers);
|
|
1819
|
-
for (const batch of batches) {
|
|
1820
|
-
const batchResults = await Promise.all(
|
|
1821
|
-
batch.map((task) => this.executeTask(task))
|
|
1822
|
-
);
|
|
1823
|
-
results.push(...batchResults);
|
|
1824
|
-
}
|
|
1825
|
-
return results;
|
|
1826
|
-
}
|
|
1827
|
-
/**
|
|
1828
|
-
* Create batches for parallel execution
|
|
1829
|
-
*/
|
|
1830
|
-
createBatches(tasks, batchSize) {
|
|
1831
|
-
const batches = [];
|
|
1832
|
-
for (let i = 0; i < tasks.length; i += batchSize) {
|
|
1833
|
-
batches.push(tasks.slice(i, i + batchSize));
|
|
1834
|
-
}
|
|
1835
|
-
return batches;
|
|
1836
|
-
}
|
|
1837
|
-
/**
|
|
1838
|
-
* Execute a single task
|
|
1839
|
-
*/
|
|
1840
|
-
async executeTask(task) {
|
|
1841
|
-
const startTime = Date.now();
|
|
1842
|
-
this.streaming?.startAgent(task.agent.name);
|
|
1843
|
-
try {
|
|
1844
|
-
const result = this.canUseWorkers() ? await this.executeTaskInWorker(task) : await task.agent.scan(task.files, task.context);
|
|
1845
|
-
const executionTime = Date.now() - startTime;
|
|
1846
|
-
this.streaming?.completeAgent(task.agent.name, result.issues);
|
|
1847
|
-
return {
|
|
1848
|
-
agent: task.agent.name,
|
|
1849
|
-
result,
|
|
1850
|
-
fromCache: false,
|
|
1851
|
-
executionTime
|
|
1852
|
-
};
|
|
1853
|
-
} catch (error) {
|
|
1854
|
-
const executionTime = Date.now() - startTime;
|
|
1855
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1856
|
-
this.streaming?.reportError(new Error(errorMessage), `Agent: ${task.agent.name}`);
|
|
1857
|
-
return {
|
|
1858
|
-
agent: task.agent.name,
|
|
1859
|
-
result: {
|
|
1860
|
-
agent: task.agent.name,
|
|
1861
|
-
issues: [],
|
|
1862
|
-
executionTime,
|
|
1863
|
-
success: false,
|
|
1864
|
-
error: errorMessage
|
|
1865
|
-
},
|
|
1866
|
-
fromCache: false,
|
|
1867
|
-
executionTime
|
|
1868
|
-
};
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
canUseWorkers() {
|
|
1872
|
-
if (!this.useWorkerThreads) {
|
|
1873
|
-
return false;
|
|
1874
|
-
}
|
|
1875
|
-
if (this.workerAvailable !== null) {
|
|
1876
|
-
return this.workerAvailable;
|
|
1877
|
-
}
|
|
1878
|
-
const workerUrl = this.getWorkerUrl();
|
|
1879
|
-
this.workerAvailable = existsSync8(fileURLToPath(workerUrl));
|
|
1880
|
-
if (!this.workerAvailable && !this.warnedWorkerFallback && !isInteractiveMode()) {
|
|
1881
|
-
console.error("Worker threads unavailable; falling back to in-process agents.");
|
|
1882
|
-
this.warnedWorkerFallback = true;
|
|
1883
|
-
}
|
|
1884
|
-
return this.workerAvailable;
|
|
1885
|
-
}
|
|
1886
|
-
getWorkerUrl() {
|
|
1887
|
-
const distDir = new URL(".", import.meta.url);
|
|
1888
|
-
return new URL("workers/agent-worker.js", distDir);
|
|
1889
|
-
}
|
|
1890
|
-
async executeTaskInWorker(task) {
|
|
1891
|
-
const workerUrl = this.getWorkerUrl();
|
|
1892
|
-
return new Promise((resolve2, reject) => {
|
|
1893
|
-
const worker = new Worker(workerUrl, {
|
|
1894
|
-
workerData: {
|
|
1895
|
-
agentName: task.agent.name,
|
|
1896
|
-
files: task.files,
|
|
1897
|
-
context: task.context
|
|
1898
|
-
}
|
|
1899
|
-
});
|
|
1900
|
-
this.activeWorkers.add(worker);
|
|
1901
|
-
const timeout = setTimeout(() => {
|
|
1902
|
-
worker.terminate().catch(() => void 0);
|
|
1903
|
-
reject(new Error(`Agent ${task.agent.name} timed out after ${task.timeoutMs}ms`));
|
|
1904
|
-
}, task.timeoutMs);
|
|
1905
|
-
worker.on("message", (message) => {
|
|
1906
|
-
if (message?.type === "result") {
|
|
1907
|
-
clearTimeout(timeout);
|
|
1908
|
-
resolve2(message.result);
|
|
1909
|
-
} else if (message?.type === "error") {
|
|
1910
|
-
clearTimeout(timeout);
|
|
1911
|
-
reject(new Error(message.error));
|
|
1912
|
-
}
|
|
1913
|
-
});
|
|
1914
|
-
worker.on("error", (error) => {
|
|
1915
|
-
clearTimeout(timeout);
|
|
1916
|
-
reject(error);
|
|
1917
|
-
});
|
|
1918
|
-
worker.on("exit", (code) => {
|
|
1919
|
-
this.activeWorkers.delete(worker);
|
|
1920
|
-
if (code !== 0) {
|
|
1921
|
-
clearTimeout(timeout);
|
|
1922
|
-
reject(new Error(`Worker stopped with exit code ${code}`));
|
|
1923
|
-
}
|
|
1924
|
-
});
|
|
1925
|
-
});
|
|
1926
|
-
}
|
|
1927
|
-
/**
|
|
1928
|
-
* Cache results for future use
|
|
1929
|
-
*/
|
|
1930
|
-
async cacheResults(results) {
|
|
1931
|
-
if (!this.cacheEnabled || !this.cache) {
|
|
1932
|
-
return;
|
|
1933
|
-
}
|
|
1934
|
-
const cachePromises = results.filter((r) => r.result.success && !r.fromCache).map((r) => {
|
|
1935
|
-
const issuesByFile = this.groupIssuesByFile(r.result.issues);
|
|
1936
|
-
const perFilePromises = Object.entries(issuesByFile).map(
|
|
1937
|
-
([file, issues]) => this.cache.setCached(file, r.agent, issues, r.executionTime)
|
|
1938
|
-
);
|
|
1939
|
-
return Promise.all(perFilePromises);
|
|
1940
|
-
});
|
|
1941
|
-
await Promise.allSettled(cachePromises);
|
|
1942
|
-
}
|
|
1943
|
-
/**
|
|
1944
|
-
* Cleanup resources
|
|
1945
|
-
*/
|
|
1946
|
-
async cleanup() {
|
|
1947
|
-
const terminationPromises = Array.from(this.activeWorkers).map(
|
|
1948
|
-
(worker) => worker.terminate()
|
|
1949
|
-
);
|
|
1950
|
-
await Promise.allSettled(terminationPromises);
|
|
1951
|
-
this.activeWorkers.clear();
|
|
1952
|
-
}
|
|
1953
|
-
groupIssuesByFile(issues) {
|
|
1954
|
-
const grouped = {};
|
|
1955
|
-
for (const issue of issues) {
|
|
1956
|
-
if (!grouped[issue.file]) {
|
|
1957
|
-
grouped[issue.file] = [];
|
|
1958
|
-
}
|
|
1959
|
-
grouped[issue.file].push(issue);
|
|
1960
|
-
}
|
|
1961
|
-
return grouped;
|
|
1962
|
-
}
|
|
1963
|
-
};
|
|
1964
|
-
function calculateOptimalConcurrency() {
|
|
1965
|
-
const numCPUs = cpus().length;
|
|
1966
|
-
const availableMemoryGB = process.memoryUsage().rss / 1024 / 1024 / 1024;
|
|
1967
|
-
let optimal = Math.max(2, Math.min(numCPUs - 1, 8));
|
|
1968
|
-
if (availableMemoryGB < 2) {
|
|
1969
|
-
optimal = Math.max(2, Math.floor(optimal / 2));
|
|
1970
|
-
}
|
|
1971
|
-
if (numCPUs > 8) {
|
|
1972
|
-
optimal = Math.min(optimal + 2, 12);
|
|
1973
|
-
}
|
|
1974
|
-
return optimal;
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
// src/utils/cache-manager.ts
|
|
1978
|
-
import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir6, stat } from "fs/promises";
|
|
1979
|
-
import { join as join7 } from "path";
|
|
1980
|
-
import { createHash as createHash2 } from "crypto";
|
|
1981
|
-
var CacheManager = class {
|
|
1982
|
-
cacheDir;
|
|
1983
|
-
indexPath;
|
|
1984
|
-
VERSION = "1.0.0";
|
|
1985
|
-
MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
1986
|
-
// 24 hours
|
|
1987
|
-
MAX_ENTRIES = 1e3;
|
|
1988
|
-
constructor(baseDir) {
|
|
1989
|
-
this.cacheDir = join7(getTrieDirectory(baseDir), "cache");
|
|
1990
|
-
this.indexPath = join7(this.cacheDir, "index.json");
|
|
1991
|
-
}
|
|
1992
|
-
/**
|
|
1993
|
-
* Generate cache key for a file and agent combination
|
|
1994
|
-
*/
|
|
1995
|
-
generateCacheKey(filePath, agent, fileHash) {
|
|
1996
|
-
const key = `${filePath}:${agent}:${fileHash}`;
|
|
1997
|
-
return createHash2("sha256").update(key).digest("hex").slice(0, 16);
|
|
1998
|
-
}
|
|
1999
|
-
/**
|
|
2000
|
-
* Get file hash for cache validation
|
|
2001
|
-
*/
|
|
2002
|
-
async getFileHash(filePath) {
|
|
2003
|
-
try {
|
|
2004
|
-
const content = await readFile6(filePath, "utf-8");
|
|
2005
|
-
const stats = await stat(filePath);
|
|
2006
|
-
const hash = createHash2("sha256").update(content).digest("hex").slice(0, 16);
|
|
2007
|
-
return {
|
|
2008
|
-
hash,
|
|
2009
|
-
size: stats.size,
|
|
2010
|
-
mtime: stats.mtime.getTime()
|
|
2011
|
-
};
|
|
2012
|
-
} catch {
|
|
2013
|
-
return { hash: "", size: 0, mtime: 0 };
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
/**
|
|
2017
|
-
* Load cache index
|
|
2018
|
-
*/
|
|
2019
|
-
async loadIndex() {
|
|
2020
|
-
try {
|
|
2021
|
-
const content = await readFile6(this.indexPath, "utf-8");
|
|
2022
|
-
return JSON.parse(content);
|
|
2023
|
-
} catch {
|
|
2024
|
-
return {
|
|
2025
|
-
version: this.VERSION,
|
|
2026
|
-
created: Date.now(),
|
|
2027
|
-
entries: {}
|
|
2028
|
-
};
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
/**
|
|
2032
|
-
* Save cache index
|
|
2033
|
-
*/
|
|
2034
|
-
async saveIndex(index) {
|
|
2035
|
-
try {
|
|
2036
|
-
await mkdir6(this.cacheDir, { recursive: true });
|
|
2037
|
-
await writeFile5(this.indexPath, JSON.stringify(index, null, 2));
|
|
2038
|
-
} catch (error) {
|
|
2039
|
-
if (!isInteractiveMode()) {
|
|
2040
|
-
console.warn("Failed to save cache index:", error);
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
/**
|
|
2045
|
-
* Clean up expired entries
|
|
2046
|
-
*/
|
|
2047
|
-
cleanupExpired(index) {
|
|
2048
|
-
const now = Date.now();
|
|
2049
|
-
const validEntries = {};
|
|
2050
|
-
for (const [key, entry] of Object.entries(index.entries)) {
|
|
2051
|
-
if (now - entry.timestamp < this.MAX_AGE_MS) {
|
|
2052
|
-
validEntries[key] = entry;
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
const entries = Object.entries(validEntries);
|
|
2056
|
-
if (entries.length > this.MAX_ENTRIES) {
|
|
2057
|
-
entries.sort((a, b) => b[1].timestamp - a[1].timestamp);
|
|
2058
|
-
const limited = entries.slice(0, this.MAX_ENTRIES);
|
|
2059
|
-
return {
|
|
2060
|
-
...index,
|
|
2061
|
-
entries: Object.fromEntries(limited)
|
|
2062
|
-
};
|
|
2063
|
-
}
|
|
2064
|
-
return {
|
|
2065
|
-
...index,
|
|
2066
|
-
entries: validEntries
|
|
2067
|
-
};
|
|
2068
|
-
}
|
|
2069
|
-
/**
|
|
2070
|
-
* Get cached result for a file and agent
|
|
2071
|
-
*
|
|
2072
|
-
* Cache automatically invalidates when files change:
|
|
2073
|
-
* - Cache key includes file hash: hash(filePath:agent:fileHash)
|
|
2074
|
-
* - When file changes, hash changes, so cache key changes
|
|
2075
|
-
* - Old cache entry won't be found (different key)
|
|
2076
|
-
* - File is automatically rescanned
|
|
2077
|
-
*
|
|
2078
|
-
* This means cache auto-updates when Claude fixes code - no manual invalidation needed!
|
|
2079
|
-
*/
|
|
2080
|
-
async getCached(filePath, agent) {
|
|
2081
|
-
try {
|
|
2082
|
-
const { hash, size: _size, mtime: _mtime } = await this.getFileHash(filePath);
|
|
2083
|
-
if (!hash) return null;
|
|
2084
|
-
const index = await this.loadIndex();
|
|
2085
|
-
const cacheKey = this.generateCacheKey(filePath, agent, hash);
|
|
2086
|
-
const entry = index.entries[cacheKey];
|
|
2087
|
-
if (!entry) return null;
|
|
2088
|
-
const isValid = entry.fileHash === hash && entry.version === this.VERSION && Date.now() - entry.timestamp < this.MAX_AGE_MS;
|
|
2089
|
-
if (!isValid) {
|
|
2090
|
-
delete index.entries[cacheKey];
|
|
2091
|
-
await this.saveIndex(index);
|
|
2092
|
-
return null;
|
|
2093
|
-
}
|
|
2094
|
-
return entry.issues;
|
|
2095
|
-
} catch {
|
|
2096
|
-
return null;
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
/**
|
|
2100
|
-
* Cache result for a file and agent
|
|
2101
|
-
*/
|
|
2102
|
-
async setCached(filePath, agent, issues, executionTime) {
|
|
2103
|
-
try {
|
|
2104
|
-
const { hash, size } = await this.getFileHash(filePath);
|
|
2105
|
-
if (!hash) return;
|
|
2106
|
-
const index = await this.loadIndex();
|
|
2107
|
-
const cacheKey = this.generateCacheKey(filePath, agent, hash);
|
|
2108
|
-
index.entries[cacheKey] = {
|
|
2109
|
-
version: this.VERSION,
|
|
2110
|
-
timestamp: Date.now(),
|
|
2111
|
-
fileHash: hash,
|
|
2112
|
-
fileSize: size,
|
|
2113
|
-
agent,
|
|
2114
|
-
issues,
|
|
2115
|
-
executionTime
|
|
2116
|
-
};
|
|
2117
|
-
const cleanedIndex = this.cleanupExpired(index);
|
|
2118
|
-
await this.saveIndex(cleanedIndex);
|
|
2119
|
-
} catch (error) {
|
|
2120
|
-
if (!isInteractiveMode()) {
|
|
2121
|
-
console.warn("Failed to cache result:", error);
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
|
-
/**
|
|
2126
|
-
* Check if multiple files have cached results
|
|
2127
|
-
*/
|
|
2128
|
-
async getCachedBatch(files, agent) {
|
|
2129
|
-
const results = /* @__PURE__ */ new Map();
|
|
2130
|
-
await Promise.all(
|
|
2131
|
-
files.map(async (file) => {
|
|
2132
|
-
const cached = await this.getCached(file, agent);
|
|
2133
|
-
if (cached) {
|
|
2134
|
-
results.set(file, cached);
|
|
2135
|
-
}
|
|
2136
|
-
})
|
|
2137
|
-
);
|
|
2138
|
-
return results;
|
|
2139
|
-
}
|
|
2140
|
-
/**
|
|
2141
|
-
* Get cache statistics
|
|
2142
|
-
*/
|
|
2143
|
-
async getStats() {
|
|
2144
|
-
try {
|
|
2145
|
-
const index = await this.loadIndex();
|
|
2146
|
-
const entries = Object.values(index.entries);
|
|
2147
|
-
const totalSizeKB = entries.reduce((acc, entry) => acc + entry.fileSize, 0) / 1024;
|
|
2148
|
-
const timestamps = entries.map((e) => e.timestamp);
|
|
2149
|
-
const agents = [...new Set(entries.map((e) => e.agent))];
|
|
2150
|
-
return {
|
|
2151
|
-
totalEntries: entries.length,
|
|
2152
|
-
totalSizeKB: Math.round(totalSizeKB),
|
|
2153
|
-
oldestEntry: timestamps.length > 0 ? Math.min(...timestamps) : null,
|
|
2154
|
-
newestEntry: timestamps.length > 0 ? Math.max(...timestamps) : null,
|
|
2155
|
-
agents
|
|
2156
|
-
};
|
|
2157
|
-
} catch {
|
|
2158
|
-
return {
|
|
2159
|
-
totalEntries: 0,
|
|
2160
|
-
totalSizeKB: 0,
|
|
2161
|
-
oldestEntry: null,
|
|
2162
|
-
newestEntry: null,
|
|
2163
|
-
agents: []
|
|
2164
|
-
};
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
/**
|
|
2168
|
-
* Clean up stale cache entries by verifying file hashes
|
|
2169
|
-
* This removes entries where files have changed or no longer exist
|
|
2170
|
-
* Called periodically to keep cache clean
|
|
2171
|
-
*
|
|
2172
|
-
* Note: Since cache keys are hashed, we can't easily reverse-engineer file paths.
|
|
2173
|
-
* However, when getCached() is called, it naturally invalidates stale entries
|
|
2174
|
-
* by checking if the current file hash matches the cached hash. This method
|
|
2175
|
-
* proactively cleans up entries for known changed files.
|
|
2176
|
-
*/
|
|
2177
|
-
async cleanupStaleEntries(filePaths) {
|
|
2178
|
-
try {
|
|
2179
|
-
const index = await this.loadIndex();
|
|
2180
|
-
let removedCount = 0;
|
|
2181
|
-
const keysToRemove = [];
|
|
2182
|
-
if (filePaths && filePaths.length > 0) {
|
|
2183
|
-
const agents = /* @__PURE__ */ new Set();
|
|
2184
|
-
for (const entry of Object.values(index.entries)) {
|
|
2185
|
-
agents.add(entry.agent);
|
|
2186
|
-
}
|
|
2187
|
-
for (const filePath of filePaths) {
|
|
2188
|
-
try {
|
|
2189
|
-
const { hash: currentHash } = await this.getFileHash(filePath);
|
|
2190
|
-
if (!currentHash) {
|
|
2191
|
-
continue;
|
|
2192
|
-
}
|
|
2193
|
-
for (const agent of agents) {
|
|
2194
|
-
for (const [key, entry] of Object.entries(index.entries)) {
|
|
2195
|
-
if (entry.agent !== agent) continue;
|
|
2196
|
-
if (entry.fileHash !== currentHash) {
|
|
2197
|
-
const oldKey = this.generateCacheKey(filePath, agent, entry.fileHash);
|
|
2198
|
-
if (oldKey === key) {
|
|
2199
|
-
keysToRemove.push(key);
|
|
2200
|
-
removedCount++;
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
} catch {
|
|
2206
|
-
continue;
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
const uniqueKeys = [...new Set(keysToRemove)];
|
|
2211
|
-
for (const key of uniqueKeys) {
|
|
2212
|
-
delete index.entries[key];
|
|
2213
|
-
}
|
|
2214
|
-
if (removedCount > 0) {
|
|
2215
|
-
await this.saveIndex(index);
|
|
2216
|
-
}
|
|
2217
|
-
return removedCount;
|
|
2218
|
-
} catch (error) {
|
|
2219
|
-
if (!isInteractiveMode()) {
|
|
2220
|
-
console.warn("Failed to cleanup stale cache entries:", error);
|
|
2221
|
-
}
|
|
2222
|
-
return 0;
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
/**
|
|
2226
|
-
* Clear all cache
|
|
2227
|
-
*/
|
|
2228
|
-
async clear() {
|
|
2229
|
-
try {
|
|
2230
|
-
const emptyIndex = {
|
|
2231
|
-
version: this.VERSION,
|
|
2232
|
-
created: Date.now(),
|
|
2233
|
-
entries: {}
|
|
2234
|
-
};
|
|
2235
|
-
await this.saveIndex(emptyIndex);
|
|
2236
|
-
} catch (error) {
|
|
2237
|
-
if (!isInteractiveMode()) {
|
|
2238
|
-
console.warn("Failed to clear cache:", error);
|
|
2239
|
-
}
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
};
|
|
2243
|
-
|
|
2244
|
-
// src/orchestrator/executor.ts
|
|
2245
|
-
var Executor = class {
|
|
2246
|
-
async executeAgents(agents, files, context, options) {
|
|
2247
|
-
const parallel = options?.parallel ?? true;
|
|
2248
|
-
const cacheEnabled = options?.cacheEnabled ?? true;
|
|
2249
|
-
const maxConcurrency = options?.maxConcurrency ?? calculateOptimalConcurrency();
|
|
2250
|
-
const useWorkerThreads = options?.useWorkerThreads ?? false;
|
|
2251
|
-
if (!isInteractiveMode()) {
|
|
2252
|
-
console.error(`Executing ${agents.length} scouts ${parallel ? "in parallel" : "sequentially"}...`);
|
|
2253
|
-
}
|
|
2254
|
-
if (parallel) {
|
|
2255
|
-
const cacheManager = cacheEnabled ? new CacheManager(context.workingDir) : null;
|
|
2256
|
-
const executor = new ParallelExecutor(cacheManager, maxConcurrency, {
|
|
2257
|
-
cacheEnabled,
|
|
2258
|
-
useWorkerThreads
|
|
2259
|
-
});
|
|
2260
|
-
if (options?.streaming) {
|
|
2261
|
-
executor.setStreaming(options.streaming);
|
|
2262
|
-
}
|
|
2263
|
-
const results = await executor.executeAgents(agents, files, {
|
|
2264
|
-
...context,
|
|
2265
|
-
config: { timeoutMs: options?.timeoutMs ?? 12e4 }
|
|
2266
|
-
});
|
|
2267
|
-
return agents.map((agent) => results.get(agent.name)).filter(Boolean);
|
|
2268
|
-
}
|
|
2269
|
-
const promises = agents.map(
|
|
2270
|
-
(agent) => this.executeAgentWithTimeout(agent, files, context, options?.timeoutMs ?? 3e4)
|
|
2271
|
-
);
|
|
2272
|
-
try {
|
|
2273
|
-
const results = await Promise.allSettled(promises);
|
|
2274
|
-
return results.map((result, index) => {
|
|
2275
|
-
if (result.status === "fulfilled") {
|
|
2276
|
-
if (!isInteractiveMode()) {
|
|
2277
|
-
console.error(`${agents[index].name} completed in ${result.value.executionTime}ms`);
|
|
2278
|
-
}
|
|
2279
|
-
return result.value;
|
|
2280
|
-
} else {
|
|
2281
|
-
if (!isInteractiveMode()) {
|
|
2282
|
-
console.error(`${agents[index].name} failed:`, result.reason);
|
|
2283
|
-
}
|
|
2284
|
-
return {
|
|
2285
|
-
agent: agents[index].name,
|
|
2286
|
-
issues: [],
|
|
2287
|
-
executionTime: 0,
|
|
2288
|
-
success: false,
|
|
2289
|
-
error: result.reason instanceof Error ? result.reason.message : String(result.reason)
|
|
2290
|
-
};
|
|
2291
|
-
}
|
|
2292
|
-
});
|
|
2293
|
-
} catch (error) {
|
|
2294
|
-
if (!isInteractiveMode()) {
|
|
2295
|
-
console.error("Executor error:", error);
|
|
2296
|
-
}
|
|
2297
|
-
return agents.map((agent) => ({
|
|
2298
|
-
agent: agent.name,
|
|
2299
|
-
issues: [],
|
|
2300
|
-
executionTime: 0,
|
|
2301
|
-
success: false,
|
|
2302
|
-
error: "Execution failed"
|
|
2303
|
-
}));
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
async executeAgentWithTimeout(agent, files, context, timeoutMs = 3e4) {
|
|
2307
|
-
return new Promise(async (resolve2, reject) => {
|
|
2308
|
-
const timeout = setTimeout(() => {
|
|
2309
|
-
reject(new Error(`Agent ${agent.name} timed out after ${timeoutMs}ms`));
|
|
2310
|
-
}, timeoutMs);
|
|
2311
|
-
try {
|
|
2312
|
-
const result = await agent.scan(files, context);
|
|
2313
|
-
clearTimeout(timeout);
|
|
2314
|
-
resolve2(result);
|
|
2315
|
-
} catch (error) {
|
|
2316
|
-
clearTimeout(timeout);
|
|
2317
|
-
reject(error);
|
|
2318
|
-
}
|
|
2319
|
-
});
|
|
2320
|
-
}
|
|
2321
|
-
};
|
|
2322
|
-
|
|
2323
|
-
// src/agent/reason.ts
|
|
2324
|
-
function buildDefaultCodeContext() {
|
|
2325
|
-
return {
|
|
2326
|
-
changeType: "general",
|
|
2327
|
-
isNewFeature: false,
|
|
2328
|
-
touchesUserData: false,
|
|
2329
|
-
touchesAuth: false,
|
|
2330
|
-
touchesPayments: false,
|
|
2331
|
-
touchesDatabase: false,
|
|
2332
|
-
touchesAPI: false,
|
|
2333
|
-
touchesUI: false,
|
|
2334
|
-
touchesHealthData: false,
|
|
2335
|
-
touchesSecurityConfig: false,
|
|
2336
|
-
linesChanged: 50,
|
|
2337
|
-
filePatterns: [],
|
|
2338
|
-
framework: "unknown",
|
|
2339
|
-
language: "typescript",
|
|
2340
|
-
touchesCrypto: false,
|
|
2341
|
-
touchesFileSystem: false,
|
|
2342
|
-
touchesThirdPartyAPI: false,
|
|
2343
|
-
touchesLogging: false,
|
|
2344
|
-
touchesErrorHandling: false,
|
|
2345
|
-
hasTests: false,
|
|
2346
|
-
complexity: "medium",
|
|
2347
|
-
patterns: {
|
|
2348
|
-
hasAsyncCode: false,
|
|
2349
|
-
hasFormHandling: false,
|
|
2350
|
-
hasFileUploads: false,
|
|
2351
|
-
hasEmailHandling: false,
|
|
2352
|
-
hasRateLimiting: false,
|
|
2353
|
-
hasWebSockets: false,
|
|
2354
|
-
hasCaching: false,
|
|
2355
|
-
hasQueue: false
|
|
2356
|
-
}
|
|
2357
|
-
};
|
|
2358
|
-
}
|
|
2359
|
-
function buildExplanation(result) {
|
|
2360
|
-
const top = [...result.files].sort((a, b) => b.score - a.score)[0];
|
|
2361
|
-
if (!top) return `Risk level ${result.overall} (no files provided)`;
|
|
2362
|
-
return `Risk level ${result.overall} because ${top.file} ${top.reasons.join(", ")}`;
|
|
2363
|
-
}
|
|
2364
|
-
function buildRecommendation(risk, hasAntiPattern) {
|
|
2365
|
-
if (hasAntiPattern || risk === "critical") {
|
|
2366
|
-
return "Block until reviewed: address anti-patterns and rerun targeted tests.";
|
|
2367
|
-
}
|
|
2368
|
-
if (risk === "high") {
|
|
2369
|
-
return "Require senior review and run full test suite before merge.";
|
|
2370
|
-
}
|
|
2371
|
-
if (risk === "medium") {
|
|
2372
|
-
return "Proceed with caution; run impacted tests and sanity checks.";
|
|
2373
|
-
}
|
|
2374
|
-
return "Low risk; proceed but keep an eye on recent changes.";
|
|
2375
|
-
}
|
|
2376
|
-
async function reasonAboutChanges(projectPath, files, options = {}) {
|
|
2377
|
-
const graph = new ContextGraph(projectPath);
|
|
2378
|
-
const { matches, byFile } = await matchPatternsForFiles(graph, files);
|
|
2379
|
-
const changeRisk = await scoreChangeSet(graph, files, byFile);
|
|
2380
|
-
const incidents = [];
|
|
2381
|
-
for (const file of files) {
|
|
2382
|
-
const fileIncidents = await graph.getIncidentsForFile(file);
|
|
2383
|
-
incidents.push(...fileIncidents);
|
|
2384
|
-
}
|
|
2385
|
-
const hasAntiPattern = matches.some((m) => m.isAntiPattern);
|
|
2386
|
-
const riskLevel = hasAntiPattern ? "critical" : changeRisk.overall;
|
|
2387
|
-
const shouldBlock = hasAntiPattern || riskLevel === "critical" || riskLevel === "high";
|
|
2388
|
-
const reasoning = {
|
|
2389
|
-
riskLevel,
|
|
2390
|
-
shouldBlock,
|
|
2391
|
-
explanation: buildExplanation(changeRisk),
|
|
2392
|
-
relevantIncidents: incidents,
|
|
2393
|
-
matchedPatterns: matches.map((m) => m.pattern),
|
|
2394
|
-
recommendation: buildRecommendation(riskLevel, hasAntiPattern),
|
|
2395
|
-
files: changeRisk.files
|
|
2396
|
-
};
|
|
2397
|
-
if (options.runAgents) {
|
|
2398
|
-
const codeContext = options.codeContext ?? buildDefaultCodeContext();
|
|
2399
|
-
const triager = new Triager();
|
|
2400
|
-
const agents = await triager.triage(codeContext);
|
|
2401
|
-
if (agents.length > 0) {
|
|
2402
|
-
const executor = new Executor();
|
|
2403
|
-
const scanContext = {
|
|
2404
|
-
workingDir: projectPath,
|
|
2405
|
-
...options.scanContext
|
|
2406
|
-
};
|
|
2407
|
-
if (codeContext.framework) scanContext.framework = codeContext.framework;
|
|
2408
|
-
if (codeContext.language) scanContext.language = codeContext.language;
|
|
2409
|
-
reasoning.agentResults = await executor.executeAgents(agents, files, scanContext, {
|
|
2410
|
-
parallel: true,
|
|
2411
|
-
timeoutMs: options.scanContext?.config?.timeoutMs ?? 6e4
|
|
2412
|
-
});
|
|
2413
|
-
} else {
|
|
2414
|
-
reasoning.agentResults = [];
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
2417
|
-
return reasoning;
|
|
2418
|
-
}
|
|
2419
|
-
async function reasonAboutChangesHumanReadable(projectPath, files, options = {}) {
|
|
2420
|
-
const reasoning = await reasonAboutChanges(projectPath, files, options);
|
|
2421
|
-
const { humanizeReasoning } = await import("./comprehension-46F7ZNKL.js");
|
|
2422
|
-
return humanizeReasoning(reasoning);
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
// src/bootstrap/stack-detector.ts
|
|
2426
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
2427
|
-
import { existsSync as existsSync9 } from "fs";
|
|
2428
|
-
import { join as join8 } from "path";
|
|
2429
|
-
var SKILL_MAPPINGS = {
|
|
2430
|
-
// Frontend Frameworks - React/Next.js
|
|
2431
|
-
"next": [
|
|
2432
|
-
"vercel-labs/agent-skills vercel-react-best-practices",
|
|
2433
|
-
"vercel-labs/agent-skills web-design-guidelines",
|
|
2434
|
-
"anthropics/skills frontend-design",
|
|
2435
|
-
"wshobson/agents nextjs-app-router-patterns"
|
|
2436
|
-
],
|
|
2437
|
-
"react": [
|
|
2438
|
-
"vercel-labs/agent-skills vercel-react-best-practices",
|
|
2439
|
-
"anthropics/skills frontend-design",
|
|
2440
|
-
"wshobson/agents react-state-management"
|
|
2441
|
-
],
|
|
2442
|
-
// Vue/Nuxt Ecosystem
|
|
2443
|
-
"vue": [
|
|
2444
|
-
"hyf0/vue-skills vue-best-practices",
|
|
2445
|
-
"hyf0/vue-skills pinia-best-practices",
|
|
2446
|
-
"hyf0/vue-skills vueuse-best-practices",
|
|
2447
|
-
"onmax/nuxt-skills vue"
|
|
2448
|
-
],
|
|
2449
|
-
"nuxt": [
|
|
2450
|
-
"onmax/nuxt-skills nuxt",
|
|
2451
|
-
"onmax/nuxt-skills nuxt-ui",
|
|
2452
|
-
"onmax/nuxt-skills nuxt-content",
|
|
2453
|
-
"onmax/nuxt-skills nuxt-modules",
|
|
2454
|
-
"onmax/nuxt-skills nuxt-better-auth",
|
|
2455
|
-
"onmax/nuxt-skills nuxthub"
|
|
2456
|
-
],
|
|
2457
|
-
"pinia": [
|
|
2458
|
-
"hyf0/vue-skills pinia-best-practices"
|
|
2459
|
-
],
|
|
2460
|
-
"@vueuse/core": [
|
|
2461
|
-
"hyf0/vue-skills vueuse-best-practices",
|
|
2462
|
-
"onmax/nuxt-skills vueuse"
|
|
2463
|
-
],
|
|
2464
|
-
// Mobile - Expo
|
|
2465
|
-
"expo": [
|
|
2466
|
-
"expo/skills building-native-ui",
|
|
2467
|
-
"expo/skills upgrading-expo",
|
|
2468
|
-
"expo/skills native-data-fetching",
|
|
2469
|
-
"expo/skills expo-dev-client",
|
|
2470
|
-
"expo/skills expo-deployment",
|
|
2471
|
-
"expo/skills expo-api-routes",
|
|
2472
|
-
"expo/skills expo-tailwind-setup",
|
|
2473
|
-
"expo/skills expo-cicd-workflows",
|
|
2474
|
-
"expo/skills use-dom"
|
|
2475
|
-
],
|
|
2476
|
-
// Mobile - React Native
|
|
2477
|
-
"react-native": [
|
|
2478
|
-
"callstackincubator/agent-skills react-native-best-practices",
|
|
2479
|
-
"wshobson/agents react-native-architecture"
|
|
2480
|
-
],
|
|
2481
|
-
// Backend Frameworks
|
|
2482
|
-
"@nestjs/core": [
|
|
2483
|
-
"kadajett/agent-nestjs-skills nestjs-best-practices"
|
|
2484
|
-
],
|
|
2485
|
-
"nestjs": [
|
|
2486
|
-
"kadajett/agent-nestjs-skills nestjs-best-practices"
|
|
2487
|
-
],
|
|
2488
|
-
"elysia": [
|
|
2489
|
-
"elysiajs/skills elysiajs"
|
|
2490
|
-
],
|
|
2491
|
-
"hono": [
|
|
2492
|
-
"elysiajs/skills elysiajs"
|
|
2493
|
-
],
|
|
2494
|
-
// Database/BaaS
|
|
2495
|
-
"@supabase/supabase-js": [
|
|
2496
|
-
"supabase/agent-skills supabase-postgres-best-practices"
|
|
2497
|
-
],
|
|
2498
|
-
"convex": [
|
|
2499
|
-
"waynesutton/convexskills convex-best-practices"
|
|
2500
|
-
],
|
|
2501
|
-
"pg": [
|
|
2502
|
-
"wshobson/agents postgresql-table-design"
|
|
2503
|
-
],
|
|
2504
|
-
"postgres": [
|
|
2505
|
-
"wshobson/agents postgresql-table-design"
|
|
2506
|
-
],
|
|
2507
|
-
// Auth
|
|
2508
|
-
"better-auth": [
|
|
2509
|
-
"better-auth/skills better-auth-best-practices",
|
|
2510
|
-
"better-auth/skills create-auth-skill"
|
|
2511
|
-
],
|
|
2512
|
-
// Payments
|
|
2513
|
-
"stripe": [
|
|
2514
|
-
"stripe/ai stripe-best-practices"
|
|
2515
|
-
],
|
|
2516
|
-
"@stripe/stripe-js": [
|
|
2517
|
-
"stripe/ai stripe-best-practices"
|
|
2518
|
-
],
|
|
2519
|
-
// Media/Graphics
|
|
2520
|
-
"remotion": [
|
|
2521
|
-
"remotion-dev/skills remotion-best-practices"
|
|
2522
|
-
],
|
|
2523
|
-
"three": [
|
|
2524
|
-
"cloudai-x/threejs-skills threejs-fundamentals",
|
|
2525
|
-
"cloudai-x/threejs-skills threejs-animation",
|
|
2526
|
-
"cloudai-x/threejs-skills threejs-materials",
|
|
2527
|
-
"cloudai-x/threejs-skills threejs-shaders",
|
|
2528
|
-
"cloudai-x/threejs-skills threejs-lighting",
|
|
2529
|
-
"cloudai-x/threejs-skills threejs-geometry",
|
|
2530
|
-
"cloudai-x/threejs-skills threejs-textures",
|
|
2531
|
-
"cloudai-x/threejs-skills threejs-loaders",
|
|
2532
|
-
"cloudai-x/threejs-skills threejs-interaction",
|
|
2533
|
-
"cloudai-x/threejs-skills threejs-postprocessing"
|
|
2534
|
-
],
|
|
2535
|
-
// UI Libraries
|
|
2536
|
-
"tailwindcss": [
|
|
2537
|
-
"wshobson/agents tailwind-design-system",
|
|
2538
|
-
"jezweb/claude-skills tailwind-v4-shadcn",
|
|
2539
|
-
"wshobson/agents responsive-design"
|
|
2540
|
-
],
|
|
2541
|
-
"@shadcn/ui": [
|
|
2542
|
-
"giuseppe-trisciuoglio/developer-kit shadcn-ui"
|
|
2543
|
-
],
|
|
2544
|
-
"shadcn": [
|
|
2545
|
-
"giuseppe-trisciuoglio/developer-kit shadcn-ui"
|
|
2546
|
-
],
|
|
2547
|
-
"@radix-ui/react-slot": [
|
|
2548
|
-
"onmax/nuxt-skills reka-ui"
|
|
2549
|
-
],
|
|
2550
|
-
// State Management
|
|
2551
|
-
"@tanstack/react-query": [
|
|
2552
|
-
"jezweb/claude-skills tanstack-query"
|
|
2553
|
-
],
|
|
2554
|
-
// Testing
|
|
2555
|
-
"playwright": [
|
|
2556
|
-
"anthropics/skills webapp-testing",
|
|
2557
|
-
"wshobson/agents e2e-testing-patterns"
|
|
2558
|
-
],
|
|
2559
|
-
"puppeteer": [
|
|
2560
|
-
"anthropics/skills webapp-testing"
|
|
2561
|
-
],
|
|
2562
|
-
"vitest": [
|
|
2563
|
-
"wshobson/agents e2e-testing-patterns"
|
|
2564
|
-
],
|
|
2565
|
-
"jest": [
|
|
2566
|
-
"wshobson/agents e2e-testing-patterns"
|
|
2567
|
-
],
|
|
2568
|
-
// DevTools/MCP
|
|
2569
|
-
"@modelcontextprotocol/sdk": [
|
|
2570
|
-
"anthropics/skills mcp-builder"
|
|
2571
|
-
],
|
|
2572
|
-
// Security
|
|
2573
|
-
"semgrep": [
|
|
2574
|
-
"trailofbits/skills semgrep"
|
|
2575
|
-
],
|
|
2576
|
-
// Monorepos
|
|
2577
|
-
"turbo": [
|
|
2578
|
-
"wshobson/agents monorepo-management"
|
|
2579
|
-
],
|
|
2580
|
-
"nx": [
|
|
2581
|
-
"wshobson/agents monorepo-management"
|
|
2582
|
-
],
|
|
2583
|
-
"lerna": [
|
|
2584
|
-
"wshobson/agents monorepo-management"
|
|
2585
|
-
],
|
|
2586
|
-
// TypeScript (handled separately based on tsconfig.json)
|
|
2587
|
-
"typescript": [
|
|
2588
|
-
"wshobson/agents typescript-advanced-types"
|
|
2589
|
-
]
|
|
2590
|
-
};
|
|
2591
|
-
var SKILL_CATEGORIES = {
|
|
2592
|
-
documents: [
|
|
2593
|
-
"anthropics/skills pdf",
|
|
2594
|
-
"anthropics/skills xlsx",
|
|
2595
|
-
"anthropics/skills pptx",
|
|
2596
|
-
"anthropics/skills docx",
|
|
2597
|
-
"anthropics/skills doc-coauthoring"
|
|
2598
|
-
],
|
|
2599
|
-
design: [
|
|
2600
|
-
"anthropics/skills canvas-design",
|
|
2601
|
-
"anthropics/skills theme-factory",
|
|
2602
|
-
"anthropics/skills web-artifacts-builder",
|
|
2603
|
-
"anthropics/skills algorithmic-art",
|
|
2604
|
-
"anthropics/skills brand-guidelines",
|
|
2605
|
-
"anthropics/skills slack-gif-creator",
|
|
2606
|
-
"nextlevelbuilder/ui-ux-pro-max ui-ux-pro-max",
|
|
2607
|
-
"superdesigndev/superdesign-skill superdesign",
|
|
2608
|
-
"wshobson/agents design-system-patterns"
|
|
2609
|
-
],
|
|
2610
|
-
marketing: [
|
|
2611
|
-
"coreyhaines31/marketingskills seo-audit",
|
|
2612
|
-
"coreyhaines31/marketingskills copywriting",
|
|
2613
|
-
"coreyhaines31/marketingskills marketing-psychology",
|
|
2614
|
-
"coreyhaines31/marketingskills programmatic-seo",
|
|
2615
|
-
"coreyhaines31/marketingskills marketing-ideas",
|
|
2616
|
-
"coreyhaines31/marketingskills copy-editing",
|
|
2617
|
-
"coreyhaines31/marketingskills pricing-strategy",
|
|
2618
|
-
"coreyhaines31/marketingskills social-content",
|
|
2619
|
-
"coreyhaines31/marketingskills launch-strategy",
|
|
2620
|
-
"coreyhaines31/marketingskills page-cro",
|
|
2621
|
-
"coreyhaines31/marketingskills competitor-alternatives",
|
|
2622
|
-
"coreyhaines31/marketingskills analytics-tracking",
|
|
2623
|
-
"coreyhaines31/marketingskills schema-markup",
|
|
2624
|
-
"coreyhaines31/marketingskills onboarding-cro",
|
|
2625
|
-
"coreyhaines31/marketingskills email-sequence",
|
|
2626
|
-
"coreyhaines31/marketingskills paid-ads",
|
|
2627
|
-
"coreyhaines31/marketingskills signup-flow-cro",
|
|
2628
|
-
"coreyhaines31/marketingskills free-tool-strategy",
|
|
2629
|
-
"coreyhaines31/marketingskills form-cro",
|
|
2630
|
-
"coreyhaines31/marketingskills paywall-upgrade-cro",
|
|
2631
|
-
"coreyhaines31/marketingskills referral-program",
|
|
2632
|
-
"coreyhaines31/marketingskills popup-cro",
|
|
2633
|
-
"coreyhaines31/marketingskills ab-test-setup"
|
|
2634
|
-
],
|
|
2635
|
-
development: [
|
|
2636
|
-
"obra/superpowers brainstorming",
|
|
2637
|
-
"obra/superpowers test-driven-development",
|
|
2638
|
-
"obra/superpowers systematic-debugging",
|
|
2639
|
-
"obra/superpowers writing-plans",
|
|
2640
|
-
"obra/superpowers executing-plans",
|
|
2641
|
-
"obra/superpowers verification-before-completion",
|
|
2642
|
-
"obra/superpowers using-superpowers",
|
|
2643
|
-
"obra/superpowers subagent-driven-development",
|
|
2644
|
-
"obra/superpowers requesting-code-review",
|
|
2645
|
-
"obra/superpowers writing-skills",
|
|
2646
|
-
"obra/superpowers dispatching-parallel-agents",
|
|
2647
|
-
"obra/superpowers receiving-code-review",
|
|
2648
|
-
"obra/superpowers using-git-worktrees",
|
|
2649
|
-
"obra/superpowers finishing-a-development-branch",
|
|
2650
|
-
"wshobson/agents code-review-excellence",
|
|
2651
|
-
"wshobson/agents api-design-principles",
|
|
2652
|
-
"wshobson/agents architecture-patterns",
|
|
2653
|
-
"wshobson/agents error-handling-patterns",
|
|
2654
|
-
"wshobson/agents nodejs-backend-patterns",
|
|
2655
|
-
"wshobson/agents microservices-patterns",
|
|
2656
|
-
"wshobson/agents modern-javascript-patterns",
|
|
2657
|
-
"wshobson/agents web-component-design",
|
|
2658
|
-
"wshobson/agents async-python-patterns",
|
|
2659
|
-
"wshobson/agents python-testing-patterns",
|
|
2660
|
-
"boristane/agent-skills logging-best-practices"
|
|
2661
|
-
],
|
|
2662
|
-
productivity: [
|
|
2663
|
-
"softaworks/agent-toolkit daily-meeting-update",
|
|
2664
|
-
"softaworks/agent-toolkit agent-md-refactor",
|
|
2665
|
-
"softaworks/agent-toolkit session-handoff",
|
|
2666
|
-
"softaworks/agent-toolkit meme-factory",
|
|
2667
|
-
"softaworks/agent-toolkit qa-test-planner",
|
|
2668
|
-
"softaworks/agent-toolkit writing-clearly-and-concisely",
|
|
2669
|
-
"softaworks/agent-toolkit commit-work",
|
|
2670
|
-
"softaworks/agent-toolkit mermaid-diagrams",
|
|
2671
|
-
"softaworks/agent-toolkit dependency-updater",
|
|
2672
|
-
"softaworks/agent-toolkit crafting-effective-readmes",
|
|
2673
|
-
"softaworks/agent-toolkit reducing-entropy",
|
|
2674
|
-
"softaworks/agent-toolkit feedback-mastery",
|
|
2675
|
-
"softaworks/agent-toolkit marp-slide",
|
|
2676
|
-
"softaworks/agent-toolkit professional-communication",
|
|
2677
|
-
"softaworks/agent-toolkit difficult-workplace-conversations",
|
|
2678
|
-
"anthropics/skills internal-comms",
|
|
2679
|
-
"othmanadi/planning-with-files planning-with-files"
|
|
2680
|
-
],
|
|
2681
|
-
security: [
|
|
2682
|
-
"trailofbits/skills semgrep",
|
|
2683
|
-
"trailofbits/skills secure-workflow-guide",
|
|
2684
|
-
"trailofbits/skills codeql",
|
|
2685
|
-
"trailofbits/skills property-based-testing",
|
|
2686
|
-
"trailofbits/skills variant-analysis",
|
|
2687
|
-
"trailofbits/skills guidelines-advisor",
|
|
2688
|
-
"trailofbits/skills sharp-edges",
|
|
2689
|
-
"trailofbits/skills differential-review",
|
|
2690
|
-
"trailofbits/skills ask-questions-if-underspecified",
|
|
2691
|
-
"squirrelscan/skills audit-website"
|
|
2692
|
-
],
|
|
2693
|
-
mobile: [
|
|
2694
|
-
"wshobson/agents mobile-ios-design",
|
|
2695
|
-
"wshobson/agents mobile-android-design",
|
|
2696
|
-
"dimillian/skills swiftui-ui-patterns",
|
|
2697
|
-
"dimillian/skills swiftui-liquid-glass"
|
|
2698
|
-
],
|
|
2699
|
-
obsidian: [
|
|
2700
|
-
"kepano/obsidian-skills obsidian-markdown",
|
|
2701
|
-
"kepano/obsidian-skills obsidian-bases",
|
|
2702
|
-
"kepano/obsidian-skills json-canvas"
|
|
2703
|
-
],
|
|
2704
|
-
prompts: [
|
|
2705
|
-
"f/awesome-chatgpt-prompts skill-lookup",
|
|
2706
|
-
"f/awesome-chatgpt-prompts prompt-lookup",
|
|
2707
|
-
"wshobson/agents prompt-engineering-patterns"
|
|
2708
|
-
],
|
|
2709
|
-
browser: [
|
|
2710
|
-
"vercel-labs/agent-browser agent-browser"
|
|
2711
|
-
],
|
|
2712
|
-
content: [
|
|
2713
|
-
"op7418/humanizer-zh humanizer-zh",
|
|
2714
|
-
"blader/humanizer humanizer",
|
|
2715
|
-
"op7418/youtube-clipper-skill youtube-clipper",
|
|
2716
|
-
"jimliu/baoyu-skills baoyu-slide-deck",
|
|
2717
|
-
"jimliu/baoyu-skills baoyu-article-illustrator",
|
|
2718
|
-
"jimliu/baoyu-skills baoyu-cover-image",
|
|
2719
|
-
"jimliu/baoyu-skills baoyu-comic",
|
|
2720
|
-
"jimliu/baoyu-skills baoyu-infographic",
|
|
2721
|
-
"jimliu/baoyu-skills baoyu-image-gen"
|
|
2722
|
-
],
|
|
2723
|
-
integrations: [
|
|
2724
|
-
"intellectronica/agent-skills context7",
|
|
2725
|
-
"softaworks/agent-toolkit gemini",
|
|
2726
|
-
"softaworks/agent-toolkit codex"
|
|
2727
|
-
]
|
|
2728
|
-
};
|
|
2729
|
-
async function detectStack(projectDir) {
|
|
2730
|
-
const stack = {
|
|
2731
|
-
suggestedSkills: [],
|
|
2732
|
-
suggestedAgents: ["security", "bugs"],
|
|
2733
|
-
dependencies: /* @__PURE__ */ new Set()
|
|
2734
|
-
};
|
|
2735
|
-
if (existsSync9(join8(projectDir, "tsconfig.json"))) {
|
|
2736
|
-
stack.language = "TypeScript";
|
|
2737
|
-
stack.suggestedSkills.push("wshobson/agents typescript-advanced-types");
|
|
2738
|
-
} else if (existsSync9(join8(projectDir, "package.json"))) {
|
|
2739
|
-
stack.language = "JavaScript";
|
|
2740
|
-
} else if (existsSync9(join8(projectDir, "requirements.txt")) || existsSync9(join8(projectDir, "pyproject.toml"))) {
|
|
2741
|
-
stack.language = "Python";
|
|
2742
|
-
} else if (existsSync9(join8(projectDir, "go.mod"))) {
|
|
2743
|
-
stack.language = "Go";
|
|
2744
|
-
} else if (existsSync9(join8(projectDir, "Cargo.toml"))) {
|
|
2745
|
-
stack.language = "Rust";
|
|
2746
|
-
}
|
|
2747
|
-
if (existsSync9(join8(projectDir, "Package.swift")) || existsSync9(join8(projectDir, "project.pbxproj")) || existsSync9(join8(projectDir, "*.xcodeproj"))) {
|
|
2748
|
-
stack.language = "Swift";
|
|
2749
|
-
stack.suggestedSkills.push("dimillian/skills swiftui-ui-patterns");
|
|
2750
|
-
stack.suggestedSkills.push("dimillian/skills swiftui-liquid-glass");
|
|
2751
|
-
}
|
|
2752
|
-
if (existsSync9(join8(projectDir, "pnpm-lock.yaml"))) {
|
|
2753
|
-
stack.packageManager = "pnpm";
|
|
2754
|
-
} else if (existsSync9(join8(projectDir, "yarn.lock"))) {
|
|
2755
|
-
stack.packageManager = "yarn";
|
|
2756
|
-
} else if (existsSync9(join8(projectDir, "bun.lockb"))) {
|
|
2757
|
-
stack.packageManager = "bun";
|
|
2758
|
-
} else if (existsSync9(join8(projectDir, "package-lock.json"))) {
|
|
2759
|
-
stack.packageManager = "npm";
|
|
2760
|
-
}
|
|
2761
|
-
if (existsSync9(join8(projectDir, ".github", "workflows"))) {
|
|
2762
|
-
stack.suggestedSkills.push("wshobson/agents github-actions-templates");
|
|
2763
|
-
}
|
|
2764
|
-
try {
|
|
2765
|
-
const pkgPath = join8(projectDir, "package.json");
|
|
2766
|
-
if (existsSync9(pkgPath)) {
|
|
2767
|
-
const pkgContent = await readFile7(pkgPath, "utf-8");
|
|
2768
|
-
const pkg = JSON.parse(pkgContent);
|
|
2769
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2770
|
-
for (const dep of Object.keys(deps)) {
|
|
2771
|
-
stack.dependencies.add(dep);
|
|
2772
|
-
}
|
|
2773
|
-
for (const dep of Object.keys(deps)) {
|
|
2774
|
-
const skills = SKILL_MAPPINGS[dep];
|
|
2775
|
-
if (skills) {
|
|
2776
|
-
stack.suggestedSkills.push(...skills);
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
if (deps["next"]) {
|
|
2780
|
-
stack.framework = `Next.js ${deps["next"].replace("^", "")}`;
|
|
2781
|
-
stack.suggestedAgents.push("accessibility", "design");
|
|
2782
|
-
} else if (deps["react"]) {
|
|
2783
|
-
stack.framework = `React ${deps["react"].replace("^", "")}`;
|
|
2784
|
-
stack.suggestedAgents.push("accessibility");
|
|
2785
|
-
} else if (deps["vue"]) {
|
|
2786
|
-
stack.framework = `Vue ${deps["vue"].replace("^", "")}`;
|
|
2787
|
-
} else if (deps["nuxt"]) {
|
|
2788
|
-
stack.framework = `Nuxt ${deps["nuxt"].replace("^", "")}`;
|
|
2789
|
-
} else if (deps["svelte"]) {
|
|
2790
|
-
stack.framework = "Svelte";
|
|
2791
|
-
} else if (deps["express"]) {
|
|
2792
|
-
stack.framework = "Express.js";
|
|
2793
|
-
} else if (deps["fastify"]) {
|
|
2794
|
-
stack.framework = "Fastify";
|
|
2795
|
-
} else if (deps["hono"]) {
|
|
2796
|
-
stack.framework = "Hono";
|
|
2797
|
-
} else if (deps["elysia"]) {
|
|
2798
|
-
stack.framework = "Elysia";
|
|
2799
|
-
} else if (deps["@nestjs/core"]) {
|
|
2800
|
-
stack.framework = "NestJS";
|
|
2801
|
-
}
|
|
2802
|
-
if (deps["next-auth"] || deps["@auth/core"]) {
|
|
2803
|
-
stack.auth = "NextAuth.js";
|
|
2804
|
-
} else if (deps["passport"]) {
|
|
2805
|
-
stack.auth = "Passport.js";
|
|
2806
|
-
} else if (deps["@clerk/nextjs"] || deps["@clerk/clerk-react"]) {
|
|
2807
|
-
stack.auth = "Clerk";
|
|
2808
|
-
} else if (deps["better-auth"]) {
|
|
2809
|
-
stack.auth = "Better Auth";
|
|
2810
|
-
}
|
|
2811
|
-
if (deps["prisma"] || deps["@prisma/client"]) {
|
|
2812
|
-
stack.database = "Prisma ORM";
|
|
2813
|
-
} else if (deps["drizzle-orm"]) {
|
|
2814
|
-
stack.database = "Drizzle ORM";
|
|
2815
|
-
} else if (deps["@supabase/supabase-js"]) {
|
|
2816
|
-
stack.database = "Supabase";
|
|
2817
|
-
} else if (deps["mongoose"]) {
|
|
2818
|
-
stack.database = "MongoDB (Mongoose)";
|
|
2819
|
-
} else if (deps["pg"]) {
|
|
2820
|
-
stack.database = "PostgreSQL";
|
|
2821
|
-
} else if (deps["convex"]) {
|
|
2822
|
-
stack.database = "Convex";
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
} catch {
|
|
2826
|
-
}
|
|
2827
|
-
if (!stack.database) {
|
|
2828
|
-
if (existsSync9(join8(projectDir, "prisma", "schema.prisma"))) {
|
|
2829
|
-
stack.database = "Prisma ORM";
|
|
2830
|
-
} else if (existsSync9(join8(projectDir, "drizzle.config.ts"))) {
|
|
2831
|
-
stack.database = "Drizzle ORM";
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
stack.suggestedSkills = [...new Set(stack.suggestedSkills)];
|
|
2835
|
-
stack.suggestedAgents = [...new Set(stack.suggestedAgents)];
|
|
2836
|
-
return stack;
|
|
2837
|
-
}
|
|
2838
|
-
function getSkillCategories() {
|
|
2839
|
-
return Object.entries(SKILL_CATEGORIES).map(([name, skills]) => ({
|
|
2840
|
-
name,
|
|
2841
|
-
count: skills.length
|
|
2842
|
-
}));
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
// src/bootstrap/files.ts
|
|
2846
|
-
import { readFile as readFile8, writeFile as writeFile6, unlink, mkdir as mkdir7 } from "fs/promises";
|
|
2847
|
-
import { existsSync as existsSync10 } from "fs";
|
|
2848
|
-
import { join as join9 } from "path";
|
|
2849
|
-
var BOOTSTRAP_FILES = [
|
|
2850
|
-
{ name: "PROJECT.md", type: "user", description: "Project overview and conventions" },
|
|
2851
|
-
{ name: "RULES.md", type: "user", description: "Coding standards agents enforce" },
|
|
2852
|
-
{ name: "TEAM.md", type: "user", description: "Team ownership and escalation" },
|
|
2853
|
-
{ name: "BOOTSTRAP.md", type: "one-time", description: "First-run setup (deleted after)" },
|
|
2854
|
-
{ name: "AGENTS.md", type: "auto", description: "Auto-generated scan context" }
|
|
2855
|
-
];
|
|
2856
|
-
async function loadBootstrapContext(workDir) {
|
|
2857
|
-
const projectDir = workDir || getWorkingDirectory(void 0, true);
|
|
2858
|
-
const trieDir = getTrieDirectory(projectDir);
|
|
2859
|
-
const files = [];
|
|
2860
|
-
const contentParts = [];
|
|
2861
|
-
let needsBootstrap2 = false;
|
|
2862
|
-
for (const file of BOOTSTRAP_FILES) {
|
|
2863
|
-
const filePath = join9(trieDir, file.name);
|
|
2864
|
-
const exists = existsSync10(filePath);
|
|
2865
|
-
if (file.name === "BOOTSTRAP.md" && exists) {
|
|
2866
|
-
needsBootstrap2 = true;
|
|
2867
|
-
}
|
|
2868
|
-
let content;
|
|
2869
|
-
if (exists) {
|
|
2870
|
-
try {
|
|
2871
|
-
content = await readFile8(filePath, "utf-8");
|
|
2872
|
-
if (content.trim() && file.type !== "one-time") {
|
|
2873
|
-
contentParts.push(`<!-- ${file.name} -->
|
|
2874
|
-
${content}`);
|
|
2875
|
-
}
|
|
2876
|
-
} catch {
|
|
2877
|
-
}
|
|
2878
|
-
}
|
|
2879
|
-
const fileEntry = {
|
|
2880
|
-
name: file.name,
|
|
2881
|
-
path: filePath,
|
|
2882
|
-
type: file.type,
|
|
2883
|
-
exists
|
|
2884
|
-
};
|
|
2885
|
-
if (content) fileEntry.content = content;
|
|
2886
|
-
files.push(fileEntry);
|
|
2887
|
-
}
|
|
2888
|
-
return {
|
|
2889
|
-
files,
|
|
2890
|
-
injectedContent: contentParts.join("\n\n---\n\n"),
|
|
2891
|
-
needsBootstrap: needsBootstrap2
|
|
2892
|
-
};
|
|
2893
|
-
}
|
|
2894
|
-
async function initializeBootstrapFiles(options = {}) {
|
|
2895
|
-
const projectDir = options.workDir || getWorkingDirectory(void 0, true);
|
|
2896
|
-
const trieDir = getTrieDirectory(projectDir);
|
|
2897
|
-
await mkdir7(trieDir, { recursive: true });
|
|
2898
|
-
const created = [];
|
|
2899
|
-
const skipped = [];
|
|
2900
|
-
const stack = await detectStack(projectDir);
|
|
2901
|
-
for (const file of BOOTSTRAP_FILES) {
|
|
2902
|
-
const filePath = join9(trieDir, file.name);
|
|
2903
|
-
const exists = existsSync10(filePath);
|
|
2904
|
-
if (exists && !options.force) {
|
|
2905
|
-
skipped.push(file.name);
|
|
2906
|
-
continue;
|
|
2907
|
-
}
|
|
2908
|
-
if (file.name === "BOOTSTRAP.md" && options.skipBootstrap) {
|
|
2909
|
-
skipped.push(file.name);
|
|
2910
|
-
continue;
|
|
2911
|
-
}
|
|
2912
|
-
if (file.name === "AGENTS.md") {
|
|
2913
|
-
skipped.push(file.name);
|
|
2914
|
-
continue;
|
|
2915
|
-
}
|
|
2916
|
-
const template = getFileTemplate(file.name, stack);
|
|
2917
|
-
if (template) {
|
|
2918
|
-
await writeFile6(filePath, template);
|
|
2919
|
-
created.push(file.name);
|
|
2920
|
-
}
|
|
2921
|
-
}
|
|
2922
|
-
return { created, skipped, stack };
|
|
2923
|
-
}
|
|
2924
|
-
async function completeBootstrap(workDir) {
|
|
2925
|
-
const projectDir = workDir || getWorkingDirectory(void 0, true);
|
|
2926
|
-
const bootstrapPath = join9(getTrieDirectory(projectDir), "BOOTSTRAP.md");
|
|
2927
|
-
try {
|
|
2928
|
-
await unlink(bootstrapPath);
|
|
2929
|
-
return true;
|
|
2930
|
-
} catch {
|
|
2931
|
-
return false;
|
|
2932
|
-
}
|
|
2933
|
-
}
|
|
2934
|
-
function needsBootstrap(workDir) {
|
|
2935
|
-
const projectDir = workDir || getWorkingDirectory(void 0, true);
|
|
2936
|
-
const bootstrapPath = join9(getTrieDirectory(projectDir), "BOOTSTRAP.md");
|
|
2937
|
-
return existsSync10(bootstrapPath);
|
|
2938
|
-
}
|
|
2939
|
-
async function loadRules(workDir) {
|
|
2940
|
-
const projectDir = workDir || getWorkingDirectory(void 0, true);
|
|
2941
|
-
const rulesPath = join9(getTrieDirectory(projectDir), "RULES.md");
|
|
2942
|
-
try {
|
|
2943
|
-
if (existsSync10(rulesPath)) {
|
|
2944
|
-
return await readFile8(rulesPath, "utf-8");
|
|
2945
|
-
}
|
|
2946
|
-
} catch {
|
|
2947
|
-
}
|
|
2948
|
-
return null;
|
|
2949
|
-
}
|
|
2950
|
-
async function loadTeamInfo(workDir) {
|
|
2951
|
-
const projectDir = workDir || getWorkingDirectory(void 0, true);
|
|
2952
|
-
const teamPath = join9(getTrieDirectory(projectDir), "TEAM.md");
|
|
2953
|
-
try {
|
|
2954
|
-
if (existsSync10(teamPath)) {
|
|
2955
|
-
return await readFile8(teamPath, "utf-8");
|
|
2956
|
-
}
|
|
2957
|
-
} catch {
|
|
2958
|
-
}
|
|
2959
|
-
return null;
|
|
2960
|
-
}
|
|
2961
|
-
function getFileTemplate(fileName, stack) {
|
|
2962
|
-
switch (fileName) {
|
|
2963
|
-
case "PROJECT.md":
|
|
2964
|
-
return getProjectTemplate2(stack);
|
|
2965
|
-
case "RULES.md":
|
|
2966
|
-
return getRulesTemplate(stack);
|
|
2967
|
-
case "TEAM.md":
|
|
2968
|
-
return getTeamTemplate();
|
|
2969
|
-
case "BOOTSTRAP.md":
|
|
2970
|
-
return getBootstrapTemplate(stack);
|
|
2971
|
-
default:
|
|
2972
|
-
return null;
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
|
-
function getProjectTemplate2(stack) {
|
|
2976
|
-
const lines = ["# Project Overview", "", "> Define your project context here. AI assistants read this first.", ""];
|
|
2977
|
-
lines.push("## Description", "", "[Describe what this project does and who it's for]", "");
|
|
2978
|
-
lines.push("## Technology Stack", "");
|
|
2979
|
-
if (stack.framework) lines.push(`- **Framework:** ${stack.framework}`);
|
|
2980
|
-
if (stack.language) lines.push(`- **Language:** ${stack.language}`);
|
|
2981
|
-
if (stack.database) lines.push(`- **Database:** ${stack.database}`);
|
|
2982
|
-
if (stack.auth) lines.push(`- **Auth:** ${stack.auth}`);
|
|
2983
|
-
if (!stack.framework && !stack.language && !stack.database) {
|
|
2984
|
-
lines.push("[Add your technology stack]");
|
|
2985
|
-
}
|
|
2986
|
-
lines.push("");
|
|
2987
|
-
lines.push("## Architecture", "", "[Key patterns and design decisions]", "");
|
|
2988
|
-
lines.push("## Coding Conventions", "", "[Style rules agents should follow]", "");
|
|
2989
|
-
lines.push("## AI Instructions", "", "When working on this project:", "1. [Instruction 1]", "2. [Instruction 2]", "3. [Instruction 3]", "");
|
|
2990
|
-
lines.push("---", "", "*Edit this file to provide project context to AI assistants.*");
|
|
2991
|
-
return lines.join("\n");
|
|
2992
|
-
}
|
|
2993
|
-
function getRulesTemplate(stack) {
|
|
2994
|
-
const packageManager = stack.packageManager || "npm";
|
|
2995
|
-
return `# Project Rules
|
|
2996
|
-
|
|
2997
|
-
> These rules are enforced by Trie agents during scans.
|
|
2998
|
-
> Violations appear as issues with [RULES] prefix.
|
|
2999
|
-
|
|
3000
|
-
## Code Style
|
|
3001
|
-
|
|
3002
|
-
1. Use named exports, not default exports
|
|
3003
|
-
2. Prefer \`${packageManager}\` for package management
|
|
3004
|
-
3. Maximum file length: 300 lines
|
|
3005
|
-
4. Use TypeScript strict mode
|
|
3006
|
-
|
|
3007
|
-
## Testing Requirements
|
|
3008
|
-
|
|
3009
|
-
1. Critical paths require unit tests
|
|
3010
|
-
2. API endpoints require integration tests
|
|
3011
|
-
3. Minimum coverage: 70%
|
|
3012
|
-
|
|
3013
|
-
## Security Rules
|
|
3014
|
-
|
|
3015
|
-
1. Never log sensitive data (passwords, tokens, PII)
|
|
3016
|
-
2. All API routes require authentication
|
|
3017
|
-
3. Rate limiting required on public endpoints
|
|
3018
|
-
4. SQL queries must use parameterized statements
|
|
3019
|
-
|
|
3020
|
-
## Review Requirements
|
|
3021
|
-
|
|
3022
|
-
1. All PRs require at least 1 reviewer
|
|
3023
|
-
2. Security-sensitive changes require security review
|
|
3024
|
-
3. Database migrations require review
|
|
3025
|
-
|
|
3026
|
-
---
|
|
3027
|
-
|
|
3028
|
-
*Edit this file to define your project's coding standards.*
|
|
3029
|
-
`;
|
|
3030
|
-
}
|
|
3031
|
-
function getTeamTemplate() {
|
|
3032
|
-
return `# Team Structure
|
|
3033
|
-
|
|
3034
|
-
## Code Ownership
|
|
3035
|
-
|
|
3036
|
-
| Area | Owner | Backup | Review Required |
|
|
3037
|
-
|------|-------|--------|-----------------|
|
|
3038
|
-
| \`/src/api/\` | @backend-team | - | 1 approver |
|
|
3039
|
-
| \`/src/ui/\` | @frontend-team | - | 1 approver |
|
|
3040
|
-
| \`/src/auth/\` | @security-team | - | Security review |
|
|
3041
|
-
|
|
3042
|
-
## Escalation
|
|
3043
|
-
|
|
3044
|
-
| Severity | First Contact | Escalate To | SLA |
|
|
3045
|
-
|----------|--------------|-------------|-----|
|
|
3046
|
-
| Critical | Team lead | CTO | 1 hour |
|
|
3047
|
-
| Serious | PR reviewer | Team lead | 4 hours |
|
|
3048
|
-
| Moderate | Self-serve | - | 1 day |
|
|
3049
|
-
|
|
3050
|
-
## Contacts
|
|
3051
|
-
|
|
3052
|
-
- **Security:** [email/slack]
|
|
3053
|
-
- **Privacy:** [email/slack]
|
|
3054
|
-
- **Emergencies:** [phone/pager]
|
|
3055
|
-
|
|
3056
|
-
---
|
|
3057
|
-
|
|
3058
|
-
*Edit this file to define your team structure.*
|
|
3059
|
-
`;
|
|
3060
|
-
}
|
|
3061
|
-
function getBootstrapTemplate(stack) {
|
|
3062
|
-
const skillCommands = stack.suggestedSkills.map((s) => `trie skills add ${s}`).join("\n");
|
|
3063
|
-
const agentList = stack.suggestedAgents.join(", ");
|
|
3064
|
-
const lines = [
|
|
3065
|
-
"# Trie Bootstrap Checklist",
|
|
3066
|
-
"",
|
|
3067
|
-
"This file guides your first scan setup. **Delete when complete.**",
|
|
3068
|
-
"",
|
|
3069
|
-
"## Auto-Detected Stack",
|
|
3070
|
-
"",
|
|
3071
|
-
"Based on your project, Trie detected:"
|
|
3072
|
-
];
|
|
3073
|
-
if (stack.framework) lines.push(`- **Framework:** ${stack.framework}`);
|
|
3074
|
-
if (stack.language) lines.push(`- **Language:** ${stack.language}`);
|
|
3075
|
-
if (stack.database) lines.push(`- **Database:** ${stack.database}`);
|
|
3076
|
-
if (stack.auth) lines.push(`- **Auth:** ${stack.auth}`);
|
|
3077
|
-
lines.push("", "## Recommended Actions", "", "### 1. Skills to Install", "Based on your stack, consider installing:", "```bash");
|
|
3078
|
-
lines.push(skillCommands || "# No specific skills detected - browse https://skills.sh");
|
|
3079
|
-
lines.push("```", "", "### 2. Agents to Enable", `Your stack suggests these agents: ${agentList || "security, bugs"}`, "");
|
|
3080
|
-
lines.push("### 3. Configure Rules", "Add your coding standards to `.trie/RULES.md`.", "");
|
|
3081
|
-
lines.push("### 4. Run First Scan", "```bash", "trie scan", "```", "");
|
|
3082
|
-
lines.push("---", "", "**Delete this file after completing setup.** Run:", "```bash", "rm .trie/BOOTSTRAP.md", "```");
|
|
3083
|
-
return lines.join("\n");
|
|
3084
|
-
}
|
|
3085
|
-
|
|
3086
|
-
// src/utils/errors.ts
|
|
3087
|
-
var TrieError = class extends Error {
|
|
3088
|
-
code;
|
|
3089
|
-
recoverable;
|
|
3090
|
-
userMessage;
|
|
3091
|
-
constructor(message, code, userMessage, recoverable = true) {
|
|
3092
|
-
super(message);
|
|
3093
|
-
this.code = code;
|
|
3094
|
-
this.recoverable = recoverable;
|
|
3095
|
-
this.userMessage = userMessage;
|
|
3096
|
-
}
|
|
3097
|
-
};
|
|
3098
|
-
function formatFriendlyError(error) {
|
|
3099
|
-
if (error instanceof TrieError) {
|
|
3100
|
-
return { userMessage: error.userMessage, code: error.code };
|
|
3101
|
-
}
|
|
3102
|
-
return {
|
|
3103
|
-
userMessage: "Something went wrong. Try again or run with --offline.",
|
|
3104
|
-
code: "UNKNOWN"
|
|
3105
|
-
};
|
|
3106
|
-
}
|
|
3107
|
-
|
|
3108
|
-
// src/context/sync.ts
|
|
3109
|
-
import fs from "fs/promises";
|
|
3110
|
-
import path4 from "path";
|
|
3111
|
-
var DEFAULT_JSON_NAME = "context.json";
|
|
3112
|
-
async function exportToJson(graph, targetPath) {
|
|
3113
|
-
const snapshot = await graph.getSnapshot();
|
|
3114
|
-
const json = JSON.stringify(snapshot, null, 2);
|
|
3115
|
-
const outputPath = targetPath ?? path4.join(getTrieDirectory(graph.projectRoot), DEFAULT_JSON_NAME);
|
|
3116
|
-
await fs.mkdir(path4.dirname(outputPath), { recursive: true });
|
|
3117
|
-
await fs.writeFile(outputPath, json, "utf8");
|
|
3118
|
-
return json;
|
|
3119
|
-
}
|
|
3120
|
-
async function importFromJson(graph, json, sourcePath) {
|
|
3121
|
-
const payload = json.trim().length > 0 ? json : await fs.readFile(sourcePath ?? path4.join(getTrieDirectory(graph.projectRoot), DEFAULT_JSON_NAME), "utf8");
|
|
3122
|
-
const snapshot = JSON.parse(payload);
|
|
3123
|
-
await graph.applySnapshot(snapshot);
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
|
-
// src/context/incident-index.ts
|
|
3127
|
-
import path6 from "path";
|
|
3128
|
-
|
|
3129
|
-
// src/context/file-trie.ts
|
|
3130
|
-
import fs2 from "fs";
|
|
3131
|
-
import path5 from "path";
|
|
3132
|
-
import { performance } from "perf_hooks";
|
|
3133
|
-
function normalizePath(filePath) {
|
|
3134
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
3135
|
-
return normalized.startsWith("./") ? normalized.slice(2) : normalized;
|
|
3136
|
-
}
|
|
3137
|
-
var FilePathTrie = class {
|
|
3138
|
-
trie = new Trie();
|
|
3139
|
-
persistPath;
|
|
3140
|
-
constructor(persistPath) {
|
|
3141
|
-
if (persistPath) this.persistPath = persistPath;
|
|
3142
|
-
if (persistPath && fs2.existsSync(persistPath)) {
|
|
3143
|
-
try {
|
|
3144
|
-
const raw = fs2.readFileSync(persistPath, "utf-8");
|
|
3145
|
-
if (raw.trim().length > 0) {
|
|
3146
|
-
const json = JSON.parse(raw);
|
|
3147
|
-
this.trie = Trie.fromJSON(json);
|
|
3148
|
-
}
|
|
3149
|
-
} catch {
|
|
3150
|
-
this.trie = new Trie();
|
|
3151
|
-
}
|
|
3152
|
-
}
|
|
3153
|
-
}
|
|
3154
|
-
addIncident(filePath, incident) {
|
|
3155
|
-
const key = normalizePath(filePath);
|
|
3156
|
-
const existing = this.trie.search(key);
|
|
3157
|
-
const incidents = existing.found && Array.isArray(existing.value) ? existing.value : [];
|
|
3158
|
-
incidents.push(incident);
|
|
3159
|
-
this.trie.insert(key, incidents);
|
|
3160
|
-
this.persist();
|
|
3161
|
-
}
|
|
3162
|
-
getIncidents(filePath) {
|
|
3163
|
-
const key = normalizePath(filePath);
|
|
3164
|
-
const result = this.trie.search(key);
|
|
3165
|
-
return result.found && Array.isArray(result.value) ? result.value : [];
|
|
3166
|
-
}
|
|
3167
|
-
getDirectoryIncidents(prefix) {
|
|
3168
|
-
const normalizedPrefix = normalizePath(prefix);
|
|
3169
|
-
const matches = this.trie.getWithPrefix(normalizedPrefix);
|
|
3170
|
-
return matches.flatMap((m) => Array.isArray(m.value) ? m.value : []).filter(Boolean);
|
|
3171
|
-
}
|
|
3172
|
-
getHotZones(threshold) {
|
|
3173
|
-
const matches = this.trie.getWithPrefix("");
|
|
3174
|
-
const zones = [];
|
|
3175
|
-
for (const match of matches) {
|
|
3176
|
-
const incidents = Array.isArray(match.value) ? match.value : [];
|
|
3177
|
-
if (incidents.length >= threshold) {
|
|
3178
|
-
zones.push({
|
|
3179
|
-
path: match.pattern,
|
|
3180
|
-
incidentCount: incidents.length,
|
|
3181
|
-
confidence: this.calculateConfidence(incidents.length)
|
|
3182
|
-
});
|
|
3183
|
-
}
|
|
3184
|
-
}
|
|
3185
|
-
return zones.sort((a, b) => b.incidentCount - a.incidentCount);
|
|
3186
|
-
}
|
|
3187
|
-
suggestPaths(partial, limit = 5) {
|
|
3188
|
-
const normalized = normalizePath(partial);
|
|
3189
|
-
const results = this.trie.getWithPrefix(normalized);
|
|
3190
|
-
return results.map((r) => ({
|
|
3191
|
-
path: r.pattern,
|
|
3192
|
-
incidentCount: Array.isArray(r.value) ? r.value.length : 0
|
|
3193
|
-
})).sort((a, b) => b.incidentCount - a.incidentCount).slice(0, limit);
|
|
3194
|
-
}
|
|
3195
|
-
timeLookup(path8) {
|
|
3196
|
-
const start = performance.now();
|
|
3197
|
-
this.getIncidents(path8);
|
|
3198
|
-
return performance.now() - start;
|
|
3199
|
-
}
|
|
3200
|
-
toJSON() {
|
|
3201
|
-
return this.trie.toJSON();
|
|
3202
|
-
}
|
|
3203
|
-
calculateConfidence(count) {
|
|
3204
|
-
const capped = Math.min(count, 10);
|
|
3205
|
-
return Math.round(capped / 10 * 100) / 100;
|
|
3206
|
-
}
|
|
3207
|
-
persist() {
|
|
3208
|
-
if (!this.persistPath) return;
|
|
3209
|
-
try {
|
|
3210
|
-
const dir = path5.dirname(this.persistPath);
|
|
3211
|
-
if (!fs2.existsSync(dir)) {
|
|
3212
|
-
fs2.mkdirSync(dir, { recursive: true });
|
|
3213
|
-
}
|
|
3214
|
-
fs2.writeFileSync(this.persistPath, JSON.stringify(this.trie.toJSON()), "utf-8");
|
|
3215
|
-
} catch {
|
|
3216
|
-
}
|
|
3217
|
-
}
|
|
3218
|
-
};
|
|
3219
|
-
|
|
3220
|
-
// src/context/incident-index.ts
|
|
3221
|
-
var IncidentIndex = class _IncidentIndex {
|
|
3222
|
-
graph;
|
|
3223
|
-
trie;
|
|
3224
|
-
projectRoot;
|
|
3225
|
-
constructor(graph, projectRoot, options) {
|
|
3226
|
-
this.graph = graph;
|
|
3227
|
-
this.projectRoot = projectRoot;
|
|
3228
|
-
this.trie = new FilePathTrie(
|
|
3229
|
-
options?.persistPath ?? path6.join(getTrieDirectory(projectRoot), "incident-trie.json")
|
|
3230
|
-
);
|
|
3231
|
-
}
|
|
3232
|
-
static async build(graph, projectRoot, options) {
|
|
3233
|
-
const index = new _IncidentIndex(graph, projectRoot, options);
|
|
3234
|
-
await index.rebuild();
|
|
3235
|
-
return index;
|
|
3236
|
-
}
|
|
3237
|
-
async rebuild() {
|
|
3238
|
-
const nodes = await this.graph.listNodes();
|
|
3239
|
-
const incidents = nodes.filter((n) => n.type === "incident");
|
|
3240
|
-
for (const incident of incidents) {
|
|
3241
|
-
const files = await this.getFilesForIncident(incident.id);
|
|
3242
|
-
this.addIncidentToTrie(incident, files);
|
|
3243
|
-
}
|
|
3244
|
-
}
|
|
3245
|
-
addIncidentToTrie(incident, files) {
|
|
3246
|
-
const meta = {
|
|
3247
|
-
id: incident.id,
|
|
3248
|
-
file: "",
|
|
3249
|
-
description: incident.data.description,
|
|
3250
|
-
severity: incident.data.severity,
|
|
3251
|
-
timestamp: incident.data.timestamp
|
|
3252
|
-
};
|
|
3253
|
-
for (const file of files) {
|
|
3254
|
-
const normalized = this.normalizePath(file);
|
|
3255
|
-
this.trie.addIncident(normalized, { ...meta, file: normalized });
|
|
3256
|
-
}
|
|
3257
|
-
}
|
|
3258
|
-
getFileTrie() {
|
|
3259
|
-
return this.trie;
|
|
3260
|
-
}
|
|
3261
|
-
async getFilesForIncident(incidentId) {
|
|
3262
|
-
const files = /* @__PURE__ */ new Set();
|
|
3263
|
-
const edges = await this.graph.getEdges(incidentId, "both");
|
|
3264
|
-
for (const edge of edges) {
|
|
3265
|
-
if (edge.type === "causedBy" || edge.type === "leadTo") {
|
|
3266
|
-
const changeId = edge.type === "causedBy" ? edge.to_id : edge.from_id;
|
|
3267
|
-
const change = await this.graph.getNode("change", changeId);
|
|
3268
|
-
if (change?.data && "files" in change.data && Array.isArray(change.data.files)) {
|
|
3269
|
-
change.data.files.forEach((f) => files.add(f));
|
|
3270
|
-
}
|
|
3271
|
-
}
|
|
3272
|
-
if (edge.type === "affects") {
|
|
3273
|
-
const fileNode = await this.graph.getNode("file", edge.to_id) || await this.graph.getNode("file", edge.from_id);
|
|
3274
|
-
if (fileNode && typeof fileNode.data?.path === "string") {
|
|
3275
|
-
files.add(fileNode.data.path);
|
|
3276
|
-
}
|
|
3277
|
-
}
|
|
3278
|
-
}
|
|
3279
|
-
return Array.from(files);
|
|
3280
|
-
}
|
|
3281
|
-
normalizePath(filePath) {
|
|
3282
|
-
const absolute = path6.isAbsolute(filePath) ? filePath : path6.join(this.projectRoot, filePath);
|
|
3283
|
-
const relative = path6.relative(this.projectRoot, absolute);
|
|
3284
|
-
return relative.replace(/\\/g, "/");
|
|
3285
|
-
}
|
|
3286
|
-
};
|
|
3287
|
-
|
|
3288
|
-
// src/agent/confidence.ts
|
|
3289
|
-
function adjustConfidence(current, outcome, step = 0.1) {
|
|
3290
|
-
const delta = outcome === "positive" ? step : -step;
|
|
3291
|
-
return clamp(current + delta);
|
|
3292
|
-
}
|
|
3293
|
-
function clamp(value) {
|
|
3294
|
-
if (Number.isNaN(value)) return 0.5;
|
|
3295
|
-
return Math.min(1, Math.max(0, value));
|
|
3296
|
-
}
|
|
3297
|
-
|
|
3298
|
-
// src/agent/pattern-discovery.ts
|
|
3299
|
-
var TriePatternDiscovery = class {
|
|
3300
|
-
constructor(graph, incidentIndex) {
|
|
3301
|
-
this.graph = graph;
|
|
3302
|
-
this.incidentIndex = incidentIndex;
|
|
3303
|
-
}
|
|
3304
|
-
discoverHotPatterns(threshold = 3) {
|
|
3305
|
-
const trie = this.incidentIndex.getFileTrie();
|
|
3306
|
-
const hotZones = trie.getHotZones(threshold);
|
|
3307
|
-
return hotZones.map((zone) => ({
|
|
3308
|
-
type: zone.path.endsWith("/") ? "directory" : "file",
|
|
3309
|
-
path: zone.path,
|
|
3310
|
-
incidentCount: zone.incidentCount,
|
|
3311
|
-
confidence: zone.confidence,
|
|
3312
|
-
relatedFiles: trie.getDirectoryIncidents(zone.path).map((i) => i.file)
|
|
3313
|
-
}));
|
|
3314
|
-
}
|
|
3315
|
-
async discoverCoOccurrences(minCount = 3) {
|
|
3316
|
-
const incidents = await this.getAllIncidents();
|
|
3317
|
-
const coOccurrences = /* @__PURE__ */ new Map();
|
|
3318
|
-
for (const inc of incidents) {
|
|
3319
|
-
const files = await this.getFilesForIncident(inc);
|
|
3320
|
-
for (let i = 0; i < files.length; i++) {
|
|
3321
|
-
for (let j = i + 1; j < files.length; j++) {
|
|
3322
|
-
const a = files[i];
|
|
3323
|
-
const b = files[j];
|
|
3324
|
-
if (!coOccurrences.has(a)) coOccurrences.set(a, /* @__PURE__ */ new Map());
|
|
3325
|
-
const counts = coOccurrences.get(a);
|
|
3326
|
-
counts.set(b, (counts.get(b) || 0) + 1);
|
|
3327
|
-
}
|
|
3328
|
-
}
|
|
3329
|
-
}
|
|
3330
|
-
const patterns = [];
|
|
3331
|
-
for (const [a, map] of coOccurrences.entries()) {
|
|
3332
|
-
for (const [b, count] of map.entries()) {
|
|
3333
|
-
if (count >= minCount) {
|
|
3334
|
-
const denom = Math.min(
|
|
3335
|
-
this.incidentIndex.getFileTrie().getIncidents(a).length || 1,
|
|
3336
|
-
this.incidentIndex.getFileTrie().getIncidents(b).length || 1
|
|
3337
|
-
);
|
|
3338
|
-
patterns.push({
|
|
3339
|
-
files: [a, b],
|
|
3340
|
-
coOccurrences: count,
|
|
3341
|
-
confidence: Math.min(1, count / denom)
|
|
3342
|
-
});
|
|
3343
|
-
}
|
|
3344
|
-
}
|
|
3345
|
-
}
|
|
3346
|
-
return patterns.sort((x, y) => y.confidence - x.confidence);
|
|
3347
|
-
}
|
|
3348
|
-
async getAllIncidents() {
|
|
3349
|
-
const nodes = await this.graph.listNodes();
|
|
3350
|
-
return nodes.filter((n) => n.type === "incident");
|
|
3351
|
-
}
|
|
3352
|
-
async getFilesForIncident(incident) {
|
|
3353
|
-
const files = /* @__PURE__ */ new Set();
|
|
3354
|
-
const edges = await this.graph.getEdges(incident.id, "both");
|
|
3355
|
-
for (const edge of edges) {
|
|
3356
|
-
if (edge.type === "causedBy" || edge.type === "leadTo") {
|
|
3357
|
-
const changeId = edge.type === "causedBy" ? edge.to_id : edge.from_id;
|
|
3358
|
-
const change = await this.graph.getNode("change", changeId);
|
|
3359
|
-
if (change?.data?.files && Array.isArray(change.data.files)) {
|
|
3360
|
-
change.data.files.forEach((f) => files.add(f));
|
|
3361
|
-
}
|
|
3362
|
-
}
|
|
3363
|
-
}
|
|
3364
|
-
return Array.from(files).map((f) => f.replace(/\\/g, "/"));
|
|
3365
|
-
}
|
|
3366
|
-
};
|
|
3367
|
-
|
|
3368
|
-
// src/agent/learning.ts
|
|
3369
|
-
var LearningSystem = class {
|
|
3370
|
-
constructor(graph, projectPath) {
|
|
3371
|
-
this.graph = graph;
|
|
3372
|
-
this.incidentIndex = new IncidentIndex(graph, projectPath);
|
|
3373
|
-
this.discovery = new TriePatternDiscovery(graph, this.incidentIndex);
|
|
3374
|
-
}
|
|
3375
|
-
incidentIndex;
|
|
3376
|
-
discovery;
|
|
3377
|
-
async onWarningHeeded(files) {
|
|
3378
|
-
await this.adjustPatterns(files, "positive");
|
|
3379
|
-
}
|
|
3380
|
-
async onWarningIgnored(files) {
|
|
3381
|
-
await this.adjustPatterns(files, "negative");
|
|
3382
|
-
}
|
|
3383
|
-
async onIncidentReported(incidentId, files) {
|
|
3384
|
-
const incident = await this.graph.getNode("incident", incidentId);
|
|
3385
|
-
if (incident && incident.type === "incident") {
|
|
3386
|
-
this.incidentIndex.addIncidentToTrie(incident, files);
|
|
3387
|
-
}
|
|
3388
|
-
await this.discoverAndStorePatterns();
|
|
3389
|
-
}
|
|
3390
|
-
async onFeedback(helpful, files = []) {
|
|
3391
|
-
await this.adjustPatterns(files, helpful ? "positive" : "negative");
|
|
3392
|
-
}
|
|
3393
|
-
async adjustPatterns(files, outcome) {
|
|
3394
|
-
if (!files.length) return;
|
|
3395
|
-
for (const file of files) {
|
|
3396
|
-
const patterns = await this.graph.getPatternsForFile(file);
|
|
3397
|
-
await Promise.all(patterns.map((p) => this.updatePatternConfidence(p, outcome)));
|
|
3398
|
-
}
|
|
3399
|
-
}
|
|
3400
|
-
async updatePatternConfidence(pattern, outcome) {
|
|
3401
|
-
const current = pattern.data.confidence ?? 0.5;
|
|
3402
|
-
const updated = adjustConfidence(current, outcome, 0.05);
|
|
3403
|
-
await this.graph.updateNode("pattern", pattern.id, { confidence: updated, lastSeen: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3404
|
-
}
|
|
3405
|
-
async discoverAndStorePatterns() {
|
|
3406
|
-
const hotPatterns = this.discovery.discoverHotPatterns();
|
|
3407
|
-
for (const hot of hotPatterns) {
|
|
3408
|
-
await this.graph.addNode("pattern", {
|
|
3409
|
-
description: `${hot.type === "directory" ? "Directory" : "File"} hot zone: ${hot.path}`,
|
|
3410
|
-
appliesTo: [hot.path],
|
|
3411
|
-
confidence: hot.confidence,
|
|
3412
|
-
occurrences: hot.incidentCount,
|
|
3413
|
-
firstSeen: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3414
|
-
lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3415
|
-
isAntiPattern: true,
|
|
3416
|
-
source: "local"
|
|
3417
|
-
});
|
|
3418
|
-
}
|
|
3419
|
-
}
|
|
3420
|
-
};
|
|
3421
|
-
|
|
3422
|
-
// src/guardian/learning-engine.ts
|
|
3423
|
-
import path7 from "path";
|
|
3424
|
-
var LearningEngine = class {
|
|
3425
|
-
projectPath;
|
|
3426
|
-
graph;
|
|
3427
|
-
learningSystem;
|
|
3428
|
-
constructor(projectPath, graph) {
|
|
3429
|
-
this.projectPath = projectPath;
|
|
3430
|
-
this.graph = graph || new ContextGraph(projectPath);
|
|
3431
|
-
this.learningSystem = new LearningSystem(this.graph, projectPath);
|
|
3432
|
-
}
|
|
3433
|
-
/**
|
|
3434
|
-
* Unified learning method: Scans history AND processes manual feedback
|
|
3435
|
-
*/
|
|
3436
|
-
async learn(options = {}) {
|
|
3437
|
-
const results = [];
|
|
3438
|
-
if (!options.manualFeedback) {
|
|
3439
|
-
const implicitCount = await this.learnFromHistory(options.limit || 20);
|
|
3440
|
-
results.push({ learned: implicitCount, source: "git-history" });
|
|
3441
|
-
}
|
|
3442
|
-
if (options.manualFeedback) {
|
|
3443
|
-
await this.recordManualFeedback(
|
|
3444
|
-
options.manualFeedback.helpful,
|
|
3445
|
-
options.manualFeedback.files,
|
|
3446
|
-
options.manualFeedback.note
|
|
3447
|
-
);
|
|
3448
|
-
results.push({ learned: options.manualFeedback.files.length || 1, source: "manual-feedback" });
|
|
3449
|
-
}
|
|
3450
|
-
return results;
|
|
3451
|
-
}
|
|
3452
|
-
/**
|
|
3453
|
-
* Scan recent commits for implicit failure signals (reverts, fixes)
|
|
3454
|
-
*/
|
|
3455
|
-
async learnFromHistory(limit = 20) {
|
|
3456
|
-
const commits = await getRecentCommits(this.projectPath, limit);
|
|
3457
|
-
const issuesToStore = [];
|
|
3458
|
-
for (const commit of commits) {
|
|
3459
|
-
const isRevert = commit.message.toLowerCase().includes("revert") || commit.message.startsWith('Revert "');
|
|
3460
|
-
const isFix = /fix(es|ed)?\s+#\d+/i.test(commit.message) || commit.message.toLowerCase().includes("bugfix");
|
|
3461
|
-
if (isRevert || isFix) {
|
|
3462
|
-
const type = isRevert ? "revert" : "fix";
|
|
3463
|
-
const diff = await getDiff(this.projectPath, commit.hash);
|
|
3464
|
-
const files = this.extractFilesFromDiff(diff);
|
|
3465
|
-
for (const file of files) {
|
|
3466
|
-
const learnedIssues = await this.extractIssuesFromDiff(diff, file, type, commit.message);
|
|
3467
|
-
issuesToStore.push(...learnedIssues);
|
|
3468
|
-
}
|
|
3469
|
-
}
|
|
3470
|
-
}
|
|
3471
|
-
if (issuesToStore.length > 0) {
|
|
3472
|
-
const result = await storeIssues(issuesToStore, path7.basename(this.projectPath), this.projectPath);
|
|
3473
|
-
return result.stored;
|
|
3474
|
-
}
|
|
3475
|
-
return 0;
|
|
3476
|
-
}
|
|
3477
|
-
/**
|
|
3478
|
-
* Record manual feedback (trie ok/bad) and adjust pattern confidence
|
|
3479
|
-
*/
|
|
3480
|
-
async recordManualFeedback(helpful, files, note) {
|
|
3481
|
-
const context = files[0] ?? "unspecified";
|
|
3482
|
-
const decision = await this.graph.addNode("decision", {
|
|
3483
|
-
context,
|
|
3484
|
-
decision: helpful ? "helpful" : "not helpful",
|
|
3485
|
-
reasoning: note ?? null,
|
|
3486
|
-
outcome: helpful ? "good" : "bad",
|
|
3487
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3488
|
-
});
|
|
3489
|
-
if (files.length > 0) {
|
|
3490
|
-
for (const file of files) {
|
|
3491
|
-
const fileNode = await this.graph.getNode("file", file);
|
|
3492
|
-
if (fileNode) {
|
|
3493
|
-
await this.graph.addEdge(decision.id, fileNode.id, "affects");
|
|
3494
|
-
}
|
|
3495
|
-
}
|
|
3496
|
-
await this.learningSystem.onFeedback(helpful, files);
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
|
-
extractFilesFromDiff(diff) {
|
|
3500
|
-
const files = /* @__PURE__ */ new Set();
|
|
3501
|
-
const lines = diff.split("\n");
|
|
3502
|
-
for (const line of lines) {
|
|
3503
|
-
if (line.startsWith("+++ b/")) {
|
|
3504
|
-
files.add(line.slice(6));
|
|
3505
|
-
}
|
|
3506
|
-
}
|
|
3507
|
-
return Array.from(files);
|
|
3508
|
-
}
|
|
3509
|
-
async extractIssuesFromDiff(diff, file, type, message) {
|
|
3510
|
-
const issues = [];
|
|
3511
|
-
const badLines = this.getBadLinesFromDiff(diff, file, type);
|
|
3512
|
-
const content = badLines.join("\n");
|
|
3513
|
-
if (!content) return [];
|
|
3514
|
-
const vulnerabilities = await scanForVulnerabilities(content, file);
|
|
3515
|
-
const vibeIssues = await scanForVibeCodeIssues(content, file);
|
|
3516
|
-
const allMatches = [...vulnerabilities, ...vibeIssues];
|
|
3517
|
-
for (const match of allMatches) {
|
|
3518
|
-
issues.push({
|
|
3519
|
-
id: `implicit-${type}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
3520
|
-
severity: "serious",
|
|
3521
|
-
issue: `Implicit failure detected via ${type}: ${message}. Linked to pattern: ${match.category}`,
|
|
3522
|
-
fix: `Review the ${type} commit and avoid this pattern in ${file}.`,
|
|
3523
|
-
file,
|
|
3524
|
-
confidence: 0.7,
|
|
3525
|
-
autoFixable: false,
|
|
3526
|
-
agent: "implicit-learning",
|
|
3527
|
-
category: match.category
|
|
3528
|
-
});
|
|
3529
|
-
}
|
|
3530
|
-
if (issues.length === 0) {
|
|
3531
|
-
issues.push({
|
|
3532
|
-
id: `implicit-${type}-${Date.now()}`,
|
|
3533
|
-
severity: "moderate",
|
|
3534
|
-
issue: `Historical ${type} detected: ${message}`,
|
|
3535
|
-
fix: `Review the changes in ${file} from this commit to avoid regression.`,
|
|
3536
|
-
file,
|
|
3537
|
-
confidence: 0.5,
|
|
3538
|
-
autoFixable: false,
|
|
3539
|
-
agent: "implicit-learning"
|
|
3540
|
-
});
|
|
3541
|
-
}
|
|
3542
|
-
return issues;
|
|
3543
|
-
}
|
|
3544
|
-
getBadLinesFromDiff(diff, file, type) {
|
|
3545
|
-
const badLines = [];
|
|
3546
|
-
const lines = diff.split("\n");
|
|
3547
|
-
let inTargetFile = false;
|
|
3548
|
-
for (const line of lines) {
|
|
3549
|
-
if (line.startsWith("+++ b/") || line.startsWith("--- a/")) {
|
|
3550
|
-
inTargetFile = line.includes(file);
|
|
3551
|
-
continue;
|
|
3552
|
-
}
|
|
3553
|
-
if (!inTargetFile) continue;
|
|
3554
|
-
if (type === "fix" && line.startsWith("-") && !line.startsWith("---")) {
|
|
3555
|
-
badLines.push(line.slice(1));
|
|
3556
|
-
} else if (type === "revert" && line.startsWith("+") && !line.startsWith("+++")) {
|
|
3557
|
-
badLines.push(line.slice(1));
|
|
3558
|
-
}
|
|
3559
|
-
}
|
|
3560
|
-
return badLines;
|
|
3561
|
-
}
|
|
3562
|
-
};
|
|
3563
|
-
|
|
3564
|
-
// src/ingest/linear-ingester.ts
|
|
3565
|
-
var LinearIngester = class {
|
|
3566
|
-
graph;
|
|
3567
|
-
constructor(_projectPath, graph) {
|
|
3568
|
-
this.graph = graph;
|
|
3569
|
-
}
|
|
3570
|
-
async syncTickets() {
|
|
3571
|
-
const apiKey = await this.getApiKey();
|
|
3572
|
-
if (!apiKey) {
|
|
3573
|
-
console.warn('Linear API key not found. Run "trie linear auth <key>" to enable ticket sync.');
|
|
3574
|
-
return;
|
|
3575
|
-
}
|
|
3576
|
-
const tickets = await this.fetchActiveTickets(apiKey);
|
|
3577
|
-
for (const ticket of tickets) {
|
|
3578
|
-
await this.ingestTicket(ticket);
|
|
3579
|
-
}
|
|
3580
|
-
}
|
|
3581
|
-
async getApiKey() {
|
|
3582
|
-
const config = await loadConfig();
|
|
3583
|
-
return config.apiKeys?.linear || process.env.LINEAR_API_KEY || null;
|
|
3584
|
-
}
|
|
3585
|
-
async fetchActiveTickets(apiKey) {
|
|
3586
|
-
const query = `
|
|
3587
|
-
query {
|
|
3588
|
-
issues(filter: { state: { type: { in: ["started", "unstarted"] } } }) {
|
|
3589
|
-
nodes {
|
|
3590
|
-
id
|
|
3591
|
-
identifier
|
|
3592
|
-
title
|
|
3593
|
-
description
|
|
3594
|
-
priority
|
|
3595
|
-
status {
|
|
3596
|
-
name
|
|
3597
|
-
}
|
|
3598
|
-
assignee {
|
|
3599
|
-
name
|
|
3600
|
-
}
|
|
3601
|
-
labels {
|
|
3602
|
-
nodes {
|
|
3603
|
-
name
|
|
3604
|
-
}
|
|
3605
|
-
}
|
|
3606
|
-
createdAt
|
|
3607
|
-
updatedAt
|
|
3608
|
-
}
|
|
3609
|
-
}
|
|
3610
|
-
}
|
|
3611
|
-
`;
|
|
3612
|
-
const response = await fetch("https://api.linear.app/graphql", {
|
|
3613
|
-
method: "POST",
|
|
3614
|
-
headers: {
|
|
3615
|
-
"Content-Type": "application/json",
|
|
3616
|
-
"Authorization": apiKey
|
|
3617
|
-
},
|
|
3618
|
-
body: JSON.stringify({ query })
|
|
3619
|
-
});
|
|
3620
|
-
if (!response.ok) {
|
|
3621
|
-
throw new Error(`Linear API error: ${response.statusText}`);
|
|
3622
|
-
}
|
|
3623
|
-
const data = await response.json();
|
|
3624
|
-
return data.data.issues.nodes;
|
|
3625
|
-
}
|
|
3626
|
-
async ingestTicket(ticket) {
|
|
3627
|
-
const labels = ticket.labels.nodes.map((l) => l.name);
|
|
3628
|
-
const intentVibe = this.extractIntentVibes(ticket.title, ticket.description, labels);
|
|
3629
|
-
const linkedFiles = this.extractLinkedFiles(ticket.description);
|
|
3630
|
-
const data = {
|
|
3631
|
-
ticketId: ticket.identifier,
|
|
3632
|
-
title: ticket.title,
|
|
3633
|
-
description: ticket.description || "",
|
|
3634
|
-
priority: this.mapPriority(ticket.priority),
|
|
3635
|
-
labels,
|
|
3636
|
-
intentVibe,
|
|
3637
|
-
linkedFiles,
|
|
3638
|
-
status: ticket.status.name,
|
|
3639
|
-
assignee: ticket.assignee?.name || null,
|
|
3640
|
-
createdAt: ticket.createdAt,
|
|
3641
|
-
updatedAt: ticket.updatedAt
|
|
3642
|
-
};
|
|
3643
|
-
await this.graph.addNode("linear-ticket", data);
|
|
3644
|
-
for (const file of linkedFiles) {
|
|
3645
|
-
const fileNode = await this.graph.getNode("file", file);
|
|
3646
|
-
if (fileNode) {
|
|
3647
|
-
await this.graph.addEdge(`linear:${ticket.identifier}`, fileNode.id, "relatedTo");
|
|
3648
|
-
}
|
|
3649
|
-
}
|
|
3650
|
-
}
|
|
3651
|
-
extractIntentVibes(title, description, labels) {
|
|
3652
|
-
const vibes = /* @__PURE__ */ new Set();
|
|
3653
|
-
const text = (title + " " + (description || "") + " " + labels.join(" ")).toLowerCase();
|
|
3654
|
-
if (text.includes("performance") || text.includes("slow") || text.includes("optimize")) vibes.add("performance");
|
|
3655
|
-
if (text.includes("security") || text.includes("auth") || text.includes("vulnerability")) vibes.add("security");
|
|
3656
|
-
if (text.includes("refactor") || text.includes("cleanup") || text.includes("debt")) vibes.add("refactor");
|
|
3657
|
-
if (text.includes("feature") || text.includes("new")) vibes.add("feature");
|
|
3658
|
-
if (text.includes("bug") || text.includes("fix") || text.includes("broken")) vibes.add("bug");
|
|
3659
|
-
if (text.includes("breaking") || text.includes("major change")) vibes.add("breaking-change");
|
|
3660
|
-
return Array.from(vibes);
|
|
3661
|
-
}
|
|
3662
|
-
extractLinkedFiles(description) {
|
|
3663
|
-
if (!description) return [];
|
|
3664
|
-
const matches = description.match(/[\\w./_-]+\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs)/gi);
|
|
3665
|
-
if (!matches) return [];
|
|
3666
|
-
return Array.from(new Set(matches.map((m) => m.replace(/^\.\/+/, ""))));
|
|
3667
|
-
}
|
|
3668
|
-
mapPriority(priority) {
|
|
3669
|
-
switch (priority) {
|
|
3670
|
-
case 1:
|
|
3671
|
-
return "urgent";
|
|
3672
|
-
case 2:
|
|
3673
|
-
return "high";
|
|
3674
|
-
case 3:
|
|
3675
|
-
return "medium";
|
|
3676
|
-
case 4:
|
|
3677
|
-
return "low";
|
|
3678
|
-
default:
|
|
3679
|
-
return "none";
|
|
3680
|
-
}
|
|
3681
|
-
}
|
|
3682
|
-
};
|
|
3683
|
-
|
|
3684
|
-
export {
|
|
3685
|
-
loadConfig,
|
|
3686
|
-
saveConfig,
|
|
3687
|
-
trackIssueOccurrence,
|
|
3688
|
-
recordBypass,
|
|
3689
|
-
shouldAutoFix,
|
|
3690
|
-
shouldBlockPush,
|
|
3691
|
-
getAutonomyConfig,
|
|
3692
|
-
formatAuditLog,
|
|
3693
|
-
getAuditStatistics,
|
|
3694
|
-
getRecentAuditLogs,
|
|
3695
|
-
getSkillAuditLogs,
|
|
3696
|
-
runShellCommandSync,
|
|
3697
|
-
getStagedChanges,
|
|
3698
|
-
getUncommittedChanges,
|
|
3699
|
-
perceiveCurrentChanges,
|
|
3700
|
-
reasonAboutChangesHumanReadable,
|
|
3701
|
-
projectInfoExists,
|
|
3702
|
-
loadProjectInfo,
|
|
3703
|
-
initProjectInfo,
|
|
3704
|
-
getProjectSection,
|
|
3705
|
-
updateProjectSection,
|
|
3706
|
-
appendToSection,
|
|
3707
|
-
getProjectSections,
|
|
3708
|
-
getProjectInfoStructured,
|
|
3709
|
-
getSkillCategories,
|
|
3710
|
-
loadBootstrapContext,
|
|
3711
|
-
initializeBootstrapFiles,
|
|
3712
|
-
completeBootstrap,
|
|
3713
|
-
needsBootstrap,
|
|
3714
|
-
loadRules,
|
|
3715
|
-
loadTeamInfo,
|
|
3716
|
-
saveCheckpoint,
|
|
3717
|
-
listCheckpoints,
|
|
3718
|
-
getLastCheckpoint,
|
|
3719
|
-
handleCheckpointCommand,
|
|
3720
|
-
formatFriendlyError,
|
|
3721
|
-
exportToJson,
|
|
3722
|
-
importFromJson,
|
|
3723
|
-
IncidentIndex,
|
|
3724
|
-
LearningEngine,
|
|
3725
|
-
LinearIngester,
|
|
3726
|
-
loadContextState,
|
|
3727
|
-
getContextForAI
|
|
3728
|
-
};
|
|
3729
|
-
//# sourceMappingURL=chunk-FLBK5ILJ.js.map
|