ghagga-core 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +3 -1
  2. package/dist/agents/consensus.d.ts +2 -1
  3. package/dist/agents/consensus.d.ts.map +1 -1
  4. package/dist/agents/consensus.js +4 -2
  5. package/dist/agents/consensus.js.map +1 -1
  6. package/dist/agents/prompts.d.ts +10 -1
  7. package/dist/agents/prompts.d.ts.map +1 -1
  8. package/dist/agents/prompts.js +24 -2
  9. package/dist/agents/prompts.js.map +1 -1
  10. package/dist/agents/simple.d.ts +2 -1
  11. package/dist/agents/simple.d.ts.map +1 -1
  12. package/dist/agents/simple.js +4 -2
  13. package/dist/agents/simple.js.map +1 -1
  14. package/dist/agents/workflow.d.ts +2 -1
  15. package/dist/agents/workflow.d.ts.map +1 -1
  16. package/dist/agents/workflow.js +12 -3
  17. package/dist/agents/workflow.js.map +1 -1
  18. package/dist/format.d.ts +11 -0
  19. package/dist/format.d.ts.map +1 -0
  20. package/dist/format.js +84 -0
  21. package/dist/format.js.map +1 -0
  22. package/dist/index.d.ts +5 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/memory/engram-client.d.ts +38 -0
  27. package/dist/memory/engram-client.d.ts.map +1 -0
  28. package/dist/memory/engram-client.js +153 -0
  29. package/dist/memory/engram-client.js.map +1 -0
  30. package/dist/memory/engram-mapping.d.ts +83 -0
  31. package/dist/memory/engram-mapping.d.ts.map +1 -0
  32. package/dist/memory/engram-mapping.js +159 -0
  33. package/dist/memory/engram-mapping.js.map +1 -0
  34. package/dist/memory/engram-types.d.ts +51 -0
  35. package/dist/memory/engram-types.d.ts.map +1 -0
  36. package/dist/memory/engram-types.js +12 -0
  37. package/dist/memory/engram-types.js.map +1 -0
  38. package/dist/memory/engram.d.ts +76 -0
  39. package/dist/memory/engram.d.ts.map +1 -0
  40. package/dist/memory/engram.js +194 -0
  41. package/dist/memory/engram.js.map +1 -0
  42. package/dist/memory/persist.d.ts +5 -5
  43. package/dist/memory/persist.d.ts.map +1 -1
  44. package/dist/memory/persist.js +23 -24
  45. package/dist/memory/persist.js.map +1 -1
  46. package/dist/memory/search.d.ts +6 -5
  47. package/dist/memory/search.d.ts.map +1 -1
  48. package/dist/memory/search.js +7 -8
  49. package/dist/memory/search.js.map +1 -1
  50. package/dist/memory/sqlite.d.ts +49 -0
  51. package/dist/memory/sqlite.d.ts.map +1 -0
  52. package/dist/memory/sqlite.js +349 -0
  53. package/dist/memory/sqlite.js.map +1 -0
  54. package/dist/pipeline.d.ts.map +1 -1
  55. package/dist/pipeline.js +167 -58
  56. package/dist/pipeline.js.map +1 -1
  57. package/dist/providers/index.d.ts +4 -0
  58. package/dist/providers/index.d.ts.map +1 -1
  59. package/dist/providers/index.js +12 -0
  60. package/dist/providers/index.js.map +1 -1
  61. package/dist/tools/cpd.d.ts.map +1 -1
  62. package/dist/tools/cpd.js +20 -9
  63. package/dist/tools/cpd.js.map +1 -1
  64. package/dist/tools/runner.d.ts +5 -2
  65. package/dist/tools/runner.d.ts.map +1 -1
  66. package/dist/tools/runner.js +37 -18
  67. package/dist/tools/runner.js.map +1 -1
  68. package/dist/tools/semgrep.d.ts.map +1 -1
  69. package/dist/tools/semgrep.js +21 -4
  70. package/dist/tools/semgrep.js.map +1 -1
  71. package/dist/tools/trivy.d.ts.map +1 -1
  72. package/dist/tools/trivy.js +6 -2
  73. package/dist/tools/trivy.js.map +1 -1
  74. package/dist/types.d.ts +133 -11
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +2 -1
  77. package/dist/types.js.map +1 -1
  78. package/dist/utils/token-budget.d.ts.map +1 -1
  79. package/dist/utils/token-budget.js +5 -0
  80. package/dist/utils/token-budget.js.map +1 -1
  81. package/package.json +4 -3
@@ -0,0 +1,349 @@
1
+ /**
2
+ * SQLite-backed memory storage using sql.js (pure WASM).
3
+ *
4
+ * Thread safety: This class is NOT thread-safe. It operates as an in-memory
5
+ * database with manual file persistence via close(). Designed for single-process
6
+ * environments (CLI, GitHub Action).
7
+ */
8
+ import initSqlJs from 'fts5-sql-bundle';
9
+ import { createHash } from 'node:crypto';
10
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
11
+ import { dirname } from 'node:path';
12
+ const SCHEMA_SQL = `
13
+ CREATE TABLE IF NOT EXISTS memory_sessions (
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ project TEXT NOT NULL,
16
+ pr_number INTEGER,
17
+ summary TEXT,
18
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
19
+ ended_at TEXT
20
+ );
21
+
22
+ CREATE TABLE IF NOT EXISTS memory_observations (
23
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
24
+ session_id INTEGER REFERENCES memory_sessions(id) ON DELETE CASCADE,
25
+ project TEXT NOT NULL,
26
+ type TEXT NOT NULL,
27
+ title TEXT NOT NULL,
28
+ content TEXT NOT NULL,
29
+ severity TEXT,
30
+ topic_key TEXT,
31
+ file_paths TEXT DEFAULT '[]',
32
+ content_hash TEXT,
33
+ revision_count INTEGER NOT NULL DEFAULT 1,
34
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
35
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS idx_obs_project ON memory_observations(project);
39
+ CREATE INDEX IF NOT EXISTS idx_obs_topic_key ON memory_observations(topic_key);
40
+ CREATE INDEX IF NOT EXISTS idx_obs_content_hash ON memory_observations(content_hash);
41
+
42
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_observations_fts
43
+ USING fts5(title, content, content='memory_observations', content_rowid='id');
44
+
45
+ CREATE TRIGGER IF NOT EXISTS obs_fts_insert AFTER INSERT ON memory_observations BEGIN
46
+ INSERT INTO memory_observations_fts(rowid, title, content)
47
+ VALUES (new.id, new.title, new.content);
48
+ END;
49
+
50
+ CREATE TRIGGER IF NOT EXISTS obs_fts_update AFTER UPDATE ON memory_observations BEGIN
51
+ INSERT INTO memory_observations_fts(memory_observations_fts, rowid, title, content)
52
+ VALUES ('delete', old.id, old.title, old.content);
53
+ INSERT INTO memory_observations_fts(rowid, title, content)
54
+ VALUES (new.id, new.title, new.content);
55
+ END;
56
+
57
+ CREATE TRIGGER IF NOT EXISTS obs_fts_delete AFTER DELETE ON memory_observations BEGIN
58
+ INSERT INTO memory_observations_fts(memory_observations_fts, rowid, title, content)
59
+ VALUES ('delete', old.id, old.title, old.content);
60
+ END;
61
+ `;
62
+ const DEDUP_WINDOW_MINUTES = 15;
63
+ export class SqliteMemoryStorage {
64
+ db;
65
+ filePath;
66
+ constructor(db, filePath) {
67
+ this.db = db;
68
+ this.filePath = filePath;
69
+ }
70
+ /**
71
+ * Async factory — handles WASM initialization and file loading.
72
+ * If filePath exists, loads the existing DB. Otherwise, creates a fresh one.
73
+ */
74
+ static async create(filePath) {
75
+ const SQL = await initSqlJs();
76
+ let db;
77
+ if (existsSync(filePath)) {
78
+ const buffer = readFileSync(filePath);
79
+ db = new SQL.Database(buffer);
80
+ }
81
+ else {
82
+ db = new SQL.Database();
83
+ }
84
+ db.run(SCHEMA_SQL);
85
+ // Migration: add severity column to existing databases
86
+ try {
87
+ db.run('ALTER TABLE memory_observations ADD COLUMN severity TEXT');
88
+ }
89
+ catch {
90
+ // Column already exists — idempotent migration
91
+ }
92
+ return new SqliteMemoryStorage(db, filePath);
93
+ }
94
+ async searchObservations(project, query, options = {}) {
95
+ const { limit = 10, type } = options;
96
+ // Convert space-separated keywords to FTS5 OR query
97
+ const ftsQuery = query
98
+ .trim()
99
+ .split(/\s+/)
100
+ .filter((w) => w.length > 0)
101
+ .map((w) => `"${w.replace(/"/g, '""')}"`)
102
+ .join(' OR ');
103
+ if (!ftsQuery)
104
+ return [];
105
+ let sql = `
106
+ SELECT o.id, o.type, o.title, o.content, o.file_paths, o.severity
107
+ FROM memory_observations o
108
+ JOIN memory_observations_fts fts ON fts.rowid = o.id
109
+ WHERE memory_observations_fts MATCH ?
110
+ AND o.project = ?
111
+ `;
112
+ const params = [ftsQuery, project];
113
+ if (type) {
114
+ sql += ' AND o.type = ?';
115
+ params.push(type);
116
+ }
117
+ sql += ' ORDER BY bm25(memory_observations_fts) LIMIT ?';
118
+ params.push(limit);
119
+ const stmt = this.db.prepare(sql);
120
+ stmt.bind(params);
121
+ const rows = [];
122
+ while (stmt.step()) {
123
+ const row = stmt.getAsObject();
124
+ rows.push({
125
+ id: row['id'],
126
+ type: row['type'],
127
+ title: row['title'],
128
+ content: row['content'],
129
+ filePaths: row['file_paths'] ? JSON.parse(row['file_paths']) : null,
130
+ severity: row['severity'] ?? null,
131
+ });
132
+ }
133
+ stmt.free();
134
+ return rows;
135
+ }
136
+ async saveObservation(data) {
137
+ const contentHash = createHash('sha256')
138
+ .update(`${data.type}:${data.title}:${data.content}`)
139
+ .digest('hex');
140
+ // Dedup: check for same content hash within 15-minute window
141
+ const dedupRows = this.db.exec(`
142
+ SELECT id, type, title, content, file_paths FROM memory_observations
143
+ WHERE content_hash = ? AND project = ?
144
+ AND created_at > datetime('now', '-${DEDUP_WINDOW_MINUTES} minutes')
145
+ LIMIT 1
146
+ `, [contentHash, data.project]);
147
+ if (dedupRows.length > 0 && dedupRows[0].values.length > 0) {
148
+ const row = dedupRows[0].values[0];
149
+ const existingId = row[0];
150
+ // If the existing observation is from a different session, reassign it
151
+ if (data.sessionId != null) {
152
+ const existingSessionRows = this.db.exec(`SELECT session_id FROM memory_observations WHERE id = ?`, [existingId]);
153
+ const existingSessionId = existingSessionRows[0]?.values[0]?.[0];
154
+ if (existingSessionId !== data.sessionId) {
155
+ this.db.run(`UPDATE memory_observations SET session_id = ?, updated_at = datetime('now') WHERE id = ?`, [data.sessionId, existingId]);
156
+ }
157
+ }
158
+ return {
159
+ id: existingId,
160
+ type: row[1],
161
+ title: row[2],
162
+ content: row[3],
163
+ filePaths: row[4] ? JSON.parse(row[4]) : null,
164
+ severity: null,
165
+ };
166
+ }
167
+ // TopicKey upsert
168
+ if (data.topicKey) {
169
+ const existingByTopic = this.db.exec(`
170
+ SELECT id FROM memory_observations
171
+ WHERE topic_key = ? AND project = ?
172
+ LIMIT 1
173
+ `, [data.topicKey, data.project]);
174
+ if (existingByTopic.length > 0 && existingByTopic[0].values.length > 0) {
175
+ const existingId = existingByTopic[0].values[0][0];
176
+ const filePathsJson = JSON.stringify(data.filePaths ?? []);
177
+ const updated = this.db.exec(`
178
+ UPDATE memory_observations
179
+ SET content = ?, title = ?, content_hash = ?, file_paths = ?,
180
+ severity = ?,
181
+ revision_count = revision_count + 1,
182
+ updated_at = datetime('now')
183
+ WHERE id = ?
184
+ RETURNING id, type, title, content, file_paths, severity
185
+ `, [data.content, data.title, contentHash, filePathsJson, data.severity ?? null, existingId]);
186
+ const row = updated[0].values[0];
187
+ return {
188
+ id: row[0],
189
+ type: row[1],
190
+ title: row[2],
191
+ content: row[3],
192
+ filePaths: row[4] ? JSON.parse(row[4]) : null,
193
+ severity: row[5] ?? null,
194
+ };
195
+ }
196
+ }
197
+ // New observation
198
+ const filePathsJson = JSON.stringify(data.filePaths ?? []);
199
+ const inserted = this.db.exec(`
200
+ INSERT INTO memory_observations
201
+ (session_id, project, type, title, content, severity, topic_key, file_paths, content_hash)
202
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
203
+ RETURNING id, type, title, content, file_paths, severity
204
+ `, [
205
+ data.sessionId ?? null,
206
+ data.project,
207
+ data.type,
208
+ data.title,
209
+ data.content,
210
+ data.severity ?? null,
211
+ data.topicKey ?? null,
212
+ filePathsJson,
213
+ contentHash,
214
+ ]);
215
+ const row = inserted[0].values[0];
216
+ return {
217
+ id: row[0],
218
+ type: row[1],
219
+ title: row[2],
220
+ content: row[3],
221
+ filePaths: row[4] ? JSON.parse(row[4]) : null,
222
+ severity: row[5] ?? null,
223
+ };
224
+ }
225
+ async createSession(data) {
226
+ const result = this.db.exec(`
227
+ INSERT INTO memory_sessions (project, pr_number)
228
+ VALUES (?, ?)
229
+ RETURNING id
230
+ `, [data.project, data.prNumber ?? null]);
231
+ return { id: result[0].values[0][0] };
232
+ }
233
+ async endSession(sessionId, summary) {
234
+ this.db.run(`
235
+ UPDATE memory_sessions
236
+ SET ended_at = datetime('now'), summary = ?
237
+ WHERE id = ?
238
+ `, [summary, sessionId]);
239
+ }
240
+ // ── Management methods ──────────────────────────────────────────
241
+ mapToDetail(row) {
242
+ return {
243
+ id: row['id'],
244
+ type: row['type'],
245
+ title: row['title'],
246
+ content: row['content'],
247
+ filePaths: row['file_paths'] ? JSON.parse(row['file_paths']) : null,
248
+ severity: row['severity'] ?? null,
249
+ project: row['project'],
250
+ topicKey: row['topic_key'] ?? null,
251
+ revisionCount: row['revision_count'],
252
+ createdAt: row['created_at'],
253
+ updatedAt: row['updated_at'],
254
+ };
255
+ }
256
+ async listObservations(options = {}) {
257
+ const { project, type, limit = 20, offset = 0 } = options;
258
+ let sql = `
259
+ SELECT id, type, title, content, file_paths, severity, project, topic_key,
260
+ revision_count, created_at, updated_at
261
+ FROM memory_observations
262
+ WHERE 1=1
263
+ `;
264
+ const params = [];
265
+ if (project) {
266
+ sql += ' AND project = ?';
267
+ params.push(project);
268
+ }
269
+ if (type) {
270
+ sql += ' AND type = ?';
271
+ params.push(type);
272
+ }
273
+ sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
274
+ params.push(limit, offset);
275
+ const stmt = this.db.prepare(sql);
276
+ stmt.bind(params);
277
+ const rows = [];
278
+ while (stmt.step()) {
279
+ const row = stmt.getAsObject();
280
+ rows.push(this.mapToDetail(row));
281
+ }
282
+ stmt.free();
283
+ return rows;
284
+ }
285
+ async getObservation(id) {
286
+ const stmt = this.db.prepare(`
287
+ SELECT id, type, title, content, file_paths, severity, project, topic_key,
288
+ revision_count, created_at, updated_at
289
+ FROM memory_observations
290
+ WHERE id = ?
291
+ `);
292
+ stmt.bind([id]);
293
+ if (!stmt.step()) {
294
+ stmt.free();
295
+ return null;
296
+ }
297
+ const row = stmt.getAsObject();
298
+ stmt.free();
299
+ return this.mapToDetail(row);
300
+ }
301
+ async deleteObservation(id) {
302
+ this.db.run('DELETE FROM memory_observations WHERE id = ?', [id]);
303
+ return this.db.getRowsModified() > 0;
304
+ }
305
+ async getStats() {
306
+ // Query 1: totals and date range
307
+ const totals = this.db.exec('SELECT COUNT(*) AS total, MIN(created_at) AS oldest, MAX(created_at) AS newest FROM memory_observations');
308
+ const totalRow = totals[0]?.values[0];
309
+ const totalObservations = totalRow?.[0] ?? 0;
310
+ const oldestObservation = totalRow?.[1] ?? null;
311
+ const newestObservation = totalRow?.[2] ?? null;
312
+ // Query 2: count by type
313
+ const byTypeResult = this.db.exec('SELECT type, COUNT(*) AS count FROM memory_observations GROUP BY type ORDER BY count DESC');
314
+ const byType = {};
315
+ if (byTypeResult.length > 0) {
316
+ for (const row of byTypeResult[0].values) {
317
+ byType[row[0]] = row[1];
318
+ }
319
+ }
320
+ // Query 3: count by project
321
+ const byProjectResult = this.db.exec('SELECT project, COUNT(*) AS count FROM memory_observations GROUP BY project ORDER BY count DESC');
322
+ const byProject = {};
323
+ if (byProjectResult.length > 0) {
324
+ for (const row of byProjectResult[0].values) {
325
+ byProject[row[0]] = row[1];
326
+ }
327
+ }
328
+ return { totalObservations, byType, byProject, oldestObservation, newestObservation };
329
+ }
330
+ async clearObservations(options = {}) {
331
+ if (options.project) {
332
+ this.db.run('DELETE FROM memory_observations WHERE project = ?', [options.project]);
333
+ }
334
+ else {
335
+ this.db.run('DELETE FROM memory_observations');
336
+ }
337
+ return this.db.getRowsModified();
338
+ }
339
+ async close() {
340
+ const dir = dirname(this.filePath);
341
+ if (!existsSync(dir)) {
342
+ mkdirSync(dir, { recursive: true });
343
+ }
344
+ const data = this.db.export();
345
+ writeFileSync(this.filePath, Buffer.from(data));
346
+ this.db.close();
347
+ }
348
+ }
349
+ //# sourceMappingURL=sqlite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/memory/sqlite.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAYH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAgB,MAAM,YAAY,CAAC;AA4B1E;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAiK9E"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAYH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAiD,MAAM,YAAY,CAAC;AAuC3G;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAiM9E"}
package/dist/pipeline.js CHANGED
@@ -33,6 +33,15 @@ function validateInput(input) {
33
33
  if (!input.diff || input.diff.trim().length === 0) {
34
34
  throw new Error('Review input must include a non-empty diff');
35
35
  }
36
+ // If AI review is explicitly disabled, no provider/model/key needed
37
+ if (input.aiReviewEnabled === false) {
38
+ return;
39
+ }
40
+ // Provider chain mode: validate the chain has entries
41
+ if (input.providerChain && input.providerChain.length > 0) {
42
+ return;
43
+ }
44
+ // Single provider mode (CLI/Action backward compat)
36
45
  if (!input.apiKey) {
37
46
  throw new Error('Review input must include an API key');
38
47
  }
@@ -58,6 +67,8 @@ function validateInput(input) {
58
67
  export async function reviewPipeline(input) {
59
68
  const startTime = Date.now();
60
69
  const emit = input.onProgress ?? (() => { });
70
+ // Resolve whether AI review is enabled
71
+ const aiEnabled = resolveAiEnabled(input);
61
72
  // ── Step 1: Validate ───────────────────────────────────────
62
73
  validateInput(input);
63
74
  emit({ step: 'validate', message: 'Input validated' });
@@ -85,17 +96,25 @@ export async function reviewPipeline(input) {
85
96
  detail: stacks.length > 0 ? stacks.map((s) => ` ${s}`).join('\n') : ' (none detected)',
86
97
  });
87
98
  // ── Step 4: Truncate diff to fit token budget ──────────────
88
- const { diffBudget } = calculateTokenBudget(input.model);
99
+ const primaryModel = resolvePrimaryModel(input);
100
+ const { diffBudget } = calculateTokenBudget(primaryModel);
89
101
  const { truncated: truncatedDiff } = truncateDiff(filteredDiff, diffBudget);
90
102
  emit({
91
103
  step: 'token-budget',
92
104
  message: `Token budget: ${diffBudget.toLocaleString()} tokens for diff`,
93
105
  });
94
106
  // ── Step 5: Run static analysis (in parallel with memory) ──
95
- emit({ step: 'static-analysis', message: 'Running static analysis & memory search...' });
107
+ // If precomputed results are available (from GitHub Actions runner), use those directly.
108
+ // Otherwise, run tools locally (CLI/Action modes).
109
+ emit({ step: 'static-analysis', message: input.precomputedStaticAnalysis
110
+ ? 'Using precomputed static analysis from runner...'
111
+ : 'Running static analysis & memory search...',
112
+ });
96
113
  const [staticResult, memoryContext] = await Promise.all([
97
- runStaticAnalysisSafe(fileList, input),
98
- searchMemorySafe(input, fileList),
114
+ input.precomputedStaticAnalysis
115
+ ? Promise.resolve(input.precomputedStaticAnalysis)
116
+ : runStaticAnalysisSafe(fileList, input),
117
+ aiEnabled ? searchMemorySafe(input, fileList) : Promise.resolve(null),
99
118
  ]);
100
119
  const staticContext = formatStaticAnalysisContext(staticResult);
101
120
  {
@@ -108,53 +127,73 @@ export async function reviewPipeline(input) {
108
127
  detail: toolsSummary + (memoryContext ? '\n memory: loaded' : '\n memory: disabled'),
109
128
  });
110
129
  }
111
- // ── Step 6: Execute agent mode ─────────────────────────────
112
- emit({ step: 'agent-start', message: `Running ${input.mode} agent...` });
130
+ // ── Step 6: Execute agent mode (or skip if AI disabled) ────
113
131
  let result;
114
- switch (input.mode) {
115
- case 'simple':
116
- result = await runSimpleReview({
117
- diff: truncatedDiff,
118
- provider: input.provider,
119
- model: input.model,
120
- apiKey: input.apiKey,
121
- staticContext,
122
- memoryContext,
123
- stackHints,
124
- onProgress: input.onProgress,
125
- });
126
- break;
127
- case 'workflow':
128
- result = await runWorkflowReview({
129
- diff: truncatedDiff,
130
- provider: input.provider,
131
- model: input.model,
132
- apiKey: input.apiKey,
133
- staticContext,
134
- memoryContext,
135
- stackHints,
136
- onProgress: input.onProgress,
137
- });
138
- break;
139
- case 'consensus':
140
- // Consensus mode uses the primary model with different stances
141
- // In production, the caller would configure multiple models via the context
142
- result = await runConsensusReview({
143
- diff: truncatedDiff,
144
- models: [
145
- { provider: input.provider, model: input.model, apiKey: input.apiKey, stance: 'for' },
146
- { provider: input.provider, model: input.model, apiKey: input.apiKey, stance: 'against' },
147
- { provider: input.provider, model: input.model, apiKey: input.apiKey, stance: 'neutral' },
148
- ],
149
- staticContext,
150
- memoryContext,
151
- stackHints,
152
- onProgress: input.onProgress,
153
- });
154
- break;
155
- default: {
156
- const _exhaustive = input.mode;
157
- throw new Error(`Unknown review mode: ${_exhaustive}`);
132
+ if (!aiEnabled) {
133
+ // Static-only mode: no LLM calls
134
+ emit({ step: 'agent-start', message: 'AI review disabled — returning static analysis only' });
135
+ result = createStaticOnlyResult(staticResult, input.mode, startTime);
136
+ }
137
+ else {
138
+ // Resolve the primary provider for agent calls
139
+ const primary = resolvePrimaryProvider(input);
140
+ emit({ step: 'agent-start', message: `Running ${input.mode} agent with ${primary.provider}/${primary.model}...` });
141
+ try {
142
+ switch (input.mode) {
143
+ case 'simple':
144
+ result = await runSimpleReview({
145
+ diff: truncatedDiff,
146
+ provider: primary.provider,
147
+ model: primary.model,
148
+ apiKey: primary.apiKey,
149
+ staticContext,
150
+ memoryContext,
151
+ stackHints,
152
+ reviewLevel: input.settings.reviewLevel,
153
+ onProgress: input.onProgress,
154
+ });
155
+ break;
156
+ case 'workflow':
157
+ result = await runWorkflowReview({
158
+ diff: truncatedDiff,
159
+ provider: primary.provider,
160
+ model: primary.model,
161
+ apiKey: primary.apiKey,
162
+ staticContext,
163
+ memoryContext,
164
+ stackHints,
165
+ reviewLevel: input.settings.reviewLevel,
166
+ onProgress: input.onProgress,
167
+ });
168
+ break;
169
+ case 'consensus':
170
+ result = await runConsensusReview({
171
+ diff: truncatedDiff,
172
+ models: [
173
+ { provider: primary.provider, model: primary.model, apiKey: primary.apiKey, stance: 'for' },
174
+ { provider: primary.provider, model: primary.model, apiKey: primary.apiKey, stance: 'against' },
175
+ { provider: primary.provider, model: primary.model, apiKey: primary.apiKey, stance: 'neutral' },
176
+ ],
177
+ staticContext,
178
+ memoryContext,
179
+ stackHints,
180
+ reviewLevel: input.settings.reviewLevel,
181
+ onProgress: input.onProgress,
182
+ });
183
+ break;
184
+ default: {
185
+ const _exhaustive = input.mode;
186
+ throw new Error(`Unknown review mode: ${_exhaustive}`);
187
+ }
188
+ }
189
+ }
190
+ catch (error) {
191
+ // All providers failed — return static results with NEEDS_HUMAN_REVIEW
192
+ console.warn('[ghagga] All AI providers failed, returning static analysis only:', error instanceof Error ? error.message : String(error));
193
+ emit({ step: 'agent-failed', message: 'AI review failed — returning static analysis only' });
194
+ result = createStaticOnlyResult(staticResult, input.mode, startTime);
195
+ result.status = 'NEEDS_HUMAN_REVIEW';
196
+ result.summary = `AI review failed (${error instanceof Error ? error.message : 'unknown error'}). Static analysis results are shown below.`;
158
197
  }
159
198
  }
160
199
  // ── Step 7: Merge static analysis into result ──────────────
@@ -180,15 +219,53 @@ export async function reviewPipeline(input) {
180
219
  }
181
220
  // Update execution time to cover the full pipeline
182
221
  result.metadata.executionTimeMs = Date.now() - startTime;
183
- // ── Step 8: Persist to memory (fire-and-forget) ────────────
184
- if (input.settings.enableMemory && input.db && input.context) {
185
- // Don't await memory persistence shouldn't block the response
186
- persistReviewObservations(input.db, input.context.repoFullName, input.context.prNumber, result).catch((error) => {
222
+ // ── Step 8: Persist to memory (awaited for SQLite correctness) ──
223
+ if (input.settings.enableMemory && input.memoryStorage && input.context) {
224
+ await persistReviewObservations(input.memoryStorage, input.context.repoFullName, input.context.prNumber, result).catch((error) => {
187
225
  console.warn('[ghagga] Memory persist failed (non-fatal):', error instanceof Error ? error.message : String(error));
188
226
  });
189
227
  }
190
228
  return result;
191
229
  }
230
+ // ─── Provider Resolution ────────────────────────────────────────
231
+ /**
232
+ * Determine if AI review is enabled.
233
+ * Defaults to true for backward compatibility (CLI/Action don't set this).
234
+ */
235
+ function resolveAiEnabled(input) {
236
+ if (input.aiReviewEnabled === false)
237
+ return false;
238
+ // If chain is explicitly empty and no single provider, treat as disabled
239
+ if (input.providerChain && input.providerChain.length === 0 && !input.provider) {
240
+ console.warn('[ghagga] AI review enabled but provider chain is empty and no single provider — treating as disabled');
241
+ return false;
242
+ }
243
+ return true;
244
+ }
245
+ /**
246
+ * Resolve the primary provider from chain or flat fields.
247
+ * Returns the first entry in the chain, or builds one from flat fields.
248
+ */
249
+ function resolvePrimaryProvider(input) {
250
+ if (input.providerChain && input.providerChain.length > 0) {
251
+ return input.providerChain[0];
252
+ }
253
+ // Backward compat: single provider from flat fields
254
+ return {
255
+ provider: input.provider,
256
+ model: input.model,
257
+ apiKey: input.apiKey,
258
+ };
259
+ }
260
+ /**
261
+ * Resolve the model name for token budget calculation.
262
+ */
263
+ function resolvePrimaryModel(input) {
264
+ if (input.providerChain && input.providerChain.length > 0) {
265
+ return input.providerChain[0].model;
266
+ }
267
+ return input.model ?? 'gpt-4o-mini';
268
+ }
192
269
  // ─── Helpers ────────────────────────────────────────────────────
193
270
  /**
194
271
  * Run static analysis with graceful degradation.
@@ -228,11 +305,11 @@ async function runStaticAnalysisSafe(fileList, input) {
228
305
  * Returns null if memory is disabled or unavailable.
229
306
  */
230
307
  async function searchMemorySafe(input, fileList) {
231
- if (!input.settings.enableMemory || !input.db || !input.context) {
308
+ if (!input.settings.enableMemory || !input.memoryStorage || !input.context) {
232
309
  return null;
233
310
  }
234
311
  try {
235
- return await searchMemoryForContext(input.db, input.context.repoFullName, fileList);
312
+ return await searchMemoryForContext(input.memoryStorage, input.context.repoFullName, fileList);
236
313
  }
237
314
  catch (error) {
238
315
  console.warn('[ghagga] Memory search failed (degrading gracefully):', error instanceof Error ? error.message : String(error));
@@ -243,6 +320,7 @@ async function searchMemorySafe(input, fileList) {
243
320
  * Create a SKIPPED result when all files are filtered out.
244
321
  */
245
322
  function createSkippedResult(input, startTime) {
323
+ const primary = input.providerChain?.[0];
246
324
  return {
247
325
  status: 'SKIPPED',
248
326
  summary: 'All files in the diff matched ignore patterns. No review was performed.',
@@ -255,8 +333,8 @@ function createSkippedResult(input, startTime) {
255
333
  memoryContext: null,
256
334
  metadata: {
257
335
  mode: input.mode,
258
- provider: input.provider,
259
- model: input.model,
336
+ provider: primary?.provider ?? input.provider ?? 'none',
337
+ model: primary?.model ?? input.model ?? 'unknown',
260
338
  tokensUsed: 0,
261
339
  executionTimeMs: Date.now() - startTime,
262
340
  toolsRun: [],
@@ -264,4 +342,35 @@ function createSkippedResult(input, startTime) {
264
342
  },
265
343
  };
266
344
  }
345
+ /**
346
+ * Create a result with only static analysis findings (no AI).
347
+ * Used when AI review is disabled or when all providers fail.
348
+ */
349
+ function createStaticOnlyResult(staticResult, mode, startTime) {
350
+ // Determine status from static findings severity
351
+ const allFindings = [
352
+ ...staticResult.semgrep.findings,
353
+ ...staticResult.trivy.findings,
354
+ ...staticResult.cpd.findings,
355
+ ];
356
+ const hasCriticalOrHigh = allFindings.some((f) => f.severity === 'critical' || f.severity === 'high');
357
+ return {
358
+ status: hasCriticalOrHigh ? 'FAILED' : 'PASSED',
359
+ summary: allFindings.length > 0
360
+ ? `Static analysis found ${allFindings.length} finding(s). AI review was not performed.`
361
+ : 'Static analysis found no issues. AI review was not performed.',
362
+ findings: [], // Will be merged in step 7
363
+ staticAnalysis: staticResult,
364
+ memoryContext: null,
365
+ metadata: {
366
+ mode,
367
+ provider: 'none',
368
+ model: 'static-only',
369
+ tokensUsed: 0,
370
+ executionTimeMs: Date.now() - startTime,
371
+ toolsRun: [],
372
+ toolsSkipped: [],
373
+ },
374
+ };
375
+ }
267
376
  //# sourceMappingURL=pipeline.js.map