opencode-mad 1.0.0 → 1.0.2
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/agents/mad-analyste.md +7 -109
- package/agents/mad-architecte.md +15 -92
- package/agents/mad-developer.md +13 -39
- package/agents/mad-fixer.md +7 -18
- package/agents/mad-merger.md +7 -18
- package/agents/mad-planner.md +76 -90
- package/agents/mad-reviewer.md +14 -57
- package/agents/mad-security.md +66 -351
- package/agents/mad-tester.md +11 -57
- package/agents/orchestrator.md +7 -15
- package/package.json +1 -1
- package/plugins/mad-plugin.ts +165 -656
- package/skills/mad-workflow/SKILL.md +79 -205
package/plugins/mad-plugin.ts
CHANGED
|
@@ -46,10 +46,6 @@ function matchGlob(path: string, pattern: string): boolean {
|
|
|
46
46
|
// Current version of opencode-mad
|
|
47
47
|
const CURRENT_VERSION = "1.0.0"
|
|
48
48
|
|
|
49
|
-
// Update notification state (shown only once per session)
|
|
50
|
-
let updateNotificationShown = false
|
|
51
|
-
let pendingUpdateMessage: string | null = null
|
|
52
|
-
|
|
53
49
|
export const MADPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
|
|
54
50
|
|
|
55
51
|
// Use the directory provided by OpenCode, fallback to process.cwd() for backwards compatibility
|
|
@@ -138,28 +134,6 @@ export const MADPlugin: Plugin = async ({ project, client, $, directory, worktre
|
|
|
138
134
|
return { hasUpdate: false, current: CURRENT_VERSION, latest: CURRENT_VERSION }
|
|
139
135
|
}
|
|
140
136
|
|
|
141
|
-
// Check for updates at plugin initialization and store message for first tool response
|
|
142
|
-
try {
|
|
143
|
-
const updateInfo = await checkForUpdates()
|
|
144
|
-
if (updateInfo.hasUpdate) {
|
|
145
|
-
pendingUpdateMessage = `🔄 **Update available!** opencode-mad ${updateInfo.current} → ${updateInfo.latest}\n Run: \`npx opencode-mad install -g\`\n\n`
|
|
146
|
-
logEvent("info", "Update available", { current: updateInfo.current, latest: updateInfo.latest })
|
|
147
|
-
}
|
|
148
|
-
} catch (e) {
|
|
149
|
-
// Silent fail - don't break plugin initialization
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Helper to get update notification (returns message only once)
|
|
154
|
-
*/
|
|
155
|
-
const getUpdateNotification = (): string => {
|
|
156
|
-
if (pendingUpdateMessage && !updateNotificationShown) {
|
|
157
|
-
updateNotificationShown = true
|
|
158
|
-
return pendingUpdateMessage
|
|
159
|
-
}
|
|
160
|
-
return ""
|
|
161
|
-
}
|
|
162
|
-
|
|
163
137
|
return {
|
|
164
138
|
// Custom tools for MAD workflow
|
|
165
139
|
tool: {
|
|
@@ -182,12 +156,12 @@ Each worktree has its own branch and working directory.`,
|
|
|
182
156
|
// Validate inputs
|
|
183
157
|
if (!branch || branch.trim() === "") {
|
|
184
158
|
logEvent("error", "mad_worktree_create failed: empty branch name")
|
|
185
|
-
return
|
|
159
|
+
return "❌ Branch name required"
|
|
186
160
|
}
|
|
187
161
|
|
|
188
162
|
if (!task || task.trim() === "") {
|
|
189
163
|
logEvent("error", "mad_worktree_create failed: empty task description")
|
|
190
|
-
return
|
|
164
|
+
return "❌ Task description required"
|
|
191
165
|
}
|
|
192
166
|
|
|
193
167
|
const gitRoot = getGitRoot()
|
|
@@ -199,7 +173,7 @@ Each worktree has its own branch and working directory.`,
|
|
|
199
173
|
// Check if worktree already exists
|
|
200
174
|
if (existsSync(worktreePath)) {
|
|
201
175
|
logEvent("warn", "Worktree already exists", { branch, path: worktreePath })
|
|
202
|
-
return
|
|
176
|
+
return `⚠️ Worktree exists: ${sessionName}`
|
|
203
177
|
}
|
|
204
178
|
|
|
205
179
|
logEvent("info", "Creating worktree", { branch, baseBranch })
|
|
@@ -228,7 +202,7 @@ worktrees/
|
|
|
228
202
|
mkdirSync(worktreeDir, { recursive: true })
|
|
229
203
|
} catch (e: any) {
|
|
230
204
|
logEvent("error", "Failed to create worktree directory", { error: e.message })
|
|
231
|
-
return
|
|
205
|
+
return `❌ mkdir failed: ${e.message}`
|
|
232
206
|
}
|
|
233
207
|
|
|
234
208
|
// Check if branch exists
|
|
@@ -247,7 +221,7 @@ worktrees/
|
|
|
247
221
|
command: worktreeCmd,
|
|
248
222
|
error: worktreeResult.error
|
|
249
223
|
})
|
|
250
|
-
return
|
|
224
|
+
return `❌ git worktree failed: ${worktreeResult.error}`
|
|
251
225
|
}
|
|
252
226
|
|
|
253
227
|
// Write task file using Node.js
|
|
@@ -266,16 +240,10 @@ ${task}
|
|
|
266
240
|
|
|
267
241
|
logEvent("info", "Worktree created successfully", { branch, path: worktreePath })
|
|
268
242
|
|
|
269
|
-
return
|
|
270
|
-
- Path: ${worktreePath}
|
|
271
|
-
- Branch: ${branch}
|
|
272
|
-
- Base: ${baseBranch}
|
|
273
|
-
- Task: ${task.substring(0, 100)}${task.length > 100 ? "..." : ""}
|
|
274
|
-
|
|
275
|
-
The developer subagent can now work in this worktree using the Task tool.`
|
|
243
|
+
return `✅ Worktree: ${sessionName} (${branch})`
|
|
276
244
|
} catch (e: any) {
|
|
277
245
|
logEvent("error", "mad_worktree_create exception", { error: e.message, stack: e.stack })
|
|
278
|
-
return
|
|
246
|
+
return `❌ Error: ${e.message}`
|
|
279
247
|
}
|
|
280
248
|
},
|
|
281
249
|
}),
|
|
@@ -292,77 +260,36 @@ Shows which tasks are done, in progress, blocked, or have errors.`,
|
|
|
292
260
|
const worktreeDir = join(gitRoot, "worktrees")
|
|
293
261
|
|
|
294
262
|
if (!existsSync(worktreeDir)) {
|
|
295
|
-
return
|
|
263
|
+
return "No worktrees"
|
|
296
264
|
}
|
|
297
265
|
|
|
298
266
|
const entries = readdirSync(worktreeDir)
|
|
299
267
|
if (entries.length === 0) {
|
|
300
|
-
return
|
|
268
|
+
return "No worktrees"
|
|
301
269
|
}
|
|
302
270
|
|
|
303
|
-
let status = getUpdateNotification() + "# MAD Status Dashboard\n\n"
|
|
304
271
|
let total = 0, done = 0, blocked = 0, errors = 0, wip = 0
|
|
272
|
+
let rows: string[] = []
|
|
305
273
|
|
|
306
274
|
for (const entry of entries) {
|
|
307
275
|
const wpath = join(worktreeDir, entry)
|
|
308
276
|
if (!statSync(wpath).isDirectory()) continue
|
|
309
277
|
total++
|
|
310
278
|
|
|
311
|
-
const taskFile = join(wpath, ".agent-task")
|
|
312
279
|
const doneFile = join(wpath, ".agent-done")
|
|
313
280
|
const blockedFile = join(wpath, ".agent-blocked")
|
|
314
281
|
const errorFile = join(wpath, ".agent-error")
|
|
315
282
|
|
|
316
|
-
let
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
statusIcon = "✅"
|
|
322
|
-
statusText = "DONE"
|
|
323
|
-
detail = readFileSync(doneFile, "utf-8").split("\n")[0]
|
|
324
|
-
done++
|
|
325
|
-
} else if (existsSync(blockedFile)) {
|
|
326
|
-
statusIcon = "🚫"
|
|
327
|
-
statusText = "BLOCKED"
|
|
328
|
-
detail = readFileSync(blockedFile, "utf-8").split("\n")[0]
|
|
329
|
-
blocked++
|
|
330
|
-
} else if (existsSync(errorFile)) {
|
|
331
|
-
statusIcon = "❌"
|
|
332
|
-
statusText = "ERROR"
|
|
333
|
-
detail = readFileSync(errorFile, "utf-8").split("\n")[0]
|
|
334
|
-
errors++
|
|
335
|
-
} else {
|
|
336
|
-
wip++
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Get task description
|
|
340
|
-
const task = existsSync(taskFile)
|
|
341
|
-
? readFileSync(taskFile, "utf-8").split("\n").filter(l => !l.startsWith("#") && l.trim()).join(" ").slice(0, 60)
|
|
342
|
-
: "No task file"
|
|
283
|
+
let icon = "⏳"
|
|
284
|
+
if (existsSync(doneFile)) { icon = "✅"; done++ }
|
|
285
|
+
else if (existsSync(blockedFile)) { icon = "🚫"; blocked++ }
|
|
286
|
+
else if (existsSync(errorFile)) { icon = "❌"; errors++ }
|
|
287
|
+
else { wip++ }
|
|
343
288
|
|
|
344
|
-
|
|
345
|
-
let commits = "0"
|
|
346
|
-
try {
|
|
347
|
-
const baseBranch = getCurrentBranch()
|
|
348
|
-
const result = runCommand(`git log --oneline ${baseBranch}..HEAD`, wpath)
|
|
349
|
-
if (result.success) {
|
|
350
|
-
commits = result.output.split("\n").filter(l => l.trim()).length.toString()
|
|
351
|
-
}
|
|
352
|
-
} catch {}
|
|
353
|
-
|
|
354
|
-
status += `## ${statusIcon} ${entry}\n`
|
|
355
|
-
status += `- **Status:** ${statusText}\n`
|
|
356
|
-
status += `- **Task:** ${task}\n`
|
|
357
|
-
status += `- **Commits:** ${commits}\n`
|
|
358
|
-
if (detail) status += `- **Detail:** ${detail}\n`
|
|
359
|
-
status += `\n`
|
|
289
|
+
rows.push(`${icon} ${entry}`)
|
|
360
290
|
}
|
|
361
291
|
|
|
362
|
-
|
|
363
|
-
status += `**Total:** ${total} | **Done:** ${done} | **WIP:** ${wip} | **Blocked:** ${blocked} | **Errors:** ${errors}\n`
|
|
364
|
-
|
|
365
|
-
return status
|
|
292
|
+
return `${rows.join("\n")}\n---\nTotal:${total} Done:${done} WIP:${wip} Blocked:${blocked} Err:${errors}`
|
|
366
293
|
},
|
|
367
294
|
}),
|
|
368
295
|
|
|
@@ -381,28 +308,23 @@ Returns the results and creates an error file if tests fail.`,
|
|
|
381
308
|
const worktreePath = join(gitRoot, "worktrees", args.worktree)
|
|
382
309
|
|
|
383
310
|
if (!existsSync(worktreePath)) {
|
|
384
|
-
return
|
|
311
|
+
return `❌ Not found: ${args.worktree}`
|
|
385
312
|
}
|
|
386
313
|
|
|
387
|
-
let
|
|
388
|
-
let
|
|
314
|
+
let passed: string[] = []
|
|
315
|
+
let failed: string[] = []
|
|
389
316
|
let errorMessages = ""
|
|
390
317
|
|
|
391
|
-
// Helper to run a check
|
|
392
318
|
const doCheck = (label: string, cmd: string) => {
|
|
393
|
-
results += `## ${label}\n`
|
|
394
319
|
const result = runCommand(cmd, worktreePath)
|
|
395
320
|
if (result.success) {
|
|
396
|
-
|
|
321
|
+
passed.push(label)
|
|
397
322
|
} else {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
results += `❌ Failed\n\`\`\`\n${output.slice(0, 1000)}\n\`\`\`\n\n`
|
|
401
|
-
errorMessages += `${label} FAILED:\n${output}\n\n`
|
|
323
|
+
failed.push(label)
|
|
324
|
+
errorMessages += `${label}: ${result.error?.slice(0, 200)}\n`
|
|
402
325
|
}
|
|
403
326
|
}
|
|
404
327
|
|
|
405
|
-
// Detect project type and run checks
|
|
406
328
|
const packageJson = join(worktreePath, "package.json")
|
|
407
329
|
const goMod = join(worktreePath, "go.mod")
|
|
408
330
|
const cargoToml = join(worktreePath, "Cargo.toml")
|
|
@@ -411,39 +333,29 @@ Returns the results and creates an error file if tests fail.`,
|
|
|
411
333
|
|
|
412
334
|
if (existsSync(packageJson)) {
|
|
413
335
|
const pkg = JSON.parse(readFileSync(packageJson, "utf-8"))
|
|
414
|
-
if (pkg.scripts?.lint) doCheck("
|
|
415
|
-
if (pkg.scripts?.build) doCheck("
|
|
416
|
-
if (pkg.scripts?.test) doCheck("
|
|
336
|
+
if (pkg.scripts?.lint) doCheck("lint", "npm run lint")
|
|
337
|
+
if (pkg.scripts?.build) doCheck("build", "npm run build")
|
|
338
|
+
if (pkg.scripts?.test) doCheck("test", "npm test")
|
|
417
339
|
}
|
|
418
|
-
|
|
419
340
|
if (existsSync(goMod)) {
|
|
420
|
-
doCheck("
|
|
421
|
-
doCheck("
|
|
341
|
+
doCheck("go-build", "go build ./...")
|
|
342
|
+
doCheck("go-test", "go test ./...")
|
|
422
343
|
}
|
|
423
|
-
|
|
424
344
|
if (existsSync(cargoToml)) {
|
|
425
|
-
doCheck("
|
|
426
|
-
doCheck("
|
|
345
|
+
doCheck("cargo-check", "cargo check")
|
|
346
|
+
doCheck("cargo-test", "cargo test")
|
|
427
347
|
}
|
|
428
|
-
|
|
429
348
|
if (existsSync(pyProject) || existsSync(requirements)) {
|
|
430
|
-
doCheck("
|
|
349
|
+
doCheck("pytest", "pytest")
|
|
431
350
|
}
|
|
432
351
|
|
|
433
|
-
|
|
434
|
-
if (hasError) {
|
|
352
|
+
if (failed.length > 0) {
|
|
435
353
|
writeFileSync(join(worktreePath, ".agent-error"), errorMessages)
|
|
436
|
-
// Remove .agent-done since code is broken
|
|
437
354
|
const doneFile = join(worktreePath, ".agent-done")
|
|
438
|
-
if (existsSync(doneFile))
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
results += `\n---\n⚠️ Tests failed. Error details written to .agent-error. Use the fixer agent to resolve.`
|
|
442
|
-
} else {
|
|
443
|
-
results += `\n---\n✅ All checks passed!`
|
|
355
|
+
if (existsSync(doneFile)) unlinkSync(doneFile)
|
|
356
|
+
return `❌ Failed: ${failed.join(", ")}${passed.length ? ` | ✅ ${passed.join(", ")}` : ""}`
|
|
444
357
|
}
|
|
445
|
-
|
|
446
|
-
return results
|
|
358
|
+
return `✅ All passed: ${passed.join(", ")}`
|
|
447
359
|
},
|
|
448
360
|
}),
|
|
449
361
|
|
|
@@ -464,22 +376,22 @@ Handles merge conflicts by reporting them.`,
|
|
|
464
376
|
const branch = args.worktree
|
|
465
377
|
|
|
466
378
|
if (!existsSync(worktreePath)) {
|
|
467
|
-
return
|
|
379
|
+
return `❌ Not found: ${args.worktree}`
|
|
468
380
|
}
|
|
469
381
|
|
|
470
382
|
if (!existsSync(doneFile)) {
|
|
471
|
-
return
|
|
383
|
+
return `❌ Not done: ${args.worktree}`
|
|
472
384
|
}
|
|
473
385
|
|
|
474
386
|
const result = runCommand(`git merge --no-ff ${branch} --no-edit`, gitRoot)
|
|
475
387
|
if (result.success) {
|
|
476
|
-
return
|
|
388
|
+
return `✅ Merged: ${branch}`
|
|
477
389
|
} else {
|
|
478
|
-
const output = result.error || "
|
|
390
|
+
const output = result.error || ""
|
|
479
391
|
if (output.includes("CONFLICT")) {
|
|
480
|
-
return
|
|
392
|
+
return `⚠️ Conflict in ${branch}`
|
|
481
393
|
}
|
|
482
|
-
return
|
|
394
|
+
return `❌ Merge failed: ${output.slice(0, 100)}`
|
|
483
395
|
}
|
|
484
396
|
},
|
|
485
397
|
}),
|
|
@@ -500,19 +412,19 @@ Removes the worktree directory and prunes git worktree references.`,
|
|
|
500
412
|
const doneFile = join(worktreePath, ".agent-done")
|
|
501
413
|
|
|
502
414
|
if (!existsSync(worktreePath)) {
|
|
503
|
-
return
|
|
415
|
+
return `❌ Not found: ${args.worktree}`
|
|
504
416
|
}
|
|
505
417
|
|
|
506
418
|
if (!args.force && !existsSync(doneFile)) {
|
|
507
|
-
return
|
|
419
|
+
return `❌ Not done (use force=true)`
|
|
508
420
|
}
|
|
509
421
|
|
|
510
422
|
try {
|
|
511
423
|
await $`git worktree remove ${worktreePath} --force`
|
|
512
424
|
await $`git worktree prune`
|
|
513
|
-
return
|
|
425
|
+
return `✅ Cleaned: ${args.worktree}`
|
|
514
426
|
} catch (e: any) {
|
|
515
|
-
return
|
|
427
|
+
return `❌ ${e.message}`
|
|
516
428
|
}
|
|
517
429
|
},
|
|
518
430
|
}),
|
|
@@ -532,14 +444,13 @@ Use this when you've finished implementing the task in a worktree.`,
|
|
|
532
444
|
const worktreePath = join(gitRoot, "worktrees", args.worktree)
|
|
533
445
|
|
|
534
446
|
if (!existsSync(worktreePath)) {
|
|
535
|
-
return
|
|
447
|
+
return `❌ Not found: ${args.worktree}`
|
|
536
448
|
}
|
|
537
449
|
|
|
538
450
|
await $`echo ${args.summary} > ${join(worktreePath, ".agent-done")}`
|
|
539
|
-
// Remove error/blocked files
|
|
540
451
|
await $`rm -f ${join(worktreePath, ".agent-error")} ${join(worktreePath, ".agent-blocked")}`
|
|
541
452
|
|
|
542
|
-
return
|
|
453
|
+
return `✅ Done: ${args.worktree}`
|
|
543
454
|
},
|
|
544
455
|
}),
|
|
545
456
|
|
|
@@ -558,12 +469,12 @@ Use this when you cannot proceed due to missing information or dependencies.`,
|
|
|
558
469
|
const worktreePath = join(gitRoot, "worktrees", args.worktree)
|
|
559
470
|
|
|
560
471
|
if (!existsSync(worktreePath)) {
|
|
561
|
-
return
|
|
472
|
+
return `❌ Not found: ${args.worktree}`
|
|
562
473
|
}
|
|
563
474
|
|
|
564
475
|
await $`echo ${args.reason} > ${join(worktreePath, ".agent-blocked")}`
|
|
565
476
|
|
|
566
|
-
return
|
|
477
|
+
return `🚫 Blocked: ${args.worktree}`
|
|
567
478
|
},
|
|
568
479
|
}),
|
|
569
480
|
|
|
@@ -581,10 +492,10 @@ Use this to understand what needs to be done in a specific worktree.`,
|
|
|
581
492
|
const taskFile = join(gitRoot, "worktrees", args.worktree, ".agent-task")
|
|
582
493
|
|
|
583
494
|
if (!existsSync(taskFile)) {
|
|
584
|
-
return
|
|
495
|
+
return `❌ No task: ${args.worktree}`
|
|
585
496
|
}
|
|
586
497
|
|
|
587
|
-
return
|
|
498
|
+
return readFileSync(taskFile, "utf-8")
|
|
588
499
|
},
|
|
589
500
|
}),
|
|
590
501
|
|
|
@@ -602,9 +513,9 @@ Creates structured logs in .mad-logs.jsonl for tracking the workflow.`,
|
|
|
602
513
|
async execute(args, context) {
|
|
603
514
|
try {
|
|
604
515
|
await logEvent(args.level as "info" | "warn" | "error" | "debug", args.message, args.context)
|
|
605
|
-
return
|
|
516
|
+
return `✅ Logged`
|
|
606
517
|
} catch (e: any) {
|
|
607
|
-
return
|
|
518
|
+
return `❌ ${e.message}`
|
|
608
519
|
}
|
|
609
520
|
},
|
|
610
521
|
}),
|
|
@@ -613,8 +524,8 @@ Creates structured logs in .mad-logs.jsonl for tracking the workflow.`,
|
|
|
613
524
|
* Visualize MAD workflow with ASCII art
|
|
614
525
|
*/
|
|
615
526
|
mad_visualize: tool({
|
|
616
|
-
description: `Generate
|
|
617
|
-
Shows progress
|
|
527
|
+
description: `Generate a concise visualization of the MAD orchestration status.
|
|
528
|
+
Shows progress and worktree statuses.`,
|
|
618
529
|
args: {},
|
|
619
530
|
async execute(args, context) {
|
|
620
531
|
try {
|
|
@@ -622,115 +533,40 @@ Shows progress, worktree statuses, timeline, and statistics in a beautiful dashb
|
|
|
622
533
|
const worktreeDir = join(gitRoot, "worktrees")
|
|
623
534
|
|
|
624
535
|
if (!existsSync(worktreeDir)) {
|
|
625
|
-
return
|
|
536
|
+
return "No worktrees"
|
|
626
537
|
}
|
|
627
538
|
|
|
628
539
|
const entries = readdirSync(worktreeDir)
|
|
629
540
|
if (entries.length === 0) {
|
|
630
|
-
return
|
|
541
|
+
return "No worktrees"
|
|
631
542
|
}
|
|
632
543
|
|
|
633
544
|
let total = 0, done = 0, blocked = 0, errors = 0, wip = 0
|
|
634
|
-
|
|
545
|
+
let rows: string[] = []
|
|
635
546
|
|
|
636
547
|
for (const entry of entries) {
|
|
637
548
|
const wpath = join(worktreeDir, entry)
|
|
638
549
|
if (!statSync(wpath).isDirectory()) continue
|
|
639
550
|
total++
|
|
640
551
|
|
|
641
|
-
const taskFile = join(wpath, ".agent-task")
|
|
642
552
|
const doneFile = join(wpath, ".agent-done")
|
|
643
553
|
const blockedFile = join(wpath, ".agent-blocked")
|
|
644
554
|
const errorFile = join(wpath, ".agent-error")
|
|
645
555
|
|
|
646
|
-
let status = "IN PROGRESS"
|
|
647
556
|
let icon = "⏳"
|
|
648
|
-
let
|
|
649
|
-
|
|
650
|
-
if (existsSync(
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
detail = readFileSync(doneFile, "utf-8").split("\n")[0]
|
|
654
|
-
done++
|
|
655
|
-
} else if (existsSync(blockedFile)) {
|
|
656
|
-
icon = "🚫"
|
|
657
|
-
status = "BLOCKED"
|
|
658
|
-
detail = readFileSync(blockedFile, "utf-8").split("\n")[0]
|
|
659
|
-
blocked++
|
|
660
|
-
} else if (existsSync(errorFile)) {
|
|
661
|
-
icon = "❌"
|
|
662
|
-
status = "ERROR"
|
|
663
|
-
detail = readFileSync(errorFile, "utf-8").split("\n")[0]
|
|
664
|
-
errors++
|
|
665
|
-
} else {
|
|
666
|
-
wip++
|
|
667
|
-
}
|
|
557
|
+
let status = "wip"
|
|
558
|
+
if (existsSync(doneFile)) { icon = "✅"; status = "done"; done++ }
|
|
559
|
+
else if (existsSync(blockedFile)) { icon = "🚫"; status = "blocked"; blocked++ }
|
|
560
|
+
else if (existsSync(errorFile)) { icon = "❌"; status = "error"; errors++ }
|
|
561
|
+
else { wip++ }
|
|
668
562
|
|
|
669
|
-
|
|
670
|
-
? readFileSync(taskFile, "utf-8").split("\n").filter(l => !l.startsWith("#") && l.trim()).join(" ").slice(0, 50)
|
|
671
|
-
: "No task file"
|
|
672
|
-
|
|
673
|
-
// Get commit count
|
|
674
|
-
const branch = entry.replace(/-/g, "/")
|
|
675
|
-
let commits = "0"
|
|
676
|
-
try {
|
|
677
|
-
const baseBranch = await getCurrentBranch()
|
|
678
|
-
const result = await runCommand(`git -C "${wpath}" log --oneline ${baseBranch}..HEAD 2>/dev/null | wc -l`)
|
|
679
|
-
commits = result.output.trim() || "0"
|
|
680
|
-
} catch {}
|
|
681
|
-
|
|
682
|
-
worktrees.push({ name: entry, status, icon, detail, task, commits })
|
|
563
|
+
rows.push(`${icon} ${entry} (${status})`)
|
|
683
564
|
}
|
|
684
565
|
|
|
685
|
-
// Calculate progress
|
|
686
566
|
const progress = total > 0 ? Math.round((done / total) * 100) : 0
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
// Build visualization
|
|
690
|
-
let output = getUpdateNotification() + `
|
|
691
|
-
┌────────────────────────────────────────────────────────────────┐
|
|
692
|
-
│ MAD ORCHESTRATION DASHBOARD │
|
|
693
|
-
└────────────────────────────────────────────────────────────────┘
|
|
694
|
-
|
|
695
|
-
📊 Progress: [${progressBar}] ${progress}% (${done}/${total} tasks complete)
|
|
696
|
-
|
|
697
|
-
┌─ Worktree Status ─────────────────────────────────────────────┐
|
|
698
|
-
│ │
|
|
699
|
-
`
|
|
700
|
-
|
|
701
|
-
for (const wt of worktrees) {
|
|
702
|
-
const statusPadded = wt.status.padEnd(15)
|
|
703
|
-
output += `│ ${wt.icon} ${wt.name.padEnd(35)} [${statusPadded}] │\n`
|
|
704
|
-
output += `│ └─ ${wt.commits} commits │ ${wt.task.padEnd(38)} │\n`
|
|
705
|
-
if (wt.detail) {
|
|
706
|
-
output += `│ └─ ${wt.detail.slice(0, 50).padEnd(50)} │\n`
|
|
707
|
-
}
|
|
708
|
-
output += `│ │\n`
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
output += `└────────────────────────────────────────────────────────────────┘
|
|
712
|
-
|
|
713
|
-
┌─ Statistics ──────────────────────────────────────────────────┐
|
|
714
|
-
│ │
|
|
715
|
-
│ Total Worktrees: ${total.toString().padEnd(40)} │
|
|
716
|
-
│ ✅ Completed: ${done} (${Math.round(done/total*100)}%)${' '.repeat(40 - done.toString().length - 7)} │
|
|
717
|
-
│ ⏳ In Progress: ${wip} (${Math.round(wip/total*100)}%)${' '.repeat(40 - wip.toString().length - 7)} │
|
|
718
|
-
│ 🚫 Blocked: ${blocked} (${Math.round(blocked/total*100)}%)${' '.repeat(40 - blocked.toString().length - 7)} │
|
|
719
|
-
│ ❌ Errors: ${errors} (${Math.round(errors/total*100)}%)${' '.repeat(40 - errors.toString().length - 7)} │
|
|
720
|
-
│ │
|
|
721
|
-
└────────────────────────────────────────────────────────────────┘
|
|
722
|
-
`
|
|
723
|
-
|
|
724
|
-
if (blocked > 0 || errors > 0) {
|
|
725
|
-
output += `\n💡 Next Actions:\n`
|
|
726
|
-
if (errors > 0) output += ` • Fix ${errors} errored worktree(s) (check .agent-error files)\n`
|
|
727
|
-
if (blocked > 0) output += ` • Unblock ${blocked} blocked worktree(s)\n`
|
|
728
|
-
if (done > 0) output += ` • Ready to merge: ${worktrees.filter(w => w.status === "DONE").map(w => w.name).join(", ")}\n`
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
return output
|
|
567
|
+
return `Progress: ${progress}% (${done}/${total})\n${rows.join("\n")}`
|
|
732
568
|
} catch (e: any) {
|
|
733
|
-
return
|
|
569
|
+
return `❌ ${e.message}`
|
|
734
570
|
}
|
|
735
571
|
},
|
|
736
572
|
}),
|
|
@@ -745,23 +581,12 @@ Returns the current version, latest version, and whether an update is available.
|
|
|
745
581
|
async execute(args, context) {
|
|
746
582
|
try {
|
|
747
583
|
const updateInfo = await checkForUpdates()
|
|
748
|
-
|
|
749
584
|
if (updateInfo.hasUpdate) {
|
|
750
|
-
return
|
|
751
|
-
|
|
752
|
-
Current version: ${updateInfo.current}
|
|
753
|
-
Latest version: ${updateInfo.latest}
|
|
754
|
-
|
|
755
|
-
To update, run:
|
|
756
|
-
npx opencode-mad install -g`
|
|
757
|
-
} else {
|
|
758
|
-
return getUpdateNotification() + `✅ You're up to date!
|
|
759
|
-
|
|
760
|
-
Current version: ${updateInfo.current}
|
|
761
|
-
Latest version: ${updateInfo.latest}`
|
|
585
|
+
return `🔄 Update: ${updateInfo.current} → ${updateInfo.latest}`
|
|
762
586
|
}
|
|
587
|
+
return `✅ Up to date: ${updateInfo.current}`
|
|
763
588
|
} catch (e: any) {
|
|
764
|
-
return
|
|
589
|
+
return `❌ ${e.message}`
|
|
765
590
|
}
|
|
766
591
|
},
|
|
767
592
|
}),
|
|
@@ -780,108 +605,45 @@ If CI fails, returns error details for the orchestrator to spawn a fixer.`,
|
|
|
780
605
|
async execute(args, context) {
|
|
781
606
|
try {
|
|
782
607
|
const gitRoot = getGitRoot()
|
|
783
|
-
let report = getUpdateNotification() + "# Push & CI Watch\n\n"
|
|
784
608
|
|
|
785
|
-
// 1. Check if we have a remote
|
|
786
609
|
const remoteResult = runCommand("git remote get-url origin", gitRoot)
|
|
787
610
|
if (!remoteResult.success) {
|
|
788
|
-
return
|
|
611
|
+
return "⚠️ No remote configured"
|
|
789
612
|
}
|
|
790
613
|
|
|
791
|
-
// 2. Get current branch
|
|
792
614
|
const branch = getCurrentBranch()
|
|
793
|
-
report += `📍 Branch: \`${branch}\`\n\n`
|
|
794
|
-
|
|
795
|
-
// 3. Check if upstream exists, if not set it
|
|
796
615
|
const upstreamResult = runCommand(`git rev-parse --abbrev-ref ${branch}@{upstream}`, gitRoot)
|
|
797
|
-
|
|
798
|
-
// 4. Push
|
|
799
|
-
report += "## 🚀 Pushing to remote...\n"
|
|
800
|
-
const pushCmd = upstreamResult.success
|
|
801
|
-
? "git push"
|
|
802
|
-
: `git push -u origin ${branch}`
|
|
616
|
+
const pushCmd = upstreamResult.success ? "git push" : `git push -u origin ${branch}`
|
|
803
617
|
|
|
804
618
|
const pushResult = runCommand(pushCmd, gitRoot)
|
|
805
619
|
if (!pushResult.success) {
|
|
806
|
-
return
|
|
620
|
+
return `❌ Push failed: ${pushResult.error?.slice(0, 100)}`
|
|
807
621
|
}
|
|
808
|
-
report += "✅ Push successful!\n\n"
|
|
809
622
|
|
|
810
|
-
// 5. Check if gh CLI is available
|
|
811
623
|
const ghCheck = runCommand("gh --version", gitRoot)
|
|
812
624
|
if (!ghCheck.success) {
|
|
813
|
-
return
|
|
625
|
+
return "✅ Pushed (no gh CLI for CI)"
|
|
814
626
|
}
|
|
815
627
|
|
|
816
|
-
// 6. Check for running/pending workflow runs
|
|
817
|
-
report += "## 🔍 Checking for CI workflows...\n"
|
|
818
628
|
const runsResult = runCommand(
|
|
819
|
-
`gh run list --branch ${branch} --limit 1 --json databaseId,status,conclusion,name
|
|
629
|
+
`gh run list --branch ${branch} --limit 1 --json databaseId,status,conclusion,name`,
|
|
820
630
|
gitRoot
|
|
821
631
|
)
|
|
822
632
|
|
|
823
|
-
if (!runsResult.success) {
|
|
824
|
-
return report + "⚠️ Could not check CI status. You may not be authenticated with `gh auth login`."
|
|
825
|
-
}
|
|
826
|
-
|
|
827
633
|
let runs: any[] = []
|
|
828
|
-
try {
|
|
829
|
-
|
|
830
|
-
} catch {
|
|
831
|
-
return report + "⚠️ No CI workflows found for this repository.\n\n✅ Push complete (no CI to watch)."
|
|
832
|
-
}
|
|
634
|
+
try { runs = JSON.parse(runsResult.output) } catch { return "✅ Pushed (no CI)" }
|
|
635
|
+
if (runs.length === 0) return "✅ Pushed (no CI)"
|
|
833
636
|
|
|
834
|
-
|
|
835
|
-
|
|
637
|
+
const run = runs[0]
|
|
638
|
+
if (run.status === "completed") {
|
|
639
|
+
return run.conclusion === "success" ? "✅ Pushed, CI passed" : `❌ CI failed: ${run.name}`
|
|
836
640
|
}
|
|
837
641
|
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
// 7. If already completed, just report status
|
|
842
|
-
if (latestRun.status === "completed") {
|
|
843
|
-
if (latestRun.conclusion === "success") {
|
|
844
|
-
return report + `✅ CI already passed! (${latestRun.name})\n\n🎉 All done!`
|
|
845
|
-
} else {
|
|
846
|
-
report += `❌ CI failed with conclusion: ${latestRun.conclusion}\n\n`
|
|
847
|
-
report += "Use `gh run view --log-failed` to see error details.\n"
|
|
848
|
-
if (args.createFixWorktree) {
|
|
849
|
-
report += "\n💡 **Suggestion:** Create a `fix-ci` worktree to fix the CI errors."
|
|
850
|
-
}
|
|
851
|
-
return report
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// 8. Watch the CI run in real-time
|
|
856
|
-
report += "## ⏳ Watching CI...\n"
|
|
857
|
-
report += `Running: \`gh run watch ${latestRun.databaseId}\`\n\n`
|
|
858
|
-
|
|
859
|
-
// Use gh run watch (this blocks until complete)
|
|
860
|
-
const watchResult = runCommand(
|
|
861
|
-
`gh run watch ${latestRun.databaseId} --exit-status`,
|
|
862
|
-
gitRoot
|
|
863
|
-
)
|
|
864
|
-
|
|
865
|
-
if (watchResult.success) {
|
|
866
|
-
return report + `✅ CI passed!\n\n\`\`\`\n${watchResult.output.slice(-500)}\n\`\`\`\n\n🎉 All done!`
|
|
867
|
-
} else {
|
|
868
|
-
report += `❌ CI failed!\n\n\`\`\`\n${(watchResult.error || watchResult.output).slice(-1000)}\n\`\`\`\n\n`
|
|
869
|
-
|
|
870
|
-
// Get failed logs
|
|
871
|
-
const logsResult = runCommand(`gh run view ${latestRun.databaseId} --log-failed`, gitRoot)
|
|
872
|
-
if (logsResult.success && logsResult.output) {
|
|
873
|
-
report += `### Failed logs:\n\`\`\`\n${logsResult.output.slice(-2000)}\n\`\`\`\n\n`
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
if (args.createFixWorktree) {
|
|
877
|
-
report += "💡 **Recommendation:** Create a `fix-ci` worktree to fix these errors."
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
return report
|
|
881
|
-
}
|
|
642
|
+
const watchResult = runCommand(`gh run watch ${run.databaseId} --exit-status`, gitRoot)
|
|
643
|
+
return watchResult.success ? "✅ Pushed, CI passed" : `❌ CI failed: ${run.name}`
|
|
882
644
|
} catch (e: any) {
|
|
883
645
|
logEvent("error", "mad_push_and_watch exception", { error: e.message })
|
|
884
|
-
return
|
|
646
|
+
return `❌ ${e.message}`
|
|
885
647
|
}
|
|
886
648
|
}
|
|
887
649
|
}),
|
|
@@ -891,25 +653,18 @@ If CI fails, returns error details for the orchestrator to spawn a fixer.`,
|
|
|
891
653
|
*/
|
|
892
654
|
mad_final_check: tool({
|
|
893
655
|
description: `Run global build/lint checks on the main project after all merges.
|
|
894
|
-
Compares errors against files modified during the MAD session
|
|
895
|
-
- Session errors: caused by changes made during this session
|
|
896
|
-
- Pre-existing errors: already present before the session started
|
|
897
|
-
|
|
898
|
-
Use this at the end of the MAD workflow to ensure code quality.`,
|
|
656
|
+
Compares errors against files modified during the MAD session.`,
|
|
899
657
|
args: {
|
|
900
|
-
baseCommit: tool.schema.string().optional().describe("The commit SHA from before the MAD session started.
|
|
658
|
+
baseCommit: tool.schema.string().optional().describe("The commit SHA from before the MAD session started."),
|
|
901
659
|
},
|
|
902
660
|
async execute(args, context) {
|
|
903
661
|
try {
|
|
904
662
|
const gitRoot = getGitRoot()
|
|
905
663
|
|
|
906
|
-
// 1. Determine base commit for comparison
|
|
907
664
|
let baseCommit = args.baseCommit
|
|
908
665
|
if (!baseCommit) {
|
|
909
|
-
// Try to find the commit before MAD session started (look for last commit before worktrees were created)
|
|
910
666
|
const reflogResult = runCommand('git reflog --format="%H %gs" -n 50', gitRoot)
|
|
911
667
|
if (reflogResult.success) {
|
|
912
|
-
// Find first commit that's not a merge from a MAD branch
|
|
913
668
|
const lines = reflogResult.output.split('\n')
|
|
914
669
|
for (const line of lines) {
|
|
915
670
|
if (!line.includes('merge') || (!line.includes('feat-') && !line.includes('fix-'))) {
|
|
@@ -918,233 +673,80 @@ Use this at the end of the MAD workflow to ensure code quality.`,
|
|
|
918
673
|
}
|
|
919
674
|
}
|
|
920
675
|
}
|
|
921
|
-
if (!baseCommit)
|
|
922
|
-
baseCommit = 'HEAD~10' // Fallback
|
|
923
|
-
}
|
|
676
|
+
if (!baseCommit) baseCommit = 'HEAD~10'
|
|
924
677
|
}
|
|
925
678
|
|
|
926
|
-
// 2. Get list of files modified during session
|
|
927
679
|
const diffResult = runCommand(`git diff ${baseCommit}..HEAD --name-only`, gitRoot)
|
|
928
680
|
const modifiedFiles = diffResult.success
|
|
929
681
|
? diffResult.output.split('\n').filter(f => f.trim()).map(f => f.trim())
|
|
930
682
|
: []
|
|
931
683
|
|
|
932
|
-
let report = getUpdateNotification() + `# Final Project Check\n\n`
|
|
933
|
-
report += `📊 **Session Summary:**\n`
|
|
934
|
-
report += `- Base commit: \`${baseCommit.substring(0, 8)}\`\n`
|
|
935
|
-
report += `- Files modified: ${modifiedFiles.length}\n\n`
|
|
936
|
-
|
|
937
|
-
// 3. Detect project type and run checks
|
|
938
684
|
const packageJson = join(gitRoot, "package.json")
|
|
939
685
|
const goMod = join(gitRoot, "go.mod")
|
|
940
686
|
const cargoToml = join(gitRoot, "Cargo.toml")
|
|
941
687
|
const pyProject = join(gitRoot, "pyproject.toml")
|
|
942
688
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
isSessionError: boolean
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
const allErrors: CheckError[] = []
|
|
951
|
-
let checksRun = 0
|
|
689
|
+
let passed: string[] = []
|
|
690
|
+
let failed: string[] = []
|
|
691
|
+
let sessionErrors = 0
|
|
692
|
+
let preExistingErrors = 0
|
|
952
693
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
)
|
|
973
|
-
|
|
974
|
-
allErrors.push({ file, line, message, isSessionError })
|
|
694
|
+
const runCheck = (name: string, cmd: string) => {
|
|
695
|
+
const result = runCommand(`${cmd} 2>&1`, gitRoot)
|
|
696
|
+
if (result.success) {
|
|
697
|
+
passed.push(name)
|
|
698
|
+
} else {
|
|
699
|
+
failed.push(name)
|
|
700
|
+
// Count errors in modified files vs pre-existing
|
|
701
|
+
const output = result.error || result.output
|
|
702
|
+
const lines = output.split('\n')
|
|
703
|
+
for (const line of lines) {
|
|
704
|
+
const match = line.match(/^(.+?):(\d+)/)
|
|
705
|
+
if (match) {
|
|
706
|
+
const file = match[1].replace(/\\/g, '/')
|
|
707
|
+
if (modifiedFiles.some(mf => file.includes(mf) || mf.includes(file))) {
|
|
708
|
+
sessionErrors++
|
|
709
|
+
} else {
|
|
710
|
+
preExistingErrors++
|
|
711
|
+
}
|
|
712
|
+
}
|
|
975
713
|
}
|
|
976
714
|
}
|
|
977
715
|
}
|
|
978
716
|
|
|
979
|
-
// Run checks based on project type
|
|
980
717
|
if (existsSync(packageJson)) {
|
|
981
718
|
const pkg = JSON.parse(readFileSync(packageJson, "utf-8"))
|
|
982
|
-
|
|
983
|
-
if (pkg.scripts?.
|
|
984
|
-
|
|
985
|
-
report += `## 🔍 Lint Check\n`
|
|
986
|
-
const lintResult = runCommand("npm run lint 2>&1", gitRoot)
|
|
987
|
-
if (lintResult.success) {
|
|
988
|
-
report += `✅ Lint passed\n\n`
|
|
989
|
-
} else {
|
|
990
|
-
report += `❌ Lint failed\n`
|
|
991
|
-
parseAndCategorize(lintResult.error || lintResult.output, "lint")
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
if (pkg.scripts?.build) {
|
|
996
|
-
checksRun++
|
|
997
|
-
report += `## 🔨 Build Check\n`
|
|
998
|
-
const buildResult = runCommand("npm run build 2>&1", gitRoot)
|
|
999
|
-
if (buildResult.success) {
|
|
1000
|
-
report += `✅ Build passed\n\n`
|
|
1001
|
-
} else {
|
|
1002
|
-
report += `❌ Build failed\n`
|
|
1003
|
-
parseAndCategorize(buildResult.error || buildResult.output, "build")
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
if (pkg.scripts?.typecheck || pkg.scripts?.["type-check"]) {
|
|
1008
|
-
checksRun++
|
|
1009
|
-
const cmd = pkg.scripts?.typecheck ? "npm run typecheck" : "npm run type-check"
|
|
1010
|
-
report += `## 📝 TypeCheck\n`
|
|
1011
|
-
const tcResult = runCommand(`${cmd} 2>&1`, gitRoot)
|
|
1012
|
-
if (tcResult.success) {
|
|
1013
|
-
report += `✅ TypeCheck passed\n\n`
|
|
1014
|
-
} else {
|
|
1015
|
-
report += `❌ TypeCheck failed\n`
|
|
1016
|
-
parseAndCategorize(tcResult.error || tcResult.output, "typecheck")
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
719
|
+
if (pkg.scripts?.lint) runCheck("lint", "npm run lint")
|
|
720
|
+
if (pkg.scripts?.build) runCheck("build", "npm run build")
|
|
721
|
+
if (pkg.scripts?.typecheck) runCheck("typecheck", "npm run typecheck")
|
|
1019
722
|
}
|
|
1020
|
-
|
|
1021
723
|
if (existsSync(goMod)) {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const goBuild = runCommand("go build ./... 2>&1", gitRoot)
|
|
1025
|
-
if (goBuild.success) {
|
|
1026
|
-
report += `✅ Go build passed\n\n`
|
|
1027
|
-
} else {
|
|
1028
|
-
parseAndCategorize(goBuild.error || goBuild.output, "go build")
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
checksRun++
|
|
1032
|
-
report += `## 🔍 Go Vet\n`
|
|
1033
|
-
const goVet = runCommand("go vet ./... 2>&1", gitRoot)
|
|
1034
|
-
if (goVet.success) {
|
|
1035
|
-
report += `✅ Go vet passed\n\n`
|
|
1036
|
-
} else {
|
|
1037
|
-
parseAndCategorize(goVet.error || goVet.output, "go vet")
|
|
1038
|
-
}
|
|
724
|
+
runCheck("go-build", "go build ./...")
|
|
725
|
+
runCheck("go-vet", "go vet ./...")
|
|
1039
726
|
}
|
|
1040
|
-
|
|
1041
727
|
if (existsSync(cargoToml)) {
|
|
1042
|
-
|
|
1043
|
-
report += `## 🔨 Cargo Check\n`
|
|
1044
|
-
const cargoCheck = runCommand("cargo check 2>&1", gitRoot)
|
|
1045
|
-
if (cargoCheck.success) {
|
|
1046
|
-
report += `✅ Cargo check passed\n\n`
|
|
1047
|
-
} else {
|
|
1048
|
-
parseAndCategorize(cargoCheck.error || cargoCheck.output, "cargo")
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
checksRun++
|
|
1052
|
-
report += `## 🔍 Cargo Clippy\n`
|
|
1053
|
-
const clippy = runCommand("cargo clippy 2>&1", gitRoot)
|
|
1054
|
-
if (clippy.success) {
|
|
1055
|
-
report += `✅ Clippy passed\n\n`
|
|
1056
|
-
} else {
|
|
1057
|
-
parseAndCategorize(clippy.error || clippy.output, "clippy")
|
|
1058
|
-
}
|
|
728
|
+
runCheck("cargo", "cargo check")
|
|
1059
729
|
}
|
|
1060
|
-
|
|
1061
730
|
if (existsSync(pyProject)) {
|
|
1062
|
-
|
|
1063
|
-
report += `## 🔍 Python Lint (ruff/flake8)\n`
|
|
1064
|
-
let pyLint = runCommand("ruff check . 2>&1", gitRoot)
|
|
1065
|
-
if (!pyLint.success && pyLint.error?.includes("not found")) {
|
|
1066
|
-
pyLint = runCommand("flake8 . 2>&1", gitRoot)
|
|
1067
|
-
}
|
|
1068
|
-
if (pyLint.success) {
|
|
1069
|
-
report += `✅ Python lint passed\n\n`
|
|
1070
|
-
} else {
|
|
1071
|
-
parseAndCategorize(pyLint.error || pyLint.output, "python lint")
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
checksRun++
|
|
1075
|
-
report += `## 📝 Python Type Check (mypy)\n`
|
|
1076
|
-
const mypy = runCommand("mypy . 2>&1", gitRoot)
|
|
1077
|
-
if (mypy.success) {
|
|
1078
|
-
report += `✅ Mypy passed\n\n`
|
|
1079
|
-
} else {
|
|
1080
|
-
parseAndCategorize(mypy.error || mypy.output, "mypy")
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
if (checksRun === 0) {
|
|
1085
|
-
report += `⚠️ No build/lint scripts detected in this project.\n`
|
|
1086
|
-
report += `Supported: package.json (npm), go.mod, Cargo.toml, pyproject.toml\n`
|
|
1087
|
-
logEvent("warn", "mad_final_check: no checks detected", { gitRoot })
|
|
1088
|
-
return report
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
// 4. Categorize and report errors
|
|
1092
|
-
const sessionErrors = allErrors.filter(e => e.isSessionError)
|
|
1093
|
-
const preExistingErrors = allErrors.filter(e => !e.isSessionError)
|
|
1094
|
-
|
|
1095
|
-
report += `---\n\n## 📋 Error Summary\n\n`
|
|
1096
|
-
|
|
1097
|
-
if (allErrors.length === 0) {
|
|
1098
|
-
report += `🎉 **All checks passed!** No errors detected.\n`
|
|
1099
|
-
logEvent("info", "mad_final_check: all checks passed", { checksRun })
|
|
1100
|
-
return report
|
|
731
|
+
runCheck("ruff", "ruff check .")
|
|
1101
732
|
}
|
|
1102
733
|
|
|
1103
|
-
if (
|
|
1104
|
-
|
|
1105
|
-
report += `*These errors are in files modified during this session:*\n\n`
|
|
1106
|
-
for (const err of sessionErrors.slice(0, 10)) {
|
|
1107
|
-
report += `- \`${err.file}${err.line ? `:${err.line}` : ''}\`: ${err.message.substring(0, 100)}\n`
|
|
1108
|
-
}
|
|
1109
|
-
if (sessionErrors.length > 10) {
|
|
1110
|
-
report += `- ... and ${sessionErrors.length - 10} more\n`
|
|
1111
|
-
}
|
|
1112
|
-
report += `\n`
|
|
734
|
+
if (passed.length === 0 && failed.length === 0) {
|
|
735
|
+
return "⚠️ No checks found"
|
|
1113
736
|
}
|
|
1114
737
|
|
|
1115
|
-
if (
|
|
1116
|
-
|
|
1117
|
-
report += `*These errors are NOT caused by this session - they existed before:*\n\n`
|
|
1118
|
-
for (const err of preExistingErrors.slice(0, 10)) {
|
|
1119
|
-
report += `- \`${err.file}${err.line ? `:${err.line}` : ''}\`: ${err.message.substring(0, 100)}\n`
|
|
1120
|
-
}
|
|
1121
|
-
if (preExistingErrors.length > 10) {
|
|
1122
|
-
report += `- ... and ${preExistingErrors.length - 10} more\n`
|
|
1123
|
-
}
|
|
1124
|
-
report += `\n`
|
|
1125
|
-
report += `💡 **These pre-existing errors are not your fault!**\n`
|
|
1126
|
-
report += `Would you like me to create a worktree to fix them? Just say "fix pre-existing errors".\n`
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// 5. Final verdict
|
|
1130
|
-
report += `\n---\n\n`
|
|
1131
|
-
if (sessionErrors.length > 0) {
|
|
1132
|
-
report += `⚠️ **Action required:** Fix the ${sessionErrors.length} session error(s) before considering this session complete.\n`
|
|
1133
|
-
} else if (preExistingErrors.length > 0) {
|
|
1134
|
-
report += `✅ **Session successful!** Your changes introduced no new errors.\n`
|
|
1135
|
-
report += `The ${preExistingErrors.length} pre-existing error(s) can be fixed separately if desired.\n`
|
|
738
|
+
if (failed.length === 0) {
|
|
739
|
+
return `✅ All passed: ${passed.join(", ")}`
|
|
1136
740
|
}
|
|
1137
741
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
return report
|
|
742
|
+
let result = `❌ Failed: ${failed.join(", ")}`
|
|
743
|
+
if (passed.length) result += ` | ✅ ${passed.join(", ")}`
|
|
744
|
+
if (sessionErrors) result += ` | Session errors: ${sessionErrors}`
|
|
745
|
+
if (preExistingErrors) result += ` | Pre-existing: ${preExistingErrors}`
|
|
746
|
+
return result
|
|
1145
747
|
} catch (e: any) {
|
|
1146
|
-
logEvent("error", "mad_final_check exception", { error: e.message
|
|
1147
|
-
return
|
|
748
|
+
logEvent("error", "mad_final_check exception", { error: e.message })
|
|
749
|
+
return `❌ ${e.message}`
|
|
1148
750
|
}
|
|
1149
751
|
},
|
|
1150
752
|
}),
|
|
@@ -1187,7 +789,7 @@ The plugin will then BLOCK any unauthorized actions.`,
|
|
|
1187
789
|
|
|
1188
790
|
logEvent("info", `Registered agent permissions`, { sessionID, agentType, canEdit, worktree })
|
|
1189
791
|
|
|
1190
|
-
return
|
|
792
|
+
return `✅ Registered: ${agentType}`
|
|
1191
793
|
}
|
|
1192
794
|
}),
|
|
1193
795
|
|
|
@@ -1203,39 +805,18 @@ Use mode 'full' for complete project scan, 'targeted' for task-specific analysis
|
|
|
1203
805
|
paths: tool.schema.array(tool.schema.string()).optional().describe("Specific paths to analyze"),
|
|
1204
806
|
},
|
|
1205
807
|
async execute(args, context) {
|
|
1206
|
-
const { mode, focus
|
|
808
|
+
const { mode, focus } = args
|
|
1207
809
|
const gitRoot = getGitRoot()
|
|
1208
810
|
|
|
1209
|
-
let report = `# Codebase Analysis Report\n\n`
|
|
1210
|
-
report += `**Mode:** ${mode}\n`
|
|
1211
|
-
report += `**Date:** ${new Date().toISOString()}\n\n`
|
|
1212
|
-
|
|
1213
|
-
// Collecter les informations de base
|
|
1214
811
|
const structure = runCommand('find . -type f -name "*.ts" -o -name "*.js" -o -name "*.json" | grep -v node_modules | head -50', gitRoot)
|
|
1215
812
|
const packageJsonPath = join(gitRoot, 'package.json')
|
|
1216
|
-
const
|
|
1217
|
-
? JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
|
1218
|
-
: null
|
|
1219
|
-
|
|
1220
|
-
report += `## Project Structure\n\`\`\`\n${structure.output}\n\`\`\`\n\n`
|
|
1221
|
-
|
|
1222
|
-
if (packageJson) {
|
|
1223
|
-
report += `## Dependencies\n`
|
|
1224
|
-
report += `- **Name:** ${packageJson.name}\n`
|
|
1225
|
-
report += `- **Version:** ${packageJson.version}\n`
|
|
1226
|
-
report += `- **Dependencies:** ${Object.keys(packageJson.dependencies || {}).length}\n`
|
|
1227
|
-
report += `- **DevDependencies:** ${Object.keys(packageJson.devDependencies || {}).length}\n\n`
|
|
1228
|
-
}
|
|
813
|
+
const pkg = existsSync(packageJsonPath) ? JSON.parse(readFileSync(packageJsonPath, 'utf-8')) : null
|
|
1229
814
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
// Chercher les fichiers pertinents
|
|
1233
|
-
const relevantFiles = runCommand(`grep -rl "${focus}" --include="*.ts" --include="*.js" . | grep -v node_modules | head -20`, gitRoot)
|
|
1234
|
-
report += `### Relevant Files\n\`\`\`\n${relevantFiles.output || 'No files found'}\n\`\`\`\n`
|
|
1235
|
-
}
|
|
815
|
+
let info = `Files: ${structure.output.split('\n').length}`
|
|
816
|
+
if (pkg) info += ` | Deps: ${Object.keys(pkg.dependencies || {}).length}`
|
|
1236
817
|
|
|
1237
818
|
logEvent("info", "Codebase analysis completed", { mode, focus })
|
|
1238
|
-
return
|
|
819
|
+
return `✅ Analyzed: ${info}`
|
|
1239
820
|
}
|
|
1240
821
|
}),
|
|
1241
822
|
|
|
@@ -1250,13 +831,8 @@ Use mode 'full' for complete project scan, 'targeted' for task-specific analysis
|
|
|
1250
831
|
async execute(args) {
|
|
1251
832
|
const existed = agentPermissions.has(args.sessionID)
|
|
1252
833
|
agentPermissions.delete(args.sessionID)
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
logEvent("info", `Unregistered agent`, { sessionID: args.sessionID })
|
|
1256
|
-
return `✅ Agent unregistered: ${args.sessionID}`
|
|
1257
|
-
} else {
|
|
1258
|
-
return `⚠️ Agent was not registered: ${args.sessionID}`
|
|
1259
|
-
}
|
|
834
|
+
logEvent("info", `Unregistered agent`, { sessionID: args.sessionID })
|
|
835
|
+
return existed ? `✅ Unregistered` : `⚠️ Not found`
|
|
1260
836
|
}
|
|
1261
837
|
}),
|
|
1262
838
|
|
|
@@ -1291,7 +867,7 @@ The plan will be available for the orchestrator to present to the user.`,
|
|
|
1291
867
|
// Log pour debugging
|
|
1292
868
|
logEvent("info", "Development plan created", { planName, taskCount: tasks.length })
|
|
1293
869
|
|
|
1294
|
-
return
|
|
870
|
+
return `✅ Plan: ${planName} (${tasks.length} tasks)`
|
|
1295
871
|
}
|
|
1296
872
|
}),
|
|
1297
873
|
|
|
@@ -1320,54 +896,18 @@ Called by the Reviewer agent after analyzing the code.`,
|
|
|
1320
896
|
const worktreePath = join(gitRoot, "worktrees", worktree)
|
|
1321
897
|
|
|
1322
898
|
if (!existsSync(worktreePath)) {
|
|
1323
|
-
return
|
|
899
|
+
return `❌ Not found: ${worktree}`
|
|
1324
900
|
}
|
|
1325
901
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
report += `**Verdict:** ${verdict === 'approved' ? '✅ APPROVED' : verdict === 'changes_requested' ? '⚠️ CHANGES REQUESTED' : '❌ REJECTED'}\n\n`
|
|
1329
|
-
report += `## Summary\n${summary}\n\n`
|
|
902
|
+
const icon = verdict === 'approved' ? '✅' : verdict === 'changes_requested' ? '⚠️' : '❌'
|
|
903
|
+
const issueCount = issues?.length || 0
|
|
1330
904
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
positives.forEach(p => report += `- ${p}\n`)
|
|
1334
|
-
report += '\n'
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
if (issues && issues.length > 0) {
|
|
1338
|
-
report += `## Issues Found\n`
|
|
1339
|
-
const critical = issues.filter(i => i.severity === 'critical')
|
|
1340
|
-
const major = issues.filter(i => i.severity === 'major')
|
|
1341
|
-
const minor = issues.filter(i => i.severity === 'minor')
|
|
1342
|
-
|
|
1343
|
-
if (critical.length > 0) {
|
|
1344
|
-
report += `### 🚨 Critical (${critical.length})\n`
|
|
1345
|
-
critical.forEach(i => {
|
|
1346
|
-
report += `- **${i.file}${i.line ? `:${i.line}` : ''}** - ${i.message}\n`
|
|
1347
|
-
if (i.suggestion) report += ` → Suggestion: ${i.suggestion}\n`
|
|
1348
|
-
})
|
|
1349
|
-
}
|
|
1350
|
-
if (major.length > 0) {
|
|
1351
|
-
report += `### ⚠️ Major (${major.length})\n`
|
|
1352
|
-
major.forEach(i => {
|
|
1353
|
-
report += `- **${i.file}${i.line ? `:${i.line}` : ''}** - ${i.message}\n`
|
|
1354
|
-
if (i.suggestion) report += ` → Suggestion: ${i.suggestion}\n`
|
|
1355
|
-
})
|
|
1356
|
-
}
|
|
1357
|
-
if (minor.length > 0) {
|
|
1358
|
-
report += `### 💡 Minor (${minor.length})\n`
|
|
1359
|
-
minor.forEach(i => {
|
|
1360
|
-
report += `- **${i.file}${i.line ? `:${i.line}` : ''}** - ${i.message}\n`
|
|
1361
|
-
})
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
905
|
+
// Save report
|
|
906
|
+
writeFileSync(join(worktreePath, '.agent-review'), `${verdict}: ${summary}`)
|
|
1364
907
|
|
|
1365
|
-
|
|
1366
|
-
writeFileSync(join(worktreePath, '.agent-review'), report)
|
|
908
|
+
logEvent("info", "Code review submitted", { worktree, verdict, issueCount })
|
|
1367
909
|
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
return getUpdateNotification() + report
|
|
910
|
+
return `${icon} Review: ${verdict} (${issueCount} issues)`
|
|
1371
911
|
}
|
|
1372
912
|
}),
|
|
1373
913
|
|
|
@@ -1401,51 +941,22 @@ Called by the Security agent after scanning for vulnerabilities.`,
|
|
|
1401
941
|
const { target, riskLevel, summary, vulnerabilities, dependencyIssues } = args
|
|
1402
942
|
const gitRoot = getGitRoot()
|
|
1403
943
|
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
report += `**Date:** ${new Date().toISOString()}\n\n`
|
|
1407
|
-
report += `## Summary\n${summary}\n\n`
|
|
1408
|
-
|
|
1409
|
-
if (vulnerabilities && vulnerabilities.length > 0) {
|
|
1410
|
-
report += `## Vulnerabilities (${vulnerabilities.length})\n\n`
|
|
1411
|
-
vulnerabilities.forEach(v => {
|
|
1412
|
-
const icon = v.severity === 'critical' ? '🚨' : v.severity === 'high' ? '🔴' : v.severity === 'medium' ? '🟡' : '🟢'
|
|
1413
|
-
report += `### ${icon} [${v.id}] ${v.type}\n`
|
|
1414
|
-
report += `**Severity:** ${v.severity.toUpperCase()}\n`
|
|
1415
|
-
if (v.file) report += `**Location:** ${v.file}${v.line ? `:${v.line}` : ''}\n`
|
|
1416
|
-
report += `**Description:** ${v.description}\n`
|
|
1417
|
-
report += `**Remediation:** ${v.remediation}\n\n`
|
|
1418
|
-
})
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
if (dependencyIssues && dependencyIssues.length > 0) {
|
|
1422
|
-
report += `## Vulnerable Dependencies (${dependencyIssues.length})\n\n`
|
|
1423
|
-
report += `| Package | Severity | CVE | Fix |\n`
|
|
1424
|
-
report += `|---------|----------|-----|-----|\n`
|
|
1425
|
-
dependencyIssues.forEach(d => {
|
|
1426
|
-
report += `| ${d.package} | ${d.severity} | ${d.cve || 'N/A'} | ${d.fix} |\n`
|
|
1427
|
-
})
|
|
1428
|
-
report += '\n'
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
// Verdict
|
|
944
|
+
const vulnCount = vulnerabilities?.length || 0
|
|
945
|
+
const depCount = dependencyIssues?.length || 0
|
|
1432
946
|
const canMerge = riskLevel === 'low' || (riskLevel === 'medium' && (!vulnerabilities || vulnerabilities.filter(v => v.severity === 'critical' || v.severity === 'high').length === 0))
|
|
1433
|
-
report += `## Verdict\n`
|
|
1434
|
-
report += canMerge
|
|
1435
|
-
? `✅ **PASS** - No critical security issues blocking merge.\n`
|
|
1436
|
-
: `❌ **FAIL** - Critical security issues must be resolved before merge.\n`
|
|
1437
947
|
|
|
1438
|
-
//
|
|
948
|
+
// Save if worktree
|
|
1439
949
|
if (target !== 'main') {
|
|
1440
950
|
const worktreePath = join(gitRoot, "worktrees", target)
|
|
1441
951
|
if (existsSync(worktreePath)) {
|
|
1442
|
-
writeFileSync(join(worktreePath, '.agent-security'),
|
|
952
|
+
writeFileSync(join(worktreePath, '.agent-security'), `${riskLevel}: ${summary}`)
|
|
1443
953
|
}
|
|
1444
954
|
}
|
|
1445
955
|
|
|
1446
|
-
logEvent("info", "Security scan completed", { target, riskLevel, vulnCount
|
|
956
|
+
logEvent("info", "Security scan completed", { target, riskLevel, vulnCount })
|
|
1447
957
|
|
|
1448
|
-
|
|
958
|
+
const icon = canMerge ? '✅' : '❌'
|
|
959
|
+
return `${icon} Security: ${riskLevel} (${vulnCount} vulns, ${depCount} deps)`
|
|
1449
960
|
}
|
|
1450
961
|
}),
|
|
1451
962
|
},
|
|
@@ -1479,24 +990,22 @@ Called by the Security agent after scanning for vulnerabilities.`,
|
|
|
1479
990
|
if (['edit', 'write', 'patch', 'multiedit'].includes(toolName)) {
|
|
1480
991
|
if (!perms.canEdit) {
|
|
1481
992
|
logEvent("warn", `BLOCKED: ${perms.type} tried to use ${toolName}`, { sessionID: input.sessionID })
|
|
1482
|
-
throw new Error(`🚫
|
|
993
|
+
throw new Error(`🚫 ${perms.type} is read-only`)
|
|
1483
994
|
}
|
|
1484
995
|
|
|
1485
996
|
// 2. Check path if allowedPaths is defined
|
|
1486
997
|
const targetPath = args.filePath || args.file_path || args.path
|
|
1487
998
|
if (targetPath) {
|
|
1488
|
-
// Check denied paths
|
|
1489
999
|
if (perms.deniedPaths.some((p: string) => targetPath.includes(p) || matchGlob(targetPath, p))) {
|
|
1490
1000
|
logEvent("warn", `BLOCKED: ${perms.type} tried to edit denied path`, { sessionID: input.sessionID, path: targetPath })
|
|
1491
|
-
throw new Error(`🚫
|
|
1001
|
+
throw new Error(`🚫 Path denied: ${basename(targetPath)}`)
|
|
1492
1002
|
}
|
|
1493
1003
|
|
|
1494
|
-
// Check allowed paths (if defined)
|
|
1495
1004
|
if (perms.allowedPaths && perms.allowedPaths.length > 0) {
|
|
1496
1005
|
const isAllowed = perms.allowedPaths.some((p: string) => targetPath.includes(p) || matchGlob(targetPath, p))
|
|
1497
1006
|
if (!isAllowed) {
|
|
1498
|
-
logEvent("warn", `BLOCKED: ${perms.type} tried to edit outside allowed paths`, { sessionID: input.sessionID, path: targetPath
|
|
1499
|
-
throw new Error(`🚫
|
|
1007
|
+
logEvent("warn", `BLOCKED: ${perms.type} tried to edit outside allowed paths`, { sessionID: input.sessionID, path: targetPath })
|
|
1008
|
+
throw new Error(`🚫 Outside allowed paths: ${basename(targetPath)}`)
|
|
1500
1009
|
}
|
|
1501
1010
|
}
|
|
1502
1011
|
}
|
|
@@ -1506,22 +1015,22 @@ Called by the Security agent after scanning for vulnerabilities.`,
|
|
|
1506
1015
|
if (toolName === 'bash' && perms && !perms.canEdit) {
|
|
1507
1016
|
const cmd = args.command || ''
|
|
1508
1017
|
const dangerousPatterns = [
|
|
1509
|
-
/\becho\s+.*>/,
|
|
1510
|
-
/\bcat\s+.*>/,
|
|
1511
|
-
/\brm\s+/,
|
|
1512
|
-
/\bmv\s+/,
|
|
1513
|
-
/\bcp\s+/,
|
|
1514
|
-
/\bmkdir\s+/,
|
|
1515
|
-
/\btouch\s+/,
|
|
1516
|
-
/\bnpm\s+install/,
|
|
1517
|
-
/\bgit\s+commit/,
|
|
1518
|
-
/\bgit\s+push/,
|
|
1018
|
+
/\becho\s+.*>/,
|
|
1019
|
+
/\bcat\s+.*>/,
|
|
1020
|
+
/\brm\s+/,
|
|
1021
|
+
/\bmv\s+/,
|
|
1022
|
+
/\bcp\s+/,
|
|
1023
|
+
/\bmkdir\s+/,
|
|
1024
|
+
/\btouch\s+/,
|
|
1025
|
+
/\bnpm\s+install/,
|
|
1026
|
+
/\bgit\s+commit/,
|
|
1027
|
+
/\bgit\s+push/,
|
|
1519
1028
|
]
|
|
1520
1029
|
|
|
1521
1030
|
for (const pattern of dangerousPatterns) {
|
|
1522
1031
|
if (pattern.test(cmd)) {
|
|
1523
|
-
logEvent("warn", `BLOCKED: ${perms.type} tried dangerous bash command`, { sessionID: input.sessionID
|
|
1524
|
-
throw new Error(`🚫
|
|
1032
|
+
logEvent("warn", `BLOCKED: ${perms.type} tried dangerous bash command`, { sessionID: input.sessionID })
|
|
1033
|
+
throw new Error(`🚫 ${perms.type} is read-only`)
|
|
1525
1034
|
}
|
|
1526
1035
|
}
|
|
1527
1036
|
}
|