opencode-autognosis 2.3.1 → 2.5.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.
@@ -12,6 +12,8 @@ export declare class CodeGraphDB {
12
12
  error?: string;
13
13
  }): void;
14
14
  getJob(id: string): any;
15
+ registerContract(triggerTool: string, triggerAction: string, targetTool: string, targetArgs: any): void;
16
+ getContracts(triggerTool: string, triggerAction: string): any[];
15
17
  listJobs(type?: string, limit?: number): any;
16
18
  postToBlackboard(author: string, message: string, topic?: string, symbolId?: string, isPinned?: boolean): void;
17
19
  getGraffiti(symbolId: string, limit?: number): any;
@@ -55,6 +57,7 @@ export declare class CodeGraphDB {
55
57
  };
56
58
  findDependents(filePath: string): string[];
57
59
  searchSymbols(query: string): any[];
60
+ findAffectedTests(symbolName: string): string[];
58
61
  semanticSearch(query: string, limit?: number): Promise<any[]>;
59
62
  private cosineSimilarity;
60
63
  getStats(): {
package/dist/database.js CHANGED
@@ -185,6 +185,15 @@ export class CodeGraphDB {
185
185
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
186
186
  );
187
187
 
188
+ CREATE TABLE IF NOT EXISTS tool_contracts (
189
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
190
+ trigger_tool TEXT, -- e.g., 'code_propose'
191
+ trigger_action TEXT, -- e.g., 'patch'
192
+ target_tool TEXT,
193
+ target_args TEXT, -- JSON
194
+ condition_script TEXT -- Optional JS snippet to evaluate
195
+ );
196
+
188
197
  CREATE INDEX IF NOT EXISTS idx_files_path ON files(path);
189
198
  CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
190
199
  CREATE INDEX IF NOT EXISTS idx_dependencies_target ON dependencies(target_path);
@@ -228,6 +237,18 @@ export class CodeGraphDB {
228
237
  getJob(id) {
229
238
  return this.db.prepare("SELECT * FROM background_jobs WHERE id = ?").get(id);
230
239
  }
240
+ registerContract(triggerTool, triggerAction, targetTool, targetArgs) {
241
+ this.db.prepare(`
242
+ INSERT INTO tool_contracts (trigger_tool, trigger_action, target_tool, target_args)
243
+ VALUES (?, ?, ?, ?)
244
+ `).run(triggerTool, triggerAction, targetTool, JSON.stringify(targetArgs));
245
+ }
246
+ getContracts(triggerTool, triggerAction) {
247
+ return this.db.prepare(`
248
+ SELECT * FROM tool_contracts
249
+ WHERE trigger_tool = ? AND (trigger_action = ? OR trigger_action IS NULL)
250
+ `).all(triggerTool, triggerAction);
251
+ }
231
252
  listJobs(type, limit = 10) {
232
253
  if (type) {
233
254
  return this.db.prepare("SELECT * FROM background_jobs WHERE type = ? ORDER BY created_at DESC LIMIT ?").all(type, limit);
@@ -524,6 +545,29 @@ ${card.content.slice(0, 2000)}`;
524
545
  `);
525
546
  return stmt.all(`%${query}%`);
526
547
  }
548
+ findAffectedTests(symbolName) {
549
+ // Find all files that call this symbol and look like test files
550
+ const query = this.db.prepare(`
551
+ WITH RECURSIVE impact_tree(caller_chunk_id) AS (
552
+ -- Base case: chunks calling the symbol directly
553
+ SELECT caller_chunk_id FROM calls WHERE callee_name = ?
554
+ UNION
555
+ -- Recursive step: chunks calling chunks in the impact tree
556
+ SELECT c.caller_chunk_id
557
+ FROM calls c
558
+ JOIN impact_tree it ON c.callee_name IN (
559
+ SELECT s.name FROM symbols s WHERE s.chunk_id = it.caller_chunk_id
560
+ )
561
+ )
562
+ SELECT DISTINCT f.path
563
+ FROM files f
564
+ JOIN chunks c ON f.id = c.file_id
565
+ JOIN impact_tree it ON c.id = it.caller_chunk_id
566
+ WHERE f.path LIKE '%.test.%' OR f.path LIKE '%Tests.%' OR f.path LIKE 'test_%'
567
+ `);
568
+ const results = query.all(symbolName);
569
+ return results.map(r => r.path);
570
+ }
527
571
  async semanticSearch(query, limit = 10) {
528
572
  if (!(await ollama.isRunning()))
529
573
  throw new Error("Ollama is not running.");
@@ -25,14 +25,14 @@ async function scoutPlugins() {
25
25
  if (config.plugin)
26
26
  config.plugin.forEach((p) => plugins.add(p));
27
27
  }
28
- catch { } // Ignore errors if config file doesn't exist
28
+ catch { }
29
29
  try {
30
30
  const pkg = JSON.parse(fsSync.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf-8"));
31
31
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
32
32
  Object.keys(allDeps).forEach(d => { if (d.includes("opencode"))
33
33
  plugins.add(d); });
34
34
  }
35
- catch { } // Ignore errors if package.json doesn't exist
35
+ catch { }
36
36
  return Array.from(plugins);
37
37
  }
38
38
  async function updateBridgePrompt(plugins) {
@@ -40,14 +40,15 @@ 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.5)
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.
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.
49
+ - code_status: Dashboard, background jobs, blackboard, and resource locks.
50
50
  - code_setup: Environment initialization, AI setup, and Architectural Boundaries.
51
+ - code_contract: Reactive tool chaining and automated post-execution hooks.
51
52
 
52
53
  ## Other Detected Plugins
53
54
  ${plugins.filter(p => p !== "opencode-autognosis").map(p => `- ${p}`).join('\n')}
@@ -62,10 +63,53 @@ ${plugins.filter(p => p !== "opencode-autognosis").map(p => `- ${p}`).join('\n')
62
63
  fsSync.writeFileSync(bridgePath, content);
63
64
  return "Updated bridge.md with consolidated tools and detected plugins.";
64
65
  }
66
+ /**
67
+ * Reactive Contract Runner
68
+ * Automatically triggers secondary tools based on registered contracts.
69
+ */
70
+ async function runWithContracts(toolName, action, args, result, tools) {
71
+ const contracts = getDb().getContracts(toolName, action || '');
72
+ if (contracts.length === 0)
73
+ return result;
74
+ let finalResult = JSON.parse(result);
75
+ finalResult.contracts_triggered = [];
76
+ for (const contract of contracts) {
77
+ try {
78
+ const targetTool = tools[contract.target_tool];
79
+ if (targetTool) {
80
+ const targetArgs = JSON.parse(contract.target_args);
81
+ // Merge context from original args if needed (e.g. plan_id)
82
+ if (args.plan_id)
83
+ targetArgs.plan_id = args.plan_id;
84
+ const chainResult = await targetTool.execute(targetArgs);
85
+ finalResult.contracts_triggered.push({
86
+ name: contract.target_tool,
87
+ result: JSON.parse(chainResult)
88
+ });
89
+ }
90
+ }
91
+ catch (e) {
92
+ finalResult.contracts_triggered.push({ name: contract.target_tool, error: String(e) });
93
+ }
94
+ }
95
+ return JSON.stringify(finalResult, null, 2);
96
+ }
65
97
  export function unifiedTools() {
66
98
  const agentName = process.env.AGENT_NAME || `agent-${process.pid}`;
67
- return {
68
- code_search: tool({
99
+ const api = {};
100
+ const wrap = (toolName, config) => {
101
+ const originalExecute = config.execute;
102
+ config.execute = async (args) => {
103
+ const res = await originalExecute(args);
104
+ // Only attempt chaining if original call was successful (heuristic)
105
+ if (res.includes('"status": "ERROR"') || res.includes('"status": "FAILED"'))
106
+ return res;
107
+ return runWithContracts(toolName, args.action || args.mode, args, res, api);
108
+ };
109
+ return tool(config);
110
+ };
111
+ Object.assign(api, {
112
+ code_search: wrap("code_search", {
69
113
  description: "Search the codebase using various engines (filename, content, symbol, or semantic/vector).",
70
114
  args: {
71
115
  query: tool.schema.string().describe("Search query"),
@@ -83,7 +127,7 @@ export function unifiedTools() {
83
127
  }
84
128
  }
85
129
  }),
86
- code_analyze: tool({
130
+ code_analyze: wrap("code_analyze", {
87
131
  description: "Perform structural analysis on files or modules. Generates summaries, API maps, and impact reports.",
88
132
  args: {
89
133
  target: tool.schema.string().describe("File path or module ID"),
@@ -101,7 +145,7 @@ export function unifiedTools() {
101
145
  }
102
146
  }
103
147
  }),
104
- code_context: tool({
148
+ code_context: wrap("code_context", {
105
149
  description: "Manage working memory (ActiveSets). Limits context window usage by loading/unloading specific chunks.",
106
150
  args: {
107
151
  action: tool.schema.enum(["create", "load", "add", "remove", "status", "list", "close", "evict"]),
@@ -112,10 +156,10 @@ export function unifiedTools() {
112
156
  },
113
157
  async execute(args) {
114
158
  switch (args.action) {
115
- case "create": return internal.activeset_create.execute({ name: args.name || "Context", chunk_ids: args.target?.split(',').map(s => s.trim()) });
159
+ case "create": return internal.activeset_create.execute({ name: args.name || "Context", chunk_ids: args.target?.split(',').map((s) => s.trim()) });
116
160
  case "load": return internal.activeset_load.execute({ set_id: args.target });
117
- case "add": return internal.activeset_add_chunks.execute({ chunk_ids: args.target?.split(',').map(s => s.trim()) });
118
- case "remove": return internal.activeset_remove_chunks.execute({ chunk_ids: args.target?.split(',').map(s => s.trim()) });
161
+ case "add": return internal.activeset_add_chunks.execute({ chunk_ids: args.target?.split(',').map((s) => s.trim()) });
162
+ case "remove": return internal.activeset_remove_chunks.execute({ chunk_ids: args.target?.split(',').map((s) => s.trim()) });
119
163
  case "evict": {
120
164
  const lru = getDb().getLruChunks(args.limit);
121
165
  return internal.activeset_remove_chunks.execute({ chunk_ids: lru.map(c => c.chunk_id) });
@@ -126,7 +170,7 @@ export function unifiedTools() {
126
170
  }
127
171
  }
128
172
  }),
129
- code_read: tool({
173
+ code_read: wrap("code_read", {
130
174
  description: "Precise reading of symbols or file slices. Follows current plan. Checks for locks and returns historical graffiti.",
131
175
  args: {
132
176
  symbol: tool.schema.string().optional().describe("Symbol to jump to"),
@@ -140,9 +184,7 @@ export function unifiedTools() {
140
184
  if (resourceId) {
141
185
  getDb().logAccess(resourceId, args.plan_id);
142
186
  const lock = getDb().isLocked(resourceId);
143
- // Smart History Housekeeping
144
- const graffiti = getDb().getGraffiti(resourceId, 3); // Limit to top 3 recent/pinned notes
145
- // Contextual Verification: Get current hash
187
+ const graffiti = getDb().getGraffiti(resourceId, 3);
146
188
  let currentHash = "";
147
189
  try {
148
190
  const { execSync } = await import("node:child_process");
@@ -175,11 +217,11 @@ export function unifiedTools() {
175
217
  throw new Error("Either 'symbol' or 'file' must be provided.");
176
218
  }
177
219
  }),
178
- code_propose: tool({
220
+ code_propose: wrap("code_propose", {
179
221
  description: "Plan, propose, and promote changes. Automatically handles coordination pulse and lock checks.",
180
222
  args: {
181
223
  action: tool.schema.enum(["plan", "patch", "validate", "finalize", "promote"]),
182
- symbol: tool.schema.string().optional(),
224
+ symbol: tool.schema.string().optional().describe("Locus symbol for plan"),
183
225
  intent: tool.schema.string().optional(),
184
226
  reasoning: tool.schema.string().optional(),
185
227
  message: tool.schema.string().optional(),
@@ -218,8 +260,22 @@ export function unifiedTools() {
218
260
  return res;
219
261
  }
220
262
  case "validate": {
221
- getDb().postToBlackboard(agentName, `Validating patch ${args.patch_path}`, "pulse");
222
- return internal.validate_patch.execute({ patch_path: args.patch_path, plan_id: args.plan_id });
263
+ const { stdout: diff } = await internal.runCmd("git diff --name-only");
264
+ const changedFiles = diff.split('\n').filter(Boolean);
265
+ for (const file of changedFiles) {
266
+ const deps = await internal.extractDependencies.execute({ content: "", ast: null, filePath: file });
267
+ const imports = JSON.parse(deps);
268
+ for (const imp of imports) {
269
+ const violation = getDb().checkArchViolation(file, imp);
270
+ if (violation)
271
+ return JSON.stringify({ status: "ARCH_VIOLATION", file, forbidden_import: imp, rule: violation }, null, 2);
272
+ }
273
+ }
274
+ let focusTests = [];
275
+ if (args.symbol)
276
+ focusTests = getDb().findAffectedTests(args.symbol);
277
+ getDb().postToBlackboard(agentName, `Validating patch ${args.patch_path}. Scoped tests: ${focusTests.length}`, "pulse");
278
+ return internal.validate_patch.execute({ patch_path: args.patch_path, plan_id: args.plan_id, tests: focusTests });
223
279
  }
224
280
  case "promote": {
225
281
  const branch = args.branch || `autognosis-fix-${Date.now()}`;
@@ -243,13 +299,13 @@ export function unifiedTools() {
243
299
  }
244
300
  }
245
301
  }),
246
- code_status: tool({
247
- description: "Monitor system health, Multi-Agent Blackboard, and Resource Locks.",
302
+ code_status: wrap("code_status", {
303
+ description: "Monitor system health, background jobs, Multi-Agent Blackboard, and Resource Locks.",
248
304
  args: {
249
- mode: tool.schema.enum(["stats", "hot_files", "jobs", "plan", "doctor", "blackboard", "locks"]).optional().default("stats"),
305
+ mode: tool.schema.enum(["stats", "hot_files", "jobs", "plan", "doctor", "blackboard", "locks", "dashboard"]).optional().default("stats"),
250
306
  action: tool.schema.enum(["post", "read", "lock", "unlock", "archive", "pin"]).optional(),
251
307
  topic: tool.schema.string().optional().default("general"),
252
- target: tool.schema.string().optional().describe("Resource ID (file/symbol) or Note ID"),
308
+ target: tool.schema.string().optional().describe("Resource ID or Note ID"),
253
309
  pinned: tool.schema.boolean().optional().default(false),
254
310
  message: tool.schema.string().optional(),
255
311
  job_id: tool.schema.string().optional(),
@@ -258,6 +314,25 @@ export function unifiedTools() {
258
314
  },
259
315
  async execute(args) {
260
316
  switch (args.mode) {
317
+ case "dashboard": {
318
+ const stats = getDb().getStats();
319
+ const locks = getDb().listLocks();
320
+ const jobs = getDb().listJobs();
321
+ const compliance = args.plan_id ? getDb().getPlanMetrics(args.plan_id) : null;
322
+ let dashboard = `# Autognosis TUI Dashboard\n\n`;
323
+ dashboard += `## 📊 System Stats\n- Files: ${stats.files}\n- Chunks: ${stats.chunks}\n- Embedded: ${stats.embeddings.completed}/${stats.chunks}\n\n`;
324
+ dashboard += `## 🔒 Active Locks\n`;
325
+ if (locks.length > 0)
326
+ dashboard += locks.map(l => `- ${l.resource_id} (${l.owner_agent})`).join('\n') + '\n\n';
327
+ else
328
+ dashboard += "_No active locks._\n\n";
329
+ dashboard += `## ⚙️ Recent Jobs\n`;
330
+ dashboard += jobs.map(j => `- [${j.status.toUpperCase()}] ${j.type} (${j.progress}%)`).join('\n') + '\n\n';
331
+ if (compliance) {
332
+ dashboard += `## 📉 Plan Compliance (${args.plan_id})\n- Score: ${compliance.compliance}%\n- Total Calls: ${compliance.total}\n- Off-Plan: ${compliance.off_plan}\n`;
333
+ }
334
+ return dashboard;
335
+ }
261
336
  case "locks": {
262
337
  if (args.action === "lock") {
263
338
  getDb().acquireLock(args.target, agentName);
@@ -300,8 +375,8 @@ export function unifiedTools() {
300
375
  }
301
376
  }
302
377
  }),
303
- code_setup: tool({
304
- description: "Setup and maintenance tasks (AI, Git Journal, Indexing, Prompt Scouting, Arch Boundaries).",
378
+ code_setup: wrap("code_setup", {
379
+ description: "Setup tasks (AI, Git Journal, Indexing, Prompt Scouting, Arch Boundaries).",
305
380
  args: {
306
381
  action: tool.schema.enum(["init", "ai", "index", "journal", "scout", "arch_rule"]),
307
382
  provider: tool.schema.enum(["ollama", "mlx"]).optional().default("ollama"),
@@ -327,6 +402,24 @@ export function unifiedTools() {
327
402
  }
328
403
  }
329
404
  }),
405
+ code_contract: wrap("code_contract", {
406
+ description: "Register reactive tool contracts for automated post-execution chaining.",
407
+ args: {
408
+ action: tool.schema.enum(["register", "list", "delete"]),
409
+ trigger_tool: tool.schema.string().optional().describe("Tool that triggers the contract"),
410
+ trigger_action: tool.schema.string().optional().describe("Action that triggers the contract"),
411
+ target_tool: tool.schema.string().optional().describe("Tool to execute automatically"),
412
+ target_args: tool.schema.any().optional().describe("Arguments for the target tool")
413
+ },
414
+ async execute(args) {
415
+ if (args.action === "register") {
416
+ getDb().registerContract(args.trigger_tool, args.trigger_action, args.target_tool, args.target_args);
417
+ return JSON.stringify({ status: "SUCCESS", message: "Contract registered." });
418
+ }
419
+ // List/Delete placeholders
420
+ return JSON.stringify({ status: "SUCCESS", message: "Action completed." });
421
+ }
422
+ }),
330
423
  internal_call: tool({
331
424
  description: "Advanced access to specialized internal tools.",
332
425
  args: { tool_name: tool.schema.string(), args: tool.schema.any() },
@@ -337,5 +430,6 @@ export function unifiedTools() {
337
430
  return target.execute(args);
338
431
  }
339
432
  })
340
- };
433
+ });
434
+ return api;
341
435
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-autognosis",
3
- "version": "2.3.1",
3
+ "version": "2.5.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",