maf 0.1.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 (136) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +411 -0
  5. data/assets/agents-contract.md +80 -0
  6. data/assets/analyst +240 -0
  7. data/assets/coord +2936 -0
  8. data/assets/dashboard +553 -0
  9. data/assets/dashboard.html +341 -0
  10. data/assets/dispatcher +1687 -0
  11. data/assets/doc-graph-refresh +286 -0
  12. data/assets/env.sh +6 -0
  13. data/assets/git-hooks/post-commit +7 -0
  14. data/assets/git-hooks/post-merge +7 -0
  15. data/assets/git-hooks/pre-commit +32 -0
  16. data/assets/harness-hooks/board-watch-opencode.js +87 -0
  17. data/assets/harness-hooks/board-watch.rb +286 -0
  18. data/assets/harness-hooks/context-watch.rb +268 -0
  19. data/assets/harness-hooks/next-task-hermes.sh +48 -0
  20. data/assets/harness-hooks/next-task.rb +97 -0
  21. data/assets/harness-hooks/session-guard.rb +128 -0
  22. data/assets/taskrc.append +11 -0
  23. data/assets/vault +224 -0
  24. data/assets/worktree-env.example.rb +26 -0
  25. data/exe/maf +14 -0
  26. data/install.md +326 -0
  27. data/lib/maf/bootstrap/claude_settings.rb +55 -0
  28. data/lib/maf/bootstrap/dependencies.rb +37 -0
  29. data/lib/maf/bootstrap/git_hook_planner.rb +68 -0
  30. data/lib/maf/bootstrap/global_taskrc_warning.rb +33 -0
  31. data/lib/maf/bootstrap/graph_home.rb +62 -0
  32. data/lib/maf/bootstrap/hook_merger.rb +53 -0
  33. data/lib/maf/bootstrap/installer.rb +66 -0
  34. data/lib/maf/bootstrap/layout_planner.rb +18 -0
  35. data/lib/maf/bootstrap/marked_block.rb +44 -0
  36. data/lib/maf/bootstrap/memory_branch.rb +77 -0
  37. data/lib/maf/bootstrap/options.rb +34 -0
  38. data/lib/maf/bootstrap/project.rb +77 -0
  39. data/lib/maf/bootstrap/script_planner.rb +81 -0
  40. data/lib/maf/bootstrap/text_planner.rb +42 -0
  41. data/lib/maf/bootstrap/vault_starter.rb +41 -0
  42. data/lib/maf/bootstrap/writer.rb +69 -0
  43. data/lib/maf/bootstrap.rb +162 -0
  44. data/lib/maf/budget.rb +59 -0
  45. data/lib/maf/cli.rb +135 -0
  46. data/lib/maf/env_exclude.rb +23 -0
  47. data/lib/maf/flow/agent_links.rb +79 -0
  48. data/lib/maf/flow/bootstrapper.rb +36 -0
  49. data/lib/maf/flow/codex_hooks.rb +50 -0
  50. data/lib/maf/flow/generator.rb +63 -0
  51. data/lib/maf/flow/harness_linker.rb +37 -0
  52. data/lib/maf/flow/hermes_hook.rb +48 -0
  53. data/lib/maf/flow/hermes_hook_setup.rb +69 -0
  54. data/lib/maf/flow/hook_files.rb +16 -0
  55. data/lib/maf/flow/hook_installer.rb +33 -0
  56. data/lib/maf/flow/legacy_codex_hook.rb +71 -0
  57. data/lib/maf/flow/manifest.rb +51 -0
  58. data/lib/maf/flow/mcp_config.rb +72 -0
  59. data/lib/maf/flow/mcp_installer.rb +45 -0
  60. data/lib/maf/flow/models.rb +61 -0
  61. data/lib/maf/flow/options.rb +65 -0
  62. data/lib/maf/flow/prompt_builder.rb +85 -0
  63. data/lib/maf/flow/prompt_text.rb +263 -0
  64. data/lib/maf/flow/report.rb +89 -0
  65. data/lib/maf/flow/role_catalog.rb +40 -0
  66. data/lib/maf/flow/role_files.rb +72 -0
  67. data/lib/maf/flow/role_stub.rb +38 -0
  68. data/lib/maf/flow/roster.rb +28 -0
  69. data/lib/maf/flow/validator.rb +38 -0
  70. data/lib/maf/flow/workflow.rb +28 -0
  71. data/lib/maf/flow.rb +84 -0
  72. data/lib/maf/local_exclude.rb +53 -0
  73. data/lib/maf/menu.rb +101 -0
  74. data/lib/maf/migrate/moves.rb +44 -0
  75. data/lib/maf/migrate/rewrites.rb +53 -0
  76. data/lib/maf/migrate/role_files.rb +35 -0
  77. data/lib/maf/migrate/runner.rb +66 -0
  78. data/lib/maf/migrate/worktrees.rb +65 -0
  79. data/lib/maf/migrate.rb +62 -0
  80. data/lib/maf/prompt.rb +40 -0
  81. data/lib/maf/retire.rb +116 -0
  82. data/lib/maf/role_limits.rb +49 -0
  83. data/lib/maf/setup_agent/args.rb +57 -0
  84. data/lib/maf/setup_agent/dispatch.rb +44 -0
  85. data/lib/maf/setup_agent/hermes_launcher.rb +34 -0
  86. data/lib/maf/setup_agent/hermes_skill.rb +26 -0
  87. data/lib/maf/setup_agent/launcher.rb +85 -0
  88. data/lib/maf/setup_agent/manifest.rb +35 -0
  89. data/lib/maf/setup_agent/project.rb +9 -0
  90. data/lib/maf/setup_agent/role_file.rb +30 -0
  91. data/lib/maf/setup_agent/runtime_hooks.rb +37 -0
  92. data/lib/maf/setup_agent/worktree.rb +50 -0
  93. data/lib/maf/setup_agent.rb +111 -0
  94. data/lib/maf/shared/git_exclude.rb +33 -0
  95. data/lib/maf/shared/git_identity.rb +41 -0
  96. data/lib/maf/shared/peak_rate.rb +20 -0
  97. data/lib/maf/shared/processes.rb +31 -0
  98. data/lib/maf/shared/project.rb +34 -0
  99. data/lib/maf/shared/roles.rb +19 -0
  100. data/lib/maf/team.rb +114 -0
  101. data/lib/maf/team_command.rb +73 -0
  102. data/lib/maf/uninstall/claude_settings.rb +40 -0
  103. data/lib/maf/uninstall/codex_hooks.rb +18 -0
  104. data/lib/maf/uninstall/commit_guard.rb +16 -0
  105. data/lib/maf/uninstall/coordination.rb +15 -0
  106. data/lib/maf/uninstall/doc_graph_hooks.rb +38 -0
  107. data/lib/maf/uninstall/git.rb +13 -0
  108. data/lib/maf/uninstall/local_files.rb +33 -0
  109. data/lib/maf/uninstall/manifest.rb +29 -0
  110. data/lib/maf/uninstall/marked_files.rb +37 -0
  111. data/lib/maf/uninstall/mcp_entries.rb +43 -0
  112. data/lib/maf/uninstall/notes.rb +31 -0
  113. data/lib/maf/uninstall/owned.rb +12 -0
  114. data/lib/maf/uninstall/role_files.rb +51 -0
  115. data/lib/maf/uninstall/runner.rb +67 -0
  116. data/lib/maf/uninstall/scripts.rb +35 -0
  117. data/lib/maf/uninstall/vault_watcher.rb +21 -0
  118. data/lib/maf/uninstall/worktrees.rb +30 -0
  119. data/lib/maf/uninstall.rb +59 -0
  120. data/lib/maf/untrack.rb +90 -0
  121. data/lib/maf/version.rb +5 -0
  122. data/lib/maf/worker_archive.rb +63 -0
  123. data/lib/maf/worker_control.rb +137 -0
  124. data/lib/maf/workers.rb +37 -0
  125. data/lib/maf.rb +5 -0
  126. data/templates/claude.md.erb +16 -0
  127. data/templates/codex.md.erb +7 -0
  128. data/templates/hermes.md.erb +12 -0
  129. data/templates/opencode.md.erb +24 -0
  130. data/templates/role-stub.yml.erb +15 -0
  131. data/templates/roles.yml +289 -0
  132. data/templates/workflows/panel.md +20 -0
  133. data/templates/workflows/plan-review.md +9 -0
  134. data/templates/workflows/simple.md +4 -0
  135. data/templates/workflows/tdd.md +8 -0
  136. metadata +193 -0
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ # session-guard.rb - authorize hooks for a registered MAF session.
4
+ require "json"
5
+ require "fileutils"
6
+ require "securerandom"
7
+
8
+ module MafSession
9
+ def self.register(root, harness, env = ENV)
10
+ token = SecureRandom.hex(32)
11
+ record = registration(root, harness, env)
12
+ save(record, token)
13
+ env.merge!("MAF_SESSION_TOKEN" => token, "COORD_SESSION_PID" => Process.pid.to_s)
14
+ end
15
+
16
+ def self.registration(root, harness, env)
17
+ { "root" => File.realpath(root), "dir" => File.realpath(Dir.pwd), "pid" => Process.pid,
18
+ "harness" => harness, "role" => env.fetch("COORD_ROLE"), "worker" => env.fetch("COORD_WORKER") }
19
+ end
20
+
21
+ def self.path(root, token)
22
+ File.join(root, ".maf", "coordination", "sessions", "#{token}.maf.json")
23
+ end
24
+
25
+ def self.save(record, token)
26
+ path = path(record.fetch("root"), token)
27
+ FileUtils.mkdir_p(File.dirname(path))
28
+ File.write(path, JSON.generate(record), mode: "w", perm: 0o600)
29
+ end
30
+
31
+ # Process ownership prevents sibling sessions from using inherited tokens.
32
+ module ProcessOwner
33
+ def self.ancestor?(owner)
34
+ return true if [Process.pid, Process.ppid].include?(owner)
35
+
36
+ includes?(Process.ppid, owner, parents)
37
+ rescue SystemCallError
38
+ false
39
+ end
40
+
41
+ def self.parents
42
+ IO.popen(%w[ps -eo pid=,ppid=], err: File::NULL, &:read)
43
+ .lines.to_h { |line| line.split.map(&:to_i) }
44
+ end
45
+
46
+ def self.includes?(pid, owner, parents)
47
+ return true if pid == owner
48
+ return false if pid <= 1 || !parents.key?(pid)
49
+
50
+ includes?(parents.fetch(pid), owner, parents)
51
+ end
52
+ end
53
+
54
+ class Guard
55
+ attr_reader :coord_dir, :taskrc, :coord
56
+
57
+ def initialize(env, input = {})
58
+ @env, @input = env, input.is_a?(Hash) ? input : {}
59
+ @record = record
60
+ @coord_dir = @record && File.join(@record.fetch("root"), ".maf", "coordination")
61
+ @taskrc = @coord_dir && File.join(@coord_dir, "taskrc")
62
+ @coord = @coord_dir && File.join(@coord_dir, "..", "bin", "coord")
63
+ end
64
+
65
+ def authorized?
66
+ @record && identity? && location? && board? && ProcessOwner.ancestor?(@record.fetch("pid")) && session?
67
+ rescue SystemCallError, KeyError, TypeError, ArgumentError
68
+ false
69
+ end
70
+
71
+ private
72
+
73
+ def record
74
+ return unless @env["MAF_SESSION_TOKEN"].to_s.match?(/\A[0-9a-f]{64}\z/)
75
+
76
+ data = JSON.parse(File.read(record_path))
77
+ data if valid_record?(data)
78
+ rescue SystemCallError, JSON::ParserError, TypeError
79
+ nil
80
+ end
81
+
82
+ def record_path
83
+ File.join(@env.fetch("COORD_DIR", ""), "sessions", "#{@env['MAF_SESSION_TOKEN']}.maf.json")
84
+ end
85
+
86
+ def valid_record?(data)
87
+ data.is_a?(Hash) && %w[root dir role worker harness].all? { |key| data[key].is_a?(String) } &&
88
+ data["pid"].is_a?(Integer) && data["pid"].positive?
89
+ end
90
+
91
+ def identity?
92
+ @record.fetch("role") == @env["COORD_ROLE"] && @record.fetch("worker") == @env["COORD_WORKER"] &&
93
+ @record.fetch("pid").to_s == @env["COORD_SESSION_PID"]
94
+ end
95
+
96
+ def location?
97
+ dir = File.realpath(@record.fetch("dir"))
98
+ current = File.realpath(@input.fetch("cwd", Dir.pwd))
99
+ current == dir || current.start_with?("#{dir}/")
100
+ end
101
+
102
+ def board?
103
+ File.realpath(@env.fetch("COORD_DIR")) == File.realpath(@coord_dir) &&
104
+ File.realpath(@env.fetch("TASKRC")) == File.realpath(@taskrc) && File.executable?(@coord)
105
+ end
106
+
107
+ def session?
108
+ id = @input["session_id"].to_s
109
+ return false if id.empty?
110
+
111
+ @input["hook_event_name"] == "SessionStart" ? bind(id) : bound?(id)
112
+ end
113
+
114
+ # A new harness session (/clear, a new Codex or opencode session) replaces
115
+ # the bound ID. Process ownership already rejects sibling processes.
116
+ # A temp file per process and a rename keep parallel SessionStart hooks safe.
117
+ def bind(id)
118
+ temp = "#{record_path}.session.#{Process.pid}"
119
+ File.write(temp, id, perm: 0o600)
120
+ File.rename(temp, "#{record_path}.session")
121
+ true
122
+ end
123
+
124
+ def bound?(id)
125
+ File.read("#{record_path}.session") == id
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,11 @@
1
+ # >>> multi-agent-flow >>>
2
+ # User Defined Attributes used by the `coord` wrapper.
3
+ # Do not remove: coord relies on these to attribute tasks to roles and workers.
4
+ uda.role.type=string
5
+ uda.worker.type=string
6
+ uda.scope.type=string
7
+ uda.taskref.type=string
8
+ uda.goalid.type=string
9
+ # Hide the "TASKRC override" footnote. coord always sets TASKRC.
10
+ verbose=blank,header,footnote,label,new-id,affected,edit,special,project,sync,filter,recur
11
+ # <<< multi-agent-flow <<<
data/assets/vault ADDED
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # vault - shared knowledge base watcher (graphify + Obsidian + MCP).
5
+ #
6
+ # Usage: vault [start|export|stop|status|age|mcp]
7
+ # vault # start the watcher (default); no-op if already running
8
+ # vault export # regenerate the Obsidian vault once, then exit
9
+ # vault stop
10
+ # vault status
11
+ # vault age [--json] # commits since the graph was built, and whether the graph is stale
12
+ # vault mcp # exec the stdio MCP server (for an MCP client config)
13
+ #
14
+ # graphify 0.9 removed the --obsidian/--obsidian-dir/--watch/--mcp flags. The
15
+ # current CLI is subcommand-based:
16
+ # graphify extract . --code-only initial code graph, no LLM
17
+ # graphify update . incremental code re-extract, no LLM
18
+ # graphify export obsidian --dir graphify-out/obsidian Obsidian notes + graph.canvas
19
+ # graphify-mcp stdio MCP server, spawned by the client
20
+ # The watcher polls `graphify update` and re-exports at VAULT_POLL seconds, so
21
+ # it does not need graphify's optional `watchdog` dependency. The graph and the
22
+ # vault live in graphify-out/ at the project root. graphify never reads that
23
+ # folder as input, except graphify-out/memory/. Bootstrap excludes it from git.
24
+ #
25
+ # Started automatically by bootstrap.rb when graphify is on PATH. Run by hand
26
+ # after installing graphify if it was not on PATH at bootstrap time.
27
+
28
+ require "fileutils"
29
+ require "json"
30
+ require "rbconfig"
31
+
32
+ abort "vault: Ruby 3.0+ required (current: #{RUBY_VERSION})." if RUBY_VERSION.split(".").first.to_i < 3
33
+
34
+ module Vault
35
+ # The default folders of graphify. An agent that runs graphify export
36
+ # writes the same vault.
37
+ GRAPH_DIR = "graphify-out"
38
+ DIR = File.join(GRAPH_DIR, "obsidian")
39
+ GRAPH = File.join(GRAPH_DIR, "graph.json")
40
+ PID_FILE = ".maf/coordination/vault.pid"
41
+ LOG_FILE = ".maf/coordination/vault.log"
42
+ # A garbage or tiny VAULT_POLL would turn the watcher into a busy loop.
43
+ MIN_POLL = 5
44
+ POLL = [(ENV["VAULT_POLL"] || 30).to_i, MIN_POLL].max
45
+ EXTRACT_CMD = ["graphify", "extract", ".", "--code-only"].freeze
46
+ DOC_GRAPH_LOCK = File.join(".maf", "coordination", "doc-graph.lock")
47
+ UPDATE_CMD = ["graphify", "update", "."].freeze
48
+ EXPORT_CMD = ["graphify", "export", "obsidian", "--dir", DIR].freeze
49
+
50
+ # Any other argument (or none) starts the watcher.
51
+ COMMANDS = { "stop" => :stop, "status" => :status, "export" => :export,
52
+ "mcp" => :mcp, "daemon" => :loop_forever }.freeze
53
+
54
+ # Every path of this script is relative to the main checkout, where the graph
55
+ # and the watcher live. A command from a worktree moves there first.
56
+ # graphify writes its output where GRAPHIFY_OUT points. Every graphify call
57
+ # of this script, and of its daemon, inherits the variable.
58
+ def self.run(argv)
59
+ Dir.chdir(Daemon.main_root)
60
+ ENV["GRAPHIFY_OUT"] = File.expand_path(GRAPH_DIR)
61
+ return age(argv) if argv.first == "age"
62
+
63
+ Daemon.public_send(COMMANDS.fetch(argv.first, :start))
64
+ end
65
+
66
+ def self.age(argv)
67
+ GraphAge.print(json: argv.include?("--json"))
68
+ end
69
+
70
+ # GraphAge counts the commits since the graph was built. The graph is stale
71
+ # when one of these commits changed a source or markdown file. The build
72
+ # time is the mtime of graph.json. It never fails: an unreadable state is
73
+ # reported as "unknown".
74
+ module GraphAge
75
+ SOURCE = /\.(md|rb|erb|py|js|jsx|ts|tsx|go|rs|java|kt|swift|c|h|cc|cpp|cs|php|sh|ex|exs|lua|scala)\z/
76
+
77
+ def self.state
78
+ return { state: "missing", commits: nil } unless File.exist?(GRAPH)
79
+
80
+ since = "--since=#{File.mtime(GRAPH).to_i}"
81
+ commits = git("rev-list", "--count", since, "HEAD")&.to_i
82
+ return { state: "unknown", commits: nil } unless commits
83
+
84
+ { state: stale?(since) ? "stale" : "fresh", commits: commits }
85
+ end
86
+
87
+ def self.stale?(since)
88
+ git("log", since, "--name-only", "--format=").to_s.lines.any? { |line| line.strip.match?(SOURCE) }
89
+ end
90
+
91
+ def self.git(*args)
92
+ output = IO.popen(["git", *args], err: File::NULL, &:read)
93
+ $?.success? ? output : nil
94
+ rescue SystemCallError
95
+ nil
96
+ end
97
+
98
+ def self.text(data)
99
+ return "graph age: #{data[:state]}" unless data[:commits]
100
+
101
+ "graph age: #{data[:commits]} #{data[:commits] == 1 ? "commit" : "commits"} since build (#{data[:state]})"
102
+ end
103
+
104
+ def self.print(json: false)
105
+ data = state
106
+ puts json ? JSON.generate(data) : text(data)
107
+ end
108
+ end
109
+
110
+ module Daemon
111
+ def self.start
112
+ return puts("vault: already running (pid #{Pid.read})") if Pid.running?
113
+ abort "vault: graphify not found on PATH. Install: pip/uv install graphifyy" unless which("graphify")
114
+
115
+ FileUtils.mkdir_p(File.dirname(LOG_FILE))
116
+ spawn_detached
117
+ end
118
+
119
+ def self.export
120
+ return puts("vault: #{GRAPH} missing. Run `vault` to build it first.") unless File.exist?(GRAPH)
121
+
122
+ system(*EXPORT_CMD) || warn("vault: obsidian export failed; see #{LOG_FILE}")
123
+ end
124
+
125
+ # MCP is a stdio server the client spawns, not a background daemon. Point
126
+ # an MCP client at this command instead of a long-running process.
127
+ def self.mcp
128
+ abort "vault: graphify-mcp not found on PATH. Install: pip/uv install graphifyy" unless which("graphify-mcp")
129
+ abort "vault: #{GRAPH} missing. Run `vault` to build it first." unless File.exist?(GRAPH)
130
+
131
+ Kernel.exec("graphify-mcp", File.expand_path(GRAPH))
132
+ end
133
+
134
+ def self.main_root
135
+ common = IO.popen(%w[git rev-parse --git-common-dir], err: File::NULL, &:read).to_s.strip
136
+ common.empty? ? Dir.pwd : File.dirname(File.expand_path(common))
137
+ rescue SystemCallError
138
+ Dir.pwd
139
+ end
140
+
141
+ def self.build_initial_graph
142
+ return if File.exist?(GRAPH)
143
+
144
+ puts "vault: no #{GRAPH} yet; building the code graph (no LLM)"
145
+ system(*EXTRACT_CMD) || warn("vault: graphify extract failed; retrying via update each poll")
146
+ end
147
+
148
+ def self.spawn_detached
149
+ pid = Process.spawn(RbConfig.ruby, File.expand_path(__FILE__), "daemon",
150
+ out: LOG_FILE, err: LOG_FILE, pgroup: true)
151
+ Process.detach(pid)
152
+ Pid.write(pid)
153
+ puts "vault: started (pid #{pid}), log at #{LOG_FILE}"
154
+ end
155
+
156
+ # The daemon builds the first graph itself, so `vault` (and bootstrap)
157
+ # return at once instead of blocking on a full extract. It exits when the
158
+ # pid file is gone (`vault stop`, or a removed checkout), so it never
159
+ # outlives its project.
160
+ def self.loop_forever
161
+ Pid.write(Process.pid)
162
+ build_initial_graph
163
+ exported_at = nil
164
+ exported_at = tick(exported_at) while File.exist?(PID_FILE)
165
+ end
166
+
167
+ # One poll: refresh the graph, re-export only when graph.json changed
168
+ # (Obsidian reloads every rewritten note), then sleep. Returns the graph
169
+ # mtime that was last exported.
170
+ def self.tick(exported_at)
171
+ system(*UPDATE_CMD) unless File.exist?(DOC_GRAPH_LOCK)
172
+ mtime = File.exist?(GRAPH) ? File.mtime(GRAPH) : nil
173
+ export if mtime && mtime != exported_at
174
+ sleep POLL
175
+ mtime || exported_at
176
+ end
177
+
178
+ def self.stop
179
+ return puts("vault: not running") unless Pid.running?
180
+
181
+ pid = Pid.read
182
+ Process.kill("TERM", -pid)
183
+ Pid.clear
184
+ puts "vault: stopped"
185
+ end
186
+
187
+ def self.status
188
+ puts Pid.running? ? "vault: running (pid #{Pid.read})" : "vault: not running"
189
+ puts File.exist?(GRAPH) ? "graph: #{GRAPH}" : "graph: #{GRAPH} missing"
190
+ puts File.directory?(DIR) ? "obsidian: #{DIR}/" : "obsidian: #{DIR}/ missing"
191
+ puts GraphAge.text(GraphAge.state)
192
+ end
193
+
194
+ def self.which(bin)
195
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? { |dir| File.executable?(File.join(dir, bin)) }
196
+ end
197
+ end
198
+
199
+ module Pid
200
+ # A corrupt or empty pid file reads as 0; Process.kill(0, 0) would signal
201
+ # this process's own group, so treat a non-positive pid as not running.
202
+ def self.running?
203
+ pid = File.exist?(PID_FILE) ? read : 0
204
+ pid.positive? && Process.kill(0, pid) && true
205
+ rescue Errno::ESRCH, Errno::ENOENT, Errno::EINVAL, RangeError
206
+ false
207
+ end
208
+
209
+ def self.read
210
+ File.read(PID_FILE).to_i
211
+ end
212
+
213
+ def self.write(pid)
214
+ FileUtils.mkdir_p(File.dirname(PID_FILE))
215
+ File.write(PID_FILE, pid)
216
+ end
217
+
218
+ def self.clear
219
+ File.delete(PID_FILE)
220
+ end
221
+ end
222
+ end
223
+
224
+ Vault.run(ARGV) if __FILE__ == $PROGRAM_NAME
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # worktree-env.rb - example project hook for `coord worktree` (Rails).
4
+ #
5
+ # Copy this file to .maf/coordination/worktree-env.rb. Git ignores that folder.
6
+ # To commit the hook, run `git add -f .maf/coordination/worktree-env.rb`.
7
+ # `coord worktree` runs the hook for each worktree and appends the output
8
+ # to the worktree's .maf/env.sh.
9
+ #
10
+ # Input: COORD_SLOT (1, 2, ...; the main worktree is slot 0) and
11
+ # COORD_WORKTREE (absolute path of the worktree).
12
+ # Output: one `export NAME=VALUE` line per variable. `coord worktree` keeps
13
+ # only those lines, so a later `source .maf/env.sh` runs no other
14
+ # line the hook prints.
15
+ #
16
+ # Use the variables in the project, for example in config/database.yml:
17
+ # test:
18
+ # database: app_test<%= ENV["TEST_ENV_NUMBER"] %>
19
+ # Then create each worktree's test database one time:
20
+ # bin/rails db:test:prepare
21
+
22
+ slot = ENV.fetch("COORD_SLOT").to_i
23
+
24
+ puts "export TEST_ENV_NUMBER=#{slot}"
25
+ puts "export PORT=#{3000 + slot}"
26
+ puts "export CAPYBARA_SERVER_PORT=#{4000 + slot}"
data/exe/maf ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # maf - multi-agent-flow command line tool.
5
+ #
6
+ # Run maf in the project root. maf uses the current directory as the project.
7
+ # Install it with: gem install maf. In a clone, link this file into a
8
+ # directory on PATH. __dir__ follows the link to find lib/.
9
+ lib = File.expand_path("../lib/maf.rb", __dir__)
10
+ abort "maf: cannot find #{lib}. Run: gem install maf. Or link exe/maf into PATH with ln -s. Do not copy it." \
11
+ unless File.exist?(lib)
12
+ require lib
13
+
14
+ Maf::CLI.new(ARGV).run