agentf 0.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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/bin/agentf +8 -0
  3. data/lib/agentf/agent_policy.rb +54 -0
  4. data/lib/agentf/agents/architect.rb +67 -0
  5. data/lib/agentf/agents/base.rb +53 -0
  6. data/lib/agentf/agents/debugger.rb +75 -0
  7. data/lib/agentf/agents/designer.rb +69 -0
  8. data/lib/agentf/agents/documenter.rb +58 -0
  9. data/lib/agentf/agents/explorer.rb +65 -0
  10. data/lib/agentf/agents/reviewer.rb +64 -0
  11. data/lib/agentf/agents/security.rb +84 -0
  12. data/lib/agentf/agents/specialist.rb +68 -0
  13. data/lib/agentf/agents/tester.rb +79 -0
  14. data/lib/agentf/agents.rb +19 -0
  15. data/lib/agentf/cli/architecture.rb +83 -0
  16. data/lib/agentf/cli/arg_parser.rb +50 -0
  17. data/lib/agentf/cli/code.rb +165 -0
  18. data/lib/agentf/cli/install.rb +112 -0
  19. data/lib/agentf/cli/memory.rb +393 -0
  20. data/lib/agentf/cli/metrics.rb +103 -0
  21. data/lib/agentf/cli/router.rb +111 -0
  22. data/lib/agentf/cli/update.rb +204 -0
  23. data/lib/agentf/commands/architecture.rb +183 -0
  24. data/lib/agentf/commands/debugger.rb +238 -0
  25. data/lib/agentf/commands/designer.rb +179 -0
  26. data/lib/agentf/commands/explorer.rb +208 -0
  27. data/lib/agentf/commands/memory_reviewer.rb +186 -0
  28. data/lib/agentf/commands/metrics.rb +272 -0
  29. data/lib/agentf/commands/security_scanner.rb +98 -0
  30. data/lib/agentf/commands/tester.rb +232 -0
  31. data/lib/agentf/commands.rb +17 -0
  32. data/lib/agentf/context_builder.rb +35 -0
  33. data/lib/agentf/installer.rb +580 -0
  34. data/lib/agentf/mcp/server.rb +310 -0
  35. data/lib/agentf/memory.rb +530 -0
  36. data/lib/agentf/packs.rb +74 -0
  37. data/lib/agentf/service/providers.rb +158 -0
  38. data/lib/agentf/tools/component_spec.rb +28 -0
  39. data/lib/agentf/tools/error_analysis.rb +19 -0
  40. data/lib/agentf/tools/file_match.rb +21 -0
  41. data/lib/agentf/tools/test_template.rb +17 -0
  42. data/lib/agentf/tools.rb +12 -0
  43. data/lib/agentf/version.rb +5 -0
  44. data/lib/agentf/workflow_contract.rb +158 -0
  45. data/lib/agentf/workflow_engine.rb +424 -0
  46. data/lib/agentf.rb +87 -0
  47. metadata +164 -0
@@ -0,0 +1,580 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "yaml"
5
+
6
+ module Agentf
7
+ class Installer
8
+ READ_ACTIONS = {
9
+ "get_recent_memories" => { cli: "agentf memory recent -n 10", tool: "agentf_memory_recent" },
10
+ "get_pitfalls" => { cli: "agentf memory pitfalls -n 10", tool: "agentf_memory_recent" },
11
+ "get_lessons" => { cli: "agentf memory lessons -n 10", tool: "agentf_memory_recent" },
12
+ "get_successes" => { cli: "agentf memory successes -n 10", tool: "agentf_memory_recent" },
13
+ "get_intents" => { cli: "agentf memory intents", tool: "agentf_memory_recent" },
14
+ "get_all_tags" => { cli: "agentf memory tags", tool: "agentf_memory_recent" },
15
+ "get_by_tag" => { cli: "agentf memory by-tag <tag> -n 10", tool: "agentf_memory_search" },
16
+ "get_by_type" => { cli: "agentf memory by-type <type> -n 10", tool: "agentf_memory_search" },
17
+ "get_by_agent" => { cli: "agentf memory by-agent <agent> -n 10", tool: "agentf_memory_search" },
18
+ "search" => { cli: "agentf memory search \"<query>\" -n 10", tool: "agentf_memory_search" },
19
+ "get_summary" => { cli: "agentf memory summary", tool: "agentf_memory_recent" }
20
+ }.freeze
21
+
22
+ WRITE_ACTIONS = {
23
+ "store_lesson" => { cli: "agentf memory add-lesson \"<title>\" \"<description>\" --agent=<AGENT> --tags=learning", tool: "agentf_memory_add_lesson" },
24
+ "store_success" => { cli: "agentf memory add-success \"<title>\" \"<description>\" --agent=<AGENT> --tags=success", tool: "agentf_memory_add_success" },
25
+ "store_pitfall" => { cli: "agentf memory add-pitfall \"<title>\" \"<description>\" --agent=<AGENT> --tags=pitfall", tool: "agentf_memory_add_pitfall" },
26
+ "store_business_intent" => { cli: "agentf memory add-business-intent \"<title>\" \"<description>\" --tags=strategy", tool: "agentf_memory_add_lesson" },
27
+ "store_feature_intent" => { cli: "agentf memory add-feature-intent \"<title>\" \"<description>\" --acceptance=\"<criteria>\"", tool: "agentf_memory_add_lesson" }
28
+ }.freeze
29
+
30
+ PROVIDER_LAYOUTS = {
31
+ "opencode" => {
32
+ "agents_dir" => ".opencode/agents",
33
+ "commands_dir" => ".opencode/commands",
34
+ "plugin_dir" => ".opencode/plugins",
35
+ "memory_dir" => ".opencode/memory",
36
+ "agent_filename" => ->(klass) { "agentf-#{klass.typed_name.downcase}.md" },
37
+ "command_filename" => ->(manifest) { "agentf-#{manifest.fetch('name')}.md" }
38
+ },
39
+ "copilot" => {
40
+ "agents_dir" => ".github/agents",
41
+ "commands_dir" => ".github/commands",
42
+ "agent_filename" => ->(klass) { "#{klass.typed_name.downcase}.agent.md" },
43
+ "command_filename" => ->(manifest) { "#{manifest.fetch('name')}.md" }
44
+ }
45
+ }.freeze
46
+
47
+ def initialize(global_root: Dir.home, local_root: Dir.pwd, dry_run: false)
48
+ @global_root = global_root
49
+ @local_root = local_root
50
+ @dry_run = dry_run
51
+ end
52
+
53
+ def install(
54
+ providers: ["opencode"],
55
+ scope: "all",
56
+ only_agents: nil,
57
+ only_commands: nil
58
+ )
59
+ providers.flat_map do |provider|
60
+ install_for_provider(
61
+ provider: provider,
62
+ scope: scope,
63
+ only_agents: only_agents,
64
+ only_commands: only_commands
65
+ )
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def install_for_provider(provider:, scope:, only_agents:, only_commands:)
72
+ layout = PROVIDER_LAYOUTS.fetch(provider.to_s) do
73
+ raise ArgumentError, "Unknown provider: #{provider}. Valid: #{PROVIDER_LAYOUTS.keys.join(', ')}"
74
+ end
75
+
76
+ writes = []
77
+ roots_for(scope).each do |root|
78
+ writes.concat(write_agents(root: root, layout: layout, provider: provider, only_agents: only_agents))
79
+ writes.concat(write_commands(root: root, layout: layout, provider: provider, only_commands: only_commands))
80
+ writes.concat(write_opencode_helpers(root: root)) if provider.to_s == "opencode"
81
+ end
82
+ writes
83
+ end
84
+
85
+ def roots_for(scope)
86
+ case scope
87
+ when "global"
88
+ [@global_root]
89
+ when "local"
90
+ [@local_root]
91
+ else
92
+ [@global_root, @local_root]
93
+ end
94
+ end
95
+
96
+ def write_agents(root:, layout:, provider:, only_agents:)
97
+ classes = discover_agents
98
+ classes = classes.select { |klass| only_agents.include?(klass.typed_name.downcase) } if only_agents
99
+
100
+ classes.map do |klass|
101
+ target = File.join(root, layout.fetch("agents_dir"), layout.fetch("agent_filename").call(klass))
102
+ write_manifest(target, render_agent_manifest(klass, provider: provider))
103
+ end
104
+ end
105
+
106
+ def write_commands(root:, layout:, provider:, only_commands:)
107
+ manifests = discover_commands
108
+ manifests = manifests.select { |manifest| only_commands.include?(manifest.fetch("name").downcase) } if only_commands
109
+
110
+ manifests.map do |manifest|
111
+ target = File.join(root, layout.fetch("commands_dir"), layout.fetch("command_filename").call(manifest))
112
+ write_manifest(target, render_command_manifest(manifest, provider: provider))
113
+ end
114
+ end
115
+
116
+ def write_opencode_helpers(root:)
117
+ writes = []
118
+ writes << write_manifest(
119
+ File.join(root, ".opencode/agents/agentf-workflow-engine.md"),
120
+ render_workflow_engine_manifest
121
+ )
122
+ writes << write_manifest(
123
+ File.join(root, ".opencode/plugins/agentf-plugin.ts"),
124
+ render_opencode_plugin
125
+ )
126
+ writes << write_manifest(
127
+ File.join(root, ".opencode/memory/agentf-redis-schema.md"),
128
+ render_opencode_memory_schema
129
+ )
130
+ writes << write_manifest(
131
+ File.join(root, "opencode.json"),
132
+ render_opencode_json
133
+ )
134
+ writes
135
+ end
136
+
137
+ def discover_agents
138
+ Agentf::Agents.constants
139
+ .map { |const| Agentf::Agents.const_get(const) }
140
+ .select { |value| value.is_a?(Class) && value < Agentf::Agents::Base }
141
+ .reject { |klass| klass == Agentf::Agents::Base }
142
+ .sort_by(&:typed_name)
143
+ end
144
+
145
+ def discover_commands
146
+ Agentf::Commands.constants
147
+ .map { |const| Agentf::Commands.const_get(const) }
148
+ .select { |value| value.is_a?(Class) && value.respond_to?(:manifest) }
149
+ .map(&:manifest)
150
+ .sort_by { |manifest| manifest.fetch("name") }
151
+ end
152
+
153
+ def write_manifest(path, payload)
154
+ return { "path" => path, "status" => "planned" } if @dry_run
155
+
156
+ FileUtils.mkdir_p(File.dirname(path))
157
+ File.write(path, payload)
158
+ { "path" => path, "status" => "written" }
159
+ end
160
+
161
+ def render_agent_manifest(klass, provider:)
162
+ meta = {
163
+ "name" => klass.typed_name,
164
+ "description" => klass.description,
165
+ "commands" => klass.commands,
166
+ "memory" => klass.memory_concepts,
167
+ "policy" => klass.policy_boundaries
168
+ }
169
+
170
+ <<~MARKDOWN
171
+ #{meta.to_yaml}---
172
+ #{klass.prompt}
173
+
174
+ ## Memory Integration
175
+ - Reads: #{Array(klass.memory_concepts["reads"]).join(", ")}
176
+ - Writes: #{Array(klass.memory_concepts["writes"]).join(", ")}
177
+ - Policy: #{klass.memory_concepts["policy"]}
178
+
179
+ ## Memory Actions
180
+ #{memory_actions_for(klass, provider: provider).join("\n")}
181
+
182
+ ## Policy Boundaries
183
+ - Always: #{Array(klass.policy_boundaries["always"]).join("; ")}
184
+ - Ask first: #{Array(klass.policy_boundaries["ask_first"]).join("; ")}
185
+ - Never: #{Array(klass.policy_boundaries["never"]).join("; ")}
186
+ - Required inputs: #{Array(klass.policy_boundaries["required_inputs"]).join(", ")}
187
+ - Required outputs: #{Array(klass.policy_boundaries["required_outputs"]).join(", ")}
188
+
189
+ #{copilot_mcp_agent_section(provider: provider)}
190
+ MARKDOWN
191
+ end
192
+
193
+ def memory_actions_for(klass, provider: "opencode")
194
+ reads = Array(klass.memory_concepts["reads"]).map { |item| item.to_s.split("#").last }
195
+ writes = Array(klass.memory_concepts["writes"]).map { |item| item.to_s.split("#").last }
196
+
197
+ actions = []
198
+
199
+ reads.each do |read_action|
200
+ next unless READ_ACTIONS[read_action]
201
+
202
+ action = format_action(READ_ACTIONS[read_action], "Read", provider)
203
+ actions << action if action
204
+ end
205
+
206
+ writes.each do |write_action|
207
+ next unless WRITE_ACTIONS[write_action]
208
+
209
+ action = format_action(WRITE_ACTIONS[write_action], "Write", provider, klass.typed_name)
210
+ actions << action if action
211
+ end
212
+
213
+ if actions.none? { |a| a.start_with?("- Read:") }
214
+ actions << "- Read: Use `agentf_memory_recent` tool"
215
+ end
216
+ if actions.none? { |a| a.start_with?("- Write:") }
217
+ actions << "- Write: Use `agentf_memory_add_lesson` tool"
218
+ end
219
+
220
+ actions
221
+ end
222
+
223
+ def format_action(action_def, type, provider, agent_name = nil)
224
+ case provider
225
+ when "opencode"
226
+ tool_name = action_def[:tool]
227
+ "- #{type}: Use `#{tool_name}` tool"
228
+ when "copilot"
229
+ cli_cmd = action_def[:cli]
230
+ cli_cmd = cli_cmd.gsub("<AGENT>", agent_name) if agent_name
231
+ "- #{type}: `#{cli_cmd}`"
232
+ else
233
+ "- #{type}: `#{action_def[:cli]}`"
234
+ end
235
+ end
236
+
237
+ def render_command_manifest(manifest, provider:)
238
+ commands = Array(manifest.fetch("commands"))
239
+ frontmatter = {
240
+ "name" => manifest.fetch("name"),
241
+ "description" => manifest.fetch("description"),
242
+ "commands" => commands
243
+ }
244
+
245
+ <<~MARKDOWN
246
+ #{frontmatter.to_yaml}---
247
+ # Commands
248
+
249
+ #{commands.map { |command| "- #{command.fetch('name')}" }.join("\n")}
250
+
251
+ #{copilot_mcp_command_section(manifest: manifest, provider: provider)}
252
+ MARKDOWN
253
+ end
254
+
255
+ def copilot_mcp_agent_section(provider:)
256
+ return "" unless provider.to_s == "copilot"
257
+
258
+ <<~MARKDOWN
259
+ ## Copilot MCP Integration
260
+
261
+ Copilot should call the local `agentf` MCP server tools for runtime actions.
262
+
263
+ - Code discovery tools: `code_glob`, `code_grep`, `code_tree`, `code_related_files`
264
+ - Memory read tools: `memory_recent`, `memory_search`
265
+ - Memory write tools (if enabled): `memory_add_lesson`, `memory_add_success`, `memory_add_pitfall`
266
+
267
+ MCP server is started via `agentf mcp-server` and runs locally over stdio.
268
+ MARKDOWN
269
+ end
270
+
271
+ def copilot_mcp_command_section(manifest:, provider:)
272
+ return "" unless provider.to_s == "copilot"
273
+
274
+ command_name = manifest.fetch("name")
275
+ recommended_tools = case command_name
276
+ when "explorer"
277
+ "`code_glob`, `code_grep`, `code_tree`, `code_related_files`"
278
+ when "memory"
279
+ "`memory_recent`, `memory_search`, `memory_add_lesson`, `memory_add_success`, `memory_add_pitfall`"
280
+ else
281
+ "`code_glob`, `code_grep`, `memory_recent`, `memory_search`"
282
+ end
283
+
284
+ <<~MARKDOWN
285
+ ## Copilot MCP Usage
286
+
287
+ For Copilot workflows, invoke these capabilities via the local `agentf` MCP server.
288
+
289
+ Recommended MCP tools for `#{command_name}`: #{recommended_tools}
290
+ MARKDOWN
291
+ end
292
+
293
+ def render_workflow_engine_manifest
294
+ <<~MARKDOWN
295
+ # AGENTF-WORKFLOW-ENGINE Agent
296
+
297
+ ## Role
298
+
299
+ The WORKFLOW_ENGINE coordinates end-to-end workflows by selecting a provider adapter (`opencode` or `copilot`), creating an execution plan, and running agents in sequence.
300
+
301
+ Implemented in `lib/agentf/workflow_engine.rb`.
302
+
303
+ ## Responsibilities
304
+
305
+ 1. Build plan from provider adapter (`Agentf::Service::Providers::OpenCode` or `Agentf::Service::Providers::Copilot`)
306
+ 2. Enrich each agent step with brain context from Redis memory
307
+ 3. Persist feature intent at workflow start
308
+ 4. Persist lessons/pitfalls from each agent execution
309
+ 5. Return full workflow state for manual review and future autonomous control
310
+ 6. Enforce workflow contract stages (`spec`, `plan`, `execute`, `review`, `finalize`) when enabled
311
+
312
+ ## Execution Flow
313
+
314
+ 1. WORKFLOW_ENGINE → Requests provider plan
315
+ 2. WORKFLOW_ENGINE → Captures feature intent in memory
316
+ 3. WORKFLOW_ENGINE → Executes planned agents sequentially
317
+ 4. Each agent → Reads relevant context + writes lessons
318
+ 5. WORKFLOW_ENGINE → Summarizes status and returns results
319
+
320
+ ## Notes
321
+
322
+ - The engine is provider-agnostic at runtime.
323
+ - Agent and tool interfaces are unchanged.
324
+ - Provider adapters own sequencing defaults.
325
+ - Workflow contract defaults to advisory mode and can be disabled with `AGENTF_WORKFLOW_CONTRACT_ENABLED=false`.
326
+ MARKDOWN
327
+ end
328
+
329
+ def render_opencode_plugin
330
+ <<~TYPESCRIPT
331
+ import { execFile } from "node:child_process";
332
+ import { promisify } from "node:util";
333
+ import path from "node:path";
334
+ import { type Plugin, tool } from "@opencode-ai/plugin/tool";
335
+ import fs from "node:fs";
336
+
337
+ const execFileAsync = promisify(execFile);
338
+
339
+ async function resolveAgentfBinary(directory: string): Promise<string> {
340
+ const gemPath = process.env.AGENTF_GEM_PATH;
341
+ if (gemPath) {
342
+ const binaryPath = path.join(gemPath, "bin", "agentf");
343
+ if (fs.existsSync(binaryPath)) {
344
+ return binaryPath;
345
+ }
346
+ }
347
+
348
+ const projectRoot = path.resolve(directory);
349
+ const projectBinary = path.join(projectRoot, "bin", "agentf");
350
+ if (fs.existsSync(projectBinary)) {
351
+ return projectBinary;
352
+ }
353
+
354
+ try {
355
+ const { stdout } = await execFileAsync("command", ["-v", "agentf"], { shell: true });
356
+ const whichPath = stdout.toString().trim();
357
+ if (whichPath && fs.existsSync(whichPath)) {
358
+ return whichPath;
359
+ }
360
+ } catch {
361
+ // command -v failed
362
+ }
363
+
364
+ throw new Error(
365
+ "agentf binary not found. Set AGENTF_GEM_PATH environment variable to the path where agentf gem is installed, " +
366
+ "or ensure bin/agentf exists in your project root. " +
367
+ "Example: AGENTF_GEM_PATH=$(bundle show agentf) opencode run \"your task\""
368
+ );
369
+ }
370
+
371
+ async function runAgentfCli(directory: string, subcommand: string, command: string, args: string[]) {
372
+ const binaryPath = await resolveAgentfBinary(directory);
373
+ const commandArgs = ["exec", "ruby", binaryPath, subcommand, command, ...args, "--json"];
374
+
375
+ const { stdout } = await execFileAsync("bundle", commandArgs, {
376
+ cwd: path.resolve(directory),
377
+ env: process.env,
378
+ maxBuffer: 1024 * 1024 * 5,
379
+ });
380
+
381
+ const text = stdout.toString().trim();
382
+ return text || "{}";
383
+ }
384
+
385
+ export const agentfPlugin: Plugin = async () => {
386
+ return {
387
+ tool: {
388
+ agentf_code_glob: tool({
389
+ description: "Find files using project glob patterns via Agentf code CLI.",
390
+ args: {
391
+ pattern: tool.schema.string().describe("Glob pattern, example: lib/**/*.rb"),
392
+ types: tool.schema.array(tool.schema.string()).optional().describe("Optional file extensions"),
393
+ },
394
+ async execute(args, context) {
395
+ const commandArgs = [];
396
+ if (args.types?.length) {
397
+ commandArgs.push(`--types=${args.types.join(",")}`);
398
+ }
399
+
400
+ return runAgentfCli(context.directory, "code", "glob", [args.pattern, ...commandArgs]);
401
+ },
402
+ }),
403
+ agentf_code_grep: tool({
404
+ description: "Search file contents via Agentf code CLI.",
405
+ args: {
406
+ pattern: tool.schema.string().describe("Regex/text to search"),
407
+ filePattern: tool.schema.string().optional().describe("Optional include pattern"),
408
+ context: tool.schema.number().int().min(0).max(20).optional().describe("Context lines"),
409
+ },
410
+ async execute(args, context) {
411
+ const commandArgs = [];
412
+ if (args.filePattern) commandArgs.push(`--file-pattern=${args.filePattern}`);
413
+ if (Number.isInteger(args.context)) commandArgs.push(`--context=${args.context}`);
414
+
415
+ return runAgentfCli(context.directory, "code", "grep", [args.pattern, ...commandArgs]);
416
+ },
417
+ }),
418
+ agentf_code_tree: tool({
419
+ description: "Get directory tree data via Agentf code CLI.",
420
+ args: {
421
+ depth: tool.schema.number().int().min(1).max(10).optional().describe("Max traversal depth"),
422
+ },
423
+ async execute(args, context) {
424
+ const depth = args.depth ?? 3;
425
+ return runAgentfCli(context.directory, "code", "tree", [`--depth=${depth}`]);
426
+ },
427
+ }),
428
+ agentf_code_related_files: tool({
429
+ description: "Find import and related file hints for a target file.",
430
+ args: {
431
+ targetFile: tool.schema.string().describe("Workspace-relative file path"),
432
+ },
433
+ async execute(args, context) {
434
+ return runAgentfCli(context.directory, "code", "related", [args.targetFile]);
435
+ },
436
+ }),
437
+ agentf_memory_recent: tool({
438
+ description: "Get recent Agentf memories from Redis.",
439
+ args: {
440
+ limit: tool.schema.number().int().min(1).max(100).optional().describe("How many memories to return"),
441
+ },
442
+ async execute(args, context) {
443
+ const limit = args.limit ?? 10;
444
+ return runAgentfCli(context.directory, "memory", "recent", ["-n", String(limit)]);
445
+ },
446
+ }),
447
+ agentf_memory_search: tool({
448
+ description: "Search Agentf memories by keyword.",
449
+ args: {
450
+ query: tool.schema.string().describe("Search query"),
451
+ limit: tool.schema.number().int().min(1).max(100).optional().describe("How many results to return"),
452
+ },
453
+ async execute(args, context) {
454
+ const limit = args.limit ?? 10;
455
+ return runAgentfCli(context.directory, "memory", "search", [args.query, "-n", String(limit)]);
456
+ },
457
+ }),
458
+ agentf_memory_add_lesson: tool({
459
+ description: "Store a lesson memory in Redis.",
460
+ args: {
461
+ title: tool.schema.string(),
462
+ description: tool.schema.string(),
463
+ agent: tool.schema.string().optional(),
464
+ tags: tool.schema.array(tool.schema.string()).optional(),
465
+ context: tool.schema.string().optional(),
466
+ },
467
+ async execute(args, context) {
468
+ const commandArgs = [args.title, args.description];
469
+ if (args.agent) commandArgs.push(`--agent=${args.agent}`);
470
+ if (args.tags?.length) commandArgs.push(`--tags=${args.tags.join(",")}`);
471
+ if (args.context) commandArgs.push(`--context=${args.context}`);
472
+
473
+ return runAgentfCli(context.directory, "memory", "add-lesson", commandArgs);
474
+ },
475
+ }),
476
+ agentf_memory_add_success: tool({
477
+ description: "Store a success memory in Redis.",
478
+ args: {
479
+ title: tool.schema.string(),
480
+ description: tool.schema.string(),
481
+ agent: tool.schema.string().optional(),
482
+ tags: tool.schema.array(tool.schema.string()).optional(),
483
+ context: tool.schema.string().optional(),
484
+ },
485
+ async execute(args, context) {
486
+ const commandArgs = [args.title, args.description];
487
+ if (args.agent) commandArgs.push(`--agent=${args.agent}`);
488
+ if (args.tags?.length) commandArgs.push(`--tags=${args.tags.join(",")}`);
489
+ if (args.context) commandArgs.push(`--context=${args.context}`);
490
+
491
+ return runAgentfCli(context.directory, "memory", "add-success", commandArgs);
492
+ },
493
+ }),
494
+ agentf_memory_add_pitfall: tool({
495
+ description: "Store a pitfall memory in Redis.",
496
+ args: {
497
+ title: tool.schema.string(),
498
+ description: tool.schema.string(),
499
+ agent: tool.schema.string().optional(),
500
+ tags: tool.schema.array(tool.schema.string()).optional(),
501
+ context: tool.schema.string().optional(),
502
+ },
503
+ async execute(args, context) {
504
+ const commandArgs = [args.title, args.description];
505
+ if (args.agent) commandArgs.push(`--agent=${args.agent}`);
506
+ if (args.tags?.length) commandArgs.push(`--tags=${args.tags.join(",")}`);
507
+ if (args.context) commandArgs.push(`--context=${args.context}`);
508
+
509
+ return runAgentfCli(context.directory, "memory", "add-pitfall", commandArgs);
510
+ },
511
+ }),
512
+ },
513
+ };
514
+ };
515
+ TYPESCRIPT
516
+ end
517
+
518
+ def render_opencode_json
519
+ <<~JSON
520
+ {
521
+ "$schema": "https://opencode.ai/config.json",
522
+ "plugin": ["./opencode/plugins/agentf-plugin"]
523
+ }
524
+ JSON
525
+ end
526
+
527
+ def render_opencode_memory_schema
528
+ <<~MARKDOWN
529
+ # Redis Memory Schema
530
+
531
+ ## Overview
532
+ This document defines the memory node structures for Agentf using Redis Stack (RedisJSON + RediSearch).
533
+
534
+ ## Memory Types
535
+
536
+ ### 1. Semantic Memory (`semantic:*`)
537
+ Used for finding similar past tasks by embedding similarity.
538
+
539
+ **Schema**:
540
+ - `id`: string
541
+ - `content`: text
542
+ - `embedding`: vector payload
543
+ - `project`: tag
544
+ - `language`: tag
545
+ - `task_type`: tag
546
+ - `success`: boolean
547
+ - `created_at`: numeric timestamp
548
+ - `agent`: string
549
+
550
+ ### 2. Episodic Memory (`episodic:*`)
551
+ Used for success, pitfall, lesson, and intent records.
552
+
553
+ **Search index**: `episodic:logs`
554
+
555
+ **Schema fields**:
556
+ - `$.id`
557
+ - `$.type`
558
+ - `$.title`
559
+ - `$.description`
560
+ - `$.project`
561
+ - `$.context`
562
+ - `$.code_snippet`
563
+ - `$.tags`
564
+ - `$.created_at`
565
+ - `$.agent`
566
+ - `$.related_task_id`
567
+ - `$.metadata.intent_kind`
568
+ - `$.metadata.priority`
569
+
570
+ ## Memory Commands
571
+
572
+ - Read recent: `agentf memory recent -n 10`
573
+ - Search: `agentf memory search "query" -n 10`
574
+ - Add lesson: `agentf memory add-lesson "<title>" "<description>" --agent=<AGENT>`
575
+ - Add success: `agentf memory add-success "<title>" "<description>" --agent=<AGENT>`
576
+ - Add pitfall: `agentf memory add-pitfall "<title>" "<description>" --agent=<AGENT>`
577
+ MARKDOWN
578
+ end
579
+ end
580
+ end