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.
@@ -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 getUpdateNotification() + "❌ Error: Branch name cannot be empty"
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 getUpdateNotification() + "❌ Error: Task description cannot be empty"
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 getUpdateNotification() + `⚠️ Worktree already exists at ${worktreePath}\nUse a different branch name or clean up with mad_cleanup.`
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 getUpdateNotification() + `❌ Error creating worktree directory: ${e.message}`
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 getUpdateNotification() + `❌ Error creating git worktree: ${worktreeResult.error}`
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 getUpdateNotification() + `✅ Worktree created successfully!
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 getUpdateNotification() + `❌ Unexpected error creating worktree: ${e.message}`
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 getUpdateNotification() + "No active MAD worktrees. Use mad_worktree_create to create one."
263
+ return "No worktrees"
296
264
  }
297
265
 
298
266
  const entries = readdirSync(worktreeDir)
299
267
  if (entries.length === 0) {
300
- return getUpdateNotification() + "No active MAD worktrees. Use mad_worktree_create to create one."
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 statusIcon = "⏳"
317
- let statusText = "IN PROGRESS"
318
- let detail = ""
319
-
320
- if (existsSync(doneFile)) {
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
- // Get commit count
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
- status += `---\n`
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 getUpdateNotification() + `Worktree not found: ${worktreePath}`
311
+ return `❌ Not found: ${args.worktree}`
385
312
  }
386
313
 
387
- let results = getUpdateNotification() + `# Test Results for ${args.worktree}\n\n`
388
- let hasError = false
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
- results += `✅ Passed\n\`\`\`\n${result.output.slice(0, 500)}\n\`\`\`\n\n`
321
+ passed.push(label)
397
322
  } else {
398
- hasError = true
399
- const output = result.error || "Unknown error"
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("Lint", "npm run lint")
415
- if (pkg.scripts?.build) doCheck("Build", "npm run build")
416
- if (pkg.scripts?.test) doCheck("Test", "npm test")
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("Go Build", "go build ./...")
421
- doCheck("Go Test", "go test ./...")
341
+ doCheck("go-build", "go build ./...")
342
+ doCheck("go-test", "go test ./...")
422
343
  }
423
-
424
344
  if (existsSync(cargoToml)) {
425
- doCheck("Cargo Check", "cargo check")
426
- doCheck("Cargo Test", "cargo test")
345
+ doCheck("cargo-check", "cargo check")
346
+ doCheck("cargo-test", "cargo test")
427
347
  }
428
-
429
348
  if (existsSync(pyProject) || existsSync(requirements)) {
430
- doCheck("Pytest", "pytest")
349
+ doCheck("pytest", "pytest")
431
350
  }
432
351
 
433
- // Write error file if tests failed
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
- unlinkSync(doneFile)
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 getUpdateNotification() + `Worktree not found: ${worktreePath}`
379
+ return `❌ Not found: ${args.worktree}`
468
380
  }
469
381
 
470
382
  if (!existsSync(doneFile)) {
471
- return getUpdateNotification() + `Cannot merge: worktree ${args.worktree} is not marked as done. Complete the task first.`
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 getUpdateNotification() + `✅ Successfully merged ${branch}!\n\n${result.output}`
388
+ return `✅ Merged: ${branch}`
477
389
  } else {
478
- const output = result.error || "Unknown error"
390
+ const output = result.error || ""
479
391
  if (output.includes("CONFLICT")) {
480
- return getUpdateNotification() + `⚠️ Merge conflict detected!\n\n${output}\n\nResolve conflicts manually or use the fixer agent.`
392
+ return `⚠️ Conflict in ${branch}`
481
393
  }
482
- return getUpdateNotification() + `❌ Merge failed:\n${output}`
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 getUpdateNotification() + `Worktree not found: ${worktreePath}`
415
+ return `❌ Not found: ${args.worktree}`
504
416
  }
505
417
 
506
418
  if (!args.force && !existsSync(doneFile)) {
507
- return getUpdateNotification() + `Worktree ${args.worktree} is not marked as done. Use force=true to cleanup anyway.`
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 getUpdateNotification() + `✅ Cleaned up worktree: ${args.worktree}`
425
+ return `✅ Cleaned: ${args.worktree}`
514
426
  } catch (e: any) {
515
- return getUpdateNotification() + `❌ Cleanup failed: ${e.message}`
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 getUpdateNotification() + `Worktree not found: ${worktreePath}`
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 getUpdateNotification() + `✅ Marked ${args.worktree} as done: ${args.summary}`
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 getUpdateNotification() + `Worktree not found: ${worktreePath}`
472
+ return `❌ Not found: ${args.worktree}`
562
473
  }
563
474
 
564
475
  await $`echo ${args.reason} > ${join(worktreePath, ".agent-blocked")}`
565
476
 
566
- return getUpdateNotification() + `🚫 Marked ${args.worktree} as blocked: ${args.reason}`
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 getUpdateNotification() + `Task file not found: ${taskFile}`
495
+ return `❌ No task: ${args.worktree}`
585
496
  }
586
497
 
587
- return getUpdateNotification() + readFileSync(taskFile, "utf-8")
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 getUpdateNotification() + `📝 Logged [${args.level.toUpperCase()}]: ${args.message}`
516
+ return `✅ Logged`
606
517
  } catch (e: any) {
607
- return getUpdateNotification() + `⚠️ Failed to write log: ${e.message}`
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 an ASCII art visualization of the MAD orchestration status.
617
- Shows progress, worktree statuses, timeline, and statistics in a beautiful dashboard.`,
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 getUpdateNotification() + "No active MAD worktrees. Use mad_worktree_create to create one."
536
+ return "No worktrees"
626
537
  }
627
538
 
628
539
  const entries = readdirSync(worktreeDir)
629
540
  if (entries.length === 0) {
630
- return getUpdateNotification() + "No active MAD worktrees. Use mad_worktree_create to create one."
541
+ return "No worktrees"
631
542
  }
632
543
 
633
544
  let total = 0, done = 0, blocked = 0, errors = 0, wip = 0
634
- const worktrees: any[] = []
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 detail = ""
649
-
650
- if (existsSync(doneFile)) {
651
- icon = ""
652
- status = "DONE"
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
- const task = existsSync(taskFile)
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
- const progressBar = "█".repeat(Math.floor(progress / 5)) + "".repeat(20 - Math.floor(progress / 5))
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 getUpdateNotification() + `❌ Error generating visualization: ${e.message}`
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 getUpdateNotification() + `🔄 Update available!
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 getUpdateNotification() + `❌ Failed to check for updates: ${e.message}`
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 report + "⚠️ No remote 'origin' configured. Skipping push."
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 report + `❌ Push failed:\n\`\`\`\n${pushResult.error}\n\`\`\`\n\nFix the issue and try again.`
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 report + "⚠️ GitHub CLI (gh) not installed. Cannot watch CI.\n\nInstall with: https://cli.github.com/"
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,event`,
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
- runs = JSON.parse(runsResult.output)
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
- if (runs.length === 0) {
835
- return report + "ℹ️ No CI workflows found for this branch.\n\n✅ Push complete!"
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 latestRun = runs[0]
839
- report += `Found workflow: **${latestRun.name}** (${latestRun.event})\n\n`
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 getUpdateNotification() + `❌ Error: ${e.message}`
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 to distinguish:
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. If not provided, will try to detect from reflog."),
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
- interface CheckError {
944
- file: string
945
- line?: number
946
- message: string
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
- // Helper to parse errors and categorize them
954
- const parseAndCategorize = (output: string, checkName: string) => {
955
- // Common patterns for file:line:message
956
- const patterns = [
957
- /^(.+?):(\d+):\d*:?\s*(.+)$/gm, // file:line:col: message
958
- /^(.+?)\((\d+),\d+\):\s*(.+)$/gm, // file(line,col): message (TypeScript)
959
- /^\s*(.+?):(\d+)\s+(.+)$/gm, // file:line message
960
- ]
961
-
962
- for (const pattern of patterns) {
963
- let match
964
- while ((match = pattern.exec(output)) !== null) {
965
- const file = match[1].trim().replace(/\\/g, '/')
966
- const line = parseInt(match[2])
967
- const message = match[3].trim()
968
-
969
- // Check if this file was modified during session
970
- const isSessionError = modifiedFiles.some(mf =>
971
- file.endsWith(mf) || mf.endsWith(file) || file.includes(mf) || mf.includes(file)
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?.lint) {
984
- checksRun++
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
- checksRun++
1023
- report += `## 🔨 Go Build\n`
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
- checksRun++
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
- checksRun++
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 (sessionErrors.length > 0) {
1104
- report += `### Session Errors (${sessionErrors.length})\n`
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 (preExistingErrors.length > 0) {
1116
- report += `### ⚠️ Pre-existing Errors (${preExistingErrors.length})\n`
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
- logEvent("info", "mad_final_check completed", {
1139
- checksRun,
1140
- sessionErrors: sessionErrors.length,
1141
- preExistingErrors: preExistingErrors.length
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, stack: e.stack })
1147
- return getUpdateNotification() + `❌ Error running final check: ${e.message}`
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 getUpdateNotification() + `✅ Agent registered: ${agentType} (canEdit: ${canEdit}, worktree: ${worktree || 'none'})`
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, paths } = args
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 packageJson = existsSync(packageJsonPath)
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
- if (mode === 'targeted' && focus) {
1231
- report += `## Targeted Analysis: ${focus}\n`
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 getUpdateNotification() + report
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
- if (existed) {
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 getUpdateNotification() + `✅ Plan '${planName}' created with ${tasks.length} tasks.\n\n${plan}`
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 getUpdateNotification() + `❌ Worktree not found: ${worktreePath}`
899
+ return `❌ Not found: ${worktree}`
1324
900
  }
1325
901
 
1326
- // Créer le rapport de review
1327
- let report = `# Code Review: ${worktree}\n\n`
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
- if (positives && positives.length > 0) {
1332
- report += `## Positives 👍\n`
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
- // Sauvegarder le rapport dans le worktree
1366
- writeFileSync(join(worktreePath, '.agent-review'), report)
908
+ logEvent("info", "Code review submitted", { worktree, verdict, issueCount })
1367
909
 
1368
- logEvent("info", "Code review submitted", { worktree, verdict, issueCount: issues?.length || 0 })
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
- let report = `# Security Scan Report: ${target}\n\n`
1405
- report += `**Risk Level:** ${riskLevel === 'critical' ? '🚨 CRITICAL' : riskLevel === 'high' ? '🔴 HIGH' : riskLevel === 'medium' ? '🟡 MEDIUM' : '🟢 LOW'}\n`
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
- // Sauvegarder si c'est un worktree
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'), report)
952
+ writeFileSync(join(worktreePath, '.agent-security'), `${riskLevel}: ${summary}`)
1443
953
  }
1444
954
  }
1445
955
 
1446
- logEvent("info", "Security scan completed", { target, riskLevel, vulnCount: vulnerabilities?.length || 0 })
956
+ logEvent("info", "Security scan completed", { target, riskLevel, vulnCount })
1447
957
 
1448
- return getUpdateNotification() + report
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(`🚫 BLOCKED: Agent type '${perms.type}' cannot use '${toolName}' tool. This agent is READ-ONLY.`)
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(`🚫 BLOCKED: Cannot edit '${targetPath}' - this path is explicitly denied for this agent.`)
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, allowedPaths: perms.allowedPaths })
1499
- throw new Error(`🚫 BLOCKED: Cannot edit '${targetPath}' - outside allowed paths: ${perms.allowedPaths.join(', ')}`)
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+.*>/, // echo > file
1510
- /\bcat\s+.*>/, // cat > file
1511
- /\brm\s+/, // rm
1512
- /\bmv\s+/, // mv
1513
- /\bcp\s+/, // cp (can create files)
1514
- /\bmkdir\s+/, // mkdir
1515
- /\btouch\s+/, // touch
1516
- /\bnpm\s+install/, // npm install (modifies node_modules)
1517
- /\bgit\s+commit/, // git commit
1518
- /\bgit\s+push/, // git 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, command: cmd })
1524
- throw new Error(`🚫 BLOCKED: Agent type '${perms.type}' cannot run '${cmd}' - this command modifies files and this agent is READ-ONLY.`)
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
  }