opencode-autognosis 2.3.0 → 2.4.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.
@@ -13,9 +13,11 @@ export declare class CodeGraphDB {
13
13
  }): void;
14
14
  getJob(id: string): any;
15
15
  listJobs(type?: string, limit?: number): any;
16
- postToBlackboard(author: string, message: string, topic?: string, symbolId?: string): void;
16
+ postToBlackboard(author: string, message: string, topic?: string, symbolId?: string, isPinned?: boolean): void;
17
+ getGraffiti(symbolId: string, limit?: number): any;
18
+ archiveGraffiti(symbolId: string): void;
19
+ pinGraffiti(id: number, pinned?: boolean): void;
17
20
  readBlackboard(topic?: string, limit?: number): any;
18
- getGraffiti(symbolId: string): any;
19
21
  acquireLock(resourceId: string, agentName: string, ttlSeconds?: number): void;
20
22
  releaseLock(resourceId: string, agentName: string): void;
21
23
  isLocked(resourceId: string): {
@@ -53,6 +55,7 @@ export declare class CodeGraphDB {
53
55
  };
54
56
  findDependents(filePath: string): string[];
55
57
  searchSymbols(query: string): any[];
58
+ findAffectedTests(symbolName: string): string[];
56
59
  semanticSearch(query: string, limit?: number): Promise<any[]>;
57
60
  private cosineSimilarity;
58
61
  getStats(): {
package/dist/database.js CHANGED
@@ -146,8 +146,11 @@ export class CodeGraphDB {
146
146
  author TEXT,
147
147
  message TEXT,
148
148
  topic TEXT,
149
- symbol_id TEXT, -- Optional link to a code symbol
150
- embedding BLOB, -- For semantic search on the blackboard
149
+ symbol_id TEXT,
150
+ git_hash TEXT, -- Version of code when note was made
151
+ is_archived BOOLEAN DEFAULT 0,
152
+ is_pinned BOOLEAN DEFAULT 0,
153
+ embedding BLOB,
151
154
  timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
152
155
  );
153
156
 
@@ -231,19 +234,54 @@ export class CodeGraphDB {
231
234
  }
232
235
  return this.db.prepare("SELECT * FROM background_jobs ORDER BY created_at DESC LIMIT ?").all(limit);
233
236
  }
234
- postToBlackboard(author, message, topic = 'general', symbolId) {
237
+ postToBlackboard(author, message, topic = 'general', symbolId, isPinned = false) {
238
+ // Get current git hash for contextual verification
239
+ let currentHash = "unknown";
240
+ try {
241
+ const { execSync } = require("node:child_process");
242
+ currentHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
243
+ }
244
+ catch { }
235
245
  const insert = this.db.prepare(`
236
- INSERT INTO blackboard (author, message, topic, symbol_id)
237
- VALUES (?, ?, ?, ?)
246
+ INSERT INTO blackboard (author, message, topic, symbol_id, git_hash, is_pinned)
247
+ VALUES (?, ?, ?, ?, ?, ?)
238
248
  RETURNING id
239
249
  `);
240
- const res = insert.get(author, message, topic, symbolId || null);
241
- // Queue for embedding (blackboard search)
250
+ const res = insert.get(author, message, topic, symbolId || null, currentHash, isPinned ? 1 : 0);
242
251
  this.db.prepare(`
243
252
  INSERT INTO embedding_queue (chunk_id, text_to_embed)
244
253
  VALUES (?, ?)
245
254
  `).run(`blackboard-${res.id}`, `${topic.toUpperCase()}: ${message}`);
246
255
  }
256
+ getGraffiti(symbolId, limit = 3) {
257
+ // Automatically archive notes older than 7 days that aren't pinned
258
+ this.db.prepare(`
259
+ UPDATE blackboard
260
+ SET is_archived = 1
261
+ WHERE is_pinned = 0
262
+ AND timestamp < datetime('now', '-7 days')
263
+ `).run();
264
+ return this.db.prepare(`
265
+ SELECT author, message, timestamp, git_hash, is_pinned
266
+ FROM blackboard
267
+ WHERE symbol_id = ?
268
+ AND is_archived = 0
269
+ ORDER BY is_pinned DESC, timestamp DESC
270
+ LIMIT ?
271
+ `).all(symbolId, limit);
272
+ }
273
+ archiveGraffiti(symbolId) {
274
+ this.db.prepare(`
275
+ UPDATE blackboard
276
+ SET is_archived = 1
277
+ WHERE symbol_id = ? AND is_pinned = 0
278
+ `).run(symbolId);
279
+ }
280
+ pinGraffiti(id, pinned = true) {
281
+ this.db.prepare(`
282
+ UPDATE blackboard SET is_pinned = ? WHERE id = ?
283
+ `).run(pinned ? 1 : 0, id);
284
+ }
247
285
  readBlackboard(topic, limit = 10) {
248
286
  if (topic) {
249
287
  return this.db.prepare(`
@@ -254,14 +292,6 @@ export class CodeGraphDB {
254
292
  SELECT * FROM blackboard ORDER BY timestamp DESC LIMIT ?
255
293
  `).all(limit);
256
294
  }
257
- getGraffiti(symbolId) {
258
- return this.db.prepare(`
259
- SELECT author, message, timestamp
260
- FROM blackboard
261
- WHERE symbol_id = ?
262
- ORDER BY timestamp DESC
263
- `).all(symbolId);
264
- }
265
295
  acquireLock(resourceId, agentName, ttlSeconds = 300) {
266
296
  // Check if already locked by someone else
267
297
  const current = this.isLocked(resourceId);
@@ -494,6 +524,29 @@ ${card.content.slice(0, 2000)}`;
494
524
  `);
495
525
  return stmt.all(`%${query}%`);
496
526
  }
527
+ findAffectedTests(symbolName) {
528
+ // Find all files that call this symbol and look like test files
529
+ const query = this.db.prepare(`
530
+ WITH RECURSIVE impact_tree(caller_chunk_id) AS (
531
+ -- Base case: chunks calling the symbol directly
532
+ SELECT caller_chunk_id FROM calls WHERE callee_name = ?
533
+ UNION
534
+ -- Recursive step: chunks calling chunks in the impact tree
535
+ SELECT c.caller_chunk_id
536
+ FROM calls c
537
+ JOIN impact_tree it ON c.callee_name IN (
538
+ SELECT s.name FROM symbols s WHERE s.chunk_id = it.caller_chunk_id
539
+ )
540
+ )
541
+ SELECT DISTINCT f.path
542
+ FROM files f
543
+ JOIN chunks c ON f.id = c.file_id
544
+ JOIN impact_tree it ON c.id = it.caller_chunk_id
545
+ WHERE f.path LIKE '%.test.%' OR f.path LIKE '%Tests.%' OR f.path LIKE 'test_%'
546
+ `);
547
+ const results = query.all(symbolName);
548
+ return results.map(r => r.path);
549
+ }
497
550
  async semanticSearch(query, limit = 10) {
498
551
  if (!(await ollama.isRunning()))
499
552
  throw new Error("Ollama is not running.");
@@ -40,14 +40,14 @@ async function updateBridgePrompt(plugins) {
40
40
  if (!fsSync.existsSync(bridgePath))
41
41
  return "bridge.md not found at " + bridgePath;
42
42
  const toolsSection = `
43
- ## Current Consolidated Tools (Autognosis v2.3)
43
+ ## Current Consolidated Tools (Autognosis v2.4)
44
44
  - code_search: Universal search (semantic, symbol, filename, content).
45
45
  - code_analyze: Deep structural analysis and impact reports.
46
- - code_context: Working memory (ActiveSet) management, LRU eviction, and Symbol Graffiti.
47
- - code_read: Precise symbol jumping and file slicing with Mutex Lock checks.
48
- - code_propose: Planning, patch generation, PR promotion, and Intent indexing.
49
- - code_status: System health, background jobs, Multi-Agent Blackboard, and Resource Locks.
50
- - code_setup: Environment initialization, AI setup, and Architectural Boundaries.
46
+ - code_context: Working memory management and LRU eviction.
47
+ - code_read: Precise reading with Mutex Lock checks and Graffiti retrieval.
48
+ - code_propose: Planning, patching, validation, and PR promotion. Now features surgical test scoping.
49
+ - code_status: Dashboard, background jobs, blackboard, and resource locks.
50
+ - code_setup: Environment initialization and architectural boundaries.
51
51
 
52
52
  ## Other Detected Plugins
53
53
  ${plugins.filter(p => p !== "opencode-autognosis").map(p => `- ${p}`).join('\n')}
@@ -127,7 +127,7 @@ export function unifiedTools() {
127
127
  }
128
128
  }),
129
129
  code_read: tool({
130
- description: "Precise reading of symbols or file slices. Follows current plan. Checks for locks and returns graffiti.",
130
+ description: "Precise reading of symbols or file slices. Follows current plan. Checks for locks and returns historical graffiti.",
131
131
  args: {
132
132
  symbol: tool.schema.string().optional().describe("Symbol to jump to"),
133
133
  file: tool.schema.string().optional().describe("File path to read"),
@@ -140,7 +140,20 @@ export function unifiedTools() {
140
140
  if (resourceId) {
141
141
  getDb().logAccess(resourceId, args.plan_id);
142
142
  const lock = getDb().isLocked(resourceId);
143
- const graffiti = getDb().getGraffiti(resourceId);
143
+ const graffiti = getDb().getGraffiti(resourceId, 3);
144
+ let currentHash = "";
145
+ try {
146
+ const { execSync } = await import("node:child_process");
147
+ currentHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
148
+ }
149
+ catch { }
150
+ const verifiedGraffiti = graffiti.map((g) => ({
151
+ author: g.author,
152
+ message: g.message,
153
+ timestamp: g.timestamp,
154
+ status: g.git_hash !== currentHash ? "LEGACY (Potentially Outdated)" : "CURRENT",
155
+ is_pinned: !!g.is_pinned
156
+ }));
144
157
  let result;
145
158
  if (args.symbol) {
146
159
  result = await internal.jump_to_symbol.execute({ symbol: args.symbol, plan_id: args.plan_id });
@@ -153,7 +166,7 @@ export function unifiedTools() {
153
166
  ...parsed,
154
167
  coordination: {
155
168
  lock_status: lock ? `LOCKED by ${lock.owner_agent}` : "FREE",
156
- graffiti: graffiti.length > 0 ? graffiti : undefined
169
+ historical_notes: verifiedGraffiti.length > 0 ? verifiedGraffiti : undefined
157
170
  }
158
171
  }, null, 2);
159
172
  }
@@ -161,10 +174,10 @@ export function unifiedTools() {
161
174
  }
162
175
  }),
163
176
  code_propose: tool({
164
- description: "Plan, propose, and promote changes. Automatically handles coordination pulse and lock checks.",
177
+ description: "Plan, propose, and promote changes. Includes patch generation, surgical test scoping, and PR promotion.",
165
178
  args: {
166
179
  action: tool.schema.enum(["plan", "patch", "validate", "finalize", "promote"]),
167
- symbol: tool.schema.string().optional(),
180
+ symbol: tool.schema.string().optional().describe("Locus symbol for plan"),
168
181
  intent: tool.schema.string().optional(),
169
182
  reasoning: tool.schema.string().optional(),
170
183
  message: tool.schema.string().optional(),
@@ -180,22 +193,19 @@ export function unifiedTools() {
180
193
  return internal.brief_fix_loop.execute({ symbol: args.symbol, intent: args.intent });
181
194
  }
182
195
  case "patch": {
183
- // 1. Check for locks on all changed files
184
- const { stdout: diff } = await internal.runCmd("git diff");
185
196
  const { stdout: files } = await internal.runCmd("git diff --name-only");
186
197
  const changedFiles = files.split('\n').filter(Boolean);
187
198
  for (const file of changedFiles) {
188
199
  const lock = getDb().isLocked(file);
189
200
  if (lock && lock.owner_agent !== agentName) {
190
- return JSON.stringify({ status: "COLLISION_PREVENTED", message: `File ${file} is locked by ${lock.owner_agent}. Use 'code_status' to investigate.` });
201
+ return JSON.stringify({ status: "COLLISION_PREVENTED", message: `File ${file} is locked by ${lock.owner_agent}.` });
191
202
  }
192
203
  }
193
- // 2. Run Policy Engine
204
+ const { stdout: diff } = await internal.runCmd("git diff");
194
205
  const violations = policyEngine.checkDiff(diff);
195
206
  if (violations.some(v => v.severity === "error")) {
196
207
  return JSON.stringify({ status: "POLICY_VIOLATION", violations, message: "Patch rejected by policy engine." }, null, 2);
197
208
  }
198
- // 3. Prepare patch and record intent
199
209
  const res = await internal.prepare_patch.execute({ message: args.message, plan_id: args.plan_id });
200
210
  const json = JSON.parse(res);
201
211
  if (json.status === "SUCCESS") {
@@ -206,8 +216,26 @@ export function unifiedTools() {
206
216
  return res;
207
217
  }
208
218
  case "validate": {
209
- getDb().postToBlackboard(agentName, `Validating patch ${args.patch_path}`, "pulse");
210
- return internal.validate_patch.execute({ patch_path: args.patch_path, plan_id: args.plan_id });
219
+ // 1. Arch Check
220
+ const { stdout: diff } = await internal.runCmd("git diff --name-only");
221
+ const changedFiles = diff.split('\n').filter(Boolean);
222
+ for (const file of changedFiles) {
223
+ const deps = await internal.extractDependencies.execute({ content: "", ast: null, filePath: file });
224
+ const imports = JSON.parse(deps);
225
+ for (const imp of imports) {
226
+ const violation = getDb().checkArchViolation(file, imp);
227
+ if (violation)
228
+ return JSON.stringify({ status: "ARCH_VIOLATION", file, forbidden_import: imp, rule: violation }, null, 2);
229
+ }
230
+ }
231
+ // 2. Surgical Test Scoping
232
+ let focusTests = [];
233
+ if (args.symbol) {
234
+ focusTests = getDb().findAffectedTests(args.symbol);
235
+ }
236
+ getDb().postToBlackboard(agentName, `Validating patch ${args.patch_path}. Scoped tests: ${focusTests.length}`, "pulse");
237
+ // Call validation with scoped tests hint
238
+ return internal.validate_patch.execute({ patch_path: args.patch_path, plan_id: args.plan_id, tests: focusTests });
211
239
  }
212
240
  case "promote": {
213
241
  const branch = args.branch || `autognosis-fix-${Date.now()}`;
@@ -234,10 +262,11 @@ export function unifiedTools() {
234
262
  code_status: tool({
235
263
  description: "Monitor system health, background jobs, Multi-Agent Blackboard, and Resource Locks.",
236
264
  args: {
237
- mode: tool.schema.enum(["stats", "hot_files", "jobs", "plan", "doctor", "blackboard", "locks"]).optional().default("stats"),
238
- action: tool.schema.enum(["post", "read", "lock", "unlock"]).optional(),
265
+ mode: tool.schema.enum(["stats", "hot_files", "jobs", "plan", "doctor", "blackboard", "locks", "dashboard"]).optional().default("stats"),
266
+ action: tool.schema.enum(["post", "read", "lock", "unlock", "archive", "pin"]).optional(),
239
267
  topic: tool.schema.string().optional().default("general"),
240
- target: tool.schema.string().optional().describe("Resource ID (file/symbol) for locks or Symbol ID for graffiti"),
268
+ target: tool.schema.string().optional().describe("Resource ID or Note ID"),
269
+ pinned: tool.schema.boolean().optional().default(false),
241
270
  message: tool.schema.string().optional(),
242
271
  job_id: tool.schema.string().optional(),
243
272
  plan_id: tool.schema.string().optional(),
@@ -245,6 +274,25 @@ export function unifiedTools() {
245
274
  },
246
275
  async execute(args) {
247
276
  switch (args.mode) {
277
+ case "dashboard": {
278
+ const stats = getDb().getStats();
279
+ const locks = getDb().listLocks();
280
+ const jobs = getDb().listJobs();
281
+ const compliance = args.plan_id ? getDb().getPlanMetrics(args.plan_id) : null;
282
+ let dashboard = `# Autognosis TUI Dashboard\n\n`;
283
+ dashboard += `## 📊 System Stats\n- Files: ${stats.files}\n- Chunks: ${stats.chunks}\n- Embedded: ${stats.embeddings.completed}/${stats.chunks}\n\n`;
284
+ dashboard += `## 🔒 Active Locks\n`;
285
+ if (locks.length > 0)
286
+ dashboard += locks.map((l) => `- ${l.resource_id} (${l.owner_agent})`).join('\n') + '\n\n';
287
+ else
288
+ dashboard += "_No active locks._\n\n";
289
+ dashboard += `## ⚙️ Recent Jobs\n`;
290
+ dashboard += jobs.map((j) => `- [${j.status.toUpperCase()}] ${j.type} (${j.progress}%)`).join('\n') + '\n\n';
291
+ if (compliance) {
292
+ dashboard += `## 📉 Plan Compliance (${args.plan_id})\n- Score: ${compliance.compliance}%\n- Total Calls: ${compliance.total}\n- Off-Plan: ${compliance.off_plan}\n`;
293
+ }
294
+ return dashboard;
295
+ }
248
296
  case "locks": {
249
297
  if (args.action === "lock") {
250
298
  getDb().acquireLock(args.target, agentName);
@@ -258,9 +306,17 @@ export function unifiedTools() {
258
306
  }
259
307
  case "blackboard": {
260
308
  if (args.action === "post") {
261
- getDb().postToBlackboard(agentName, args.message, args.topic, args.target);
309
+ getDb().postToBlackboard(agentName, args.message, args.topic, args.target, args.pinned);
262
310
  return JSON.stringify({ status: "SUCCESS", message: "Posted to blackboard." });
263
311
  }
312
+ else if (args.action === "archive") {
313
+ getDb().archiveGraffiti(args.target);
314
+ return JSON.stringify({ status: "SUCCESS", message: `Archived notes for ${args.target}` });
315
+ }
316
+ else if (args.action === "pin") {
317
+ getDb().pinGraffiti(parseInt(args.target, 10), true);
318
+ return JSON.stringify({ status: "SUCCESS", message: `Pinned note ${args.target}` });
319
+ }
264
320
  return JSON.stringify({ status: "SUCCESS", entries: getDb().readBlackboard(args.topic) });
265
321
  }
266
322
  case "hot_files": return internal.journal_query_hot_files.execute({ path_prefix: args.path });
@@ -280,14 +336,14 @@ export function unifiedTools() {
280
336
  }
281
337
  }),
282
338
  code_setup: tool({
283
- description: "Setup and maintenance tasks (AI, Git Journal, Indexing, Prompt Scouting, Arch Boundaries).",
339
+ description: "Setup tasks (AI, Git Journal, Indexing, Prompt Scouting, Arch Boundaries).",
284
340
  args: {
285
341
  action: tool.schema.enum(["init", "ai", "index", "journal", "scout", "arch_rule"]),
286
342
  provider: tool.schema.enum(["ollama", "mlx"]).optional().default("ollama"),
287
343
  model: tool.schema.string().optional(),
288
344
  limit: tool.schema.number().optional(),
289
- source: tool.schema.string().optional().describe("Source target pattern"),
290
- target: tool.schema.string().optional().describe("Target target pattern (forbidden)")
345
+ source: tool.schema.string().optional(),
346
+ target: tool.schema.string().optional()
291
347
  },
292
348
  async execute(args) {
293
349
  switch (args.action) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-autognosis",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Advanced RAG-powered codebase awareness for OpenCode agents. Features Chunk Cards synthesis, hierarchical reasoning, ActiveSet working memory, and performance optimization for enterprise-scale repositories.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",