opencode-mad 0.2.0 → 0.3.1
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.
- package/README.md +238 -235
- package/agents/mad-fixer.md +44 -31
- package/agents/mad-merger.md +45 -18
- package/agents/mad-tester.md +187 -224
- package/agents/orchestrator.md +510 -430
- package/install.js +85 -66
- package/package.json +36 -36
- package/plugins/mad-plugin.ts +627 -546
- package/skills/mad-workflow/SKILL.md +27 -0
package/plugins/mad-plugin.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
-
import { tool } from "@opencode-ai/plugin"
|
|
3
|
-
import { readFileSync, existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync, appendFileSync } from "fs"
|
|
4
|
-
import { join, basename } from "path"
|
|
5
|
-
import { execSync } from "child_process"
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
import { tool } from "@opencode-ai/plugin"
|
|
3
|
+
import { readFileSync, existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync, appendFileSync } from "fs"
|
|
4
|
+
import { join, basename } from "path"
|
|
5
|
+
import { execSync } from "child_process"
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* MAD - Multi-Agent Dev Plugin for OpenCode
|
|
@@ -11,67 +11,117 @@ import { execSync } from "child_process"
|
|
|
11
11
|
* The orchestrator agent decomposes tasks and delegates to developer subagents
|
|
12
12
|
* running in parallel via OpenCode's Task tool.
|
|
13
13
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return result.
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Helper to
|
|
57
|
-
*/
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
14
|
+
|
|
15
|
+
// Current version of opencode-mad
|
|
16
|
+
const CURRENT_VERSION = "0.3.1"
|
|
17
|
+
|
|
18
|
+
// Update notification state (shown only once per session)
|
|
19
|
+
let updateNotificationShown = false
|
|
20
|
+
let pendingUpdateMessage: string | null = null
|
|
21
|
+
|
|
22
|
+
export const MADPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Helper to run shell commands with proper error handling (cross-platform)
|
|
26
|
+
*/
|
|
27
|
+
const runCommand = (cmd: string, cwd?: string): { success: boolean; output: string; error?: string } => {
|
|
28
|
+
try {
|
|
29
|
+
const output = execSync(cmd, {
|
|
30
|
+
encoding: "utf-8",
|
|
31
|
+
cwd: cwd || process.cwd(),
|
|
32
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
33
|
+
})
|
|
34
|
+
return { success: true, output: output.trim() }
|
|
35
|
+
} catch (e: any) {
|
|
36
|
+
return {
|
|
37
|
+
success: false,
|
|
38
|
+
output: "",
|
|
39
|
+
error: e.stderr?.toString() || e.message || "Unknown error"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Helper to get git root with error handling
|
|
46
|
+
*/
|
|
47
|
+
const getGitRoot = (): string => {
|
|
48
|
+
const result = runCommand("git rev-parse --show-toplevel")
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
throw new Error(`Not a git repository or git not found: ${result.error}`)
|
|
51
|
+
}
|
|
52
|
+
return result.output.replace(/\\/g, "/")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Helper to get current branch with fallback
|
|
57
|
+
*/
|
|
58
|
+
const getCurrentBranch = (): string => {
|
|
59
|
+
const result = runCommand("git symbolic-ref --short HEAD")
|
|
60
|
+
return result.success ? result.output : "main"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Helper to log MAD events
|
|
65
|
+
*/
|
|
66
|
+
const logEvent = (level: "info" | "warn" | "error" | "debug", message: string, context?: any) => {
|
|
67
|
+
try {
|
|
68
|
+
const gitRoot = getGitRoot()
|
|
69
|
+
const logFile = join(gitRoot, ".mad-logs.jsonl")
|
|
70
|
+
const logEntry = JSON.stringify({
|
|
71
|
+
timestamp: new Date().toISOString(),
|
|
72
|
+
level,
|
|
73
|
+
message,
|
|
74
|
+
context
|
|
75
|
+
}) + "\n"
|
|
76
|
+
|
|
77
|
+
appendFileSync(logFile, logEntry)
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// Silent fail for logging - don't break the workflow
|
|
80
|
+
console.error("Failed to write log:", e)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check for updates from npm registry
|
|
86
|
+
*/
|
|
87
|
+
const checkForUpdates = async (): Promise<{ hasUpdate: boolean; current: string; latest: string }> => {
|
|
88
|
+
try {
|
|
89
|
+
const result = runCommand("npm view opencode-mad version")
|
|
90
|
+
if (result.success) {
|
|
91
|
+
const latestVersion = result.output.trim()
|
|
92
|
+
return {
|
|
93
|
+
hasUpdate: latestVersion !== CURRENT_VERSION,
|
|
94
|
+
current: CURRENT_VERSION,
|
|
95
|
+
latest: latestVersion
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// Silent fail - don't break startup if npm check fails
|
|
100
|
+
}
|
|
101
|
+
return { hasUpdate: false, current: CURRENT_VERSION, latest: CURRENT_VERSION }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check for updates at plugin initialization and store message for first tool response
|
|
105
|
+
try {
|
|
106
|
+
const updateInfo = await checkForUpdates()
|
|
107
|
+
if (updateInfo.hasUpdate) {
|
|
108
|
+
pendingUpdateMessage = `🔄 **Update available!** opencode-mad ${updateInfo.current} → ${updateInfo.latest}\n Run: \`npx opencode-mad install -g\`\n\n`
|
|
109
|
+
logEvent("info", "Update available", { current: updateInfo.current, latest: updateInfo.latest })
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
// Silent fail - don't break plugin initialization
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Helper to get update notification (returns message only once)
|
|
117
|
+
*/
|
|
118
|
+
const getUpdateNotification = (): string => {
|
|
119
|
+
if (pendingUpdateMessage && !updateNotificationShown) {
|
|
120
|
+
updateNotificationShown = true
|
|
121
|
+
return pendingUpdateMessage
|
|
122
|
+
}
|
|
123
|
+
return ""
|
|
124
|
+
}
|
|
75
125
|
|
|
76
126
|
return {
|
|
77
127
|
// Custom tools for MAD workflow
|
|
@@ -80,322 +130,322 @@ export const MADPlugin: Plugin = async ({ project, client, $, directory, worktre
|
|
|
80
130
|
/**
|
|
81
131
|
* Create a git worktree for parallel development
|
|
82
132
|
*/
|
|
83
|
-
mad_worktree_create: tool({
|
|
84
|
-
description: `Create a new git worktree branch for parallel development.
|
|
85
|
-
Use this to set up isolated development environments for subtasks.
|
|
86
|
-
Each worktree has its own branch and working directory.`,
|
|
87
|
-
args: {
|
|
88
|
-
branch: tool.schema.string().describe("Branch name for the worktree (e.g., 'feat/auth-login')"),
|
|
89
|
-
task: tool.schema.string().describe("Description of the task to be done in this worktree"),
|
|
90
|
-
},
|
|
91
|
-
async execute(args, context) {
|
|
92
|
-
try {
|
|
93
|
-
const { branch, task } = args
|
|
94
|
-
|
|
95
|
-
// Validate inputs
|
|
96
|
-
if (!branch || branch.trim() === "") {
|
|
97
|
-
logEvent("error", "mad_worktree_create failed: empty branch name")
|
|
98
|
-
return "❌ Error: Branch name cannot be empty"
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!task || task.trim() === "") {
|
|
102
|
-
logEvent("error", "mad_worktree_create failed: empty task description")
|
|
103
|
-
return "❌ Error: Task description cannot be empty"
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const gitRoot = getGitRoot()
|
|
107
|
-
const baseBranch = getCurrentBranch()
|
|
108
|
-
const sessionName = branch.replace(/\//g, "-")
|
|
109
|
-
const worktreeDir = join(gitRoot, "worktrees")
|
|
110
|
-
const worktreePath = join(worktreeDir, sessionName)
|
|
111
|
-
|
|
112
|
-
// Check if worktree already exists
|
|
113
|
-
if (existsSync(worktreePath)) {
|
|
114
|
-
logEvent("warn", "Worktree already exists", { branch, path: worktreePath })
|
|
115
|
-
return `⚠️ Worktree already exists at ${worktreePath}\nUse a different branch name or clean up with mad_cleanup.`
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
logEvent("info", "Creating worktree", { branch, baseBranch })
|
|
119
|
-
|
|
120
|
-
// Ensure .agent-* files are in .gitignore
|
|
121
|
-
const gitignorePath = join(gitRoot, ".gitignore")
|
|
122
|
-
let gitignoreContent = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : ""
|
|
123
|
-
if (!gitignoreContent.includes(".agent-")) {
|
|
124
|
-
const additions = `
|
|
125
|
-
# MAD agent files (never commit)
|
|
126
|
-
.agent-task
|
|
127
|
-
.agent-done
|
|
128
|
-
.agent-blocked
|
|
129
|
-
.agent-error
|
|
130
|
-
.mad-logs.jsonl
|
|
131
|
-
|
|
132
|
-
# Worktrees directory
|
|
133
|
-
worktrees/
|
|
134
|
-
`
|
|
135
|
-
appendFileSync(gitignorePath, additions)
|
|
136
|
-
runCommand(`git add "${gitignorePath}" && git commit -m "chore: add MAD agent files to gitignore"`, gitRoot)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Create worktree directory using Node.js
|
|
140
|
-
try {
|
|
141
|
-
mkdirSync(worktreeDir, { recursive: true })
|
|
142
|
-
} catch (e: any) {
|
|
143
|
-
logEvent("error", "Failed to create worktree directory", { error: e.message })
|
|
144
|
-
return `❌ Error creating worktree directory: ${e.message}`
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Check if branch exists
|
|
148
|
-
const branchCheckResult = runCommand(`git rev-parse --verify ${branch}`)
|
|
149
|
-
const branchExists = branchCheckResult.success
|
|
150
|
-
|
|
151
|
-
// Create worktree
|
|
152
|
-
const worktreeCmd = branchExists
|
|
153
|
-
? `git worktree add "${worktreePath}" ${branch}`
|
|
154
|
-
: `git worktree add -b ${branch} "${worktreePath}" ${baseBranch}`
|
|
155
|
-
|
|
156
|
-
const worktreeResult = runCommand(worktreeCmd, gitRoot)
|
|
157
|
-
if (!worktreeResult.success) {
|
|
158
|
-
logEvent("error", "Failed to create git worktree", {
|
|
159
|
-
branch,
|
|
160
|
-
command: worktreeCmd,
|
|
161
|
-
error: worktreeResult.error
|
|
162
|
-
})
|
|
163
|
-
return `❌ Error creating git worktree: ${worktreeResult.error}`
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Write task file using Node.js
|
|
167
|
-
const taskContent = `# Agent Task
|
|
168
|
-
# Branch: ${branch}
|
|
169
|
-
# Created: ${new Date().toISOString()}
|
|
170
|
-
# Base: ${baseBranch}
|
|
171
|
-
|
|
172
|
-
${task}
|
|
173
|
-
`
|
|
174
|
-
try {
|
|
175
|
-
writeFileSync(join(worktreePath, ".agent-task"), taskContent)
|
|
176
|
-
} catch (e: any) {
|
|
177
|
-
logEvent("warn", "Failed to write task file", { error: e.message })
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
logEvent("info", "Worktree created successfully", { branch, path: worktreePath })
|
|
181
|
-
|
|
182
|
-
return `✅ Worktree created successfully!
|
|
183
|
-
- Path: ${worktreePath}
|
|
184
|
-
- Branch: ${branch}
|
|
185
|
-
- Base: ${baseBranch}
|
|
186
|
-
- Task: ${task.substring(0, 100)}${task.length > 100 ? "..." : ""}
|
|
187
|
-
|
|
188
|
-
The developer subagent can now work in this worktree using the Task tool.`
|
|
189
|
-
} catch (e: any) {
|
|
190
|
-
logEvent("error", "mad_worktree_create exception", { error: e.message, stack: e.stack })
|
|
191
|
-
return `❌ Unexpected error creating worktree: ${e.message}`
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
}),
|
|
133
|
+
mad_worktree_create: tool({
|
|
134
|
+
description: `Create a new git worktree branch for parallel development.
|
|
135
|
+
Use this to set up isolated development environments for subtasks.
|
|
136
|
+
Each worktree has its own branch and working directory.`,
|
|
137
|
+
args: {
|
|
138
|
+
branch: tool.schema.string().describe("Branch name for the worktree (e.g., 'feat/auth-login')"),
|
|
139
|
+
task: tool.schema.string().describe("Description of the task to be done in this worktree"),
|
|
140
|
+
},
|
|
141
|
+
async execute(args, context) {
|
|
142
|
+
try {
|
|
143
|
+
const { branch, task } = args
|
|
144
|
+
|
|
145
|
+
// Validate inputs
|
|
146
|
+
if (!branch || branch.trim() === "") {
|
|
147
|
+
logEvent("error", "mad_worktree_create failed: empty branch name")
|
|
148
|
+
return getUpdateNotification() + "❌ Error: Branch name cannot be empty"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!task || task.trim() === "") {
|
|
152
|
+
logEvent("error", "mad_worktree_create failed: empty task description")
|
|
153
|
+
return getUpdateNotification() + "❌ Error: Task description cannot be empty"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const gitRoot = getGitRoot()
|
|
157
|
+
const baseBranch = getCurrentBranch()
|
|
158
|
+
const sessionName = branch.replace(/\//g, "-")
|
|
159
|
+
const worktreeDir = join(gitRoot, "worktrees")
|
|
160
|
+
const worktreePath = join(worktreeDir, sessionName)
|
|
161
|
+
|
|
162
|
+
// Check if worktree already exists
|
|
163
|
+
if (existsSync(worktreePath)) {
|
|
164
|
+
logEvent("warn", "Worktree already exists", { branch, path: worktreePath })
|
|
165
|
+
return getUpdateNotification() + `⚠️ Worktree already exists at ${worktreePath}\nUse a different branch name or clean up with mad_cleanup.`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
logEvent("info", "Creating worktree", { branch, baseBranch })
|
|
169
|
+
|
|
170
|
+
// Ensure .agent-* files are in .gitignore
|
|
171
|
+
const gitignorePath = join(gitRoot, ".gitignore")
|
|
172
|
+
let gitignoreContent = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : ""
|
|
173
|
+
if (!gitignoreContent.includes(".agent-")) {
|
|
174
|
+
const additions = `
|
|
175
|
+
# MAD agent files (never commit)
|
|
176
|
+
.agent-task
|
|
177
|
+
.agent-done
|
|
178
|
+
.agent-blocked
|
|
179
|
+
.agent-error
|
|
180
|
+
.mad-logs.jsonl
|
|
181
|
+
|
|
182
|
+
# Worktrees directory
|
|
183
|
+
worktrees/
|
|
184
|
+
`
|
|
185
|
+
appendFileSync(gitignorePath, additions)
|
|
186
|
+
runCommand(`git add "${gitignorePath}" && git commit -m "chore: add MAD agent files to gitignore"`, gitRoot)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Create worktree directory using Node.js
|
|
190
|
+
try {
|
|
191
|
+
mkdirSync(worktreeDir, { recursive: true })
|
|
192
|
+
} catch (e: any) {
|
|
193
|
+
logEvent("error", "Failed to create worktree directory", { error: e.message })
|
|
194
|
+
return getUpdateNotification() + `❌ Error creating worktree directory: ${e.message}`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check if branch exists
|
|
198
|
+
const branchCheckResult = runCommand(`git rev-parse --verify ${branch}`)
|
|
199
|
+
const branchExists = branchCheckResult.success
|
|
200
|
+
|
|
201
|
+
// Create worktree
|
|
202
|
+
const worktreeCmd = branchExists
|
|
203
|
+
? `git worktree add "${worktreePath}" ${branch}`
|
|
204
|
+
: `git worktree add -b ${branch} "${worktreePath}" ${baseBranch}`
|
|
205
|
+
|
|
206
|
+
const worktreeResult = runCommand(worktreeCmd, gitRoot)
|
|
207
|
+
if (!worktreeResult.success) {
|
|
208
|
+
logEvent("error", "Failed to create git worktree", {
|
|
209
|
+
branch,
|
|
210
|
+
command: worktreeCmd,
|
|
211
|
+
error: worktreeResult.error
|
|
212
|
+
})
|
|
213
|
+
return getUpdateNotification() + `❌ Error creating git worktree: ${worktreeResult.error}`
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Write task file using Node.js
|
|
217
|
+
const taskContent = `# Agent Task
|
|
218
|
+
# Branch: ${branch}
|
|
219
|
+
# Created: ${new Date().toISOString()}
|
|
220
|
+
# Base: ${baseBranch}
|
|
221
|
+
|
|
222
|
+
${task}
|
|
223
|
+
`
|
|
224
|
+
try {
|
|
225
|
+
writeFileSync(join(worktreePath, ".agent-task"), taskContent)
|
|
226
|
+
} catch (e: any) {
|
|
227
|
+
logEvent("warn", "Failed to write task file", { error: e.message })
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
logEvent("info", "Worktree created successfully", { branch, path: worktreePath })
|
|
231
|
+
|
|
232
|
+
return getUpdateNotification() + `✅ Worktree created successfully!
|
|
233
|
+
- Path: ${worktreePath}
|
|
234
|
+
- Branch: ${branch}
|
|
235
|
+
- Base: ${baseBranch}
|
|
236
|
+
- Task: ${task.substring(0, 100)}${task.length > 100 ? "..." : ""}
|
|
237
|
+
|
|
238
|
+
The developer subagent can now work in this worktree using the Task tool.`
|
|
239
|
+
} catch (e: any) {
|
|
240
|
+
logEvent("error", "mad_worktree_create exception", { error: e.message, stack: e.stack })
|
|
241
|
+
return getUpdateNotification() + `❌ Unexpected error creating worktree: ${e.message}`
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
195
245
|
|
|
196
246
|
/**
|
|
197
247
|
* Get status of all active worktrees/agents
|
|
198
248
|
*/
|
|
199
|
-
mad_status: tool({
|
|
200
|
-
description: `Get the status of all MAD worktrees and their development progress.
|
|
201
|
-
Shows which tasks are done, in progress, blocked, or have errors.`,
|
|
202
|
-
args: {},
|
|
203
|
-
async execute(args, context) {
|
|
204
|
-
const gitRoot = getGitRoot()
|
|
205
|
-
const worktreeDir = join(gitRoot, "worktrees")
|
|
206
|
-
|
|
207
|
-
if (!existsSync(worktreeDir)) {
|
|
208
|
-
return "No active MAD worktrees. Use mad_worktree_create to create one."
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const entries = readdirSync(worktreeDir)
|
|
212
|
-
if (entries.length === 0) {
|
|
213
|
-
return "No active MAD worktrees. Use mad_worktree_create to create one."
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
let status = "# MAD Status Dashboard\n\n"
|
|
217
|
-
let total = 0, done = 0, blocked = 0, errors = 0, wip = 0
|
|
218
|
-
|
|
219
|
-
for (const entry of entries) {
|
|
220
|
-
const wpath = join(worktreeDir, entry)
|
|
221
|
-
if (!statSync(wpath).isDirectory()) continue
|
|
222
|
-
total++
|
|
223
|
-
|
|
224
|
-
const taskFile = join(wpath, ".agent-task")
|
|
225
|
-
const doneFile = join(wpath, ".agent-done")
|
|
226
|
-
const blockedFile = join(wpath, ".agent-blocked")
|
|
227
|
-
const errorFile = join(wpath, ".agent-error")
|
|
228
|
-
|
|
229
|
-
let statusIcon = "⏳"
|
|
230
|
-
let statusText = "IN PROGRESS"
|
|
231
|
-
let detail = ""
|
|
232
|
-
|
|
233
|
-
if (existsSync(doneFile)) {
|
|
234
|
-
statusIcon = "✅"
|
|
235
|
-
statusText = "DONE"
|
|
236
|
-
detail = readFileSync(doneFile, "utf-8").split("\n")[0]
|
|
237
|
-
done++
|
|
238
|
-
} else if (existsSync(blockedFile)) {
|
|
239
|
-
statusIcon = "🚫"
|
|
240
|
-
statusText = "BLOCKED"
|
|
241
|
-
detail = readFileSync(blockedFile, "utf-8").split("\n")[0]
|
|
242
|
-
blocked++
|
|
243
|
-
} else if (existsSync(errorFile)) {
|
|
244
|
-
statusIcon = "❌"
|
|
245
|
-
statusText = "ERROR"
|
|
246
|
-
detail = readFileSync(errorFile, "utf-8").split("\n")[0]
|
|
247
|
-
errors++
|
|
248
|
-
} else {
|
|
249
|
-
wip++
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Get task description
|
|
253
|
-
const task = existsSync(taskFile)
|
|
254
|
-
? readFileSync(taskFile, "utf-8").split("\n").filter(l => !l.startsWith("#") && l.trim()).join(" ").slice(0, 60)
|
|
255
|
-
: "No task file"
|
|
256
|
-
|
|
257
|
-
// Get commit count
|
|
258
|
-
let commits = "0"
|
|
259
|
-
try {
|
|
260
|
-
const baseBranch = getCurrentBranch()
|
|
261
|
-
const result = runCommand(`git log --oneline ${baseBranch}..HEAD`, wpath)
|
|
262
|
-
if (result.success) {
|
|
263
|
-
commits = result.output.split("\n").filter(l => l.trim()).length.toString()
|
|
264
|
-
}
|
|
265
|
-
} catch {}
|
|
266
|
-
|
|
267
|
-
status += `## ${statusIcon} ${entry}\n`
|
|
268
|
-
status += `- **Status:** ${statusText}\n`
|
|
269
|
-
status += `- **Task:** ${task}\n`
|
|
270
|
-
status += `- **Commits:** ${commits}\n`
|
|
271
|
-
if (detail) status += `- **Detail:** ${detail}\n`
|
|
272
|
-
status += `\n`
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
status += `---\n`
|
|
276
|
-
status += `**Total:** ${total} | **Done:** ${done} | **WIP:** ${wip} | **Blocked:** ${blocked} | **Errors:** ${errors}\n`
|
|
277
|
-
|
|
278
|
-
return status
|
|
279
|
-
},
|
|
280
|
-
}),
|
|
249
|
+
mad_status: tool({
|
|
250
|
+
description: `Get the status of all MAD worktrees and their development progress.
|
|
251
|
+
Shows which tasks are done, in progress, blocked, or have errors.`,
|
|
252
|
+
args: {},
|
|
253
|
+
async execute(args, context) {
|
|
254
|
+
const gitRoot = getGitRoot()
|
|
255
|
+
const worktreeDir = join(gitRoot, "worktrees")
|
|
256
|
+
|
|
257
|
+
if (!existsSync(worktreeDir)) {
|
|
258
|
+
return getUpdateNotification() + "No active MAD worktrees. Use mad_worktree_create to create one."
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const entries = readdirSync(worktreeDir)
|
|
262
|
+
if (entries.length === 0) {
|
|
263
|
+
return getUpdateNotification() + "No active MAD worktrees. Use mad_worktree_create to create one."
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let status = getUpdateNotification() + "# MAD Status Dashboard\n\n"
|
|
267
|
+
let total = 0, done = 0, blocked = 0, errors = 0, wip = 0
|
|
268
|
+
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
const wpath = join(worktreeDir, entry)
|
|
271
|
+
if (!statSync(wpath).isDirectory()) continue
|
|
272
|
+
total++
|
|
273
|
+
|
|
274
|
+
const taskFile = join(wpath, ".agent-task")
|
|
275
|
+
const doneFile = join(wpath, ".agent-done")
|
|
276
|
+
const blockedFile = join(wpath, ".agent-blocked")
|
|
277
|
+
const errorFile = join(wpath, ".agent-error")
|
|
278
|
+
|
|
279
|
+
let statusIcon = "⏳"
|
|
280
|
+
let statusText = "IN PROGRESS"
|
|
281
|
+
let detail = ""
|
|
282
|
+
|
|
283
|
+
if (existsSync(doneFile)) {
|
|
284
|
+
statusIcon = "✅"
|
|
285
|
+
statusText = "DONE"
|
|
286
|
+
detail = readFileSync(doneFile, "utf-8").split("\n")[0]
|
|
287
|
+
done++
|
|
288
|
+
} else if (existsSync(blockedFile)) {
|
|
289
|
+
statusIcon = "🚫"
|
|
290
|
+
statusText = "BLOCKED"
|
|
291
|
+
detail = readFileSync(blockedFile, "utf-8").split("\n")[0]
|
|
292
|
+
blocked++
|
|
293
|
+
} else if (existsSync(errorFile)) {
|
|
294
|
+
statusIcon = "❌"
|
|
295
|
+
statusText = "ERROR"
|
|
296
|
+
detail = readFileSync(errorFile, "utf-8").split("\n")[0]
|
|
297
|
+
errors++
|
|
298
|
+
} else {
|
|
299
|
+
wip++
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Get task description
|
|
303
|
+
const task = existsSync(taskFile)
|
|
304
|
+
? readFileSync(taskFile, "utf-8").split("\n").filter(l => !l.startsWith("#") && l.trim()).join(" ").slice(0, 60)
|
|
305
|
+
: "No task file"
|
|
306
|
+
|
|
307
|
+
// Get commit count
|
|
308
|
+
let commits = "0"
|
|
309
|
+
try {
|
|
310
|
+
const baseBranch = getCurrentBranch()
|
|
311
|
+
const result = runCommand(`git log --oneline ${baseBranch}..HEAD`, wpath)
|
|
312
|
+
if (result.success) {
|
|
313
|
+
commits = result.output.split("\n").filter(l => l.trim()).length.toString()
|
|
314
|
+
}
|
|
315
|
+
} catch {}
|
|
316
|
+
|
|
317
|
+
status += `## ${statusIcon} ${entry}\n`
|
|
318
|
+
status += `- **Status:** ${statusText}\n`
|
|
319
|
+
status += `- **Task:** ${task}\n`
|
|
320
|
+
status += `- **Commits:** ${commits}\n`
|
|
321
|
+
if (detail) status += `- **Detail:** ${detail}\n`
|
|
322
|
+
status += `\n`
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
status += `---\n`
|
|
326
|
+
status += `**Total:** ${total} | **Done:** ${done} | **WIP:** ${wip} | **Blocked:** ${blocked} | **Errors:** ${errors}\n`
|
|
327
|
+
|
|
328
|
+
return status
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
281
331
|
|
|
282
332
|
/**
|
|
283
333
|
* Run tests on a worktree
|
|
284
334
|
*/
|
|
285
|
-
mad_test: tool({
|
|
286
|
-
description: `Run tests/build/lint on a MAD worktree to verify the code is working.
|
|
287
|
-
Automatically detects the project type (Node.js, Python, Go, Rust) and runs appropriate checks.
|
|
288
|
-
Returns the results and creates an error file if tests fail.`,
|
|
289
|
-
args: {
|
|
290
|
-
worktree: tool.schema.string().describe("Worktree session name (e.g., 'feat-auth-login')"),
|
|
291
|
-
},
|
|
292
|
-
async execute(args, context) {
|
|
293
|
-
const gitRoot = getGitRoot()
|
|
294
|
-
const worktreePath = join(gitRoot, "worktrees", args.worktree)
|
|
295
|
-
|
|
296
|
-
if (!existsSync(worktreePath)) {
|
|
297
|
-
return `Worktree not found: ${worktreePath}`
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
let results = `# Test Results for ${args.worktree}\n\n`
|
|
301
|
-
let hasError = false
|
|
302
|
-
let errorMessages = ""
|
|
303
|
-
|
|
304
|
-
// Helper to run a check
|
|
305
|
-
const doCheck = (label: string, cmd: string) => {
|
|
306
|
-
results += `## ${label}\n`
|
|
307
|
-
const result = runCommand(cmd, worktreePath)
|
|
308
|
-
if (result.success) {
|
|
309
|
-
results += `✅ Passed\n\`\`\`\n${result.output.slice(0, 500)}\n\`\`\`\n\n`
|
|
310
|
-
} else {
|
|
311
|
-
hasError = true
|
|
312
|
-
const output = result.error || "Unknown error"
|
|
313
|
-
results += `❌ Failed\n\`\`\`\n${output.slice(0, 1000)}\n\`\`\`\n\n`
|
|
314
|
-
errorMessages += `${label} FAILED:\n${output}\n\n`
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Detect project type and run checks
|
|
319
|
-
const packageJson = join(worktreePath, "package.json")
|
|
320
|
-
const goMod = join(worktreePath, "go.mod")
|
|
321
|
-
const cargoToml = join(worktreePath, "Cargo.toml")
|
|
322
|
-
const pyProject = join(worktreePath, "pyproject.toml")
|
|
323
|
-
const requirements = join(worktreePath, "requirements.txt")
|
|
324
|
-
|
|
325
|
-
if (existsSync(packageJson)) {
|
|
326
|
-
const pkg = JSON.parse(readFileSync(packageJson, "utf-8"))
|
|
327
|
-
if (pkg.scripts?.lint) doCheck("Lint", "npm run lint")
|
|
328
|
-
if (pkg.scripts?.build) doCheck("Build", "npm run build")
|
|
329
|
-
if (pkg.scripts?.test) doCheck("Test", "npm test")
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (existsSync(goMod)) {
|
|
333
|
-
doCheck("Go Build", "go build ./...")
|
|
334
|
-
doCheck("Go Test", "go test ./...")
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (existsSync(cargoToml)) {
|
|
338
|
-
doCheck("Cargo Check", "cargo check")
|
|
339
|
-
doCheck("Cargo Test", "cargo test")
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (existsSync(pyProject) || existsSync(requirements)) {
|
|
343
|
-
doCheck("Pytest", "pytest")
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Write error file if tests failed
|
|
347
|
-
if (hasError) {
|
|
348
|
-
writeFileSync(join(worktreePath, ".agent-error"), errorMessages)
|
|
349
|
-
// Remove .agent-done since code is broken
|
|
350
|
-
const doneFile = join(worktreePath, ".agent-done")
|
|
351
|
-
if (existsSync(doneFile)) {
|
|
352
|
-
unlinkSync(doneFile)
|
|
353
|
-
}
|
|
354
|
-
results += `\n---\n⚠️ Tests failed. Error details written to .agent-error. Use the fixer agent to resolve.`
|
|
355
|
-
} else {
|
|
356
|
-
results += `\n---\n✅ All checks passed!`
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return results
|
|
360
|
-
},
|
|
361
|
-
}),
|
|
335
|
+
mad_test: tool({
|
|
336
|
+
description: `Run tests/build/lint on a MAD worktree to verify the code is working.
|
|
337
|
+
Automatically detects the project type (Node.js, Python, Go, Rust) and runs appropriate checks.
|
|
338
|
+
Returns the results and creates an error file if tests fail.`,
|
|
339
|
+
args: {
|
|
340
|
+
worktree: tool.schema.string().describe("Worktree session name (e.g., 'feat-auth-login')"),
|
|
341
|
+
},
|
|
342
|
+
async execute(args, context) {
|
|
343
|
+
const gitRoot = getGitRoot()
|
|
344
|
+
const worktreePath = join(gitRoot, "worktrees", args.worktree)
|
|
345
|
+
|
|
346
|
+
if (!existsSync(worktreePath)) {
|
|
347
|
+
return getUpdateNotification() + `Worktree not found: ${worktreePath}`
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let results = getUpdateNotification() + `# Test Results for ${args.worktree}\n\n`
|
|
351
|
+
let hasError = false
|
|
352
|
+
let errorMessages = ""
|
|
353
|
+
|
|
354
|
+
// Helper to run a check
|
|
355
|
+
const doCheck = (label: string, cmd: string) => {
|
|
356
|
+
results += `## ${label}\n`
|
|
357
|
+
const result = runCommand(cmd, worktreePath)
|
|
358
|
+
if (result.success) {
|
|
359
|
+
results += `✅ Passed\n\`\`\`\n${result.output.slice(0, 500)}\n\`\`\`\n\n`
|
|
360
|
+
} else {
|
|
361
|
+
hasError = true
|
|
362
|
+
const output = result.error || "Unknown error"
|
|
363
|
+
results += `❌ Failed\n\`\`\`\n${output.slice(0, 1000)}\n\`\`\`\n\n`
|
|
364
|
+
errorMessages += `${label} FAILED:\n${output}\n\n`
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Detect project type and run checks
|
|
369
|
+
const packageJson = join(worktreePath, "package.json")
|
|
370
|
+
const goMod = join(worktreePath, "go.mod")
|
|
371
|
+
const cargoToml = join(worktreePath, "Cargo.toml")
|
|
372
|
+
const pyProject = join(worktreePath, "pyproject.toml")
|
|
373
|
+
const requirements = join(worktreePath, "requirements.txt")
|
|
374
|
+
|
|
375
|
+
if (existsSync(packageJson)) {
|
|
376
|
+
const pkg = JSON.parse(readFileSync(packageJson, "utf-8"))
|
|
377
|
+
if (pkg.scripts?.lint) doCheck("Lint", "npm run lint")
|
|
378
|
+
if (pkg.scripts?.build) doCheck("Build", "npm run build")
|
|
379
|
+
if (pkg.scripts?.test) doCheck("Test", "npm test")
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (existsSync(goMod)) {
|
|
383
|
+
doCheck("Go Build", "go build ./...")
|
|
384
|
+
doCheck("Go Test", "go test ./...")
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (existsSync(cargoToml)) {
|
|
388
|
+
doCheck("Cargo Check", "cargo check")
|
|
389
|
+
doCheck("Cargo Test", "cargo test")
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (existsSync(pyProject) || existsSync(requirements)) {
|
|
393
|
+
doCheck("Pytest", "pytest")
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Write error file if tests failed
|
|
397
|
+
if (hasError) {
|
|
398
|
+
writeFileSync(join(worktreePath, ".agent-error"), errorMessages)
|
|
399
|
+
// Remove .agent-done since code is broken
|
|
400
|
+
const doneFile = join(worktreePath, ".agent-done")
|
|
401
|
+
if (existsSync(doneFile)) {
|
|
402
|
+
unlinkSync(doneFile)
|
|
403
|
+
}
|
|
404
|
+
results += `\n---\n⚠️ Tests failed. Error details written to .agent-error. Use the fixer agent to resolve.`
|
|
405
|
+
} else {
|
|
406
|
+
results += `\n---\n✅ All checks passed!`
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return results
|
|
410
|
+
},
|
|
411
|
+
}),
|
|
362
412
|
|
|
363
413
|
/**
|
|
364
414
|
* Merge completed worktrees
|
|
365
415
|
*/
|
|
366
|
-
mad_merge: tool({
|
|
367
|
-
description: `Merge a completed worktree branch back into the current branch.
|
|
368
|
-
Only merges if the worktree is marked as done (.agent-done exists).
|
|
369
|
-
Handles merge conflicts by reporting them.`,
|
|
370
|
-
args: {
|
|
371
|
-
worktree: tool.schema.string().describe("Worktree session name to merge (e.g., 'feat-auth-login')"),
|
|
372
|
-
},
|
|
373
|
-
async execute(args, context) {
|
|
374
|
-
const gitRoot = getGitRoot()
|
|
375
|
-
const worktreePath = join(gitRoot, "worktrees", args.worktree)
|
|
376
|
-
const doneFile = join(worktreePath, ".agent-done")
|
|
377
|
-
const branch = args.worktree.replace(/-/g, "/")
|
|
378
|
-
|
|
379
|
-
if (!existsSync(worktreePath)) {
|
|
380
|
-
return `Worktree not found: ${worktreePath}`
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (!existsSync(doneFile)) {
|
|
384
|
-
return `Cannot merge: worktree ${args.worktree} is not marked as done. Complete the task first.`
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const result = runCommand(`git merge ${branch} --no-edit`, gitRoot)
|
|
388
|
-
if (result.success) {
|
|
389
|
-
return `✅ Successfully merged ${branch}!\n\n${result.output}`
|
|
390
|
-
} else {
|
|
391
|
-
const output = result.error || "Unknown error"
|
|
392
|
-
if (output.includes("CONFLICT")) {
|
|
393
|
-
return `⚠️ Merge conflict detected!\n\n${output}\n\nResolve conflicts manually or use the fixer agent.`
|
|
394
|
-
}
|
|
395
|
-
return `❌ Merge failed:\n${output}`
|
|
396
|
-
}
|
|
397
|
-
},
|
|
398
|
-
}),
|
|
416
|
+
mad_merge: tool({
|
|
417
|
+
description: `Merge a completed worktree branch back into the current branch.
|
|
418
|
+
Only merges if the worktree is marked as done (.agent-done exists).
|
|
419
|
+
Handles merge conflicts by reporting them.`,
|
|
420
|
+
args: {
|
|
421
|
+
worktree: tool.schema.string().describe("Worktree session name to merge (e.g., 'feat-auth-login')"),
|
|
422
|
+
},
|
|
423
|
+
async execute(args, context) {
|
|
424
|
+
const gitRoot = getGitRoot()
|
|
425
|
+
const worktreePath = join(gitRoot, "worktrees", args.worktree)
|
|
426
|
+
const doneFile = join(worktreePath, ".agent-done")
|
|
427
|
+
const branch = args.worktree.replace(/-/g, "/")
|
|
428
|
+
|
|
429
|
+
if (!existsSync(worktreePath)) {
|
|
430
|
+
return getUpdateNotification() + `Worktree not found: ${worktreePath}`
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!existsSync(doneFile)) {
|
|
434
|
+
return getUpdateNotification() + `Cannot merge: worktree ${args.worktree} is not marked as done. Complete the task first.`
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const result = runCommand(`git merge ${branch} --no-edit`, gitRoot)
|
|
438
|
+
if (result.success) {
|
|
439
|
+
return getUpdateNotification() + `✅ Successfully merged ${branch}!\n\n${result.output}`
|
|
440
|
+
} else {
|
|
441
|
+
const output = result.error || "Unknown error"
|
|
442
|
+
if (output.includes("CONFLICT")) {
|
|
443
|
+
return getUpdateNotification() + `⚠️ Merge conflict detected!\n\n${output}\n\nResolve conflicts manually or use the fixer agent.`
|
|
444
|
+
}
|
|
445
|
+
return getUpdateNotification() + `❌ Merge failed:\n${output}`
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
}),
|
|
399
449
|
|
|
400
450
|
/**
|
|
401
451
|
* Cleanup finished worktrees
|
|
@@ -413,19 +463,19 @@ Removes the worktree directory and prunes git worktree references.`,
|
|
|
413
463
|
const doneFile = join(worktreePath, ".agent-done")
|
|
414
464
|
|
|
415
465
|
if (!existsSync(worktreePath)) {
|
|
416
|
-
return `Worktree not found: ${worktreePath}`
|
|
466
|
+
return getUpdateNotification() + `Worktree not found: ${worktreePath}`
|
|
417
467
|
}
|
|
418
468
|
|
|
419
469
|
if (!args.force && !existsSync(doneFile)) {
|
|
420
|
-
return `Worktree ${args.worktree} is not marked as done. Use force=true to cleanup anyway.`
|
|
470
|
+
return getUpdateNotification() + `Worktree ${args.worktree} is not marked as done. Use force=true to cleanup anyway.`
|
|
421
471
|
}
|
|
422
472
|
|
|
423
473
|
try {
|
|
424
474
|
await $`git worktree remove ${worktreePath} --force`
|
|
425
475
|
await $`git worktree prune`
|
|
426
|
-
return `✅ Cleaned up worktree: ${args.worktree}`
|
|
476
|
+
return getUpdateNotification() + `✅ Cleaned up worktree: ${args.worktree}`
|
|
427
477
|
} catch (e: any) {
|
|
428
|
-
return `❌ Cleanup failed: ${e.message}`
|
|
478
|
+
return getUpdateNotification() + `❌ Cleanup failed: ${e.message}`
|
|
429
479
|
}
|
|
430
480
|
},
|
|
431
481
|
}),
|
|
@@ -445,14 +495,14 @@ Use this when you've finished implementing the task in a worktree.`,
|
|
|
445
495
|
const worktreePath = join(gitRoot, "worktrees", args.worktree)
|
|
446
496
|
|
|
447
497
|
if (!existsSync(worktreePath)) {
|
|
448
|
-
return `Worktree not found: ${worktreePath}`
|
|
498
|
+
return getUpdateNotification() + `Worktree not found: ${worktreePath}`
|
|
449
499
|
}
|
|
450
500
|
|
|
451
501
|
await $`echo ${args.summary} > ${join(worktreePath, ".agent-done")}`
|
|
452
502
|
// Remove error/blocked files
|
|
453
503
|
await $`rm -f ${join(worktreePath, ".agent-error")} ${join(worktreePath, ".agent-blocked")}`
|
|
454
504
|
|
|
455
|
-
return `✅ Marked ${args.worktree} as done: ${args.summary}`
|
|
505
|
+
return getUpdateNotification() + `✅ Marked ${args.worktree} as done: ${args.summary}`
|
|
456
506
|
},
|
|
457
507
|
}),
|
|
458
508
|
|
|
@@ -471,183 +521,214 @@ Use this when you cannot proceed due to missing information or dependencies.`,
|
|
|
471
521
|
const worktreePath = join(gitRoot, "worktrees", args.worktree)
|
|
472
522
|
|
|
473
523
|
if (!existsSync(worktreePath)) {
|
|
474
|
-
return `Worktree not found: ${worktreePath}`
|
|
524
|
+
return getUpdateNotification() + `Worktree not found: ${worktreePath}`
|
|
475
525
|
}
|
|
476
526
|
|
|
477
527
|
await $`echo ${args.reason} > ${join(worktreePath, ".agent-blocked")}`
|
|
478
528
|
|
|
479
|
-
return `🚫 Marked ${args.worktree} as blocked: ${args.reason}`
|
|
529
|
+
return getUpdateNotification() + `🚫 Marked ${args.worktree} as blocked: ${args.reason}`
|
|
530
|
+
},
|
|
531
|
+
}),
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Read task from a worktree
|
|
535
|
+
*/
|
|
536
|
+
mad_read_task: tool({
|
|
537
|
+
description: `Read the task description from a worktree's .agent-task file.
|
|
538
|
+
Use this to understand what needs to be done in a specific worktree.`,
|
|
539
|
+
args: {
|
|
540
|
+
worktree: tool.schema.string().describe("Worktree session name"),
|
|
541
|
+
},
|
|
542
|
+
async execute(args, context) {
|
|
543
|
+
const gitRoot = await getGitRoot()
|
|
544
|
+
const taskFile = join(gitRoot, "worktrees", args.worktree, ".agent-task")
|
|
545
|
+
|
|
546
|
+
if (!existsSync(taskFile)) {
|
|
547
|
+
return getUpdateNotification() + `Task file not found: ${taskFile}`
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return getUpdateNotification() + readFileSync(taskFile, "utf-8")
|
|
551
|
+
},
|
|
552
|
+
}),
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Log MAD orchestration events
|
|
556
|
+
*/
|
|
557
|
+
mad_log: tool({
|
|
558
|
+
description: `Log MAD orchestration events for debugging and monitoring.
|
|
559
|
+
Creates structured logs in .mad-logs.jsonl for tracking the workflow.`,
|
|
560
|
+
args: {
|
|
561
|
+
level: tool.schema.enum(["info", "warn", "error", "debug"]).describe("Log level"),
|
|
562
|
+
message: tool.schema.string().describe("Log message"),
|
|
563
|
+
context: tool.schema.object({}).optional().describe("Additional context data")
|
|
564
|
+
},
|
|
565
|
+
async execute(args, context) {
|
|
566
|
+
try {
|
|
567
|
+
await logEvent(args.level as "info" | "warn" | "error" | "debug", args.message, args.context)
|
|
568
|
+
return getUpdateNotification() + `📝 Logged [${args.level.toUpperCase()}]: ${args.message}`
|
|
569
|
+
} catch (e: any) {
|
|
570
|
+
return getUpdateNotification() + `⚠️ Failed to write log: ${e.message}`
|
|
571
|
+
}
|
|
480
572
|
},
|
|
481
573
|
}),
|
|
482
574
|
|
|
483
|
-
/**
|
|
484
|
-
*
|
|
485
|
-
*/
|
|
486
|
-
|
|
487
|
-
description: `
|
|
488
|
-
|
|
489
|
-
args: {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
`
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (blocked > 0) output += ` • Unblock ${blocked} blocked worktree(s)\n`
|
|
641
|
-
if (done > 0) output += ` • Ready to merge: ${worktrees.filter(w => w.status === "DONE").map(w => w.name).join(", ")}\n`
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
return output
|
|
645
|
-
} catch (e: any) {
|
|
646
|
-
return `❌ Error generating visualization: ${e.message}`
|
|
647
|
-
}
|
|
648
|
-
},
|
|
649
|
-
}),
|
|
650
|
-
},
|
|
575
|
+
/**
|
|
576
|
+
* Visualize MAD workflow with ASCII art
|
|
577
|
+
*/
|
|
578
|
+
mad_visualize: tool({
|
|
579
|
+
description: `Generate an ASCII art visualization of the MAD orchestration status.
|
|
580
|
+
Shows progress, worktree statuses, timeline, and statistics in a beautiful dashboard.`,
|
|
581
|
+
args: {},
|
|
582
|
+
async execute(args, context) {
|
|
583
|
+
try {
|
|
584
|
+
const gitRoot = await getGitRoot()
|
|
585
|
+
const worktreeDir = join(gitRoot, "worktrees")
|
|
586
|
+
|
|
587
|
+
if (!existsSync(worktreeDir)) {
|
|
588
|
+
return getUpdateNotification() + "No active MAD worktrees. Use mad_worktree_create to create one."
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const entries = readdirSync(worktreeDir)
|
|
592
|
+
if (entries.length === 0) {
|
|
593
|
+
return getUpdateNotification() + "No active MAD worktrees. Use mad_worktree_create to create one."
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
let total = 0, done = 0, blocked = 0, errors = 0, wip = 0
|
|
597
|
+
const worktrees: any[] = []
|
|
598
|
+
|
|
599
|
+
for (const entry of entries) {
|
|
600
|
+
const wpath = join(worktreeDir, entry)
|
|
601
|
+
if (!statSync(wpath).isDirectory()) continue
|
|
602
|
+
total++
|
|
603
|
+
|
|
604
|
+
const taskFile = join(wpath, ".agent-task")
|
|
605
|
+
const doneFile = join(wpath, ".agent-done")
|
|
606
|
+
const blockedFile = join(wpath, ".agent-blocked")
|
|
607
|
+
const errorFile = join(wpath, ".agent-error")
|
|
608
|
+
|
|
609
|
+
let status = "IN PROGRESS"
|
|
610
|
+
let icon = "⏳"
|
|
611
|
+
let detail = ""
|
|
612
|
+
|
|
613
|
+
if (existsSync(doneFile)) {
|
|
614
|
+
icon = "✅"
|
|
615
|
+
status = "DONE"
|
|
616
|
+
detail = readFileSync(doneFile, "utf-8").split("\n")[0]
|
|
617
|
+
done++
|
|
618
|
+
} else if (existsSync(blockedFile)) {
|
|
619
|
+
icon = "🚫"
|
|
620
|
+
status = "BLOCKED"
|
|
621
|
+
detail = readFileSync(blockedFile, "utf-8").split("\n")[0]
|
|
622
|
+
blocked++
|
|
623
|
+
} else if (existsSync(errorFile)) {
|
|
624
|
+
icon = "❌"
|
|
625
|
+
status = "ERROR"
|
|
626
|
+
detail = readFileSync(errorFile, "utf-8").split("\n")[0]
|
|
627
|
+
errors++
|
|
628
|
+
} else {
|
|
629
|
+
wip++
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const task = existsSync(taskFile)
|
|
633
|
+
? readFileSync(taskFile, "utf-8").split("\n").filter(l => !l.startsWith("#") && l.trim()).join(" ").slice(0, 50)
|
|
634
|
+
: "No task file"
|
|
635
|
+
|
|
636
|
+
// Get commit count
|
|
637
|
+
const branch = entry.replace(/-/g, "/")
|
|
638
|
+
let commits = "0"
|
|
639
|
+
try {
|
|
640
|
+
const baseBranch = await getCurrentBranch()
|
|
641
|
+
const result = await runCommand(`git -C "${wpath}" log --oneline ${baseBranch}..HEAD 2>/dev/null | wc -l`)
|
|
642
|
+
commits = result.output.trim() || "0"
|
|
643
|
+
} catch {}
|
|
644
|
+
|
|
645
|
+
worktrees.push({ name: entry, status, icon, detail, task, commits })
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Calculate progress
|
|
649
|
+
const progress = total > 0 ? Math.round((done / total) * 100) : 0
|
|
650
|
+
const progressBar = "█".repeat(Math.floor(progress / 5)) + "░".repeat(20 - Math.floor(progress / 5))
|
|
651
|
+
|
|
652
|
+
// Build visualization
|
|
653
|
+
let output = getUpdateNotification() + `
|
|
654
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
655
|
+
│ MAD ORCHESTRATION DASHBOARD │
|
|
656
|
+
└────────────────────────────────────────────────────────────────┘
|
|
657
|
+
|
|
658
|
+
📊 Progress: [${progressBar}] ${progress}% (${done}/${total} tasks complete)
|
|
659
|
+
|
|
660
|
+
┌─ Worktree Status ─────────────────────────────────────────────┐
|
|
661
|
+
│ │
|
|
662
|
+
`
|
|
663
|
+
|
|
664
|
+
for (const wt of worktrees) {
|
|
665
|
+
const statusPadded = wt.status.padEnd(15)
|
|
666
|
+
output += `│ ${wt.icon} ${wt.name.padEnd(35)} [${statusPadded}] │\n`
|
|
667
|
+
output += `│ └─ ${wt.commits} commits │ ${wt.task.padEnd(38)} │\n`
|
|
668
|
+
if (wt.detail) {
|
|
669
|
+
output += `│ └─ ${wt.detail.slice(0, 50).padEnd(50)} │\n`
|
|
670
|
+
}
|
|
671
|
+
output += `│ │\n`
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
output += `└────────────────────────────────────────────────────────────────┘
|
|
675
|
+
|
|
676
|
+
┌─ Statistics ──────────────────────────────────────────────────┐
|
|
677
|
+
│ │
|
|
678
|
+
│ Total Worktrees: ${total.toString().padEnd(40)} │
|
|
679
|
+
│ ✅ Completed: ${done} (${Math.round(done/total*100)}%)${' '.repeat(40 - done.toString().length - 7)} │
|
|
680
|
+
│ ⏳ In Progress: ${wip} (${Math.round(wip/total*100)}%)${' '.repeat(40 - wip.toString().length - 7)} │
|
|
681
|
+
│ 🚫 Blocked: ${blocked} (${Math.round(blocked/total*100)}%)${' '.repeat(40 - blocked.toString().length - 7)} │
|
|
682
|
+
│ ❌ Errors: ${errors} (${Math.round(errors/total*100)}%)${' '.repeat(40 - errors.toString().length - 7)} │
|
|
683
|
+
│ │
|
|
684
|
+
└────────────────────────────────────────────────────────────────┘
|
|
685
|
+
`
|
|
686
|
+
|
|
687
|
+
if (blocked > 0 || errors > 0) {
|
|
688
|
+
output += `\n💡 Next Actions:\n`
|
|
689
|
+
if (errors > 0) output += ` • Fix ${errors} errored worktree(s) (check .agent-error files)\n`
|
|
690
|
+
if (blocked > 0) output += ` • Unblock ${blocked} blocked worktree(s)\n`
|
|
691
|
+
if (done > 0) output += ` • Ready to merge: ${worktrees.filter(w => w.status === "DONE").map(w => w.name).join(", ")}\n`
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return output
|
|
695
|
+
} catch (e: any) {
|
|
696
|
+
return getUpdateNotification() + `❌ Error generating visualization: ${e.message}`
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
}),
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Check for opencode-mad updates
|
|
703
|
+
*/
|
|
704
|
+
mad_check_update: tool({
|
|
705
|
+
description: `Check if a newer version of opencode-mad is available on npm.
|
|
706
|
+
Returns the current version, latest version, and whether an update is available.`,
|
|
707
|
+
args: {},
|
|
708
|
+
async execute(args, context) {
|
|
709
|
+
try {
|
|
710
|
+
const updateInfo = await checkForUpdates()
|
|
711
|
+
|
|
712
|
+
if (updateInfo.hasUpdate) {
|
|
713
|
+
return getUpdateNotification() + `🔄 Update available!
|
|
714
|
+
|
|
715
|
+
Current version: ${updateInfo.current}
|
|
716
|
+
Latest version: ${updateInfo.latest}
|
|
717
|
+
|
|
718
|
+
To update, run:
|
|
719
|
+
npx opencode-mad install -g`
|
|
720
|
+
} else {
|
|
721
|
+
return getUpdateNotification() + `✅ You're up to date!
|
|
722
|
+
|
|
723
|
+
Current version: ${updateInfo.current}
|
|
724
|
+
Latest version: ${updateInfo.latest}`
|
|
725
|
+
}
|
|
726
|
+
} catch (e: any) {
|
|
727
|
+
return getUpdateNotification() + `❌ Failed to check for updates: ${e.message}`
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
}),
|
|
731
|
+
},
|
|
651
732
|
|
|
652
733
|
// Event hooks
|
|
653
734
|
event: async ({ event }) => {
|