opencode-autognosis 2.0.5 → 2.3.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.
@@ -6,7 +6,8 @@ import { activeSetTools } from "./activeset.js";
6
6
  import { chunkCardsTools } from "./chunk-cards.js";
7
7
  import { moduleSummariesTools } from "./module-summaries.js";
8
8
  import { performanceTools } from "./performance-optimization.js";
9
- import { graphTools } from "./database.js";
9
+ import { graphTools, getDb } from "./database.js";
10
+ import { policyEngine } from "./services/policy.js";
10
11
  const PROJECT_ROOT = process.cwd();
11
12
  // Aggregate all internal tools
12
13
  const internal = {
@@ -19,23 +20,19 @@ const internal = {
19
20
  };
20
21
  async function scoutPlugins() {
21
22
  const plugins = new Set();
22
- // 1. Check opencode.jsonc
23
23
  try {
24
24
  const config = JSON.parse(fsSync.readFileSync(path.join(PROJECT_ROOT, "opencode.jsonc"), "utf-8"));
25
25
  if (config.plugin)
26
26
  config.plugin.forEach((p) => plugins.add(p));
27
27
  }
28
- catch { }
29
- // 2. Check package.json dependencies
28
+ catch { } // Ignore errors if config file doesn't exist
30
29
  try {
31
30
  const pkg = JSON.parse(fsSync.readFileSync(path.join(PROJECT_ROOT, "package.json"), "utf-8"));
32
31
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
33
- Object.keys(allDeps).forEach(d => {
34
- if (d.includes("opencode"))
35
- plugins.add(d);
36
- });
32
+ Object.keys(allDeps).forEach(d => { if (d.includes("opencode"))
33
+ plugins.add(d); });
37
34
  }
38
- catch { }
35
+ catch { } // Ignore errors if package.json doesn't exist
39
36
  return Array.from(plugins);
40
37
  }
41
38
  async function updateBridgePrompt(plugins) {
@@ -43,20 +40,19 @@ async function updateBridgePrompt(plugins) {
43
40
  if (!fsSync.existsSync(bridgePath))
44
41
  return "bridge.md not found at " + bridgePath;
45
42
  const toolsSection = `
46
- ## Current Consolidated Tools (Autognosis v2)
43
+ ## Current Consolidated Tools (Autognosis v2.3)
47
44
  - code_search: Universal search (semantic, symbol, filename, content).
48
45
  - code_analyze: Deep structural analysis and impact reports.
49
- - code_context: Working memory (ActiveSet) management.
50
- - code_read: Precise symbol jumping and file slicing.
51
- - code_propose: Planning and patch generation.
52
- - code_status: System health and background job monitoring.
53
- - code_setup: Environment initialization and maintenance.
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.
54
51
 
55
52
  ## Other Detected Plugins
56
53
  ${plugins.filter(p => p !== "opencode-autognosis").map(p => `- ${p}`).join('\n')}
57
54
  `;
58
55
  let content = fsSync.readFileSync(bridgePath, "utf-8");
59
- // Replace or Append Tool Usage section
60
56
  if (content.includes("## Current Consolidated Tools")) {
61
57
  content = content.replace(/## Current Consolidated Tools[\s\S]*?(?=\n#|$)/, toolsSection);
62
58
  }
@@ -67,14 +63,15 @@ ${plugins.filter(p => p !== "opencode-autognosis").map(p => `- ${p}`).join('\n')
67
63
  return "Updated bridge.md with consolidated tools and detected plugins.";
68
64
  }
69
65
  export function unifiedTools() {
66
+ const agentName = process.env.AGENT_NAME || `agent-${process.pid}`;
70
67
  return {
71
68
  code_search: tool({
72
69
  description: "Search the codebase using various engines (filename, content, symbol, or semantic/vector).",
73
70
  args: {
74
71
  query: tool.schema.string().describe("Search query"),
75
- mode: tool.schema.enum(["filename", "content", "symbol", "semantic"]).optional().default("filename").describe("Search strategy"),
76
- path: tool.schema.string().optional().default(".").describe("Root path for search"),
77
- limit: tool.schema.number().optional().default(10).describe("Max results"),
72
+ mode: tool.schema.enum(["filename", "content", "symbol", "semantic"]).optional().default("filename"),
73
+ path: tool.schema.string().optional().default("."),
74
+ limit: tool.schema.number().optional().default(10),
78
75
  plan_id: tool.schema.string().optional()
79
76
  },
80
77
  async execute(args) {
@@ -90,7 +87,7 @@ export function unifiedTools() {
90
87
  description: "Perform structural analysis on files or modules. Generates summaries, API maps, and impact reports.",
91
88
  args: {
92
89
  target: tool.schema.string().describe("File path or module ID"),
93
- mode: tool.schema.enum(["summary", "api", "invariant", "module", "impact", "reasoning"]).optional().default("summary"),
90
+ mode: tool.schema.enum(["summary", "api", "invariant", "module", "impact", "reasoning", "callers"]).optional().default("summary"),
94
91
  force: tool.schema.boolean().optional().default(false),
95
92
  plan_id: tool.schema.string().optional()
96
93
  },
@@ -99,6 +96,7 @@ export function unifiedTools() {
99
96
  case "module": return internal.module_synthesize.execute({ file_path: args.target, force_resynthesize: args.force });
100
97
  case "impact": return internal.brief_fix_loop.execute({ symbol: args.target, intent: "impact_analysis" });
101
98
  case "reasoning": return internal.module_hierarchical_reasoning.execute({ module_id: args.target });
99
+ case "callers": return internal.graph_search_symbols.execute({ query: args.target });
102
100
  default: return internal.chunk_create_card.execute({ file_path: args.target, chunk_type: args.mode, force_recreate: args.force });
103
101
  }
104
102
  }
@@ -106,18 +104,22 @@ export function unifiedTools() {
106
104
  code_context: tool({
107
105
  description: "Manage working memory (ActiveSets). Limits context window usage by loading/unloading specific chunks.",
108
106
  args: {
109
- action: tool.schema.enum(["create", "load", "add", "remove", "status", "list", "close"]),
110
- target: tool.schema.string().optional().describe("ActiveSet ID or Chunk IDs (comma separated)"),
111
- name: tool.schema.string().optional().describe("Name for new ActiveSet"),
107
+ action: tool.schema.enum(["create", "load", "add", "remove", "status", "list", "close", "evict"]),
108
+ target: tool.schema.string().optional().describe("ActiveSet ID or Chunk IDs"),
109
+ name: tool.schema.string().optional(),
110
+ limit: tool.schema.number().optional().default(5),
112
111
  plan_id: tool.schema.string().optional()
113
112
  },
114
113
  async execute(args) {
115
- const chunk_ids = args.target?.split(',').map(s => s.trim());
116
114
  switch (args.action) {
117
- case "create": return internal.activeset_create.execute({ name: args.name || "Context", chunk_ids });
115
+ case "create": return internal.activeset_create.execute({ name: args.name || "Context", chunk_ids: args.target?.split(',').map(s => s.trim()) });
118
116
  case "load": return internal.activeset_load.execute({ set_id: args.target });
119
- case "add": return internal.activeset_add_chunks.execute({ chunk_ids: chunk_ids });
120
- case "remove": return internal.activeset_remove_chunks.execute({ chunk_ids: chunk_ids });
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()) });
119
+ case "evict": {
120
+ const lru = getDb().getLruChunks(args.limit);
121
+ return internal.activeset_remove_chunks.execute({ chunk_ids: lru.map(c => c.chunk_id) });
122
+ }
121
123
  case "list": return internal.activeset_list.execute({});
122
124
  case "close": return internal.activeset_close.execute({});
123
125
  default: return internal.activeset_get_current.execute({});
@@ -125,7 +127,7 @@ export function unifiedTools() {
125
127
  }
126
128
  }),
127
129
  code_read: tool({
128
- description: "Precise reading of symbols or file slices. Follows the current plan.",
130
+ description: "Precise reading of symbols or file slices. Follows current plan. Checks for locks and returns graffiti.",
129
131
  args: {
130
132
  symbol: tool.schema.string().optional().describe("Symbol to jump to"),
131
133
  file: tool.schema.string().optional().describe("File path to read"),
@@ -134,61 +136,166 @@ export function unifiedTools() {
134
136
  plan_id: tool.schema.string().optional()
135
137
  },
136
138
  async execute(args) {
137
- if (args.symbol)
138
- return internal.jump_to_symbol.execute({ symbol: args.symbol, plan_id: args.plan_id });
139
- if (args.file && args.start_line && args.end_line) {
140
- return internal.read_slice.execute({ file: args.file, start_line: args.start_line, end_line: args.end_line, plan_id: args.plan_id });
139
+ const resourceId = args.symbol || args.file;
140
+ if (resourceId) {
141
+ getDb().logAccess(resourceId, args.plan_id);
142
+ const lock = getDb().isLocked(resourceId);
143
+ const graffiti = getDb().getGraffiti(resourceId);
144
+ let result;
145
+ if (args.symbol) {
146
+ result = await internal.jump_to_symbol.execute({ symbol: args.symbol, plan_id: args.plan_id });
147
+ }
148
+ else {
149
+ result = await internal.read_slice.execute({ file: args.file, start_line: args.start_line, end_line: args.end_line, plan_id: args.plan_id });
150
+ }
151
+ const parsed = JSON.parse(result);
152
+ return JSON.stringify({
153
+ ...parsed,
154
+ coordination: {
155
+ lock_status: lock ? `LOCKED by ${lock.owner_agent}` : "FREE",
156
+ graffiti: graffiti.length > 0 ? graffiti : undefined
157
+ }
158
+ }, null, 2);
141
159
  }
142
- throw new Error("Either 'symbol' or 'file' with line range must be provided.");
160
+ throw new Error("Either 'symbol' or 'file' must be provided.");
143
161
  }
144
162
  }),
145
163
  code_propose: tool({
146
- description: "Plan and propose changes. Generates worklists, diffs, and validates them.",
164
+ description: "Plan, propose, and promote changes. Automatically handles coordination pulse and lock checks.",
147
165
  args: {
148
- action: tool.schema.enum(["plan", "patch", "validate", "finalize"]),
149
- symbol: tool.schema.string().optional().describe("Locus symbol for plan"),
150
- intent: tool.schema.string().optional().describe("Work intent (e.g. refactor)"),
151
- message: tool.schema.string().optional().describe("Commit message for patch"),
152
- patch_path: tool.schema.string().optional().describe("Path to .diff file"),
166
+ action: tool.schema.enum(["plan", "patch", "validate", "finalize", "promote"]),
167
+ symbol: tool.schema.string().optional(),
168
+ intent: tool.schema.string().optional(),
169
+ reasoning: tool.schema.string().optional(),
170
+ message: tool.schema.string().optional(),
171
+ patch_path: tool.schema.string().optional(),
172
+ branch: tool.schema.string().optional(),
153
173
  plan_id: tool.schema.string().optional(),
154
174
  outcome: tool.schema.string().optional()
155
175
  },
156
176
  async execute(args) {
157
177
  switch (args.action) {
158
- case "plan": return internal.brief_fix_loop.execute({ symbol: args.symbol, intent: args.intent });
159
- case "patch": return internal.prepare_patch.execute({ message: args.message, plan_id: args.plan_id });
160
- case "validate": return internal.validate_patch.execute({ patch_path: args.patch_path, plan_id: args.plan_id });
161
- case "finalize": return internal.finalize_plan.execute({ plan_id: args.plan_id, outcome: args.outcome });
178
+ case "plan": {
179
+ getDb().postToBlackboard(agentName, `Planning ${args.intent} for ${args.symbol}`, "pulse");
180
+ return internal.brief_fix_loop.execute({ symbol: args.symbol, intent: args.intent });
181
+ }
182
+ case "patch": {
183
+ // 1. Check for locks on all changed files
184
+ const { stdout: diff } = await internal.runCmd("git diff");
185
+ const { stdout: files } = await internal.runCmd("git diff --name-only");
186
+ const changedFiles = files.split('\n').filter(Boolean);
187
+ for (const file of changedFiles) {
188
+ const lock = getDb().isLocked(file);
189
+ 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.` });
191
+ }
192
+ }
193
+ // 2. Run Policy Engine
194
+ const violations = policyEngine.checkDiff(diff);
195
+ if (violations.some(v => v.severity === "error")) {
196
+ return JSON.stringify({ status: "POLICY_VIOLATION", violations, message: "Patch rejected by policy engine." }, null, 2);
197
+ }
198
+ // 3. Prepare patch and record intent
199
+ const res = await internal.prepare_patch.execute({ message: args.message, plan_id: args.plan_id });
200
+ const json = JSON.parse(res);
201
+ if (json.status === "SUCCESS") {
202
+ if (args.reasoning)
203
+ getDb().storeIntent(json.patch_id, args.reasoning, args.plan_id || "adhoc");
204
+ getDb().postToBlackboard(agentName, `Proposed patch ${json.patch_id}: ${args.message}`, "pulse");
205
+ }
206
+ return res;
207
+ }
208
+ 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 });
211
+ }
212
+ case "promote": {
213
+ const branch = args.branch || `autognosis-fix-${Date.now()}`;
214
+ const { execSync } = await import("node:child_process");
215
+ try {
216
+ execSync(`git checkout -b ${branch}`);
217
+ execSync(`git apply ${args.patch_path}`);
218
+ execSync(`git add . && git commit -m "${args.message || 'Automated promotion'}"`);
219
+ execSync(`gh pr create --title "${args.message}" --body "Automated promotion from Autognosis v2."`);
220
+ getDb().postToBlackboard(agentName, `Promoted patch to PR on branch ${branch}`, "pulse");
221
+ return JSON.stringify({ status: "SUCCESS", promoted_to: branch, pr: "OPENED" }, null, 2);
222
+ }
223
+ catch (e) {
224
+ return JSON.stringify({ status: "ERROR", message: e.message }, null, 2);
225
+ }
226
+ }
227
+ case "finalize": {
228
+ getDb().postToBlackboard(agentName, `Finalized plan ${args.plan_id} with outcome: ${args.outcome}`, "pulse");
229
+ return internal.finalize_plan.execute({ plan_id: args.plan_id, outcome: args.outcome });
230
+ }
162
231
  }
163
232
  }
164
233
  }),
165
234
  code_status: tool({
166
- description: "Monitor system health, background jobs, and plan metrics.",
235
+ description: "Monitor system health, background jobs, Multi-Agent Blackboard, and Resource Locks.",
167
236
  args: {
168
- mode: tool.schema.enum(["stats", "hot_files", "jobs", "plan"]).optional().default("stats"),
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(),
239
+ 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"),
241
+ message: tool.schema.string().optional(),
169
242
  job_id: tool.schema.string().optional(),
170
243
  plan_id: tool.schema.string().optional(),
171
244
  path: tool.schema.string().optional().default("")
172
245
  },
173
246
  async execute(args) {
174
247
  switch (args.mode) {
248
+ case "locks": {
249
+ if (args.action === "lock") {
250
+ getDb().acquireLock(args.target, agentName);
251
+ return JSON.stringify({ status: "SUCCESS", message: `Locked ${args.target}` });
252
+ }
253
+ else if (args.action === "unlock") {
254
+ getDb().releaseLock(args.target, agentName);
255
+ return JSON.stringify({ status: "SUCCESS", message: `Unlocked ${args.target}` });
256
+ }
257
+ return JSON.stringify({ status: "SUCCESS", active_locks: getDb().listLocks() });
258
+ }
259
+ case "blackboard": {
260
+ if (args.action === "post") {
261
+ getDb().postToBlackboard(agentName, args.message, args.topic, args.target);
262
+ return JSON.stringify({ status: "SUCCESS", message: "Posted to blackboard." });
263
+ }
264
+ return JSON.stringify({ status: "SUCCESS", entries: getDb().readBlackboard(args.topic) });
265
+ }
175
266
  case "hot_files": return internal.journal_query_hot_files.execute({ path_prefix: args.path });
176
267
  case "jobs": return internal.graph_background_status.execute({ job_id: args.job_id });
177
268
  case "plan": return internal.graph_get_plan_metrics.execute({ plan_id: args.plan_id });
269
+ case "doctor": {
270
+ const stats = getDb().getStats();
271
+ let logSnippet = "";
272
+ try {
273
+ logSnippet = fsSync.readFileSync(path.join(PROJECT_ROOT, ".opencode", "logs", "autognosis.log"), "utf-8").split('\n').slice(-20).join('\n');
274
+ }
275
+ catch (e) { }
276
+ return JSON.stringify({ status: "HEALTHY", stats, recent_logs: logSnippet }, null, 2);
277
+ }
178
278
  default: return internal.graph_stats.execute({});
179
279
  }
180
280
  }
181
281
  }),
182
282
  code_setup: tool({
183
- description: "One-time setup and maintenance tasks (AI, Git Journal, Indexing, Prompt Scouting).",
283
+ description: "Setup and maintenance tasks (AI, Git Journal, Indexing, Prompt Scouting, Arch Boundaries).",
184
284
  args: {
185
- action: tool.schema.enum(["init", "ai", "index", "journal", "scout"]),
186
- model: tool.schema.string().optional().describe("AI Model name"),
187
- limit: tool.schema.number().optional().describe("History limit")
285
+ action: tool.schema.enum(["init", "ai", "index", "journal", "scout", "arch_rule"]),
286
+ provider: tool.schema.enum(["ollama", "mlx"]).optional().default("ollama"),
287
+ model: tool.schema.string().optional(),
288
+ 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)")
188
291
  },
189
292
  async execute(args) {
190
293
  switch (args.action) {
191
- case "ai": return internal.autognosis_setup_ai.execute({ model: args.model });
294
+ case "arch_rule": {
295
+ getDb().addArchRule(args.source, args.target);
296
+ return JSON.stringify({ status: "SUCCESS", message: `Architecture rule added: ${args.source} cannot import ${args.target}` });
297
+ }
298
+ case "ai": return internal.autognosis_setup_ai.execute({ provider: args.provider, model: args.model });
192
299
  case "index": return internal.perf_incremental_index.execute({ background: true });
193
300
  case "journal": return internal.journal_build.execute({ limit: args.limit });
194
301
  case "scout": {
@@ -200,11 +307,8 @@ export function unifiedTools() {
200
307
  }
201
308
  }),
202
309
  internal_call: tool({
203
- description: "Advanced access to specialized internal tools. Use only when unified tools are insufficient.",
204
- args: {
205
- tool_name: tool.schema.string().describe("Internal tool name"),
206
- args: tool.schema.any().describe("Arguments for the internal tool")
207
- },
310
+ description: "Advanced access to specialized internal tools.",
311
+ args: { tool_name: tool.schema.string(), args: tool.schema.any() },
208
312
  async execute({ tool_name, args }) {
209
313
  const target = internal[tool_name];
210
314
  if (!target)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-autognosis",
3
- "version": "2.0.5",
3
+ "version": "2.3.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",
@@ -48,10 +48,12 @@
48
48
  "@opencode-ai/sdk": "^1.1.40",
49
49
  "@types/better-sqlite3": "^7.6.13",
50
50
  "@types/node": "^20.0.0",
51
+ "bun-types": "^1.3.8",
51
52
  "typescript": "^5.0.0",
52
53
  "zod": "^4.3.6"
53
54
  },
54
55
  "dependencies": {
55
- "better-sqlite3": "^12.6.2"
56
+ "better-sqlite3": "^12.6.2",
57
+ "chokidar": "^5.0.0"
56
58
  }
57
59
  }